Skip to main content

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    /// Strong owners of this closure's module scope, pinned only when the
36    /// closure is stored in a process/thread-local registry that outlives the
37    /// VM that created it (reminder providers, session/lifecycle hooks). See
38    /// [`RetainedModuleScope`] and [`VmClosure::retained_for_host_registry`].
39    /// `None` for the overwhelmingly common short-lived closure, whose module
40    /// scope stays alive through the live VM's `module_cache`.
41    pub retained_module_scope: Option<Arc<RetainedModuleScope>>,
42}
43
44pub type ModuleFunctionRegistry = Arc<VmMutex<BTreeMap<String, Arc<VmClosure>>>>;
45pub type WeakModuleFunctionRegistry = Weak<VmMutex<BTreeMap<String, Arc<VmClosure>>>>;
46pub type ModuleState = Arc<VmMutex<VmEnv>>;
47pub type WeakModuleState = Weak<VmMutex<VmEnv>>;
48
49/// Strong owners of a closure's module function table and module-level state.
50///
51/// A [`VmClosure`] resolves sibling module `pub fn`s through its module's
52/// function registry, which it references only via a [`Weak`]
53/// ([`VmClosure::module_functions`] / [`module_state`](VmClosure::module_state)).
54/// The sole strong owner of that registry is normally the registering VM's
55/// `module_cache`. When a closure is registered into a process/thread-local
56/// registry (reminder providers, session/lifecycle hooks) it outlives that VM;
57/// once the VM tears down, the `Weak` dangles and a sibling-fn call inside the
58/// invoked closure falls through name resolution to host-bridge dispatch. This
59/// pins strong owners so the `Weak` stays upgradeable for the closure's whole
60/// retained lifetime.
61///
62/// The fields are intentionally unread — their sole purpose is to keep the
63/// referenced `Arc`s alive.
64#[derive(Debug)]
65pub struct RetainedModuleScope {
66    _functions: Option<ModuleFunctionRegistry>,
67    _state: Option<ModuleState>,
68}
69
70impl VmClosure {
71    pub(crate) fn module_functions(&self) -> Option<ModuleFunctionRegistry> {
72        self.module_functions
73            .as_ref()
74            .and_then(WeakModuleFunctionRegistry::upgrade)
75    }
76
77    pub(crate) fn module_state(&self) -> Option<ModuleState> {
78        self.module_state
79            .as_ref()
80            .and_then(WeakModuleState::upgrade)
81    }
82
83    /// Return a clone of this closure suitable for storage in a process- or
84    /// thread-local registry that outlives the VM that created it (reminder
85    /// providers, session/lifecycle hooks). The clone pins strong owners of
86    /// this closure's module function table and module-level state
87    /// ([`RetainedModuleScope`]), so its body still resolves sibling module
88    /// `pub fn`s after the registering VM — the only other strong owner, via
89    /// `module_cache` — is dropped.
90    ///
91    /// The owners are pinned on a *clone* (a fresh `Arc<VmClosure>` that is
92    /// never itself a member of any function registry), so retaining a closure
93    /// that IS a module `pub fn` cannot form an `Arc` cycle with its registry.
94    ///
95    /// A no-op refcount bump when there is nothing to pin: the closure is
96    /// already pinned, or its `Weak`s do not upgrade — e.g. an entry-chunk
97    /// closure whose sibling functions live in captured `env` rather than a
98    /// module registry, which resolves without this.
99    pub(crate) fn retained_for_host_registry(self: &Arc<Self>) -> Arc<Self> {
100        if self.retained_module_scope.is_some() {
101            return Arc::clone(self);
102        }
103        let functions = self.module_functions();
104        let state = self.module_state();
105        if functions.is_none() && state.is_none() {
106            return Arc::clone(self);
107        }
108        let mut pinned = (**self).clone();
109        pinned.retained_module_scope = Some(Arc::new(RetainedModuleScope {
110            _functions: functions,
111            _state: state,
112        }));
113        Arc::new(pinned)
114    }
115}
116
117/// VM environment for variable storage.
118///
119/// `Scope::vars` is wrapped in `Arc` so that `VmEnv::clone()` is cheap
120/// (Arc bump per scope) instead of a deep walk of every BTreeMap. The
121/// VM saves and restores `env` snapshots on every function call, and
122/// the call hot path dominates orchestration-heavy workloads. With
123/// `Arc<BTreeMap<..>>`, the per-scope clone collapses to a refcount
124/// bump, and `Arc::make_mut` only does a deep copy when the scope is
125/// still shared with a saved snapshot — which is exactly the case where
126/// the caller would have needed an isolated copy anyway. Reads still go
127/// through the `BTreeMap` directly via `Deref`.
128#[derive(Debug, Clone)]
129pub struct VmEnv {
130    pub(crate) scopes: Vec<Scope>,
131}
132
133#[derive(Debug, Clone)]
134pub(crate) struct Scope {
135    pub(crate) vars: Arc<BTreeMap<String, (VmValue, bool)>>, // (value, mutable)
136}
137
138/// Process-wide shared empty binding map.
139///
140/// Every block entry pushes a fresh [`Scope`], but inside a function body its
141/// bindings compile to local slots (`DefLocalSlot`) rather than env writes, so
142/// the pushed scope is overwhelmingly *empty* — a hot loop whose body is a
143/// block would otherwise `Arc::new(BTreeMap::new())`-allocate (and free) one
144/// map per iteration. Sharing a single immutable empty map makes
145/// [`Scope::empty`] a refcount bump instead; the first real `define`/`assign`
146/// copies-on-write away from this shared map via `Arc::make_mut` (the insert
147/// paths already do), so a scope that never binds anything never allocates.
148static EMPTY_SCOPE_VARS: std::sync::LazyLock<Arc<BTreeMap<String, (VmValue, bool)>>> =
149    std::sync::LazyLock::new(|| Arc::new(BTreeMap::new()));
150
151impl Scope {
152    #[inline]
153    fn empty() -> Self {
154        Self {
155            vars: Arc::clone(&EMPTY_SCOPE_VARS),
156        }
157    }
158}
159
160impl Drop for Scope {
161    fn drop(&mut self) {
162        // Deeply nested script values (e.g. `x = [x]` built in a loop, which
163        // adds no VM call frames and so never trips `max_vm_frames`) live in
164        // scope bindings. Their default recursive drop would overflow the
165        // native stack and abort the whole process — an uncatchable failure.
166        // When this scope holds the last reference to its bindings and any
167        // value is a nested container, tear the bindings down iteratively
168        // instead. `Arc::get_mut` succeeds only for a uniquely-owned scope, so
169        // shared snapshots fall through to the cheap default drop and the real
170        // teardown happens later at the last owner (also a `Scope`).
171        if let Some(map) = Arc::get_mut(&mut self.vars) {
172            if map
173                .values()
174                .any(|(value, _)| super::recursion::is_recursive_container(value))
175            {
176                let bindings = std::mem::take(map);
177                super::recursion::dismantle_values(bindings.into_values().map(|(value, _)| value));
178            }
179        }
180    }
181}
182
183impl Default for VmEnv {
184    fn default() -> Self {
185        Self::new()
186    }
187}
188
189impl VmEnv {
190    pub fn new() -> Self {
191        Self {
192            scopes: vec![Scope::empty()],
193        }
194    }
195
196    pub fn push_scope(&mut self) {
197        self.scopes.push(Scope::empty());
198    }
199
200    /// Clone the scope stack for a fresh call frame, reserving room for the
201    /// one empty scope every invocation pushes for the callee's body.
202    ///
203    /// `Vec::clone` allocates at exactly `len` capacity, so the `push_scope`
204    /// that immediately follows on the call hot path would otherwise force a
205    /// reallocation and copy of the whole scope stack. Reserving the extra
206    /// slot up front folds those two allocations into one. When a caller does
207    /// not end up pushing (no path currently does, but it stays correct if one
208    /// is added), the only cost is a single unused `Scope` slot of capacity.
209    pub(crate) fn cloned_for_call(&self) -> VmEnv {
210        let mut scopes = Vec::with_capacity(self.scopes.len() + 1);
211        scopes.extend(self.scopes.iter().cloned());
212        VmEnv { scopes }
213    }
214
215    pub fn pop_scope(&mut self) {
216        if self.scopes.len() > 1 {
217            self.scopes.pop();
218        }
219    }
220
221    pub fn scope_depth(&self) -> usize {
222        self.scopes.len()
223    }
224
225    pub fn truncate_scopes(&mut self, target_depth: usize) {
226        let min_depth = target_depth.max(1);
227        while self.scopes.len() > min_depth {
228            self.scopes.pop();
229        }
230    }
231
232    pub fn get(&self, name: &str) -> Option<VmValue> {
233        for scope in self.scopes.iter().rev() {
234            if let Some((val, _)) = scope.vars.get(name) {
235                return Some(val.clone());
236            }
237        }
238        None
239    }
240
241    pub(crate) fn contains(&self, name: &str) -> bool {
242        self.scopes
243            .iter()
244            .rev()
245            .any(|scope| scope.vars.contains_key(name))
246    }
247
248    pub fn define(&mut self, name: &str, value: VmValue, mutable: bool) -> Result<(), VmError> {
249        if let Some(scope) = self.scopes.last_mut() {
250            if let Some((_, existing_mutable)) = scope.vars.get(name) {
251                if !existing_mutable && !mutable {
252                    return Err(VmError::Runtime(format!(
253                        "Cannot redeclare immutable variable '{name}' in the same scope (use 'var' for mutable bindings)"
254                    )));
255                }
256            }
257            if let Some((previous, _)) =
258                Arc::make_mut(&mut scope.vars).insert(name.to_string(), (value, mutable))
259            {
260                super::recursion::dismantle(previous);
261            }
262        }
263        Ok(())
264    }
265
266    pub fn all_variables(&self) -> crate::value::DictMap {
267        let mut vars = crate::value::DictMap::new();
268        for scope in &self.scopes {
269            for (name, (value, _)) in scope.vars.iter() {
270                vars.insert(crate::value::intern_key(name), value.clone());
271            }
272        }
273        vars
274    }
275
276    pub fn assign(&mut self, name: &str, value: VmValue) -> Result<(), VmError> {
277        for scope in self.scopes.iter_mut().rev() {
278            if let Some((_, mutable)) = scope.vars.get(name) {
279                if !mutable {
280                    return Err(VmError::ImmutableAssignment(name.to_string()));
281                }
282                if let Some((previous, _)) =
283                    Arc::make_mut(&mut scope.vars).insert(name.to_string(), (value, true))
284                {
285                    // Iterative teardown so overwriting a deeply nested binding
286                    // cannot overflow the stack on drop (scalars are a no-op).
287                    super::recursion::dismantle(previous);
288                }
289                return Ok(());
290            }
291        }
292        Err(VmError::UndefinedVariable(name.to_string()))
293    }
294
295    /// Debugger-only variant of `assign` that rebinds the name even if
296    /// the existing binding was declared with `let`. Pipeline authors
297    /// overwhelmingly use `let`, so a strict mutability check would
298    /// make the DAP `setVariable` request useless for "what-if"
299    /// iteration — which is the whole point of the feature. Preserves
300    /// the original mutability flag so the VM's runtime behavior is
301    /// unchanged after the debugger overrides.
302    pub fn assign_debug(&mut self, name: &str, value: VmValue) -> Result<(), VmError> {
303        for scope in self.scopes.iter_mut().rev() {
304            if let Some((_, mutable)) = scope.vars.get(name) {
305                let mutable = *mutable;
306                Arc::make_mut(&mut scope.vars).insert(name.to_string(), (value, mutable));
307                return Ok(());
308            }
309        }
310        Err(VmError::UndefinedVariable(name.to_string()))
311    }
312}
313
314/// Compute Levenshtein edit distance between two strings.
315fn levenshtein(a: &str, b: &str) -> usize {
316    let a: Vec<char> = a.chars().collect();
317    let b: Vec<char> = b.chars().collect();
318    let (m, n) = (a.len(), b.len());
319    let mut prev = (0..=n).collect::<Vec<_>>();
320    let mut curr = vec![0; n + 1];
321    for i in 1..=m {
322        curr[0] = i;
323        for j in 1..=n {
324            let cost = usize::from(a[i - 1] != b[j - 1]);
325            curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
326        }
327        std::mem::swap(&mut prev, &mut curr);
328    }
329    prev[n]
330}
331
332/// Find the closest match from a list of candidates using Levenshtein distance.
333/// Returns `Some(suggestion)` if a candidate is within `max_dist` edits.
334pub fn closest_match<'a>(name: &str, candidates: impl Iterator<Item = &'a str>) -> Option<String> {
335    let max_dist = match name.len() {
336        0..=2 => 1,
337        3..=5 => 2,
338        _ => 3,
339    };
340    candidates
341        .filter(|c| *c != name && !c.starts_with("__"))
342        .map(|c| (c, levenshtein(name, c)))
343        .filter(|(_, d)| *d <= max_dist)
344        // Prefer smallest distance, then closest length to original, then alphabetical
345        .min_by(|(a, da), (b, db)| {
346            da.cmp(db)
347                .then_with(|| {
348                    let a_diff = (a.len() as isize - name.len() as isize).unsigned_abs();
349                    let b_diff = (b.len() as isize - name.len() as isize).unsigned_abs();
350                    a_diff.cmp(&b_diff)
351                })
352                .then_with(|| a.cmp(b))
353        })
354        .map(|(c, _)| c.to_string())
355}
356
357#[cfg(test)]
358mod scope_alloc_tests {
359    use super::*;
360
361    #[test]
362    fn empty_scopes_share_one_backing_map() {
363        // Pushing block scopes (the per-iteration cost in a loop body) must not
364        // allocate: every empty scope shares the process-wide empty map.
365        let mut env = VmEnv::new();
366        env.push_scope();
367        env.push_scope();
368        for scope in &env.scopes {
369            assert!(Arc::ptr_eq(&scope.vars, &EMPTY_SCOPE_VARS));
370        }
371    }
372
373    #[test]
374    fn define_copies_on_write_without_disturbing_siblings() {
375        let mut env = VmEnv::new();
376        env.push_scope(); // shares EMPTY
377        env.define("x", VmValue::Int(1), true).unwrap();
378        // The bound scope copied on write away from the shared empty map...
379        let top = env.scopes.last().unwrap();
380        assert!(!Arc::ptr_eq(&top.vars, &EMPTY_SCOPE_VARS));
381        // ...while the root scope (untouched) still shares it.
382        assert!(Arc::ptr_eq(&env.scopes[0].vars, &EMPTY_SCOPE_VARS));
383        assert!(matches!(env.get("x"), Some(VmValue::Int(1))));
384        // Popping the scope drops the binding entirely.
385        env.pop_scope();
386        assert!(env.get("x").is_none());
387    }
388}