I'm trying to encode stack discipline using types, my previous version just uses runtime checks, and panics if slot usage is invalid. by far, I managed to utilize a scoped API with callbacks:
let stack_base = thread.base();
stack_base.push(4, |frame| {
// bind variables to individual slots, slots borrows frame
let [a, b, c, d] = frame.slots() else { unreachable!() };
if some_condition(a, b) {
frame.push(1, |frame| {
// technically we don't need to shadow the outer frame
// but we already bound individual slots, we don't need the outer frame
let e = frame.top_slot();
do_some_work(c, d, e);
});
}
for i in 0..100 {
frame.push(2, |frame| {
let &[k, v] = frame.slots() else { unreachable!() };
do_some_more_work(i, k, v, a, b, c);
});
}
});
of course there's plenty room for improvements, for example I can use const generics to eliminate more runtime checks, but over all, I think the stack based VM runtime fits nicely with a scoped API.
I just want to explore some other possibilities: can we somehow make use of lifetimes to design an alternative API style? here's what I got so far:
trait StackDiscipline {
// ...
}
struct Empty;
impl StackDiscipline for Empty {}
struct Frame<'a, Below> {
below: &'a Below,
// ...
}
impl<'a, Below: StackDiscipline> StackDiscipline for Frame<'a, Below> {}
//-----------------------------------------------------------------------------
let stack_base = thread.base(); // stack_base: `Empty`
let frame = stack_base.push(4); // frame: `Frame<'1, Empty>`
let [a, b, c, d] = frame.slots() else { unreachable!() };
for i in 0..100 {
// the inner frame borrows the outer frame
let frame = frame.push(2); // frame: `Frame<'2, Frame<'1, Empty>>`
let &[k, v] = frame.slots() else { unreachable!() };
do_some_more_work(i, k, v, a, b, c);
}
I think (I might be wrong) the two styles of API should be equivalent and can be transformed mechanically. what the current API cannot guarantee (at compile time) however, is to prevent the user from accidentally "forking" the stack.
because user code needs to access to slots both from the inner most frame as well as from outer frames, so an inner frame can only hold shared reference to the outer frame. if, on the other hand, the inner frame borrows the outer frame exclusively, user code will not be able to access the outer frame slots anymore.
so my question is, without runtime checks, is possible to enforce a linear sequence of stack frames without invalidating slots of outer frames under the current lifetime rules?