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