diff --git a/src/ast/environment.rs b/src/ast/environment.rs index 078ed34..0fd7681 100644 --- a/src/ast/environment.rs +++ b/src/ast/environment.rs @@ -37,6 +37,24 @@ fn make_rtl_lookup() -> RtlLookupFunc { Rc::new(|name: &str, args: &[StaticType]| intrinsics::lookup(name, args)) } +/// Frozen snapshot of the RTL bootstrap state. +/// +/// Created once after `rtl::register()` completes. All fields are immutable. +/// Multiple [`Environment`] instances can be created from a single `Rtl` +/// via [`Environment::from_rtl`], enabling independent execution contexts +/// that share the same native function set. +pub struct Rtl { + /// The frozen RTL scope (scope index 0) containing all native bindings. + pub scope: CompilerScope, + /// Number of RTL slots — marks the boundary between RTL and user slots + /// in the global value vectors. + pub slot_count: u32, + pub types: Vec, + pub purity: Vec, + pub values: Vec, + pub rtl_docs: Vec, +} + pub struct Environment { pub root_types: Rc>>, pub root_purity: Rc>>, @@ -57,6 +75,8 @@ pub struct Environment { /// Documentation for symbols defined in Myc source (via `;;` comments before `def`/`macro`). /// Key: symbol name. Value: concatenated doc lines joined by newlines. pub myc_docs: Rc>>, + /// Frozen RTL snapshot from which this environment was created. + pub rtl: Rc, } struct EnvFunctionRegistry { @@ -122,7 +142,9 @@ impl Default for Environment { impl Environment { pub fn new() -> Self { - let env = Self { + // Phase 1: Bootstrap — fixed_scope_idx = -1 allows allocate_slot to + // write freely into scope 0 (the RTL scope). + 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())), @@ -140,11 +162,31 @@ impl Environment { loaded_modules: Rc::new(RefCell::new(HashSet::new())), rtl_docs: Rc::new(RefCell::new(Vec::new())), myc_docs: Rc::new(RefCell::new(HashMap::new())), + // Placeholder — overwritten below after bootstrap. + rtl: Rc::new(Rtl { + scope: CompilerScope::new(), + slot_count: 0, + types: vec![], + purity: vec![], + values: vec![], + rtl_docs: vec![], + }), }; + rtl::register(&env); - - let mut env = env; + + // Phase 2: Freeze scope 0 and snapshot the RTL state. env.fixed_scope_idx = 0; + let rtl_slot_count = *env.root_slot_count.borrow(); + 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(), + rtl_docs: std::mem::take(&mut env.rtl_docs.borrow_mut()), + }); + // Push the first mutable user scope (Level 1) env.root_scopes.borrow_mut().push(CompilerScope::new()); @@ -160,6 +202,52 @@ impl Environment { env } + /// Creates a fresh execution environment from a frozen [`Rtl`] snapshot. + /// + /// The new environment starts with the RTL slots pre-loaded and a clean + /// user scope. Multiple environments created from the same `Rtl` are fully + /// independent: user definitions, macros, and loaded modules do not leak + /// across them. + pub fn from_rtl(rtl: Rc) -> Self { + let scopes = vec![rtl.scope.clone(), CompilerScope::new()]; + + 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())), + fixed_scope_idx: 0, + root_scopes: Rc::new(RefCell::new(scopes)), + root_slot_count: Rc::new(RefCell::new(rtl.slot_count)), + 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())), + debug_mode: false, + optimization: true, + macro_registry: Rc::new(RefCell::new(MacroRegistry::new())), + pipeline_generators: Rc::new(RefCell::new(Vec::new())), + search_paths: Rc::new(RefCell::new(Vec::new())), + loaded_modules: Rc::new(RefCell::new(HashSet::new())), + rtl_docs: Rc::new(RefCell::new(Vec::new())), + myc_docs: Rc::new(RefCell::new(HashMap::new())), + rtl, + }; + + if let Ok(cwd) = std::env::current_dir() { + env.add_search_path(&cwd); + let rtl_path = cwd.join("rtl"); + if rtl_path.exists() { + env.add_search_path(rtl_path); + } + } + + env + } + + /// Returns the frozen RTL snapshot this environment was bootstrapped from. + pub fn rtl(&self) -> Rc { + Rc::clone(&self.rtl) + } + pub fn add_search_path(&self, path: impl AsRef) { self.search_paths.borrow_mut().push(path.as_ref().to_path_buf()); } @@ -445,8 +533,7 @@ impl Environment { /// Returns the names of all bindings registered in the fixed RTL scope. /// Useful for introspection (e.g., MCP server listing available built-ins). pub fn list_bindings(&self) -> Vec { - let root_scopes = self.root_scopes.borrow(); - let mut names: Vec = root_scopes[0] + let mut names: Vec = self.rtl.scope .locals .keys() .map(|sym| sym.name.to_string()) @@ -460,11 +547,9 @@ impl Environment { /// (derived from `StaticType`), one-liner, optional description, and examples. pub fn list_rtl_docs(&self) -> Vec { use crate::ast::nodes::Symbol; - let docs = self.rtl_docs.borrow(); - let root_scopes = self.root_scopes.borrow(); - let scope = &root_scopes[0]; + let scope = &self.rtl.scope; - let mut entries: Vec = docs.iter().map(|entry| { + let mut entries: Vec = self.rtl.rtl_docs.iter().map(|entry| { // Derive the signature from the registered StaticType. let sig = scope .locals @@ -505,16 +590,14 @@ impl Environment { /// or `None` if the symbol has no doc entry. pub fn get_rtl_doc(&self, name: &str) -> Option { use crate::ast::nodes::Symbol; - let root_scopes = self.root_scopes.borrow(); - let sig = root_scopes[0] + let sig = self.rtl.scope .locals .get(&Symbol::from(name)) .map(|info| info._ty.to_doc_string()) .unwrap_or_else(|| "unknown".to_string()); // Check RTL docs first - let docs = self.rtl_docs.borrow(); - if let Some(entry) = docs.iter().find(|e| e.name == name) { + if let Some(entry) = self.rtl.rtl_docs.iter().find(|e| e.name == name) { let mut out = format!("{} : {}\n {}", entry.name, sig, entry.one_liner); if let Some(desc) = entry.description { out.push_str(&format!("\n {}", desc)); diff --git a/src/utils/tester.rs b/src/utils/tester.rs index f0238ed..bd749f5 100644 --- a/src/utils/tester.rs +++ b/src/utils/tester.rs @@ -55,7 +55,7 @@ pub fn run_functional_tests_with_optimization(enabled: bool) -> Vec results } -pub fn ca(update: bool, filter: Option<&str>) -> Vec { +pub fn run_benchmarks(update: bool, filter: Option<&str>) -> Vec { let mut results = Vec::new(); let entries = fs::read_dir("examples").unwrap(); let is_release = !cfg!(debug_assertions); diff --git a/tests/rtl.rs b/tests/rtl.rs index e1a4bb9..fb7b8d2 100644 --- a/tests/rtl.rs +++ b/tests/rtl.rs @@ -118,3 +118,22 @@ fn test_date_parsing() { panic!("Expected DateTime, got {:?}", res); } } + +#[test] +fn test_environments_from_shared_rtl_are_independent() { + let rtl = Environment::new().rtl(); + + // Two fresh environments from the same frozen RTL snapshot. + let env1 = Environment::from_rtl(rtl.clone()); + let env2 = Environment::from_rtl(rtl.clone()); + + // Define a variable in env1 only. + env1.run_script("(do (def x 42) 0)").unwrap(); + + // x must not be visible in env2. + assert!(env2.run_script("x").is_err(), "User binding leaked across environments"); + + // RTL functions must work in both environments. + assert_eq!(format!("{}", env1.run_script("(+ 1 2)").unwrap()), "3"); + assert_eq!(format!("{}", env2.run_script("(+ 1 2)").unwrap()), "3"); +}