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}