diff --git a/src/ast/compiler/optimizer/engine.rs b/src/ast/compiler/optimizer/engine.rs index c65a5a8..a8320d6 100644 --- a/src/ast/compiler/optimizer/engine.rs +++ b/src/ast/compiler/optimizer/engine.rs @@ -3,6 +3,7 @@ use crate::ast::nodes::{ IdentifierBinding, LambdaBinding, Node, NodeKind, UpvalueIdx, VirtualId, }; use crate::ast::types::{Purity, Value}; +use crate::ast::vm::GlobalStore; use std::cell::RefCell; use std::rc::Rc; @@ -15,7 +16,7 @@ use super::utils::{collect_pattern_addrs, PathTracker, UsageInfo}; pub struct Optimizer { pub enabled: bool, max_passes: usize, - pub globals: Option>>>, + pub globals: Option, pub root_purity: Option>>>, pub lambda_registry: Option>>, } @@ -31,7 +32,7 @@ impl Optimizer { } } - pub fn with_globals(mut self, globals: Rc>>) -> Self { + pub fn with_globals(mut self, globals: GlobalStore) -> Self { self.globals = Some(globals); self } @@ -127,14 +128,11 @@ impl Optimizer { // 3. Fallback for Globals: check the actual VM environment if let Address::Global(idx) = addr - && let Some(globals_rc) = &self.globals + && let Some(globals_store) = &self.globals + && let Some(val) = globals_store.get(idx.0 as usize) + && inliner.is_inlinable_value(&val, addr) { - let globals = globals_rc.borrow(); - if let Some(val) = globals.get(idx.0 as usize) - && inliner.is_inlinable_value(val, addr) - { - return Rc::new(folder.make_constant_node(val.clone(), node)); - } + return Rc::new(folder.make_constant_node(val, node)); } } diff --git a/src/ast/compiler/optimizer/folder.rs b/src/ast/compiler/optimizer/folder.rs index 6672dc7..f1296cd 100644 --- a/src/ast/compiler/optimizer/folder.rs +++ b/src/ast/compiler/optimizer/folder.rs @@ -2,15 +2,15 @@ use crate::ast::nodes::{ Address, AnalyzedNode, IdentifierBinding, Node, NodeKind, NodeMetrics, }; use crate::ast::types::{Purity, RecordLayout, StaticType, Value}; -use std::cell::RefCell; +use crate::ast::vm::GlobalStore; use std::rc::Rc; pub struct Folder<'a> { - pub globals: &'a Option>>>, + pub globals: &'a Option, } impl<'a> Folder<'a> { - pub fn new(globals: &'a Option>>>) -> Self { + pub fn new(globals: &'a Option) -> Self { Self { globals } } @@ -94,7 +94,7 @@ impl<'a> Folder<'a> { NodeKind::Identifier { binding: IdentifierBinding::Reference(Address::Global(idx)), .. - } => self.globals.as_ref()?.borrow().get(idx.0 as usize)?.clone(), + } => self.globals.as_ref()?.get(idx.0 as usize)?, NodeKind::Constant(val) => val.clone(), _ => return None, }; diff --git a/src/ast/compiler/optimizer/inliner.rs b/src/ast/compiler/optimizer/inliner.rs index 6ba1897..e22539e 100644 --- a/src/ast/compiler/optimizer/inliner.rs +++ b/src/ast/compiler/optimizer/inliner.rs @@ -2,6 +2,7 @@ use crate::ast::nodes::{ Address, AnalyzedNode, IdentifierBinding, NodeKind, VirtualId, }; use crate::ast::types::{Purity, Value}; +use crate::ast::vm::GlobalStore; use std::cell::RefCell; use std::collections::HashSet; @@ -11,13 +12,13 @@ use super::substitution_map::SubstitutionMap; use super::utils::UsageInfo; pub struct Inliner<'a> { - pub globals: &'a Option>>>, + pub globals: &'a Option, pub root_purity: &'a Option>>>, } impl<'a> Inliner<'a> { pub fn new( - globals: &'a Option>>>, + globals: &'a Option, root_purity: &'a Option>>>, ) -> Self { Self { diff --git a/src/ast/environment.rs b/src/ast/environment.rs index 0fd7681..723f23f 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -4,7 +4,7 @@ use crate::ast::compiler::{CapturePass, TypeChecker, TypedNode}; use crate::ast::nodes::{AnalyzedPhase, Symbol, SyntaxKind, SyntaxNode}; use crate::ast::parser::Parser; use crate::ast::closure::Closure; -use crate::ast::vm::{TracingObserver, VM}; +use crate::ast::vm::{GlobalStore, TracingObserver, VM}; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; @@ -51,7 +51,7 @@ pub struct Rtl { pub slot_count: u32, pub types: Vec, pub purity: Vec, - pub values: Vec, + pub values: Rc<[Value]>, pub rtl_docs: Vec, } @@ -129,7 +129,8 @@ 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(self.root_values.clone()); + let mut vm = VM::new(GlobalStore::new(self.root_values.clone(), 0)); + // rtl_len=0: MacroEvaluator runs during bootstrap or with flat root_values vm.run(&exec_ast) } } @@ -168,7 +169,7 @@ impl Environment { slot_count: 0, types: vec![], purity: vec![], - values: vec![], + values: Rc::from([]), rtl_docs: vec![], }), }; @@ -178,15 +179,23 @@ 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(); env.rtl = Rc::new(Rtl { scope: env.root_scopes.borrow()[0].clone(), slot_count: rtl_slot_count, types: env.root_types.borrow().clone(), purity: env.root_purity.borrow().clone(), - values: env.root_values.borrow().clone(), + values: Rc::clone(&rtl_vals), 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())); + // Push the first mutable user scope (Level 1) env.root_scopes.borrow_mut().push(CompilerScope::new()); @@ -214,7 +223,7 @@ 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.clone())), + root_values: Rc::new(RefCell::new(rtl.values.to_vec())), fixed_scope_idx: 0, root_scopes: Rc::new(RefCell::new(scopes)), root_slot_count: Rc::new(RefCell::new(rtl.slot_count)), @@ -248,6 +257,14 @@ impl Environment { Rc::clone(&self.rtl) } + /// Returns a [`GlobalStore`] view over this environment's global value space. + /// + /// 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) + } + pub fn add_search_path(&self, path: impl AsRef) { self.search_paths.borrow_mut().push(path.as_ref().to_path_buf()); } @@ -707,7 +724,7 @@ impl Environment { // 4. Optimize let optimizer = Optimizer::new(self.optimization) - .with_globals(self.root_values.clone()) + .with_globals(self.global_store()) .with_purity(self.root_purity.clone()) .with_registry(self.typed_function_registry.clone()); let optimized = optimizer.optimize(specialized); @@ -744,7 +761,7 @@ impl Environment { } // Slow path: run the node once to extract the resulting Closure. - let mut vm = VM::new(self.root_values.clone()); + let mut vm = VM::new(self.global_store()); let res = vm.run(&node)?; if let Value::Closure(obj) = res { Ok(obj) @@ -755,7 +772,7 @@ impl Environment { /// Creates a new `VM` connected to this environment's global scope. pub fn create_vm(&self) -> VM { - VM::new(self.root_values.clone()) + VM::new(self.global_store()) } fn specialize_node(&self, node: AnalyzedNode) -> AnalyzedNode { @@ -766,7 +783,7 @@ impl Environment { let rtl_lookup = make_rtl_lookup(); let typed_reg = self.typed_function_registry.clone(); let mono_cache = self.monomorph_cache.clone(); - let root_values = self.root_values.clone(); + let root_values = self.global_store(); let root_types = self.root_types.clone(); let root_purity = self.root_purity.clone(); let optimization = self.optimization; @@ -866,7 +883,7 @@ impl Environment { let compiled = self.compile(source).into_result()?; let linked = self.link(compiled); - let mut vm = VM::new(self.root_values.clone()); + let mut vm = VM::new(self.global_store()); let mut observer = TracingObserver::new(); // 1. Run the script wrapper (returns a closure representing the script) diff --git a/src/ast/rtl/streams.rs b/src/ast/rtl/streams.rs index 3e2ec27..53a806d 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::VM; +use crate::ast::vm::{GlobalStore, VM}; use std::cell::RefCell; use std::rc::Rc; @@ -473,7 +473,10 @@ pub fn register(env: &Environment) { // (create-ticker condition-closure) -> StreamNode let ticker_generators = env.pipeline_generators.clone(); - let globals = env.root_values.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); env.register_native_fn( "create-ticker", @@ -533,7 +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 - let globals = env.root_values.clone(); + // Same RTL-only capture as create-ticker above. + let globals = GlobalStore::new(env.root_values.clone(), 0); env.register_native_fn( "pipe", StaticType::PolymorphicFn { diff --git a/src/ast/vm.rs b/src/ast/vm.rs index 82827c7..92361d9 100644 --- a/src/ast/vm.rs +++ b/src/ast/vm.rs @@ -82,9 +82,43 @@ 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. +/// +/// True RTL/user splitting (sharing the immutable RTL portion across environments) +/// is a planned future optimization once the stream-bootstrap timing is resolved. +#[derive(Clone)] +pub struct GlobalStore { + values: Rc>>, + /// Index boundary between frozen RTL slots and mutable user slots. + pub rtl_len: usize, +} + +impl GlobalStore { + pub fn new(values: Rc>>, rtl_len: usize) -> Self { + Self { values, rtl_len } + } + + pub fn get(&self, idx: usize) -> Option { + self.values.borrow().get(idx).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); + } + v[idx] = value; + } +} + pub struct VM { stack: Vec, - globals: Rc>>, + globals: GlobalStore, frames: Vec, /// Side-channel for tail-call signaling. Set by `eval_internal` when a tail-call /// is requested; consumed by `resolve_tail_calls` and the inline TCO loop. @@ -92,7 +126,7 @@ pub struct VM { } impl VM { - pub fn new(globals: Rc>>) -> Self { + pub fn new(globals: GlobalStore) -> Self { Self { stack: Vec::new(), globals, @@ -883,13 +917,9 @@ impl VM { } } Address::Global(idx) => { - let g_idx = idx.0 as usize; - let globals = self.globals.borrow(); - if g_idx < globals.len() { - Ok(globals[g_idx].clone()) - } else { - Err(format!("Global access out of bounds {}", idx)) - } + self.globals + .get(idx.0 as usize) + .ok_or_else(|| format!("Global access out of bounds {}", idx)) } Address::Upvalue(idx) => { let frame = self.frames.last().ok_or("No call frame")?; @@ -926,12 +956,7 @@ impl VM { Ok(()) } Address::Global(idx) => { - let g_idx = idx.0 as usize; - let mut globals = self.globals.borrow_mut(); - if g_idx >= globals.len() { - globals.resize(g_idx + 1, Value::Void); - } - globals[g_idx] = value; + self.globals.set(idx.0 as usize, value); Ok(()) } Address::Upvalue(idx) => {