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.
This commit is contained in:
2026-03-31 15:22:51 +02:00
parent 8fd2f2d113
commit af8fb5cb7f
7 changed files with 43 additions and 124 deletions
+10 -1
View File
@@ -139,6 +139,10 @@ pub type BindingResult = (
pub struct Binder {
functions: Vec<FunctionCompiler>,
capture_map: HashMap<Identity, std::collections::HashSet<Identity>>,
/// 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<Address<VirtualId>> {
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<BoundPhase>) -> Option<u32> {
@@ -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(),
+4 -2
View File
@@ -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;
}
_ => {}
}
+11 -18
View File
@@ -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)
+9 -9
View File
@@ -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);
}
}
+6 -89
View File
@@ -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<BoundPhase>) -> Node<BoundPhase> {
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<BoundPhase>, 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<TypedNode> {
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<Rc<Closure>, 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<Value, String> {
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))
}
}
+2 -3
View File
@@ -105,18 +105,17 @@ pub fn run_benchmarks(update: bool, filter: Option<&str>) -> Vec<BenchmarkResult
};
// Helper to measure sum of VM execution times over N executions in one environment.
// A single VM is reused across iterations: run_with_args resets stack/frames
// A single VM is reused across iterations: run resets stack/frames
// on each call, so no re-allocation is needed between runs.
let measure_sum =
|n: u32, node: &TypedNode| -> Result<Duration, String> {
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)
+1 -2
View File
@@ -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)) => (),