Skip to main content

jetro_core/data/
context.rs

1//! Evaluation context shared by the VM, builtins, and pipeline executor.
2//!
3//! `Env` carries the three binding forms the language exposes — the document
4//! root (`$`), the current item (`@`), and named let-bindings. It is cloned
5//! per-scope but kept cheap via `SmallVec` (inline storage for ≤4 vars).
6
7use crate::data::value::Val;
8use smallvec::SmallVec;
9use std::sync::Arc;
10
11/// Evaluation error carrying a human-readable message. Propagated through
12/// `Result<Val, EvalError>` across all execution layers.
13#[derive(Debug, Clone)]
14pub struct EvalError(pub String);
15
16impl std::fmt::Display for EvalError {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        write!(f, "eval error: {}", self.0)
19    }
20}
21
22impl std::error::Error for EvalError {}
23
24
25/// Saved-state token for the hot-loop lambda binding protocol.
26/// `push_lam` returns one; `pop_lam` consumes it. Avoids full `Env` clone
27/// per iteration — only `current` and the single named binding are swapped.
28pub struct LamFrame {
29    /// Previous value of `Env::current`, restored by `pop_lam`.
30    prev_current: Val,
31    /// Describes what happened to the named variable slot so `pop_lam` can undo it.
32    prev_var: LamVarPrev,
33}
34
35/// Encodes the three possible states of a named-variable slot before `push_lam`.
36enum LamVarPrev {
37    /// No variable name was bound; only `current` was updated.
38    None,
39    /// The variable was not present and was appended; `pop_lam` must pop it.
40    Pushed,
41    /// The variable existed at `usize`; its previous `Val` is saved for restoration.
42    Replaced(usize, Val),
43}
44
45/// Per-scope evaluation environment. Cloned on scope entry; mutated in place
46/// for tight loops via `push_lam`/`pop_lam`. Carries `root` ($), `current`
47/// (@), and a flat var list for let-bindings.
48#[derive(Clone)]
49pub struct Env {
50    /// Flat list of named let-bindings; searched in reverse for shadowing.
51    vars: SmallVec<[(Arc<str>, Val); 4]>,
52    /// The document root bound to `$`; immutable within a single query.
53    pub root: Val,
54    /// The current focus bound to `@`; updated per iteration in loops and chains.
55    pub current: Val,
56}
57
58impl Env {
59    /// Create a fresh environment with `root` bound to both `$` and `@`.
60    pub fn new(root: Val) -> Self {
61        Self {
62            vars: SmallVec::new(),
63            root: root.clone(),
64            current: root,
65        }
66    }
67
68    /// Return a child environment that inherits all vars and root but sets a new `current`.
69    #[inline]
70    pub fn with_current(&self, current: Val) -> Self {
71        Self {
72            vars: self.vars.clone(),
73            root: self.root.clone(),
74            current,
75        }
76    }
77
78    /// Replace `current` in place and return the displaced value for later restoration.
79    #[inline]
80    pub fn swap_current(&mut self, new: Val) -> Val {
81        std::mem::replace(&mut self.current, new)
82    }
83
84    /// Restore a previously swapped `current` value without allocating a new `Env`.
85    #[inline]
86    pub fn restore_current(&mut self, old: Val) {
87        self.current = old;
88    }
89
90    /// Return `true` when no let-bindings are currently in scope. Used by
91    /// HOF kernel fast paths to detect that a `LoadIdent` cannot resolve
92    /// to a binding and is therefore safe to interpret as a field read on
93    /// the current item.
94    #[inline]
95    pub fn has_no_vars(&self) -> bool {
96        self.vars.is_empty()
97    }
98
99    /// Look up a named variable; searches in reverse so the innermost binding wins.
100    #[inline]
101    pub fn get_var(&self, name: &str) -> Option<&Val> {
102        self.vars
103            .iter()
104            .rev()
105            .find(|(k, _)| k.as_ref() == name)
106            .map(|(_, v)| v)
107    }
108
109    /// Return a child environment that shadows (or inserts) `name = val`; does not mutate `self`.
110    pub fn with_var(&self, name: &str, val: Val) -> Self {
111        let mut vars = self.vars.clone();
112        if let Some(pos) = vars.iter().position(|(k, _)| k.as_ref() == name) {
113            vars[pos].1 = val;
114        } else {
115            vars.push((Arc::from(name), val));
116        }
117        Self {
118            vars,
119            root: self.root.clone(),
120            current: self.current.clone(),
121        }
122    }
123
124    /// Bind `val` to `current` (and optionally to `name`) in place for one loop iteration.
125    /// Returns a `LamFrame` that records the displaced state; must be balanced with `pop_lam`.
126    #[inline]
127    pub fn push_lam(&mut self, name: Option<&str>, val: Val) -> LamFrame {
128        let prev_current = std::mem::replace(&mut self.current, val.clone());
129        let prev_var = match name {
130            None => LamVarPrev::None,
131            Some(n) => {
132                if let Some(pos) = self.vars.iter().position(|(k, _)| k.as_ref() == n) {
133                    let prev = std::mem::replace(&mut self.vars[pos].1, val);
134                    LamVarPrev::Replaced(pos, prev)
135                } else {
136                    self.vars.push((Arc::from(n), val));
137                    LamVarPrev::Pushed
138                }
139            }
140        };
141        LamFrame {
142            prev_current,
143            prev_var,
144        }
145    }
146
147    /// Restore `Env` to the state captured in `frame`; must be called after every `push_lam`.
148    #[inline]
149    pub fn pop_lam(&mut self, frame: LamFrame) {
150        self.current = frame.prev_current;
151        match frame.prev_var {
152            LamVarPrev::None => {}
153            LamVarPrev::Pushed => {
154                self.vars.pop();
155            }
156            LamVarPrev::Replaced(pos, prev) => {
157                self.vars[pos].1 = prev;
158            }
159        }
160    }
161}