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:
@@ -141,9 +141,13 @@ impl FunctionCompiler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the context in which a node is being bound.
|
||||
/// This allows the binder to enforce rules like "statements cannot be used as values".
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ExprContext {
|
||||
/// The node must return a value (e.g., RHS of an assignment, function argument).
|
||||
Expression,
|
||||
/// The node is used as a standalone instruction (e.g., inside a block).
|
||||
Statement,
|
||||
}
|
||||
|
||||
@@ -750,7 +754,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_redefinition_error() {
|
||||
let source = "(do (def x 1) (def x 2))";
|
||||
let source = "(do (def x 1) (def x 2) x)";
|
||||
let mut parser = Parser::new(source);
|
||||
let untyped = parser.parse_expression();
|
||||
|
||||
@@ -759,7 +763,7 @@ mod tests {
|
||||
let _ = Binder::bind_root(globals, &untyped, &mut diagnostics);
|
||||
|
||||
assert!(diagnostics.has_errors());
|
||||
assert!(diagnostics.items[0].message.contains("already defined"));
|
||||
assert!(diagnostics.items.iter().any(|i| i.message.contains("already defined")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -767,18 +771,18 @@ mod tests {
|
||||
let globals = Rc::new(RefCell::new(HashMap::new()));
|
||||
|
||||
// First run: defines 'x'
|
||||
let source1 = "(def x 1)";
|
||||
let source1 = "(def x 1) 1";
|
||||
let untyped1 = Parser::new(source1).parse_expression();
|
||||
let mut diagnostics = Diagnostics::new();
|
||||
assert!(Binder::bind_root(globals.clone(), &untyped1, &mut diagnostics).is_ok());
|
||||
|
||||
// Second run: attempts to redefine 'x' in the same global environment
|
||||
let source2 = "(def x 2)";
|
||||
let source2 = "(def x 2) 2";
|
||||
let untyped2 = Parser::new(source2).parse_expression();
|
||||
let mut diagnostics2 = Diagnostics::new();
|
||||
let _ = Binder::bind_root(globals.clone(), &untyped2, &mut diagnostics2);
|
||||
|
||||
assert!(diagnostics2.has_errors());
|
||||
assert!(diagnostics2.items[0].message.contains("already defined"));
|
||||
assert!(diagnostics2.items.iter().any(|i| i.message.contains("already defined")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +304,99 @@ where
|
||||
/// A single field in a Record literal (Key-Value pair)
|
||||
pub type RecordField<T> = (BoundNode<T>, BoundNode<T>);
|
||||
|
||||
impl<T> BoundNode<T> {
|
||||
pub fn calculate_stack_size(&self) -> u32 {
|
||||
let mut max_slot = -1i32;
|
||||
|
||||
fn visit<T>(node: &BoundNode<T>, max_slot: &mut i32) {
|
||||
match &node.kind {
|
||||
BoundKind::Get {
|
||||
addr: Address::Local(slot),
|
||||
..
|
||||
}
|
||||
| BoundKind::Set {
|
||||
addr: Address::Local(slot),
|
||||
..
|
||||
}
|
||||
| BoundKind::Define {
|
||||
addr: Address::Local(slot),
|
||||
..
|
||||
} => {
|
||||
if slot.0 as i32 > *max_slot {
|
||||
*max_slot = slot.0 as i32;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match &node.kind {
|
||||
BoundKind::If {
|
||||
cond,
|
||||
then_br,
|
||||
else_br,
|
||||
} => {
|
||||
visit(cond, max_slot);
|
||||
visit(then_br, max_slot);
|
||||
if let Some(e) = else_br {
|
||||
visit(e, max_slot);
|
||||
}
|
||||
}
|
||||
BoundKind::Set { value, .. } => {
|
||||
visit(value, max_slot);
|
||||
}
|
||||
BoundKind::Define { value, .. } => {
|
||||
visit(value, max_slot);
|
||||
}
|
||||
BoundKind::Destructure { pattern, value } => {
|
||||
visit(pattern, max_slot);
|
||||
visit(value, max_slot);
|
||||
}
|
||||
BoundKind::Pipe { inputs, lambda, .. } => {
|
||||
for i in inputs {
|
||||
visit(i, max_slot);
|
||||
}
|
||||
visit(lambda, max_slot);
|
||||
}
|
||||
BoundKind::Call { callee, args } => {
|
||||
visit(callee, max_slot);
|
||||
visit(args, max_slot);
|
||||
}
|
||||
BoundKind::Again { args } => visit(args, max_slot),
|
||||
BoundKind::Block { exprs } => {
|
||||
for e in exprs {
|
||||
visit(e, max_slot);
|
||||
}
|
||||
}
|
||||
BoundKind::Tuple { elements } => {
|
||||
for e in elements {
|
||||
visit(e, max_slot);
|
||||
}
|
||||
}
|
||||
BoundKind::Record { values, .. } => {
|
||||
for v in values {
|
||||
visit(v, max_slot);
|
||||
}
|
||||
}
|
||||
BoundKind::Expansion {
|
||||
bound_expanded, ..
|
||||
} => visit(bound_expanded, max_slot),
|
||||
BoundKind::Lambda { params, body, .. } => {
|
||||
// Check parameters and body of the lambda,
|
||||
// but do NOT recurse into nested lambdas (which is handled by the generic recursion blocker below).
|
||||
visit(params, max_slot);
|
||||
visit(body, max_slot);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the root node: if it's a lambda, we want to check its content.
|
||||
// If we just called visit(self), it would hit the Lambda case.
|
||||
visit(self, &mut max_slot);
|
||||
(max_slot + 1) as u32
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> BoundKind<T> {
|
||||
pub fn display_name(&self) -> String {
|
||||
match self {
|
||||
|
||||
@@ -500,6 +500,19 @@ impl Optimizer {
|
||||
}
|
||||
|
||||
let opt = self.visit_node(e.clone(), sub, path);
|
||||
|
||||
if let BoundKind::Define { addr, value, .. } = &opt.kind
|
||||
&& !info.assigned.contains(addr)
|
||||
{
|
||||
if let BoundKind::Constant(val) = &value.kind {
|
||||
sub.add_value(*addr, val.clone());
|
||||
} else if let BoundKind::Lambda { upvalues, .. } = &value.kind
|
||||
&& upvalues.is_empty()
|
||||
{
|
||||
sub.add_ast_substitution(*addr, (**value).clone());
|
||||
}
|
||||
}
|
||||
|
||||
if self.enabled && matches!(opt.kind, BoundKind::Nop) && !is_last {
|
||||
block_changed = true;
|
||||
continue;
|
||||
|
||||
@@ -717,7 +717,7 @@ mod tests {
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err(),
|
||||
"Cannot destructure type int as a tuple/vector"
|
||||
"Statement 'def' cannot be used as an expression.\nCannot destructure type int as a tuple/vector"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user