From 3b063dc2c919d430e62fe1acdff0b9fe9be3a4c3 Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Tue, 10 Mar 2026 17:03:46 +0100 Subject: [PATCH] Add block scoping and statement/expression distinction Implement strict block scoping and distinguish between statements and expressions. This change introduces hierarchical scopes for functions and blocks, enforces that `def` is a statement with no return value, and prevents statements from appearing in expression positions or as the last element of a block. A new example `design-flaw.myc` is added to demonstrate the uninitialized variable issue solved by these changes. --- docs/binder-refactoring-log.md | 45 +++++++++++++++++++ examples/design-flaw.myc | 2 +- src/ast/compiler/binder.rs | 81 ++++++++++++++++++++-------------- 3 files changed, 95 insertions(+), 33 deletions(-) create mode 100644 docs/binder-refactoring-log.md diff --git a/docs/binder-refactoring-log.md b/docs/binder-refactoring-log.md new file mode 100644 index 0000000..42c8cc7 --- /dev/null +++ b/docs/binder-refactoring-log.md @@ -0,0 +1,45 @@ +# Development Log: Binder Refactoring & Block Scoping + +## Status Quo +- Myc currently uses a flat scope per function. +- `def` is treated as a side-effect expression that "leaks" into the function scope regardless of nesting (e.g., inside `if` or `do`). +- This leads to uninitialized variables at runtime if the definition is skipped by control flow. + +## Goal +Implement strict **Block Scoping** and distinguish between **Statements** and **Expressions** to eliminate undefined variable states. + +### Rules +1. **Block Scoping:** Every `(do ...)` and `if` branch creates a new lexical scope. +2. **Statements vs. Expressions:** + - `def` is a **Statement**. + - Statements have no return value (Void). + - Statements **cannot** be used as expressions (e.g., as arguments, RHS of assignments, or conditions). + - Statements **cannot** be the last element of a block (since the block's value is determined by its last expression). +3. **No Shadowing:** Redefining a symbol within the **same** scope level is an error. Shadowing from outer scopes is allowed (standard lexical scoping). +4. **Self-Reference:** A variable is only available in its scope **after** its definition is complete (RHS of `def` sees outer scope). + +## Implementation Plan + +### Phase 1: Binder Infrastructure (In Progress) +1. [ ] Update `CompilerScope` to support hierarchical nesting via a stack in `FunctionCompiler`. +2. [ ] Implement `push_scope` and `pop_scope` in `Binder`. +3. [ ] Refactor `resolve_variable` to traverse the scope stack (inner-to-outer). +4. [ ] Update `define_variable` to enforce "No Shadowing" at the current scope level. + +### Phase 2: Statement/Expression Validation +1. [ ] Introduce a mechanism to track "Context" (Expression vs. Statement) during binding. +2. [ ] Validate that `def` is only used in statement positions. +3. [ ] Enforce that the last node in a `BoundKind::Block` is an expression. + +### Phase 3: VM Compatibility +1. [ ] Ensure the VM handles "Void" results from statements correctly. +2. [ ] (Optional) Optimize stack allocation based on scope depth. + +--- + +## Log Entries + +### 2024-05-22: Initialization +- Analyzed `design-flaw.myc`. +- Defined the "Statement" concept to solve the uninitialized variable problem. +- Initialized this log. diff --git a/examples/design-flaw.myc b/examples/design-flaw.myc index bd5ee60..427b931 100644 --- a/examples/design-flaw.myc +++ b/examples/design-flaw.myc @@ -1,7 +1,7 @@ (do (do - (def x "hh") + (if false (def x "hh")) ) (print x) ) diff --git a/src/ast/compiler/binder.rs b/src/ast/compiler/binder.rs index a20bd45..8f7e961 100644 --- a/src/ast/compiler/binder.rs +++ b/src/ast/compiler/binder.rs @@ -21,40 +21,14 @@ struct LocalInfo { #[derive(Debug, Clone)] struct CompilerScope { locals: HashMap, - slot_count: u32, } impl CompilerScope { fn new() -> Self { Self { locals: HashMap::new(), - slot_count: 0, } } - - fn define(&mut self, sym: &Symbol, identity: Identity) -> Result { - if self.locals.contains_key(sym) { - return Err(format!( - "Variable '{}' is already defined in this scope level.", - sym.name - )); - } - let slot = LocalSlot(self.slot_count); - self.locals.insert( - sym.clone(), - LocalInfo { - slot, - identity, - _ty: StaticType::Any, - }, - ); - self.slot_count += 1; - Ok(slot) - } - - fn resolve(&self, sym: &Symbol) -> Option { - self.locals.get(sym).cloned() - } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -65,7 +39,8 @@ enum ScopeKind { struct FunctionCompiler { identity: Identity, - scope: CompilerScope, + scopes: Vec, + slot_count: u32, upvalues: Vec
, kind: ScopeKind, } @@ -74,12 +49,21 @@ impl FunctionCompiler { fn new(kind: ScopeKind, identity: Identity) -> Self { Self { identity, - scope: CompilerScope::new(), + scopes: vec![CompilerScope::new()], + slot_count: 0, upvalues: Vec::new(), kind, } } + fn push_scope(&mut self) { + self.scopes.push(CompilerScope::new()); + } + + fn pop_scope(&mut self) { + self.scopes.pop(); + } + fn define_variable( &mut self, name: &Symbol, @@ -98,14 +82,39 @@ impl FunctionCompiler { let idx = GlobalIdx(globals_map.len() as u32); globals_map.insert(name.clone(), (idx, identity)); Ok(Address::Global(idx)) - } + } ScopeKind::Local => { - let slot = self.scope.define(name, identity)?; + 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 slot = LocalSlot(self.slot_count); + current_scope.locals.insert( + name.clone(), + LocalInfo { + slot, + identity, + _ty: StaticType::Any, + }, + ); + self.slot_count += 1; Ok(Address::Local(slot)) } } } + fn resolve_local(&self, sym: &Symbol) -> Option { + for scope in self.scopes.iter().rev() { + if let Some(info) = scope.locals.get(sym) { + return Some(info.clone()); + } + } + None + } + fn add_upvalue(&mut self, addr: Address) -> UpvalueIdx { if let Some(idx) = self.upvalues.iter().position(|&a| a == addr) { return UpvalueIdx(idx as u32); @@ -207,10 +216,16 @@ impl Binder { else_br, } => { let cond = self.bind(cond, diag); + + self.functions.last_mut().unwrap().push_scope(); let then_br = self.bind(then_br, diag); + self.functions.last_mut().unwrap().pop_scope(); + let mut else_br_bound = None; if let Some(e) = else_br { + self.functions.last_mut().unwrap().push_scope(); else_br_bound = Some(Rc::new(self.bind(e, diag))); + self.functions.last_mut().unwrap().pop_scope(); } self.make_node( @@ -384,10 +399,12 @@ impl Binder { } UntypedKind::Block { exprs } => { + self.functions.last_mut().unwrap().push_scope(); let mut bound_exprs = Vec::new(); for expr in exprs { bound_exprs.push(Rc::new(self.bind(expr, diag))); } + self.functions.last_mut().unwrap().pop_scope(); self.make_node( node.identity.clone(), BoundKind::Block { exprs: bound_exprs }, @@ -485,13 +502,13 @@ impl Binder { let current_fn_idx = self.functions.len() - 1; // 1. Try local in current function - if let Some(info) = self.functions[current_fn_idx].scope.resolve(sym) { + if let Some(info) = self.functions[current_fn_idx].resolve_local(sym) { return Some(Address::Local(info.slot)); } // 2. Try enclosing scopes (capture chain) for i in (0..current_fn_idx).rev() { - if let Some(info) = self.functions[i].scope.resolve(sym) { + if let Some(info) = self.functions[i].resolve_local(sym) { let mut addr = Address::Local(info.slot); // Record the capture for each lambda level in between