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}
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
77impl Scope {
78    #[inline]
79    fn empty() -> Self {
80        Self {
81            vars: Arc::new(std::collections::BTreeMap::new()),
82        }
83    }
84}
85
86impl Drop for Scope {
87    fn drop(&mut self) {
88        // Deeply nested script values (e.g. `x = [x]` built in a loop, which
89        // adds no VM call frames and so never trips `max_vm_frames`) live in
90        // scope bindings. Their default recursive drop would overflow the
91        // native stack and abort the whole process — an uncatchable failure.
92        // When this scope holds the last reference to its bindings and any
93        // value is a nested container, tear the bindings down iteratively
94        // instead. `Arc::get_mut` succeeds only for a uniquely-owned scope, so
95        // shared snapshots fall through to the cheap default drop and the real
96        // teardown happens later at the last owner (also a `Scope`).
97        if let Some(map) = Arc::get_mut(&mut self.vars) {
98            if map
99                .values()
100                .any(|(value, _)| super::recursion::is_recursive_container(value))
101            {
102                let bindings = std::mem::take(map);
103                super::recursion::dismantle_values(bindings.into_values().map(|(value, _)| value));
104            }
105        }
106    }
107}
108
109impl Default for VmEnv {
110    fn default() -> Self {
111        Self::new()
112    }
113}
114
115impl VmEnv {
116    pub fn new() -> Self {
117        Self {
118            scopes: vec![Scope::empty()],
119        }
120    }
121
122    pub fn push_scope(&mut self) {
123        self.scopes.push(Scope::empty());
124    }
125
126    pub fn pop_scope(&mut self) {
127        if self.scopes.len() > 1 {
128            self.scopes.pop();
129        }
130    }
131
132    pub fn scope_depth(&self) -> usize {
133        self.scopes.len()
134    }
135
136    pub fn truncate_scopes(&mut self, target_depth: usize) {
137        let min_depth = target_depth.max(1);
138        while self.scopes.len() > min_depth {
139            self.scopes.pop();
140        }
141    }
142
143    pub fn get(&self, name: &str) -> Option<VmValue> {
144        for scope in self.scopes.iter().rev() {
145            if let Some((val, _)) = scope.vars.get(name) {
146                return Some(val.clone());
147            }
148        }
149        None
150    }
151
152    pub(crate) fn contains(&self, name: &str) -> bool {
153        self.scopes
154            .iter()
155            .rev()
156            .any(|scope| scope.vars.contains_key(name))
157    }
158
159    pub fn define(&mut self, name: &str, value: VmValue, mutable: bool) -> Result<(), VmError> {
160        if let Some(scope) = self.scopes.last_mut() {
161            if let Some((_, existing_mutable)) = scope.vars.get(name) {
162                if !existing_mutable && !mutable {
163                    return Err(VmError::Runtime(format!(
164                        "Cannot redeclare immutable variable '{name}' in the same scope (use 'var' for mutable bindings)"
165                    )));
166                }
167            }
168            if let Some((previous, _)) =
169                Arc::make_mut(&mut scope.vars).insert(name.to_string(), (value, mutable))
170            {
171                super::recursion::dismantle(previous);
172            }
173        }
174        Ok(())
175    }
176
177    pub fn all_variables(&self) -> crate::value::DictMap {
178        let mut vars = crate::value::DictMap::new();
179        for scope in &self.scopes {
180            for (name, (value, _)) in scope.vars.iter() {
181                vars.insert(name.clone(), value.clone());
182            }
183        }
184        vars
185    }
186
187    pub fn assign(&mut self, name: &str, value: VmValue) -> Result<(), VmError> {
188        for scope in self.scopes.iter_mut().rev() {
189            if let Some((_, mutable)) = scope.vars.get(name) {
190                if !mutable {
191                    return Err(VmError::ImmutableAssignment(name.to_string()));
192                }
193                if let Some((previous, _)) =
194                    Arc::make_mut(&mut scope.vars).insert(name.to_string(), (value, true))
195                {
196                    // Iterative teardown so overwriting a deeply nested binding
197                    // cannot overflow the stack on drop (scalars are a no-op).
198                    super::recursion::dismantle(previous);
199                }
200                return Ok(());
201            }
202        }
203        Err(VmError::UndefinedVariable(name.to_string()))
204    }
205
206    /// Debugger-only variant of `assign` that rebinds the name even if
207    /// the existing binding was declared with `let`. Pipeline authors
208    /// overwhelmingly use `let`, so a strict mutability check would
209    /// make the DAP `setVariable` request useless for "what-if"
210    /// iteration — which is the whole point of the feature. Preserves
211    /// the original mutability flag so the VM's runtime behavior is
212    /// unchanged after the debugger overrides.
213    pub fn assign_debug(&mut self, name: &str, value: VmValue) -> Result<(), VmError> {
214        for scope in self.scopes.iter_mut().rev() {
215            if let Some((_, mutable)) = scope.vars.get(name) {
216                let mutable = *mutable;
217                Arc::make_mut(&mut scope.vars).insert(name.to_string(), (value, mutable));
218                return Ok(());
219            }
220        }
221        Err(VmError::UndefinedVariable(name.to_string()))
222    }
223}
224
225/// Compute Levenshtein edit distance between two strings.
226fn levenshtein(a: &str, b: &str) -> usize {
227    let a: Vec<char> = a.chars().collect();
228    let b: Vec<char> = b.chars().collect();
229    let (m, n) = (a.len(), b.len());
230    let mut prev = (0..=n).collect::<Vec<_>>();
231    let mut curr = vec![0; n + 1];
232    for i in 1..=m {
233        curr[0] = i;
234        for j in 1..=n {
235            let cost = usize::from(a[i - 1] != b[j - 1]);
236            curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
237        }
238        std::mem::swap(&mut prev, &mut curr);
239    }
240    prev[n]
241}
242
243/// Find the closest match from a list of candidates using Levenshtein distance.
244/// Returns `Some(suggestion)` if a candidate is within `max_dist` edits.
245pub fn closest_match<'a>(name: &str, candidates: impl Iterator<Item = &'a str>) -> Option<String> {
246    let max_dist = match name.len() {
247        0..=2 => 1,
248        3..=5 => 2,
249        _ => 3,
250    };
251    candidates
252        .filter(|c| *c != name && !c.starts_with("__"))
253        .map(|c| (c, levenshtein(name, c)))
254        .filter(|(_, d)| *d <= max_dist)
255        // Prefer smallest distance, then closest length to original, then alphabetical
256        .min_by(|(a, da), (b, db)| {
257            da.cmp(db)
258                .then_with(|| {
259                    let a_diff = (a.len() as isize - name.len() as isize).unsigned_abs();
260                    let b_diff = (b.len() as isize - name.len() as isize).unsigned_abs();
261                    a_diff.cmp(&b_diff)
262                })
263                .then_with(|| a.cmp(b))
264        })
265        .map(|(c, _)| c.to_string())
266}