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:
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)) => (),
|
||||
|
||||
Reference in New Issue
Block a user