Refactor: Implement late stack size calculation

This commit introduces a new mechanism for calculating the required
stack size for closures at bind time, rather than storing it in the AST.
This is achieved by adding a `calculate_stack_size` method to
`BoundNode`.

Key changes include:
- `BoundNode::calculate_stack_size`: Recursively traverses the bound AST
  to find the maximum `LocalSlot` index used.
- Recursion blocker for lambdas: Ensures that nested lambdas are not
  visited during stack size calculation for the outer closure,
  preventing incorrect stack size estimations.
- VM update: The `VM::run` method now accepts and uses the
  pre-calculated stack size for setting up call frames.
- `Closure` struct update: Stores the `stack_size` directly within the
  `Closure` struct.
- Binder and TypeChecker updates: Minor adjustments to accommodate the
  new strategy and error reporting.
- Optimizer enhancements: Constant and pure-lambda propagation for
  `Define` statements within blocks to ensure inlinability.

This refactoring addresses issues related to accurate stack size
management and improves the overall robustness of the binder and VM.
This commit is contained in:
Michael Schimmel
2026-03-10 20:05:32 +01:00
parent cb4d3525c2
commit 8c865681ff
8 changed files with 1063 additions and 896 deletions
+78 -46
View File
@@ -16,6 +16,8 @@ pub struct Closure {
pub exec_node: Rc<ExecNode>,
pub upvalues: Vec<Rc<RefCell<Value>>>,
pub positional_count: Option<u32>,
/// The number of stack slots required for this closure (calculated late).
pub stack_size: u32,
}
impl Closure {
@@ -26,6 +28,7 @@ impl Closure {
exec: Rc<ExecNode>,
upvalues: Vec<Rc<RefCell<Value>>>,
positional_count: Option<u32>,
stack_size: u32,
) -> Self {
Self {
parameter_node: params,
@@ -33,6 +36,7 @@ impl Closure {
exec_node: exec,
upvalues,
positional_count,
stack_size,
}
}
}
@@ -137,16 +141,8 @@ impl VM {
}
}
pub fn run(&mut self, root: &ExecNode) -> Result<Value, String> {
self.stack.clear();
self.frames.clear();
self.frames.push(CallFrame {
stack_base: 0,
closure: None,
});
let result = self.eval(root);
self.frames.pop();
self.resolve_tail_calls(&mut NoOpObserver, result)
pub fn run(&mut self, root: &ExecNode, stack_size: u32) -> Result<Value, String> {
self.run_with_observer(&mut NoOpObserver, root, stack_size)
}
pub fn resolve_tail_calls<O: VMObserver>(
@@ -215,23 +211,7 @@ impl VM {
closure_obj: Rc<dyn Object>,
args: &[Value],
) -> Result<Value, String> {
let closure = closure_obj.as_any().downcast_ref::<Closure>().unwrap();
self.stack.clear();
self.frames.clear();
self.frames.push(CallFrame {
stack_base: 0,
closure: Some(closure_obj.clone()),
});
if let Some(count) = closure.positional_count
&& args.len() == count as usize
{
self.stack.extend_from_slice(args);
} else {
self.unpack(&closure.parameter_node, args, &mut 0)?;
}
let result = self.eval(&closure.exec_node);
self.frames.pop();
self.resolve_tail_calls(&mut NoOpObserver, result)
self.run_with_args_observed(&mut NoOpObserver, closure_obj, args)
}
pub fn run_with_args_observed<O: VMObserver>(
@@ -240,13 +220,14 @@ impl VM {
closure_obj: Rc<dyn Object>,
args: &[Value],
) -> Result<Value, String> {
let closure = closure_obj.as_any().downcast_ref::<Closure>().unwrap();
let closure = closure_obj
.as_any()
.downcast_ref::<Closure>()
.ok_or_else(|| "Object is not a closure".to_string())?;
self.stack.clear();
self.frames.clear();
self.frames.push(CallFrame {
stack_base: 0,
closure: Some(closure_obj.clone()),
});
if let Some(count) = closure.positional_count
&& args.len() == count as usize
{
@@ -254,7 +235,21 @@ impl VM {
} else {
self.unpack(&closure.parameter_node, args, &mut 0)?;
}
let result = self.eval_observed(observer, &closure.exec_node);
// Fill remaining stack slots with Void
let current_stack = self.stack.len() as u32;
if closure.stack_size > current_stack {
for _ in 0..(closure.stack_size - current_stack) {
self.stack.push(Value::Void);
}
}
self.frames.push(CallFrame {
stack_base: 0,
closure: Some(closure_obj.clone()),
});
let result = self.eval_internal(observer, &closure.exec_node);
self.frames.pop();
self.resolve_tail_calls(observer, result)
}
@@ -263,23 +258,24 @@ impl VM {
&mut self,
observer: &mut O,
root: &ExecNode,
stack_size: u32,
) -> Result<Value, String> {
self.stack.clear();
self.frames.clear();
for _ in 0..stack_size {
self.stack.push(Value::Void);
}
self.frames.push(CallFrame {
stack_base: 0,
closure: None,
});
let result = self.eval_observed(observer, root);
let result = self.eval_internal(observer, root);
self.frames.pop();
self.resolve_tail_calls(observer, result)
}
#[inline(always)]
fn eval(&mut self, node: &ExecNode) -> Result<Value, String> {
self.eval_internal(&mut NoOpObserver, node)
}
fn eval_observed<O: VMObserver>(
&mut self,
observer: &mut O,
@@ -316,13 +312,31 @@ impl VM {
..
} => {
let val = self.eval_internal(obs, value)?;
let final_val = if !captured_by.is_empty() && matches!(addr, Address::Local(_)) {
Value::Cell(Rc::new(RefCell::new(val)))
let mut needs_cell_wrap = false;
if !captured_by.is_empty()
&& let Address::Local(slot) = addr
{
let frame = self.frames.last().unwrap();
let abs_index = frame.stack_base + (slot.0 as usize);
// Robustness Fix: If the slot is already a Cell (due to forward capture
// in a recursive scenario or complex pre-allocation), don't wrap it again.
// This prevents Cell(Cell(Value)) nesting which causes type errors.
if abs_index >= self.stack.len() || !matches!(self.stack[abs_index], Value::Cell(_)) {
needs_cell_wrap = true;
}
}
let store_val = if needs_cell_wrap {
Value::Cell(Rc::new(RefCell::new(val.clone())))
} else {
val
val.clone()
};
self.set_value(*addr, final_val.clone())?;
Ok(final_val)
self.set_value(*addr, store_val)?;
// Define always evaluates to the unwrapped value for immediate use.
Ok(val)
}
BoundKind::Destructure { pattern, value } => {
let val = self.eval_internal(obs, value)?;
@@ -494,12 +508,14 @@ impl VM {
for addr in upvalues {
captured.push(self.capture_upvalue(*addr)?);
}
let stack_size = body.calculate_stack_size();
let closure = Closure::new(
params.ty.original.clone(),
body.ty.original.clone(),
body.clone(),
captured,
*positional_count,
stack_size,
);
Ok(Value::Object(Rc::new(closure)))
}
@@ -720,7 +736,23 @@ impl VM {
}
_ => {
self.stack.truncate(base);
return Err(format!("Attempt to call non-function: {}", current_func));
let variant_name = match current_func {
Value::Void => "Void",
Value::Bool(_) => "Bool",
Value::Int(_) => "Int",
Value::Float(_) => "Float",
Value::DateTime(_) => "DateTime",
Value::Text(_) => "Text",
Value::Keyword(_) => "Keyword",
Value::Tuple(_) => "Tuple",
Value::Record(_, _) => "Record",
Value::FieldAccessor(_) => "FieldAccessor",
Value::Function(_) => "Function",
Value::Object(_) => "Object",
Value::Cell(_) => "Cell",
Value::TailCallRequest(_) => "TailCallRequest",
};
return Err(format!("Attempt to call non-function: {} (Variant: {})", current_func, variant_name));
}
};
@@ -879,7 +911,7 @@ impl VM {
Ok(cell)
}
} else {
Err(format!("Stack underflow capture local {}", slot))
Err(format!("Stack access out of bounds: trying to capture local {} at index {} but stack size is only {}", slot, abs_index, self.stack.len()))
}
}
Address::Upvalue(idx) => {