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/// A shared, mutable cell backing a captured binding.
134///
135/// A local that a nested closure captures is stored behind a `Cell` instead of
136/// inline. Cloning a [`Scope`] (which happens on every call and every closure
137/// mint) refcount-bumps this `Arc`, so the defining frame and every closure
138/// that captured the binding all point at the *same* cell — a write through any
139/// of them is observed by all of them. This is what makes closure capture
140/// **by reference** (JS/Python/Swift semantics) while keeping distinct
141/// variables independent (`let b = a` still copies the value out of `a`'s
142/// cell into `b`'s binding). See `docs/design/closure-reference-capture.md`.
143pub(crate) type BindingCell = Arc<VmMutex<VmValue>>;
144
145/// One name's binding in a [`Scope`].
146///
147/// `Value` is the ordinary, unshared binding — a read clones the value out and
148/// a write replaces it (copy-on-assignment), exactly as before. `Cell` is a
149/// binding captured by a nested closure: the value lives behind a shared
150/// [`BindingCell`] so reads clone the inner value out (value semantics for
151/// reads is preserved) and writes go *through* the cell rather than replacing
152/// the map entry — which also sidesteps the scope-map copy-on-write, so shared
153/// mutation survives the per-call env clone.
154#[derive(Debug, Clone)]
155pub(crate) enum Binding {
156 Value { value: VmValue, mutable: bool },
157 Cell { cell: BindingCell, mutable: bool },
158}
159
160impl Binding {
161 #[inline]
162 pub(crate) fn mutable(&self) -> bool {
163 match self {
164 Binding::Value { mutable, .. } | Binding::Cell { mutable, .. } => *mutable,
165 }
166 }
167
168 /// The current value of this binding, cloned out. Reads never expose the
169 /// cell itself — value semantics for reads is identical for both variants.
170 #[inline]
171 pub(crate) fn read(&self) -> VmValue {
172 match self {
173 Binding::Value { value, .. } => value.clone(),
174 Binding::Cell { cell, .. } => cell.lock().clone(),
175 }
176 }
177
178 /// Ownership-taking accessor for the iterative teardown paths. A `Value`
179 /// yields its inner value directly. A `Cell` yields its inner value only
180 /// when this binding holds the *last* reference to the shared cell; a
181 /// still-shared cell yields `None` and is left for its own `Arc` drop to
182 /// reclaim once the final closure releases it.
183 #[inline]
184 pub(crate) fn into_teardown_value(self) -> Option<VmValue> {
185 match self {
186 Binding::Value { value, .. } => Some(value),
187 Binding::Cell { cell, .. } => Arc::into_inner(cell).map(VmMutex::into_inner),
188 }
189 }
190
191 /// Whether this binding *uniquely* owns a deeply-nested container that the
192 /// default recursive drop could overflow the native stack on. A `Value`
193 /// checks its container directly. A `Cell` only qualifies when unshared
194 /// (`strong_count == 1`) — a cell still held by a live closure must not be
195 /// torn down from here — and is peeked with `try_lock` so a drop never
196 /// blocks.
197 #[inline]
198 fn owns_recursive_container(&self) -> bool {
199 match self {
200 Binding::Value { value, .. } => super::recursion::is_recursive_container(value),
201 Binding::Cell { cell, .. } => {
202 Arc::strong_count(cell) == 1
203 && cell
204 .try_lock()
205 .map(|v| super::recursion::is_recursive_container(&v))
206 .unwrap_or(false)
207 }
208 }
209 }
210}
211
212#[derive(Debug, Clone)]
213pub(crate) struct Scope {
214 pub(crate) vars: Arc<BTreeMap<String, Binding>>,
215}
216
217/// Process-wide shared empty binding map.
218///
219/// Every block entry pushes a fresh [`Scope`], but inside a function body its
220/// bindings compile to local slots (`DefLocalSlot`) rather than env writes, so
221/// the pushed scope is overwhelmingly *empty* — a hot loop whose body is a
222/// block would otherwise `Arc::new(BTreeMap::new())`-allocate (and free) one
223/// map per iteration. Sharing a single immutable empty map makes
224/// [`Scope::empty`] a refcount bump instead; the first real `define`/`assign`
225/// copies-on-write away from this shared map via `Arc::make_mut` (the insert
226/// paths already do), so a scope that never binds anything never allocates.
227static EMPTY_SCOPE_VARS: std::sync::LazyLock<Arc<BTreeMap<String, Binding>>> =
228 std::sync::LazyLock::new(|| Arc::new(BTreeMap::new()));
229
230impl Scope {
231 #[inline]
232 fn empty() -> Self {
233 Self {
234 vars: Arc::clone(&EMPTY_SCOPE_VARS),
235 }
236 }
237}
238
239impl Drop for Scope {
240 fn drop(&mut self) {
241 // Deeply nested script values (e.g. `x = [x]` built in a loop, which
242 // adds no VM call frames and so never trips `max_vm_frames`) live in
243 // scope bindings. Their default recursive drop would overflow the
244 // native stack and abort the whole process — an uncatchable failure.
245 // When this scope holds the last reference to its bindings and any
246 // value is a nested container, tear the bindings down iteratively
247 // instead. `Arc::get_mut` succeeds only for a uniquely-owned scope, so
248 // shared snapshots fall through to the cheap default drop and the real
249 // teardown happens later at the last owner (also a `Scope`).
250 //
251 // A still-shared `Cell` may outlive this scope (a live closure holds
252 // it), so its `Arc` refcount — not this map's — governs when its inner
253 // value drops. `into_teardown_value` therefore only reclaims a cell we
254 // uniquely own; shared cells fall through to their own `Arc` drop.
255 if let Some(map) = Arc::get_mut(&mut self.vars) {
256 if map.values().any(Binding::owns_recursive_container) {
257 let bindings = std::mem::take(map);
258 super::recursion::dismantle_values(
259 bindings
260 .into_values()
261 .filter_map(Binding::into_teardown_value),
262 );
263 }
264 }
265 }
266}
267
268impl Default for VmEnv {
269 fn default() -> Self {
270 Self::new()
271 }
272}
273
274impl VmEnv {
275 pub fn new() -> Self {
276 Self {
277 scopes: vec![Scope::empty()],
278 }
279 }
280
281 pub fn push_scope(&mut self) {
282 self.scopes.push(Scope::empty());
283 }
284
285 /// Clone the scope stack for a fresh call frame, reserving room for the
286 /// one empty scope every invocation pushes for the callee's body.
287 ///
288 /// `Vec::clone` allocates at exactly `len` capacity, so the `push_scope`
289 /// that immediately follows on the call hot path would otherwise force a
290 /// reallocation and copy of the whole scope stack. Reserving the extra
291 /// slot up front folds those two allocations into one. When a caller does
292 /// not end up pushing (no path currently does, but it stays correct if one
293 /// is added), the only cost is a single unused `Scope` slot of capacity.
294 pub(crate) fn cloned_for_call(&self) -> VmEnv {
295 let mut scopes = Vec::with_capacity(self.scopes.len() + 1);
296 scopes.extend(self.scopes.iter().cloned());
297 VmEnv { scopes }
298 }
299
300 pub fn pop_scope(&mut self) {
301 if self.scopes.len() > 1 {
302 self.scopes.pop();
303 }
304 }
305
306 pub fn scope_depth(&self) -> usize {
307 self.scopes.len()
308 }
309
310 pub fn truncate_scopes(&mut self, target_depth: usize) {
311 let min_depth = target_depth.max(1);
312 while self.scopes.len() > min_depth {
313 self.scopes.pop();
314 }
315 }
316
317 pub fn get(&self, name: &str) -> Option<VmValue> {
318 for scope in self.scopes.iter().rev() {
319 if let Some(binding) = scope.vars.get(name) {
320 return Some(binding.read());
321 }
322 }
323 None
324 }
325
326 pub(crate) fn contains(&self, name: &str) -> bool {
327 self.scopes
328 .iter()
329 .rev()
330 .any(|scope| scope.vars.contains_key(name))
331 }
332
333 pub fn define(&mut self, name: &str, value: VmValue, mutable: bool) -> Result<(), VmError> {
334 self.define_binding(name, Binding::Value { value, mutable })
335 }
336
337 /// Define `name` as a **captured** binding: a fresh shared cell holding
338 /// `value`. Emitted for a local that a nested closure captures. A closure
339 /// minted after this point clones the enclosing env (refcount-bumping the
340 /// cell), so its reads and writes of `name` flow through the same cell as
341 /// the defining frame. Called once per activation, so each activation gets
342 /// a distinct cell (per-iteration loop captures stay independent).
343 pub(crate) fn define_cell(
344 &mut self,
345 name: &str,
346 value: VmValue,
347 mutable: bool,
348 ) -> Result<(), VmError> {
349 self.define_binding(
350 name,
351 Binding::Cell {
352 cell: Arc::new(VmMutex::new(value)),
353 mutable,
354 },
355 )
356 }
357
358 fn define_binding(&mut self, name: &str, binding: Binding) -> Result<(), VmError> {
359 if let Some(scope) = self.scopes.last_mut() {
360 if let Some(existing) = scope.vars.get(name) {
361 if !existing.mutable() && !binding.mutable() {
362 return Err(VmError::Runtime(format!(
363 "Cannot redeclare immutable variable '{name}' in the same scope (use 'let' for mutable bindings)"
364 )));
365 }
366 }
367 if let Some(Binding::Value { value, .. }) =
368 Arc::make_mut(&mut scope.vars).insert(name.to_string(), binding)
369 {
370 super::recursion::dismantle(value);
371 }
372 }
373 Ok(())
374 }
375
376 pub fn all_variables(&self) -> crate::value::DictMap {
377 let mut vars = crate::value::DictMap::new();
378 for scope in &self.scopes {
379 for (name, binding) in scope.vars.iter() {
380 vars.insert(crate::value::intern_key(name), binding.read());
381 }
382 }
383 vars
384 }
385
386 pub fn assign(&mut self, name: &str, value: VmValue) -> Result<(), VmError> {
387 for scope in self.scopes.iter_mut().rev() {
388 let Some(existing) = scope.vars.get(name) else {
389 continue;
390 };
391 if !existing.mutable() {
392 return Err(VmError::ImmutableAssignment(name.to_string()));
393 }
394 match existing {
395 // Write *through* the shared cell: the entry is not replaced,
396 // so the scope-map copy-on-write is sidestepped and every
397 // holder of this cell (the defining frame, sibling closures)
398 // observes the update.
399 Binding::Cell { cell, .. } => {
400 let previous = std::mem::replace(&mut *cell.lock(), value);
401 super::recursion::dismantle(previous);
402 }
403 Binding::Value { .. } => {
404 // Iterative teardown so overwriting a deeply nested binding
405 // cannot overflow the stack on drop (scalars are a no-op).
406 // The prior binding here is always a `Value` (a name is
407 // either always boxed or never — see the compiler's capture
408 // pre-pass), so only that arm needs draining.
409 if let Some(Binding::Value { value, .. }) = Arc::make_mut(&mut scope.vars)
410 .insert(
411 name.to_string(),
412 Binding::Value {
413 value,
414 mutable: true,
415 },
416 )
417 {
418 super::recursion::dismantle(value);
419 }
420 }
421 }
422 return Ok(());
423 }
424 Err(VmError::UndefinedVariable(name.to_string()))
425 }
426
427 /// Debugger-only variant of `assign` that rebinds the name even if
428 /// the existing binding was declared with `let`. Pipeline authors
429 /// overwhelmingly use `let`, so a strict mutability check would
430 /// make the DAP `setVariable` request useless for "what-if"
431 /// iteration — which is the whole point of the feature. Preserves
432 /// the original mutability flag so the VM's runtime behavior is
433 /// unchanged after the debugger overrides.
434 pub fn assign_debug(&mut self, name: &str, value: VmValue) -> Result<(), VmError> {
435 for scope in self.scopes.iter_mut().rev() {
436 let Some(existing) = scope.vars.get(name) else {
437 continue;
438 };
439 match existing {
440 // Preserve the shared-cell identity so a debugger override of a
441 // captured binding is still observed by the closures holding it.
442 Binding::Cell { cell, .. } => {
443 *cell.lock() = value;
444 }
445 Binding::Value { mutable, .. } => {
446 let mutable = *mutable;
447 Arc::make_mut(&mut scope.vars)
448 .insert(name.to_string(), Binding::Value { value, mutable });
449 }
450 }
451 return Ok(());
452 }
453 Err(VmError::UndefinedVariable(name.to_string()))
454 }
455}
456
457/// Compute Levenshtein edit distance between two strings.
458fn levenshtein(a: &str, b: &str) -> usize {
459 let a: Vec<char> = a.chars().collect();
460 let b: Vec<char> = b.chars().collect();
461 let (m, n) = (a.len(), b.len());
462 let mut prev = (0..=n).collect::<Vec<_>>();
463 let mut curr = vec![0; n + 1];
464 for i in 1..=m {
465 curr[0] = i;
466 for j in 1..=n {
467 let cost = usize::from(a[i - 1] != b[j - 1]);
468 curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
469 }
470 std::mem::swap(&mut prev, &mut curr);
471 }
472 prev[n]
473}
474
475/// Find the closest match from a list of candidates using Levenshtein distance.
476/// Returns `Some(suggestion)` if a candidate is within `max_dist` edits.
477pub fn closest_match<'a>(name: &str, candidates: impl Iterator<Item = &'a str>) -> Option<String> {
478 let max_dist = match name.len() {
479 0..=2 => 1,
480 3..=5 => 2,
481 _ => 3,
482 };
483 candidates
484 .filter(|c| *c != name && !c.starts_with("__"))
485 .map(|c| (c, levenshtein(name, c)))
486 .filter(|(_, d)| *d <= max_dist)
487 // Prefer smallest distance, then closest length to original, then alphabetical
488 .min_by(|(a, da), (b, db)| {
489 da.cmp(db)
490 .then_with(|| {
491 let a_diff = (a.len() as isize - name.len() as isize).unsigned_abs();
492 let b_diff = (b.len() as isize - name.len() as isize).unsigned_abs();
493 a_diff.cmp(&b_diff)
494 })
495 .then_with(|| a.cmp(b))
496 })
497 .map(|(c, _)| c.to_string())
498}
499
500#[cfg(test)]
501mod scope_alloc_tests {
502 use super::*;
503
504 #[test]
505 fn empty_scopes_share_one_backing_map() {
506 // Pushing block scopes (the per-iteration cost in a loop body) must not
507 // allocate: every empty scope shares the process-wide empty map.
508 let mut env = VmEnv::new();
509 env.push_scope();
510 env.push_scope();
511 for scope in &env.scopes {
512 assert!(Arc::ptr_eq(&scope.vars, &EMPTY_SCOPE_VARS));
513 }
514 }
515
516 #[test]
517 fn define_copies_on_write_without_disturbing_siblings() {
518 let mut env = VmEnv::new();
519 env.push_scope(); // shares EMPTY
520 env.define("x", VmValue::Int(1), true).unwrap();
521 // The bound scope copied on write away from the shared empty map...
522 let top = env.scopes.last().unwrap();
523 assert!(!Arc::ptr_eq(&top.vars, &EMPTY_SCOPE_VARS));
524 // ...while the root scope (untouched) still shares it.
525 assert!(Arc::ptr_eq(&env.scopes[0].vars, &EMPTY_SCOPE_VARS));
526 assert!(matches!(env.get("x"), Some(VmValue::Int(1))));
527 // Popping the scope drops the binding entirely.
528 env.pop_scope();
529 assert!(env.get("x").is_none());
530 }
531}