Skip to main content

tabnas/
context.rs

1// Copyright (c) 2013-2026 Richard Rodger, MIT License
2
3use crate::error::TabnasError;
4use crate::options::Options;
5use crate::rule::{Rule, RuleSnapshot};
6use crate::token::Token;
7use crate::value::unwrap_arc;
8use crate::value::Value;
9use indexmap::IndexMap;
10use std::cell::RefCell;
11use std::collections::VecDeque;
12use std::fmt;
13use std::rc::Rc;
14use std::sync::Arc;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ActionError {
18    pub code: String,
19    pub detail: String,
20}
21
22impl ActionError {
23    pub fn new(code: impl Into<String>, detail: impl Into<String>) -> Self {
24        Self {
25            code: code.into(),
26            detail: detail.into(),
27        }
28    }
29}
30
31impl fmt::Display for ActionError {
32    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33        formatter.write_str(&self.detail)
34    }
35}
36
37impl std::error::Error for ActionError {}
38
39/// Read-only identity and grammar summary for the owning parser instance.
40/// Callbacks that need to mutate grammar receive `&mut Tabnas` during plugin
41/// installation; parse-time callbacks use this stable per-parse snapshot.
42#[derive(Debug, Clone, Default, PartialEq, Eq)]
43pub struct InstanceInfo {
44    pub id: String,
45    pub parent_id: Option<String>,
46    pub tag: String,
47    pub plugins: Vec<String>,
48    pub rule_names: Vec<String>,
49}
50
51/// Typed form of TypeScript's `parent_ctx` parse argument. Custom metadata
52/// and plugin state are deep-merged into the new Context, while errors and
53/// parser-owned cursor/rule fields always start fresh for each parse.
54#[derive(Debug, Clone, Default, PartialEq)]
55pub struct ContextSeed {
56    pub meta: Option<Value>,
57    pub u: IndexMap<String, Value>,
58}
59
60impl From<ActionError> for TabnasError {
61    fn from(action_error: ActionError) -> Self {
62        let mut error = TabnasError::new(action_error.code, "", "", 0, 1, 1);
63        error.detail = action_error.detail;
64        error
65    }
66}
67
68/// Drop the frames that left the stack, snapshot the frames that arrived.
69fn follow_stack(frames: &mut Vec<Rc<RuleSnapshot>>, stack: &[Rule]) {
70    frames.truncate(stack.len());
71    for rule in &stack[frames.len()..] {
72        frames.push(rule.snapshot());
73    }
74}
75
76/// Debug-only equality between a retained snapshot and the rule it describes.
77/// It covers the state a snapshot copies by value; `node` is shared through
78/// an `Rc`, and the rule links are snapshots in their own right, so neither
79/// can drift.
80///
81/// Values go through `deep_equal` rather than `==`. A grammar may put a NaN
82/// on a rule, `Value` derives `PartialEq`, and NaN is not equal to itself,
83/// so `==` would report an unchanged frame as drifted and panic a debug
84/// build over a legitimate parse.
85#[cfg(debug_assertions)]
86fn same_rule(snapshot: &crate::RuleSnapshot, rule: &Rule) -> bool {
87    snapshot.i == rule.i
88        && snapshot.d == rule.d
89        // `child_node` is deliberately absent: it lives on the rule
90        // rather than the snapshot, so there is nothing to compare.
91        && snapshot.name == rule.name
92        && snapshot.state == rule.state
93        && snapshot.need == rule.need
94        && snapshot.bo == rule.bo
95        && snapshot.ao == rule.ao
96        && snapshot.bc == rule.bc
97        && snapshot.ac == rule.ac
98        && snapshot.next_rule_name == rule.next_rule_name
99        && snapshot.n == rule.n
100        && same_values(&snapshot.u, &rule.u)
101        && same_values(&snapshot.k, &rule.k)
102        && same_tokens(&snapshot.o, &rule.o)
103        && same_tokens(&snapshot.c, &rule.c)
104        && same_link(&snapshot.parent_rule, &rule.parent_rule)
105        && same_link(&snapshot.child_rule, &rule.child_rule)
106        && same_link(&snapshot.prev_rule, &rule.prev_rule)
107        && same_link(&snapshot.next_rule, &rule.next_rule)
108}
109
110#[cfg(debug_assertions)]
111fn same_values(
112    left: &std::collections::HashMap<String, Value>,
113    right: &std::collections::HashMap<String, Value>,
114) -> bool {
115    left.len() == right.len()
116        && left
117            .iter()
118            .all(|(key, value)| right.get(key).is_some_and(|other| value.deep_equal(other)))
119}
120
121/// `val_fn` is a callback and `ignored` is trivia the lexer attaches once;
122/// neither is state the parse loop revises, so the comparison stops at the
123/// fields a moved frame would show.
124#[cfg(debug_assertions)]
125fn same_tokens(left: &[Token], right: &[Token]) -> bool {
126    left.len() == right.len()
127        && left.iter().zip(right).all(|(left, right)| {
128            left.name == right.name
129                && left.tin == right.tin
130                && left.src == right.src
131                && left.len == right.len
132                && left.site == right.site
133                && left.err == right.err
134                && left.why == right.why
135                && left.val.deep_equal(&right.val)
136                && same_values(left.use_data(), right.use_data())
137        })
138}
139
140/// The rule links are snapshots themselves, so identity is the question
141/// worth asking: a relinked frame points at a different snapshot, and
142/// comparing the pointers says so without walking the ancestry.
143#[cfg(debug_assertions)]
144fn same_link(
145    snapshot: &Option<std::rc::Rc<crate::RuleSnapshot>>,
146    rule: &Option<std::rc::Rc<crate::RuleSnapshot>>,
147) -> bool {
148    match (snapshot, rule) {
149        (None, None) => true,
150        (Some(left), Some(right)) => std::rc::Rc::ptr_eq(left, right),
151        _ => false,
152    }
153}
154
155/// Mutable state for one parse run.
156///
157/// Consumed tokens are retained in `v` so actions can mark and rewind the
158/// parser without asking the lexer to scan source text a second time. Marks
159/// are absolute (`v_abs`), so bounded-history eviction does not change their
160/// meaning.
161#[derive(Debug)]
162pub struct Context {
163    /// Zero-based rule-loop iteration currently being processed.
164    pub iteration: usize,
165    /// Full source text for plugin callbacks and diagnostics.
166    pub source: String,
167    /// Caller-supplied per-parse metadata.
168    pub meta: Value,
169    /// Custom per-parse plugin data bag.
170    pub u: IndexMap<String, Value>,
171    /// Errors recorded so far during recovery.
172    pub errs: Vec<TabnasError>,
173    /// Resolved options for this parse. Shared with the parser and its
174    /// lexer, which is sound because nothing writes to them once a parse
175    /// has started. Each parse still gets the options as they stood when
176    /// it began, so
177    /// callbacks cannot mutate the shared parser configuration.
178    pub options: Arc<Options>,
179    /// Owning instance identity, installed plugins, and grammar names.
180    pub instance: InstanceInfo,
181    /// Snapshot of the current rule and its ancestor stack. The live rule is
182    /// still supplied separately to callbacks so mutation remains explicit.
183    pub rule: Option<Rc<RuleSnapshot>>,
184    pub rule_stack: Vec<Rc<RuleSnapshot>>,
185    /// The same stack as the engine wrote it, kept only where debug
186    /// assertions are on. `rule_stack` is public and a callback may write to
187    /// it; this one is what the engine checks itself against.
188    #[cfg(debug_assertions)]
189    rule_stack_shadow: Vec<Rc<RuleSnapshot>>,
190    /// Retained consumed-token history, oldest first.
191    /// A `VecDeque` because the history is trimmed from its front once
192    /// it outgrows `options.rewind.history`. As a `Vec` that trim moved
193    /// every retained token, which amortised to one `Token` memmove per
194    /// token consumed -- 1.8% of a parse, for a buffer nothing reads
195    /// unless a rewind happens.
196    pub v: VecDeque<Token>,
197    /// Absolute number of tokens consumed minus tokens rewound.
198    pub v_abs: usize,
199    /// Current lookahead buffer, oldest first.
200    pub t: Vec<Token>,
201    replay: VecDeque<Token>,
202    history_limit: Option<usize>,
203    root: Option<Rc<RefCell<Value>>>,
204    pub(crate) recover_at: Option<usize>,
205    pub(crate) recover_si: Option<usize>,
206    pub(crate) bad_to: Option<usize>,
207    pub(crate) bad_error: Option<usize>,
208}
209
210impl Context {
211    pub(crate) fn new(
212        history_limit: Option<usize>,
213        source: impl Into<String>,
214        meta: Value,
215        options: Arc<Options>,
216        instance: InstanceInfo,
217    ) -> Self {
218        Self {
219            iteration: 0,
220            source: source.into(),
221            meta,
222            u: IndexMap::new(),
223            errs: Vec::new(),
224            options,
225            instance,
226            rule: None,
227            rule_stack: Vec::new(),
228            #[cfg(debug_assertions)]
229            rule_stack_shadow: Vec::new(),
230            v: VecDeque::new(),
231            v_abs: 0,
232            t: Vec::with_capacity(8),
233            replay: VecDeque::new(),
234            history_limit,
235            root: None,
236            recover_at: None,
237            recover_si: None,
238            bad_to: None,
239            bad_error: None,
240        }
241    }
242
243    /// Record the current absolute parse position for a later rewind.
244    pub fn mark(&self) -> usize {
245        self.v_abs
246    }
247
248    /// Most recently consumed token.
249    pub fn v1(&self) -> Option<&Token> {
250        self.v.back()
251    }
252
253    /// Token consumed immediately before `v1`.
254    pub fn v2(&self) -> Option<&Token> {
255        self.v.get(self.v.len().wrapping_sub(2))
256    }
257
258    pub fn t0(&self) -> Option<&Token> {
259        self.t.first()
260    }
261
262    pub fn t1(&self) -> Option<&Token> {
263        self.t.get(1)
264    }
265
266    pub fn set_t0(&mut self, token: Token) {
267        if self.t.is_empty() {
268            self.t.push(token);
269        } else {
270            self.t[0] = token;
271        }
272    }
273
274    pub fn set_t1(&mut self, token: Token) {
275        while self.t.len() < 2 {
276            self.t.push(Token::no_token());
277        }
278        self.t[1] = token;
279    }
280
281    pub fn set_v1(&mut self, token: Token) {
282        if let Some(last) = self.v.back_mut() {
283            *last = token;
284        } else {
285            self.v.push_back(token);
286        }
287    }
288
289    pub fn set_v2(&mut self, token: Token) {
290        match self.v.len() {
291            0 => self.v.push_back(token),
292            1 => self.v.push_front(token),
293            length => self.v[length - 2] = token,
294        }
295    }
296
297    pub fn root(&self) -> Option<Value> {
298        self.root.as_ref().map(|root| root.borrow().clone())
299    }
300
301    pub(crate) fn set_root(&mut self, root: Rc<RefCell<Value>>) {
302        self.root = Some(root);
303    }
304
305    pub(crate) fn set_active(&mut self, rule: &Rule, stack: &[Rule]) {
306        self.set_rule(rule);
307        self.sync_rule_stack(stack);
308    }
309
310    /// Bring `rule_stack` into step with the parse loop's ancestor stack.
311    ///
312    /// Rebuilding every entry here, which is what this used to do, costs one
313    /// deep snapshot per ancestor per loop iteration. A grammar whose rules
314    /// nest with the input pays that on every step, so a recogniser like the
315    /// even-palindrome one runs in O(n^2) where the TypeScript and Go engines
316    /// run in O(n) — they publish the stack as live rule handles and copy
317    /// nothing. Measured on a 16 KiB palindrome: 155 s before, 0.25 s after.
318    ///
319    /// A frame is only ever mutated while it is the rule the loop is working
320    /// on, and that rule is not in `stack` — it is passed separately and
321    /// re-snapshotted every call. So a frame's snapshot, taken when the frame
322    /// was pushed, still describes it for as long as it stays buried.
323    ///
324    /// This is how the mature engines carry it. TypeScript writes
325    /// `ctx.rs[ctx.rsI++] = rule` and reads back `ctx.rs[--ctx.rsI]`; Go does
326    /// the same through `ctx.RS` and `ctx.RSI`. Neither rebuilds, and both
327    /// leave the stack as reachable from a callback as this one is. Rust was
328    /// the outlier, and being the outlier is what cost it the extra order.
329    ///
330    /// `rule_stack` is a public field, so a callback can write to it, and a
331    /// write that keeps the length is carried forward from here rather than
332    /// overwritten. That matches what a callback writing to `ctx.rs` gets
333    /// from the other two engines. It is also why the assertion below reads
334    /// `rule_stack_shadow` and not `rule_stack`: the claim being checked is
335    /// about the engine's own bookkeeping, and a debug build must not panic
336    /// because a callback reached into a field the engine publishes.
337    ///
338    /// That a buried frame does not move is a claim about the whole parse
339    /// loop, recovery paths included, so it is checked rather than asserted
340    /// in prose: the assertion compares every retained frame against the live
341    /// rule it describes, on every call, and the test suite runs with debug
342    /// assertions on.
343    fn sync_rule_stack(&mut self, stack: &[Rule]) {
344        follow_stack(&mut self.rule_stack, stack);
345        #[cfg(debug_assertions)]
346        {
347            follow_stack(&mut self.rule_stack_shadow, stack);
348            debug_assert!(
349                self.rule_stack_shadow
350                    .iter()
351                    .zip(stack)
352                    .all(|(snapshot, rule)| same_rule(snapshot, rule)),
353                "the incremental rule stack drifted from the live parse stack"
354            );
355        }
356    }
357
358    pub(crate) fn set_rule(&mut self, rule: &Rule) {
359        self.rule = Some(rule.snapshot());
360    }
361
362    pub(crate) fn apply_seed(&mut self, seed: &ContextSeed) {
363        if let Some(meta) = &seed.meta {
364            self.meta = merge_seed_value(self.meta.clone(), meta.clone());
365        }
366        for (key, value) in &seed.u {
367            let previous = self.u.shift_remove(key).unwrap_or(Value::Undefined);
368            self.u
369                .insert(key.clone(), merge_seed_value(previous, value.clone()));
370        }
371        self.errs.clear();
372    }
373
374    /// Replay every token consumed since `mark`.
375    ///
376    /// Already-fetched lookahead remains behind the rewound tokens. An error
377    /// means the requested mark has fallen outside the retained history
378    /// window; callers can increase `options.rewind.history` or select
379    /// unbounded history.
380    pub fn rewind(&mut self, mark: usize) -> Result<(), ActionError> {
381        let Some(count) = self.v_abs.checked_sub(mark) else {
382            return Ok(());
383        };
384        if count == 0 {
385            return Ok(());
386        }
387        if count > self.v.len() {
388            return Err(ActionError::new(
389                "internal",
390                format!(
391                    "tabnas: ctx.rewind target {mark} is outside the retained history window \
392                 (oldest mark available is {}, current is {}); increase \
393                 options.rewind.history",
394                    self.v_abs - self.v.len(),
395                    self.v_abs,
396                ),
397            ));
398        }
399
400        let retained_at = self.v.len() - count;
401        let rewound = self.v.split_off(retained_at);
402        let lookahead = std::mem::take(&mut self.t);
403        let pending = std::mem::take(&mut self.replay);
404        self.replay = rewound
405            .into_iter()
406            .chain(lookahead)
407            .chain(pending)
408            .collect();
409        self.v_abs -= count;
410        Ok(())
411    }
412
413    pub(crate) fn record_consumed(&mut self, count: usize) {
414        if count == 0 {
415            return;
416        }
417        let count = count.min(self.t.len());
418        self.v.extend(self.t.drain(0..count));
419        self.v_abs += count;
420
421        if let Some(limit) = self.history_limit {
422            if self.v.len() > 2 * limit {
423                let remove = self.v.len() - limit;
424                self.v.drain(0..remove);
425            }
426        }
427    }
428
429    pub(crate) fn next_replay(&mut self) -> Option<Token> {
430        self.replay.pop_front()
431    }
432
433    pub(crate) fn take_replay(&mut self) -> VecDeque<Token> {
434        std::mem::take(&mut self.replay)
435    }
436
437    pub(crate) fn restore_replay(&mut self, replay: VecDeque<Token>) {
438        self.replay = replay;
439    }
440}
441
442fn merge_seed_value(base: Value, overlay: Value) -> Value {
443    match (base, overlay) {
444        (base, Value::Undefined) => base,
445        (Value::Object(base), Value::Object(overlay)) => {
446            let mut base = unwrap_arc(base);
447            for (key, value) in unwrap_arc(overlay) {
448                let previous = base.shift_remove(&key).unwrap_or(Value::Undefined);
449                base.insert(key, merge_seed_value(previous, value));
450            }
451            Value::object(base)
452        }
453        (_, overlay) => overlay,
454    }
455}