1use std::cmp::Ordering;
2use std::collections::{BTreeMap, BTreeSet, HashMap};
3use std::hash::{BuildHasherDefault, Hash, Hasher};
4use std::mem::size_of;
5
6pub const EMPTY_RETURN_STATE: usize = usize::MAX;
7const COMPACT_EMPTY_RETURN_STATE: u32 = u32::MAX;
8
9#[derive(Debug, Default)]
11pub struct PredictionFxHasher {
12 hash: u64,
13}
14
15const FX_ROT: u32 = 5;
16const FX_SEED: u64 = 0x51_7c_c1_b7_27_22_0a_95;
17
18impl Hasher for PredictionFxHasher {
19 #[inline]
20 fn write(&mut self, bytes: &[u8]) {
21 let mut bytes = bytes;
22 while bytes.len() >= 8 {
23 let (head, rest) = bytes.split_at(8);
24 let word = u64::from_le_bytes(head.try_into().expect("8-byte chunk"));
25 self.hash = (self.hash.rotate_left(FX_ROT) ^ word).wrapping_mul(FX_SEED);
26 bytes = rest;
27 }
28 for &byte in bytes {
29 self.hash = (self.hash.rotate_left(FX_ROT) ^ u64::from(byte)).wrapping_mul(FX_SEED);
30 }
31 }
32
33 #[inline]
34 fn write_u8(&mut self, value: u8) {
35 self.hash = (self.hash.rotate_left(FX_ROT) ^ u64::from(value)).wrapping_mul(FX_SEED);
36 }
37
38 #[inline]
39 fn write_u32(&mut self, value: u32) {
40 self.hash = (self.hash.rotate_left(FX_ROT) ^ u64::from(value)).wrapping_mul(FX_SEED);
41 }
42
43 #[inline]
44 fn write_u64(&mut self, value: u64) {
45 self.hash = (self.hash.rotate_left(FX_ROT) ^ value).wrapping_mul(FX_SEED);
46 }
47
48 #[inline]
49 fn write_usize(&mut self, value: usize) {
50 self.hash = (self.hash.rotate_left(FX_ROT) ^ value as u64).wrapping_mul(FX_SEED);
51 }
52
53 #[inline]
54 fn write_i32(&mut self, value: i32) {
55 self.write_u32(i32::cast_unsigned(value));
56 }
57
58 #[inline]
59 fn finish(&self) -> u64 {
60 self.hash
61 }
62}
63
64type FxHashMap<K, V> = HashMap<K, V, BuildHasherDefault<PredictionFxHasher>>;
65
66#[repr(transparent)]
68#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
69pub struct ContextId(u32);
70
71pub const EMPTY_CONTEXT: ContextId = ContextId(0);
72
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74enum ContextTag {
75 Empty,
76 Singleton,
77 Array,
78}
79
80#[derive(Clone, Copy, Debug)]
81struct ContextRecord {
82 tag: ContextTag,
83 cached_hash: u64,
84 parent_or_start: u32,
85 return_state_or_len: u32,
86}
87
88#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
90pub struct PredictionContextStats {
91 pub contexts_created: usize,
92 pub singleton_contexts: usize,
93 pub array_contexts: usize,
94 pub array_entries: usize,
95 pub interner_hits: usize,
96 pub pooled_bytes: usize,
97 pub retained_bytes: usize,
100 pub context_capacity: usize,
101 pub array_parent_capacity: usize,
102 pub array_return_state_capacity: usize,
103 pub interner_capacity: usize,
104 pub workspace_merge_cache_entries: usize,
105 pub workspace_merge_cache_capacity: usize,
106 pub workspace_entry_capacity: usize,
107 pub outer_context_cache_hits: usize,
108 pub outer_context_cache_misses: usize,
109}
110
111#[derive(Debug)]
113pub(crate) struct ContextArena {
114 records: Vec<ContextRecord>,
115 array_parents: Vec<ContextId>,
116 array_return_states: Vec<u32>,
117 interner_heads: FxHashMap<u64, ContextId>,
118 interner_next: Vec<Option<ContextId>>,
119 interner_hits: usize,
120 #[cfg(debug_assertions)]
121 generation: u64,
122}
123
124#[cfg(debug_assertions)]
125fn next_context_arena_generation() -> u64 {
126 use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
127
128 static NEXT_GENERATION: AtomicU64 = AtomicU64::new(1);
129 NEXT_GENERATION.fetch_add(1, AtomicOrdering::Relaxed)
130}
131
132impl ContextArena {
133 pub(crate) fn new() -> Self {
134 let empty = ContextRecord {
135 tag: ContextTag::Empty,
136 cached_hash: prediction_context_empty_hash(),
137 parent_or_start: 0,
138 return_state_or_len: 0,
139 };
140 let mut interner_heads = FxHashMap::default();
141 interner_heads.insert(empty.cached_hash, EMPTY_CONTEXT);
142 Self {
143 records: vec![empty],
144 array_parents: Vec::new(),
145 array_return_states: Vec::new(),
146 interner_heads,
147 interner_next: vec![None],
148 interner_hits: 0,
149 #[cfg(debug_assertions)]
150 generation: next_context_arena_generation(),
151 }
152 }
153
154 #[cfg(debug_assertions)]
155 pub(crate) const fn generation(&self) -> u64 {
156 self.generation
157 }
158
159 pub(crate) fn stats(&self) -> PredictionContextStats {
160 let mut singleton_contexts = 0;
161 let mut array_contexts = 0;
162 for record in &self.records {
163 match record.tag {
164 ContextTag::Empty => {}
165 ContextTag::Singleton => singleton_contexts += 1,
166 ContextTag::Array => array_contexts += 1,
167 }
168 }
169 PredictionContextStats {
170 contexts_created: self.records.len(),
171 singleton_contexts,
172 array_contexts,
173 array_entries: self.array_parents.len(),
174 interner_hits: self.interner_hits,
175 pooled_bytes: self.records.len() * size_of::<ContextRecord>()
176 + self.array_parents.len() * size_of::<ContextId>()
177 + self.array_return_states.len() * size_of::<u32>()
178 + self.interner_next.len() * size_of::<Option<ContextId>>(),
179 retained_bytes: self.records.capacity() * size_of::<ContextRecord>()
180 + self.array_parents.capacity() * size_of::<ContextId>()
181 + self.array_return_states.capacity() * size_of::<u32>()
182 + self.interner_heads.capacity() * size_of::<(u64, ContextId)>()
183 + self.interner_next.capacity() * size_of::<Option<ContextId>>(),
184 context_capacity: self.records.capacity(),
185 array_parent_capacity: self.array_parents.capacity(),
186 array_return_state_capacity: self.array_return_states.capacity(),
187 interner_capacity: self.interner_heads.capacity(),
188 workspace_merge_cache_entries: 0,
189 workspace_merge_cache_capacity: 0,
190 workspace_entry_capacity: 0,
191 outer_context_cache_hits: 0,
192 outer_context_cache_misses: 0,
193 }
194 }
195
196 pub(crate) fn singleton(&mut self, parent: ContextId, return_state: usize) -> ContextId {
197 self.assert_valid(parent);
198 if return_state == EMPTY_RETURN_STATE {
199 return EMPTY_CONTEXT;
200 }
201 #[cfg(feature = "perf-counters")]
202 crate::perf::record_context_cache_call();
203 let return_state =
204 u32::try_from(return_state).expect("prediction return state must fit in u32");
205 let cached_hash = prediction_context_singleton_hash(self.cached_hash(parent), return_state);
206 if let Some(existing) = self.find_interned(cached_hash, |record| {
207 record.tag == ContextTag::Singleton
208 && record.parent_or_start == parent.0
209 && record.return_state_or_len == return_state
210 }) {
211 self.interner_hits = self.interner_hits.saturating_add(1);
212 #[cfg(feature = "perf-counters")]
213 crate::perf::record_context_cache_hit();
214 return existing;
215 }
216 #[cfg(feature = "perf-counters")]
217 {
218 crate::perf::record_context_cache_miss();
219 crate::perf::record_context_cache_insert();
220 }
221 self.push_record(ContextRecord {
222 tag: ContextTag::Singleton,
223 cached_hash,
224 parent_or_start: parent.0,
225 return_state_or_len: return_state,
226 })
227 }
228
229 fn intern_entries(&mut self, entries: &[(ContextId, u32)]) -> ContextId {
230 match entries {
231 [] => EMPTY_CONTEXT,
232 [(parent, return_state)] => {
233 if *return_state == COMPACT_EMPTY_RETURN_STATE {
234 EMPTY_CONTEXT
235 } else {
236 self.singleton(
237 *parent,
238 usize::try_from(*return_state).expect("u32 return state fits in usize"),
239 )
240 }
241 }
242 _ => {
243 debug_assert!(entries
244 .windows(2)
245 .all(|pair| { compare_entries(pair[0], pair[1]) == Ordering::Less }));
246 #[cfg(feature = "perf-counters")]
247 crate::perf::record_context_cache_call();
248 let cached_hash = prediction_context_array_hash(self, entries);
249 if let Some(existing) = self.find_interned(cached_hash, |record| {
250 if record.tag != ContextTag::Array
251 || usize::try_from(record.return_state_or_len).ok() != Some(entries.len())
252 {
253 return false;
254 }
255 let start =
256 usize::try_from(record.parent_or_start).expect("u32 pool index fits usize");
257 let end = start + entries.len();
258 self.array_parents[start..end]
259 .iter()
260 .copied()
261 .zip(self.array_return_states[start..end].iter().copied())
262 .eq(entries.iter().copied())
263 }) {
264 self.interner_hits = self.interner_hits.saturating_add(1);
265 #[cfg(feature = "perf-counters")]
266 crate::perf::record_context_cache_hit();
267 return existing;
268 }
269 #[cfg(feature = "perf-counters")]
270 {
271 crate::perf::record_context_cache_miss();
272 crate::perf::record_context_cache_insert();
273 }
274 let start = u32::try_from(self.array_parents.len())
275 .expect("prediction-context parent pool must fit in u32");
276 let len = u32::try_from(entries.len())
277 .expect("prediction-context array length must fit in u32");
278 self.array_parents
279 .extend(entries.iter().map(|(parent, _)| *parent));
280 self.array_return_states
281 .extend(entries.iter().map(|(_, return_state)| *return_state));
282 self.push_record(ContextRecord {
283 tag: ContextTag::Array,
284 cached_hash,
285 parent_or_start: start,
286 return_state_or_len: len,
287 })
288 }
289 }
290 }
291
292 fn find_interned(
293 &self,
294 cached_hash: u64,
295 matches: impl Fn(&ContextRecord) -> bool,
296 ) -> Option<ContextId> {
297 let mut candidate = self.interner_heads.get(&cached_hash).copied();
298 while let Some(id) = candidate {
299 let index = usize::try_from(id.0).expect("u32 context ID fits in usize");
300 let record = &self.records[index];
301 if matches(record) {
302 return Some(id);
303 }
304 candidate = self.interner_next[index];
305 }
306 None
307 }
308
309 fn push_record(&mut self, record: ContextRecord) -> ContextId {
310 let id = ContextId(
311 u32::try_from(self.records.len()).expect("prediction-context arena must fit in u32"),
312 );
313 let previous = self.interner_heads.insert(record.cached_hash, id);
314 self.records.push(record);
315 self.interner_next.push(previous);
316 id
317 }
318
319 pub(crate) fn merge(
320 &mut self,
321 left: ContextId,
322 right: ContextId,
323 root_is_wildcard: bool,
324 workspace: &mut PredictionWorkspace,
325 ) -> ContextId {
326 self.assert_valid(left);
327 self.assert_valid(right);
328 #[cfg(feature = "perf-counters")]
329 crate::perf::record_context_merge_call();
330 if left == right {
331 #[cfg(feature = "perf-counters")]
332 crate::perf::record_context_merge_identical();
333 return left;
334 }
335 let key = MergeKey::new(left, right, root_is_wildcard);
336 if let Some(merged) = workspace.merge_cache.get(&key).copied() {
337 #[cfg(feature = "perf-counters")]
338 crate::perf::record_context_merge_cache_hit();
339 return merged;
340 }
341 #[cfg(feature = "perf-counters")]
342 {
343 crate::perf::record_context_merge_cache_miss();
344 crate::perf::record_context_merge_uncached();
345 }
346 let merged = if root_is_wildcard && (left == EMPTY_CONTEXT || right == EMPTY_CONTEXT) {
347 EMPTY_CONTEXT
348 } else {
349 self.merge_uncached(left, right, root_is_wildcard, workspace)
350 };
351 workspace.merge_cache.insert(key, merged);
352 merged
353 }
354
355 fn merge_uncached(
356 &mut self,
357 left: ContextId,
358 right: ContextId,
359 root_is_wildcard: bool,
360 workspace: &mut PredictionWorkspace,
361 ) -> ContextId {
362 match (self.tag(left), self.tag(right)) {
363 (ContextTag::Array, ContextTag::Array) => {
364 self.merge_arrays(left, right, root_is_wildcard, workspace)
365 }
366 (ContextTag::Array, _) => {
367 let entry = self.first_entry(right);
368 self.merge_array_with_entry(left, entry, root_is_wildcard, workspace)
369 }
370 (_, ContextTag::Array) => {
371 let entry = self.first_entry(left);
372 self.merge_array_with_entry(right, entry, root_is_wildcard, workspace)
373 }
374 _ => self.merge_two_entries(
375 self.first_entry(left),
376 self.first_entry(right),
377 root_is_wildcard,
378 workspace,
379 ),
380 }
381 }
382
383 fn merge_two_entries(
384 &mut self,
385 left: (ContextId, u32),
386 right: (ContextId, u32),
387 root_is_wildcard: bool,
388 workspace: &mut PredictionWorkspace,
389 ) -> ContextId {
390 if left.1 == right.1 {
391 let parent = if left.0 == right.0 {
392 left.0
393 } else {
394 self.merge(left.0, right.0, root_is_wildcard, workspace)
395 };
396 return self.intern_entries(&[(parent, left.1)]);
397 }
398
399 let start = workspace.entries.len();
400 if right.1 < left.1 {
401 workspace.entries.extend([right, left]);
402 } else {
403 workspace.entries.extend([left, right]);
404 }
405 self.intern_workspace_entries(workspace, start)
406 }
407
408 fn intern_workspace_entries(
409 &mut self,
410 workspace: &mut PredictionWorkspace,
411 start: usize,
412 ) -> ContextId {
413 let context = self.intern_entries(&workspace.entries[start..]);
414 workspace.entries.truncate(start);
415 context
416 }
417
418 fn merge_array_with_entry(
419 &mut self,
420 array: ContextId,
421 entry: (ContextId, u32),
422 root_is_wildcard: bool,
423 workspace: &mut PredictionWorkspace,
424 ) -> ContextId {
425 let array_len = self.len(array);
426 let mut insert_index = array_len;
427 for index in 0..array_len {
428 let current = self.entry(array, index).expect("array entry in range");
429 match entry.1.cmp(¤t.1) {
430 Ordering::Less => {
431 insert_index = index;
432 break;
433 }
434 Ordering::Equal => {
435 let parent = if entry.0 == current.0 {
436 current.0
437 } else {
438 self.merge(entry.0, current.0, root_is_wildcard, workspace)
439 };
440 if parent == current.0 {
441 return array;
442 }
443
444 let start = workspace.entries.len();
445 for entry_index in 0..array_len {
446 let array_entry = self
447 .entry(array, entry_index)
448 .expect("array entry in range");
449 workspace.entries.push(if entry_index == index {
450 (parent, current.1)
451 } else {
452 array_entry
453 });
454 }
455 return self.intern_workspace_entries(workspace, start);
456 }
457 Ordering::Greater => {}
458 }
459 }
460
461 let start = workspace.entries.len();
462 for index in 0..insert_index {
463 workspace
464 .entries
465 .push(self.entry(array, index).expect("array entry in range"));
466 }
467 workspace.entries.push(entry);
468 for index in insert_index..array_len {
469 workspace
470 .entries
471 .push(self.entry(array, index).expect("array entry in range"));
472 }
473 self.intern_workspace_entries(workspace, start)
474 }
475
476 fn merge_arrays(
477 &mut self,
478 left: ContextId,
479 right: ContextId,
480 root_is_wildcard: bool,
481 workspace: &mut PredictionWorkspace,
482 ) -> ContextId {
483 let start = workspace.entries.len();
484 let left_len = self.len(left);
485 let right_len = self.len(right);
486 let mut left_index = 0;
487 let mut right_index = 0;
488 while left_index < left_len && right_index < right_len {
489 let left_entry = self.entry(left, left_index).expect("array entry in range");
490 let right_entry = self
491 .entry(right, right_index)
492 .expect("array entry in range");
493 match left_entry.1.cmp(&right_entry.1) {
494 Ordering::Less => {
495 workspace.entries.push(left_entry);
496 left_index += 1;
497 }
498 Ordering::Greater => {
499 workspace.entries.push(right_entry);
500 right_index += 1;
501 }
502 Ordering::Equal => {
503 let parent = if left_entry.0 == right_entry.0 {
504 left_entry.0
505 } else {
506 self.merge(left_entry.0, right_entry.0, root_is_wildcard, workspace)
507 };
508 workspace.entries.push((parent, left_entry.1));
509 left_index += 1;
510 right_index += 1;
511 }
512 }
513 }
514 while left_index < left_len {
515 workspace
516 .entries
517 .push(self.entry(left, left_index).expect("array entry in range"));
518 left_index += 1;
519 }
520 while right_index < right_len {
521 workspace.entries.push(
522 self.entry(right, right_index)
523 .expect("array entry in range"),
524 );
525 right_index += 1;
526 }
527 self.intern_workspace_entries(workspace, start)
528 }
529
530 pub(crate) fn len(&self, context: ContextId) -> usize {
531 let record = self.record(context);
532 match record.tag {
533 ContextTag::Empty | ContextTag::Singleton => 1,
534 ContextTag::Array => usize::try_from(record.return_state_or_len)
535 .expect("u32 context length fits in usize"),
536 }
537 }
538
539 pub(crate) fn is_empty(&self, context: ContextId) -> bool {
540 self.assert_valid(context);
541 context == EMPTY_CONTEXT
542 }
543
544 pub(crate) fn has_empty_path(&self, context: ContextId) -> bool {
545 if context == EMPTY_CONTEXT {
546 return true;
547 }
548 let record = self.record(context);
549 match record.tag {
550 ContextTag::Empty => true,
551 ContextTag::Singleton => false,
552 ContextTag::Array => {
553 let len = usize::try_from(record.return_state_or_len)
554 .expect("u32 context length fits in usize");
555 let start = usize::try_from(record.parent_or_start)
556 .expect("u32 context pool index fits in usize");
557 self.array_return_states[start + len - 1] == COMPACT_EMPTY_RETURN_STATE
558 }
559 }
560 }
561
562 pub(crate) fn return_state(&self, context: ContextId, index: usize) -> Option<usize> {
563 let (_, return_state) = self.entry(context, index)?;
564 Some(expand_return_state(return_state))
565 }
566
567 pub(crate) fn parent(&self, context: ContextId, index: usize) -> Option<ContextId> {
568 if context == EMPTY_CONTEXT {
569 self.assert_valid(context);
570 return None;
571 }
572 self.entry(context, index).map(|(parent, _)| parent)
573 }
574
575 fn first_entry(&self, context: ContextId) -> (ContextId, u32) {
576 self.entry(context, 0)
577 .expect("empty and singleton contexts have one logical entry")
578 }
579
580 fn entry(&self, context: ContextId, index: usize) -> Option<(ContextId, u32)> {
581 let record = self.record(context);
582 match record.tag {
583 ContextTag::Empty if index == 0 => Some((EMPTY_CONTEXT, COMPACT_EMPTY_RETURN_STATE)),
584 ContextTag::Singleton if index == 0 => Some((
585 ContextId(record.parent_or_start),
586 record.return_state_or_len,
587 )),
588 ContextTag::Array => {
589 let len = usize::try_from(record.return_state_or_len).ok()?;
590 if index >= len {
591 return None;
592 }
593 let start = usize::try_from(record.parent_or_start).ok()?;
594 Some((
595 self.array_parents[start + index],
596 self.array_return_states[start + index],
597 ))
598 }
599 ContextTag::Empty | ContextTag::Singleton => None,
600 }
601 }
602
603 fn tag(&self, context: ContextId) -> ContextTag {
604 self.record(context).tag
605 }
606
607 fn cached_hash(&self, context: ContextId) -> u64 {
608 self.record(context).cached_hash
609 }
610
611 fn record(&self, context: ContextId) -> &ContextRecord {
612 self.assert_valid(context);
613 &self.records[usize::try_from(context.0).expect("u32 context ID fits in usize")]
614 }
615
616 pub(crate) fn assert_valid(&self, context: ContextId) {
617 assert!(
618 usize::try_from(context.0).is_ok_and(|index| index < self.records.len()),
619 "prediction ContextId does not belong to this store"
620 );
621 }
622
623 pub(crate) fn import_all(
624 &mut self,
625 source: &Self,
626 workspace: &mut PredictionWorkspace,
627 ) -> Vec<ContextId> {
628 workspace.entries.clear();
629 let mut remap = Vec::with_capacity(source.records.len());
630 remap.push(EMPTY_CONTEXT);
631 for source_index in 1..source.records.len() {
632 let source_id = ContextId(
633 u32::try_from(source_index).expect("source prediction-context ID fits in u32"),
634 );
635 let imported = match source.tag(source_id) {
636 ContextTag::Empty => EMPTY_CONTEXT,
637 ContextTag::Singleton => {
638 let (parent, return_state) = source.first_entry(source_id);
639 let parent_index =
640 usize::try_from(parent.0).expect("u32 context ID fits usize");
641 assert!(
642 parent_index < remap.len(),
643 "prediction contexts must reference earlier arena records"
644 );
645 self.singleton(remap[parent_index], expand_return_state(return_state))
646 }
647 ContextTag::Array => {
648 let start = workspace.entries.len();
649 for entry_index in 0..source.len(source_id) {
650 let (parent, return_state) = source
651 .entry(source_id, entry_index)
652 .expect("source array entry in range");
653 let parent_index =
654 usize::try_from(parent.0).expect("u32 context ID fits usize");
655 assert!(
656 parent_index < remap.len(),
657 "prediction contexts must reference earlier arena records"
658 );
659 workspace.entries.push((remap[parent_index], return_state));
660 }
661 workspace
662 .entries
663 .sort_unstable_by(|left, right| compare_entries(*left, *right));
664 workspace.entries.dedup();
665 self.intern_workspace_entries(workspace, start)
666 }
667 };
668 remap.push(imported);
669 }
670 remap
671 }
672}
673
674impl Default for ContextArena {
675 fn default() -> Self {
676 Self::new()
677 }
678}
679
680fn compare_entries(left: (ContextId, u32), right: (ContextId, u32)) -> Ordering {
681 left.1.cmp(&right.1)
682}
683
684fn expand_return_state(return_state: u32) -> usize {
685 if return_state == COMPACT_EMPTY_RETURN_STATE {
686 EMPTY_RETURN_STATE
687 } else {
688 usize::try_from(return_state).expect("u32 return state fits in usize")
689 }
690}
691
692fn prediction_context_empty_hash() -> u64 {
693 let mut hasher = PredictionFxHasher::default();
694 hasher.write_u8(0);
695 hasher.finish()
696}
697
698fn prediction_context_singleton_hash(parent_hash: u64, return_state: u32) -> u64 {
699 let mut hasher = PredictionFxHasher::default();
700 hasher.write_u8(1);
701 hasher.write_u64(parent_hash);
702 hasher.write_u32(return_state);
703 hasher.finish()
704}
705
706fn prediction_context_array_hash(arena: &ContextArena, entries: &[(ContextId, u32)]) -> u64 {
707 let mut hasher = PredictionFxHasher::default();
708 hasher.write_u8(2);
709 hasher.write_usize(entries.len());
710 for (parent, _) in entries {
711 hasher.write_u64(arena.cached_hash(*parent));
712 }
713 hasher.write_usize(entries.len());
714 for (_, return_state) in entries {
715 hasher.write_u32(*return_state);
716 }
717 hasher.finish()
718}
719
720#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
721struct MergeKey {
722 left: ContextId,
723 right: ContextId,
724 root_is_wildcard: bool,
725}
726
727impl MergeKey {
728 fn new(left: ContextId, right: ContextId, root_is_wildcard: bool) -> Self {
729 let (left, right) = if right < left {
730 (right, left)
731 } else {
732 (left, right)
733 };
734 Self {
735 left,
736 right,
737 root_is_wildcard,
738 }
739 }
740}
741
742const MAX_RETAINED_MERGE_CACHE_ENTRIES: usize = 65_536;
743const MAX_RETAINED_CONTEXT_ENTRIES: usize = 16_384;
744
745#[derive(Debug, Default)]
747pub(crate) struct PredictionWorkspace {
748 merge_cache: FxHashMap<MergeKey, ContextId>,
749 entries: Vec<(ContextId, u32)>,
750}
751
752impl PredictionWorkspace {
753 pub(crate) fn reset(&mut self) {
754 if self.merge_cache.capacity() > MAX_RETAINED_MERGE_CACHE_ENTRIES {
755 self.merge_cache = FxHashMap::default();
756 } else {
757 self.merge_cache.clear();
758 }
759 if self.entries.capacity() > MAX_RETAINED_CONTEXT_ENTRIES {
760 self.entries = Vec::new();
761 } else {
762 self.entries.clear();
763 }
764 }
765
766 pub(crate) fn merge_cache_capacity(&self) -> usize {
767 self.merge_cache.capacity()
768 }
769
770 pub(crate) fn merge_cache_len(&self) -> usize {
771 self.merge_cache.len()
772 }
773
774 pub(crate) const fn entry_capacity(&self) -> usize {
775 self.entries.capacity()
776 }
777
778 pub(crate) fn retained_bytes(&self) -> usize {
779 self.merge_cache.capacity() * size_of::<(MergeKey, ContextId)>()
780 + self.entries.capacity() * size_of::<(ContextId, u32)>()
781 }
782}
783
784#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
785pub enum SemanticContext {
786 None,
787 Predicate {
788 rule_index: usize,
789 pred_index: usize,
790 context_dependent: bool,
791 },
792 Precedence {
793 precedence: i32,
794 },
795 And(Vec<Self>),
796 Or(Vec<Self>),
797}
798
799impl SemanticContext {
800 pub const fn none() -> Self {
801 Self::None
802 }
803
804 pub fn and(left: Self, right: Self) -> Self {
805 combine_semantic_context(left, right, true)
806 }
807
808 pub fn or(left: Self, right: Self) -> Self {
809 combine_semantic_context(left, right, false)
810 }
811
812 pub const fn is_none(&self) -> bool {
813 matches!(self, Self::None)
814 }
815}
816
817fn combine_semantic_context(
818 left: SemanticContext,
819 right: SemanticContext,
820 and: bool,
821) -> SemanticContext {
822 if left == right {
823 return left;
824 }
825 if left.is_none() {
826 return right;
827 }
828 if right.is_none() {
829 return left;
830 }
831 let mut entries = Vec::new();
832 for context in [left, right] {
833 match (and, context) {
834 (true, SemanticContext::And(children)) | (false, SemanticContext::Or(children)) => {
835 entries.extend(children);
836 }
837 (_, other) => entries.push(other),
838 }
839 }
840 entries.sort();
841 entries.dedup();
842 if and {
843 SemanticContext::And(entries)
844 } else {
845 SemanticContext::Or(entries)
846 }
847}
848
849#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
850pub(crate) struct AtnConfig {
851 pub(crate) state: usize,
852 pub(crate) alt: usize,
853 pub(crate) context: ContextId,
854 pub(crate) semantic_context: SemanticContext,
855 pub(crate) reaches_into_outer_context: usize,
856 pub(crate) precedence_filter_suppressed: bool,
857 #[cfg(debug_assertions)]
858 context_generation: u64,
859}
860
861impl AtnConfig {
862 pub(crate) fn new(state: usize, alt: usize, context: ContextId, arena: &ContextArena) -> Self {
863 arena.assert_valid(context);
864 Self {
865 state,
866 alt,
867 context,
868 semantic_context: SemanticContext::None,
869 reaches_into_outer_context: 0,
870 precedence_filter_suppressed: false,
871 #[cfg(debug_assertions)]
872 context_generation: arena.generation(),
873 }
874 }
875
876 #[must_use]
877 #[cfg(test)]
878 pub(crate) fn with_semantic_context(mut self, semantic_context: SemanticContext) -> Self {
879 self.semantic_context = semantic_context;
880 self
881 }
882
883 pub(crate) fn set_context(&mut self, context: ContextId, arena: &ContextArena) {
884 arena.assert_valid(context);
885 self.context = context;
886 #[cfg(debug_assertions)]
887 {
888 self.context_generation = arena.generation();
889 }
890 }
891
892 pub(crate) fn moved_to(&self, state: usize, context: ContextId, arena: &ContextArena) -> Self {
893 let mut moved = Self::new(state, self.alt, context, arena);
894 moved.semantic_context = self.semantic_context.clone();
895 moved.reaches_into_outer_context = self.reaches_into_outer_context;
896 moved.precedence_filter_suppressed = self.precedence_filter_suppressed;
897 moved
898 }
899
900 pub(crate) fn assert_store(&self, arena: &ContextArena) {
901 arena.assert_valid(self.context);
902 #[cfg(debug_assertions)]
903 assert_eq!(
904 self.context_generation,
905 arena.generation(),
906 "ATN config carries a ContextId from another prediction store"
907 );
908 }
909}
910
911#[derive(Clone, Debug, Default)]
912pub(crate) struct AtnConfigSet {
913 configs: Vec<AtnConfig>,
914 config_index: FxHashMap<AtnConfigKey, usize>,
915 full_context: bool,
916 unique_alt: Option<usize>,
917 conflicting_alts: BTreeSet<usize>,
918 has_semantic_context: bool,
919 dips_into_outer_context: bool,
920 readonly: bool,
921}
922
923impl AtnConfigSet {
924 pub(crate) fn new() -> Self {
925 Self::default()
926 }
927
928 pub(crate) fn new_full_context(full_context: bool) -> Self {
929 Self {
930 configs: Vec::new(),
931 config_index: FxHashMap::default(),
932 full_context,
933 unique_alt: None,
934 conflicting_alts: BTreeSet::new(),
935 has_semantic_context: false,
936 dips_into_outer_context: false,
937 readonly: false,
938 }
939 }
940
941 pub(crate) fn add(
943 &mut self,
944 config: AtnConfig,
945 arena: &mut ContextArena,
946 workspace: &mut PredictionWorkspace,
947 ) -> bool {
948 assert!(!self.readonly, "cannot mutate readonly ATN config set");
949 config.assert_store(arena);
950 #[cfg(feature = "perf-counters")]
951 crate::perf::record_config_add_call();
952 if !config.semantic_context.is_none() {
953 self.has_semantic_context = true;
954 }
955 if config.reaches_into_outer_context > 0 {
956 self.dips_into_outer_context = true;
957 }
958 let key = AtnConfigKey::from(&config);
959 if let Some(existing_index) = self.config_index.get(&key).copied() {
960 #[cfg(feature = "perf-counters")]
961 crate::perf::record_config_merge();
962 let existing = &mut self.configs[existing_index];
963 existing.assert_store(arena);
964 existing.context = arena.merge(
965 existing.context,
966 config.context,
967 !self.full_context,
968 workspace,
969 );
970 existing.reaches_into_outer_context = existing
971 .reaches_into_outer_context
972 .max(config.reaches_into_outer_context);
973 existing.precedence_filter_suppressed |= config.precedence_filter_suppressed;
974 self.conflicting_alts.clear();
975 false
976 } else {
977 let index = self.configs.len();
978 self.config_index.insert(key, index);
979 self.configs.push(config);
980 #[cfg(feature = "perf-counters")]
981 crate::perf::record_config_insert(self.configs.len());
982 self.unique_alt = None;
983 self.conflicting_alts.clear();
984 true
985 }
986 }
987
988 pub(crate) fn configs(&self) -> &[AtnConfig] {
989 &self.configs
990 }
991
992 pub(crate) fn into_configs(self) -> Vec<AtnConfig> {
993 self.configs
994 }
995
996 pub(crate) const fn is_empty(&self) -> bool {
997 self.configs.is_empty()
998 }
999
1000 pub(crate) const fn len(&self) -> usize {
1001 self.configs.len()
1002 }
1003
1004 pub(crate) fn set_readonly(&mut self, readonly: bool) {
1005 self.readonly = readonly;
1006 if readonly {
1007 self.config_index = FxHashMap::default();
1008 self.conflicting_alts.clear();
1009 }
1010 }
1011
1012 pub(crate) const fn full_context(&self) -> bool {
1013 self.full_context
1014 }
1015
1016 pub(crate) const fn has_semantic_context(&self) -> bool {
1017 self.has_semantic_context
1018 }
1019
1020 pub(crate) fn unique_alt(&mut self) -> Option<usize> {
1021 if self.unique_alt.is_none() {
1022 self.unique_alt = unique_alt(self.configs());
1023 }
1024 self.unique_alt
1025 }
1026
1027 pub(crate) fn alts(&self) -> BTreeSet<usize> {
1028 self.configs.iter().map(|config| config.alt).collect()
1029 }
1030
1031 pub(crate) fn conflicting_alt_subsets(&self) -> Vec<BTreeSet<usize>> {
1032 conflicting_alt_subsets(self.configs())
1033 }
1034
1035 pub(crate) fn conflicting_alts(&mut self) -> BTreeSet<usize> {
1036 if self.conflicting_alts.is_empty() {
1037 self.conflicting_alts = self
1038 .conflicting_alt_subsets()
1039 .into_iter()
1040 .filter(|alts| alts.len() > 1)
1041 .flatten()
1042 .collect();
1043 }
1044 self.conflicting_alts.clone()
1045 }
1046
1047 pub(crate) fn remap_contexts(&mut self, remap: &[ContextId], arena: &ContextArena) {
1048 for config in &mut self.configs {
1049 let index = usize::try_from(config.context.0).expect("u32 context ID fits usize");
1050 config.set_context(
1051 *remap
1052 .get(index)
1053 .expect("every imported context ID has a remap"),
1054 arena,
1055 );
1056 }
1057 self.config_index.clear();
1058 if !self.readonly {
1059 for (index, config) in self.configs.iter().enumerate() {
1060 self.config_index.insert(AtnConfigKey::from(config), index);
1061 }
1062 }
1063 }
1064
1065 pub(crate) fn fingerprint(&self) -> u64 {
1066 let mut hasher = PredictionFxHasher::default();
1067 self.configs.hash(&mut hasher);
1068 self.full_context.hash(&mut hasher);
1069 self.has_semantic_context.hash(&mut hasher);
1070 self.dips_into_outer_context.hash(&mut hasher);
1071 hasher.finish()
1072 }
1073
1074 pub(crate) fn retained_bytes(&self) -> usize {
1075 self.configs.capacity() * size_of::<AtnConfig>()
1076 + self.config_index.capacity() * size_of::<(AtnConfigKey, usize)>()
1077 }
1078}
1079
1080impl PartialEq for AtnConfigSet {
1081 fn eq(&self, other: &Self) -> bool {
1082 self.configs == other.configs
1083 && self.full_context == other.full_context
1084 && self.has_semantic_context == other.has_semantic_context
1085 && self.dips_into_outer_context == other.dips_into_outer_context
1086 }
1087}
1088
1089impl Eq for AtnConfigSet {}
1090
1091impl Ord for AtnConfigSet {
1092 fn cmp(&self, other: &Self) -> Ordering {
1093 self.configs
1094 .cmp(&other.configs)
1095 .then_with(|| self.full_context.cmp(&other.full_context))
1096 .then_with(|| self.has_semantic_context.cmp(&other.has_semantic_context))
1097 .then_with(|| {
1098 self.dips_into_outer_context
1099 .cmp(&other.dips_into_outer_context)
1100 })
1101 }
1102}
1103
1104impl PartialOrd for AtnConfigSet {
1105 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1106 Some(self.cmp(other))
1107 }
1108}
1109
1110#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
1111struct AtnConfigKey {
1112 state: usize,
1113 alt: usize,
1114 semantic_context: SemanticContext,
1115}
1116
1117impl From<&AtnConfig> for AtnConfigKey {
1118 fn from(config: &AtnConfig) -> Self {
1119 Self {
1120 state: config.state,
1121 alt: config.alt,
1122 semantic_context: config.semantic_context.clone(),
1123 }
1124 }
1125}
1126
1127pub(crate) fn unique_alt(configs: &[AtnConfig]) -> Option<usize> {
1128 let mut alt = None;
1129 for config in configs {
1130 match alt {
1131 None => alt = Some(config.alt),
1132 Some(existing) if existing == config.alt => {}
1133 Some(_) => return None,
1134 }
1135 }
1136 alt
1137}
1138
1139pub(crate) fn conflicting_alt_subsets(configs: &[AtnConfig]) -> Vec<BTreeSet<usize>> {
1140 let mut by_state_context = FxHashMap::<(usize, ContextId), BTreeSet<usize>>::default();
1141 for config in configs {
1142 by_state_context
1143 .entry((config.state, config.context))
1144 .or_default()
1145 .insert(config.alt);
1146 }
1147 by_state_context.into_values().collect()
1148}
1149
1150pub(crate) fn all_subsets_conflict(alt_subsets: &[BTreeSet<usize>]) -> bool {
1151 alt_subsets.iter().all(|alts| alts.len() > 1)
1152}
1153
1154pub(crate) fn all_subsets_equal(alt_subsets: &[BTreeSet<usize>]) -> bool {
1155 let mut subsets = alt_subsets.iter();
1156 let Some(first) = subsets.next() else {
1157 return true;
1158 };
1159 subsets.all(|alts| alts == first)
1160}
1161
1162pub(crate) fn single_viable_alt(alt_subsets: &[BTreeSet<usize>]) -> Option<usize> {
1163 let mut result = None;
1164 for alts in alt_subsets {
1165 let min_alt = alts.iter().next().copied()?;
1166 match result {
1167 None => result = Some(min_alt),
1168 Some(existing) if existing == min_alt => {}
1169 Some(_) => return None,
1170 }
1171 }
1172 result
1173}
1174
1175pub(crate) fn has_sll_conflict_terminating_prediction(
1176 configs: &AtnConfigSet,
1177 is_rule_stop_state: impl Fn(usize) -> bool,
1178) -> bool {
1179 if configs
1180 .configs()
1181 .iter()
1182 .all(|config| is_rule_stop_state(config.state))
1183 {
1184 return true;
1185 }
1186 let alt_subsets = configs.conflicting_alt_subsets();
1187 alt_subsets.iter().any(|alts| alts.len() > 1)
1188 && !has_state_associated_with_one_alt(configs.configs())
1189}
1190
1191fn has_state_associated_with_one_alt(configs: &[AtnConfig]) -> bool {
1192 let mut by_state = BTreeMap::<usize, BTreeSet<usize>>::new();
1193 for config in configs {
1194 by_state.entry(config.state).or_default().insert(config.alt);
1195 }
1196 by_state.values().any(|alts| alts.len() == 1)
1197}
1198
1199#[cfg(test)]
1200mod tests {
1201 use super::*;
1202
1203 #[test]
1204 fn arena_interns_singletons_without_per_context_objects() {
1205 let mut arena = ContextArena::new();
1206 let first = arena.singleton(EMPTY_CONTEXT, 7);
1207 let second = arena.singleton(EMPTY_CONTEXT, 7);
1208
1209 assert_eq!(first, second);
1210 assert_eq!(arena.stats().singleton_contexts, 1);
1211 assert_eq!(arena.stats().interner_hits, 1);
1212 }
1213
1214 #[test]
1215 fn array_interner_verifies_payload_after_hash_collision() {
1216 let mut arena = ContextArena::new();
1217 let first_parent = arena.singleton(EMPTY_CONTEXT, 1);
1218 let second_parent = arena.singleton(EMPTY_CONTEXT, 2);
1219 let expected = [(first_parent, 10), (second_parent, 20)];
1220 let colliding = [(second_parent, 10), (first_parent, 20)];
1221 let cached_hash = prediction_context_array_hash(&arena, &expected);
1222 let start = u32::try_from(arena.array_parents.len()).expect("pool index fits u32");
1223 arena
1224 .array_parents
1225 .extend(colliding.iter().map(|(parent, _)| *parent));
1226 arena
1227 .array_return_states
1228 .extend(colliding.iter().map(|(_, return_state)| *return_state));
1229 let collision = arena.push_record(ContextRecord {
1230 tag: ContextTag::Array,
1231 cached_hash,
1232 parent_or_start: start,
1233 return_state_or_len: 2,
1234 });
1235
1236 let interned = arena.intern_entries(&expected);
1237
1238 assert_ne!(interned, collision);
1239 assert_eq!(arena.entry(interned, 0), Some(expected[0]));
1240 assert_eq!(arena.entry(interned, 1), Some(expected[1]));
1241 }
1242
1243 #[test]
1244 fn merge_with_empty_preserves_full_context_empty_path() {
1245 let mut arena = ContextArena::new();
1246 let mut workspace = PredictionWorkspace::default();
1247 let singleton = arena.singleton(EMPTY_CONTEXT, 42);
1248
1249 let merged = arena.merge(singleton, EMPTY_CONTEXT, false, &mut workspace);
1250
1251 assert_eq!(arena.len(merged), 2);
1252 assert_eq!(arena.return_state(merged, 0), Some(42));
1253 assert_eq!(arena.parent(merged, 0), Some(EMPTY_CONTEXT));
1254 assert_eq!(arena.return_state(merged, 1), Some(EMPTY_RETURN_STATE));
1255 assert!(arena.has_empty_path(merged));
1256 }
1257
1258 #[test]
1259 fn wildcard_merge_collapses_to_empty() {
1260 let mut arena = ContextArena::new();
1261 let mut workspace = PredictionWorkspace::default();
1262 let singleton = arena.singleton(EMPTY_CONTEXT, 42);
1263
1264 assert_eq!(
1265 arena.merge(singleton, EMPTY_CONTEXT, true, &mut workspace),
1266 EMPTY_CONTEXT
1267 );
1268 }
1269
1270 #[test]
1271 fn merge_is_order_independent() {
1272 let mut arena = ContextArena::new();
1273 let mut workspace = PredictionWorkspace::default();
1274 let left_parent = arena.singleton(EMPTY_CONTEXT, 100);
1275 let right_parent = arena.singleton(EMPTY_CONTEXT, 200);
1276 let left = arena.singleton(left_parent, 7);
1277 let right = arena.singleton(right_parent, 7);
1278
1279 let left_right = arena.merge(left, right, false, &mut workspace);
1280 workspace.reset();
1281 let right_left = arena.merge(right, left, false, &mut workspace);
1282
1283 assert_eq!(left_right, right_left);
1284 assert_eq!(arena.len(left_right), 1);
1285 let merged_parent = arena.parent(left_right, 0).expect("merged parent");
1286 assert_eq!(arena.len(merged_parent), 2);
1287 assert_eq!(arena.return_state(merged_parent, 0), Some(100));
1288 assert_eq!(arena.return_state(merged_parent, 1), Some(200));
1289 }
1290
1291 #[test]
1292 fn import_remaps_contexts_into_destination_arena() {
1293 let mut source = ContextArena::new();
1294 let parent = source.singleton(EMPTY_CONTEXT, 3);
1295 let child = source.singleton(parent, 9);
1296 let mut destination = ContextArena::new();
1297 let mut workspace = PredictionWorkspace::default();
1298
1299 let remap = destination.import_all(&source, &mut workspace);
1300 let imported = remap[usize::try_from(child.0).expect("context ID fits usize")];
1301
1302 assert_eq!(destination.return_state(imported, 0), Some(9));
1303 let imported_parent = destination.parent(imported, 0).expect("parent");
1304 assert_eq!(destination.return_state(imported_parent, 0), Some(3));
1305 }
1306
1307 #[test]
1308 fn config_set_merges_context_ids() {
1309 let mut arena = ContextArena::new();
1310 let mut workspace = PredictionWorkspace::default();
1311 let left = arena.singleton(EMPTY_CONTEXT, 1);
1312 let right = arena.singleton(EMPTY_CONTEXT, 2);
1313 let mut set = AtnConfigSet::new_full_context(true);
1314
1315 assert!(set.add(
1316 AtnConfig::new(1, 1, left, &arena),
1317 &mut arena,
1318 &mut workspace
1319 ));
1320 assert!(!set.add(
1321 AtnConfig::new(1, 1, right, &arena),
1322 &mut arena,
1323 &mut workspace
1324 ));
1325 assert_eq!(set.len(), 1);
1326 assert_eq!(arena.len(set.configs()[0].context), 2);
1327 }
1328
1329 #[test]
1330 fn workspace_drops_pathological_capacity() {
1331 let mut workspace = PredictionWorkspace::default();
1332 workspace
1333 .merge_cache
1334 .reserve(MAX_RETAINED_MERGE_CACHE_ENTRIES.saturating_mul(2));
1335 workspace
1336 .entries
1337 .reserve(MAX_RETAINED_CONTEXT_ENTRIES.saturating_mul(2));
1338 workspace.reset();
1339
1340 assert!(workspace.merge_cache.capacity() <= MAX_RETAINED_MERGE_CACHE_ENTRIES);
1341 assert!(workspace.entries.capacity() <= MAX_RETAINED_CONTEXT_ENTRIES);
1342 }
1343
1344 mod upstream_graph_nodes {
1345 use super::*;
1346 use std::collections::{BTreeSet, HashMap, VecDeque};
1347 use std::fmt::Write;
1348
1349 const EMPTY_WILDCARD_DOT: &str = concat!(
1350 "digraph G {\n",
1351 "rankdir=LR;\n",
1352 " s0[label=\"*\"];\n",
1353 "}\n",
1354 );
1355 const EMPTY_FULL_CONTEXT_DOT: &str = concat!(
1356 "digraph G {\n",
1357 "rankdir=LR;\n",
1358 " s0[label=\"$\"];\n",
1359 "}\n",
1360 );
1361 const X_EMPTY_FULL_CONTEXT_DOT: &str = concat!(
1362 "digraph G {\n",
1363 "rankdir=LR;\n",
1364 " s0[shape=record, label=\"<p0>|<p1>$\"];\n",
1365 " s1[label=\"$\"];\n",
1366 " s0:p0->s1[label=\"9\"];\n",
1367 "}\n",
1368 );
1369 const A_DOT: &str = concat!(
1370 "digraph G {\n",
1371 "rankdir=LR;\n",
1372 " s0[label=\"0\"];\n",
1373 " s1[label=\"*\"];\n",
1374 " s0->s1[label=\"1\"];\n",
1375 "}\n",
1376 );
1377 const A_EMPTY_AX_FULL_CONTEXT_DOT: &str = concat!(
1378 "digraph G {\n",
1379 "rankdir=LR;\n",
1380 " s0[label=\"0\"];\n",
1381 " s1[shape=record, label=\"<p0>|<p1>$\"];\n",
1382 " s2[label=\"$\"];\n",
1383 " s0->s1[label=\"1\"];\n",
1384 " s1:p0->s2[label=\"9\"];\n",
1385 "}\n",
1386 );
1387 const NESTED_FULL_CONTEXT_DOT: &str = concat!(
1388 "digraph G {\n",
1389 "rankdir=LR;\n",
1390 " s0[shape=record, label=\"<p0>|<p1>$\"];\n",
1391 " s1[shape=record, label=\"<p0>|<p1>$\"];\n",
1392 " s2[label=\"$\"];\n",
1393 " s0:p0->s1[label=\"8\"];\n",
1394 " s1:p0->s2[label=\"8\"];\n",
1395 "}\n",
1396 );
1397 const A_B_DOT: &str = concat!(
1398 "digraph G {\n",
1399 "rankdir=LR;\n",
1400 " s0[shape=record, label=\"<p0>|<p1>\"];\n",
1401 " s1[label=\"*\"];\n",
1402 " s0:p0->s1[label=\"1\"];\n",
1403 " s0:p1->s1[label=\"2\"];\n",
1404 "}\n",
1405 );
1406 const AX_AX_DOT: &str = concat!(
1407 "digraph G {\n",
1408 "rankdir=LR;\n",
1409 " s0[label=\"0\"];\n",
1410 " s1[label=\"1\"];\n",
1411 " s2[label=\"*\"];\n",
1412 " s0->s1[label=\"1\"];\n",
1413 " s1->s2[label=\"9\"];\n",
1414 "}\n",
1415 );
1416 const ABX_ABX_DOT: &str = concat!(
1417 "digraph G {\n",
1418 "rankdir=LR;\n",
1419 " s0[label=\"0\"];\n",
1420 " s1[label=\"1\"];\n",
1421 " s2[label=\"2\"];\n",
1422 " s3[label=\"*\"];\n",
1423 " s0->s1[label=\"1\"];\n",
1424 " s1->s2[label=\"2\"];\n",
1425 " s2->s3[label=\"9\"];\n",
1426 "}\n",
1427 );
1428 const ABX_ACX_DOT: &str = concat!(
1429 "digraph G {\n",
1430 "rankdir=LR;\n",
1431 " s0[label=\"0\"];\n",
1432 " s1[shape=record, label=\"<p0>|<p1>\"];\n",
1433 " s2[label=\"2\"];\n",
1434 " s3[label=\"*\"];\n",
1435 " s0->s1[label=\"1\"];\n",
1436 " s1:p0->s2[label=\"2\"];\n",
1437 " s1:p1->s2[label=\"3\"];\n",
1438 " s2->s3[label=\"9\"];\n",
1439 "}\n",
1440 );
1441 const AX_BX_DOT: &str = concat!(
1442 "digraph G {\n",
1443 "rankdir=LR;\n",
1444 " s0[shape=record, label=\"<p0>|<p1>\"];\n",
1445 " s1[label=\"1\"];\n",
1446 " s2[label=\"*\"];\n",
1447 " s0:p0->s1[label=\"1\"];\n",
1448 " s0:p1->s1[label=\"2\"];\n",
1449 " s1->s2[label=\"9\"];\n",
1450 "}\n",
1451 );
1452 const AX_BY_DOT: &str = concat!(
1453 "digraph G {\n",
1454 "rankdir=LR;\n",
1455 " s0[shape=record, label=\"<p0>|<p1>\"];\n",
1456 " s2[label=\"2\"];\n",
1457 " s3[label=\"*\"];\n",
1458 " s1[label=\"1\"];\n",
1459 " s0:p0->s1[label=\"1\"];\n",
1460 " s0:p1->s2[label=\"2\"];\n",
1461 " s2->s3[label=\"10\"];\n",
1462 " s1->s3[label=\"9\"];\n",
1463 "}\n",
1464 );
1465 const A_EMPTY_BX_DOT: &str = concat!(
1466 "digraph G {\n",
1467 "rankdir=LR;\n",
1468 " s0[shape=record, label=\"<p0>|<p1>\"];\n",
1469 " s2[label=\"2\"];\n",
1470 " s1[label=\"*\"];\n",
1471 " s0:p0->s1[label=\"1\"];\n",
1472 " s0:p1->s2[label=\"2\"];\n",
1473 " s2->s1[label=\"9\"];\n",
1474 "}\n",
1475 );
1476 const A_EMPTY_BX_FULL_CONTEXT_DOT: &str = concat!(
1477 "digraph G {\n",
1478 "rankdir=LR;\n",
1479 " s0[shape=record, label=\"<p0>|<p1>\"];\n",
1480 " s2[label=\"2\"];\n",
1481 " s1[label=\"$\"];\n",
1482 " s0:p0->s1[label=\"1\"];\n",
1483 " s0:p1->s2[label=\"2\"];\n",
1484 " s2->s1[label=\"9\"];\n",
1485 "}\n",
1486 );
1487 const AEX_BFX_DOT: &str = concat!(
1488 "digraph G {\n",
1489 "rankdir=LR;\n",
1490 " s0[shape=record, label=\"<p0>|<p1>\"];\n",
1491 " s2[label=\"2\"];\n",
1492 " s3[label=\"3\"];\n",
1493 " s4[label=\"*\"];\n",
1494 " s1[label=\"1\"];\n",
1495 " s0:p0->s1[label=\"1\"];\n",
1496 " s0:p1->s2[label=\"2\"];\n",
1497 " s2->s3[label=\"6\"];\n",
1498 " s3->s4[label=\"9\"];\n",
1499 " s1->s3[label=\"5\"];\n",
1500 "}\n",
1501 );
1502 const A_B_C_DOT: &str = concat!(
1503 "digraph G {\n",
1504 "rankdir=LR;\n",
1505 " s0[shape=record, label=\"<p0>|<p1>|<p2>\"];\n",
1506 " s1[label=\"*\"];\n",
1507 " s0:p0->s1[label=\"1\"];\n",
1508 " s0:p1->s1[label=\"2\"];\n",
1509 " s0:p2->s1[label=\"3\"];\n",
1510 "}\n",
1511 );
1512 const AAX_AAY_DOT: &str = concat!(
1513 "digraph G {\n",
1514 "rankdir=LR;\n",
1515 " s0[label=\"0\"];\n",
1516 " s1[shape=record, label=\"<p0>|<p1>\"];\n",
1517 " s2[label=\"*\"];\n",
1518 " s0->s1[label=\"1\"];\n",
1519 " s1:p0->s2[label=\"9\"];\n",
1520 " s1:p1->s2[label=\"10\"];\n",
1521 "}\n",
1522 );
1523 const AAXC_AAYD_DOT: &str = concat!(
1524 "digraph G {\n",
1525 "rankdir=LR;\n",
1526 " s0[shape=record, label=\"<p0>|<p1>|<p2>\"];\n",
1527 " s2[label=\"*\"];\n",
1528 " s1[shape=record, label=\"<p0>|<p1>\"];\n",
1529 " s0:p0->s1[label=\"1\"];\n",
1530 " s0:p1->s2[label=\"3\"];\n",
1531 " s0:p2->s2[label=\"4\"];\n",
1532 " s1:p0->s2[label=\"9\"];\n",
1533 " s1:p1->s2[label=\"10\"];\n",
1534 "}\n",
1535 );
1536 const AAUBV_ACWDX_DOT: &str = concat!(
1537 "digraph G {\n",
1538 "rankdir=LR;\n",
1539 " s0[shape=record, label=\"<p0>|<p1>|<p2>|<p3>\"];\n",
1540 " s4[label=\"4\"];\n",
1541 " s5[label=\"*\"];\n",
1542 " s3[label=\"3\"];\n",
1543 " s2[label=\"2\"];\n",
1544 " s1[label=\"1\"];\n",
1545 " s0:p0->s1[label=\"1\"];\n",
1546 " s0:p1->s2[label=\"2\"];\n",
1547 " s0:p2->s3[label=\"3\"];\n",
1548 " s0:p3->s4[label=\"4\"];\n",
1549 " s4->s5[label=\"9\"];\n",
1550 " s3->s5[label=\"8\"];\n",
1551 " s2->s5[label=\"7\"];\n",
1552 " s1->s5[label=\"6\"];\n",
1553 "}\n",
1554 );
1555 const AAUBV_ABVDX_DOT: &str = concat!(
1556 "digraph G {\n",
1557 "rankdir=LR;\n",
1558 " s0[shape=record, label=\"<p0>|<p1>|<p2>\"];\n",
1559 " s3[label=\"3\"];\n",
1560 " s4[label=\"*\"];\n",
1561 " s2[label=\"2\"];\n",
1562 " s1[label=\"1\"];\n",
1563 " s0:p0->s1[label=\"1\"];\n",
1564 " s0:p1->s2[label=\"2\"];\n",
1565 " s0:p2->s3[label=\"4\"];\n",
1566 " s3->s4[label=\"9\"];\n",
1567 " s2->s4[label=\"7\"];\n",
1568 " s1->s4[label=\"6\"];\n",
1569 "}\n",
1570 );
1571 const AAUBV_ABWDX_DOT: &str = concat!(
1572 "digraph G {\n",
1573 "rankdir=LR;\n",
1574 " s0[shape=record, label=\"<p0>|<p1>|<p2>\"];\n",
1575 " s3[label=\"3\"];\n",
1576 " s4[label=\"*\"];\n",
1577 " s2[shape=record, label=\"<p0>|<p1>\"];\n",
1578 " s1[label=\"1\"];\n",
1579 " s0:p0->s1[label=\"1\"];\n",
1580 " s0:p1->s2[label=\"2\"];\n",
1581 " s0:p2->s3[label=\"4\"];\n",
1582 " s3->s4[label=\"9\"];\n",
1583 " s2:p0->s4[label=\"7\"];\n",
1584 " s2:p1->s4[label=\"8\"];\n",
1585 " s1->s4[label=\"6\"];\n",
1586 "}\n",
1587 );
1588 const AAUBV_ABVDU_DOT: &str = concat!(
1589 "digraph G {\n",
1590 "rankdir=LR;\n",
1591 " s0[shape=record, label=\"<p0>|<p1>|<p2>\"];\n",
1592 " s2[label=\"2\"];\n",
1593 " s3[label=\"*\"];\n",
1594 " s1[label=\"1\"];\n",
1595 " s0:p0->s1[label=\"1\"];\n",
1596 " s0:p1->s2[label=\"2\"];\n",
1597 " s0:p2->s1[label=\"4\"];\n",
1598 " s2->s3[label=\"7\"];\n",
1599 " s1->s3[label=\"6\"];\n",
1600 "}\n",
1601 );
1602 const AAUBU_ACUDU_DOT: &str = concat!(
1603 "digraph G {\n",
1604 "rankdir=LR;\n",
1605 " s0[shape=record, label=\"<p0>|<p1>|<p2>|<p3>\"];\n",
1606 " s1[label=\"1\"];\n",
1607 " s2[label=\"*\"];\n",
1608 " s0:p0->s1[label=\"1\"];\n",
1609 " s0:p1->s1[label=\"2\"];\n",
1610 " s0:p2->s1[label=\"3\"];\n",
1611 " s0:p3->s1[label=\"4\"];\n",
1612 " s1->s2[label=\"6\"];\n",
1613 "}\n",
1614 );
1615
1616 #[derive(Clone, Copy)]
1617 enum ContextSpec {
1618 Empty,
1619 Chain(&'static [usize]),
1620 Array(&'static [&'static [usize]]),
1621 }
1622
1623 #[derive(Clone, Copy)]
1624 enum Scenario {
1625 Merge {
1626 left: ContextSpec,
1627 right: ContextSpec,
1628 },
1629 NestedFullContext,
1630 }
1631
1632 struct GraphCase {
1633 source_test: &'static str,
1634 logical_id: &'static str,
1635 scenario: Scenario,
1636 root_is_wildcard: bool,
1637 expected: &'static str,
1638 }
1639
1640 impl GraphCase {
1641 const fn merge(
1642 source_test: &'static str,
1643 logical_id: &'static str,
1644 left: ContextSpec,
1645 right: ContextSpec,
1646 root_is_wildcard: bool,
1647 expected: &'static str,
1648 ) -> Self {
1649 Self {
1650 source_test,
1651 logical_id,
1652 scenario: Scenario::Merge { left, right },
1653 root_is_wildcard,
1654 expected,
1655 }
1656 }
1657
1658 const fn nested_full_context(
1659 source_test: &'static str,
1660 logical_id: &'static str,
1661 expected: &'static str,
1662 ) -> Self {
1663 Self {
1664 source_test,
1665 logical_id,
1666 scenario: Scenario::NestedFullContext,
1667 root_is_wildcard: false,
1668 expected,
1669 }
1670 }
1671 }
1672
1673 const CASES: &[GraphCase] = &[
1674 GraphCase::merge(
1675 "test_$_$",
1676 "testgraphnodes-test-9ea85e6b69",
1677 ContextSpec::Empty,
1678 ContextSpec::Empty,
1679 true,
1680 EMPTY_WILDCARD_DOT,
1681 ),
1682 GraphCase::merge(
1683 "test_$_$_fullctx",
1684 "testgraphnodes-test-fullctx-3a6b2d8201",
1685 ContextSpec::Empty,
1686 ContextSpec::Empty,
1687 false,
1688 EMPTY_FULL_CONTEXT_DOT,
1689 ),
1690 GraphCase::merge(
1691 "test_x_$",
1692 "testgraphnodes-test-x-546922b23c",
1693 ContextSpec::Chain(&[9]),
1694 ContextSpec::Empty,
1695 true,
1696 EMPTY_WILDCARD_DOT,
1697 ),
1698 GraphCase::merge(
1699 "test_x_$_fullctx",
1700 "testgraphnodes-test-x-fullctx-7fdaaf473e",
1701 ContextSpec::Chain(&[9]),
1702 ContextSpec::Empty,
1703 false,
1704 X_EMPTY_FULL_CONTEXT_DOT,
1705 ),
1706 GraphCase::merge(
1707 "test_$_x",
1708 "testgraphnodes-test-x-546922b23c",
1709 ContextSpec::Empty,
1710 ContextSpec::Chain(&[9]),
1711 true,
1712 EMPTY_WILDCARD_DOT,
1713 ),
1714 GraphCase::merge(
1715 "test_$_x_fullctx",
1716 "testgraphnodes-test-x-fullctx-7fdaaf473e",
1717 ContextSpec::Empty,
1718 ContextSpec::Chain(&[9]),
1719 false,
1720 X_EMPTY_FULL_CONTEXT_DOT,
1721 ),
1722 GraphCase::merge(
1723 "test_a_a",
1724 "testgraphnodes-test-a-a-429589e373",
1725 ContextSpec::Chain(&[1]),
1726 ContextSpec::Chain(&[1]),
1727 true,
1728 A_DOT,
1729 ),
1730 GraphCase::merge(
1731 "test_a$_ax",
1732 "testgraphnodes-test-a-ax-fd976a340d",
1733 ContextSpec::Chain(&[1]),
1734 ContextSpec::Chain(&[9, 1]),
1735 true,
1736 A_DOT,
1737 ),
1738 GraphCase::merge(
1739 "test_a$_ax_fullctx",
1740 "testgraphnodes-test-a-ax-fullctx-502155fcf9",
1741 ContextSpec::Chain(&[1]),
1742 ContextSpec::Chain(&[9, 1]),
1743 false,
1744 A_EMPTY_AX_FULL_CONTEXT_DOT,
1745 ),
1746 GraphCase::merge(
1747 "test_ax$_a$",
1748 "testgraphnodes-test-ax-a-62a48f251b",
1749 ContextSpec::Chain(&[9, 1]),
1750 ContextSpec::Chain(&[1]),
1751 true,
1752 A_DOT,
1753 ),
1754 GraphCase::nested_full_context(
1755 "test_aa$_a$_$_fullCtx",
1756 "testgraphnodes-test-aa-a-fullctx-8e728ea773",
1757 NESTED_FULL_CONTEXT_DOT,
1758 ),
1759 GraphCase::merge(
1760 "test_ax$_a$_fullctx",
1761 "testgraphnodes-test-ax-a-fullctx-7ef9c1d6b2",
1762 ContextSpec::Chain(&[9, 1]),
1763 ContextSpec::Chain(&[1]),
1764 false,
1765 A_EMPTY_AX_FULL_CONTEXT_DOT,
1766 ),
1767 GraphCase::merge(
1768 "test_a_b",
1769 "testgraphnodes-test-a-b-080058428f",
1770 ContextSpec::Chain(&[1]),
1771 ContextSpec::Chain(&[2]),
1772 true,
1773 A_B_DOT,
1774 ),
1775 GraphCase::merge(
1776 "test_ax_ax_same",
1777 "testgraphnodes-test-ax-ax-same-1504dc3dd3",
1778 ContextSpec::Chain(&[9, 1]),
1779 ContextSpec::Chain(&[9, 1]),
1780 true,
1781 AX_AX_DOT,
1782 ),
1783 GraphCase::merge(
1784 "test_ax_ax",
1785 "testgraphnodes-test-ax-ax-48f57578fa",
1786 ContextSpec::Chain(&[9, 1]),
1787 ContextSpec::Chain(&[9, 1]),
1788 true,
1789 AX_AX_DOT,
1790 ),
1791 GraphCase::merge(
1792 "test_abx_abx",
1793 "testgraphnodes-test-abx-abx-77366e32e9",
1794 ContextSpec::Chain(&[9, 2, 1]),
1795 ContextSpec::Chain(&[9, 2, 1]),
1796 true,
1797 ABX_ABX_DOT,
1798 ),
1799 GraphCase::merge(
1800 "test_abx_acx",
1801 "testgraphnodes-test-abx-acx-a3af7f90fa",
1802 ContextSpec::Chain(&[9, 2, 1]),
1803 ContextSpec::Chain(&[9, 3, 1]),
1804 true,
1805 ABX_ACX_DOT,
1806 ),
1807 GraphCase::merge(
1808 "test_ax_bx_same",
1809 "testgraphnodes-test-ax-bx-same-d0506bf7a9",
1810 ContextSpec::Chain(&[9, 1]),
1811 ContextSpec::Chain(&[9, 2]),
1812 true,
1813 AX_BX_DOT,
1814 ),
1815 GraphCase::merge(
1816 "test_ax_bx",
1817 "testgraphnodes-test-ax-bx-1ea2df9a04",
1818 ContextSpec::Chain(&[9, 1]),
1819 ContextSpec::Chain(&[9, 2]),
1820 true,
1821 AX_BX_DOT,
1822 ),
1823 GraphCase::merge(
1824 "test_ax_by",
1825 "testgraphnodes-test-ax-by-47815d59d2",
1826 ContextSpec::Chain(&[9, 1]),
1827 ContextSpec::Chain(&[10, 2]),
1828 true,
1829 AX_BY_DOT,
1830 ),
1831 GraphCase::merge(
1832 "test_a$_bx",
1833 "testgraphnodes-test-a-bx-b15f7b876f",
1834 ContextSpec::Chain(&[1]),
1835 ContextSpec::Chain(&[9, 2]),
1836 true,
1837 A_EMPTY_BX_DOT,
1838 ),
1839 GraphCase::merge(
1840 "test_a$_bx_fullctx",
1841 "testgraphnodes-test-a-bx-fullctx-a35242b6cf",
1842 ContextSpec::Chain(&[1]),
1843 ContextSpec::Chain(&[9, 2]),
1844 false,
1845 A_EMPTY_BX_FULL_CONTEXT_DOT,
1846 ),
1847 GraphCase::merge(
1848 "test_aex_bfx",
1849 "testgraphnodes-test-aex-bfx-07ad9de126",
1850 ContextSpec::Chain(&[9, 5, 1]),
1851 ContextSpec::Chain(&[9, 6, 2]),
1852 true,
1853 AEX_BFX_DOT,
1854 ),
1855 GraphCase::merge(
1856 "test_A$_A$_fullctx",
1857 "testgraphnodes-test-a-a-fullctx-b023f64b6c",
1858 ContextSpec::Array(&[&[]]),
1859 ContextSpec::Array(&[&[]]),
1860 false,
1861 EMPTY_FULL_CONTEXT_DOT,
1862 ),
1863 GraphCase::merge(
1864 "test_Aab_Ac",
1865 "testgraphnodes-test-aab-ac-139c5b709d",
1866 ContextSpec::Array(&[&[1], &[2]]),
1867 ContextSpec::Array(&[&[3]]),
1868 true,
1869 A_B_C_DOT,
1870 ),
1871 GraphCase::merge(
1872 "test_Aa_Aa",
1873 "testgraphnodes-test-aa-aa-0a175c83db",
1874 ContextSpec::Array(&[&[1]]),
1875 ContextSpec::Array(&[&[1]]),
1876 true,
1877 A_DOT,
1878 ),
1879 GraphCase::merge(
1880 "test_Aa_Abc",
1881 "testgraphnodes-test-aa-abc-db12d99894",
1882 ContextSpec::Array(&[&[1]]),
1883 ContextSpec::Array(&[&[2], &[3]]),
1884 true,
1885 A_B_C_DOT,
1886 ),
1887 GraphCase::merge(
1888 "test_Aac_Ab",
1889 "testgraphnodes-test-aac-ab-ef785e17e7",
1890 ContextSpec::Array(&[&[1], &[3]]),
1891 ContextSpec::Array(&[&[2]]),
1892 true,
1893 A_B_C_DOT,
1894 ),
1895 GraphCase::merge(
1896 "test_Aab_Aa",
1897 "testgraphnodes-test-aab-aa-d90d8d54f0",
1898 ContextSpec::Array(&[&[1], &[2]]),
1899 ContextSpec::Array(&[&[1]]),
1900 true,
1901 A_B_DOT,
1902 ),
1903 GraphCase::merge(
1904 "test_Aab_Ab",
1905 "testgraphnodes-test-aab-ab-e2d46352b4",
1906 ContextSpec::Array(&[&[1], &[2]]),
1907 ContextSpec::Array(&[&[2]]),
1908 true,
1909 A_B_DOT,
1910 ),
1911 GraphCase::merge(
1912 "test_Aax_Aby",
1913 "testgraphnodes-test-aax-aby-cccf935759",
1914 ContextSpec::Array(&[&[9, 1]]),
1915 ContextSpec::Array(&[&[10, 2]]),
1916 true,
1917 AX_BY_DOT,
1918 ),
1919 GraphCase::merge(
1920 "test_Aax_Aay",
1921 "testgraphnodes-test-aax-aay-c0f9b80842",
1922 ContextSpec::Array(&[&[9, 1]]),
1923 ContextSpec::Array(&[&[10, 1]]),
1924 true,
1925 AAX_AAY_DOT,
1926 ),
1927 GraphCase::merge(
1928 "test_Aaxc_Aayd",
1929 "testgraphnodes-test-aaxc-aayd-a73533f64d",
1930 ContextSpec::Array(&[&[9, 1], &[3]]),
1931 ContextSpec::Array(&[&[10, 1], &[4]]),
1932 true,
1933 AAXC_AAYD_DOT,
1934 ),
1935 GraphCase::merge(
1936 "test_Aaubv_Acwdx",
1937 "testgraphnodes-test-aaubv-acwdx-f479c849df",
1938 ContextSpec::Array(&[&[6, 1], &[7, 2]]),
1939 ContextSpec::Array(&[&[8, 3], &[9, 4]]),
1940 true,
1941 AAUBV_ACWDX_DOT,
1942 ),
1943 GraphCase::merge(
1944 "test_Aaubv_Abvdx",
1945 "testgraphnodes-test-aaubv-abvdx-01eb5714fe",
1946 ContextSpec::Array(&[&[6, 1], &[7, 2]]),
1947 ContextSpec::Array(&[&[7, 2], &[9, 4]]),
1948 true,
1949 AAUBV_ABVDX_DOT,
1950 ),
1951 GraphCase::merge(
1952 "test_Aaubv_Abwdx",
1953 "testgraphnodes-test-aaubv-abwdx-7953c9b489",
1954 ContextSpec::Array(&[&[6, 1], &[7, 2]]),
1955 ContextSpec::Array(&[&[8, 2], &[9, 4]]),
1956 true,
1957 AAUBV_ABWDX_DOT,
1958 ),
1959 GraphCase::merge(
1960 "test_Aaubv_Abvdu",
1961 "testgraphnodes-test-aaubv-abvdu-ecc8850384",
1962 ContextSpec::Array(&[&[6, 1], &[7, 2]]),
1963 ContextSpec::Array(&[&[7, 2], &[6, 4]]),
1964 true,
1965 AAUBV_ABVDU_DOT,
1966 ),
1967 GraphCase::merge(
1968 "test_Aaubu_Acudu",
1969 "testgraphnodes-test-aaubu-acudu-7cb798b616",
1970 ContextSpec::Array(&[&[6, 1], &[6, 2]]),
1971 ContextSpec::Array(&[&[6, 3], &[6, 4]]),
1972 true,
1973 AAUBU_ACUDU_DOT,
1974 ),
1975 ];
1976
1977 fn build_chain(arena: &mut ContextArena, return_states: &[usize]) -> ContextId {
1978 let mut context = EMPTY_CONTEXT;
1979 for &return_state in return_states {
1980 context = arena.singleton(context, return_state);
1981 }
1982 context
1983 }
1984
1985 fn build_context(arena: &mut ContextArena, spec: ContextSpec) -> ContextId {
1986 match spec {
1987 ContextSpec::Empty => EMPTY_CONTEXT,
1988 ContextSpec::Chain(return_states) => build_chain(arena, return_states),
1989 ContextSpec::Array(chains) => {
1990 let mut entries = Vec::with_capacity(chains.len());
1991 for return_states in chains {
1992 let context = build_chain(arena, return_states);
1993 entries.push(arena.first_entry(context));
1994 }
1995 arena.intern_entries(&entries)
1996 }
1997 }
1998 }
1999
2000 fn run_case(case: &GraphCase) -> String {
2001 let mut arena = ContextArena::new();
2002 let mut workspace = PredictionWorkspace::default();
2003 let merged = match case.scenario {
2004 Scenario::Merge { left, right } => {
2005 let left = build_context(&mut arena, left);
2006 let right = build_context(&mut arena, right);
2007 arena.merge(left, right, case.root_is_wildcard, &mut workspace)
2008 }
2009 Scenario::NestedFullContext => {
2010 let child = arena.singleton(EMPTY_CONTEXT, 8);
2011 let right = arena.merge(EMPTY_CONTEXT, child, false, &mut workspace);
2012 let left = arena.singleton(right, 8);
2013 arena.merge(left, right, false, &mut workspace)
2014 }
2015 };
2016 render_dot(&arena, merged, case.root_is_wildcard)
2017 }
2018
2019 fn render_dot(arena: &ContextArena, context: ContextId, root_is_wildcard: bool) -> String {
2020 let mut nodes = String::new();
2021 let mut edges = String::new();
2022 let mut context_ids = HashMap::new();
2023 let mut work_list = VecDeque::new();
2024 context_ids.insert(context, 0);
2025 work_list.push_back(context);
2026
2027 while let Some(current) = work_list.pop_front() {
2028 let current_id = context_ids[¤t];
2029 let len = arena.len(current);
2030 write!(&mut nodes, " s{current_id}[").expect("write to string");
2031 if len > 1 {
2032 nodes.push_str("shape=record, ");
2033 }
2034 nodes.push_str("label=\"");
2035 if arena.is_empty(current) {
2036 nodes.push(if root_is_wildcard { '*' } else { '$' });
2037 } else if len > 1 {
2038 for index in 0..len {
2039 if index > 0 {
2040 nodes.push('|');
2041 }
2042 write!(&mut nodes, "<p{index}>").expect("write to string");
2043 if arena.return_state(current, index) == Some(EMPTY_RETURN_STATE) {
2044 nodes.push(if root_is_wildcard { '*' } else { '$' });
2045 }
2046 }
2047 } else {
2048 write!(&mut nodes, "{current_id}").expect("write to string");
2049 }
2050 nodes.push_str("\"];\n");
2051
2052 if arena.is_empty(current) {
2053 continue;
2054 }
2055 for index in 0..len {
2056 let return_state = arena
2057 .return_state(current, index)
2058 .expect("context entry in range");
2059 if return_state == EMPTY_RETURN_STATE {
2060 continue;
2061 }
2062 let parent = arena.parent(current, index).expect("non-empty parent");
2063 let parent_id = if let Some(&parent_id) = context_ids.get(&parent) {
2064 parent_id
2065 } else {
2066 let parent_id = context_ids.len();
2067 context_ids.insert(parent, parent_id);
2068 work_list.push_front(parent);
2069 parent_id
2070 };
2071
2072 write!(&mut edges, " s{current_id}").expect("write to string");
2073 if len > 1 {
2074 write!(&mut edges, ":p{index}").expect("write to string");
2075 }
2076 writeln!(&mut edges, "->s{parent_id}[label=\"{return_state}\"];")
2077 .expect("write to string");
2078 }
2079 }
2080
2081 let mut dot = String::from("digraph G {\nrankdir=LR;\n");
2082 dot.push_str(&nodes);
2083 dot.push_str(&edges);
2084 dot.push_str("}\n");
2085 dot
2086 }
2087
2088 #[test]
2089 fn pinned_upstream_test_graph_nodes_matches_dot() {
2090 assert_eq!(CASES.len(), 38, "pinned Java source case inventory drifted");
2091 let source_tests = CASES
2092 .iter()
2093 .map(|case| case.source_test)
2094 .collect::<BTreeSet<_>>();
2095 assert_eq!(
2096 source_tests.len(),
2097 38,
2098 "pinned Java source test names must be unique"
2099 );
2100 let logical_ids = CASES
2101 .iter()
2102 .map(|case| case.logical_id)
2103 .collect::<BTreeSet<_>>();
2104 assert_eq!(
2105 logical_ids.len(),
2106 36,
2107 "pinned upstream logical row inventory drifted"
2108 );
2109
2110 let selector = std::env::var("ANTLR_GRAPH_NODE_CASE").ok();
2111 let selected = CASES
2112 .iter()
2113 .filter(|case| {
2114 selector
2115 .as_deref()
2116 .is_none_or(|logical_id| case.logical_id == logical_id)
2117 })
2118 .collect::<Vec<_>>();
2119 assert!(
2120 !selected.is_empty(),
2121 "ANTLR_GRAPH_NODE_CASE={:?} matched no logical row",
2122 selector.as_deref().unwrap_or_default()
2123 );
2124
2125 let mut mismatches = Vec::new();
2126 for case in &selected {
2127 let actual = run_case(case);
2128 if actual != case.expected {
2129 mismatches.push(format!(
2130 "logical_id={}\nsource_test={}\n--- expected\n{}--- actual\n{}",
2131 case.logical_id, case.source_test, case.expected, actual
2132 ));
2133 }
2134 }
2135
2136 assert!(
2137 mismatches.is_empty(),
2138 "TestGraphNodes DOT mismatches ({}/{} source cases):\n\n{}",
2139 mismatches.len(),
2140 selected.len(),
2141 mismatches.join("\n")
2142 );
2143 }
2144 }
2145}