1use std::collections::{BTreeMap, BTreeSet, HashMap};
18use std::fmt;
19
20use super::ir::{
21 Automaton, CallArg, CallId, ClockId, EndMethod, EndPoint, FirstMatchId, LeafId, LocalVarId,
22 ThreadStateId, Transition,
23};
24use super::wide;
25
26#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct Attempt {
29 pub admits_empty: bool,
32 pub ends: BTreeSet<u32>,
34}
35
36pub type Frame = Vec<Option<i64>>;
39
40pub type Matches = smallvec::SmallVec<[(u32, Frame); 1]>;
44
45#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct LocalAttempt {
49 pub admits_empty: bool,
51 pub matches: BTreeSet<(u32, Frame)>,
55}
56
57impl LocalAttempt {
58 pub fn ends(&self) -> BTreeSet<u32> {
60 self.matches.iter().map(|(t, _)| *t).collect()
61 }
62}
63
64#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
69pub struct CallEvent {
70 pub tick: u32,
71 pub call: CallId,
72 pub args: Vec<Option<i64>>,
73}
74
75#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct CallAttempt {
79 pub attempt: LocalAttempt,
81 pub calls: BTreeSet<CallEvent>,
86}
87
88#[derive(Clone, Debug, PartialEq, Eq)]
90pub enum TraceError {
91 UnsupportedEdge(&'static str),
95 UnassignedRead(LocalVarId),
99 EndWithoutBegin(FirstMatchId),
102 BeginWhileOpen(FirstMatchId),
106 MatchedWithoutClocks,
109 NestedMatched,
113 BadClocks,
115 BadTimeline,
118 NotASequence,
121}
122
123impl fmt::Display for TraceError {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 match self {
126 TraceError::NotASequence => write!(
127 f,
128 "sva trace interpreter: the matches of a property whose root is not a sequence \
129 were asked for"
130 ),
131 TraceError::BadTimeline => write!(
132 f,
133 "sva trace interpreter: a timeline's tick lists must be strictly increasing and \
134 inside the trace"
135 ),
136 TraceError::MatchedWithoutClocks => write!(
137 f,
138 "sva trace interpreter: a `.matched` end point needs a clock model \
139 (attempt_with_clocks)"
140 ),
141 TraceError::NestedMatched => write!(
142 f,
143 "sva trace interpreter: a `.matched` inside an instance run on another clock is \
144 not modelled"
145 ),
146 TraceError::BadClocks => write!(
147 f,
148 "sva trace interpreter: the clock model's tick times must be strictly increasing"
149 ),
150 TraceError::UnsupportedEdge(kind) => {
151 write!(f, "sva trace interpreter: {kind} edges are not interpreted")
152 }
153 TraceError::EndWithoutBegin(g) => write!(
154 f,
155 "sva trace interpreter: FirstMatchEnd({}) reached without its FirstMatchBegin",
156 g.0
157 ),
158 TraceError::BeginWhileOpen(g) => write!(
159 f,
160 "sva trace interpreter: FirstMatchBegin({}) reached while the group is open",
161 g.0
162 ),
163 TraceError::UnassignedRead(v) => write!(
164 f,
165 "sva trace interpreter: read of unassigned local variable {}",
166 v.0
167 ),
168 }
169 }
170}
171
172impl std::error::Error for TraceError {}
173
174pub fn read_local(frame: &[Option<i64>], var: LocalVarId) -> Result<i64, TraceError> {
178 frame
179 .get(var.0 as usize)
180 .copied()
181 .flatten()
182 .ok_or(TraceError::UnassignedRead(var))
183}
184
185type Config = (u32, u32, ClockId, Vec<(FirstMatchId, u32)>, Frame);
193
194type Eval<'a> = &'a dyn Fn(LeafId, u32, &[Option<i64>]) -> Result<i64, TraceError>;
197
198pub fn attempt(
208 au: &Automaton,
209 start: u32,
210 len: u32,
211 leaf: &dyn Fn(LeafId, u32) -> bool,
212) -> Result<Attempt, TraceError> {
213 let matches = run(
214 au,
215 start,
216 len,
217 &|id, tick, _| Ok(i64::from(leaf(id, tick))),
218 false,
219 )?;
220 Ok(Attempt {
221 admits_empty: au.admits_empty,
222 ends: matches.0.into_iter().map(|(t, _)| t).collect(),
223 })
224}
225
226pub fn attempt_with_locals(
237 au: &Automaton,
238 start: u32,
239 len: u32,
240 eval: Eval<'_>,
241) -> Result<LocalAttempt, TraceError> {
242 Ok(LocalAttempt {
243 admits_empty: au.admits_empty,
244 matches: run(au, start, len, eval, true)?.0,
245 })
246}
247
248pub fn attempt_with_calls(
254 au: &Automaton,
255 start: u32,
256 len: u32,
257 eval: Eval<'_>,
258) -> Result<CallAttempt, TraceError> {
259 let (matches, calls) = run(au, start, len, eval, true)?;
260 Ok(CallAttempt {
261 attempt: LocalAttempt {
262 admits_empty: au.admits_empty,
263 matches,
264 },
265 calls,
266 })
267}
268
269pub fn attempt_with_clocks(
280 au: &Automaton,
281 start: u32,
282 clocks: &TraceClocks,
283 eval: Eval<'_>,
284) -> Result<CallAttempt, TraceError> {
285 let strictly_increasing = |v: &[u64]| v.windows(2).all(|w| w[0] < w[1]);
286 if !strictly_increasing(&clocks.dest) || !strictly_increasing(&clocks.source) {
287 return Err(TraceError::BadClocks);
288 }
289 let mut runner = Runner {
290 eval,
291 locals: true,
292 len: clocks.dest.len() as u32,
293 clocks: Some(clocks),
294 timeline: None,
295 status: HashMap::new(),
296 calls: BTreeSet::new(),
297 word: None,
298 };
299 let matches = runner.attempt(au, start, Space::Dest)?;
300 Ok(CallAttempt {
301 attempt: LocalAttempt {
302 admits_empty: au.admits_empty,
303 matches,
304 },
305 calls: runner.calls,
306 })
307}
308
309#[derive(Clone, Debug, PartialEq, Eq, Hash)]
321pub struct TailWord {
322 pub real: u32,
323 pub mid: Vec<bool>,
324 pub tail_top: bool,
325}
326
327impl TailWord {
328 pub fn weak(real: u32) -> Self {
330 TailWord {
331 real,
332 mid: Vec::new(),
333 tail_top: true,
334 }
335 }
336
337 pub fn strong(real: u32) -> Self {
339 TailWord {
340 real,
341 mid: Vec::new(),
342 tail_top: false,
343 }
344 }
345
346 pub fn extent(&self) -> u32 {
349 self.real + self.mid.len() as u32
350 }
351
352 pub fn letter(&self, tick: u32) -> Option<bool> {
355 if tick < self.real {
356 None
357 } else {
358 Some(
359 self.mid
360 .get((tick - self.real) as usize)
361 .copied()
362 .unwrap_or(self.tail_top),
363 )
364 }
365 }
366
367 pub fn flip(&self) -> Self {
370 TailWord {
371 real: self.real,
372 mid: self.mid.iter().map(|t| !t).collect(),
373 tail_top: !self.tail_top,
374 }
375 }
376
377 pub fn cut_then_top(&self, n: u32) -> Self {
380 if n <= self.real {
381 return TailWord::weak(n);
382 }
383 let keep = (n - self.real) as usize;
384 let mid = (0..keep)
385 .map(|i| self.mid.get(i).copied().unwrap_or(self.tail_top))
386 .collect();
387 let mut w = TailWord {
388 real: self.real,
389 mid,
390 tail_top: true,
391 };
392 while w.mid.last() == Some(&true) {
394 w.mid.pop();
395 }
396 w
397 }
398}
399
400#[derive(Clone, Debug, PartialEq, Eq)]
411pub struct Timeline {
412 real: u32,
413 ticks: Vec<Vec<u32>>,
414}
415
416impl Timeline {
417 pub fn new(real: u32, ticks: Vec<Vec<u32>>) -> Result<Self, TraceError> {
420 let ok = ticks
421 .iter()
422 .all(|l| l.windows(2).all(|w| w[0] < w[1]) && l.iter().all(|&t| t < real));
423 if ok {
424 Ok(Timeline { real, ticks })
425 } else {
426 Err(TraceError::BadTimeline)
427 }
428 }
429
430 pub fn from_rows(rows: &[&[bool]]) -> Self {
433 let real = rows.iter().map(|r| r.len() as u32).max().unwrap_or(0);
434 let ticks = rows
435 .iter()
436 .map(|r| {
437 r.iter()
438 .enumerate()
439 .filter(|&(_, &b)| b)
440 .map(|(t, _)| t as u32)
441 .collect()
442 })
443 .collect();
444 Timeline { real, ticks }
445 }
446
447 pub fn real(&self) -> u32 {
449 self.real
450 }
451
452 pub fn truncated(&self, n: u32) -> Timeline {
456 let real = n.min(self.real);
457 Timeline {
458 real,
459 ticks: self
460 .ticks
461 .iter()
462 .map(|l| l.iter().copied().take_while(|&t| t < real).collect())
463 .collect(),
464 }
465 }
466
467 fn list(&self, c: ClockId) -> Option<&[u32]> {
468 self.ticks.get(c.0 as usize).map(Vec::as_slice)
469 }
470
471 pub fn is_tick(&self, c: ClockId, p: u32) -> bool {
473 match self.list(c) {
474 _ if p >= self.real => true,
475 None => true,
476 Some(l) => l.binary_search(&p).is_ok(),
477 }
478 }
479
480 pub fn next_tick(&self, c: ClockId, p: u32) -> u32 {
482 match self.list(c) {
483 _ if p >= self.real => p,
484 None => p,
485 Some(l) => l
486 .get(l.partition_point(|&t| t < p))
487 .copied()
488 .unwrap_or(self.real),
489 }
490 }
491
492 pub fn kth_after(&self, c: ClockId, p: u32, k: u32) -> u32 {
496 if k == 0 {
497 return p;
498 }
499 match self.list(c) {
500 _ if p >= self.real => p + k,
501 None => p + k,
502 Some(l) => {
503 let at_or_before = l.partition_point(|&t| t <= p);
504 let j = at_or_before + k as usize - 1;
505 match l.get(j) {
506 Some(&t) => t,
507 None => self.real + (j - l.len()) as u32,
508 }
509 }
510 }
511 }
512}
513
514pub fn attempt_word(
524 au: &Automaton,
525 entry: ThreadStateId,
526 frame0: &[Option<i64>],
527 start: u32,
528 word: &TailWord,
529 horizon: u32,
530 eval: Eval<'_>,
531) -> Result<BTreeSet<(u32, Frame)>, TraceError> {
532 attempt_word_in(
533 au,
534 entry,
535 frame0,
536 start,
537 word,
538 horizon,
539 eval,
540 None,
541 au.entry_clock,
542 )
543}
544
545#[allow(clippy::too_many_arguments)]
551pub fn attempt_word_in(
552 au: &Automaton,
553 entry: ThreadStateId,
554 frame0: &[Option<i64>],
555 start: u32,
556 word: &TailWord,
557 horizon: u32,
558 eval: Eval<'_>,
559 timeline: Option<&Timeline>,
560 dom: ClockId,
561) -> Result<BTreeSet<(u32, Frame)>, TraceError> {
562 attempt_word_calls(au, entry, frame0, start, word, horizon, eval, timeline, dom)
563 .map(|(matches, _)| matches)
564}
565
566#[allow(clippy::too_many_arguments)]
571pub fn attempt_word_calls(
572 au: &Automaton,
573 entry: ThreadStateId,
574 frame0: &[Option<i64>],
575 start: u32,
576 word: &TailWord,
577 horizon: u32,
578 eval: Eval<'_>,
579 timeline: Option<&Timeline>,
580 dom: ClockId,
581) -> Result<RunResult, TraceError> {
582 let mut runner = Runner {
583 eval,
584 locals: true,
585 len: horizon,
586 clocks: None,
587 timeline,
588 status: HashMap::new(),
589 calls: BTreeSet::new(),
590 word: Some(word),
591 };
592 let matches = runner.attempt_from(au, entry, frame0.to_vec(), start, dom, Space::Dest)?;
593 Ok((matches, runner.calls))
594}
595
596pub type RunResult = (BTreeSet<(u32, Frame)>, BTreeSet<CallEvent>);
599
600fn run(
604 au: &Automaton,
605 start: u32,
606 len: u32,
607 eval: Eval<'_>,
608 locals: bool,
609) -> Result<RunResult, TraceError> {
610 let mut runner = Runner {
611 eval,
612 locals,
613 len,
614 clocks: None,
615 timeline: None,
616 status: HashMap::new(),
617 calls: BTreeSet::new(),
618 word: None,
619 };
620 let matches = runner.attempt(au, start, Space::Dest)?;
621 Ok((matches, runner.calls))
622}
623
624#[derive(Clone, Debug, PartialEq, Eq)]
630pub struct TraceClocks {
631 pub dest: Vec<u64>,
632 pub source: Vec<u64>,
633}
634
635#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
638enum Space {
639 Dest,
640 Source,
641}
642
643type EndStatus = BTreeMap<u32, BTreeSet<Frame>>;
646
647struct Runner<'a> {
651 eval: Eval<'a>,
652 locals: bool,
653 len: u32,
655 clocks: Option<&'a TraceClocks>,
656 timeline: Option<&'a Timeline>,
659 status: HashMap<(usize, Space), EndStatus>,
662 calls: BTreeSet<CallEvent>,
663 word: Option<&'a TailWord>,
666}
667
668impl Runner<'_> {
669 fn len_of(&self, space: Space) -> u32 {
670 match space {
671 Space::Dest => self.len,
672 Space::Source => self.clocks.map_or(0, |c| c.source.len() as u32),
673 }
674 }
675
676 fn free_run(&mut self, au: &Automaton, space: Space) -> Result<EndStatus, TraceError> {
680 let mut out = EndStatus::new();
681 for start in 0..self.len_of(space) {
682 if space == Space::Dest
684 && self
685 .timeline
686 .is_some_and(|t| !t.is_tick(au.entry_clock, start))
687 {
688 continue;
689 }
690 for (end, frame) in self.attempt(au, start, space)? {
691 out.entry(end).or_default().insert(frame);
692 }
693 }
694 Ok(out)
695 }
696
697 fn end_status(&mut self, ep: &EndPoint, space: Space) -> Result<&EndStatus, TraceError> {
699 let key = (std::ptr::from_ref(ep) as usize, space);
700 if !self.status.contains_key(&key) {
701 let computed = self.compute_status(ep, space)?;
702 self.status.insert(key, computed);
703 }
704 Ok(&self.status[&key])
705 }
706
707 fn compute_status(&mut self, ep: &EndPoint, space: Space) -> Result<EndStatus, TraceError> {
708 match (ep.method, space) {
709 (EndMethod::Triggered, _) => self.free_run(&ep.automaton, space),
711 (EndMethod::Matched, Space::Source) => Err(TraceError::NestedMatched),
712 (EndMethod::Matched, Space::Dest) => {
713 let Some(clocks) = self.clocks else {
714 return self.free_run(&ep.automaton, Space::Dest);
718 };
719 let source = self.free_run(&ep.automaton, Space::Source)?;
720 let mut out = EndStatus::new();
721 for (d, &hi) in clocks.dest.iter().enumerate() {
722 let lo = d.checked_sub(1).map(|p| clocks.dest[p]);
726 let mut frames = BTreeSet::new();
727 for (&end, fs) in &source {
728 let t = clocks.source[end as usize];
729 if t <= hi && lo.is_none_or(|l| t > l) {
730 frames.extend(fs.iter().cloned());
731 }
732 }
733 if !frames.is_empty() {
734 out.insert(d as u32, frames);
735 }
736 }
737 Ok(out)
738 }
739 }
740 }
741
742 fn attempt(
744 &mut self,
745 au: &Automaton,
746 start: u32,
747 space: Space,
748 ) -> Result<BTreeSet<(u32, Frame)>, TraceError> {
749 let frame = vec![None; au.local_vars.len()];
752 self.attempt_from(au, au.start, frame, start, au.entry_clock, space)
753 }
754
755 fn attempt_from(
760 &mut self,
761 au: &Automaton,
762 entry: ThreadStateId,
763 frame0: Frame,
764 start: u32,
765 dom0: ClockId,
766 space: Space,
767 ) -> Result<BTreeSet<(u32, Frame)>, TraceError> {
768 let (raw_eval, locals, len, word) = (self.eval, self.locals, self.len_of(space), self.word);
769 let timeline = if space == Space::Dest {
771 self.timeline
772 } else {
773 None
774 };
775 let step = |dom: ClockId, from: u32, k: u32| -> u32 {
777 timeline.map_or(from + k, |t| t.kth_after(dom, from, k))
778 };
779 let start = timeline.map_or(start, |t| t.next_tick(dom0, start));
780 let letter = |tick: u32| -> Option<bool> {
784 if space == Space::Dest {
785 word.and_then(|w| w.letter(tick))
786 } else {
787 None
788 }
789 };
790 let eval = |leaf: LeafId, tick: u32, frame: &[Option<i64>]| -> Result<i64, TraceError> {
791 match letter(tick) {
792 Some(_) => Ok(0),
793 None => raw_eval(leaf, tick, frame),
794 }
795 };
796 let mut matches = BTreeSet::new();
797 let mut table: BTreeMap<(FirstMatchId, u32), u32> = BTreeMap::new();
799 let mut seen: BTreeSet<Config> = BTreeSet::new();
800 let mut work: BTreeSet<Config> = BTreeSet::new();
801 if start < len {
802 work.insert((start, entry.0, dom0, Vec::new(), frame0));
803 }
804 while let Some((tick, state, dom, tags, frame)) = work.pop_first() {
805 if !seen.insert((tick, state, dom, tags.clone(), frame.clone())) {
806 continue;
807 }
808 let st = au.state(ThreadStateId(state));
809 for edge in &st.edges {
810 if let Some(g) = edge.guard {
811 let holds = match letter(tick) {
812 Some(top) => top,
813 None => raw_eval(g, tick, &frame)? != 0,
814 };
815 if !holds {
816 continue;
817 }
818 }
819 let target = edge.target.0;
820 match &edge.label {
821 Transition::Epsilon => {
822 work.insert((tick, target, dom, tags.clone(), frame.clone()));
823 }
824 Transition::Delay { min, max } if timeline.is_none() => {
825 let room = len - 1 - tick;
826 let hi = max.map_or(room, |m| m.min(room));
827 for k in *min..=hi {
828 work.insert((tick + k, target, dom, tags.clone(), frame.clone()));
829 }
830 }
831 Transition::Delay { min, max } => {
834 let mut k = *min;
835 while max.is_none_or(|m| k <= m) {
836 let to = step(dom, tick, k);
837 if to >= len {
838 break;
839 }
840 work.insert((to, target, dom, tags.clone(), frame.clone()));
841 k += 1;
842 }
843 }
844 Transition::ClockSync { to, strict } => {
845 let at = if *strict {
846 step(*to, tick, 1)
847 } else {
848 timeline.map_or(tick, |t| t.next_tick(*to, tick))
849 };
850 if at < len {
851 work.insert((at, target, *to, tags.clone(), frame.clone()));
852 }
853 }
854 Transition::Match { .. } => {
855 matches.insert((tick, frame.clone()));
856 }
857 Transition::FirstMatchBegin { group } => {
858 if tags.iter().any(|(g, _)| g == group) {
859 return Err(TraceError::BeginWhileOpen(*group));
860 }
861 let mut t2 = tags.clone();
862 t2.push((*group, tick));
863 t2.sort();
864 work.insert((tick, target, dom, t2, frame.clone()));
865 }
866 Transition::FirstMatchEnd { group } => {
867 let Some(pos) = tags.iter().position(|(g, _)| g == group) else {
868 return Err(TraceError::EndWithoutBegin(*group));
869 };
870 let begin = tags[pos].1;
871 let earliest = *table.entry((*group, begin)).or_insert(tick);
872 if earliest == tick {
873 let mut t2 = tags.clone();
874 t2.remove(pos);
875 work.insert((tick, target, dom, t2, frame.clone()));
876 }
877 }
878 Transition::Abort { .. } => return Err(TraceError::UnsupportedEdge("Abort")),
879 Transition::LocalVarWrite { var, value } => {
880 if !locals {
881 return Err(TraceError::UnsupportedEdge("LocalVarWrite"));
882 }
883 let v = au.local_var(*var).cast(eval(*value, tick, &frame)?);
884 let mut f2 = frame.clone();
885 f2[var.0 as usize] = Some(v);
886 work.insert((tick, target, dom, tags.clone(), f2));
887 }
888 Transition::LocalVarCopy { dst, src } => {
889 if !locals {
890 return Err(TraceError::UnsupportedEdge("LocalVarCopy"));
891 }
892 let mut f2 = frame.clone();
893 wide::copy_local(
894 &mut f2,
895 *dst,
896 au.local_var(*dst),
897 &frame,
898 *src,
899 au.local_var(*src),
900 );
901 work.insert((tick, target, dom, tags.clone(), f2));
902 }
903 Transition::LocalVarStep { var, delta } => {
904 if !locals {
905 return Err(TraceError::UnsupportedEdge("LocalVarStep"));
906 }
907 let decl = au.local_var(*var);
908 let mut f2 = frame.clone();
909 if decl.is_wide() {
910 wide::step_local(&mut f2, *var, decl, *delta)
911 .ok_or(TraceError::UnassignedRead(*var))?;
912 } else {
913 let cur = read_local(&frame, *var)?;
914 f2[var.0 as usize] = Some(decl.cast(cur.wrapping_add(*delta)));
915 }
916 work.insert((tick, target, dom, tags.clone(), f2));
917 }
918 Transition::SubroutineCall { call, args } => {
919 if !locals {
920 return Err(TraceError::UnsupportedEdge("SubroutineCall"));
921 }
922 let mut vals = Vec::with_capacity(args.len());
923 for a in args {
924 vals.push(match a {
925 CallArg::Value(leaf) => Some(eval(*leaf, tick, &frame)?),
926 CallArg::Ref | CallArg::Const => None,
927 });
928 }
929 if letter(tick).is_none() {
931 self.calls.insert(CallEvent {
932 tick,
933 call: *call,
934 args: vals,
935 });
936 }
937 work.insert((tick, target, dom, tags.clone(), frame.clone()));
938 }
939 Transition::EndPoint { point, flag } => {
940 if !locals {
941 return Err(TraceError::UnsupportedEdge("EndPoint"));
942 }
943 if let Some(top) = letter(tick) {
944 if let Some(slot) = flag {
947 let mut f2 = frame.clone();
948 f2[slot.0 as usize] = Some(i64::from(top));
949 work.insert((tick, target, dom, tags.clone(), f2));
950 } else if top {
951 work.insert((tick, target, dom, tags.clone(), frame.clone()));
952 }
953 continue;
954 }
955 let ep = au.end_point(*point);
956 let handoff = (ep.method == EndMethod::Matched && self.clocks.is_none())
960 .then(|| {
961 (0..tick)
962 .rev()
963 .find(|&p| self.timeline.is_none_or(|t| t.is_tick(dom, p)))
964 .map_or(0, |p| p + 1)
965 });
966 let status = self.end_status(ep, space)?;
967 let frames: Vec<Frame> = match handoff {
968 Some(lo) => status
969 .range(lo..=tick)
970 .flat_map(|(_, s)| s.iter().cloned())
971 .collect::<BTreeSet<Frame>>()
972 .into_iter()
973 .collect(),
974 None => status
975 .get(&tick)
976 .map(|s| s.iter().cloned().collect())
977 .unwrap_or_default(),
978 };
979 if let Some(slot) = flag {
980 let mut f2 = frame.clone();
983 f2[slot.0 as usize] = Some(i64::from(!frames.is_empty()));
984 work.insert((tick, target, dom, tags.clone(), f2));
985 } else {
986 for sub in frames {
989 let mut f2 = frame.clone();
990 for &(from, to) in &ep.flow_out {
991 wide::copy_local(
992 &mut f2,
993 to,
994 au.local_var(to),
995 &sub,
996 from,
997 au.local_var(from),
998 );
999 }
1000 work.insert((tick, target, dom, tags.clone(), f2));
1001 }
1002 }
1003 }
1004 }
1005 }
1006 }
1007 Ok(matches)
1008 }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use super::*;
1014 use crate::sva::ir::{AbortMode, LocalVarId};
1015
1016 fn rows(table: &'static [&'static [bool]]) -> impl Fn(LeafId, u32) -> bool {
1018 move |id, t| {
1019 table[id.0 as usize]
1020 .get(t as usize)
1021 .copied()
1022 .unwrap_or(false)
1023 }
1024 }
1025
1026 fn a_then_b() -> Automaton {
1028 let mut a = Automaton::empty();
1029 let s0 = a.start;
1030 let (s1, s2, s3) = (a.new_state(), a.new_state(), a.new_state());
1031 a.add_edge(s0, Some(LeafId(0)), Transition::Epsilon, s1);
1032 a.add_edge(
1033 s1,
1034 None,
1035 Transition::Delay {
1036 min: 1,
1037 max: Some(1),
1038 },
1039 s2,
1040 );
1041 a.add_edge(s2, Some(LeafId(1)), Transition::Epsilon, s3);
1042 a.add_edge(s3, None, Transition::Match { actions: vec![] }, s3);
1043 a
1044 }
1045
1046 #[test]
1047 fn concatenation_ends_one_tick_after_the_start() {
1048 let a = a_then_b();
1049 let tr = rows(&[&[true, false, true], &[false, true, false]]);
1050 let at0 = attempt(&a, 0, 3, &tr).unwrap();
1051 assert_eq!(at0.ends, BTreeSet::from([1]));
1052 assert!(!at0.admits_empty);
1053 assert!(attempt(&a, 1, 3, &tr).unwrap().ends.is_empty());
1055 assert!(attempt(&a, 2, 3, &tr).unwrap().ends.is_empty());
1057 }
1058
1059 #[test]
1060 fn delay_window_yields_every_end_tick_in_range() {
1061 let mut a = Automaton::empty();
1062 let s0 = a.start;
1063 let (s1, s2) = (a.new_state(), a.new_state());
1064 a.add_edge(
1065 s0,
1066 None,
1067 Transition::Delay {
1068 min: 1,
1069 max: Some(3),
1070 },
1071 s1,
1072 );
1073 a.add_edge(s1, Some(LeafId(0)), Transition::Epsilon, s2);
1074 a.add_edge(s2, None, Transition::Match { actions: vec![] }, s2);
1075 let tr = rows(&[&[false, true, false, true, true]]);
1076 assert_eq!(
1077 attempt(&a, 0, 5, &tr).unwrap().ends,
1078 BTreeSet::from([1, 3]),
1079 "tick 4 is outside the window, tick 2 fails the guard"
1080 );
1081 }
1082
1083 #[test]
1084 fn unbounded_delay_stops_at_the_end_of_the_trace() {
1085 let mut a = Automaton::empty();
1086 let s0 = a.start;
1087 let s1 = a.new_state();
1088 a.add_edge(s0, None, Transition::Delay { min: 0, max: None }, s1);
1089 a.add_edge(s1, None, Transition::Match { actions: vec![] }, s1);
1090 let tr = rows(&[]);
1091 assert_eq!(
1092 attempt(&a, 1, 4, &tr).unwrap().ends,
1093 BTreeSet::from([1, 2, 3])
1094 );
1095 }
1096
1097 fn first_of_window() -> Automaton {
1100 let mut a = Automaton::empty();
1101 let g = a.new_first_match_group();
1102 let s0 = a.start;
1103 let (s1, s2, s3, s4) = (a.new_state(), a.new_state(), a.new_state(), a.new_state());
1104 a.add_edge(s0, None, Transition::FirstMatchBegin { group: g }, s1);
1105 a.add_edge(
1106 s1,
1107 None,
1108 Transition::Delay {
1109 min: 0,
1110 max: Some(2),
1111 },
1112 s2,
1113 );
1114 a.add_edge(s2, Some(LeafId(0)), Transition::Epsilon, s3);
1115 a.add_edge(s3, None, Transition::FirstMatchEnd { group: g }, s4);
1116 a.add_edge(s4, None, Transition::Match { actions: vec![] }, s4);
1117 a
1118 }
1119
1120 #[test]
1121 fn first_match_keeps_only_the_earliest_end() {
1122 let a = first_of_window();
1123 let tr = rows(&[&[false, true, true, true]]);
1124 assert_eq!(attempt(&a, 0, 4, &tr).unwrap().ends, BTreeSet::from([1]));
1125 let mut plain = a.clone();
1128 for st in &mut plain.states {
1129 for e in &mut st.edges {
1130 if matches!(
1131 e.label,
1132 Transition::FirstMatchBegin { .. } | Transition::FirstMatchEnd { .. }
1133 ) {
1134 e.label = Transition::Epsilon;
1135 }
1136 }
1137 }
1138 assert_eq!(
1139 attempt(&plain, 0, 4, &tr).unwrap().ends,
1140 BTreeSet::from([1, 2]),
1141 "window is ticks 0..=2 and the guard holds on 1 and 2"
1142 );
1143 }
1144
1145 #[test]
1146 fn first_match_regions_beginning_on_different_ticks_are_independent() {
1147 let mut a = Automaton::empty();
1150 let g = a.new_first_match_group();
1151 let s0 = a.start;
1152 let (s1, s2, s3, s4, s5) = (
1153 a.new_state(),
1154 a.new_state(),
1155 a.new_state(),
1156 a.new_state(),
1157 a.new_state(),
1158 );
1159 a.add_edge(
1160 s0,
1161 None,
1162 Transition::Delay {
1163 min: 0,
1164 max: Some(1),
1165 },
1166 s1,
1167 );
1168 a.add_edge(s1, None, Transition::FirstMatchBegin { group: g }, s2);
1169 a.add_edge(
1170 s2,
1171 None,
1172 Transition::Delay {
1173 min: 0,
1174 max: Some(2),
1175 },
1176 s3,
1177 );
1178 a.add_edge(s3, Some(LeafId(0)), Transition::Epsilon, s4);
1179 a.add_edge(s4, None, Transition::FirstMatchEnd { group: g }, s5);
1180 a.add_edge(s5, None, Transition::Match { actions: vec![] }, s5);
1181 let tr = rows(&[&[false, true, false, true, false]]);
1185 assert_eq!(attempt(&a, 0, 5, &tr).unwrap().ends, BTreeSet::from([1]));
1186 let tr = rows(&[&[true, true, false, false, false]]);
1190 assert_eq!(attempt(&a, 0, 5, &tr).unwrap().ends, BTreeSet::from([0, 1]));
1191 let tr = rows(&[&[false, false, true, true, false]]);
1194 assert_eq!(attempt(&a, 0, 5, &tr).unwrap().ends, BTreeSet::from([2]));
1195 let tr = rows(&[&[false, false, false, true, false]]);
1198 assert_eq!(attempt(&a, 0, 5, &tr).unwrap().ends, BTreeSet::from([3]));
1199 }
1200
1201 #[test]
1202 fn first_match_end_without_begin_is_a_named_error() {
1203 let mut a = Automaton::empty();
1204 let s0 = a.start;
1205 let s1 = a.new_state();
1206 a.add_edge(
1207 s0,
1208 None,
1209 Transition::FirstMatchEnd {
1210 group: FirstMatchId(7),
1211 },
1212 s1,
1213 );
1214 let tr = rows(&[]);
1215 assert_eq!(
1216 attempt(&a, 0, 2, &tr),
1217 Err(TraceError::EndWithoutBegin(FirstMatchId(7)))
1218 );
1219 }
1220
1221 #[test]
1222 fn nested_begin_of_an_open_group_is_a_named_error() {
1223 let mut a = Automaton::empty();
1224 let g = FirstMatchId(0);
1225 let s0 = a.start;
1226 let (s1, s2) = (a.new_state(), a.new_state());
1227 a.add_edge(s0, None, Transition::FirstMatchBegin { group: g }, s1);
1228 a.add_edge(s1, None, Transition::FirstMatchBegin { group: g }, s2);
1229 let tr = rows(&[]);
1230 assert_eq!(attempt(&a, 0, 2, &tr), Err(TraceError::BeginWhileOpen(g)));
1231 }
1232
1233 type Eval<'a> = &'a dyn Fn(LeafId, u32, &[Option<i64>]) -> Result<i64, TraceError>;
1237
1238 fn write_then_guard() -> Automaton {
1240 let mut a = Automaton::empty();
1241 let v0 = a.new_local_var(4, false);
1242 let s0 = a.start;
1243 let (s1, s2, s3) = (a.new_state(), a.new_state(), a.new_state());
1244 a.add_edge(
1245 s0,
1246 None,
1247 Transition::LocalVarWrite {
1248 var: v0,
1249 value: LeafId(0),
1250 },
1251 s1,
1252 );
1253 a.add_edge(
1254 s1,
1255 None,
1256 Transition::Delay {
1257 min: 1,
1258 max: Some(1),
1259 },
1260 s2,
1261 );
1262 a.add_edge(s2, Some(LeafId(1)), Transition::Epsilon, s3);
1263 a.add_edge(s3, None, Transition::Match { actions: vec![] }, s3);
1264 a
1265 }
1266
1267 #[test]
1268 fn a_local_write_is_cast_to_the_slot_type_and_later_leaves_read_it() {
1269 let a = write_then_guard();
1270 let ev: Eval = &|id, _t, frame| match id.0 {
1273 0 => Ok(0x1f),
1274 1 => Ok(i64::from(read_local(frame, LocalVarId(0))? == 15)),
1275 _ => unreachable!(),
1276 };
1277 let r = attempt_with_locals(&a, 0, 3, ev).unwrap();
1278 assert_eq!(r.ends(), BTreeSet::from([1]));
1279 assert_eq!(
1280 r.matches,
1281 BTreeSet::from([(1, vec![Some(0xf)])]),
1282 "the frame at the match carries the cast value"
1283 );
1284 let ev: Eval = &|id, _t, frame| match id.0 {
1286 0 => Ok(0x1f),
1287 1 => Ok(i64::from(read_local(frame, LocalVarId(0))? == 0x1f)),
1288 _ => unreachable!(),
1289 };
1290 assert!(
1291 attempt_with_locals(&a, 0, 3, ev)
1292 .unwrap()
1293 .matches
1294 .is_empty()
1295 );
1296 }
1297
1298 #[test]
1299 fn reading_an_unassigned_slot_is_a_named_error() {
1300 let mut a = write_then_guard();
1301 a.states[0].edges[0].label = Transition::Epsilon;
1303 let ev: Eval = &|id, _t, frame| match id.0 {
1304 1 => Ok(i64::from(read_local(frame, LocalVarId(0))? == 15)),
1305 _ => unreachable!(),
1306 };
1307 assert_eq!(
1308 attempt_with_locals(&a, 0, 3, ev),
1309 Err(TraceError::UnassignedRead(LocalVarId(0)))
1310 );
1311 let e = TraceError::UnassignedRead(LocalVarId(3));
1312 assert!(e.to_string().contains("unassigned local variable 3"), "{e}");
1313 }
1314
1315 #[test]
1318 fn forked_threads_keep_independent_frames() {
1319 let mut a = Automaton::empty();
1320 let v0 = a.new_local_var(8, false);
1321 let s0 = a.start;
1322 let (l, r, end) = (a.new_state(), a.new_state(), a.new_state());
1323 a.add_edge(s0, None, Transition::Epsilon, l);
1324 a.add_edge(s0, None, Transition::Epsilon, r);
1325 for (from, leaf) in [(l, 0), (r, 1)] {
1326 a.add_edge(
1327 from,
1328 None,
1329 Transition::LocalVarWrite {
1330 var: v0,
1331 value: LeafId(leaf),
1332 },
1333 end,
1334 );
1335 }
1336 a.add_edge(end, None, Transition::Match { actions: vec![] }, end);
1337 let ev: Eval = &|id, _t, _f| Ok(10 + i64::from(id.0));
1338 let r = attempt_with_locals(&a, 0, 1, ev).unwrap();
1339 assert_eq!(
1340 r.matches,
1341 BTreeSet::from([(0, vec![Some(10)]), (0, vec![Some(11)])])
1342 );
1343 assert_eq!(r.ends(), BTreeSet::from([0]));
1344 }
1345
1346 #[test]
1349 fn a_local_copy_casts_to_the_destination_and_keeps_unassigned() {
1350 let mut a = Automaton::empty();
1351 let wide = a.new_local_var(8, true);
1352 let narrow = a.new_local_var(4, true);
1353 let never = a.new_local_var(8, true);
1354 let sink = a.new_local_var(8, true);
1355 let s0 = a.start;
1356 let (s1, s2, s3, s4) = (a.new_state(), a.new_state(), a.new_state(), a.new_state());
1357 a.add_edge(
1358 s0,
1359 None,
1360 Transition::LocalVarWrite {
1361 var: wide,
1362 value: LeafId(0),
1363 },
1364 s1,
1365 );
1366 a.add_edge(
1367 s1,
1368 None,
1369 Transition::LocalVarCopy {
1370 dst: narrow,
1371 src: wide,
1372 },
1373 s2,
1374 );
1375 a.add_edge(
1377 s2,
1378 None,
1379 Transition::LocalVarWrite {
1380 var: sink,
1381 value: LeafId(0),
1382 },
1383 s3,
1384 );
1385 a.add_edge(
1386 s3,
1387 None,
1388 Transition::LocalVarCopy {
1389 dst: sink,
1390 src: never,
1391 },
1392 s4,
1393 );
1394 a.add_edge(s4, None, Transition::Match { actions: vec![] }, s4);
1395 let ev: Eval = &|_, _, _| Ok(0x39);
1396 let r = attempt_with_locals(&a, 0, 1, ev).unwrap();
1397 assert_eq!(
1399 r.matches,
1400 BTreeSet::from([(0, vec![Some(0x39), Some(-7), None, None])])
1401 );
1402 }
1403
1404 #[test]
1406 fn the_plain_attempt_refuses_local_edges() {
1407 let a = write_then_guard();
1408 let tr = rows(&[]);
1409 assert_eq!(
1410 attempt(&a, 0, 3, &tr),
1411 Err(TraceError::UnsupportedEdge("LocalVarWrite"))
1412 );
1413 let mut b = Automaton::empty();
1414 let (x, y) = (b.new_local_var(1, false), b.new_local_var(1, false));
1415 let (s0, s1) = (b.start, b.new_state());
1416 b.add_edge(s0, None, Transition::LocalVarCopy { dst: x, src: y }, s1);
1417 assert_eq!(
1418 attempt(&b, 0, 3, &tr),
1419 Err(TraceError::UnsupportedEdge("LocalVarCopy"))
1420 );
1421 }
1422
1423 fn write_then_steps(width: u32, signed: bool, deltas: &[i64]) -> Automaton {
1427 let mut a = Automaton::empty();
1428 let v0 = a.new_local_var(width, signed);
1429 let mut cur = a.start;
1430 let next = a.new_state();
1431 a.add_edge(
1432 cur,
1433 None,
1434 Transition::LocalVarWrite {
1435 var: v0,
1436 value: LeafId(0),
1437 },
1438 next,
1439 );
1440 cur = next;
1441 for d in deltas {
1442 let next = a.new_state();
1443 a.add_edge(
1444 cur,
1445 None,
1446 Transition::LocalVarStep { var: v0, delta: *d },
1447 next,
1448 );
1449 cur = next;
1450 }
1451 a.add_edge(cur, None, Transition::Match { actions: vec![] }, cur);
1452 a
1453 }
1454
1455 #[test]
1456 fn a_step_adds_its_delta_and_wraps_at_the_slot_width() {
1457 let ev: Eval = &|_, _, _| Ok(14);
1458 let a = write_then_steps(4, false, &[1, 1, 1]);
1459 assert_eq!(
1461 attempt_with_locals(&a, 0, 1, ev).unwrap().matches,
1462 BTreeSet::from([(0, vec![Some(1)])])
1463 );
1464 let a = write_then_steps(4, true, &[1, 1]);
1465 assert_eq!(
1467 attempt_with_locals(&a, 0, 1, ev).unwrap().matches,
1468 BTreeSet::from([(0, vec![Some(0)])])
1469 );
1470 let a = write_then_steps(8, false, &[-1, -1]);
1471 assert_eq!(
1472 attempt_with_locals(&a, 0, 1, ev).unwrap().matches,
1473 BTreeSet::from([(0, vec![Some(12)])])
1474 );
1475 }
1476
1477 #[test]
1478 fn a_step_of_an_unassigned_slot_is_a_named_error() {
1479 let mut a = write_then_steps(8, false, &[1]);
1480 a.states[0].edges[0].label = Transition::Epsilon;
1481 let ev: Eval = &|_, _, _| Ok(0);
1482 assert_eq!(
1483 attempt_with_locals(&a, 0, 1, ev),
1484 Err(TraceError::UnassignedRead(LocalVarId(0)))
1485 );
1486 }
1487
1488 #[test]
1492 fn a_wide_local_is_written_stepped_and_copied_as_a_whole() {
1493 let mut a = Automaton::empty();
1494 let w = a.new_local_var(130, false);
1495 let w2 = a.new_local_var(130, false);
1496 let n = a.new_local_var(8, false);
1497 assert_eq!(
1498 (w.0, w2.0, n.0, a.local_vars.len()),
1499 (0, 6, 12, 13),
1500 "three limbs of value and of unknown plane per wide variable"
1501 );
1502 let mut cur = a.start;
1503 for slot in 0..6u32 {
1504 let next = a.new_state();
1505 a.add_edge(
1506 cur,
1507 None,
1508 Transition::LocalVarWrite {
1509 var: LocalVarId(slot),
1510 value: LeafId(slot),
1511 },
1512 next,
1513 );
1514 cur = next;
1515 }
1516 for t in [
1517 Transition::LocalVarStep { var: w, delta: 1 },
1518 Transition::LocalVarCopy { dst: w2, src: w },
1519 Transition::LocalVarCopy { dst: n, src: w },
1520 ] {
1521 let next = a.new_state();
1522 a.add_edge(cur, None, t, next);
1523 cur = next;
1524 }
1525 a.add_edge(cur, None, Transition::Match { actions: vec![] }, cur);
1526 let words = [-1i64, 1, 3, 0, 0, 0];
1528 let ev: Eval = &|id, _t, _f| Ok(words[id.0 as usize]);
1529 let r = attempt_with_locals(&a, 0, 1, ev).unwrap();
1530 let limbs = [Some(0), Some(2), Some(3), Some(0), Some(0), Some(0)];
1531 let mut frame = limbs.to_vec();
1532 frame.extend(limbs);
1533 frame.push(Some(0));
1534 assert_eq!(
1535 r.matches,
1536 BTreeSet::from([(0, frame)]),
1537 "!0 + 1 carries into limb 1; the copies take every limb, the narrow one the low byte"
1538 );
1539 }
1540
1541 #[test]
1543 fn a_step_of_a_wide_local_with_an_unknown_bit_is_unknown() {
1544 let mut a = Automaton::empty();
1545 let w = a.new_local_var(70, false);
1546 let mut cur = a.start;
1547 for slot in 0..4u32 {
1548 let next = a.new_state();
1549 a.add_edge(
1550 cur,
1551 None,
1552 Transition::LocalVarWrite {
1553 var: LocalVarId(slot),
1554 value: LeafId(slot),
1555 },
1556 next,
1557 );
1558 cur = next;
1559 }
1560 let next = a.new_state();
1561 a.add_edge(
1562 cur,
1563 None,
1564 Transition::LocalVarStep { var: w, delta: 1 },
1565 next,
1566 );
1567 a.add_edge(next, None, Transition::Match { actions: vec![] }, next);
1568 let words = [5i64, 0, 1, 0];
1569 let ev: Eval = &|id, _t, _f| Ok(words[id.0 as usize]);
1570 let r = attempt_with_locals(&a, 0, 1, ev).unwrap();
1571 assert_eq!(
1572 r.matches,
1573 BTreeSet::from([(0, vec![Some(0), Some(0), Some(-1), Some(0x3f)])])
1574 );
1575 }
1576
1577 fn two_calls() -> Automaton {
1580 let mut a = Automaton::empty();
1581 let s0 = a.start;
1582 let (s1, s2, s3, s4, s5) = (
1583 a.new_state(),
1584 a.new_state(),
1585 a.new_state(),
1586 a.new_state(),
1587 a.new_state(),
1588 );
1589 a.add_edge(s0, Some(LeafId(0)), Transition::Epsilon, s1);
1590 a.add_edge(
1591 s1,
1592 None,
1593 Transition::SubroutineCall {
1594 call: CallId(0),
1595 args: vec![CallArg::Value(LeafId(1)), CallArg::Ref, CallArg::Const],
1596 },
1597 s2,
1598 );
1599 a.add_edge(
1600 s2,
1601 None,
1602 Transition::Delay {
1603 min: 1,
1604 max: Some(1),
1605 },
1606 s3,
1607 );
1608 a.add_edge(s3, Some(LeafId(2)), Transition::Epsilon, s4);
1609 a.add_edge(
1610 s4,
1611 None,
1612 Transition::SubroutineCall {
1613 call: CallId(1),
1614 args: vec![],
1615 },
1616 s5,
1617 );
1618 a.add_edge(s5, None, Transition::Match { actions: vec![] }, s5);
1619 a
1620 }
1621
1622 #[test]
1623 fn a_call_is_recorded_with_its_sampled_arguments() {
1624 let a = two_calls();
1625 let ev: Eval = &|id, t, _| match id.0 {
1627 1 => Ok(100 + i64::from(t)),
1628 _ => Ok(1),
1629 };
1630 let r = attempt_with_calls(&a, 0, 3, ev).unwrap();
1631 assert_eq!(r.attempt.ends(), BTreeSet::from([1]));
1632 assert_eq!(
1633 r.calls,
1634 BTreeSet::from([
1635 CallEvent {
1636 tick: 0,
1637 call: CallId(0),
1638 args: vec![Some(100), None, None]
1639 },
1640 CallEvent {
1641 tick: 1,
1642 call: CallId(1),
1643 args: vec![]
1644 },
1645 ])
1646 );
1647 }
1648
1649 #[test]
1653 fn a_call_fires_even_when_the_longer_sequence_fails() {
1654 let a = two_calls();
1655 let ev: Eval = &|id, _, _| match id.0 {
1656 2 => Ok(0),
1657 _ => Ok(1),
1658 };
1659 let r = attempt_with_calls(&a, 0, 3, ev).unwrap();
1660 assert!(r.attempt.matches.is_empty());
1661 assert_eq!(
1662 r.calls.iter().map(|c| c.call).collect::<Vec<_>>(),
1663 vec![CallId(0)]
1664 );
1665 }
1666
1667 #[test]
1670 fn calls_and_writes_run_in_list_order() {
1671 let mut a = Automaton::empty();
1672 let v0 = a.new_local_var(8, false);
1673 let s0 = a.start;
1674 let (s1, s2, s3, s4) = (a.new_state(), a.new_state(), a.new_state(), a.new_state());
1675 a.add_edge(
1676 s0,
1677 None,
1678 Transition::LocalVarWrite {
1679 var: v0,
1680 value: LeafId(0),
1681 },
1682 s1,
1683 );
1684 a.add_edge(
1685 s1,
1686 None,
1687 Transition::SubroutineCall {
1688 call: CallId(0),
1689 args: vec![CallArg::Value(LeafId(1))],
1690 },
1691 s2,
1692 );
1693 a.add_edge(s2, None, Transition::LocalVarStep { var: v0, delta: 5 }, s3);
1694 a.add_edge(
1695 s3,
1696 None,
1697 Transition::SubroutineCall {
1698 call: CallId(1),
1699 args: vec![CallArg::Value(LeafId(1))],
1700 },
1701 s4,
1702 );
1703 a.add_edge(s4, None, Transition::Match { actions: vec![] }, s4);
1704 let ev: Eval = &|id, _, frame| match id.0 {
1706 0 => Ok(3),
1707 _ => read_local(frame, LocalVarId(0)),
1708 };
1709 let r = attempt_with_calls(&a, 0, 1, ev).unwrap();
1710 let by_call: Vec<_> = r.calls.iter().map(|c| (c.call.0, c.args[0])).collect();
1711 assert_eq!(by_call, vec![(0, Some(3)), (1, Some(8))]);
1712 assert_eq!(r.attempt.matches, BTreeSet::from([(0, vec![Some(8)])]));
1713 }
1714
1715 #[test]
1716 fn the_plain_attempt_refuses_call_edges() {
1717 let a = two_calls();
1718 let tr = rows(&[&[true], &[true], &[true]]);
1719 assert_eq!(
1720 attempt(&a, 0, 3, &tr),
1721 Err(TraceError::UnsupportedEdge("SubroutineCall"))
1722 );
1723 let a = write_then_steps(4, false, &[1]);
1724 assert_eq!(
1725 attempt(&a, 0, 3, &tr),
1726 Err(TraceError::UnsupportedEdge("LocalVarWrite"))
1727 );
1728 let mut b = Automaton::empty();
1729 let v = b.new_local_var(1, false);
1730 let (s0, s1) = (b.start, b.new_state());
1731 b.add_edge(s0, None, Transition::LocalVarStep { var: v, delta: 1 }, s1);
1732 assert_eq!(
1733 attempt(&b, 0, 3, &tr),
1734 Err(TraceError::UnsupportedEdge("LocalVarStep"))
1735 );
1736 }
1737
1738 #[test]
1739 fn abort_edges_are_refused_not_ignored() {
1740 let mut a = Automaton::empty();
1741 let s0 = a.start;
1742 let s1 = a.new_state();
1743 a.add_edge(
1744 s0,
1745 None,
1746 Transition::Abort {
1747 leaf: LeafId(0),
1748 mode: AbortMode::Accept,
1749 },
1750 s1,
1751 );
1752 let tr = rows(&[]);
1753 assert_eq!(
1754 attempt(&a, 0, 2, &tr),
1755 Err(TraceError::UnsupportedEdge("Abort"))
1756 );
1757 let ev: Eval = &|_, _, _| Ok(1);
1759 assert_eq!(
1760 attempt_with_locals(&a, 0, 2, ev),
1761 Err(TraceError::UnsupportedEdge("Abort"))
1762 );
1763 }
1764
1765 #[test]
1766 fn start_outside_the_trace_matches_nothing_and_admits_empty_passes_through() {
1767 let mut a = a_then_b();
1768 a.admits_empty = true;
1769 let tr = rows(&[&[true], &[true]]);
1770 let r = attempt(&a, 5, 3, &tr).unwrap();
1771 assert!(r.ends.is_empty());
1772 assert!(r.admits_empty);
1773 }
1774
1775 fn one_tick(leaf: u32) -> Automaton {
1777 let mut a = Automaton::empty();
1778 let s0 = a.start;
1779 let s1 = a.new_state();
1780 a.add_edge(s0, Some(LeafId(leaf)), Transition::Epsilon, s1);
1781 a.add_edge(s1, None, Transition::Match { actions: vec![] }, s1);
1782 a
1783 }
1784
1785 fn tests_end_point(
1788 method: EndMethod,
1789 sub: Automaton,
1790 flow_out: Vec<(LocalVarId, LocalVarId)>,
1791 flag: Option<LocalVarId>,
1792 then: Option<LeafId>,
1793 ) -> Automaton {
1794 let mut a = Automaton::empty();
1795 a.local_vars = sub.local_vars.clone();
1796 let p = a.new_end_point(EndPoint {
1797 method,
1798 site: 0,
1799 automaton: sub,
1800 flow_out,
1801 });
1802 let s0 = a.start;
1803 let (s1, s2) = (a.new_state(), a.new_state());
1804 a.add_edge(s0, None, Transition::EndPoint { point: p, flag }, s1);
1805 a.add_edge(s1, then, Transition::Epsilon, s2);
1806 a.add_edge(s2, None, Transition::Match { actions: vec![] }, s2);
1807 a
1808 }
1809
1810 #[test]
1814 fn triggered_holds_where_the_instance_ends_whatever_the_start() {
1815 let a = tests_end_point(EndMethod::Triggered, a_then_b(), vec![], None, None);
1816 let ev: Eval = &|id, t, _| {
1817 let rows: [[i64; 4]; 2] = [[1, 0, 1, 0], [0, 1, 0, 0]];
1818 Ok(rows[id.0 as usize][t as usize])
1819 };
1820 for start in 0..4 {
1821 let r = attempt_with_locals(&a, start, 4, ev).unwrap();
1822 let want: BTreeSet<u32> = if start == 1 {
1823 BTreeSet::from([1])
1824 } else {
1825 BTreeSet::new()
1826 };
1827 assert_eq!(r.ends(), want, "start {start}");
1828 }
1829 }
1830
1831 #[test]
1834 fn an_empty_match_never_triggers() {
1835 let mut sub = one_tick(0);
1836 sub.admits_empty = true;
1837 let a = tests_end_point(EndMethod::Triggered, sub, vec![], None, None);
1838 let never: Eval = &|_, _, _| Ok(0);
1839 for start in 0..3 {
1840 assert!(
1841 attempt_with_locals(&a, start, 3, never)
1842 .unwrap()
1843 .matches
1844 .is_empty(),
1845 "empty match fired the end point at start {start}"
1846 );
1847 }
1848 let once: Eval = &|_, t, _| Ok(i64::from(t == 1));
1849 assert_eq!(
1850 attempt_with_locals(&a, 1, 3, once).unwrap().ends(),
1851 BTreeSet::from([1])
1852 );
1853 }
1854
1855 #[test]
1858 fn the_flag_form_stores_the_status_and_never_blocks() {
1859 let mut sub = one_tick(0);
1860 let flag = sub.new_local_var(1, false);
1861 let a = tests_end_point(
1862 EndMethod::Triggered,
1863 sub,
1864 vec![],
1865 Some(flag),
1866 Some(LeafId(1)),
1867 );
1868 let ev: Eval = &|id, t, frame| match id.0 {
1870 0 => Ok(i64::from(t == 0 || t == 2)),
1871 _ => Ok(i64::from(read_local(frame, LocalVarId(0))? == 0)),
1872 };
1873 let r = attempt_with_locals(&a, 0, 4, ev).unwrap();
1874 assert!(r.matches.is_empty(), "tick 0 is triggered, `!flag` fails");
1875 let r = attempt_with_locals(&a, 1, 4, ev).unwrap();
1876 assert_eq!(r.matches, BTreeSet::from([(1, vec![Some(0)])]));
1877 let r = attempt_with_locals(&a, 3, 4, ev).unwrap();
1878 assert_eq!(r.matches, BTreeSet::from([(3, vec![Some(0)])]));
1879 let r = attempt_with_locals(&a, 2, 4, ev).unwrap();
1880 assert!(r.matches.is_empty());
1881 }
1882
1883 #[test]
1886 fn a_maximal_application_forks_per_frame_and_flows_variables_out() {
1887 let mut sub = Automaton::empty();
1888 let dst = sub.new_local_var(8, false); let src = sub.new_local_var(8, false); let s0 = sub.start;
1891 let (w1, w2, m) = (sub.new_state(), sub.new_state(), sub.new_state());
1892 sub.add_edge(s0, None, Transition::Epsilon, w1);
1893 sub.add_edge(s0, None, Transition::Epsilon, w2);
1894 sub.add_edge(
1895 w1,
1896 None,
1897 Transition::LocalVarWrite {
1898 var: src,
1899 value: LeafId(0),
1900 },
1901 m,
1902 );
1903 sub.add_edge(
1904 w2,
1905 None,
1906 Transition::LocalVarWrite {
1907 var: src,
1908 value: LeafId(1),
1909 },
1910 m,
1911 );
1912 sub.add_edge(m, None, Transition::Match { actions: vec![] }, m);
1913 let a = tests_end_point(EndMethod::Triggered, sub, vec![(src, dst)], None, None);
1914 let ev: Eval = &|id, t, _| Ok(10 * (i64::from(id.0) + 1) + i64::from(t));
1915 let r = attempt_with_locals(&a, 2, 3, ev).unwrap();
1916 assert_eq!(
1917 r.matches,
1918 BTreeSet::from([(2, vec![Some(12), None]), (2, vec![Some(22), None]),]),
1919 "two frames, two threads; the instance's own slot stays out of the thread"
1920 );
1921 }
1922
1923 #[test]
1926 fn matched_hands_off_across_clocks() {
1927 let a = tests_end_point(EndMethod::Matched, one_tick(0), vec![], None, None);
1928 let clocks = TraceClocks {
1929 dest: vec![10, 20, 30],
1930 source: vec![5, 12, 18, 25, 31],
1931 };
1932 let ev: Eval = &|_, t, _| Ok(i64::from(matches!(t, 0..=2 | 4)));
1934 let ends = |start| {
1935 attempt_with_clocks(&a, start, &clocks, ev)
1936 .unwrap()
1937 .attempt
1938 .ends()
1939 };
1940 assert_eq!(ends(0), BTreeSet::from([0]), "t=5 <= 10");
1941 assert_eq!(ends(1), BTreeSet::from([1]), "t=12 and t=18 in (10, 20]");
1942 assert!(
1943 ends(2).is_empty(),
1944 "t=25 does not hold, t=31 is past tick 30"
1945 );
1946 let at_tick = TraceClocks {
1948 dest: vec![10, 20],
1949 source: vec![10, 15],
1950 };
1951 let only_first: Eval = &|_, t, _| Ok(i64::from(t == 0));
1952 let r = attempt_with_clocks(&a, 0, &at_tick, only_first).unwrap();
1953 assert_eq!(r.attempt.ends(), BTreeSet::from([0]));
1954 let r = attempt_with_clocks(&a, 1, &at_tick, only_first).unwrap();
1956 assert!(r.attempt.matches.is_empty());
1957 }
1958
1959 #[test]
1960 fn matched_needs_a_clock_model_and_does_not_nest() {
1961 let a = tests_end_point(EndMethod::Matched, one_tick(0), vec![], None, None);
1962 let ev: Eval = &|_, _, _| Ok(1);
1963 assert!(attempt_with_locals(&a, 0, 2, ev).is_ok());
1966 let clocks = TraceClocks {
1967 dest: vec![1, 2],
1968 source: vec![1, 2],
1969 };
1970 let inner = tests_end_point(EndMethod::Matched, one_tick(0), vec![], None, None);
1971 let outer = tests_end_point(EndMethod::Matched, inner, vec![], None, None);
1972 assert_eq!(
1973 attempt_with_clocks(&outer, 0, &clocks, ev),
1974 Err(TraceError::NestedMatched)
1975 );
1976 let bad = TraceClocks {
1977 dest: vec![2, 2],
1978 source: vec![1],
1979 };
1980 assert_eq!(
1981 attempt_with_clocks(&a, 0, &bad, ev),
1982 Err(TraceError::BadClocks)
1983 );
1984 }
1985
1986 #[test]
1988 fn calls_inside_an_instance_are_reported() {
1989 let a = tests_end_point(EndMethod::Triggered, two_calls(), vec![], None, None);
1990 let ev: Eval = &|_, _, _| Ok(1);
1991 let r = attempt_with_calls(&a, 1, 3, ev).unwrap();
1992 assert_eq!(r.attempt.ends(), BTreeSet::from([1]));
1993 assert!(r.calls.iter().any(|c| c.call == CallId(1)));
1994 }
1995
1996 #[test]
1997 fn the_plain_attempt_refuses_end_point_edges() {
1998 let a = tests_end_point(EndMethod::Triggered, one_tick(0), vec![], None, None);
1999 let tr = rows(&[&[true]]);
2000 assert_eq!(
2001 attempt(&a, 0, 1, &tr),
2002 Err(TraceError::UnsupportedEdge("EndPoint"))
2003 );
2004 }
2005
2006 #[test]
2007 fn display_names_the_group() {
2008 let e = TraceError::EndWithoutBegin(FirstMatchId(3));
2009 assert!(e.to_string().contains("FirstMatchEnd(3)"), "{e}");
2010 let e = TraceError::BeginWhileOpen(FirstMatchId(4));
2011 assert!(e.to_string().contains("FirstMatchBegin(4)"), "{e}");
2012 let e = TraceError::UnsupportedEdge("Abort");
2013 assert!(e.to_string().contains("Abort"), "{e}");
2014 }
2015
2016 #[test]
2017 fn tail_word_letters_flip_and_cut() {
2018 let w = TailWord::weak(2);
2019 assert_eq!(w.letter(1), None);
2020 assert_eq!(w.letter(2), Some(true));
2021 assert_eq!(w.letter(99), Some(true));
2022 assert_eq!(w.extent(), 2);
2023 let s = w.flip();
2024 assert_eq!(s, TailWord::strong(2));
2025 assert_eq!(s.letter(5), Some(false));
2026 let cut = s.cut_then_top(4);
2028 assert_eq!(cut.letter(2), Some(false));
2029 assert_eq!(cut.letter(3), Some(false));
2030 assert_eq!(cut.letter(4), Some(true));
2031 assert_eq!(cut.extent(), 4);
2032 assert_eq!(cut.flip().letter(2), Some(true), "flip swaps mid letters");
2033 assert_eq!(cut.flip().letter(4), Some(false));
2034 assert_eq!(s.cut_then_top(1), TailWord::weak(1));
2036 assert_eq!(w.cut_then_top(5), TailWord::weak(2));
2038 }
2039
2040 #[test]
2042 fn attempt_word_guards_follow_the_tail_letter() {
2043 let a = a_then_b();
2044 let table: &[&[bool]] = &[&[true], &[false]];
2045 let eval = |l: LeafId, t: u32, _: &[Option<i64>]| -> Result<i64, TraceError> {
2046 Ok(i64::from(
2047 table[l.0 as usize]
2048 .get(t as usize)
2049 .copied()
2050 .unwrap_or(false),
2051 ))
2052 };
2053 let run = |w: &TailWord| {
2054 attempt_word(&a, a.start, &[], 0, w, 6, &eval)
2055 .unwrap()
2056 .into_iter()
2057 .map(|(e, _)| e)
2058 .collect::<Vec<_>>()
2059 };
2060 assert_eq!(run(&TailWord::weak(1)), vec![1]);
2062 assert_eq!(run(&TailWord::strong(1)), Vec::<u32>::new());
2064 let on_tail = attempt_word(&a, a.start, &[], 3, &TailWord::weak(1), 6, &eval).unwrap();
2066 assert_eq!(
2067 on_tail.into_iter().map(|(e, _)| e).collect::<Vec<_>>(),
2068 vec![4]
2069 );
2070 }
2071
2072 #[test]
2076 fn attempt_word_calls_records_real_ticks_only() {
2077 let a = two_calls();
2078 let eval = |l: LeafId, t: u32, _: &[Option<i64>]| -> Result<i64, TraceError> {
2079 Ok(match l.0 {
2080 1 => 100 + i64::from(t),
2081 _ => 1,
2082 })
2083 };
2084 let run = |real: u32| {
2085 attempt_word_calls(
2086 &a,
2087 a.start,
2088 &[],
2089 0,
2090 &TailWord::weak(real),
2091 6,
2092 &eval,
2093 None,
2094 a.entry_clock,
2095 )
2096 .unwrap()
2097 };
2098 let (matches, calls) = run(1);
2099 assert_eq!(
2100 matches.into_iter().map(|(e, _)| e).collect::<Vec<_>>(),
2101 vec![1]
2102 );
2103 assert_eq!(
2104 calls.into_iter().collect::<Vec<_>>(),
2105 vec![CallEvent {
2106 tick: 0,
2107 call: CallId(0),
2108 args: vec![Some(100), None, None]
2109 }],
2110 "the second call would fire on tick 1, a `⊤` letter"
2111 );
2112 let (_, calls) = run(3);
2113 assert_eq!(
2114 calls.iter().map(|c| (c.tick, c.call.0)).collect::<Vec<_>>(),
2115 vec![(0, 0), (1, 1)]
2116 );
2117 let plain = attempt_word(&a, a.start, &[], 0, &TailWord::weak(3), 6, &eval).unwrap();
2119 assert_eq!(plain, run(3).0);
2120 }
2121
2122 #[test]
2124 fn attempt_word_starts_at_the_given_entry_with_the_given_frame() {
2125 let mut a = Automaton::empty();
2126 let v = a.new_local_var(8, false);
2127 let entry = a.new_state();
2128 let end = a.new_state();
2129 a.add_edge(entry, None, Transition::Match { actions: vec![] }, entry);
2130 let _ = end;
2131 let eval = |_: LeafId, _: u32, _: &[Option<i64>]| Ok(1);
2132 let frame = vec![Some(7)];
2133 let got = attempt_word(&a, entry, &frame, 0, &TailWord::weak(1), 3, &eval).unwrap();
2134 assert_eq!(
2135 got.into_iter().collect::<Vec<_>>(),
2136 vec![(0, vec![Some(7)])]
2137 );
2138 assert_eq!(v, LocalVarId(0));
2139 }
2140
2141 const C0: ClockId = ClockId(0);
2144 const C1: ClockId = ClockId(1);
2145
2146 #[test]
2147 fn timeline_counts_strictly_subsequent_ticks_and_continues_into_the_tail() {
2148 let tl = Timeline::new(6, vec![vec![0, 2, 4], vec![1, 2]]).unwrap();
2150 assert_eq!(tl.real(), 6);
2151 assert!(tl.is_tick(C0, 2) && !tl.is_tick(C0, 3) && tl.is_tick(C1, 1));
2152 assert_eq!(tl.kth_after(C1, 0, 1), 1);
2154 assert_eq!(
2155 tl.kth_after(C1, 1, 1),
2156 2,
2157 "strictly later: not tick 1 itself"
2158 );
2159 assert_eq!(
2160 tl.kth_after(C1, 2, 1),
2161 6,
2162 "list exhausted: the tail starts at `real`"
2163 );
2164 assert_eq!(tl.kth_after(C0, 0, 2), 4);
2165 assert_eq!(tl.kth_after(C0, 4, 3), 8, "every tail position is a tick");
2166 assert_eq!(
2167 tl.kth_after(C0, 9, 2),
2168 11,
2169 "past `real` every position ticks"
2170 );
2171 assert_eq!(tl.kth_after(C0, 3, 0), 3, "k = 0 is the position itself");
2172 assert_eq!(tl.next_tick(C0, 2), 2);
2174 assert_eq!(tl.next_tick(C0, 3), 4);
2175 assert_eq!(tl.next_tick(C0, 5), 6);
2176 assert_eq!(tl.next_tick(C1, 7), 7);
2177 assert_eq!(tl.kth_after(ClockId(5), 1, 1), 2);
2179 assert_eq!(tl.next_tick(ClockId(5), 3), 3);
2180 let rows = Timeline::from_rows(&[&[true, false, true], &[false, true]]);
2181 assert_eq!(rows.real(), 3);
2182 assert!(rows.is_tick(C1, 1) && !rows.is_tick(C1, 0));
2183 }
2184
2185 #[test]
2186 fn a_malformed_timeline_is_a_named_error() {
2187 assert_eq!(
2188 Timeline::new(4, vec![vec![2, 2]]),
2189 Err(TraceError::BadTimeline)
2190 );
2191 assert_eq!(
2192 Timeline::new(4, vec![vec![1, 4]]),
2193 Err(TraceError::BadTimeline),
2194 "a tick must be a real step"
2195 );
2196 assert!(
2197 TraceError::BadTimeline
2198 .to_string()
2199 .contains("strictly increasing")
2200 );
2201 }
2202
2203 fn cross_clock(strict: bool) -> Automaton {
2206 let mut a = Automaton::empty();
2207 let s0 = a.start;
2208 let (s1, s2, s3) = (a.new_state(), a.new_state(), a.new_state());
2209 a.add_edge(s0, Some(LeafId(0)), Transition::Epsilon, s1);
2210 a.add_edge(s1, None, Transition::ClockSync { to: C1, strict }, s2);
2211 a.add_edge(s2, Some(LeafId(1)), Transition::Epsilon, s3);
2212 a.add_edge(s3, None, Transition::Match { actions: vec![] }, s3);
2213 a
2214 }
2215
2216 fn ends_on(
2218 a: &Automaton,
2219 start: u32,
2220 tl: &Timeline,
2221 leaves: &'static [&'static [bool]],
2222 ) -> Vec<u32> {
2223 let ev = move |l: LeafId, t: u32, _: &[Option<i64>]| -> Result<i64, TraceError> {
2224 Ok(i64::from(
2225 leaves[l.0 as usize]
2226 .get(t as usize)
2227 .copied()
2228 .unwrap_or(false),
2229 ))
2230 };
2231 attempt_word_in(
2232 a,
2233 a.start,
2234 &[],
2235 start,
2236 &TailWord::weak(tl.real()),
2237 tl.real(),
2238 &ev,
2239 Some(tl),
2240 C0,
2241 )
2242 .unwrap()
2243 .into_iter()
2244 .map(|(e, _)| e)
2245 .collect()
2246 }
2247
2248 #[test]
2250 fn strict_clock_sync_waits_for_the_nearest_strictly_later_tick() {
2251 let tl = Timeline::from_rows(&[
2253 &[true, false, false, false, true],
2254 &[true, false, true, true, false],
2255 ]);
2256 let leaves: &'static [&'static [bool]] = &[
2259 &[true, false, false, false, false],
2260 &[true, false, false, true, false],
2261 ];
2262 assert!(
2263 ends_on(&cross_clock(true), 0, &tl, leaves).is_empty(),
2264 "tick 2 is the target and l1 is false there; tick 0 and 3 are not read"
2265 );
2266 let leaves: &'static [&'static [bool]] = &[
2267 &[true, false, false, false, false],
2268 &[true, false, true, true, false],
2269 ];
2270 assert_eq!(ends_on(&cross_clock(true), 0, &tl, leaves), vec![2]);
2271 }
2272
2273 #[test]
2275 fn nonstrict_clock_sync_may_overlap_the_current_tick() {
2276 let tl = Timeline::from_rows(&[
2277 &[true, false, false, false, true],
2278 &[true, false, true, true, false],
2279 ]);
2280 let leaves: &'static [&'static [bool]] = &[
2281 &[true, false, false, false, false],
2282 &[true, false, false, false, false],
2283 ];
2284 assert_eq!(
2285 ends_on(&cross_clock(false), 0, &tl, leaves),
2286 vec![0],
2287 "tick 0 is a tick of both clocks: the match overlaps"
2288 );
2289 let tl = Timeline::from_rows(&[&[true, false, false, false], &[false, false, true, false]]);
2292 let leaves: &'static [&'static [bool]] =
2293 &[&[true, false, false, false], &[false, false, true, false]];
2294 assert_eq!(ends_on(&cross_clock(false), 0, &tl, leaves), vec![2]);
2295 assert_eq!(ends_on(&cross_clock(true), 0, &tl, leaves), vec![2]);
2296 }
2297
2298 #[test]
2300 fn an_attempt_aligns_its_start_to_the_entry_clock() {
2301 let a = a_then_b();
2302 let tl = Timeline::from_rows(&[&[false, false, true, false, true, false]]);
2303 let leaves: &'static [&'static [bool]] = &[
2304 &[false, false, true, false, false, false],
2305 &[false, false, false, false, true, false],
2306 ];
2307 assert_eq!(ends_on(&a, 0, &tl, leaves), vec![4]);
2310 }
2311
2312 #[test]
2314 fn delay_counts_ticks_of_the_threads_current_clock() {
2315 let mut a = cross_clock(true);
2317 let m = ThreadStateId(3);
2318 let (d, g, e) = (a.new_state(), a.new_state(), a.new_state());
2319 a.states[m.0 as usize].edges.clear();
2320 a.add_edge(
2321 m,
2322 None,
2323 Transition::Delay {
2324 min: 2,
2325 max: Some(2),
2326 },
2327 d,
2328 );
2329 a.add_edge(d, Some(LeafId(2)), Transition::Epsilon, g);
2330 a.add_edge(g, None, Transition::Match { actions: vec![] }, g);
2331 let _ = e;
2332 let tl = Timeline::from_rows(&[
2334 &[true, false, false, false, false, false, false],
2335 &[false, true, false, true, true, false, true],
2336 ]);
2337 let leaves: &'static [&'static [bool]] = &[
2338 &[true, false, false, false, false, false, false],
2339 &[false, true, false, false, false, false, false],
2340 &[false, false, false, false, true, false, true],
2341 ];
2342 assert_eq!(ends_on(&a, 0, &tl, leaves), vec![4]);
2345 }
2346
2347 #[test]
2349 fn without_a_timeline_a_clock_sync_is_plain_delay() {
2350 let table: &'static [&'static [bool]] = &[&[true, false, false], &[false, true, true]];
2351 let tr = rows(table);
2352 let strict = attempt(&cross_clock(true), 0, 3, &tr).unwrap();
2353 assert_eq!(strict.ends, BTreeSet::from([1]));
2354 let loose = attempt(&cross_clock(false), 0, 3, &tr).unwrap();
2355 assert!(
2356 loose.ends.is_empty(),
2357 "##0 reads l1 at tick 0, where it is false"
2358 );
2359 }
2360}