Refactor GlobalStore to use separate RTL and user value stores
The `GlobalStore` was refactored to clearly distinguish between immutable RTL values and mutable user-defined global slots. The `Environment` struct now holds: - `rtl_values`: An immutable `Rc<[Value]>` for pre-defined RTL values. - `user_values`: An `Rc<RefCell<Vec<Value>>>` for user-defined mutable globals. The `GlobalStore` struct now takes both `rtl` and `user` as parameters and uses `rtl_len` to determine which store to access. This change separates concerns and better reflects the immutability of RTL values after the bootstrap phase, aligning with the project's concurrency rules. Documentation in `docs/Analysis_Environment.md` was updated to reflect these structural changes.
This commit is contained in:
+34
-21
@@ -58,7 +58,11 @@ pub struct Rtl {
|
||||
pub struct Environment {
|
||||
pub root_types: Rc<RefCell<Vec<StaticType>>>,
|
||||
pub root_purity: Rc<RefCell<Vec<Purity>>>,
|
||||
pub root_values: Rc<RefCell<Vec<Value>>>,
|
||||
/// 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<RefCell<Vec<Value>>>,
|
||||
pub fixed_scope_idx: i32,
|
||||
pub root_scopes: Rc<RefCell<Vec<CompilerScope>>>,
|
||||
pub root_slot_count: Rc<RefCell<u32>>,
|
||||
@@ -97,7 +101,7 @@ struct RuntimeMacroEvaluator {
|
||||
root_scopes: Rc<RefCell<Vec<CompilerScope>>>,
|
||||
root_slot_count: Rc<RefCell<u32>>,
|
||||
root_types: Rc<RefCell<Vec<StaticType>>>,
|
||||
root_values: Rc<RefCell<Vec<Value>>>,
|
||||
globals: GlobalStore,
|
||||
root_purity: Rc<RefCell<Vec<Purity>>>,
|
||||
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<Path>) {
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+26
-14
@@ -84,35 +84,47 @@ impl VMObserver for TracingObserver {
|
||||
|
||||
/// Unified view of the global value space for VM and optimizer access.
|
||||
///
|
||||
/// Wraps a flat `Vec<Value>` 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<RefCell<Vec<Value>>>`, 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<RefCell<Vec<Value>>>,
|
||||
/// Index boundary between frozen RTL slots and mutable user slots.
|
||||
rtl: Rc<[Value]>,
|
||||
user: Rc<RefCell<Vec<Value>>>,
|
||||
/// 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<RefCell<Vec<Value>>>, rtl_len: usize) -> Self {
|
||||
Self { values, rtl_len }
|
||||
pub fn new(rtl: Rc<[Value]>, user: Rc<RefCell<Vec<Value>>>, rtl_len: usize) -> Self {
|
||||
Self { rtl, user, rtl_len }
|
||||
}
|
||||
|
||||
pub fn get(&self, idx: usize) -> Option<Value> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user