Skip to main content

antlr4_runtime/
prediction.rs

1// SPDX-License-Identifier: BSD-3-Clause
2// Copyright (c) 2026 Konstantin Vyatkin
3use std::cmp::Ordering;
4use std::collections::{BTreeMap, BTreeSet, HashMap};
5use std::hash::{BuildHasherDefault, Hash, Hasher};
6use std::mem::size_of;
7
8pub const EMPTY_RETURN_STATE: usize = usize::MAX;
9const COMPACT_EMPTY_RETURN_STATE: u32 = u32::MAX;
10
11/// Lightweight `FxHash`-style hasher used on prediction hot paths.
12#[derive(Debug, Default)]
13pub struct PredictionFxHasher {
14    hash: u64,
15}
16
17const FX_ROT: u32 = 5;
18const FX_SEED: u64 = 0x51_7c_c1_b7_27_22_0a_95;
19
20impl Hasher for PredictionFxHasher {
21    #[inline]
22    fn write(&mut self, bytes: &[u8]) {
23        let mut bytes = bytes;
24        while bytes.len() >= 8 {
25            let (head, rest) = bytes.split_at(8);
26            let word = u64::from_le_bytes(head.try_into().expect("8-byte chunk"));
27            self.hash = (self.hash.rotate_left(FX_ROT) ^ word).wrapping_mul(FX_SEED);
28            bytes = rest;
29        }
30        for &byte in bytes {
31            self.hash = (self.hash.rotate_left(FX_ROT) ^ u64::from(byte)).wrapping_mul(FX_SEED);
32        }
33    }
34
35    #[inline]
36    fn write_u8(&mut self, value: u8) {
37        self.hash = (self.hash.rotate_left(FX_ROT) ^ u64::from(value)).wrapping_mul(FX_SEED);
38    }
39
40    #[inline]
41    fn write_u32(&mut self, value: u32) {
42        self.hash = (self.hash.rotate_left(FX_ROT) ^ u64::from(value)).wrapping_mul(FX_SEED);
43    }
44
45    #[inline]
46    fn write_u64(&mut self, value: u64) {
47        self.hash = (self.hash.rotate_left(FX_ROT) ^ value).wrapping_mul(FX_SEED);
48    }
49
50    #[inline]
51    fn write_usize(&mut self, value: usize) {
52        self.hash = (self.hash.rotate_left(FX_ROT) ^ value as u64).wrapping_mul(FX_SEED);
53    }
54
55    #[inline]
56    fn write_i32(&mut self, value: i32) {
57        self.write_u32(i32::cast_unsigned(value));
58    }
59
60    #[inline]
61    fn finish(&self) -> u64 {
62        self.hash
63    }
64}
65
66type FxHashMap<K, V> = HashMap<K, V, BuildHasherDefault<PredictionFxHasher>>;
67
68/// Store-local identity for one canonical prediction-context graph node.
69#[repr(transparent)]
70#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
71pub struct ContextId(u32);
72
73pub const EMPTY_CONTEXT: ContextId = ContextId(0);
74
75impl ContextId {
76    pub(crate) const fn compact(self) -> u32 {
77        self.0
78    }
79}
80
81#[derive(Clone, Copy, Debug, Eq, PartialEq)]
82enum ContextTag {
83    Empty,
84    Singleton,
85    Array,
86}
87
88#[derive(Clone, Copy, Debug)]
89struct ContextRecord {
90    tag: ContextTag,
91    cached_hash: u64,
92    parent_or_start: u32,
93    return_state_or_len: u32,
94}
95
96/// Allocation and interning totals for one prediction-context arena.
97#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
98pub struct PredictionContextStats {
99    pub contexts_created: usize,
100    pub singleton_contexts: usize,
101    pub array_contexts: usize,
102    pub array_entries: usize,
103    pub interner_hits: usize,
104    pub pooled_bytes: usize,
105    /// Element storage implied by retained capacities, excluding allocator and
106    /// hash-table control metadata.
107    pub retained_bytes: usize,
108    pub context_capacity: usize,
109    pub array_parent_capacity: usize,
110    pub array_return_state_capacity: usize,
111    pub interner_capacity: usize,
112    pub workspace_merge_cache_entries: usize,
113    pub workspace_merge_cache_capacity: usize,
114    pub workspace_entry_capacity: usize,
115    pub outer_context_cache_hits: usize,
116    pub outer_context_cache_misses: usize,
117}
118
119/// Canonical compact storage paired with one learned parser DFA store.
120#[derive(Debug)]
121pub(crate) struct ContextArena {
122    records: Vec<ContextRecord>,
123    array_parents: Vec<ContextId>,
124    array_return_states: Vec<u32>,
125    interner_heads: FxHashMap<u64, ContextId>,
126    interner_next: Vec<Option<ContextId>>,
127    interner_hits: usize,
128    #[cfg(debug_assertions)]
129    generation: u64,
130}
131
132#[cfg(debug_assertions)]
133fn next_context_arena_generation() -> u64 {
134    use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
135
136    static NEXT_GENERATION: AtomicU64 = AtomicU64::new(1);
137    NEXT_GENERATION.fetch_add(1, AtomicOrdering::Relaxed)
138}
139
140impl ContextArena {
141    pub(crate) fn new() -> Self {
142        let empty = ContextRecord {
143            tag: ContextTag::Empty,
144            cached_hash: prediction_context_empty_hash(),
145            parent_or_start: 0,
146            return_state_or_len: 0,
147        };
148        let mut interner_heads = FxHashMap::default();
149        interner_heads.insert(empty.cached_hash, EMPTY_CONTEXT);
150        Self {
151            records: vec![empty],
152            array_parents: Vec::new(),
153            array_return_states: Vec::new(),
154            interner_heads,
155            interner_next: vec![None],
156            interner_hits: 0,
157            #[cfg(debug_assertions)]
158            generation: next_context_arena_generation(),
159        }
160    }
161
162    #[cfg(debug_assertions)]
163    pub(crate) const fn generation(&self) -> u64 {
164        self.generation
165    }
166
167    pub(crate) fn stats(&self) -> PredictionContextStats {
168        let mut singleton_contexts = 0;
169        let mut array_contexts = 0;
170        for record in &self.records {
171            match record.tag {
172                ContextTag::Empty => {}
173                ContextTag::Singleton => singleton_contexts += 1,
174                ContextTag::Array => array_contexts += 1,
175            }
176        }
177        PredictionContextStats {
178            contexts_created: self.records.len(),
179            singleton_contexts,
180            array_contexts,
181            array_entries: self.array_parents.len(),
182            interner_hits: self.interner_hits,
183            pooled_bytes: self.records.len() * size_of::<ContextRecord>()
184                + self.array_parents.len() * size_of::<ContextId>()
185                + self.array_return_states.len() * size_of::<u32>()
186                + self.interner_next.len() * size_of::<Option<ContextId>>(),
187            retained_bytes: self.records.capacity() * size_of::<ContextRecord>()
188                + self.array_parents.capacity() * size_of::<ContextId>()
189                + self.array_return_states.capacity() * size_of::<u32>()
190                + self.interner_heads.capacity() * size_of::<(u64, ContextId)>()
191                + self.interner_next.capacity() * size_of::<Option<ContextId>>(),
192            context_capacity: self.records.capacity(),
193            array_parent_capacity: self.array_parents.capacity(),
194            array_return_state_capacity: self.array_return_states.capacity(),
195            interner_capacity: self.interner_heads.capacity(),
196            workspace_merge_cache_entries: 0,
197            workspace_merge_cache_capacity: 0,
198            workspace_entry_capacity: 0,
199            outer_context_cache_hits: 0,
200            outer_context_cache_misses: 0,
201        }
202    }
203
204    pub(crate) fn singleton(&mut self, parent: ContextId, return_state: usize) -> ContextId {
205        self.assert_valid(parent);
206        if return_state == EMPTY_RETURN_STATE {
207            return EMPTY_CONTEXT;
208        }
209        #[cfg(feature = "perf-counters")]
210        crate::perf::record_context_cache_call();
211        let return_state =
212            u32::try_from(return_state).expect("prediction return state must fit in u32");
213        let cached_hash = prediction_context_singleton_hash(self.cached_hash(parent), return_state);
214        if let Some(existing) = self.find_interned(cached_hash, |record| {
215            record.tag == ContextTag::Singleton
216                && record.parent_or_start == parent.0
217                && record.return_state_or_len == return_state
218        }) {
219            self.interner_hits = self.interner_hits.saturating_add(1);
220            #[cfg(feature = "perf-counters")]
221            crate::perf::record_context_cache_hit();
222            return existing;
223        }
224        #[cfg(feature = "perf-counters")]
225        {
226            crate::perf::record_context_cache_miss();
227            crate::perf::record_context_cache_insert();
228        }
229        self.push_record(ContextRecord {
230            tag: ContextTag::Singleton,
231            cached_hash,
232            parent_or_start: parent.0,
233            return_state_or_len: return_state,
234        })
235    }
236
237    fn intern_entries(&mut self, entries: &[(ContextId, u32)]) -> ContextId {
238        match entries {
239            [] => EMPTY_CONTEXT,
240            [(parent, return_state)] => {
241                if *return_state == COMPACT_EMPTY_RETURN_STATE {
242                    EMPTY_CONTEXT
243                } else {
244                    self.singleton(
245                        *parent,
246                        usize::try_from(*return_state).expect("u32 return state fits in usize"),
247                    )
248                }
249            }
250            _ => {
251                debug_assert!(
252                    entries
253                        .windows(2)
254                        .all(|pair| { compare_entries(pair[0], pair[1]) == Ordering::Less })
255                );
256                #[cfg(feature = "perf-counters")]
257                crate::perf::record_context_cache_call();
258                let cached_hash = prediction_context_array_hash(self, entries);
259                if let Some(existing) = self.find_interned(cached_hash, |record| {
260                    if record.tag != ContextTag::Array
261                        || usize::try_from(record.return_state_or_len).ok() != Some(entries.len())
262                    {
263                        return false;
264                    }
265                    let start =
266                        usize::try_from(record.parent_or_start).expect("u32 pool index fits usize");
267                    let end = start + entries.len();
268                    self.array_parents[start..end]
269                        .iter()
270                        .copied()
271                        .zip(self.array_return_states[start..end].iter().copied())
272                        .eq(entries.iter().copied())
273                }) {
274                    self.interner_hits = self.interner_hits.saturating_add(1);
275                    #[cfg(feature = "perf-counters")]
276                    crate::perf::record_context_cache_hit();
277                    return existing;
278                }
279                #[cfg(feature = "perf-counters")]
280                {
281                    crate::perf::record_context_cache_miss();
282                    crate::perf::record_context_cache_insert();
283                }
284                let start = u32::try_from(self.array_parents.len())
285                    .expect("prediction-context parent pool must fit in u32");
286                let len = u32::try_from(entries.len())
287                    .expect("prediction-context array length must fit in u32");
288                self.array_parents
289                    .extend(entries.iter().map(|(parent, _)| *parent));
290                self.array_return_states
291                    .extend(entries.iter().map(|(_, return_state)| *return_state));
292                self.push_record(ContextRecord {
293                    tag: ContextTag::Array,
294                    cached_hash,
295                    parent_or_start: start,
296                    return_state_or_len: len,
297                })
298            }
299        }
300    }
301
302    fn find_interned(
303        &self,
304        cached_hash: u64,
305        matches: impl Fn(&ContextRecord) -> bool,
306    ) -> Option<ContextId> {
307        let mut candidate = self.interner_heads.get(&cached_hash).copied();
308        while let Some(id) = candidate {
309            let index = usize::try_from(id.0).expect("u32 context ID fits in usize");
310            let record = &self.records[index];
311            if matches(record) {
312                return Some(id);
313            }
314            candidate = self.interner_next[index];
315        }
316        None
317    }
318
319    fn push_record(&mut self, record: ContextRecord) -> ContextId {
320        let id = ContextId(
321            u32::try_from(self.records.len()).expect("prediction-context arena must fit in u32"),
322        );
323        let previous = self.interner_heads.insert(record.cached_hash, id);
324        self.records.push(record);
325        self.interner_next.push(previous);
326        id
327    }
328
329    pub(crate) fn merge(
330        &mut self,
331        left: ContextId,
332        right: ContextId,
333        root_is_wildcard: bool,
334        workspace: &mut PredictionWorkspace,
335    ) -> ContextId {
336        self.assert_valid(left);
337        self.assert_valid(right);
338        #[cfg(feature = "perf-counters")]
339        crate::perf::record_context_merge_call();
340        if left == right {
341            #[cfg(feature = "perf-counters")]
342            crate::perf::record_context_merge_identical();
343            return left;
344        }
345        let key = MergeKey::new(left, right, root_is_wildcard);
346        if let Some(merged) = workspace.merge_cache.get(&key).copied() {
347            #[cfg(feature = "perf-counters")]
348            crate::perf::record_context_merge_cache_hit();
349            return merged;
350        }
351        #[cfg(feature = "perf-counters")]
352        {
353            crate::perf::record_context_merge_cache_miss();
354            crate::perf::record_context_merge_uncached();
355        }
356        let merged = if root_is_wildcard && (left == EMPTY_CONTEXT || right == EMPTY_CONTEXT) {
357            EMPTY_CONTEXT
358        } else {
359            self.merge_uncached(left, right, root_is_wildcard, workspace)
360        };
361        workspace.merge_cache.insert(key, merged);
362        merged
363    }
364
365    fn merge_uncached(
366        &mut self,
367        left: ContextId,
368        right: ContextId,
369        root_is_wildcard: bool,
370        workspace: &mut PredictionWorkspace,
371    ) -> ContextId {
372        match (self.tag(left), self.tag(right)) {
373            (ContextTag::Array, ContextTag::Array) => {
374                self.merge_arrays(left, right, root_is_wildcard, workspace)
375            }
376            (ContextTag::Array, _) => {
377                let entry = self.first_entry(right);
378                self.merge_array_with_entry(left, entry, root_is_wildcard, workspace)
379            }
380            (_, ContextTag::Array) => {
381                let entry = self.first_entry(left);
382                self.merge_array_with_entry(right, entry, root_is_wildcard, workspace)
383            }
384            _ => self.merge_two_entries(
385                self.first_entry(left),
386                self.first_entry(right),
387                root_is_wildcard,
388                workspace,
389            ),
390        }
391    }
392
393    fn merge_two_entries(
394        &mut self,
395        left: (ContextId, u32),
396        right: (ContextId, u32),
397        root_is_wildcard: bool,
398        workspace: &mut PredictionWorkspace,
399    ) -> ContextId {
400        if left.1 == right.1 {
401            let parent = if left.0 == right.0 {
402                left.0
403            } else {
404                self.merge(left.0, right.0, root_is_wildcard, workspace)
405            };
406            return self.intern_entries(&[(parent, left.1)]);
407        }
408
409        let start = workspace.entries.len();
410        if right.1 < left.1 {
411            workspace.entries.extend([right, left]);
412        } else {
413            workspace.entries.extend([left, right]);
414        }
415        self.intern_workspace_entries(workspace, start)
416    }
417
418    fn intern_workspace_entries(
419        &mut self,
420        workspace: &mut PredictionWorkspace,
421        start: usize,
422    ) -> ContextId {
423        let context = self.intern_entries(&workspace.entries[start..]);
424        workspace.entries.truncate(start);
425        context
426    }
427
428    fn merge_array_with_entry(
429        &mut self,
430        array: ContextId,
431        entry: (ContextId, u32),
432        root_is_wildcard: bool,
433        workspace: &mut PredictionWorkspace,
434    ) -> ContextId {
435        let array_len = self.len(array);
436        let mut insert_index = array_len;
437        for index in 0..array_len {
438            let current = self.entry(array, index).expect("array entry in range");
439            match entry.1.cmp(&current.1) {
440                Ordering::Less => {
441                    insert_index = index;
442                    break;
443                }
444                Ordering::Equal => {
445                    let parent = if entry.0 == current.0 {
446                        current.0
447                    } else {
448                        self.merge(entry.0, current.0, root_is_wildcard, workspace)
449                    };
450                    if parent == current.0 {
451                        return array;
452                    }
453
454                    let start = workspace.entries.len();
455                    for entry_index in 0..array_len {
456                        let array_entry = self
457                            .entry(array, entry_index)
458                            .expect("array entry in range");
459                        workspace.entries.push(if entry_index == index {
460                            (parent, current.1)
461                        } else {
462                            array_entry
463                        });
464                    }
465                    return self.intern_workspace_entries(workspace, start);
466                }
467                Ordering::Greater => {}
468            }
469        }
470
471        let start = workspace.entries.len();
472        for index in 0..insert_index {
473            workspace
474                .entries
475                .push(self.entry(array, index).expect("array entry in range"));
476        }
477        workspace.entries.push(entry);
478        for index in insert_index..array_len {
479            workspace
480                .entries
481                .push(self.entry(array, index).expect("array entry in range"));
482        }
483        self.intern_workspace_entries(workspace, start)
484    }
485
486    fn merge_arrays(
487        &mut self,
488        left: ContextId,
489        right: ContextId,
490        root_is_wildcard: bool,
491        workspace: &mut PredictionWorkspace,
492    ) -> ContextId {
493        let start = workspace.entries.len();
494        let left_len = self.len(left);
495        let right_len = self.len(right);
496        let mut left_index = 0;
497        let mut right_index = 0;
498        while left_index < left_len && right_index < right_len {
499            let left_entry = self.entry(left, left_index).expect("array entry in range");
500            let right_entry = self
501                .entry(right, right_index)
502                .expect("array entry in range");
503            match left_entry.1.cmp(&right_entry.1) {
504                Ordering::Less => {
505                    workspace.entries.push(left_entry);
506                    left_index += 1;
507                }
508                Ordering::Greater => {
509                    workspace.entries.push(right_entry);
510                    right_index += 1;
511                }
512                Ordering::Equal => {
513                    let parent = if left_entry.0 == right_entry.0 {
514                        left_entry.0
515                    } else {
516                        self.merge(left_entry.0, right_entry.0, root_is_wildcard, workspace)
517                    };
518                    workspace.entries.push((parent, left_entry.1));
519                    left_index += 1;
520                    right_index += 1;
521                }
522            }
523        }
524        while left_index < left_len {
525            workspace
526                .entries
527                .push(self.entry(left, left_index).expect("array entry in range"));
528            left_index += 1;
529        }
530        while right_index < right_len {
531            workspace.entries.push(
532                self.entry(right, right_index)
533                    .expect("array entry in range"),
534            );
535            right_index += 1;
536        }
537        self.intern_workspace_entries(workspace, start)
538    }
539
540    pub(crate) fn len(&self, context: ContextId) -> usize {
541        let record = self.record(context);
542        match record.tag {
543            ContextTag::Empty | ContextTag::Singleton => 1,
544            ContextTag::Array => usize::try_from(record.return_state_or_len)
545                .expect("u32 context length fits in usize"),
546        }
547    }
548
549    pub(crate) fn is_empty(&self, context: ContextId) -> bool {
550        self.assert_valid(context);
551        context == EMPTY_CONTEXT
552    }
553
554    pub(crate) fn has_empty_path(&self, context: ContextId) -> bool {
555        if context == EMPTY_CONTEXT {
556            return true;
557        }
558        let record = self.record(context);
559        match record.tag {
560            ContextTag::Empty => true,
561            ContextTag::Singleton => false,
562            ContextTag::Array => {
563                let len = usize::try_from(record.return_state_or_len)
564                    .expect("u32 context length fits in usize");
565                let start = usize::try_from(record.parent_or_start)
566                    .expect("u32 context pool index fits in usize");
567                self.array_return_states[start + len - 1] == COMPACT_EMPTY_RETURN_STATE
568            }
569        }
570    }
571
572    pub(crate) fn return_state(&self, context: ContextId, index: usize) -> Option<usize> {
573        let (_, return_state) = self.entry(context, index)?;
574        Some(expand_return_state(return_state))
575    }
576
577    pub(crate) fn parent(&self, context: ContextId, index: usize) -> Option<ContextId> {
578        if context == EMPTY_CONTEXT {
579            self.assert_valid(context);
580            return None;
581        }
582        self.entry(context, index).map(|(parent, _)| parent)
583    }
584
585    fn first_entry(&self, context: ContextId) -> (ContextId, u32) {
586        self.entry(context, 0)
587            .expect("empty and singleton contexts have one logical entry")
588    }
589
590    fn entry(&self, context: ContextId, index: usize) -> Option<(ContextId, u32)> {
591        let record = self.record(context);
592        match record.tag {
593            ContextTag::Empty if index == 0 => Some((EMPTY_CONTEXT, COMPACT_EMPTY_RETURN_STATE)),
594            ContextTag::Singleton if index == 0 => Some((
595                ContextId(record.parent_or_start),
596                record.return_state_or_len,
597            )),
598            ContextTag::Array => {
599                let len = usize::try_from(record.return_state_or_len).ok()?;
600                if index >= len {
601                    return None;
602                }
603                let start = usize::try_from(record.parent_or_start).ok()?;
604                Some((
605                    self.array_parents[start + index],
606                    self.array_return_states[start + index],
607                ))
608            }
609            ContextTag::Empty | ContextTag::Singleton => None,
610        }
611    }
612
613    fn tag(&self, context: ContextId) -> ContextTag {
614        self.record(context).tag
615    }
616
617    fn cached_hash(&self, context: ContextId) -> u64 {
618        self.record(context).cached_hash
619    }
620
621    fn record(&self, context: ContextId) -> &ContextRecord {
622        self.assert_valid(context);
623        &self.records[usize::try_from(context.0).expect("u32 context ID fits in usize")]
624    }
625
626    pub(crate) fn assert_valid(&self, context: ContextId) {
627        assert!(
628            usize::try_from(context.0).is_ok_and(|index| index < self.records.len()),
629            "prediction ContextId does not belong to this store"
630        );
631    }
632
633    pub(crate) fn import_all(
634        &mut self,
635        source: &Self,
636        workspace: &mut PredictionWorkspace,
637    ) -> Vec<ContextId> {
638        workspace.entries.clear();
639        let mut remap = Vec::with_capacity(source.records.len());
640        remap.push(EMPTY_CONTEXT);
641        for source_index in 1..source.records.len() {
642            let source_id = ContextId(
643                u32::try_from(source_index).expect("source prediction-context ID fits in u32"),
644            );
645            let imported = match source.tag(source_id) {
646                ContextTag::Empty => EMPTY_CONTEXT,
647                ContextTag::Singleton => {
648                    let (parent, return_state) = source.first_entry(source_id);
649                    let parent_index =
650                        usize::try_from(parent.0).expect("u32 context ID fits usize");
651                    assert!(
652                        parent_index < remap.len(),
653                        "prediction contexts must reference earlier arena records"
654                    );
655                    self.singleton(remap[parent_index], expand_return_state(return_state))
656                }
657                ContextTag::Array => {
658                    let start = workspace.entries.len();
659                    for entry_index in 0..source.len(source_id) {
660                        let (parent, return_state) = source
661                            .entry(source_id, entry_index)
662                            .expect("source array entry in range");
663                        let parent_index =
664                            usize::try_from(parent.0).expect("u32 context ID fits usize");
665                        assert!(
666                            parent_index < remap.len(),
667                            "prediction contexts must reference earlier arena records"
668                        );
669                        workspace.entries.push((remap[parent_index], return_state));
670                    }
671                    workspace
672                        .entries
673                        .sort_unstable_by(|left, right| compare_entries(*left, *right));
674                    workspace.entries.dedup();
675                    self.intern_workspace_entries(workspace, start)
676                }
677            };
678            remap.push(imported);
679        }
680        remap
681    }
682}
683
684impl Default for ContextArena {
685    fn default() -> Self {
686        Self::new()
687    }
688}
689
690fn compare_entries(left: (ContextId, u32), right: (ContextId, u32)) -> Ordering {
691    left.1.cmp(&right.1)
692}
693
694fn expand_return_state(return_state: u32) -> usize {
695    if return_state == COMPACT_EMPTY_RETURN_STATE {
696        EMPTY_RETURN_STATE
697    } else {
698        usize::try_from(return_state).expect("u32 return state fits in usize")
699    }
700}
701
702fn prediction_context_empty_hash() -> u64 {
703    let mut hasher = PredictionFxHasher::default();
704    hasher.write_u8(0);
705    hasher.finish()
706}
707
708fn prediction_context_singleton_hash(parent_hash: u64, return_state: u32) -> u64 {
709    let mut hasher = PredictionFxHasher::default();
710    hasher.write_u8(1);
711    hasher.write_u64(parent_hash);
712    hasher.write_u32(return_state);
713    hasher.finish()
714}
715
716fn prediction_context_array_hash(arena: &ContextArena, entries: &[(ContextId, u32)]) -> u64 {
717    let mut hasher = PredictionFxHasher::default();
718    hasher.write_u8(2);
719    hasher.write_usize(entries.len());
720    for (parent, _) in entries {
721        hasher.write_u64(arena.cached_hash(*parent));
722    }
723    hasher.write_usize(entries.len());
724    for (_, return_state) in entries {
725        hasher.write_u32(*return_state);
726    }
727    hasher.finish()
728}
729
730#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
731struct MergeKey {
732    left: ContextId,
733    right: ContextId,
734    root_is_wildcard: bool,
735}
736
737impl MergeKey {
738    fn new(left: ContextId, right: ContextId, root_is_wildcard: bool) -> Self {
739        let (left, right) = if right < left {
740            (right, left)
741        } else {
742            (left, right)
743        };
744        Self {
745            left,
746            right,
747            root_is_wildcard,
748        }
749    }
750}
751
752const MAX_RETAINED_MERGE_CACHE_ENTRIES: usize = 65_536;
753const MAX_RETAINED_CONTEXT_ENTRIES: usize = 16_384;
754
755/// Reusable per-prediction merge cache and temporary compact entry storage.
756#[derive(Debug, Default)]
757pub(crate) struct PredictionWorkspace {
758    merge_cache: FxHashMap<MergeKey, ContextId>,
759    entries: Vec<(ContextId, u32)>,
760}
761
762impl PredictionWorkspace {
763    pub(crate) fn reset(&mut self) {
764        if self.merge_cache.capacity() > MAX_RETAINED_MERGE_CACHE_ENTRIES {
765            self.merge_cache = FxHashMap::default();
766        } else {
767            self.merge_cache.clear();
768        }
769        if self.entries.capacity() > MAX_RETAINED_CONTEXT_ENTRIES {
770            self.entries = Vec::new();
771        } else {
772            self.entries.clear();
773        }
774    }
775
776    pub(crate) fn merge_cache_capacity(&self) -> usize {
777        self.merge_cache.capacity()
778    }
779
780    pub(crate) fn merge_cache_len(&self) -> usize {
781        self.merge_cache.len()
782    }
783
784    pub(crate) const fn entry_capacity(&self) -> usize {
785        self.entries.capacity()
786    }
787
788    pub(crate) fn retained_bytes(&self) -> usize {
789        self.merge_cache.capacity() * size_of::<(MergeKey, ContextId)>()
790            + self.entries.capacity() * size_of::<(ContextId, u32)>()
791    }
792}
793
794#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
795pub enum SemanticContext {
796    None,
797    Predicate {
798        rule_index: usize,
799        pred_index: usize,
800        context_dependent: bool,
801    },
802    Precedence {
803        precedence: i32,
804    },
805    And(Vec<Self>),
806    Or(Vec<Self>),
807}
808
809impl SemanticContext {
810    pub const fn none() -> Self {
811        Self::None
812    }
813
814    pub fn and(left: Self, right: Self) -> Self {
815        combine_semantic_context(left, right, true)
816    }
817
818    pub fn or(left: Self, right: Self) -> Self {
819        combine_semantic_context(left, right, false)
820    }
821
822    pub const fn is_none(&self) -> bool {
823        matches!(self, Self::None)
824    }
825}
826
827#[repr(transparent)]
828#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
829pub(crate) struct SemanticContextId(u32);
830
831#[derive(Debug)]
832pub(crate) struct SemanticContextArena {
833    records: Vec<SemanticContext>,
834    interner_heads: FxHashMap<u64, SemanticContextId>,
835    interner_next: Vec<Option<SemanticContextId>>,
836}
837
838impl SemanticContextArena {
839    pub(crate) fn new() -> Self {
840        let semantic_context = SemanticContext::None;
841        let cached_hash = semantic_context_hash(&semantic_context);
842        let mut interner_heads = FxHashMap::default();
843        interner_heads.insert(cached_hash, SemanticContextId::default());
844        Self {
845            records: vec![semantic_context],
846            interner_heads,
847            interner_next: vec![None],
848        }
849    }
850
851    pub(crate) fn intern(&mut self, semantic_context: SemanticContext) -> SemanticContextId {
852        if semantic_context.is_none() {
853            return SemanticContextId::default();
854        }
855        let cached_hash = semantic_context_hash(&semantic_context);
856        if let Some(id) = self.find_interned(cached_hash, &semantic_context) {
857            return id;
858        }
859        let id = SemanticContextId(
860            u32::try_from(self.records.len()).expect("semantic-context arena exhausted"),
861        );
862        let previous = self.interner_heads.insert(cached_hash, id);
863        self.records.push(semantic_context);
864        self.interner_next.push(previous);
865        id
866    }
867
868    pub(crate) fn and(
869        &mut self,
870        left: SemanticContextId,
871        right: SemanticContext,
872    ) -> SemanticContextId {
873        let combined = SemanticContext::and(self.get(left).clone(), right);
874        self.intern(combined)
875    }
876
877    pub(crate) fn get(&self, id: SemanticContextId) -> &SemanticContext {
878        self.assert_valid(id);
879        &self.records[usize::try_from(id.0).expect("u32 semantic-context ID fits usize")]
880    }
881
882    pub(crate) fn import_all(&mut self, source: &Self) -> Vec<SemanticContextId> {
883        source
884            .records
885            .iter()
886            .cloned()
887            .map(|semantic_context| self.intern(semantic_context))
888            .collect()
889    }
890
891    pub(crate) const fn len(&self) -> usize {
892        self.records.len()
893    }
894
895    pub(crate) fn retained_bytes(&self) -> usize {
896        self.records.capacity() * size_of::<SemanticContext>()
897            + self
898                .records
899                .iter()
900                .map(semantic_context_heap_retained_bytes)
901                .sum::<usize>()
902            + self.interner_heads.capacity() * size_of::<(u64, SemanticContextId)>()
903            + self.interner_next.capacity() * size_of::<Option<SemanticContextId>>()
904    }
905
906    fn find_interned(
907        &self,
908        cached_hash: u64,
909        semantic_context: &SemanticContext,
910    ) -> Option<SemanticContextId> {
911        let mut candidate = self.interner_heads.get(&cached_hash).copied();
912        while let Some(id) = candidate {
913            let index = usize::try_from(id.0).ok()?;
914            if self.records.get(index) == Some(semantic_context) {
915                return Some(id);
916            }
917            candidate = self.interner_next.get(index).copied().flatten();
918        }
919        None
920    }
921
922    fn assert_valid(&self, id: SemanticContextId) {
923        assert!(
924            usize::try_from(id.0).is_ok_and(|index| index < self.records.len()),
925            "semantic-context ID does not belong to this store"
926        );
927    }
928}
929
930impl Default for SemanticContextArena {
931    fn default() -> Self {
932        Self::new()
933    }
934}
935
936fn semantic_context_hash(semantic_context: &SemanticContext) -> u64 {
937    let mut hasher = PredictionFxHasher::default();
938    semantic_context.hash(&mut hasher);
939    hasher.finish()
940}
941
942fn semantic_context_heap_retained_bytes(semantic_context: &SemanticContext) -> usize {
943    match semantic_context {
944        SemanticContext::And(children) | SemanticContext::Or(children) => {
945            children.capacity() * size_of::<SemanticContext>()
946                + children
947                    .iter()
948                    .map(semantic_context_heap_retained_bytes)
949                    .sum::<usize>()
950        }
951        SemanticContext::None
952        | SemanticContext::Predicate { .. }
953        | SemanticContext::Precedence { .. } => 0,
954    }
955}
956
957fn combine_semantic_context(
958    left: SemanticContext,
959    right: SemanticContext,
960    and: bool,
961) -> SemanticContext {
962    if left == right {
963        return left;
964    }
965    if left.is_none() {
966        return right;
967    }
968    if right.is_none() {
969        return left;
970    }
971    let mut entries = Vec::new();
972    for context in [left, right] {
973        match (and, context) {
974            (true, SemanticContext::And(children)) | (false, SemanticContext::Or(children)) => {
975                entries.extend(children);
976            }
977            (_, other) => entries.push(other),
978        }
979    }
980    entries.sort();
981    entries.dedup();
982    if and {
983        SemanticContext::And(entries)
984    } else {
985        SemanticContext::Or(entries)
986    }
987}
988
989#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
990pub(crate) struct PredictionRuleCall {
991    pub(crate) source_state: usize,
992    pub(crate) rule_index: usize,
993}
994
995#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
996pub(crate) struct PredictionPredicateCall {
997    pub(crate) rule_index: usize,
998    pub(crate) pred_index: usize,
999    pub(crate) rule_calls: Vec<PredictionRuleCall>,
1000}
1001
1002#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
1003struct PredictionSemanticProvenance {
1004    active_rule_calls: Vec<PredictionRuleCall>,
1005    predicate_calls: Vec<PredictionPredicateCall>,
1006}
1007
1008#[repr(transparent)]
1009#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
1010pub(crate) struct PredictionSemanticProvenanceId(u32);
1011
1012#[derive(Debug, Default)]
1013pub(crate) struct PredictionSemanticProvenanceArena {
1014    records: Vec<PredictionSemanticProvenance>,
1015    interner_heads: FxHashMap<u64, PredictionSemanticProvenanceId>,
1016    interner_next: Vec<Option<PredictionSemanticProvenanceId>>,
1017}
1018
1019impl PredictionSemanticProvenanceArena {
1020    pub(crate) fn enter_rule(
1021        &mut self,
1022        id: PredictionSemanticProvenanceId,
1023        source_state: usize,
1024        rule_index: usize,
1025    ) -> PredictionSemanticProvenanceId {
1026        let mut provenance = self.get(id).cloned().unwrap_or_default();
1027        provenance.active_rule_calls.push(PredictionRuleCall {
1028            source_state,
1029            rule_index,
1030        });
1031        self.intern(provenance)
1032    }
1033
1034    pub(crate) fn exit_rule(
1035        &mut self,
1036        id: PredictionSemanticProvenanceId,
1037    ) -> PredictionSemanticProvenanceId {
1038        let Some(mut provenance) = self.get(id).cloned() else {
1039            return PredictionSemanticProvenanceId::default();
1040        };
1041        provenance.active_rule_calls.pop();
1042        self.intern(provenance)
1043    }
1044
1045    pub(crate) fn record_predicate(
1046        &mut self,
1047        id: PredictionSemanticProvenanceId,
1048        rule_index: usize,
1049        pred_index: usize,
1050    ) -> PredictionSemanticProvenanceId {
1051        let mut provenance = self.get(id).cloned().unwrap_or_default();
1052        let call = PredictionPredicateCall {
1053            rule_index,
1054            pred_index,
1055            rule_calls: provenance.active_rule_calls.clone(),
1056        };
1057        if !provenance.predicate_calls.contains(&call) {
1058            provenance.predicate_calls.push(call);
1059        }
1060        self.intern(provenance)
1061    }
1062
1063    pub(crate) fn predicate_calls(
1064        &self,
1065        id: PredictionSemanticProvenanceId,
1066    ) -> &[PredictionPredicateCall] {
1067        self.get(id)
1068            .map_or(&[], |provenance| provenance.predicate_calls.as_slice())
1069    }
1070
1071    pub(crate) const fn len(&self) -> usize {
1072        self.records.len()
1073    }
1074
1075    pub(crate) fn retained_bytes(&self) -> usize {
1076        self.records.capacity() * size_of::<PredictionSemanticProvenance>()
1077            + self
1078                .records
1079                .iter()
1080                .map(|provenance| {
1081                    provenance.active_rule_calls.capacity() * size_of::<PredictionRuleCall>()
1082                        + provenance.predicate_calls.capacity()
1083                            * size_of::<PredictionPredicateCall>()
1084                        + provenance
1085                            .predicate_calls
1086                            .iter()
1087                            .map(|call| {
1088                                call.rule_calls.capacity() * size_of::<PredictionRuleCall>()
1089                            })
1090                            .sum::<usize>()
1091                })
1092                .sum::<usize>()
1093            + self.interner_heads.capacity() * size_of::<(u64, PredictionSemanticProvenanceId)>()
1094            + self.interner_next.capacity() * size_of::<Option<PredictionSemanticProvenanceId>>()
1095    }
1096
1097    fn get(&self, id: PredictionSemanticProvenanceId) -> Option<&PredictionSemanticProvenance> {
1098        let index = id.0.checked_sub(1)?;
1099        self.records.get(usize::try_from(index).ok()?)
1100    }
1101
1102    fn find_interned(
1103        &self,
1104        cached_hash: u64,
1105        provenance: &PredictionSemanticProvenance,
1106    ) -> Option<PredictionSemanticProvenanceId> {
1107        let mut candidate = self.interner_heads.get(&cached_hash).copied();
1108        while let Some(id) = candidate {
1109            let index = usize::try_from(id.0.checked_sub(1)?).ok()?;
1110            if self.records.get(index) == Some(provenance) {
1111                return Some(id);
1112            }
1113            candidate = self.interner_next.get(index).copied().flatten();
1114        }
1115        None
1116    }
1117
1118    fn intern(
1119        &mut self,
1120        provenance: PredictionSemanticProvenance,
1121    ) -> PredictionSemanticProvenanceId {
1122        if provenance.active_rule_calls.is_empty() && provenance.predicate_calls.is_empty() {
1123            return PredictionSemanticProvenanceId::default();
1124        }
1125        let mut hasher = PredictionFxHasher::default();
1126        provenance.hash(&mut hasher);
1127        let cached_hash = hasher.finish();
1128        if let Some(id) = self.find_interned(cached_hash, &provenance) {
1129            return id;
1130        }
1131        let id = PredictionSemanticProvenanceId(
1132            u32::try_from(self.records.len() + 1)
1133                .expect("prediction semantic provenance arena exhausted"),
1134        );
1135        assert!(
1136            id.0 <= ATN_CONFIG_PROVENANCE_MASK,
1137            "prediction semantic provenance arena exhausted"
1138        );
1139        let previous = self.interner_heads.insert(cached_hash, id);
1140        self.records.push(provenance);
1141        self.interner_next.push(previous);
1142        id
1143    }
1144}
1145
1146const ATN_CONFIG_PRECEDENCE_FILTER_SUPPRESSED: u32 = 1 << 31;
1147const ATN_CONFIG_PROVENANCE_MASK: u32 = !ATN_CONFIG_PRECEDENCE_FILTER_SUPPRESSED;
1148
1149#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
1150pub(crate) struct AtnConfig {
1151    pub(crate) state: usize,
1152    pub(crate) alt: usize,
1153    pub(crate) context: ContextId,
1154    semantic_context: SemanticContextId,
1155    pub(crate) reaches_into_outer_context: usize,
1156    semantic_provenance_and_flags: u32,
1157    #[cfg(debug_assertions)]
1158    context_generation: u64,
1159}
1160
1161impl AtnConfig {
1162    pub(crate) fn new(state: usize, alt: usize, context: ContextId, arena: &ContextArena) -> Self {
1163        arena.assert_valid(context);
1164        Self {
1165            state,
1166            alt,
1167            context,
1168            semantic_context: SemanticContextId::default(),
1169            reaches_into_outer_context: 0,
1170            semantic_provenance_and_flags: 0,
1171            #[cfg(debug_assertions)]
1172            context_generation: arena.generation(),
1173        }
1174    }
1175
1176    #[must_use]
1177    #[cfg(test)]
1178    pub(crate) fn with_semantic_context(
1179        mut self,
1180        semantic_context: SemanticContext,
1181        arena: &mut SemanticContextArena,
1182    ) -> Self {
1183        self.semantic_context = arena.intern(semantic_context);
1184        self
1185    }
1186
1187    pub(crate) const fn semantic_context_id(&self) -> SemanticContextId {
1188        self.semantic_context
1189    }
1190
1191    pub(crate) fn semantic_context<'a>(
1192        &self,
1193        arena: &'a SemanticContextArena,
1194    ) -> &'a SemanticContext {
1195        arena.get(self.semantic_context)
1196    }
1197
1198    pub(crate) const fn has_semantic_context(&self) -> bool {
1199        self.semantic_context.0 != 0
1200    }
1201
1202    pub(crate) fn set_semantic_context(
1203        &mut self,
1204        semantic_context: SemanticContextId,
1205        arena: &SemanticContextArena,
1206    ) {
1207        arena.assert_valid(semantic_context);
1208        self.semantic_context = semantic_context;
1209    }
1210
1211    pub(crate) fn set_context(&mut self, context: ContextId, arena: &ContextArena) {
1212        arena.assert_valid(context);
1213        self.context = context;
1214        #[cfg(debug_assertions)]
1215        {
1216            self.context_generation = arena.generation();
1217        }
1218    }
1219
1220    pub(crate) fn moved_to(&self, state: usize, context: ContextId, arena: &ContextArena) -> Self {
1221        let mut moved = Self::new(state, self.alt, context, arena);
1222        moved.semantic_context = self.semantic_context;
1223        moved.reaches_into_outer_context = self.reaches_into_outer_context;
1224        moved.semantic_provenance_and_flags = self.semantic_provenance_and_flags;
1225        moved
1226    }
1227
1228    pub(crate) fn enter_prediction_rule(
1229        &mut self,
1230        arena: &mut PredictionSemanticProvenanceArena,
1231        source_state: usize,
1232        rule_index: usize,
1233    ) {
1234        let id = arena.enter_rule(self.semantic_provenance_id(), source_state, rule_index);
1235        self.set_semantic_provenance_id(id);
1236    }
1237
1238    pub(crate) fn exit_prediction_rule(&mut self, arena: &mut PredictionSemanticProvenanceArena) {
1239        let id = arena.exit_rule(self.semantic_provenance_id());
1240        self.set_semantic_provenance_id(id);
1241    }
1242
1243    pub(crate) fn record_prediction_predicate(
1244        &mut self,
1245        arena: &mut PredictionSemanticProvenanceArena,
1246        rule_index: usize,
1247        pred_index: usize,
1248    ) {
1249        let id = arena.record_predicate(self.semantic_provenance_id(), rule_index, pred_index);
1250        self.set_semantic_provenance_id(id);
1251    }
1252
1253    pub(crate) const fn semantic_provenance_id(&self) -> PredictionSemanticProvenanceId {
1254        PredictionSemanticProvenanceId(
1255            self.semantic_provenance_and_flags & ATN_CONFIG_PROVENANCE_MASK,
1256        )
1257    }
1258
1259    pub(crate) const fn semantic_provenance_and_flags(&self) -> u32 {
1260        self.semantic_provenance_and_flags
1261    }
1262
1263    fn set_semantic_provenance_id(&mut self, id: PredictionSemanticProvenanceId) {
1264        debug_assert_eq!(id.0 & ATN_CONFIG_PRECEDENCE_FILTER_SUPPRESSED, 0);
1265        self.semantic_provenance_and_flags =
1266            (self.semantic_provenance_and_flags & ATN_CONFIG_PRECEDENCE_FILTER_SUPPRESSED) | id.0;
1267    }
1268
1269    pub(crate) const fn precedence_filter_suppressed(&self) -> bool {
1270        self.semantic_provenance_and_flags & ATN_CONFIG_PRECEDENCE_FILTER_SUPPRESSED != 0
1271    }
1272
1273    pub(crate) const fn suppress_precedence_filter(&mut self) {
1274        self.semantic_provenance_and_flags |= ATN_CONFIG_PRECEDENCE_FILTER_SUPPRESSED;
1275    }
1276
1277    pub(crate) const fn merge_precedence_filter_suppression(&mut self, other: &Self) {
1278        if other.precedence_filter_suppressed() {
1279            self.suppress_precedence_filter();
1280        }
1281    }
1282
1283    pub(crate) fn assert_store(&self, arena: &ContextArena) {
1284        arena.assert_valid(self.context);
1285        #[cfg(debug_assertions)]
1286        assert_eq!(
1287            self.context_generation,
1288            arena.generation(),
1289            "ATN config carries a ContextId from another prediction store"
1290        );
1291    }
1292}
1293
1294#[derive(Clone, Debug, Default)]
1295pub(crate) struct AtnConfigSet {
1296    configs: Vec<AtnConfig>,
1297    config_index: FxHashMap<AtnConfigKey, usize>,
1298    full_context: bool,
1299    unique_alt: Option<usize>,
1300    conflicting_alts: BTreeSet<usize>,
1301    has_semantic_context: bool,
1302    dips_into_outer_context: bool,
1303    readonly: bool,
1304}
1305
1306impl AtnConfigSet {
1307    pub(crate) fn new() -> Self {
1308        Self::default()
1309    }
1310
1311    pub(crate) fn new_full_context(full_context: bool) -> Self {
1312        Self {
1313            configs: Vec::new(),
1314            config_index: FxHashMap::default(),
1315            full_context,
1316            unique_alt: None,
1317            conflicting_alts: BTreeSet::new(),
1318            has_semantic_context: false,
1319            dips_into_outer_context: false,
1320            readonly: false,
1321        }
1322    }
1323
1324    /// Adds a configuration, merging contexts for equivalent config keys.
1325    pub(crate) fn add(
1326        &mut self,
1327        config: AtnConfig,
1328        arena: &mut ContextArena,
1329        workspace: &mut PredictionWorkspace,
1330    ) -> bool {
1331        assert!(!self.readonly, "cannot mutate readonly ATN config set");
1332        config.assert_store(arena);
1333        #[cfg(feature = "perf-counters")]
1334        crate::perf::record_config_add_call();
1335        if config.has_semantic_context() {
1336            self.has_semantic_context = true;
1337        }
1338        if config.reaches_into_outer_context > 0 {
1339            self.dips_into_outer_context = true;
1340        }
1341        let key = AtnConfigKey::from(&config);
1342        if let Some(existing_index) = self.config_index.get(&key).copied() {
1343            #[cfg(feature = "perf-counters")]
1344            crate::perf::record_config_merge();
1345            let existing = &mut self.configs[existing_index];
1346            existing.assert_store(arena);
1347            existing.context = arena.merge(
1348                existing.context,
1349                config.context,
1350                !self.full_context,
1351                workspace,
1352            );
1353            existing.reaches_into_outer_context = existing
1354                .reaches_into_outer_context
1355                .max(config.reaches_into_outer_context);
1356            existing.merge_precedence_filter_suppression(&config);
1357            self.conflicting_alts.clear();
1358            false
1359        } else {
1360            let index = self.configs.len();
1361            self.config_index.insert(key, index);
1362            self.configs.push(config);
1363            #[cfg(feature = "perf-counters")]
1364            crate::perf::record_config_insert(self.configs.len());
1365            self.unique_alt = None;
1366            self.conflicting_alts.clear();
1367            true
1368        }
1369    }
1370
1371    pub(crate) fn configs(&self) -> &[AtnConfig] {
1372        &self.configs
1373    }
1374
1375    pub(crate) fn into_configs(self) -> Vec<AtnConfig> {
1376        self.configs
1377    }
1378
1379    pub(crate) const fn is_empty(&self) -> bool {
1380        self.configs.is_empty()
1381    }
1382
1383    pub(crate) const fn len(&self) -> usize {
1384        self.configs.len()
1385    }
1386
1387    pub(crate) fn set_readonly(&mut self, readonly: bool) {
1388        self.readonly = readonly;
1389        if readonly {
1390            self.config_index = FxHashMap::default();
1391            self.conflicting_alts.clear();
1392        }
1393    }
1394
1395    pub(crate) const fn full_context(&self) -> bool {
1396        self.full_context
1397    }
1398
1399    pub(crate) const fn has_semantic_context(&self) -> bool {
1400        self.has_semantic_context
1401    }
1402
1403    pub(crate) fn unique_alt(&mut self) -> Option<usize> {
1404        if self.unique_alt.is_none() {
1405            self.unique_alt = unique_alt(self.configs());
1406        }
1407        self.unique_alt
1408    }
1409
1410    pub(crate) fn alts(&self) -> BTreeSet<usize> {
1411        self.configs.iter().map(|config| config.alt).collect()
1412    }
1413
1414    pub(crate) fn conflicting_alt_subsets(&self) -> Vec<BTreeSet<usize>> {
1415        conflicting_alt_subsets(self.configs())
1416    }
1417
1418    pub(crate) fn conflicting_alts(&mut self) -> BTreeSet<usize> {
1419        if self.conflicting_alts.is_empty() {
1420            self.conflicting_alts = self
1421                .conflicting_alt_subsets()
1422                .into_iter()
1423                .filter(|alts| alts.len() > 1)
1424                .flatten()
1425                .collect();
1426        }
1427        self.conflicting_alts.clone()
1428    }
1429
1430    pub(crate) fn remap_store_ids(
1431        &mut self,
1432        context_remap: &[ContextId],
1433        semantic_context_remap: &[SemanticContextId],
1434        contexts: &ContextArena,
1435        semantic_contexts: &SemanticContextArena,
1436    ) {
1437        for config in &mut self.configs {
1438            let index = usize::try_from(config.context.0).expect("u32 context ID fits usize");
1439            config.set_context(
1440                *context_remap
1441                    .get(index)
1442                    .expect("every imported context ID has a remap"),
1443                contexts,
1444            );
1445            let semantic_index = usize::try_from(config.semantic_context_id().0)
1446                .expect("u32 semantic-context ID fits usize");
1447            config.set_semantic_context(
1448                *semantic_context_remap
1449                    .get(semantic_index)
1450                    .expect("every imported semantic-context ID has a remap"),
1451                semantic_contexts,
1452            );
1453        }
1454        self.config_index.clear();
1455        if !self.readonly {
1456            for (index, config) in self.configs.iter().enumerate() {
1457                self.config_index.insert(AtnConfigKey::from(config), index);
1458            }
1459        }
1460    }
1461
1462    pub(crate) fn fingerprint(&self) -> u64 {
1463        let mut hasher = PredictionFxHasher::default();
1464        self.configs.hash(&mut hasher);
1465        self.full_context.hash(&mut hasher);
1466        self.has_semantic_context.hash(&mut hasher);
1467        self.dips_into_outer_context.hash(&mut hasher);
1468        hasher.finish()
1469    }
1470
1471    pub(crate) fn retained_bytes(&self) -> usize {
1472        self.configs.capacity() * size_of::<AtnConfig>()
1473            + self.config_index.capacity() * size_of::<(AtnConfigKey, usize)>()
1474    }
1475}
1476
1477impl PartialEq for AtnConfigSet {
1478    fn eq(&self, other: &Self) -> bool {
1479        self.configs == other.configs
1480            && self.full_context == other.full_context
1481            && self.has_semantic_context == other.has_semantic_context
1482            && self.dips_into_outer_context == other.dips_into_outer_context
1483    }
1484}
1485
1486impl Eq for AtnConfigSet {}
1487
1488impl Ord for AtnConfigSet {
1489    fn cmp(&self, other: &Self) -> Ordering {
1490        self.configs
1491            .cmp(&other.configs)
1492            .then_with(|| self.full_context.cmp(&other.full_context))
1493            .then_with(|| self.has_semantic_context.cmp(&other.has_semantic_context))
1494            .then_with(|| {
1495                self.dips_into_outer_context
1496                    .cmp(&other.dips_into_outer_context)
1497            })
1498    }
1499}
1500
1501impl PartialOrd for AtnConfigSet {
1502    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1503        Some(self.cmp(other))
1504    }
1505}
1506
1507#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
1508struct AtnConfigKey {
1509    state: usize,
1510    alt: usize,
1511    semantic_context: SemanticContextId,
1512    semantic_provenance: PredictionSemanticProvenanceId,
1513}
1514
1515impl From<&AtnConfig> for AtnConfigKey {
1516    fn from(config: &AtnConfig) -> Self {
1517        Self {
1518            state: config.state,
1519            alt: config.alt,
1520            semantic_context: config.semantic_context_id(),
1521            semantic_provenance: config.semantic_provenance_id(),
1522        }
1523    }
1524}
1525
1526pub(crate) fn unique_alt(configs: &[AtnConfig]) -> Option<usize> {
1527    let mut alt = None;
1528    for config in configs {
1529        match alt {
1530            None => alt = Some(config.alt),
1531            Some(existing) if existing == config.alt => {}
1532            Some(_) => return None,
1533        }
1534    }
1535    alt
1536}
1537
1538pub(crate) fn conflicting_alt_subsets(configs: &[AtnConfig]) -> Vec<BTreeSet<usize>> {
1539    let mut by_state_context = FxHashMap::<(usize, ContextId), BTreeSet<usize>>::default();
1540    for config in configs {
1541        by_state_context
1542            .entry((config.state, config.context))
1543            .or_default()
1544            .insert(config.alt);
1545    }
1546    by_state_context.into_values().collect()
1547}
1548
1549pub(crate) fn all_subsets_conflict(alt_subsets: &[BTreeSet<usize>]) -> bool {
1550    alt_subsets.iter().all(|alts| alts.len() > 1)
1551}
1552
1553pub(crate) fn all_subsets_equal(alt_subsets: &[BTreeSet<usize>]) -> bool {
1554    let mut subsets = alt_subsets.iter();
1555    let Some(first) = subsets.next() else {
1556        return true;
1557    };
1558    subsets.all(|alts| alts == first)
1559}
1560
1561pub(crate) fn single_viable_alt(alt_subsets: &[BTreeSet<usize>]) -> Option<usize> {
1562    let mut result = None;
1563    for alts in alt_subsets {
1564        let min_alt = alts.iter().next().copied()?;
1565        match result {
1566            None => result = Some(min_alt),
1567            Some(existing) if existing == min_alt => {}
1568            Some(_) => return None,
1569        }
1570    }
1571    result
1572}
1573
1574#[derive(Clone, Debug, Eq, PartialEq)]
1575pub(crate) struct SllConflict {
1576    pub(crate) alts: BTreeSet<usize>,
1577    pub(crate) exact: bool,
1578    pub(crate) from_context_containment: bool,
1579}
1580
1581pub(crate) fn has_sll_conflict_terminating_prediction(
1582    configs: &AtnConfigSet,
1583    is_rule_stop_state: impl Fn(usize) -> bool,
1584) -> bool {
1585    if configs
1586        .configs()
1587        .iter()
1588        .all(|config| is_rule_stop_state(config.state))
1589    {
1590        return true;
1591    }
1592    let alt_subsets = configs.conflicting_alt_subsets();
1593    alt_subsets.iter().any(|alts| alts.len() > 1)
1594        && !has_state_associated_with_one_alt(configs.configs())
1595}
1596
1597pub(crate) fn exact_context_sll_conflict(
1598    configs: &AtnConfigSet,
1599    arena: &mut ContextArena,
1600    workspace: &mut PredictionWorkspace,
1601    is_rule_stop_state: impl Fn(usize) -> bool,
1602) -> Option<SllConflict> {
1603    debug_assert!(!configs.full_context());
1604    if configs.len() <= 1 || unique_alt(configs.configs()).is_some() {
1605        return None;
1606    }
1607
1608    let state_key = |config: &AtnConfig| {
1609        if is_rule_stop_state(config.state) {
1610            None
1611        } else {
1612            Some(config.state)
1613        }
1614    };
1615    let mut sorted = configs.configs().iter().collect::<Vec<_>>();
1616    sorted.sort_unstable_by_key(|config| (state_key(config), config.alt));
1617
1618    let min_alt = sorted.first()?.alt;
1619    let mut state_start = 0;
1620    let mut represented_alts = Vec::new();
1621    let mut exact = !configs.dips_into_outer_context;
1622    let mut alts = BTreeSet::from([min_alt]);
1623
1624    while state_start < sorted.len() {
1625        let current_state = state_key(sorted[state_start]);
1626        let mut state_end = state_start + 1;
1627        while state_end < sorted.len() && state_key(sorted[state_end]) == current_state {
1628            state_end += 1;
1629        }
1630        if sorted[state_start].alt != min_alt {
1631            return None;
1632        }
1633
1634        let mut current_alts = Vec::new();
1635        let mut alt_start = state_start;
1636        let mut joined_min_context = None;
1637        while alt_start < state_end {
1638            let current_alt = sorted[alt_start].alt;
1639            current_alts.push(current_alt);
1640            alts.insert(current_alt);
1641
1642            let mut alt_end = alt_start + 1;
1643            while alt_end < state_end && sorted[alt_end].alt == current_alt {
1644                alt_end += 1;
1645            }
1646            let joined_context = sorted[alt_start + 1..alt_end]
1647                .iter()
1648                .fold(sorted[alt_start].context, |joined, config| {
1649                    arena.merge(joined, config.context, true, workspace)
1650                });
1651            if current_alt == min_alt {
1652                joined_min_context = Some(joined_context);
1653            } else {
1654                let min_context =
1655                    joined_min_context.expect("minimum alternative starts every state group");
1656                if arena.merge(min_context, joined_context, true, workspace) != min_context {
1657                    return None;
1658                }
1659                exact &= min_context == joined_context;
1660            }
1661            alt_start = alt_end;
1662        }
1663
1664        if represented_alts.is_empty() {
1665            represented_alts = current_alts;
1666        } else {
1667            exact &= represented_alts == current_alts;
1668        }
1669        state_start = state_end;
1670    }
1671
1672    Some(SllConflict {
1673        alts,
1674        exact,
1675        from_context_containment: true,
1676    })
1677}
1678
1679fn has_state_associated_with_one_alt(configs: &[AtnConfig]) -> bool {
1680    let mut by_state = BTreeMap::<usize, BTreeSet<usize>>::new();
1681    for config in configs {
1682        by_state.entry(config.state).or_default().insert(config.alt);
1683    }
1684    by_state.values().any(|alts| alts.len() == 1)
1685}
1686
1687#[cfg(test)]
1688#[allow(clippy::disallowed_methods)] // `insta` assertion macros unwrap internal I/O.
1689mod tests {
1690    use super::*;
1691
1692    #[test]
1693    fn arena_interns_singletons_without_per_context_objects() {
1694        let mut arena = ContextArena::new();
1695        let first = arena.singleton(EMPTY_CONTEXT, 7);
1696        let second = arena.singleton(EMPTY_CONTEXT, 7);
1697
1698        assert_eq!(first, second);
1699        assert_eq!(arena.stats().singleton_contexts, 1);
1700        assert_eq!(arena.stats().interner_hits, 1);
1701    }
1702
1703    #[test]
1704    fn array_interner_verifies_payload_after_hash_collision() {
1705        let mut arena = ContextArena::new();
1706        let first_parent = arena.singleton(EMPTY_CONTEXT, 1);
1707        let second_parent = arena.singleton(EMPTY_CONTEXT, 2);
1708        let expected = [(first_parent, 10), (second_parent, 20)];
1709        let colliding = [(second_parent, 10), (first_parent, 20)];
1710        let cached_hash = prediction_context_array_hash(&arena, &expected);
1711        let start = u32::try_from(arena.array_parents.len()).expect("pool index fits u32");
1712        arena
1713            .array_parents
1714            .extend(colliding.iter().map(|(parent, _)| *parent));
1715        arena
1716            .array_return_states
1717            .extend(colliding.iter().map(|(_, return_state)| *return_state));
1718        let collision = arena.push_record(ContextRecord {
1719            tag: ContextTag::Array,
1720            cached_hash,
1721            parent_or_start: start,
1722            return_state_or_len: 2,
1723        });
1724
1725        let interned = arena.intern_entries(&expected);
1726
1727        assert_ne!(interned, collision);
1728        assert_eq!(arena.entry(interned, 0), Some(expected[0]));
1729        assert_eq!(arena.entry(interned, 1), Some(expected[1]));
1730    }
1731
1732    #[test]
1733    fn merge_with_empty_preserves_full_context_empty_path() {
1734        let mut arena = ContextArena::new();
1735        let mut workspace = PredictionWorkspace::default();
1736        let singleton = arena.singleton(EMPTY_CONTEXT, 42);
1737
1738        let merged = arena.merge(singleton, EMPTY_CONTEXT, false, &mut workspace);
1739
1740        assert_eq!(arena.len(merged), 2);
1741        assert_eq!(arena.return_state(merged, 0), Some(42));
1742        assert_eq!(arena.parent(merged, 0), Some(EMPTY_CONTEXT));
1743        assert_eq!(arena.return_state(merged, 1), Some(EMPTY_RETURN_STATE));
1744        assert!(arena.has_empty_path(merged));
1745    }
1746
1747    #[test]
1748    fn wildcard_merge_collapses_to_empty() {
1749        let mut arena = ContextArena::new();
1750        let mut workspace = PredictionWorkspace::default();
1751        let singleton = arena.singleton(EMPTY_CONTEXT, 42);
1752
1753        assert_eq!(
1754            arena.merge(singleton, EMPTY_CONTEXT, true, &mut workspace),
1755            EMPTY_CONTEXT
1756        );
1757    }
1758
1759    #[test]
1760    fn merge_is_order_independent() {
1761        let mut arena = ContextArena::new();
1762        let mut workspace = PredictionWorkspace::default();
1763        let left_parent = arena.singleton(EMPTY_CONTEXT, 100);
1764        let right_parent = arena.singleton(EMPTY_CONTEXT, 200);
1765        let left = arena.singleton(left_parent, 7);
1766        let right = arena.singleton(right_parent, 7);
1767
1768        let left_right = arena.merge(left, right, false, &mut workspace);
1769        workspace.reset();
1770        let right_left = arena.merge(right, left, false, &mut workspace);
1771
1772        assert_eq!(left_right, right_left);
1773        assert_eq!(arena.len(left_right), 1);
1774        let merged_parent = arena.parent(left_right, 0).expect("merged parent");
1775        assert_eq!(arena.len(merged_parent), 2);
1776        assert_eq!(arena.return_state(merged_parent, 0), Some(100));
1777        assert_eq!(arena.return_state(merged_parent, 1), Some(200));
1778    }
1779
1780    #[test]
1781    fn import_remaps_contexts_into_destination_arena() {
1782        let mut source = ContextArena::new();
1783        let parent = source.singleton(EMPTY_CONTEXT, 3);
1784        let child = source.singleton(parent, 9);
1785        let mut destination = ContextArena::new();
1786        let mut workspace = PredictionWorkspace::default();
1787
1788        let remap = destination.import_all(&source, &mut workspace);
1789        let imported = remap[usize::try_from(child.0).expect("context ID fits usize")];
1790
1791        assert_eq!(destination.return_state(imported, 0), Some(9));
1792        let imported_parent = destination.parent(imported, 0).expect("parent");
1793        assert_eq!(destination.return_state(imported_parent, 0), Some(3));
1794    }
1795
1796    #[test]
1797    fn semantic_context_arena_interns_imports_and_accounts_for_payloads() {
1798        let predicate = SemanticContext::Predicate {
1799            rule_index: 2,
1800            pred_index: 3,
1801            context_dependent: true,
1802        };
1803        let mut source = SemanticContextArena::new();
1804        let predicate_id = source.intern(predicate.clone());
1805        assert_eq!(source.intern(predicate), predicate_id);
1806
1807        let combined_id = source.and(predicate_id, SemanticContext::Precedence { precedence: 4 });
1808        assert_ne!(combined_id, predicate_id);
1809        assert_eq!(source.len(), 3);
1810        assert!(source.retained_bytes() >= source.len() * size_of::<SemanticContext>());
1811
1812        let mut destination = SemanticContextArena::new();
1813        let distracting_id = destination.intern(SemanticContext::Precedence { precedence: 99 });
1814        assert_eq!(distracting_id, predicate_id, "both arenas allocate ID 1");
1815        let remap = destination.import_all(&source);
1816        let imported = remap[usize::try_from(combined_id.0).expect("semantic ID fits usize")];
1817        assert_eq!(destination.get(imported), source.get(combined_id));
1818        assert_ne!(imported, combined_id);
1819    }
1820
1821    #[test]
1822    fn semantic_context_arena_verifies_hash_collisions() {
1823        let mut arena = SemanticContextArena::new();
1824        let first = SemanticContext::Precedence { precedence: 1 };
1825        let first_id = arena.intern(first);
1826        let second = SemanticContext::Precedence { precedence: 2 };
1827        arena
1828            .interner_heads
1829            .insert(semantic_context_hash(&second), first_id);
1830
1831        let second_id = arena.intern(second.clone());
1832        assert_ne!(second_id, first_id);
1833        assert_eq!(arena.intern(second), second_id);
1834    }
1835
1836    #[test]
1837    fn config_set_merges_context_ids() {
1838        let mut arena = ContextArena::new();
1839        let mut workspace = PredictionWorkspace::default();
1840        let left = arena.singleton(EMPTY_CONTEXT, 1);
1841        let right = arena.singleton(EMPTY_CONTEXT, 2);
1842        let mut set = AtnConfigSet::new_full_context(true);
1843
1844        assert!(set.add(
1845            AtnConfig::new(1, 1, left, &arena),
1846            &mut arena,
1847            &mut workspace
1848        ));
1849        assert!(!set.add(
1850            AtnConfig::new(1, 1, right, &arena),
1851            &mut arena,
1852            &mut workspace
1853        ));
1854        assert_eq!(set.len(), 1);
1855        assert_eq!(arena.len(set.configs()[0].context), 2);
1856    }
1857
1858    #[test]
1859    fn exact_context_conflict_proves_containment_without_shared_context_ids() {
1860        let mut arena = ContextArena::new();
1861        let mut workspace = PredictionWorkspace::default();
1862        let first = arena.singleton(EMPTY_CONTEXT, 10);
1863        let second = arena.singleton(EMPTY_CONTEXT, 20);
1864        let containing = arena.merge(first, second, true, &mut workspace);
1865        let mut configs = AtnConfigSet::new();
1866        configs.add(
1867            AtnConfig::new(7, 1, containing, &arena),
1868            &mut arena,
1869            &mut workspace,
1870        );
1871        configs.add(
1872            AtnConfig::new(7, 2, first, &arena),
1873            &mut arena,
1874            &mut workspace,
1875        );
1876
1877        assert!(!has_sll_conflict_terminating_prediction(&configs, |_| {
1878            false
1879        }));
1880        insta::assert_debug_snapshot!(
1881            "exact_context_conflict_proves_containment_without_shared_context_ids",
1882            exact_context_sll_conflict(&configs, &mut arena, &mut workspace, |_| false)
1883        );
1884    }
1885
1886    #[test]
1887    fn exact_context_conflict_joins_semantically_distinct_configs() {
1888        let mut arena = ContextArena::new();
1889        let mut semantic_contexts = SemanticContextArena::new();
1890        let mut workspace = PredictionWorkspace::default();
1891        let first = arena.singleton(EMPTY_CONTEXT, 10);
1892        let second = arena.singleton(EMPTY_CONTEXT, 20);
1893        let joined = arena.merge(first, second, true, &mut workspace);
1894        let predicate = |pred_index| SemanticContext::Predicate {
1895            rule_index: 0,
1896            pred_index,
1897            context_dependent: false,
1898        };
1899        let mut configs = AtnConfigSet::new();
1900        configs.add(
1901            AtnConfig::new(7, 1, first, &arena)
1902                .with_semantic_context(predicate(0), &mut semantic_contexts),
1903            &mut arena,
1904            &mut workspace,
1905        );
1906        configs.add(
1907            AtnConfig::new(7, 1, second, &arena)
1908                .with_semantic_context(predicate(1), &mut semantic_contexts),
1909            &mut arena,
1910            &mut workspace,
1911        );
1912        configs.add(
1913            AtnConfig::new(7, 2, joined, &arena),
1914            &mut arena,
1915            &mut workspace,
1916        );
1917
1918        assert!(!has_sll_conflict_terminating_prediction(&configs, |_| {
1919            false
1920        }));
1921        insta::assert_debug_snapshot!(
1922            "exact_context_conflict_joins_semantically_distinct_configs",
1923            exact_context_sll_conflict(&configs, &mut arena, &mut workspace, |_| false)
1924        );
1925    }
1926
1927    #[test]
1928    fn exact_context_conflict_declines_a_non_contained_context() {
1929        let mut arena = ContextArena::new();
1930        let mut workspace = PredictionWorkspace::default();
1931        let first = arena.singleton(EMPTY_CONTEXT, 10);
1932        let second = arena.singleton(EMPTY_CONTEXT, 20);
1933        let mut configs = AtnConfigSet::new();
1934        configs.add(
1935            AtnConfig::new(7, 1, first, &arena),
1936            &mut arena,
1937            &mut workspace,
1938        );
1939        configs.add(
1940            AtnConfig::new(7, 2, second, &arena),
1941            &mut arena,
1942            &mut workspace,
1943        );
1944
1945        assert_eq!(
1946            exact_context_sll_conflict(&configs, &mut arena, &mut workspace, |_| false),
1947            None
1948        );
1949    }
1950
1951    #[test]
1952    fn exact_context_conflict_requires_the_minimum_alt_in_every_state() {
1953        let mut arena = ContextArena::new();
1954        let mut workspace = PredictionWorkspace::default();
1955        let context = arena.singleton(EMPTY_CONTEXT, 10);
1956        let mut configs = AtnConfigSet::new();
1957        for (state, alt) in [(7, 1), (7, 2), (8, 2)] {
1958            configs.add(
1959                AtnConfig::new(state, alt, context, &arena),
1960                &mut arena,
1961                &mut workspace,
1962            );
1963        }
1964
1965        assert_eq!(
1966            exact_context_sll_conflict(&configs, &mut arena, &mut workspace, |_| false),
1967            None
1968        );
1969    }
1970
1971    #[test]
1972    fn exact_context_conflict_marks_different_state_alt_sets_inexact() {
1973        let mut arena = ContextArena::new();
1974        let mut workspace = PredictionWorkspace::default();
1975        let context = arena.singleton(EMPTY_CONTEXT, 10);
1976        let mut configs = AtnConfigSet::new();
1977        for (state, alt) in [(7, 1), (7, 2), (8, 1), (8, 2), (8, 3)] {
1978            configs.add(
1979                AtnConfig::new(state, alt, context, &arena),
1980                &mut arena,
1981                &mut workspace,
1982            );
1983        }
1984
1985        insta::assert_debug_snapshot!(
1986            "exact_context_conflict_marks_different_state_alt_sets_inexact",
1987            exact_context_sll_conflict(&configs, &mut arena, &mut workspace, |_| false)
1988        );
1989    }
1990
1991    #[test]
1992    fn exact_context_conflict_marks_outer_context_reach_inexact() {
1993        let mut arena = ContextArena::new();
1994        let mut workspace = PredictionWorkspace::default();
1995        let context = arena.singleton(EMPTY_CONTEXT, 10);
1996        let mut configs = AtnConfigSet::new();
1997        let mut outer = AtnConfig::new(7, 1, context, &arena);
1998        outer.reaches_into_outer_context = 1;
1999        configs.add(outer, &mut arena, &mut workspace);
2000        configs.add(
2001            AtnConfig::new(7, 2, context, &arena),
2002            &mut arena,
2003            &mut workspace,
2004        );
2005
2006        insta::assert_debug_snapshot!(
2007            "exact_context_conflict_marks_outer_context_reach_inexact",
2008            exact_context_sll_conflict(&configs, &mut arena, &mut workspace, |_| false)
2009        );
2010    }
2011
2012    #[test]
2013    fn predicate_provenance_is_idempotent_per_rule_path() {
2014        let arena = ContextArena::new();
2015        let mut provenance = PredictionSemanticProvenanceArena::default();
2016        let mut config = AtnConfig::new(1, 1, EMPTY_CONTEXT, &arena);
2017        config.enter_prediction_rule(&mut provenance, 4, 2);
2018        config.record_prediction_predicate(&mut provenance, 2, 3);
2019        let after_first = provenance
2020            .predicate_calls(config.semantic_provenance_id())
2021            .to_vec();
2022
2023        config.record_prediction_predicate(&mut provenance, 2, 3);
2024
2025        assert_eq!(
2026            provenance.predicate_calls(config.semantic_provenance_id()),
2027            after_first,
2028            "revisiting one predicate on the same rule path must not grow closure keys"
2029        );
2030    }
2031
2032    #[test]
2033    fn provenance_arena_stores_records_once_and_verifies_hash_collisions() {
2034        let mut arena = PredictionSemanticProvenanceArena::default();
2035        let first = PredictionSemanticProvenance {
2036            active_rule_calls: vec![PredictionRuleCall {
2037                source_state: 4,
2038                rule_index: 2,
2039            }],
2040            predicate_calls: Vec::new(),
2041        };
2042        let first_id = arena.intern(first.clone());
2043
2044        assert_eq!(arena.intern(first), first_id);
2045        assert_eq!(arena.records.len(), 1);
2046        assert_eq!(arena.interner_next.len(), 1);
2047
2048        let second = PredictionSemanticProvenance {
2049            active_rule_calls: vec![PredictionRuleCall {
2050                source_state: 5,
2051                rule_index: 3,
2052            }],
2053            predicate_calls: Vec::new(),
2054        };
2055        let mut hasher = PredictionFxHasher::default();
2056        second.hash(&mut hasher);
2057        arena.interner_heads.insert(hasher.finish(), first_id);
2058
2059        let second_id = arena.intern(second.clone());
2060        assert_ne!(second_id, first_id);
2061        assert_eq!(arena.intern(second), second_id);
2062        assert_eq!(arena.records.len(), 2);
2063        assert_eq!(arena.interner_next.len(), 2);
2064    }
2065
2066    #[test]
2067    fn config_set_keeps_distinct_prediction_provenance() {
2068        let mut arena = ContextArena::new();
2069        let semantic_contexts = SemanticContextArena::new();
2070        let mut provenance = PredictionSemanticProvenanceArena::default();
2071        let mut workspace = PredictionWorkspace::default();
2072        let mut first = AtnConfig::new(1, 1, EMPTY_CONTEXT, &arena);
2073        first.enter_prediction_rule(&mut provenance, 4, 2);
2074        let mut second = AtnConfig::new(1, 1, EMPTY_CONTEXT, &arena);
2075        second.enter_prediction_rule(&mut provenance, 5, 3);
2076        let mut set = AtnConfigSet::new();
2077
2078        assert!(set.add(first.clone(), &mut arena, &mut workspace));
2079        assert!(set.add(second, &mut arena, &mut workspace));
2080        assert!(!set.add(first, &mut arena, &mut workspace));
2081        assert_eq!(set.len(), 2);
2082        assert_eq!(set.config_index.len(), set.len());
2083
2084        set.remap_store_ids(
2085            &[EMPTY_CONTEXT],
2086            &[SemanticContextId::default()],
2087            &arena,
2088            &semantic_contexts,
2089        );
2090        assert_eq!(set.config_index.len(), set.len());
2091    }
2092
2093    #[cfg(target_pointer_width = "64")]
2094    #[test]
2095    fn parser_config_hot_path_layout_stays_compact() {
2096        // Inline SemanticContext storage was 64 bytes in release / 72 in debug;
2097        // cloning it into AtnConfigKey made each key 56 bytes.
2098        let debug_generation = if cfg!(debug_assertions) {
2099            size_of::<u64>()
2100        } else {
2101            0
2102        };
2103        assert_eq!(size_of::<AtnConfig>(), 40 + debug_generation);
2104        assert_eq!(size_of::<AtnConfigKey>(), 24);
2105    }
2106
2107    #[test]
2108    fn workspace_drops_pathological_capacity() {
2109        let mut workspace = PredictionWorkspace::default();
2110        workspace
2111            .merge_cache
2112            .reserve(MAX_RETAINED_MERGE_CACHE_ENTRIES.saturating_mul(2));
2113        workspace
2114            .entries
2115            .reserve(MAX_RETAINED_CONTEXT_ENTRIES.saturating_mul(2));
2116        workspace.reset();
2117
2118        assert!(workspace.merge_cache.capacity() <= MAX_RETAINED_MERGE_CACHE_ENTRIES);
2119        assert!(workspace.entries.capacity() <= MAX_RETAINED_CONTEXT_ENTRIES);
2120    }
2121
2122    mod upstream_graph_nodes {
2123        use super::*;
2124        use std::collections::{BTreeSet, HashMap, VecDeque};
2125        use std::fmt::Write;
2126
2127        const EMPTY_WILDCARD_DOT: &str = concat!(
2128            "digraph G {\n",
2129            "rankdir=LR;\n",
2130            "  s0[label=\"*\"];\n",
2131            "}\n",
2132        );
2133        const EMPTY_FULL_CONTEXT_DOT: &str = concat!(
2134            "digraph G {\n",
2135            "rankdir=LR;\n",
2136            "  s0[label=\"$\"];\n",
2137            "}\n",
2138        );
2139        const X_EMPTY_FULL_CONTEXT_DOT: &str = concat!(
2140            "digraph G {\n",
2141            "rankdir=LR;\n",
2142            "  s0[shape=record, label=\"<p0>|<p1>$\"];\n",
2143            "  s1[label=\"$\"];\n",
2144            "  s0:p0->s1[label=\"9\"];\n",
2145            "}\n",
2146        );
2147        const A_DOT: &str = concat!(
2148            "digraph G {\n",
2149            "rankdir=LR;\n",
2150            "  s0[label=\"0\"];\n",
2151            "  s1[label=\"*\"];\n",
2152            "  s0->s1[label=\"1\"];\n",
2153            "}\n",
2154        );
2155        const A_EMPTY_AX_FULL_CONTEXT_DOT: &str = concat!(
2156            "digraph G {\n",
2157            "rankdir=LR;\n",
2158            "  s0[label=\"0\"];\n",
2159            "  s1[shape=record, label=\"<p0>|<p1>$\"];\n",
2160            "  s2[label=\"$\"];\n",
2161            "  s0->s1[label=\"1\"];\n",
2162            "  s1:p0->s2[label=\"9\"];\n",
2163            "}\n",
2164        );
2165        const NESTED_FULL_CONTEXT_DOT: &str = concat!(
2166            "digraph G {\n",
2167            "rankdir=LR;\n",
2168            "  s0[shape=record, label=\"<p0>|<p1>$\"];\n",
2169            "  s1[shape=record, label=\"<p0>|<p1>$\"];\n",
2170            "  s2[label=\"$\"];\n",
2171            "  s0:p0->s1[label=\"8\"];\n",
2172            "  s1:p0->s2[label=\"8\"];\n",
2173            "}\n",
2174        );
2175        const A_B_DOT: &str = concat!(
2176            "digraph G {\n",
2177            "rankdir=LR;\n",
2178            "  s0[shape=record, label=\"<p0>|<p1>\"];\n",
2179            "  s1[label=\"*\"];\n",
2180            "  s0:p0->s1[label=\"1\"];\n",
2181            "  s0:p1->s1[label=\"2\"];\n",
2182            "}\n",
2183        );
2184        const AX_AX_DOT: &str = concat!(
2185            "digraph G {\n",
2186            "rankdir=LR;\n",
2187            "  s0[label=\"0\"];\n",
2188            "  s1[label=\"1\"];\n",
2189            "  s2[label=\"*\"];\n",
2190            "  s0->s1[label=\"1\"];\n",
2191            "  s1->s2[label=\"9\"];\n",
2192            "}\n",
2193        );
2194        const ABX_ABX_DOT: &str = concat!(
2195            "digraph G {\n",
2196            "rankdir=LR;\n",
2197            "  s0[label=\"0\"];\n",
2198            "  s1[label=\"1\"];\n",
2199            "  s2[label=\"2\"];\n",
2200            "  s3[label=\"*\"];\n",
2201            "  s0->s1[label=\"1\"];\n",
2202            "  s1->s2[label=\"2\"];\n",
2203            "  s2->s3[label=\"9\"];\n",
2204            "}\n",
2205        );
2206        const ABX_ACX_DOT: &str = concat!(
2207            "digraph G {\n",
2208            "rankdir=LR;\n",
2209            "  s0[label=\"0\"];\n",
2210            "  s1[shape=record, label=\"<p0>|<p1>\"];\n",
2211            "  s2[label=\"2\"];\n",
2212            "  s3[label=\"*\"];\n",
2213            "  s0->s1[label=\"1\"];\n",
2214            "  s1:p0->s2[label=\"2\"];\n",
2215            "  s1:p1->s2[label=\"3\"];\n",
2216            "  s2->s3[label=\"9\"];\n",
2217            "}\n",
2218        );
2219        const AX_BX_DOT: &str = concat!(
2220            "digraph G {\n",
2221            "rankdir=LR;\n",
2222            "  s0[shape=record, label=\"<p0>|<p1>\"];\n",
2223            "  s1[label=\"1\"];\n",
2224            "  s2[label=\"*\"];\n",
2225            "  s0:p0->s1[label=\"1\"];\n",
2226            "  s0:p1->s1[label=\"2\"];\n",
2227            "  s1->s2[label=\"9\"];\n",
2228            "}\n",
2229        );
2230        const AX_BY_DOT: &str = concat!(
2231            "digraph G {\n",
2232            "rankdir=LR;\n",
2233            "  s0[shape=record, label=\"<p0>|<p1>\"];\n",
2234            "  s2[label=\"2\"];\n",
2235            "  s3[label=\"*\"];\n",
2236            "  s1[label=\"1\"];\n",
2237            "  s0:p0->s1[label=\"1\"];\n",
2238            "  s0:p1->s2[label=\"2\"];\n",
2239            "  s2->s3[label=\"10\"];\n",
2240            "  s1->s3[label=\"9\"];\n",
2241            "}\n",
2242        );
2243        const A_EMPTY_BX_DOT: &str = concat!(
2244            "digraph G {\n",
2245            "rankdir=LR;\n",
2246            "  s0[shape=record, label=\"<p0>|<p1>\"];\n",
2247            "  s2[label=\"2\"];\n",
2248            "  s1[label=\"*\"];\n",
2249            "  s0:p0->s1[label=\"1\"];\n",
2250            "  s0:p1->s2[label=\"2\"];\n",
2251            "  s2->s1[label=\"9\"];\n",
2252            "}\n",
2253        );
2254        const A_EMPTY_BX_FULL_CONTEXT_DOT: &str = concat!(
2255            "digraph G {\n",
2256            "rankdir=LR;\n",
2257            "  s0[shape=record, label=\"<p0>|<p1>\"];\n",
2258            "  s2[label=\"2\"];\n",
2259            "  s1[label=\"$\"];\n",
2260            "  s0:p0->s1[label=\"1\"];\n",
2261            "  s0:p1->s2[label=\"2\"];\n",
2262            "  s2->s1[label=\"9\"];\n",
2263            "}\n",
2264        );
2265        const AEX_BFX_DOT: &str = concat!(
2266            "digraph G {\n",
2267            "rankdir=LR;\n",
2268            "  s0[shape=record, label=\"<p0>|<p1>\"];\n",
2269            "  s2[label=\"2\"];\n",
2270            "  s3[label=\"3\"];\n",
2271            "  s4[label=\"*\"];\n",
2272            "  s1[label=\"1\"];\n",
2273            "  s0:p0->s1[label=\"1\"];\n",
2274            "  s0:p1->s2[label=\"2\"];\n",
2275            "  s2->s3[label=\"6\"];\n",
2276            "  s3->s4[label=\"9\"];\n",
2277            "  s1->s3[label=\"5\"];\n",
2278            "}\n",
2279        );
2280        const A_B_C_DOT: &str = concat!(
2281            "digraph G {\n",
2282            "rankdir=LR;\n",
2283            "  s0[shape=record, label=\"<p0>|<p1>|<p2>\"];\n",
2284            "  s1[label=\"*\"];\n",
2285            "  s0:p0->s1[label=\"1\"];\n",
2286            "  s0:p1->s1[label=\"2\"];\n",
2287            "  s0:p2->s1[label=\"3\"];\n",
2288            "}\n",
2289        );
2290        const AAX_AAY_DOT: &str = concat!(
2291            "digraph G {\n",
2292            "rankdir=LR;\n",
2293            "  s0[label=\"0\"];\n",
2294            "  s1[shape=record, label=\"<p0>|<p1>\"];\n",
2295            "  s2[label=\"*\"];\n",
2296            "  s0->s1[label=\"1\"];\n",
2297            "  s1:p0->s2[label=\"9\"];\n",
2298            "  s1:p1->s2[label=\"10\"];\n",
2299            "}\n",
2300        );
2301        const AAXC_AAYD_DOT: &str = concat!(
2302            "digraph G {\n",
2303            "rankdir=LR;\n",
2304            "  s0[shape=record, label=\"<p0>|<p1>|<p2>\"];\n",
2305            "  s2[label=\"*\"];\n",
2306            "  s1[shape=record, label=\"<p0>|<p1>\"];\n",
2307            "  s0:p0->s1[label=\"1\"];\n",
2308            "  s0:p1->s2[label=\"3\"];\n",
2309            "  s0:p2->s2[label=\"4\"];\n",
2310            "  s1:p0->s2[label=\"9\"];\n",
2311            "  s1:p1->s2[label=\"10\"];\n",
2312            "}\n",
2313        );
2314        const AAUBV_ACWDX_DOT: &str = concat!(
2315            "digraph G {\n",
2316            "rankdir=LR;\n",
2317            "  s0[shape=record, label=\"<p0>|<p1>|<p2>|<p3>\"];\n",
2318            "  s4[label=\"4\"];\n",
2319            "  s5[label=\"*\"];\n",
2320            "  s3[label=\"3\"];\n",
2321            "  s2[label=\"2\"];\n",
2322            "  s1[label=\"1\"];\n",
2323            "  s0:p0->s1[label=\"1\"];\n",
2324            "  s0:p1->s2[label=\"2\"];\n",
2325            "  s0:p2->s3[label=\"3\"];\n",
2326            "  s0:p3->s4[label=\"4\"];\n",
2327            "  s4->s5[label=\"9\"];\n",
2328            "  s3->s5[label=\"8\"];\n",
2329            "  s2->s5[label=\"7\"];\n",
2330            "  s1->s5[label=\"6\"];\n",
2331            "}\n",
2332        );
2333        const AAUBV_ABVDX_DOT: &str = concat!(
2334            "digraph G {\n",
2335            "rankdir=LR;\n",
2336            "  s0[shape=record, label=\"<p0>|<p1>|<p2>\"];\n",
2337            "  s3[label=\"3\"];\n",
2338            "  s4[label=\"*\"];\n",
2339            "  s2[label=\"2\"];\n",
2340            "  s1[label=\"1\"];\n",
2341            "  s0:p0->s1[label=\"1\"];\n",
2342            "  s0:p1->s2[label=\"2\"];\n",
2343            "  s0:p2->s3[label=\"4\"];\n",
2344            "  s3->s4[label=\"9\"];\n",
2345            "  s2->s4[label=\"7\"];\n",
2346            "  s1->s4[label=\"6\"];\n",
2347            "}\n",
2348        );
2349        const AAUBV_ABWDX_DOT: &str = concat!(
2350            "digraph G {\n",
2351            "rankdir=LR;\n",
2352            "  s0[shape=record, label=\"<p0>|<p1>|<p2>\"];\n",
2353            "  s3[label=\"3\"];\n",
2354            "  s4[label=\"*\"];\n",
2355            "  s2[shape=record, label=\"<p0>|<p1>\"];\n",
2356            "  s1[label=\"1\"];\n",
2357            "  s0:p0->s1[label=\"1\"];\n",
2358            "  s0:p1->s2[label=\"2\"];\n",
2359            "  s0:p2->s3[label=\"4\"];\n",
2360            "  s3->s4[label=\"9\"];\n",
2361            "  s2:p0->s4[label=\"7\"];\n",
2362            "  s2:p1->s4[label=\"8\"];\n",
2363            "  s1->s4[label=\"6\"];\n",
2364            "}\n",
2365        );
2366        const AAUBV_ABVDU_DOT: &str = concat!(
2367            "digraph G {\n",
2368            "rankdir=LR;\n",
2369            "  s0[shape=record, label=\"<p0>|<p1>|<p2>\"];\n",
2370            "  s2[label=\"2\"];\n",
2371            "  s3[label=\"*\"];\n",
2372            "  s1[label=\"1\"];\n",
2373            "  s0:p0->s1[label=\"1\"];\n",
2374            "  s0:p1->s2[label=\"2\"];\n",
2375            "  s0:p2->s1[label=\"4\"];\n",
2376            "  s2->s3[label=\"7\"];\n",
2377            "  s1->s3[label=\"6\"];\n",
2378            "}\n",
2379        );
2380        const AAUBU_ACUDU_DOT: &str = concat!(
2381            "digraph G {\n",
2382            "rankdir=LR;\n",
2383            "  s0[shape=record, label=\"<p0>|<p1>|<p2>|<p3>\"];\n",
2384            "  s1[label=\"1\"];\n",
2385            "  s2[label=\"*\"];\n",
2386            "  s0:p0->s1[label=\"1\"];\n",
2387            "  s0:p1->s1[label=\"2\"];\n",
2388            "  s0:p2->s1[label=\"3\"];\n",
2389            "  s0:p3->s1[label=\"4\"];\n",
2390            "  s1->s2[label=\"6\"];\n",
2391            "}\n",
2392        );
2393
2394        #[derive(Clone, Copy)]
2395        enum ContextSpec {
2396            Empty,
2397            Chain(&'static [usize]),
2398            Array(&'static [&'static [usize]]),
2399        }
2400
2401        #[derive(Clone, Copy)]
2402        enum Scenario {
2403            Merge {
2404                left: ContextSpec,
2405                right: ContextSpec,
2406            },
2407            NestedFullContext,
2408        }
2409
2410        struct GraphCase {
2411            source_test: &'static str,
2412            logical_id: &'static str,
2413            scenario: Scenario,
2414            root_is_wildcard: bool,
2415            expected: &'static str,
2416        }
2417
2418        impl GraphCase {
2419            const fn merge(
2420                source_test: &'static str,
2421                logical_id: &'static str,
2422                left: ContextSpec,
2423                right: ContextSpec,
2424                root_is_wildcard: bool,
2425                expected: &'static str,
2426            ) -> Self {
2427                Self {
2428                    source_test,
2429                    logical_id,
2430                    scenario: Scenario::Merge { left, right },
2431                    root_is_wildcard,
2432                    expected,
2433                }
2434            }
2435
2436            const fn nested_full_context(
2437                source_test: &'static str,
2438                logical_id: &'static str,
2439                expected: &'static str,
2440            ) -> Self {
2441                Self {
2442                    source_test,
2443                    logical_id,
2444                    scenario: Scenario::NestedFullContext,
2445                    root_is_wildcard: false,
2446                    expected,
2447                }
2448            }
2449        }
2450
2451        const CASES: &[GraphCase] = &[
2452            GraphCase::merge(
2453                "test_$_$",
2454                "testgraphnodes-test-9ea85e6b69",
2455                ContextSpec::Empty,
2456                ContextSpec::Empty,
2457                true,
2458                EMPTY_WILDCARD_DOT,
2459            ),
2460            GraphCase::merge(
2461                "test_$_$_fullctx",
2462                "testgraphnodes-test-fullctx-3a6b2d8201",
2463                ContextSpec::Empty,
2464                ContextSpec::Empty,
2465                false,
2466                EMPTY_FULL_CONTEXT_DOT,
2467            ),
2468            GraphCase::merge(
2469                "test_x_$",
2470                "testgraphnodes-test-x-546922b23c",
2471                ContextSpec::Chain(&[9]),
2472                ContextSpec::Empty,
2473                true,
2474                EMPTY_WILDCARD_DOT,
2475            ),
2476            GraphCase::merge(
2477                "test_x_$_fullctx",
2478                "testgraphnodes-test-x-fullctx-7fdaaf473e",
2479                ContextSpec::Chain(&[9]),
2480                ContextSpec::Empty,
2481                false,
2482                X_EMPTY_FULL_CONTEXT_DOT,
2483            ),
2484            GraphCase::merge(
2485                "test_$_x",
2486                "testgraphnodes-test-x-546922b23c",
2487                ContextSpec::Empty,
2488                ContextSpec::Chain(&[9]),
2489                true,
2490                EMPTY_WILDCARD_DOT,
2491            ),
2492            GraphCase::merge(
2493                "test_$_x_fullctx",
2494                "testgraphnodes-test-x-fullctx-7fdaaf473e",
2495                ContextSpec::Empty,
2496                ContextSpec::Chain(&[9]),
2497                false,
2498                X_EMPTY_FULL_CONTEXT_DOT,
2499            ),
2500            GraphCase::merge(
2501                "test_a_a",
2502                "testgraphnodes-test-a-a-429589e373",
2503                ContextSpec::Chain(&[1]),
2504                ContextSpec::Chain(&[1]),
2505                true,
2506                A_DOT,
2507            ),
2508            GraphCase::merge(
2509                "test_a$_ax",
2510                "testgraphnodes-test-a-ax-fd976a340d",
2511                ContextSpec::Chain(&[1]),
2512                ContextSpec::Chain(&[9, 1]),
2513                true,
2514                A_DOT,
2515            ),
2516            GraphCase::merge(
2517                "test_a$_ax_fullctx",
2518                "testgraphnodes-test-a-ax-fullctx-502155fcf9",
2519                ContextSpec::Chain(&[1]),
2520                ContextSpec::Chain(&[9, 1]),
2521                false,
2522                A_EMPTY_AX_FULL_CONTEXT_DOT,
2523            ),
2524            GraphCase::merge(
2525                "test_ax$_a$",
2526                "testgraphnodes-test-ax-a-62a48f251b",
2527                ContextSpec::Chain(&[9, 1]),
2528                ContextSpec::Chain(&[1]),
2529                true,
2530                A_DOT,
2531            ),
2532            GraphCase::nested_full_context(
2533                "test_aa$_a$_$_fullCtx",
2534                "testgraphnodes-test-aa-a-fullctx-8e728ea773",
2535                NESTED_FULL_CONTEXT_DOT,
2536            ),
2537            GraphCase::merge(
2538                "test_ax$_a$_fullctx",
2539                "testgraphnodes-test-ax-a-fullctx-7ef9c1d6b2",
2540                ContextSpec::Chain(&[9, 1]),
2541                ContextSpec::Chain(&[1]),
2542                false,
2543                A_EMPTY_AX_FULL_CONTEXT_DOT,
2544            ),
2545            GraphCase::merge(
2546                "test_a_b",
2547                "testgraphnodes-test-a-b-080058428f",
2548                ContextSpec::Chain(&[1]),
2549                ContextSpec::Chain(&[2]),
2550                true,
2551                A_B_DOT,
2552            ),
2553            GraphCase::merge(
2554                "test_ax_ax_same",
2555                "testgraphnodes-test-ax-ax-same-1504dc3dd3",
2556                ContextSpec::Chain(&[9, 1]),
2557                ContextSpec::Chain(&[9, 1]),
2558                true,
2559                AX_AX_DOT,
2560            ),
2561            GraphCase::merge(
2562                "test_ax_ax",
2563                "testgraphnodes-test-ax-ax-48f57578fa",
2564                ContextSpec::Chain(&[9, 1]),
2565                ContextSpec::Chain(&[9, 1]),
2566                true,
2567                AX_AX_DOT,
2568            ),
2569            GraphCase::merge(
2570                "test_abx_abx",
2571                "testgraphnodes-test-abx-abx-77366e32e9",
2572                ContextSpec::Chain(&[9, 2, 1]),
2573                ContextSpec::Chain(&[9, 2, 1]),
2574                true,
2575                ABX_ABX_DOT,
2576            ),
2577            GraphCase::merge(
2578                "test_abx_acx",
2579                "testgraphnodes-test-abx-acx-a3af7f90fa",
2580                ContextSpec::Chain(&[9, 2, 1]),
2581                ContextSpec::Chain(&[9, 3, 1]),
2582                true,
2583                ABX_ACX_DOT,
2584            ),
2585            GraphCase::merge(
2586                "test_ax_bx_same",
2587                "testgraphnodes-test-ax-bx-same-d0506bf7a9",
2588                ContextSpec::Chain(&[9, 1]),
2589                ContextSpec::Chain(&[9, 2]),
2590                true,
2591                AX_BX_DOT,
2592            ),
2593            GraphCase::merge(
2594                "test_ax_bx",
2595                "testgraphnodes-test-ax-bx-1ea2df9a04",
2596                ContextSpec::Chain(&[9, 1]),
2597                ContextSpec::Chain(&[9, 2]),
2598                true,
2599                AX_BX_DOT,
2600            ),
2601            GraphCase::merge(
2602                "test_ax_by",
2603                "testgraphnodes-test-ax-by-47815d59d2",
2604                ContextSpec::Chain(&[9, 1]),
2605                ContextSpec::Chain(&[10, 2]),
2606                true,
2607                AX_BY_DOT,
2608            ),
2609            GraphCase::merge(
2610                "test_a$_bx",
2611                "testgraphnodes-test-a-bx-b15f7b876f",
2612                ContextSpec::Chain(&[1]),
2613                ContextSpec::Chain(&[9, 2]),
2614                true,
2615                A_EMPTY_BX_DOT,
2616            ),
2617            GraphCase::merge(
2618                "test_a$_bx_fullctx",
2619                "testgraphnodes-test-a-bx-fullctx-a35242b6cf",
2620                ContextSpec::Chain(&[1]),
2621                ContextSpec::Chain(&[9, 2]),
2622                false,
2623                A_EMPTY_BX_FULL_CONTEXT_DOT,
2624            ),
2625            GraphCase::merge(
2626                "test_aex_bfx",
2627                "testgraphnodes-test-aex-bfx-07ad9de126",
2628                ContextSpec::Chain(&[9, 5, 1]),
2629                ContextSpec::Chain(&[9, 6, 2]),
2630                true,
2631                AEX_BFX_DOT,
2632            ),
2633            GraphCase::merge(
2634                "test_A$_A$_fullctx",
2635                "testgraphnodes-test-a-a-fullctx-b023f64b6c",
2636                ContextSpec::Array(&[&[]]),
2637                ContextSpec::Array(&[&[]]),
2638                false,
2639                EMPTY_FULL_CONTEXT_DOT,
2640            ),
2641            GraphCase::merge(
2642                "test_Aab_Ac",
2643                "testgraphnodes-test-aab-ac-139c5b709d",
2644                ContextSpec::Array(&[&[1], &[2]]),
2645                ContextSpec::Array(&[&[3]]),
2646                true,
2647                A_B_C_DOT,
2648            ),
2649            GraphCase::merge(
2650                "test_Aa_Aa",
2651                "testgraphnodes-test-aa-aa-0a175c83db",
2652                ContextSpec::Array(&[&[1]]),
2653                ContextSpec::Array(&[&[1]]),
2654                true,
2655                A_DOT,
2656            ),
2657            GraphCase::merge(
2658                "test_Aa_Abc",
2659                "testgraphnodes-test-aa-abc-db12d99894",
2660                ContextSpec::Array(&[&[1]]),
2661                ContextSpec::Array(&[&[2], &[3]]),
2662                true,
2663                A_B_C_DOT,
2664            ),
2665            GraphCase::merge(
2666                "test_Aac_Ab",
2667                "testgraphnodes-test-aac-ab-ef785e17e7",
2668                ContextSpec::Array(&[&[1], &[3]]),
2669                ContextSpec::Array(&[&[2]]),
2670                true,
2671                A_B_C_DOT,
2672            ),
2673            GraphCase::merge(
2674                "test_Aab_Aa",
2675                "testgraphnodes-test-aab-aa-d90d8d54f0",
2676                ContextSpec::Array(&[&[1], &[2]]),
2677                ContextSpec::Array(&[&[1]]),
2678                true,
2679                A_B_DOT,
2680            ),
2681            GraphCase::merge(
2682                "test_Aab_Ab",
2683                "testgraphnodes-test-aab-ab-e2d46352b4",
2684                ContextSpec::Array(&[&[1], &[2]]),
2685                ContextSpec::Array(&[&[2]]),
2686                true,
2687                A_B_DOT,
2688            ),
2689            GraphCase::merge(
2690                "test_Aax_Aby",
2691                "testgraphnodes-test-aax-aby-cccf935759",
2692                ContextSpec::Array(&[&[9, 1]]),
2693                ContextSpec::Array(&[&[10, 2]]),
2694                true,
2695                AX_BY_DOT,
2696            ),
2697            GraphCase::merge(
2698                "test_Aax_Aay",
2699                "testgraphnodes-test-aax-aay-c0f9b80842",
2700                ContextSpec::Array(&[&[9, 1]]),
2701                ContextSpec::Array(&[&[10, 1]]),
2702                true,
2703                AAX_AAY_DOT,
2704            ),
2705            GraphCase::merge(
2706                "test_Aaxc_Aayd",
2707                "testgraphnodes-test-aaxc-aayd-a73533f64d",
2708                ContextSpec::Array(&[&[9, 1], &[3]]),
2709                ContextSpec::Array(&[&[10, 1], &[4]]),
2710                true,
2711                AAXC_AAYD_DOT,
2712            ),
2713            GraphCase::merge(
2714                "test_Aaubv_Acwdx",
2715                "testgraphnodes-test-aaubv-acwdx-f479c849df",
2716                ContextSpec::Array(&[&[6, 1], &[7, 2]]),
2717                ContextSpec::Array(&[&[8, 3], &[9, 4]]),
2718                true,
2719                AAUBV_ACWDX_DOT,
2720            ),
2721            GraphCase::merge(
2722                "test_Aaubv_Abvdx",
2723                "testgraphnodes-test-aaubv-abvdx-01eb5714fe",
2724                ContextSpec::Array(&[&[6, 1], &[7, 2]]),
2725                ContextSpec::Array(&[&[7, 2], &[9, 4]]),
2726                true,
2727                AAUBV_ABVDX_DOT,
2728            ),
2729            GraphCase::merge(
2730                "test_Aaubv_Abwdx",
2731                "testgraphnodes-test-aaubv-abwdx-7953c9b489",
2732                ContextSpec::Array(&[&[6, 1], &[7, 2]]),
2733                ContextSpec::Array(&[&[8, 2], &[9, 4]]),
2734                true,
2735                AAUBV_ABWDX_DOT,
2736            ),
2737            GraphCase::merge(
2738                "test_Aaubv_Abvdu",
2739                "testgraphnodes-test-aaubv-abvdu-ecc8850384",
2740                ContextSpec::Array(&[&[6, 1], &[7, 2]]),
2741                ContextSpec::Array(&[&[7, 2], &[6, 4]]),
2742                true,
2743                AAUBV_ABVDU_DOT,
2744            ),
2745            GraphCase::merge(
2746                "test_Aaubu_Acudu",
2747                "testgraphnodes-test-aaubu-acudu-7cb798b616",
2748                ContextSpec::Array(&[&[6, 1], &[6, 2]]),
2749                ContextSpec::Array(&[&[6, 3], &[6, 4]]),
2750                true,
2751                AAUBU_ACUDU_DOT,
2752            ),
2753        ];
2754
2755        fn build_chain(arena: &mut ContextArena, return_states: &[usize]) -> ContextId {
2756            let mut context = EMPTY_CONTEXT;
2757            for &return_state in return_states {
2758                context = arena.singleton(context, return_state);
2759            }
2760            context
2761        }
2762
2763        fn build_context(arena: &mut ContextArena, spec: ContextSpec) -> ContextId {
2764            match spec {
2765                ContextSpec::Empty => EMPTY_CONTEXT,
2766                ContextSpec::Chain(return_states) => build_chain(arena, return_states),
2767                ContextSpec::Array(chains) => {
2768                    let mut entries = Vec::with_capacity(chains.len());
2769                    for return_states in chains {
2770                        let context = build_chain(arena, return_states);
2771                        entries.push(arena.first_entry(context));
2772                    }
2773                    arena.intern_entries(&entries)
2774                }
2775            }
2776        }
2777
2778        fn run_case(case: &GraphCase) -> String {
2779            let mut arena = ContextArena::new();
2780            let mut workspace = PredictionWorkspace::default();
2781            let merged = match case.scenario {
2782                Scenario::Merge { left, right } => {
2783                    let left = build_context(&mut arena, left);
2784                    let right = build_context(&mut arena, right);
2785                    arena.merge(left, right, case.root_is_wildcard, &mut workspace)
2786                }
2787                Scenario::NestedFullContext => {
2788                    let child = arena.singleton(EMPTY_CONTEXT, 8);
2789                    let right = arena.merge(EMPTY_CONTEXT, child, false, &mut workspace);
2790                    let left = arena.singleton(right, 8);
2791                    arena.merge(left, right, false, &mut workspace)
2792                }
2793            };
2794            render_dot(&arena, merged, case.root_is_wildcard)
2795        }
2796
2797        fn render_dot(arena: &ContextArena, context: ContextId, root_is_wildcard: bool) -> String {
2798            let mut nodes = String::new();
2799            let mut edges = String::new();
2800            let mut context_ids = HashMap::new();
2801            let mut work_list = VecDeque::new();
2802            context_ids.insert(context, 0);
2803            work_list.push_back(context);
2804
2805            while let Some(current) = work_list.pop_front() {
2806                let current_id = context_ids[&current];
2807                let len = arena.len(current);
2808                write!(&mut nodes, "  s{current_id}[").expect("write to string");
2809                if len > 1 {
2810                    nodes.push_str("shape=record, ");
2811                }
2812                nodes.push_str("label=\"");
2813                if arena.is_empty(current) {
2814                    nodes.push(if root_is_wildcard { '*' } else { '$' });
2815                } else if len > 1 {
2816                    for index in 0..len {
2817                        if index > 0 {
2818                            nodes.push('|');
2819                        }
2820                        write!(&mut nodes, "<p{index}>").expect("write to string");
2821                        if arena.return_state(current, index) == Some(EMPTY_RETURN_STATE) {
2822                            nodes.push(if root_is_wildcard { '*' } else { '$' });
2823                        }
2824                    }
2825                } else {
2826                    write!(&mut nodes, "{current_id}").expect("write to string");
2827                }
2828                nodes.push_str("\"];\n");
2829
2830                if arena.is_empty(current) {
2831                    continue;
2832                }
2833                for index in 0..len {
2834                    let return_state = arena
2835                        .return_state(current, index)
2836                        .expect("context entry in range");
2837                    if return_state == EMPTY_RETURN_STATE {
2838                        continue;
2839                    }
2840                    let parent = arena.parent(current, index).expect("non-empty parent");
2841                    let parent_id = if let Some(&parent_id) = context_ids.get(&parent) {
2842                        parent_id
2843                    } else {
2844                        let parent_id = context_ids.len();
2845                        context_ids.insert(parent, parent_id);
2846                        work_list.push_front(parent);
2847                        parent_id
2848                    };
2849
2850                    write!(&mut edges, "  s{current_id}").expect("write to string");
2851                    if len > 1 {
2852                        write!(&mut edges, ":p{index}").expect("write to string");
2853                    }
2854                    writeln!(&mut edges, "->s{parent_id}[label=\"{return_state}\"];")
2855                        .expect("write to string");
2856                }
2857            }
2858
2859            let mut dot = String::from("digraph G {\nrankdir=LR;\n");
2860            dot.push_str(&nodes);
2861            dot.push_str(&edges);
2862            dot.push_str("}\n");
2863            dot
2864        }
2865
2866        #[test]
2867        fn pinned_upstream_test_graph_nodes_matches_dot() {
2868            assert_eq!(CASES.len(), 38, "pinned Java source case inventory drifted");
2869            let source_tests = CASES
2870                .iter()
2871                .map(|case| case.source_test)
2872                .collect::<BTreeSet<_>>();
2873            assert_eq!(
2874                source_tests.len(),
2875                38,
2876                "pinned Java source test names must be unique"
2877            );
2878            let logical_ids = CASES
2879                .iter()
2880                .map(|case| case.logical_id)
2881                .collect::<BTreeSet<_>>();
2882            assert_eq!(
2883                logical_ids.len(),
2884                36,
2885                "pinned upstream logical row inventory drifted"
2886            );
2887
2888            let selector = std::env::var("ANTLR_GRAPH_NODE_CASE").ok();
2889            let selected = CASES
2890                .iter()
2891                .filter(|case| {
2892                    selector
2893                        .as_deref()
2894                        .is_none_or(|logical_id| case.logical_id == logical_id)
2895                })
2896                .collect::<Vec<_>>();
2897            assert!(
2898                !selected.is_empty(),
2899                "ANTLR_GRAPH_NODE_CASE={:?} matched no logical row",
2900                selector.as_deref().unwrap_or_default()
2901            );
2902
2903            let mut mismatches = Vec::new();
2904            for case in &selected {
2905                let actual = run_case(case);
2906                if actual != case.expected {
2907                    mismatches.push(format!(
2908                        "logical_id={}\nsource_test={}\n--- expected\n{}--- actual\n{}",
2909                        case.logical_id, case.source_test, case.expected, actual
2910                    ));
2911                }
2912            }
2913
2914            assert!(
2915                mismatches.is_empty(),
2916                "TestGraphNodes DOT mismatches ({}/{} source cases):\n\n{}",
2917                mismatches.len(),
2918                selected.len(),
2919                mismatches.join("\n")
2920            );
2921        }
2922    }
2923}