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.
This commit is contained in:
Michael Schimmel
2026-03-12 11:22:40 +01:00
parent bb77caf1af
commit be2bef373f
4 changed files with 53 additions and 40 deletions
+40 -34
View File
@@ -31,28 +31,24 @@ impl CompilerScope {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ScopeKind {
Root,
Local,
}
struct FunctionCompiler {
identity: Identity,
scopes: Vec<CompilerScope>,
slot_count: u32,
upvalues: Vec<Address>,
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<RefCell<HashMap<Symbol, (GlobalIdx, Identity)>>>,
) -> Result<Address, String> {
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<RefCell<HashMap<Symbol, (GlobalIdx, Identity)>>>) -> Self {
pub fn new(
globals: Rc<RefCell<HashMap<Symbol, (GlobalIdx, Identity)>>>,
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<RefCell<HashMap<Symbol, (GlobalIdx, Identity)>>>,
fixed_scope_idx: i32,
node: &Node<UntypedKind>,
diagnostics: &mut Diagnostics,
) -> Result<(BoundNode, HashMap<Identity, Vec<Identity>>), 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")));
}
}
+3 -3
View File
@@ -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: {:?}",
+1 -1
View File
@@ -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"
);
}
+9 -2
View File
@@ -75,6 +75,7 @@ pub struct Environment {
pub global_types: Rc<RefCell<HashMap<GlobalIdx, StaticType>>>,
pub global_purity: Rc<RefCell<HashMap<GlobalIdx, Purity>>>,
pub global_values: Rc<RefCell<Vec<Value>>>,
pub fixed_scope_idx: i32,
pub function_registry: Rc<RefCell<GlobalFunctionRegistry>>,
pub typed_function_registry: Rc<RefCell<GlobalAnalyzedRegistry>>,
pub monomorph_cache: Rc<RefCell<MonoCache>>,
@@ -112,6 +113,7 @@ struct RuntimeMacroEvaluator {
global_names: Rc<RefCell<HashMap<Symbol, (GlobalIdx, Identity)>>>,
global_types: Rc<RefCell<HashMap<GlobalIdx, StaticType>>>,
global_values: Rc<RefCell<Vec<Value>>>,
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);