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}