diff --git a/docs/Analysis_Environment.md b/docs/Analysis_Environment.md index df424df..7783581 100644 --- a/docs/Analysis_Environment.md +++ b/docs/Analysis_Environment.md @@ -13,7 +13,7 @@ Die `Environment`-Struktur fungiert im Projekt als zentraler "State-Manager" und ## 2. Prüfung auf Boilerplate Der Code weist an mehreren Stellen typischen Rust-Boilerplate für Single-Threaded-Interpreter auf: -* **Das `Rc`-Muster:** Um denselben globalen Zustand zwischen Parser, Compiler, Pipeline und VM zu teilen, bestehen 13 der 17 Felder aus `Rc>`. Das zwingt überall im Code zu redundantem `.borrow()`, `.borrow_mut()` und `.clone()` (z. B. beim Klonen in `RuntimeMacroEvaluator` über `get_expander`). +* **Das `Rc`-Muster:** Um denselben globalen Zustand zwischen Parser, Compiler, Pipeline und VM zu teilen, bestehen 14 der 19 Felder aus `Rc>`. Das zwingt überall im Code zu redundantem `.borrow()`, `.borrow_mut()` und `.clone()` (z. B. beim Klonen in `RuntimeMacroEvaluator` über `get_expander`). Ausnahme: `rtl_values: Rc<[Value]>` ist bewusst **ohne** `RefCell` — die eingefrorenen RTL-Werte sind nach dem Bootstrap-Freeze immutable und werden im VM-Hotpath direkt gelesen. * **Fehler-Mapping:** Beim Modulladen wird oft repetitiv mit `.map_err(|e: String| format!("...", e))` Boilerplate geschrieben, anstatt einen zentralen `Error`-Typen mit `thiserror` oder `anyhow` zu verwenden. * **Manuelle AST-Traversierung:** In `discover_globals` und `collect_doc_comments` wird der AST per Hand (mittels `match` und Rekursion) durchlaufen, anstatt ein zentrales "Visitor-Pattern" wiederzuverwenden. diff --git a/src/ast/environment.rs b/src/ast/environment.rs index 723f23f..582bb68 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -58,7 +58,11 @@ pub struct Rtl { pub struct Environment { pub root_types: Rc>>, pub root_purity: Rc>>, - pub root_values: Rc>>, + /// Frozen RTL value slice — shared across all environments from the same [`Rtl`]. + /// Empty placeholder during the bootstrap phase; set after the RTL freeze. + rtl_values: Rc<[Value]>, + /// Mutable user-defined global slots. Indexed as `[0..)` i.e. offset from `rtl.slot_count`. + user_values: Rc>>, pub fixed_scope_idx: i32, pub root_scopes: Rc>>, pub root_slot_count: Rc>, @@ -97,7 +101,7 @@ struct RuntimeMacroEvaluator { root_scopes: Rc>>, root_slot_count: Rc>, root_types: Rc>>, - root_values: Rc>>, + globals: GlobalStore, root_purity: Rc>>, fixed_scope_idx: i32, } @@ -129,8 +133,7 @@ impl MacroEvaluator for RuntimeMacroEvaluator { let exec_ast = Lowering::lower(Analyzer::analyze(&typed_ast, &self.root_purity.borrow())); // Minimal analysis for macro eval - let mut vm = VM::new(GlobalStore::new(self.root_values.clone(), 0)); - // rtl_len=0: MacroEvaluator runs during bootstrap or with flat root_values + let mut vm = VM::new(self.globals.clone()); vm.run(&exec_ast) } } @@ -148,7 +151,8 @@ impl Environment { let mut env = Self { root_types: Rc::new(RefCell::new(Vec::new())), root_purity: Rc::new(RefCell::new(Vec::new())), - root_values: Rc::new(RefCell::new(Vec::new())), + rtl_values: Rc::from([]), // placeholder — filled after freeze + 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_slot_count: Rc::new(RefCell::new(0)), @@ -179,7 +183,8 @@ impl Environment { // 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_vals: Rc<[Value]> = env.root_values.borrow().clone().into(); + // 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(); env.rtl = Rc::new(Rtl { scope: env.root_scopes.borrow()[0].clone(), slot_count: rtl_slot_count, @@ -189,12 +194,13 @@ impl Environment { rtl_docs: std::mem::take(&mut env.rtl_docs.borrow_mut()), }); - // Replace root_values with a fresh Rc. Any RTL closures registered during - // bootstrap (e.g. stream generators in streams.rs) still hold the OLD Rc, - // which is now a frozen, RTL-only view. Script-level VMs use this new Rc - // and grow it with user slots — streams cannot access user globals. - // This enforces the invariant: stream lambdas are RTL-only execution contexts. - env.root_values = Rc::new(RefCell::new(rtl_vals.to_vec())); + // Set the frozen RTL slice. Stream closures registered during bootstrap + // (e.g. in streams.rs) hold a GlobalStore that captured the OLD user_values + // Rc (bootstrap scratch = RTL values, never written again). After resetting + // user_values here, script VMs use the new empty Rc and grow it with user + // slots. Streams cannot access user globals — RTL-only invariant enforced. + env.rtl_values = Rc::clone(&rtl_vals); + env.user_values = Rc::new(RefCell::new(Vec::new())); // Push the first mutable user scope (Level 1) env.root_scopes.borrow_mut().push(CompilerScope::new()); @@ -223,7 +229,8 @@ impl Environment { let env = Self { root_types: Rc::new(RefCell::new(rtl.types.clone())), root_purity: Rc::new(RefCell::new(rtl.purity.clone())), - root_values: Rc::new(RefCell::new(rtl.values.to_vec())), + rtl_values: Rc::clone(&rtl.values), // O(1) Rc increment — no clone of RTL values! + user_values: Rc::new(RefCell::new(Vec::new())), fixed_scope_idx: 0, root_scopes: Rc::new(RefCell::new(scopes)), root_slot_count: Rc::new(RefCell::new(rtl.slot_count)), @@ -262,7 +269,11 @@ impl Environment { /// The RTL portion is a shared immutable slice; the user portion is a /// per-environment mutable vec. Both are reference-counted — cloning is cheap. pub fn global_store(&self) -> GlobalStore { - GlobalStore::new(Rc::clone(&self.root_values), self.rtl.slot_count as usize) + GlobalStore::new( + Rc::clone(&self.rtl_values), + Rc::clone(&self.user_values), + self.rtl.slot_count as usize, + ) } pub fn add_search_path(&self, path: impl AsRef) { @@ -292,7 +303,7 @@ impl Environment { root_scopes: self.root_scopes.clone(), root_slot_count: self.root_slot_count.clone(), root_types: self.root_types.clone(), - root_values: self.root_values.clone(), + globals: self.global_store(), root_purity: self.root_purity.clone(), fixed_scope_idx: self.fixed_scope_idx, }; @@ -442,12 +453,14 @@ impl Environment { let bound_ast = CapturePass::apply(bound_ast, &captures); - // Pre-allocate global slots to prevent out-of-bounds during specialization/optimization + // Pre-allocate user global slots to prevent out-of-bounds during specialization/optimization. + // root_slot_count includes RTL slots; subtract to get the number of user-only slots. { - let mut values = self.root_values.borrow_mut(); - let count = *self.root_slot_count.borrow(); - if (count as usize) > values.len() { - values.resize(count as usize, Value::Void); + let mut user = self.user_values.borrow_mut(); + let count = *self.root_slot_count.borrow() as usize; + let user_count = count.saturating_sub(self.rtl.slot_count as usize); + if user_count > user.len() { + user.resize(user_count, Value::Void); } } @@ -511,7 +524,7 @@ impl Environment { *slot_count += 1; self.root_types.borrow_mut().push(ty); self.root_purity.borrow_mut().push(purity); - self.root_values.borrow_mut().push(val); + self.user_values.borrow_mut().push(val); } pub fn register_native( diff --git a/src/ast/rtl/streams.rs b/src/ast/rtl/streams.rs index 53a806d..06baf2e 100644 --- a/src/ast/rtl/streams.rs +++ b/src/ast/rtl/streams.rs @@ -1,6 +1,6 @@ use crate::ast::rtl::series::{RingBuffer, ScalarValue, SeriesMember}; use crate::ast::types::{Object, PipeFn, Value}; -use crate::ast::vm::{GlobalStore, VM}; +use crate::ast::vm::VM; use std::cell::RefCell; use std::rc::Rc; @@ -473,10 +473,10 @@ pub fn register(env: &Environment) { // (create-ticker condition-closure) -> StreamNode let ticker_generators = env.pipeline_generators.clone(); - // Captures root_values at registration time (during bootstrap = RTL-only). - // After Environment::new() replaces root_values, this Rc becomes a frozen - // RTL-only view — enforcing that stream lambdas cannot access user globals. - let globals = GlobalStore::new(env.root_values.clone(), 0); + // Captures global_store() during bootstrap (user_values = RTL scratch, rtl_len = 0). + // After Environment::new() resets user_values, this GlobalStore holds the OLD + // bootstrap Rc (RTL values only, frozen). Stream lambdas run in an RTL-only context. + let globals = env.global_store(); env.register_native_fn( "create-ticker", @@ -536,8 +536,8 @@ pub fn register(env: &Environment) { // (pipe inputs lambda) -> StreamNode // inputs: a Tuple of StreamNodes or a single StreamNode // lambda: a Closure or Function - // Same RTL-only capture as create-ticker above. - let globals = GlobalStore::new(env.root_values.clone(), 0); + // Same RTL-only bootstrap capture as create-ticker above. + let globals = env.global_store(); env.register_native_fn( "pipe", StaticType::PolymorphicFn { diff --git a/src/ast/vm.rs b/src/ast/vm.rs index 92361d9..0bc7993 100644 --- a/src/ast/vm.rs +++ b/src/ast/vm.rs @@ -84,35 +84,47 @@ impl VMObserver for TracingObserver { /// Unified view of the global value space for VM and optimizer access. /// -/// Wraps a flat `Vec` containing both RTL slots `[0..rtl_len)` and user -/// slots `[rtl_len..)`. The `rtl_len` boundary is informational — it enables -/// a debug-mode write guard and prepares for future RTL sharing. +/// Splits the global address space into two regions: +/// - RTL slots `[0..rtl_len)`: immutable `Rc<[Value]>`, shared across all environments +/// created from the same [`Rtl`](crate::ast::environment::Rtl) snapshot. No `RefCell` needed. +/// - User slots `[rtl_len..)`: mutable `Rc>>`, per-environment. /// -/// True RTL/user splitting (sharing the immutable RTL portion across environments) -/// is a planned future optimization once the stream-bootstrap timing is resolved. +/// During the RTL bootstrap phase `rtl_len` is 0 and both RTL and user values live in the +/// `user` vec. After the freeze, `rtl` holds the immutable snapshot and `user` starts +/// empty and grows as the script defines new globals. +/// +/// Stream closures capture a `GlobalStore` during bootstrap (before the freeze), so their +/// `rtl_len` remains 0 and they read all values from `user`. This enforces the invariant +/// that stream lambdas run in an RTL-only execution context. #[derive(Clone)] pub struct GlobalStore { - values: Rc>>, - /// Index boundary between frozen RTL slots and mutable user slots. + rtl: Rc<[Value]>, + user: Rc>>, + /// Index boundary: slots `[0..rtl_len)` are served from `rtl`, the rest from `user`. pub rtl_len: usize, } impl GlobalStore { - pub fn new(values: Rc>>, rtl_len: usize) -> Self { - Self { values, rtl_len } + pub fn new(rtl: Rc<[Value]>, user: Rc>>, rtl_len: usize) -> Self { + Self { rtl, user, rtl_len } } pub fn get(&self, idx: usize) -> Option { - self.values.borrow().get(idx).cloned() + if idx < self.rtl_len { + self.rtl.get(idx).cloned() + } else { + self.user.borrow().get(idx - self.rtl_len).cloned() + } } fn set(&self, idx: usize, value: Value) { debug_assert!(idx >= self.rtl_len, "Cannot write to immutable RTL slot {}", idx); - let mut v = self.values.borrow_mut(); - if idx >= v.len() { - v.resize(idx + 1, Value::Void); + let u = idx - self.rtl_len; + let mut v = self.user.borrow_mut(); + if u >= v.len() { + v.resize(u + 1, Value::Void); } - v[idx] = value; + v[u] = value; } }