Skip to main content

jetro_core/
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::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    /// Look up a named variable; searches in reverse so the innermost binding wins.
91    #[inline]
92    pub fn get_var(&self, name: &str) -> Option<&Val> {
93        self.vars
94            .iter()
95            .rev()
96            .find(|(k, _)| k.as_ref() == name)
97            .map(|(_, v)| v)
98    }
99
100    /// Return a child environment that shadows (or inserts) `name = val`; does not mutate `self`.
101    pub fn with_var(&self, name: &str, val: Val) -> Self {
102        let mut vars = self.vars.clone();
103        if let Some(pos) = vars.iter().position(|(k, _)| k.as_ref() == name) {
104            vars[pos].1 = val;
105        } else {
106            vars.push((Arc::from(name), val));
107        }
108        Self {
109            vars,
110            root: self.root.clone(),
111            current: self.current.clone(),
112        }
113    }
114
115    /// Bind `val` to `current` (and optionally to `name`) in place for one loop iteration.
116    /// Returns a `LamFrame` that records the displaced state; must be balanced with `pop_lam`.
117    #[inline]
118    pub fn push_lam(&mut self, name: Option<&str>, val: Val) -> LamFrame {
119        let prev_current = std::mem::replace(&mut self.current, val.clone());
120        let prev_var = match name {
121            None => LamVarPrev::None,
122            Some(n) => {
123                if let Some(pos) = self.vars.iter().position(|(k, _)| k.as_ref() == n) {
124                    let prev = std::mem::replace(&mut self.vars[pos].1, val);
125                    LamVarPrev::Replaced(pos, prev)
126                } else {
127                    self.vars.push((Arc::from(n), val));
128                    LamVarPrev::Pushed
129                }
130            }
131        };
132        LamFrame {
133            prev_current,
134            prev_var,
135        }
136    }
137
138    /// Restore `Env` to the state captured in `frame`; must be called after every `push_lam`.
139    #[inline]
140    pub fn pop_lam(&mut self, frame: LamFrame) {
141        self.current = frame.prev_current;
142        match frame.prev_var {
143            LamVarPrev::None => {}
144            LamVarPrev::Pushed => {
145                self.vars.pop();
146            }
147            LamVarPrev::Replaced(pos, prev) => {
148                self.vars[pos].1 = prev;
149            }
150        }
151    }
152}