Skip to main content

rdf_parsers/
a_star.rs

1use std::{
2    collections::{BinaryHeap, HashMap},
3    fmt::Debug,
4};
5
6use crate::{Error, FatToken, IncrementalBias, Step, TokenTrait, list::List};
7
8/// Controls whether the A* search performs fault-tolerant error recovery.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum ParseMode {
11    /// Full fault-tolerant mode: explores error-recovery branches, tracks
12    /// role-agreement fingerprints, maintains a best-at-EOF fallback, and
13    /// applies an incremental heuristic.  Use when the document may contain
14    /// errors and a best-effort CST is required.
15    FaultTolerant,
16    /// Fast mode: no error-recovery branches, no prevInfo fingerprints, no
17    /// heuristic precomputation, no best-at-EOF fallback.  Returns `None`
18    /// from `consume` if the document contains any error.  Use when the
19    /// document is known to be correct and maximum throughput is desired.
20    Fast,
21}
22
23#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
24pub struct Fingerprint(pub u128);
25
26fn descend(fp: Fingerprint, branch_id: u32) -> Fingerprint {
27    const M: u128 = 0x9E3779B97F4A7C15_6A09E667F3BCC909;
28    Fingerprint(fp.0.wrapping_mul(M) ^ (branch_id as u128).wrapping_add(0xD6E8FEB86659FD93))
29}
30
31pub trait ParserTrait: Debug + Sized + 'static {
32    type Kind: 'static + TokenTrait;
33
34    fn step(&self, el: &Element<Self>, state: &mut AStar<Self>);
35    fn at(&self) -> usize;
36    fn element_kind(&self) -> Self::Kind;
37
38    /// Precomputed dist(q, a): minimum insertion cost to reach a point where
39    /// terminal `terminal` can be matched, starting from this parser state
40    /// (kind, state_number).  Default: 0 (always admissible).
41    fn state_dist(&self, _terminal: &Self::Kind) -> isize {
42        0
43    }
44}
45
46enum IsExpectedElement {
47    True,
48    False,
49    Unkown,
50}
51
52/// Default maximum number of heap pops before the search gives up and returns
53/// the best partial result found so far.  Prevents pathological inputs from
54/// blocking indefinitely.
55pub const DEFAULT_MAX_ITERATIONS: usize = 1_000_000;
56
57/// Maximum number of consecutive error operations (insertions + deletions)
58/// before an element is dropped.  Prevents unbounded error cascades on
59/// deeply broken input.
60pub const DEFAULT_MAX_REPAIR_SPAN: u16 = 10;
61
62/// Number of consecutive error operations that triggers sync-point scanning.
63/// The paper's (R3S-n) is always available; we gate it at 2 to avoid
64/// scanning overhead on isolated single-token errors while still activating
65/// early enough to prevent dead-end branch accumulation.
66const SYNC_THRESHOLD: u16 = 2;
67
68/// Maximum number of tokens to scan ahead when looking for a sync point.
69const MAX_SYNC_SCAN: usize = 50;
70
71pub struct AStar<'a, R: ParserTrait> {
72    /// Maps each state key to the best (lowest) cost seen so far (queued or
73    /// processed).  Used for lazy-deletion dedup: elements with worse (higher)
74    /// costs are rejected early in `add_element`; stale heap entries are
75    /// skipped in `consume`.
76    done: HashMap<(Fingerprint, usize, usize), isize>,
77    tokens: &'a [FatToken<R::Kind>],
78    todo: BinaryHeap<Element<R>>,
79    pub bias: IncrementalBias,
80    /// Admissible heuristic: `heuristic[pos]` is a non-positive lower bound on
81    /// the minimum additional cost from token position `pos` to end of input.
82    /// Computed as suffix sum of potential agreement bonuses: each remaining
83    /// token with `old_kind` could yield a `-strength` discount.  This is
84    /// optimistic (assumes every token lands in its previous role) and
85    /// therefore admissible.
86    /// In `Fast` mode this is always empty; 0 is used directly.
87    heuristic: Vec<isize>,
88    /// Number of elements popped from the heap so far.
89    iterations: usize,
90    /// Maximum number of heap pops before giving up.  See `DEFAULT_MAX_ITERATIONS`.
91    max_iterations: usize,
92    /// Best element that reached end-of-input during search, tracked as
93    /// `(cost, step_list, parent_depth)`.  Used as a fallback when the search
94    /// times out before finding a fully-unwound parse (depth == 1).
95    /// Never populated in `Fast` mode.
96    best_at_eof: Option<(isize, List<Step<R::Kind>>, usize)>,
97    /// Whether to perform fault-tolerant error recovery.
98    mode: ParseMode,
99    /// Maximum consecutive error operations before an element is dropped.
100    max_repair_span: u16,
101}
102
103impl<'a, R: ParserTrait> AStar<'a, R> {
104    fn new(tokens: &'a [FatToken<R::Kind>], bias: IncrementalBias, max_iterations: usize) -> Self {
105        let mut heuristic = vec![0isize; tokens.len() + 1];
106        for i in (0..tokens.len()).rev() {
107            let offset = if tokens[i].kind.skips() {
108                0
109            } else {
110                tokens[i].kind.max_error_value()
111            };
112            // Maybe off by one
113            heuristic[i] = heuristic[i + 1] + offset;
114        }
115        Self {
116            tokens,
117            done: HashMap::new(),
118            todo: BinaryHeap::new(),
119            bias,
120            heuristic,
121            iterations: 0,
122            max_iterations,
123            best_at_eof: None,
124            mode: ParseMode::FaultTolerant,
125            max_repair_span: DEFAULT_MAX_REPAIR_SPAN,
126        }
127    }
128
129    /// Creates an `AStar` configured for fast, non-fault-tolerant parsing.
130    /// Skips heuristic precomputation, incremental bias, and best-at-EOF
131    /// tracking.  Returns `None` from `consume` if the document contains any
132    /// error.
133    fn new_fast(tokens: &'a [FatToken<R::Kind>]) -> Self {
134        Self {
135            tokens,
136            done: HashMap::new(),
137            todo: BinaryHeap::new(),
138            bias: IncrementalBias { strength: 0 },
139            heuristic: Vec::new(),
140            iterations: 0,
141            max_iterations: usize::MAX,
142            best_at_eof: None,
143            mode: ParseMode::Fast,
144            max_repair_span: u16::MAX,
145        }
146    }
147
148    pub fn consume(&mut self, root: R) -> Option<List<Step<R::Kind>>> {
149        let start_idx = self.get_actual_index(0);
150        let h = if self.mode == ParseMode::Fast {
151            0
152        } else if let Some(heurisitic) = self.heuristic.get(start_idx) {
153            *heurisitic
154        } else {
155            return Some(List::default());
156        };
157        self.todo.push(Element::new_at(root, h, start_idx));
158
159        loop {
160            let e = self.todo.pop()?;
161            self.iterations += 1;
162
163            if let Some((head, _)) = e.parent.head() {
164                let state = (e.state.0, e.state.1, head.at());
165
166                // Skip stale entries: a better-scoring element for this state
167                // was enqueued after this one.
168                match self.done.get(&state) {
169                    Some(&best) if best < e.cost => continue,
170                    _ => {}
171                }
172
173                if e.state.1 == self.tokens.len() {
174                    let depth = e.parent.len();
175                    if self.mode == ParseMode::FaultTolerant
176                        && self
177                            .best_at_eof
178                            .as_ref()
179                            .map_or(true, |&(c, _, _)| e.cost < c)
180                    {
181                        self.best_at_eof = Some((e.cost, e.list.clone(), depth));
182                    }
183                    if depth == 1 {
184                        // In Fast mode, reject any parse that contains an error step.
185                        if self.mode == ParseMode::Fast && e.has_error {
186                            continue;
187                        }
188                        return Some(e.list);
189                    }
190                }
191
192                // Enforce the iteration budget before expanding this element.
193                // In Fast mode max_iterations is usize::MAX, so this never fires.
194                if self.iterations >= self.max_iterations {
195                    eprintln!("Max iterations reached");
196                    break;
197                }
198
199                head.step(&e, self);
200
201                // Token deletion and sync-ahead: in fault-tolerant mode, offer
202                // to skip the current token at a deletion cost.
203                //
204                // Gate on shifts_since_pop: the paper (Kim & Yi, §3.3) proves
205                // that when t > 1 (multiple shifts since last reduction), the
206                // only useful operations are insertions via shift/goto paths.
207                // Offering deletions at this point generates redundant configs
208                // that the A* will eventually discard — but they bloat the heap.
209                if self.mode == ParseMode::FaultTolerant && e.shifts_since_pop <= 1 {
210                    self.try_delete_token(&e);
211                    self.try_sync_ahead(&e);
212                }
213            }
214        }
215
216        // Timeout (or empty heap with no complete parse).  Return the best
217        // element that reached end-of-input, closing any still-open grammar
218        // rules with synthetic End steps.
219        self.best_at_eof.take().map(|(_, list, depth)| {
220            let mut list = list;
221            for _ in 0..depth.saturating_sub(1) {
222                list = list.prepend(Step::end());
223            }
224            list
225        })
226    }
227
228    /// Like `add_element` but pre-charges the element with the minimum error
229    /// cost implied by the current token before adding it when in Fast mode.
230    /// In FaultTolerant mode, explores the branch without pre-charging to allow
231    /// lower-cost alternatives to be found.  Called by generated `push_rule`
232    /// code at sub-rule entry.
233    ///
234    /// `pushed_kind.min_error_for_token(tok)` returns 0 when the token is
235    /// reachable inside the sub-rule (or the rule is nullable), and a positive
236    /// value otherwise.  Pre-charging in Fast mode lets early pruning happen
237    /// automatically via the `has_error` guard in `add_element`.
238    pub fn add_element_checked(&mut self, element: Element<R>, pushed_kind: R::Kind) -> bool {
239        if self.mode == ParseMode::Fast {
240            let mut pos = element.state.1;
241            while self.tokens.get(pos).map_or(false, |t| t.kind.skips()) {
242                pos += 1;
243            }
244            let element = if let Some(tok) = self.tokens.get(pos) {
245                let min_err = pushed_kind.min_error_for_token(&tok.kind);
246                if min_err > 0 {
247                    Element {
248                        cost: element.cost + min_err,
249                        has_error: true,
250                        ..element
251                    }
252                } else {
253                    element
254                }
255            } else {
256                element
257            };
258            return self.add_element(element);
259        }
260        self.add_element(element)
261    }
262
263    pub fn add_element(&mut self, mut element: Element<R>) -> bool {
264        // In Fast mode, elements that already contain an error can never
265        // contribute to a valid parse.  Dropping them here prevents the heap
266        // from filling with dead branches and stops infinite-loop searches on
267        // invalid documents.
268        if self.mode == ParseMode::Fast && element.has_error {
269            return false;
270        }
271
272        // Drop elements that have exceeded the maximum consecutive error span.
273        if element.consecutive_errors > self.max_repair_span {
274            return false;
275        }
276
277        // dist(q, a) heuristic boost: the top-of-stack rule's minimum
278        // insertion cost to reach the next input token.  This is additive with
279        // the suffix-sum heuristic (which counts matching costs for remaining
280        // tokens) because they measure different, non-overlapping costs:
281        //   h_suffix = lower bound on cost of matching all remaining tokens
282        //   dist     = lower bound on insertion cost before next token can match
283        if self.mode == ParseMode::FaultTolerant {
284            if let Some((head, _)) = element.parent.head() {
285                let mut pos = element.state.1;
286                while self.tokens.get(pos).map_or(false, |t| t.kind.skips()) {
287                    pos += 1;
288                }
289                let new_d = self
290                    .tokens
291                    .get(pos)
292                    .map(|tok| head.state_dist(&tok.kind).max(0))
293                    .unwrap_or(0);
294                element.h = element.h - element.dist + new_d;
295                element.dist = new_d;
296            }
297        }
298
299        let at = element.parent.head().map(|x| x.0.at()).unwrap_or(0);
300        let state = (element.state.0, element.state.1, at);
301
302        match self.done.entry(state) {
303            std::collections::hash_map::Entry::Occupied(mut e) => {
304                if element.cost >= *e.get() {
305                    return false; // reject: equal-or-worse (higher cost) duplicate
306                }
307                *e.get_mut() = element.cost; // better (lower cost) path found
308            }
309            std::collections::hash_map::Entry::Vacant(v) => {
310                v.insert(element.cost);
311            }
312        }
313
314        self.todo.push(element);
315        true
316    }
317
318    fn is_expected_element(
319        &self,
320        element: &Element<R>,
321        found: &FatToken<R::Kind>,
322    ) -> IsExpectedElement {
323        match found.old_kind() {
324            Some(old_fp) if old_fp == element.state.0 => IsExpectedElement::True,
325            Some(_) => IsExpectedElement::False,
326            None => IsExpectedElement::Unkown,
327        }
328    }
329
330    pub fn expect_as(&mut self, element: &Element<R>, token: R::Kind) -> Element<R> {
331        let (out, fallback) = self.expect_with_fallback(element, token, false);
332        if let Some(fb) = fallback {
333            if let Some(popped) = fb.pop() {
334                self.add_element(popped);
335            }
336        }
337        out
338    }
339
340    fn get_actual_index(&mut self, mut idx: usize) -> usize {
341        while self
342            .tokens
343            .get(idx)
344            .map(|x| x.kind.skips())
345            .unwrap_or(false)
346        {
347            idx += 1;
348        }
349
350        idx
351    }
352
353    fn expect_with_fallback(
354        &mut self,
355        element: &Element<R>,
356        token: R::Kind,
357        wrapped: bool,
358    ) -> (Element<R>, Option<Element<R>>) {
359        let create_list = |step: Step<R::Kind>| {
360            if wrapped {
361                element
362                    .list
363                    .prepend(Step::start(token.clone()))
364                    .prepend(step)
365                    .prepend(Step::end())
366            } else {
367                element.list.prepend(step)
368            }
369        };
370
371        let idx = element.state.1;
372
373        // This thing actually matches
374        if let Some(found) = self.tokens.get(idx) {
375            if found.kind == token {
376                let next = self.get_actual_index(idx + 1);
377                // Update bracket depth after consuming this token.
378                let new_depth = element.current_depth + token.bracket_delta();
379
380                let (fallback, bias, h) = if self.mode == ParseMode::Fast {
381                    // Fast mode: no role-conflict check, no fallback branch, h = 0.
382                    (None, 0, 0)
383                } else {
384                    let is_expected_element = self.is_expected_element(element, found);
385                    let (fallback, bias) = match is_expected_element {
386                        IsExpectedElement::True | IsExpectedElement::Unkown => (None, 0),
387                        IsExpectedElement::False => {
388                            // Compute the depth delta this token carries from the
389                            // previous parse and how it relates to the current depth.
390                            let depth_delta = found
391                                .old_depth()
392                                .map(|old| element.current_depth as i16 - old as i16);
393                            let committed_delta = element.assumed_depth_delta as i16;
394                            if depth_delta == Some(committed_delta) && committed_delta != 0 {
395                                // This conflict is fully explained by the depth shift
396                                // already committed to — treat it as a free agree.
397                                (None, 0)
398                            } else if depth_delta == Some(committed_delta) {
399                                // Same depth, fingerprint wrong: genuine role conflict.
400                                // Restore the original +50 bias so the A* prefers a
401                                // correct parse over reusing a token in the wrong role.
402                                let insert_error = Some(Element {
403                                    list: create_list(Step::error(Error::Expected(
404                                        token.clone().into(),
405                                    ))),
406                                    parent: element.parent.clone(),
407                                    cost: element.cost + token.max_error_value(),
408                                    h: element.h,
409                                    state: element.state,
410                                    has_error: true,
411                                    assumed_depth_delta: element.assumed_depth_delta,
412                                    current_depth: element.current_depth,
413                                    consecutive_errors: element
414                                        .consecutive_errors
415                                        .saturating_add(1),
416                                    shifts_since_pop: element.shifts_since_pop,
417                                    dist: element.dist,
418                                });
419                                (insert_error, 50)
420                            } else {
421                                // New or different depth conflict. Create two branches:
422                                // (a) adopt this depth delta (one-time cost), and
423                                // (b) insert an error token instead (don't consume).
424                                let insert_error = Some(Element {
425                                    list: create_list(Step::error(Error::Expected(
426                                        token.clone().into(),
427                                    ))),
428                                    parent: element.parent.clone(),
429                                    cost: element.cost + token.max_error_value(),
430                                    h: element.h,
431                                    state: element.state,
432                                    has_error: true,
433                                    assumed_depth_delta: element.assumed_depth_delta,
434                                    current_depth: element.current_depth,
435                                    consecutive_errors: element
436                                        .consecutive_errors
437                                        .saturating_add(1),
438                                    shifts_since_pop: element.shifts_since_pop,
439                                    dist: element.dist,
440                                });
441                                // One-time delta-adoption cost: proportional to how much
442                                // the assumed delta changes.  When depth info is unavailable
443                                // (None — because the previous parse had errors and the token
444                                // was not at depth 0), treat as a free adoption: we cannot
445                                // distinguish a genuine role conflict from a legitimate depth
446                                // shift, so we do not penalise either.
447                                let new_delta = depth_delta
448                                    .map(|d| d.clamp(i8::MIN as i16, i8::MAX as i16) as i8)
449                                    .unwrap_or(element.assumed_depth_delta);
450                                let delta_change =
451                                    (new_delta as i16 - element.assumed_depth_delta as i16).abs();
452                                let adoption_cost = delta_change as isize * token.max_error_value();
453                                (insert_error, adoption_cost)
454                            }
455                        }
456                    };
457                    (fallback, bias, self.heuristic[next])
458                };
459
460                let matched = Element {
461                    list: create_list(Step::Bump(element.state.0)),
462                    parent: element.parent.clone(),
463                    cost: element.cost + token.max_error_value() + bias,
464                    h,
465                    state: (element.state.0, next),
466                    has_error: element.has_error,
467                    assumed_depth_delta: if bias > 0 {
468                        // We adopted a new delta; compute it from the token's depth.
469                        found
470                            .old_depth()
471                            .map(|old| {
472                                (element.current_depth as i16 - old as i16)
473                                    .clamp(i8::MIN as i16, i8::MAX as i16)
474                                    as i8
475                            })
476                            .unwrap_or(element.assumed_depth_delta)
477                    } else {
478                        element.assumed_depth_delta
479                    },
480                    current_depth: new_depth,
481                    consecutive_errors: 0, // successful match resets counter
482                    shifts_since_pop: element.shifts_since_pop.saturating_add(1),
483                    dist: 0, // h is a fresh suffix-sum; no dist folded in yet
484                };
485
486                return (matched, fallback);
487            }
488        }
489
490        let h = if self.mode == ParseMode::Fast {
491            0
492        } else {
493            self.heuristic[idx]
494        };
495
496        // The thing didn't match, so we just add that we expected the thing
497        let error = Element {
498            list: create_list(Step::error(Error::Expected(token.clone().into()))),
499            parent: element.parent.clone(),
500            cost: element.cost + token.max_error_value(),
501            h,
502            state: (element.state.0, idx),
503            has_error: true,
504            assumed_depth_delta: element.assumed_depth_delta,
505            current_depth: element.current_depth,
506            consecutive_errors: element.consecutive_errors.saturating_add(1),
507            shifts_since_pop: element.shifts_since_pop,
508            dist: 0, // h is a fresh suffix-sum
509        };
510
511        (error, None)
512    }
513
514    /// Like `expect_as` but wraps the token in inline CST Start/End nodes,
515    /// avoiding a push_rule + pop cycle (~3 Rc allocs, 2 heap ops, 1 fingerprint).
516    ///
517    /// Returns `(main, fallback)`.  On a role conflict the caller must apply
518    /// `pop_push(next_rule)` to both before enqueuing so they get distinct
519    /// `done` keys.
520    pub fn expect_as_inline(
521        &mut self,
522        element: &Element<R>,
523        token: R::Kind,
524    ) -> (Element<R>, Option<Element<R>>) {
525        self.expect_with_fallback(element, token, true)
526    }
527
528    /// Offer to skip (delete) the current token at a deletion cost.
529    ///
530    /// This is the recursive-descent analog of the (R3D) delete rule in
531    /// Kim & Yi.  The token is not consumed by any grammar rule — it will
532    /// appear in the CST wrapped in an Error node.  The grammar state and
533    /// parent stack are unchanged; only the token position advances.
534    fn try_delete_token(&mut self, element: &Element<R>) {
535        let pos = element.state.1;
536        let Some(tok) = self.tokens.get(pos) else {
537            return;
538        };
539        // Don't delete whitespace / trivia (positions always point past them).
540        if tok.kind.skips() {
541            return;
542        }
543
544        let del_cost = tok.kind.deletion_cost();
545        let next = self.get_actual_index(pos + 1);
546        let h = if let Some(&hv) = self.heuristic.get(next) {
547            hv
548        } else {
549            0
550        };
551
552        // History penalty: deleting a token that had a role in the previous
553        // parse is discouraged — it's more likely the user intended to keep it.
554        let history_penalty = if tok.old_kind().is_some() {
555            self.bias.strength
556        } else {
557            0
558        };
559
560        let del_element = Element {
561            list: element.list.prepend(Step::Delete),
562            parent: element.parent.clone(),
563            cost: element.cost + del_cost + history_penalty,
564            h,
565            state: (element.state.0, next),
566            has_error: true,
567            assumed_depth_delta: element.assumed_depth_delta,
568            current_depth: element.current_depth,
569            consecutive_errors: element.consecutive_errors.saturating_add(1),
570            shifts_since_pop: element.shifts_since_pop,
571            dist: 0, // h is a fresh suffix-sum
572        };
573        self.add_element(del_element);
574    }
575
576    /// Synchronisation-point skip-ahead: when consecutive errors exceed a
577    /// threshold, scan ahead for a token that an ancestor rule can accept.
578    /// If found, create a single recovery element that:
579    ///   1. Pops rules to the accepting ancestor
580    ///   2. Wraps all skipped tokens in a single Error node (via Delete steps)
581    ///   3. Resets consecutive_errors to 0
582    ///
583    /// This is the recursive-descent analog of Kim & Yi's (R3S-n) unrestricted
584    /// shift — instead of shifting through LR states we pop grammar rules.
585    fn try_sync_ahead(&mut self, element: &Element<R>) {
586        if element.consecutive_errors < SYNC_THRESHOLD {
587            return;
588        }
589        let pos = element.state.1;
590        let max_scan = (pos + MAX_SYNC_SCAN).min(self.tokens.len());
591
592        // Track the best sync candidate (lowest cost).
593        let mut best: Option<Element<R>> = None;
594
595        for scan_pos in (pos + 1)..max_scan {
596            let tok = &self.tokens[scan_pos];
597            if tok.kind.skips() {
598                continue;
599            }
600
601            // Walk the parent stack looking for an ancestor that can accept
602            // this token (min_error_for_token == 0).
603            let mut parent = element.parent.clone();
604            let mut list = element.list.clone();
605
606            loop {
607                let Some(((rule, saved_fp), tail)) =
608                    parent.slice().map(|((r, fp), t)| ((r, fp), t))
609                else {
610                    break;
611                };
612
613                let kind = rule.element_kind();
614                if kind.min_error_for_token(&tok.kind) == 0 {
615                    // This ancestor can accept the sync token.
616                    // Cost: deletion cost for each non-trivia token we skip.
617                    let skip_cost: isize = (pos..scan_pos)
618                        .filter_map(|p| self.tokens.get(p))
619                        .filter(|t| !t.kind.skips())
620                        .map(|t| t.kind.deletion_cost())
621                        .sum();
622
623                    let next = self.get_actual_index(scan_pos);
624                    let h = self.heuristic.get(next).copied().unwrap_or(0);
625
626                    let candidate = Element {
627                        list,
628                        parent: parent.clone(),
629                        cost: element.cost + skip_cost,
630                        h,
631                        state: (*saved_fp, next),
632                        has_error: true,
633                        assumed_depth_delta: element.assumed_depth_delta,
634                        current_depth: element.current_depth,
635                        consecutive_errors: 0,
636                        shifts_since_pop: 0, // sync resets — we popped to ancestor
637                        dist: 0,             // h is a fresh suffix-sum
638                    };
639
640                    let f = candidate.cost + candidate.h;
641                    if best.as_ref().map_or(true, |b| f < b.cost + b.h) {
642                        best = Some(candidate);
643                    }
644                    break;
645                }
646
647                // Pop this rule: append an End step.
648                list = list.prepend(Step::end());
649                parent = tail.clone();
650
651                // Don't pop the root rule.
652                if parent.len() == 0 {
653                    break;
654                }
655            }
656        }
657
658        if let Some(sync_element) = best {
659            self.add_element(sync_element);
660        }
661    }
662}
663
664#[derive(Debug)]
665pub struct Element<R: ParserTrait> {
666    list: List<Step<R::Kind>>,
667    parent: List<(R, Fingerprint)>,
668    /// Accumulated error cost (lower = better).  Starts at 0; increases on
669    /// error insertions, unchanged on successful matches.  Bias adjustments
670    /// may slightly decrease cost (agreement) or increase it (conflict).
671    cost: isize,
672    /// Admissible heuristic: non-negative lower bound on the minimum
673    /// additional cost from the current token position to end of input.
674    /// Currently always 0.  `f = cost + h` is the A* priority.
675    h: isize,
676    state: (Fingerprint, usize),
677    /// Set to `true` the moment any `Step::Error` is prepended to this
678    /// element's list.  In `Fast` mode, elements with `has_error = true` are
679    /// never returned as a successful parse result.
680    has_error: bool,
681    /// The bracket nesting depth shift this path has committed to.  When a
682    /// block of tokens moves to a different nesting level, the first conflict
683    /// adopts the delta (paying a one-time cost); subsequent tokens in the
684    /// same block that conflict by the same delta are free.
685    assumed_depth_delta: i8,
686    /// The current bracket nesting depth of the parser at this element's token
687    /// position.  Incremented when an opener is bumped, decremented for closers.
688    current_depth: i8,
689    /// Number of consecutive error operations (Error insertions + Delete steps)
690    /// since the last successful token match (Bump).  Used by the bounded-repair
691    /// mechanism: elements exceeding `max_repair_span` are dropped.
692    consecutive_errors: u16,
693    /// Consecutive shifts (bumps) since the last reduction (pop).  Paper's `t`
694    /// counter.  When > 0, the parser is at a known intra-rule state, so
695    /// `dist(q, a)` is exact (not diluted by the conservative pop = 0 fallback).
696    /// This enables hard pruning of unreachable branches.
697    shifts_since_pop: u16,
698    /// The `state_dist(next_token)` value already folded into `h`.  Maintained
699    /// so that `add_element` can replace it exactly (`h = h - dist + new_d`)
700    /// rather than accumulating it across push/pop expansions at the same token
701    /// position.  Invariant: `h = h_suffix + dist`.  Reset to 0 whenever `h`
702    /// is assigned a fresh suffix-sum value.
703    dist: isize,
704}
705impl<R: ParserTrait> PartialEq for Element<R> {
706    fn eq(&self, other: &Self) -> bool {
707        (self.cost + self.h) == (other.cost + other.h) && self.parent.len() == other.parent.len()
708    }
709}
710impl<R: ParserTrait> Eq for Element<R> {}
711impl<R: ParserTrait> Element<R> {
712    fn new_at(current: R, h: isize, at: usize) -> Self {
713        let parent = List::default();
714        let head = Fingerprint(0);
715        Self {
716            list: List::default(),
717            parent: parent.prepend((current, head)),
718            cost: 0,
719            h,
720            state: (Fingerprint(1), at),
721            has_error: false,
722            assumed_depth_delta: 0,
723            current_depth: 0,
724            consecutive_errors: 0,
725            shifts_since_pop: 0,
726            dist: 0,
727        }
728    }
729
730    pub fn pop_push(&self, rule: R) -> Self {
731        let ((_, f), tail) = self.parent.slice().unwrap();
732        let parent = tail.prepend((rule, *f));
733        Self {
734            parent,
735            list: self.list.clone(),
736            cost: self.cost,
737            h: self.h,
738            state: self.state.clone(),
739            has_error: self.has_error,
740            assumed_depth_delta: self.assumed_depth_delta,
741            current_depth: self.current_depth,
742            consecutive_errors: self.consecutive_errors,
743            shifts_since_pop: self.shifts_since_pop,
744            dist: self.dist,
745        }
746    }
747
748    pub fn push(&self, rule: R) -> Self {
749        let kind = rule.element_kind();
750        let (s, a) = self.state;
751        let parent = self.parent.prepend((rule, s));
752        let list = self.list.prepend(Step::start(kind.clone()));
753        let s = descend(s, kind.branch());
754        Self {
755            parent,
756            list,
757            cost: self.cost,
758            h: self.h,
759            state: (s, a),
760            has_error: self.has_error,
761            assumed_depth_delta: self.assumed_depth_delta,
762            current_depth: self.current_depth,
763            consecutive_errors: self.consecutive_errors,
764            shifts_since_pop: self.shifts_since_pop,
765            dist: self.dist,
766        }
767    }
768
769    pub fn pop(&self) -> Option<Self> {
770        let ((_, f), parent) = self.parent.slice()?;
771        let list = self.list.prepend(Step::end());
772        let (_, a) = self.state;
773        Some(Self {
774            parent: parent.clone(),
775            list,
776            cost: self.cost,
777            h: self.h,
778            state: (*f, a),
779            has_error: self.has_error,
780            assumed_depth_delta: self.assumed_depth_delta,
781            current_depth: self.current_depth,
782            consecutive_errors: self.consecutive_errors,
783            shifts_since_pop: 0, // reduction resets t counter
784            dist: self.dist,
785        })
786    }
787}
788
789impl<R: ParserTrait> Ord for Element<R> {
790    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
791        // Min-heap on f = cost + h (lower f = fewer errors = higher priority).
792        // Tiebreak: prefer shallower parent stacks (fewer pending rules).
793        let f_self = self.cost + self.h;
794        let f_other = other.cost + other.h;
795        f_other
796            .cmp(&f_self)
797            .then(other.parent.len().cmp(&self.parent.len()))
798    }
799}
800impl<R: ParserTrait> PartialOrd for Element<R> {
801    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
802        Some(self.cmp(other))
803    }
804}
805
806pub fn a_star<R: ParserTrait>(
807    root: R,
808    tokens: &[FatToken<R::Kind>],
809    bias: IncrementalBias,
810    max_iterations: usize,
811) -> List<Step<R::Kind>> {
812    let mut state = AStar::new(tokens, bias, max_iterations);
813    // let h0 = state.heuristic[0];
814    // state.todo.push(Element::new(root, h0));
815    // Lets update the element to point to something that isn't whitespace
816    state.consume(root).unwrap_or_default()
817}
818
819/// Like `a_star` but runs in non-fault-tolerant fast mode.  Returns `None` if
820/// the document contains any error; `Some(steps)` on a clean parse.
821pub fn a_star_fast<R: ParserTrait>(
822    root: R,
823    tokens: &[FatToken<R::Kind>],
824) -> Option<List<Step<R::Kind>>> {
825    let mut state = AStar::new_fast(tokens);
826    state.consume(root)
827}