harn_vm/value/env.rs
1use std::collections::BTreeMap;
2use std::path::PathBuf;
3use std::sync::{Arc, Weak};
4
5use crate::chunk::CompiledFunctionRef;
6
7use super::{VmError, VmMutex, VmValue};
8
9/// A compiled closure value.
10#[derive(Debug, Clone)]
11pub struct VmClosure {
12 pub func: CompiledFunctionRef,
13 pub env: VmEnv,
14 /// Source directory for this closure's originating module.
15 /// When set, `render()` and other source-relative builtins resolve
16 /// paths relative to this directory instead of the entry pipeline.
17 pub source_dir: Option<PathBuf>,
18 /// Module-local named functions that should resolve before builtin fallback.
19 /// This lets selectively imported functions keep private sibling helpers
20 /// without exporting them into the caller's environment.
21 pub module_functions: Option<WeakModuleFunctionRegistry>,
22 /// Shared, mutable module-level env: holds top-level `var` / `let`
23 /// bindings declared at the module root (caches, counters, lazily
24 /// initialized registries). All closures created from the same
25 /// module import point at the same shared mutable env, so a
26 /// mutation inside one function is visible to every other function
27 /// in that module on subsequent calls. `closure.env` still holds
28 /// the per-closure lexical snapshot (captured function args from
29 /// enclosing scopes, etc.) and is unchanged by this — `module_state`
30 /// is a separate lookup layer consulted after the local env and
31 /// before globals. Created in `import_declarations` after the
32 /// module's init chunk runs, so the initial values from `var x = ...`
33 /// land in it.
34 pub module_state: Option<WeakModuleState>,
35}
36
37pub type ModuleFunctionRegistry = Arc<VmMutex<BTreeMap<String, Arc<VmClosure>>>>;
38pub type WeakModuleFunctionRegistry = Weak<VmMutex<BTreeMap<String, Arc<VmClosure>>>>;
39pub type ModuleState = Arc<VmMutex<VmEnv>>;
40pub type WeakModuleState = Weak<VmMutex<VmEnv>>;
41
42impl VmClosure {
43 pub(crate) fn module_functions(&self) -> Option<ModuleFunctionRegistry> {
44 self.module_functions
45 .as_ref()
46 .and_then(WeakModuleFunctionRegistry::upgrade)
47 }
48
49 pub(crate) fn module_state(&self) -> Option<ModuleState> {
50 self.module_state
51 .as_ref()
52 .and_then(WeakModuleState::upgrade)
53 }
54}
55
56/// VM environment for variable storage.
57///
58/// `Scope::vars` is wrapped in `Arc` so that `VmEnv::clone()` is cheap
59/// (Arc bump per scope) instead of a deep walk of every BTreeMap. The
60/// VM saves and restores `env` snapshots on every function call, and
61/// the call hot path dominates orchestration-heavy workloads. With
62/// `Arc<BTreeMap<..>>`, the per-scope clone collapses to a refcount
63/// bump, and `Arc::make_mut` only does a deep copy when the scope is
64/// still shared with a saved snapshot — which is exactly the case where
65/// the caller would have needed an isolated copy anyway. Reads still go
66/// through the `BTreeMap` directly via `Deref`.
67#[derive(Debug, Clone)]
68pub struct VmEnv {
69 pub(crate) scopes: Vec<Scope>,
70}
71
72#[derive(Debug, Clone)]
73pub(crate) struct Scope {
74 pub(crate) vars: Arc<BTreeMap<String, (VmValue, bool)>>, // (value, mutable)
75}
76
77/// Process-wide shared empty binding map.
78///
79/// Every block entry pushes a fresh [`Scope`], but inside a function body its
80/// bindings compile to local slots (`DefLocalSlot`) rather than env writes, so
81/// the pushed scope is overwhelmingly *empty* — a hot loop whose body is a
82/// block would otherwise `Arc::new(BTreeMap::new())`-allocate (and free) one
83/// map per iteration. Sharing a single immutable empty map makes
84/// [`Scope::empty`] a refcount bump instead; the first real `define`/`assign`
85/// copies-on-write away from this shared map via `Arc::make_mut` (the insert
86/// paths already do), so a scope that never binds anything never allocates.
87static EMPTY_SCOPE_VARS: std::sync::LazyLock<Arc<BTreeMap<String, (VmValue, bool)>>> =
88 std::sync::LazyLock::new(|| Arc::new(BTreeMap::new()));
89
90impl Scope {
91 #[inline]
92 fn empty() -> Self {
93 Self {
94 vars: Arc::clone(&EMPTY_SCOPE_VARS),
95 }
96 }
97}
98
99impl Drop for Scope {
100 fn drop(&mut self) {
101 // Deeply nested script values (e.g. `x = [x]` built in a loop, which
102 // adds no VM call frames and so never trips `max_vm_frames`) live in
103 // scope bindings. Their default recursive drop would overflow the
104 // native stack and abort the whole process — an uncatchable failure.
105 // When this scope holds the last reference to its bindings and any
106 // value is a nested container, tear the bindings down iteratively
107 // instead. `Arc::get_mut` succeeds only for a uniquely-owned scope, so
108 // shared snapshots fall through to the cheap default drop and the real
109 // teardown happens later at the last owner (also a `Scope`).
110 if let Some(map) = Arc::get_mut(&mut self.vars) {
111 if map
112 .values()
113 .any(|(value, _)| super::recursion::is_recursive_container(value))
114 {
115 let bindings = std::mem::take(map);
116 super::recursion::dismantle_values(bindings.into_values().map(|(value, _)| value));
117 }
118 }
119 }
120}
121
122impl Default for VmEnv {
123 fn default() -> Self {
124 Self::new()
125 }
126}
127
128impl VmEnv {
129 pub fn new() -> Self {
130 Self {
131 scopes: vec![Scope::empty()],
132 }
133 }
134
135 pub fn push_scope(&mut self) {
136 self.scopes.push(Scope::empty());
137 }
138
139 /// Clone the scope stack for a fresh call frame, reserving room for the
140 /// one empty scope every invocation pushes for the callee's body.
141 ///
142 /// `Vec::clone` allocates at exactly `len` capacity, so the `push_scope`
143 /// that immediately follows on the call hot path would otherwise force a
144 /// reallocation and copy of the whole scope stack. Reserving the extra
145 /// slot up front folds those two allocations into one. When a caller does
146 /// not end up pushing (no path currently does, but it stays correct if one
147 /// is added), the only cost is a single unused `Scope` slot of capacity.
148 pub(crate) fn cloned_for_call(&self) -> VmEnv {
149 let mut scopes = Vec::with_capacity(self.scopes.len() + 1);
150 scopes.extend(self.scopes.iter().cloned());
151 VmEnv { scopes }
152 }
153
154 pub fn pop_scope(&mut self) {
155 if self.scopes.len() > 1 {
156 self.scopes.pop();
157 }
158 }
159
160 pub fn scope_depth(&self) -> usize {
161 self.scopes.len()
162 }
163
164 pub fn truncate_scopes(&mut self, target_depth: usize) {
165 let min_depth = target_depth.max(1);
166 while self.scopes.len() > min_depth {
167 self.scopes.pop();
168 }
169 }
170
171 pub fn get(&self, name: &str) -> Option<VmValue> {
172 for scope in self.scopes.iter().rev() {
173 if let Some((val, _)) = scope.vars.get(name) {
174 return Some(val.clone());
175 }
176 }
177 None
178 }
179
180 pub(crate) fn contains(&self, name: &str) -> bool {
181 self.scopes
182 .iter()
183 .rev()
184 .any(|scope| scope.vars.contains_key(name))
185 }
186
187 pub fn define(&mut self, name: &str, value: VmValue, mutable: bool) -> Result<(), VmError> {
188 if let Some(scope) = self.scopes.last_mut() {
189 if let Some((_, existing_mutable)) = scope.vars.get(name) {
190 if !existing_mutable && !mutable {
191 return Err(VmError::Runtime(format!(
192 "Cannot redeclare immutable variable '{name}' in the same scope (use 'var' for mutable bindings)"
193 )));
194 }
195 }
196 if let Some((previous, _)) =
197 Arc::make_mut(&mut scope.vars).insert(name.to_string(), (value, mutable))
198 {
199 super::recursion::dismantle(previous);
200 }
201 }
202 Ok(())
203 }
204
205 pub fn all_variables(&self) -> crate::value::DictMap {
206 let mut vars = crate::value::DictMap::new();
207 for scope in &self.scopes {
208 for (name, (value, _)) in scope.vars.iter() {
209 vars.insert(crate::value::intern_key(name), value.clone());
210 }
211 }
212 vars
213 }
214
215 pub fn assign(&mut self, name: &str, value: VmValue) -> Result<(), VmError> {
216 for scope in self.scopes.iter_mut().rev() {
217 if let Some((_, mutable)) = scope.vars.get(name) {
218 if !mutable {
219 return Err(VmError::ImmutableAssignment(name.to_string()));
220 }
221 if let Some((previous, _)) =
222 Arc::make_mut(&mut scope.vars).insert(name.to_string(), (value, true))
223 {
224 // Iterative teardown so overwriting a deeply nested binding
225 // cannot overflow the stack on drop (scalars are a no-op).
226 super::recursion::dismantle(previous);
227 }
228 return Ok(());
229 }
230 }
231 Err(VmError::UndefinedVariable(name.to_string()))
232 }
233
234 /// Debugger-only variant of `assign` that rebinds the name even if
235 /// the existing binding was declared with `let`. Pipeline authors
236 /// overwhelmingly use `let`, so a strict mutability check would
237 /// make the DAP `setVariable` request useless for "what-if"
238 /// iteration — which is the whole point of the feature. Preserves
239 /// the original mutability flag so the VM's runtime behavior is
240 /// unchanged after the debugger overrides.
241 pub fn assign_debug(&mut self, name: &str, value: VmValue) -> Result<(), VmError> {
242 for scope in self.scopes.iter_mut().rev() {
243 if let Some((_, mutable)) = scope.vars.get(name) {
244 let mutable = *mutable;
245 Arc::make_mut(&mut scope.vars).insert(name.to_string(), (value, mutable));
246 return Ok(());
247 }
248 }
249 Err(VmError::UndefinedVariable(name.to_string()))
250 }
251}
252
253/// Compute Levenshtein edit distance between two strings.
254fn levenshtein(a: &str, b: &str) -> usize {
255 let a: Vec<char> = a.chars().collect();
256 let b: Vec<char> = b.chars().collect();
257 let (m, n) = (a.len(), b.len());
258 let mut prev = (0..=n).collect::<Vec<_>>();
259 let mut curr = vec![0; n + 1];
260 for i in 1..=m {
261 curr[0] = i;
262 for j in 1..=n {
263 let cost = usize::from(a[i - 1] != b[j - 1]);
264 curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
265 }
266 std::mem::swap(&mut prev, &mut curr);
267 }
268 prev[n]
269}
270
271/// Find the closest match from a list of candidates using Levenshtein distance.
272/// Returns `Some(suggestion)` if a candidate is within `max_dist` edits.
273pub fn closest_match<'a>(name: &str, candidates: impl Iterator<Item = &'a str>) -> Option<String> {
274 let max_dist = match name.len() {
275 0..=2 => 1,
276 3..=5 => 2,
277 _ => 3,
278 };
279 candidates
280 .filter(|c| *c != name && !c.starts_with("__"))
281 .map(|c| (c, levenshtein(name, c)))
282 .filter(|(_, d)| *d <= max_dist)
283 // Prefer smallest distance, then closest length to original, then alphabetical
284 .min_by(|(a, da), (b, db)| {
285 da.cmp(db)
286 .then_with(|| {
287 let a_diff = (a.len() as isize - name.len() as isize).unsigned_abs();
288 let b_diff = (b.len() as isize - name.len() as isize).unsigned_abs();
289 a_diff.cmp(&b_diff)
290 })
291 .then_with(|| a.cmp(b))
292 })
293 .map(|(c, _)| c.to_string())
294}
295
296#[cfg(test)]
297mod scope_alloc_tests {
298 use super::*;
299
300 #[test]
301 fn empty_scopes_share_one_backing_map() {
302 // Pushing block scopes (the per-iteration cost in a loop body) must not
303 // allocate: every empty scope shares the process-wide empty map.
304 let mut env = VmEnv::new();
305 env.push_scope();
306 env.push_scope();
307 for scope in &env.scopes {
308 assert!(Arc::ptr_eq(&scope.vars, &EMPTY_SCOPE_VARS));
309 }
310 }
311
312 #[test]
313 fn define_copies_on_write_without_disturbing_siblings() {
314 let mut env = VmEnv::new();
315 env.push_scope(); // shares EMPTY
316 env.define("x", VmValue::Int(1), true).unwrap();
317 // The bound scope copied on write away from the shared empty map...
318 let top = env.scopes.last().unwrap();
319 assert!(!Arc::ptr_eq(&top.vars, &EMPTY_SCOPE_VARS));
320 // ...while the root scope (untouched) still shares it.
321 assert!(Arc::ptr_eq(&env.scopes[0].vars, &EMPTY_SCOPE_VARS));
322 assert!(matches!(env.get("x"), Some(VmValue::Int(1))));
323 // Popping the scope drops the binding entirely.
324 env.pop_scope();
325 assert!(env.get("x").is_none());
326 }
327}