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}