Refactor Binder and Environment, remove fixed_scope_idx

The `fixed_scope_idx` field has been removed from `Binder` and
`Environment`. This field was used to enforce immutability on certain
scopes during binding.

The logic for handling the immutability of the root scope (scope 0) has
been moved and refactored. Now, during the `Environment::new` bootstrap
phase, the addresses within the RTL scope (scope 0) are directly
translated from `Address::Local` to `Address::Global`. This ensures that
the Binder inherently treats these as global and immutable without
needing an explicit `fixed_scope_idx` check.

The `Binder::bind_root` signature has been updated to reflect this
change by removing the `fixed_scope_idx` parameter. Consequently, tests
and other usages of `bind_root` have been adjusted.

This change simplifies the binding process by centralizing the
immutability handling to the environment setup phase, making the
Binder's logic cleaner.
This commit is contained in:
2026-03-31 11:19:56 +02:00
parent 2cf48566f7
commit 35f5ea0db3
5 changed files with 31 additions and 74 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ Die `Environment`-Struktur fungiert im Projekt als zentraler "State-Manager" und
* **Modulladung & Abhängigkeiten:** `preload_dependencies` und `discover_globals` lesen `#use`-Abhängigkeiten, durchsuchen den Code vorab nach globalen Definitionen (`def`) und Makros und laden die Standardbibliothek (`prelude.myc`). * **Modulladung & Abhängigkeiten:** `preload_dependencies` und `discover_globals` lesen `#use`-Abhängigkeiten, durchsuchen den Code vorab nach globalen Definitionen (`def`) und Makros und laden die Standardbibliothek (`prelude.myc`).
* **Kompilierungs-Pipeline:** Die Methoden `compile`, `compile_syntax`, `compile_pipeline` und `link` steuern den Code durch alle Compiler-Phasen: Macro-Expansion -> Binding -> Type-Checking -> Analysis -> Specialization -> Optimization -> Lowering. * **Kompilierungs-Pipeline:** Die Methoden `compile`, `compile_syntax`, `compile_pipeline` und `link` steuern den Code durch alle Compiler-Phasen: Macro-Expansion -> Binding -> Type-Checking -> Analysis -> Specialization -> Optimization -> Lowering.
* **Makro-Evaluierung:** Die interne Struktur `RuntimeMacroEvaluator` wird genutzt, um AST-Knoten zur Compile-Zeit an eine VM zu übergeben und den Code für Makros auszuführen. * **Makro-Evaluierung:** Die interne Struktur `RuntimeMacroEvaluator` wird genutzt, um AST-Knoten zur Compile-Zeit an eine VM zu übergeben und den Code für Makros auszuführen.
* **Laufzeit-Ausführung & RTL (Runtime Library):** Methoden wie `run_script`, `run_debug` und `instantiate` starten die `VM`. Zudem gibt es Methoden (`register_native`, `allocate_slot`), um native Rust-Funktionen (Intrinsics) im globalen Scope (`fixed_scope_idx = 0`) zu registrieren. * **Laufzeit-Ausführung & RTL (Runtime Library):** Methoden wie `run_script`, `run_debug` und `instantiate` starten die `VM`. Zudem gibt es Methoden (`register_native`, `allocate_slot`), um native Rust-Funktionen (Intrinsics) im globalen RTL-Scope (Scope 0) zu registrieren.
* **Dokumentations-Registry:** Es speichert sowohl RTL-Dokumentation als auch aus dem Source-Code extrahierte Kommentare (`myc_docs`). * **Dokumentations-Registry:** Es speichert sowohl RTL-Dokumentation als auch aus dem Source-Code extrahierte Kommentare (`myc_docs`).
## 2. Prüfung auf Boilerplate ## 2. Prüfung auf Boilerplate
+6 -54
View File
@@ -1,5 +1,5 @@
use crate::ast::nodes::{ use crate::ast::nodes::{
Address, AssignBinding, BoundPhase, DeclarationKind, DefBinding, GlobalIdx, Address, AssignBinding, BoundPhase, DeclarationKind, DefBinding,
IdentifierBinding, LambdaBinding, Node, NodeKind, SyntaxKind, SyntaxNode, Symbol, IdentifierBinding, LambdaBinding, Node, NodeKind, SyntaxKind, SyntaxNode, Symbol,
UpvalueIdx, VirtualId, UpvalueIdx, VirtualId,
}; };
@@ -124,19 +124,16 @@ pub type BindingResult = (
pub struct Binder { pub struct Binder {
functions: Vec<FunctionCompiler>, functions: Vec<FunctionCompiler>,
capture_map: HashMap<Identity, std::collections::HashSet<Identity>>, capture_map: HashMap<Identity, std::collections::HashSet<Identity>>,
fixed_scope_idx: i32,
} }
impl Binder { impl Binder {
pub fn new( pub fn new(
initial_scopes: Vec<CompilerScope>, initial_scopes: Vec<CompilerScope>,
initial_slot_count: u32, initial_slot_count: u32,
fixed_scope_idx: i32,
) -> Self { ) -> Self {
let mut binder = Self { let mut binder = Self {
functions: Vec::new(), functions: Vec::new(),
capture_map: HashMap::new(), capture_map: HashMap::new(),
fixed_scope_idx,
}; };
binder.functions.push(FunctionCompiler::new( binder.functions.push(FunctionCompiler::new(
NodeIdentity::new(SourceLocation { NodeIdentity::new(SourceLocation {
@@ -152,11 +149,10 @@ impl Binder {
pub fn bind_root( pub fn bind_root(
initial_scopes: Vec<CompilerScope>, initial_scopes: Vec<CompilerScope>,
initial_slot_count: u32, initial_slot_count: u32,
fixed_scope_idx: i32,
node: &SyntaxNode, node: &SyntaxNode,
diagnostics: &mut Diagnostics, diagnostics: &mut Diagnostics,
) -> Result<BindingResult, String> { ) -> Result<BindingResult, String> {
let mut binder = Self::new(initial_scopes, initial_slot_count, fixed_scope_idx); let mut binder = Self::new(initial_scopes, initial_slot_count);
let bound = binder.bind(node, ExprContext::Expression, diagnostics); let bound = binder.bind(node, ExprContext::Expression, diagnostics);
let final_captures = binder let final_captures = binder
@@ -181,22 +177,6 @@ impl Binder {
_kind: DeclarationKind, _kind: DeclarationKind,
diag: &mut Diagnostics, diag: &mut Diagnostics,
) -> Option<Address<VirtualId>> { ) -> Option<Address<VirtualId>> {
let current_fn_idx = self.functions.len() - 1;
if current_fn_idx == 0 {
let current_scope_idx = self.functions[0].scopes.len() as i32 - 1;
if current_scope_idx <= self.fixed_scope_idx {
diag.push_error(
format!(
"Cannot define '{}': Scope {} is frozen/immutable.",
name.name, current_scope_idx
),
None,
);
return None;
}
}
let current_fn = self.functions.last_mut().unwrap(); let current_fn = self.functions.last_mut().unwrap();
match current_fn.define_variable(name, identity) { match current_fn.define_variable(name, identity) {
Ok(addr) => Some(addr), Ok(addr) => Some(addr),
@@ -562,25 +542,13 @@ impl Binder {
// 2. Try enclosing scopes (capture chain) // 2. Try enclosing scopes (capture chain)
for i in (0..current_fn_idx).rev() { for i in (0..current_fn_idx).rev() {
if let Some((info, scope_idx)) = self.functions[i].resolve_local(sym) { if let Some((info, _scope_idx)) = self.functions[i].resolve_local(sym) {
// If it's in the root script and within a frozen scope, translate to Global
let is_frozen_root = i == 0 && (scope_idx as i32) <= self.fixed_scope_idx;
if let Address::Global(_) = info.addr { if let Address::Global(_) = info.addr {
return Some(info.addr); return Some(info.addr);
} }
let mut addr = info.addr; let mut addr = info.addr;
if is_frozen_root
&& let Address::Local(slot) = addr {
addr = Address::Global(GlobalIdx(slot.0));
}
if let Address::Global(_) = addr {
return Some(addr);
}
// Record the capture for each lambda level in between // Record the capture for each lambda level in between
for k in (i + 1)..=current_fn_idx { for k in (i + 1)..=current_fn_idx {
let lambda_id = self.functions[k].identity.clone(); let lambda_id = self.functions[k].identity.clone();
@@ -723,7 +691,7 @@ mod tests {
let syntax = parser.parse_expression(); let syntax = parser.parse_expression();
let mut diagnostics = Diagnostics::new(); let mut diagnostics = Diagnostics::new();
let (bound, captures, _scopes, _slots) = Binder::bind_root(vec![], 0, 0, &syntax, &mut diagnostics).unwrap(); let (bound, captures, _scopes, _slots) = Binder::bind_root(vec![], 0, &syntax, &mut diagnostics).unwrap();
if let NodeKind::Lambda { body, .. } = &bound.kind { if let NodeKind::Lambda { body, .. } = &bound.kind {
if let NodeKind::Block { exprs } = &body.kind { if let NodeKind::Block { exprs } = &body.kind {
@@ -756,7 +724,7 @@ mod tests {
let syntax = parser.parse_expression(); let syntax = parser.parse_expression();
let mut diagnostics = Diagnostics::new(); let mut diagnostics = Diagnostics::new();
let (bound, captures, _scopes, _slots) = Binder::bind_root(vec![], 0, 0, &syntax, &mut diagnostics).unwrap(); let (bound, captures, _scopes, _slots) = Binder::bind_root(vec![], 0, &syntax, &mut diagnostics).unwrap();
if let NodeKind::Lambda { body, .. } = &bound.kind { if let NodeKind::Lambda { body, .. } = &bound.kind {
if let NodeKind::Block { exprs } = &body.kind { if let NodeKind::Block { exprs } = &body.kind {
@@ -789,26 +757,10 @@ mod tests {
let syntax = parser.parse_expression(); let syntax = parser.parse_expression();
let mut diagnostics = Diagnostics::new(); let mut diagnostics = Diagnostics::new();
let _ = Binder::bind_root(vec![], 0, 0, &syntax, &mut diagnostics); let _ = Binder::bind_root(vec![], 0, &syntax, &mut diagnostics);
assert!(diagnostics.has_errors()); assert!(diagnostics.has_errors());
assert!(diagnostics.items.iter().any(|i| i.message.contains("already defined"))); assert!(diagnostics.items.iter().any(|i| i.message.contains("already defined")));
} }
#[test]
fn test_repro_global_redefinition() {
let source1 = "(def x 1) 1";
let syntax1 = Parser::new(source1).parse_expression();
let mut diagnostics = Diagnostics::new();
let (_, _, scopes1, slots1) = Binder::bind_root(vec![], 0, -1, &syntax1, &mut diagnostics).unwrap();
let source2 = "(def x 2) 2";
let syntax2 = Parser::new(source2).parse_expression();
let mut diagnostics2 = Diagnostics::new();
// Here we simulate frozen scope by passing fixed_scope_idx = 0
let _ = Binder::bind_root(scopes1, slots1, 0, &syntax2, &mut diagnostics2);
assert!(diagnostics2.has_errors());
assert!(diagnostics2.items.iter().any(|i| i.message.contains("frozen/immutable")));
}
} }
+8 -8
View File
@@ -633,7 +633,7 @@ mod tests {
use crate::ast::compiler::binder::{CompilerScope, LocalInfo}; use crate::ast::compiler::binder::{CompilerScope, LocalInfo};
use crate::ast::compiler::Binder; use crate::ast::compiler::Binder;
use crate::ast::diagnostics::Diagnostics; use crate::ast::diagnostics::Diagnostics;
use crate::ast::nodes::{Address, VirtualId}; use crate::ast::nodes::{Address, GlobalIdx, VirtualId};
use crate::ast::parser::Parser; use crate::ast::parser::Parser;
use crate::ast::types::{NodeIdentity, Purity, SourceLocation, StaticType, Value}; use crate::ast::types::{NodeIdentity, Purity, SourceLocation, StaticType, Value};
@@ -778,7 +778,7 @@ mod tests {
locals.insert( locals.insert(
Symbol::from("*"), Symbol::from("*"),
LocalInfo { LocalInfo {
addr: Address::Local(VirtualId(0)), addr: Address::Global(GlobalIdx(0)),
identity: NodeIdentity::new(SourceLocation { identity: NodeIdentity::new(SourceLocation {
line: 0, line: 0,
col: 0, col: 0,
@@ -790,8 +790,8 @@ mod tests {
let initial_scopes = vec![CompilerScope { locals }]; let initial_scopes = vec![CompilerScope { locals }];
let mut diag = Diagnostics::new(); let mut diag = Diagnostics::new();
// fixed_scope_idx = 0 means scope 0 is frozen (Global) // Scope 0 simulates a frozen RTL scope with Global addresses
let result = Binder::bind_root(initial_scopes, 1, 0, &expanded, &mut diag); let result = Binder::bind_root(initial_scopes, 1, &expanded, &mut diag);
assert!( assert!(
result.is_ok(), result.is_ok(),
"Should find global '*' Error: {:?}", "Should find global '*' Error: {:?}",
@@ -815,7 +815,7 @@ mod tests {
let expanded = expander.expand(syntax).unwrap(); let expanded = expander.expand(syntax).unwrap();
let mut diag = Diagnostics::new(); let mut diag = Diagnostics::new();
let result = Binder::bind_root(vec![], 0, 0, &expanded, &mut diag); let result = Binder::bind_root(vec![], 0, &expanded, &mut diag);
assert!(result.is_ok(), "Hygiene failed: {:?}", result.err()); assert!(result.is_ok(), "Hygiene failed: {:?}", result.err());
} }
@@ -835,7 +835,7 @@ mod tests {
let expanded = expander.expand(syntax).unwrap(); let expanded = expander.expand(syntax).unwrap();
let mut diag = Diagnostics::new(); let mut diag = Diagnostics::new();
let result = Binder::bind_root(vec![], 0, 0, &expanded, &mut diag); let result = Binder::bind_root(vec![], 0, &expanded, &mut diag);
assert!( assert!(
result.is_ok(), result.is_ok(),
"Explicit Hygiene with Backticks failed: {:?}", "Explicit Hygiene with Backticks failed: {:?}",
@@ -861,7 +861,7 @@ mod tests {
let expanded = expander.expand(syntax).unwrap(); let expanded = expander.expand(syntax).unwrap();
let mut diag = Diagnostics::new(); let mut diag = Diagnostics::new();
let result = Binder::bind_root(vec![], 0, 0, &expanded, &mut diag); let result = Binder::bind_root(vec![], 0, &expanded, &mut diag);
assert!( assert!(
result.is_ok(), result.is_ok(),
"~nonparam should resolve to call-site variable: {:?}", "~nonparam should resolve to call-site variable: {:?}",
@@ -887,7 +887,7 @@ mod tests {
let expanded = expander.expand(syntax).unwrap(); let expanded = expander.expand(syntax).unwrap();
let mut diag = Diagnostics::new(); let mut diag = Diagnostics::new();
let result = Binder::bind_root(vec![], 0, 0, &expanded, &mut diag); let result = Binder::bind_root(vec![], 0, &expanded, &mut diag);
assert!( assert!(
result.is_ok(), result.is_ok(),
"Hygiene isolation for def should still hold: {:?}", "Hygiene isolation for def should still hold: {:?}",
+15 -10
View File
@@ -67,7 +67,6 @@ pub struct Environment {
rtl_values: Rc<[Value]>, rtl_values: Rc<[Value]>,
/// Mutable user-defined global slots. Indexed as `[0..)` i.e. offset from `rtl.slot_count`. /// Mutable user-defined global slots. Indexed as `[0..)` i.e. offset from `rtl.slot_count`.
user_values: Rc<RefCell<Vec<Value>>>, user_values: Rc<RefCell<Vec<Value>>>,
pub fixed_scope_idx: i32,
pub root_scopes: Rc<RefCell<Vec<CompilerScope>>>, pub root_scopes: Rc<RefCell<Vec<CompilerScope>>>,
pub root_slot_count: Rc<RefCell<u32>>, pub root_slot_count: Rc<RefCell<u32>>,
pub function_registry: Rc<RefCell<GlobalFunctionRegistry>>, pub function_registry: Rc<RefCell<GlobalFunctionRegistry>>,
@@ -110,7 +109,6 @@ struct RuntimeMacroEvaluator {
root_types: Rc<RefCell<Vec<StaticType>>>, root_types: Rc<RefCell<Vec<StaticType>>>,
globals: GlobalStore, globals: GlobalStore,
root_purity: Rc<RefCell<Vec<Purity>>>, root_purity: Rc<RefCell<Vec<Purity>>>,
fixed_scope_idx: i32,
compiler_hooks: Rc<HashMap<u32, Rc<dyn RtlCompilerHook>>>, compiler_hooks: Rc<HashMap<u32, Rc<dyn RtlCompilerHook>>>,
} }
@@ -130,7 +128,7 @@ impl MacroEvaluator for RuntimeMacroEvaluator {
let initial_scopes = self.root_scopes.borrow().clone(); let initial_scopes = self.root_scopes.borrow().clone();
let initial_slot_count = *self.root_slot_count.borrow(); let initial_slot_count = *self.root_slot_count.borrow();
let (bound_ast, captures, _, _) = Binder::bind_root(initial_scopes, initial_slot_count, self.fixed_scope_idx, node, &mut diag)?; let (bound_ast, captures, _, _) = Binder::bind_root(initial_scopes, initial_slot_count, node, &mut diag)?;
let bound_ast = CapturePass::apply(bound_ast, &captures); let bound_ast = CapturePass::apply(bound_ast, &captures);
let checker = TypeChecker::new(self.root_types.clone(), Rc::clone(&self.compiler_hooks)); let checker = TypeChecker::new(self.root_types.clone(), Rc::clone(&self.compiler_hooks));
let typed_ast = checker.check(&bound_ast, &[], &mut diag); let typed_ast = checker.check(&bound_ast, &[], &mut diag);
@@ -154,14 +152,12 @@ impl Default for Environment {
impl Environment { impl Environment {
pub fn new() -> Self { pub fn new() -> Self {
// Phase 1: Bootstrap — fixed_scope_idx = -1 allows allocate_slot to // Phase 1: Bootstrap — allocate_slot writes freely into scope 0 (the RTL scope).
// write freely into scope 0 (the RTL scope).
let mut env = Self { let mut env = Self {
root_types: Rc::new(RefCell::new(Vec::new())), root_types: Rc::new(RefCell::new(Vec::new())),
root_purity: Rc::new(RefCell::new(Vec::new())), root_purity: Rc::new(RefCell::new(Vec::new())),
rtl_values: Rc::from([]), // placeholder — filled after freeze rtl_values: Rc::from([]), // placeholder — filled after freeze
user_values: Rc::new(RefCell::new(Vec::new())), // bootstrap scratch during RTL phase user_values: Rc::new(RefCell::new(Vec::new())), // bootstrap scratch during RTL phase
fixed_scope_idx: -1,
root_scopes: Rc::new(RefCell::new(vec![CompilerScope::new()])), root_scopes: Rc::new(RefCell::new(vec![CompilerScope::new()])),
root_slot_count: Rc::new(RefCell::new(0)), root_slot_count: Rc::new(RefCell::new(0)),
function_registry: Rc::new(RefCell::new(HashMap::new())), function_registry: Rc::new(RefCell::new(HashMap::new())),
@@ -191,11 +187,22 @@ impl Environment {
rtl::register(&env); rtl::register(&env);
// Phase 2: Freeze scope 0 and snapshot the RTL state. // Phase 2: Freeze scope 0 and snapshot the RTL state.
env.fixed_scope_idx = 0;
let rtl_slot_count = *env.root_slot_count.borrow(); let rtl_slot_count = *env.root_slot_count.borrow();
// user_values was used as the bootstrap scratch; freeze it into an immutable slice. // user_values was used as the bootstrap scratch; freeze it into an immutable slice.
let rtl_vals: Rc<[Value]> = env.user_values.borrow().clone().into(); let rtl_vals: Rc<[Value]> = env.user_values.borrow().clone().into();
let frozen_hooks = Rc::new(std::mem::take(&mut *env.compiler_hooks.borrow_mut())); let frozen_hooks = Rc::new(std::mem::take(&mut *env.compiler_hooks.borrow_mut()));
// Rewrite RTL scope addresses: Local(VirtualId) -> Global(GlobalIdx)
// so the Binder never needs to do this translation at resolve time.
{
let mut scopes = env.root_scopes.borrow_mut();
for info in scopes[0].locals.values_mut() {
if let Address::Local(vid) = info.addr {
info.addr = Address::Global(GlobalIdx(vid.0));
}
}
}
env.rtl = Rc::new(Rtl { env.rtl = Rc::new(Rtl {
scope: env.root_scopes.borrow()[0].clone(), scope: env.root_scopes.borrow()[0].clone(),
slot_count: rtl_slot_count, slot_count: rtl_slot_count,
@@ -243,7 +250,6 @@ impl Environment {
root_purity: Rc::new(RefCell::new(rtl.purity.clone())), root_purity: Rc::new(RefCell::new(rtl.purity.clone())),
rtl_values: Rc::clone(&rtl.values), // O(1) Rc increment — no clone of RTL values! rtl_values: Rc::clone(&rtl.values), // O(1) Rc increment — no clone of RTL values!
user_values: Rc::new(RefCell::new(Vec::new())), user_values: Rc::new(RefCell::new(Vec::new())),
fixed_scope_idx: 0,
root_scopes: Rc::new(RefCell::new(scopes)), root_scopes: Rc::new(RefCell::new(scopes)),
root_slot_count: Rc::new(RefCell::new(rtl.slot_count)), root_slot_count: Rc::new(RefCell::new(rtl.slot_count)),
function_registry: Rc::new(RefCell::new(HashMap::new())), function_registry: Rc::new(RefCell::new(HashMap::new())),
@@ -318,7 +324,6 @@ impl Environment {
root_types: self.root_types.clone(), root_types: self.root_types.clone(),
globals: self.global_store(), globals: self.global_store(),
root_purity: self.root_purity.clone(), root_purity: self.root_purity.clone(),
fixed_scope_idx: self.fixed_scope_idx,
compiler_hooks: Rc::clone(&self.rtl.compiler_hooks), compiler_hooks: Rc::clone(&self.rtl.compiler_hooks),
}; };
MacroExpander::new(self.macro_registry.borrow().clone(), evaluator) MacroExpander::new(self.macro_registry.borrow().clone(), evaluator)
@@ -453,7 +458,7 @@ impl Environment {
let initial_slot_count = *self.root_slot_count.borrow(); let initial_slot_count = *self.root_slot_count.borrow();
let (bound_ast, captures, final_scopes, final_slot_count) = let (bound_ast, captures, final_scopes, final_slot_count) =
match Binder::bind_root(initial_scopes, initial_slot_count, self.fixed_scope_idx, &expanded_ast, diagnostics) { match Binder::bind_root(initial_scopes, initial_slot_count, &expanded_ast, diagnostics) {
Ok(res) => res, Ok(res) => res,
Err(e) => { Err(e) => {
diagnostics.push_error(e, None); diagnostics.push_error(e, None);
+1 -1
View File
@@ -2,7 +2,7 @@
;; Standard macros and core utilities. ;; Standard macros and core utilities.
(do (do
(def func1 (print "Hello World")) (def func1 (fn [] (print "Hello World")))
(macro while [cond body] (macro while [cond body]
`((fn [] (if ~cond `((fn [] (if ~cond