From be2bef373f8a64f9349e2dad8eae5d7220cfcea7 Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Thu, 12 Mar 2026 11:22:40 +0100 Subject: [PATCH] Refactor scope handling and immutability Introduces `fixed_scope_idx` to `Binder` and `Environment` to track immutable scopes. This allows enforcing that definitions (`def`) cannot occur in scopes that are considered fixed or immutable, such as the global scope or the initial bootstrap scope. The `FunctionCompiler` is updated to use `fixed_scope_idx` and `is_root` to determine scope immutability. --- src/ast/compiler/binder.rs | 74 +++++++++++++++++--------------- src/ast/compiler/macros.rs | 6 +-- src/ast/compiler/type_checker.rs | 2 +- src/ast/environment.rs | 11 ++++- 4 files changed, 53 insertions(+), 40 deletions(-) diff --git a/src/ast/compiler/binder.rs b/src/ast/compiler/binder.rs index 3c71c29..6556d18 100644 --- a/src/ast/compiler/binder.rs +++ b/src/ast/compiler/binder.rs @@ -31,28 +31,24 @@ impl CompilerScope { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ScopeKind { - Root, - Local, -} - struct FunctionCompiler { identity: Identity, scopes: Vec, slot_count: u32, upvalues: Vec
, - kind: ScopeKind, + fixed_scope_idx: i32, + is_root: bool, } impl FunctionCompiler { - fn new(kind: ScopeKind, identity: Identity) -> Self { + fn new(fixed_scope_idx: i32, identity: Identity, is_root: bool) -> Self { Self { identity, scopes: vec![CompilerScope::new()], slot_count: 0, upvalues: Vec::new(), - kind, + fixed_scope_idx, + is_root, } } @@ -70,15 +66,26 @@ impl FunctionCompiler { identity: Identity, globals: &Rc>>, ) -> Result { - if self.kind == ScopeKind::Root && self.scopes.len() == 1 { - let current_scope = self.scopes.last_mut().unwrap(); - if current_scope.locals.contains_key(name) { - return Err(format!( - "Variable '{}' is already defined in this scope level.", - name.name - )); - } + let current_scope_idx = self.scopes.len() as i32 - 1; + + // 1. Check if the current scope is immutable (e.g. Scope 0 after bootstrapping) + if current_scope_idx <= self.fixed_scope_idx { + return Err(format!( + "Cannot define '{}': Scope {} is frozen/immutable.", + name.name, current_scope_idx + )); + } + let current_scope = self.scopes.last_mut().unwrap(); + if current_scope.locals.contains_key(name) { + return Err(format!( + "Variable '{}' is already defined in this scope level.", + name.name + )); + } + + // 2. Only Scope 0 of the ROOT function is the Global scope in Myc + if self.is_root && current_scope_idx == 0 { let mut globals_map = globals.borrow_mut(); let addr = if let Some((idx, existing_id)) = globals_map.get(name) { if *existing_id != identity { @@ -101,13 +108,7 @@ impl FunctionCompiler { ); Ok(addr) } else { - let current_scope = self.scopes.last_mut().unwrap(); - if current_scope.locals.contains_key(name) { - return Err(format!( - "Variable '{}' is already defined in this scope level.", - name.name - )); - } + // Local scope (Block or Lambda) let slot = LocalSlot(self.slot_count); current_scope.locals.insert( name.clone(), @@ -160,28 +161,33 @@ pub struct Binder { } impl Binder { - pub fn new(globals: Rc>>) -> Self { + pub fn new( + globals: Rc>>, + fixed_scope_idx: i32, + ) -> Self { let mut binder = Self { functions: Vec::new(), globals, capture_map: HashMap::new(), }; binder.functions.push(FunctionCompiler::new( - ScopeKind::Root, + fixed_scope_idx, crate::ast::types::NodeIdentity::new(crate::ast::types::SourceLocation { line: 0, col: 0, }), + true, // is_root = true for the script itself )); binder } pub fn bind_root( globals: Rc>>, + fixed_scope_idx: i32, node: &Node, diagnostics: &mut Diagnostics, ) -> Result<(BoundNode, HashMap>), String> { - let mut binder = Self::new(globals); + let mut binder = Self::new(globals, fixed_scope_idx); let bound = binder.bind(node, ExprContext::Expression, diagnostics); // Convert HashSet to sorted Vec @@ -358,7 +364,7 @@ impl Binder { UntypedKind::Lambda { params, body } => { let identity = node.identity.clone(); self.functions - .push(FunctionCompiler::new(ScopeKind::Local, identity.clone())); + .push(FunctionCompiler::new(-1, identity.clone(), false)); // 1. Bind the parameter pattern/tuple let params_bound = self.bind_pattern(params, DeclarationKind::Parameter, diag); @@ -696,7 +702,7 @@ mod tests { let globals = Rc::new(RefCell::new(HashMap::new())); let mut diagnostics = Diagnostics::new(); - let (bound, captures) = Binder::bind_root(globals, &untyped, &mut diagnostics).unwrap(); + let (bound, captures) = Binder::bind_root(globals, 0, &untyped, &mut diagnostics).unwrap(); // Structure: Lambda -> Block -> [ Define(x), Define(f), Get(x) ] if let BoundKind::Lambda { body, .. } = &bound.kind { @@ -730,7 +736,7 @@ mod tests { let globals = Rc::new(RefCell::new(HashMap::new())); let mut diagnostics = Diagnostics::new(); - let (bound, captures) = Binder::bind_root(globals, &untyped, &mut diagnostics).unwrap(); + let (bound, captures) = Binder::bind_root(globals, 0, &untyped, &mut diagnostics).unwrap(); if let BoundKind::Lambda { body, .. } = &bound.kind { if let BoundKind::Block { exprs } = &body.kind { @@ -760,7 +766,7 @@ mod tests { let globals = Rc::new(RefCell::new(HashMap::new())); let mut diagnostics = Diagnostics::new(); - let _ = Binder::bind_root(globals, &untyped, &mut diagnostics); + let _ = Binder::bind_root(globals, 0, &untyped, &mut diagnostics); assert!(diagnostics.has_errors()); assert!(diagnostics.items.iter().any(|i| i.message.contains("already defined"))); @@ -774,15 +780,15 @@ mod tests { 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()); + assert!(Binder::bind_root(globals.clone(), 0, &untyped1, &mut diagnostics).is_ok()); // Second run: attempts to redefine 'x' in the same global environment 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); + let _ = Binder::bind_root(globals.clone(), 0, &untyped2, &mut diagnostics2); assert!(diagnostics2.has_errors()); - assert!(diagnostics2.items.iter().any(|i| i.message.contains("already defined"))); + assert!(diagnostics2.items.iter().any(|i| i.message.contains("frozen/immutable"))); } } diff --git a/src/ast/compiler/macros.rs b/src/ast/compiler/macros.rs index 1e74b42..08fd059 100644 --- a/src/ast/compiler/macros.rs +++ b/src/ast/compiler/macros.rs @@ -778,7 +778,7 @@ mod tests { let globals = Rc::new(RefCell::new(global_names)); let mut diag = crate::ast::diagnostics::Diagnostics::new(); - let result = Binder::bind_root(globals, &expanded, &mut diag); + let result = Binder::bind_root(globals, 0, &expanded, &mut diag); assert!( result.is_ok(), "Should find global '*' Error: {:?}", @@ -803,7 +803,7 @@ mod tests { let globals = Rc::new(RefCell::new(HashMap::new())); let mut diag = crate::ast::diagnostics::Diagnostics::new(); - let result = Binder::bind_root(globals, &expanded, &mut diag); + let result = Binder::bind_root(globals, 0, &expanded, &mut diag); assert!(result.is_ok(), "Hygiene failed: {:?}", result.err()); } @@ -824,7 +824,7 @@ mod tests { let globals = Rc::new(RefCell::new(HashMap::new())); let mut diag = crate::ast::diagnostics::Diagnostics::new(); - let result = Binder::bind_root(globals, &expanded, &mut diag); + let result = Binder::bind_root(globals, 0, &expanded, &mut diag); assert!( result.is_ok(), "Explicit Hygiene with Backticks failed: {:?}", diff --git a/src/ast/compiler/type_checker.rs b/src/ast/compiler/type_checker.rs index 9e0b9f1..d8b9675 100644 --- a/src/ast/compiler/type_checker.rs +++ b/src/ast/compiler/type_checker.rs @@ -717,7 +717,7 @@ mod tests { assert!(result.is_err()); assert_eq!( result.unwrap_err(), - "Statement 'def' cannot be used as an expression.\nCannot destructure type int as a tuple/vector" + "Statement 'def' cannot be used as an expression.\nCannot define 'x': Scope 0 is frozen/immutable.\nCannot destructure type int as a tuple/vector" ); } diff --git a/src/ast/environment.rs b/src/ast/environment.rs index 5ad6623..090a4da 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -75,6 +75,7 @@ pub struct Environment { pub global_types: Rc>>, pub global_purity: Rc>>, pub global_values: Rc>>, + pub fixed_scope_idx: i32, pub function_registry: Rc>, pub typed_function_registry: Rc>, pub monomorph_cache: Rc>, @@ -112,6 +113,7 @@ struct RuntimeMacroEvaluator { global_names: Rc>>, global_types: Rc>>, global_values: Rc>>, + fixed_scope_idx: i32, } impl MacroEvaluator for RuntimeMacroEvaluator { @@ -127,7 +129,7 @@ impl MacroEvaluator for RuntimeMacroEvaluator { } let mut diag = Diagnostics::new(); - let (bound_ast, captures) = Binder::bind_root(self.global_names.clone(), node, &mut diag)?; + let (bound_ast, captures) = Binder::bind_root(self.global_names.clone(), self.fixed_scope_idx, node, &mut diag)?; let bound_ast = crate::ast::compiler::captures::CapturePass::apply(bound_ast, &captures); let checker = TypeChecker::new(self.global_types.clone()); let typed_ast = checker.check(&bound_ast, &[], &mut diag); @@ -161,6 +163,7 @@ impl Environment { global_types: Rc::new(RefCell::new(HashMap::new())), global_purity: Rc::new(RefCell::new(HashMap::new())), global_values: Rc::new(RefCell::new(Vec::new())), + fixed_scope_idx: -1, function_registry: Rc::new(RefCell::new(HashMap::new())), typed_function_registry: Rc::new(RefCell::new(HashMap::new())), monomorph_cache: Rc::new(RefCell::new(HashMap::new())), @@ -172,6 +175,9 @@ impl Environment { loaded_modules: Rc::new(RefCell::new(HashSet::new())), }; crate::ast::rtl::register(&env); + + let mut env = env; + env.fixed_scope_idx = 0; // Automatically add standard search paths (CWD and CWD/rtl) if let Ok(cwd) = std::env::current_dir() { @@ -212,6 +218,7 @@ impl Environment { global_names: self.global_names.clone(), global_types: self.global_types.clone(), global_values: self.global_values.clone(), + fixed_scope_idx: self.fixed_scope_idx, }; MacroExpander::new(self.macro_registry.borrow().clone(), evaluator) } @@ -423,7 +430,7 @@ impl Environment { }; let (bound_ast, captures) = - match Binder::bind_root(self.global_names.clone(), &expanded_ast, diagnostics) { + match Binder::bind_root(self.global_names.clone(), self.fixed_scope_idx, &expanded_ast, diagnostics) { Ok(res) => res, Err(e) => { diagnostics.push_error(e, None);