Introduce Rtl struct and from_rtl constructor
This commit refactors the `Environment` struct to better manage the Rust Runtime Library (RTL) bootstrap state. A new `Rtl` struct is introduced to hold a frozen snapshot of the initial RTL state, including scopes, types, purity, values, and documentation. This snapshot is created once after `rtl::register()` completes. The `Environment::new()` constructor is updated to perform a two-phase bootstrap: first, it registers RTL functions, and then it freezes the scope 0 and captures the RTL state into the new `Rtl` struct. A new constructor, `Environment::from_rtl()`, is added. This allows creating independent execution environments that all share the same underlying RTL state. This is crucial for isolation between different execution contexts, as user definitions and loaded modules will not leak between environments created from the same `Rtl` snapshot. The `Environment::list_bindings` and `Environment::list_rtl_docs` methods are updated to use the `rtl` field for accessing the RTL scope and documentation, reinforcing the concept of a shared, immutable RTL state. A new test, `test_environments_from_shared_rtl_are_independent`, is added to verify that environments created from the same `Rtl` snapshot are indeed independent and do not leak user-defined bindings.
This commit is contained in:
+95
-12
@@ -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<StaticType>,
|
||||
pub purity: Vec<Purity>,
|
||||
pub values: Vec<Value>,
|
||||
pub rtl_docs: Vec<RtlDocEntry>,
|
||||
}
|
||||
|
||||
pub struct Environment {
|
||||
pub root_types: Rc<RefCell<Vec<StaticType>>>,
|
||||
pub root_purity: Rc<RefCell<Vec<Purity>>>,
|
||||
@@ -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<RefCell<HashMap<String, String>>>,
|
||||
/// Frozen RTL snapshot from which this environment was created.
|
||||
pub rtl: Rc<Rtl>,
|
||||
}
|
||||
|
||||
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<Rtl>) -> 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<Rtl> {
|
||||
Rc::clone(&self.rtl)
|
||||
}
|
||||
|
||||
pub fn add_search_path(&self, path: impl AsRef<Path>) {
|
||||
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<String> {
|
||||
let root_scopes = self.root_scopes.borrow();
|
||||
let mut names: Vec<String> = root_scopes[0]
|
||||
let mut names: Vec<String> = 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<String> {
|
||||
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<String> = docs.iter().map(|entry| {
|
||||
let mut entries: Vec<String> = 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<String> {
|
||||
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));
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ pub fn run_functional_tests_with_optimization(enabled: bool) -> Vec<TestResult>
|
||||
results
|
||||
}
|
||||
|
||||
pub fn ca(update: bool, filter: Option<&str>) -> Vec<BenchmarkResult> {
|
||||
pub fn run_benchmarks(update: bool, filter: Option<&str>) -> Vec<BenchmarkResult> {
|
||||
let mut results = Vec::new();
|
||||
let entries = fs::read_dir("examples").unwrap();
|
||||
let is_release = !cfg!(debug_assertions);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user