From af8fb5cb7f4e2377ec520b4420f832896c50c160 Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 31 Mar 2026 15:22:51 +0200 Subject: [PATCH] Refactor binder to use scope depth Introduced `scope_depth` to the `Binder` to track block nesting. This allows for correctly determining whether a `def` binding should be `Address::Global` (at scope depth 0) or `Address::Local`. The `type_checker` was also updated to handle `Program` and `Block` nodes by creating a new `TypeContext` for them. Removed the `wrap_as_lambda` method from `Environment` as the compiler now always produces a `Program` node, effectively handling the wrapping at the AST level. Correspondingly, `instantiate` and `run_script_compiled` were simplified to directly run the `Program` node. --- src/ast/compiler/binder.rs | 11 ++- src/ast/compiler/type_checker/context.rs | 6 +- src/ast/compiler/type_checker/mod.rs | 29 +++----- src/ast/compiler/type_checker/tests.rs | 18 ++--- src/ast/environment.rs | 95 ++---------------------- src/utils/tester.rs | 5 +- tests/closures.rs | 3 +- 7 files changed, 43 insertions(+), 124 deletions(-) diff --git a/src/ast/compiler/binder.rs b/src/ast/compiler/binder.rs index 7368ea8..515a442 100644 --- a/src/ast/compiler/binder.rs +++ b/src/ast/compiler/binder.rs @@ -139,6 +139,10 @@ pub type BindingResult = ( pub struct Binder { functions: Vec, capture_map: HashMap>, + /// Block nesting depth. At 0 (inside a `Program` node), `def` bindings + /// receive `Address::Global` so their values persist in the GlobalStore. + /// Incremented by `Block`, not by `Program`. + scope_depth: u32, } impl Binder { @@ -146,6 +150,7 @@ impl Binder { let mut binder = Self { functions: Vec::new(), capture_map: HashMap::new(), + scope_depth: 0, }; binder.functions.push(FunctionCompiler::new( NodeIdentity::new(SourceLocation { line: 0, col: 0 }), @@ -186,7 +191,7 @@ impl Binder { _kind: DeclarationKind, diag: &mut Diagnostics, ) -> Option> { - let as_global = self.functions.len() == 1; + let as_global = self.scope_depth == 0; let current_fn = self.functions.last_mut().unwrap(); match current_fn.define_variable(name, identity, as_global) { Ok(addr) => Some(addr), @@ -354,10 +359,12 @@ impl Binder { let identity = node.identity.clone(); self.functions .push(FunctionCompiler::new(identity.clone(), vec![], 0)); + self.scope_depth += 1; let params_bound = self.bind_pattern(params.as_ref(), DeclarationKind::Parameter, diag); let body_bound = self.bind(body.as_ref(), ExprContext::Expression, diag); + self.scope_depth -= 1; let compiled_fn = self.functions.pop().unwrap(); fn count_params(node: &Node) -> Option { @@ -429,6 +436,7 @@ impl Binder { SyntaxKind::Block { exprs } => { self.functions.last_mut().unwrap().push_scope(); + self.scope_depth += 1; let mut bound_exprs = Vec::new(); for (i, expr) in exprs.iter().enumerate() { let expr_ctx = if i == exprs.len() - 1 { @@ -438,6 +446,7 @@ impl Binder { }; bound_exprs.push(Rc::new(self.bind(expr, expr_ctx, diag))); } + self.scope_depth -= 1; self.functions.last_mut().unwrap().pop_scope(); self.make_node( node.identity.clone(), diff --git a/src/ast/compiler/type_checker/context.rs b/src/ast/compiler/type_checker/context.rs index 96f95f7..f202609 100644 --- a/src/ast/compiler/type_checker/context.rs +++ b/src/ast/compiler/type_checker/context.rs @@ -63,9 +63,11 @@ impl<'a> TypeContext<'a> { } Address::Global(idx) => { let mut rt = self.root_types.borrow_mut(); - if (idx.0 as usize) < rt.len() { - rt[idx.0 as usize] = ty; + let i = idx.0 as usize; + if i >= rt.len() { + rt.resize(i + 1, StaticType::Any); } + rt[i] = ty; } _ => {} } diff --git a/src/ast/compiler/type_checker/mod.rs b/src/ast/compiler/type_checker/mod.rs index 698eddf..a29536b 100644 --- a/src/ast/compiler/type_checker/mod.rs +++ b/src/ast/compiler/type_checker/mod.rs @@ -1,17 +1,14 @@ -mod context; -mod inference; -mod finalize; mod check; +mod context; +mod finalize; +mod inference; #[cfg(test)] mod tests; use crate::ast::compiler::call_hooks::RtlCompilerHook; use crate::ast::diagnostics::Diagnostics; -use crate::ast::nodes::{ - BoundLike, BoundPhase, LambdaBinding, - Node, NodeKind, TypedNode, -}; +use crate::ast::nodes::{BoundLike, BoundPhase, LambdaBinding, Node, NodeKind, TypedNode}; use crate::ast::types::{Signature, StaticType}; use std::cell::Cell; use std::collections::HashMap; @@ -72,11 +69,7 @@ impl TypeChecker { diag: &mut Diagnostics, ) -> TypedNode { match &node.kind { - NodeKind::Lambda { - params, - body, - info, - } => { + NodeKind::Lambda { params, body, info } => { let upvalues = &info.upvalues; let positional_count = info.positional_count; @@ -95,12 +88,8 @@ impl TypeChecker { StaticType::Tuple(arg_types.to_vec()) }; - let params_typed = self.check_params( - params.as_ref(), - &arg_tuple_ty, - &mut lambda_ctx, - diag, - ); + let params_typed = + self.check_params(params.as_ref(), &arg_tuple_ty, &mut lambda_ctx, diag); let body_typed = self.check_node(body, &mut lambda_ctx, diag); let ret_ty = body_typed.ty.clone(); @@ -125,6 +114,10 @@ impl TypeChecker { comments: node.comments.clone(), } } + NodeKind::Block { .. } | NodeKind::Program { .. } => { + let mut ctx = TypeContext::new(64, vec![], &self.root_types, None); + self.check_node(node, &mut ctx, diag) + } _ => { let mut root_ctx = TypeContext::new(0, vec![], &self.root_types, None); self.check_node(node, &mut root_ctx, diag) diff --git a/src/ast/compiler/type_checker/tests.rs b/src/ast/compiler/type_checker/tests.rs index 1ee7340..61da5c1 100644 --- a/src/ast/compiler/type_checker/tests.rs +++ b/src/ast/compiler/type_checker/tests.rs @@ -27,9 +27,9 @@ fn test_inference_constants() { fn test_inference_variable_propagation() { // (do (def x 10) x) -> The last 'x' must be Int let typed = check_source("(do (def x 10) x)"); - // Outer is Lambda, Body is Block - if let NodeKind::Lambda { body, .. } = &typed.kind { - if let NodeKind::Block { exprs } = &body.kind { + // compile() wraps in a Program node; the (do ...) block is the single child. + if let NodeKind::Program { exprs: prog } = &typed.kind { + if let NodeKind::Block { exprs } = &prog[0].kind { let last_expr = exprs.last().unwrap(); assert_eq!( last_expr.ty, @@ -37,10 +37,10 @@ fn test_inference_variable_propagation() { "Variable 'x' should be inferred as Int" ); } else { - panic!("Expected block in lambda body"); + panic!("Expected Block inside Program"); } } else { - panic!("Expected Lambda wrapper"); + panic!("Expected Program, got {:?}", typed.kind); } } @@ -78,8 +78,8 @@ fn test_inference_lambda_return() { fn test_inference_assignment_updates_type() { // (do (def x 10) (assign x 20.5) x) -> x becomes Float after assignment let typed = check_source("(do (def x 10) (assign x 20.5) x)"); - if let NodeKind::Lambda { body, .. } = &typed.kind { - if let NodeKind::Block { exprs } = &body.kind { + if let NodeKind::Program { exprs: prog } = &typed.kind { + if let NodeKind::Block { exprs } = &prog[0].kind { let last_expr = exprs.last().unwrap(); assert_eq!( last_expr.ty, @@ -87,10 +87,10 @@ fn test_inference_assignment_updates_type() { "Variable 'x' should be specialized to Float after assignment" ); } else { - panic!("Expected block"); + panic!("Expected Block inside Program"); } } else { - panic!("Expected Lambda"); + panic!("Expected Program, got {:?}", typed.kind); } } diff --git a/src/ast/environment.rs b/src/ast/environment.rs index 66489e3..8ea961f 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -4,7 +4,6 @@ use crate::ast::compiler::call_hooks::RtlCompilerHook; use crate::ast::compiler::{CapturePass, TypeChecker, TypedNode}; use crate::ast::nodes::{AnalyzedPhase, BoundPhase, Symbol, SyntaxKind, SyntaxNode}; use crate::ast::parser::Parser; -use crate::ast::closure::Closure; use crate::ast::vm::{GlobalStore, TracingObserver, VM}; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; @@ -13,7 +12,7 @@ use std::rc::Rc; use crate::ast::nodes::{ Address, AnalyzedNode, ExecNode, GlobalAnalyzedRegistry, GlobalFunctionRegistry, GlobalIdx, - LambdaBinding, Node, NodeKind, VirtualId, + Node, NodeKind, VirtualId, }; use crate::ast::compiler::dumper::Dumper; use crate::ast::compiler::lambda_collector::LambdaCollector; @@ -440,32 +439,6 @@ impl Environment { Some(bound_ast) } - /// Wraps a bound AST in a parameterless lambda (unless it already is one). - fn wrap_as_lambda(&self, bound_ast: Node) -> Node { - if let NodeKind::Lambda { .. } = bound_ast.kind { - bound_ast - } else { - Node { - identity: bound_ast.identity.clone(), - kind: NodeKind::Lambda { - params: Rc::new(Node { - identity: bound_ast.identity.clone(), - kind: NodeKind::Tuple { elements: vec![] }, - ty: (), - comments: Rc::from([]), - }), - body: Rc::new(bound_ast), - info: LambdaBinding { - upvalues: vec![], - positional_count: Some(0), - }, - }, - ty: (), - comments: Rc::from([]), - } - } - } - /// Type-checks a bound AST and collects doc comments. fn type_check(&self, bound_ast: &Node, diagnostics: &mut Diagnostics) -> TypedNode { let checker = TypeChecker::new(self.root_types.clone(), Rc::clone(&self.rtl.compiler_hooks)); @@ -474,12 +447,11 @@ impl Environment { typed } - /// Full compilation pipeline: expand → bind → wrap → type-check. + /// Full compilation pipeline: expand → bind → type-check. fn compile_pipeline(&self, syntax_ast: SyntaxNode, diagnostics: &mut Diagnostics) -> Option { let expanded = self.expand(syntax_ast, diagnostics)?; let bound = self.bind_and_update(&expanded, diagnostics)?; - let wrapped = self.wrap_as_lambda(bound); - let typed = self.type_check(&wrapped, diagnostics); + let typed = self.type_check(&bound, diagnostics); Some(typed) } @@ -738,17 +710,7 @@ impl Environment { } let mut parser = Parser::new(source); - let syntax_ast = parser.parse_expression(); - - if !parser.at_eof() { - parser - .diagnostics - .push_error("Unexpected trailing expressions in script.", None); - return CompilationResult { - ast: None, - diagnostics: parser.diagnostics, - }; - } + let syntax_ast = parser.parse_program(); let mut diagnostics = parser.diagnostics; let typed_ast = self.compile_pipeline(syntax_ast, &mut diagnostics); @@ -780,43 +742,6 @@ impl Environment { Lowering::lower(optimized) } - /// Converts a linked `ExecNode` into a callable `Closure`. - /// - /// Fast path: if the node is already a top-level lambda without upvalues, - /// the `Closure` is constructed directly without running the VM. - /// - /// Slow path: the node is executed once to obtain its resulting `Closure` - /// value (e.g. a `do`-block that returns a lambda). - /// - /// The caller is responsible for creating a `VM` and invoking the closure - /// via `vm.run_with_args`. This keeps VM lifecycle and error handling at - /// the call site, where the required strategy (single run vs. repeated - /// benchmark iterations) is known. - pub fn instantiate(&self, node: ExecNode) -> Result, String> { - // Fast path: top-level lambda without upvalues — build Closure directly. - if let NodeKind::Lambda { params, body, info } = &node.kind - && info.upvalues.is_empty() - { - return Ok(Rc::new(Closure::new( - params.clone(), - body.ty.original.clone(), - body.clone(), - Vec::new(), - info.positional_count, - node.ty.stack_size, - ))); - } - - // Slow path: run the node once to extract the resulting Closure. - let mut vm = VM::new(self.global_store()); - let res = vm.run(&node)?; - if let Value::Closure(obj) = res { - Ok(obj) - } else { - Err("Script did not produce a callable closure".to_string()) - } - } - /// Creates a new `VM` connected to this environment's global scope. pub fn create_vm(&self) -> VM { VM::new(self.global_store()) @@ -920,9 +845,8 @@ impl Environment { pub fn run_script_compiled(&self, compiled: TypedNode) -> Result { let linked = self.link(compiled); - let closure = self.instantiate(linked)?; let mut vm = self.create_vm(); - let res = vm.run_with_args(closure, &[])?; + let res = vm.run(&linked)?; self.run_pipeline(); Ok(res) } @@ -935,17 +859,10 @@ impl Environment { let mut vm = VM::new(self.global_store()); let mut observer = TracingObserver::new(); - // 1. Run the script wrapper (returns a closure representing the script) let result = vm.run_with_observer(&mut observer, &linked); - // 2. Execute the root closure immediately to get the actual script result. - // All Myc scripts are wrapped in a parameterless lambda for consistency. - let mut final_result = result; - if let Ok(Value::Closure(obj)) = &final_result { - final_result = vm.run_with_args_observed(&mut observer, obj.clone(), &[]); - } self.run_pipeline(); - Ok((final_result, observer.logs)) + Ok((result, observer.logs)) } } diff --git a/src/utils/tester.rs b/src/utils/tester.rs index bd749f5..9dc95db 100644 --- a/src/utils/tester.rs +++ b/src/utils/tester.rs @@ -105,18 +105,17 @@ pub fn run_benchmarks(update: bool, filter: Option<&str>) -> Vec Result { let env = Environment::new(); let linked = env.link(node.clone()); - let closure = env.instantiate(linked)?; let mut vm = env.create_vm(); let mut total = Duration::ZERO; for _ in 0..n { let start = Instant::now(); - let _ = vm.run_with_args(closure.clone(), &[])?; + let _ = vm.run(&linked)?; total += start.elapsed(); } Ok(total) diff --git a/tests/closures.rs b/tests/closures.rs index a2af81e..fe79ac7 100644 --- a/tests/closures.rs +++ b/tests/closures.rs @@ -18,8 +18,7 @@ fn test_closure_modification_from_source() { .into_result() .expect("Failed to compile"); let linked = env.link(compiled); - let closure = env.instantiate(linked).expect("Failed to instantiate"); - let result = env.create_vm().run_with_args(closure, &[]); + let result = env.create_vm().run(&linked); match result { Ok(Value::Int(20)) => (),