use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::fmt;
use super::ir::{
Automaton, CallArg, CallId, ClockId, EndMethod, EndPoint, FirstMatchId, LeafId, LocalVarId,
ThreadStateId, Transition,
};
use super::wide;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Attempt {
pub admits_empty: bool,
pub ends: BTreeSet<u32>,
}
pub type Frame = Vec<Option<i64>>;
pub type Matches = smallvec::SmallVec<[(u32, Frame); 1]>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LocalAttempt {
pub admits_empty: bool,
pub matches: BTreeSet<(u32, Frame)>,
}
impl LocalAttempt {
pub fn ends(&self) -> BTreeSet<u32> {
self.matches.iter().map(|(t, _)| *t).collect()
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct CallEvent {
pub tick: u32,
pub call: CallId,
pub args: Vec<Option<i64>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CallAttempt {
pub attempt: LocalAttempt,
pub calls: BTreeSet<CallEvent>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TraceError {
UnsupportedEdge(&'static str),
UnassignedRead(LocalVarId),
EndWithoutBegin(FirstMatchId),
BeginWhileOpen(FirstMatchId),
MatchedWithoutClocks,
NestedMatched,
BadClocks,
BadTimeline,
NotASequence,
}
impl fmt::Display for TraceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TraceError::NotASequence => write!(
f,
"sva trace interpreter: the matches of a property whose root is not a sequence \
were asked for"
),
TraceError::BadTimeline => write!(
f,
"sva trace interpreter: a timeline's tick lists must be strictly increasing and \
inside the trace"
),
TraceError::MatchedWithoutClocks => write!(
f,
"sva trace interpreter: a `.matched` end point needs a clock model \
(attempt_with_clocks)"
),
TraceError::NestedMatched => write!(
f,
"sva trace interpreter: a `.matched` inside an instance run on another clock is \
not modelled"
),
TraceError::BadClocks => write!(
f,
"sva trace interpreter: the clock model's tick times must be strictly increasing"
),
TraceError::UnsupportedEdge(kind) => {
write!(f, "sva trace interpreter: {kind} edges are not interpreted")
}
TraceError::EndWithoutBegin(g) => write!(
f,
"sva trace interpreter: FirstMatchEnd({}) reached without its FirstMatchBegin",
g.0
),
TraceError::BeginWhileOpen(g) => write!(
f,
"sva trace interpreter: FirstMatchBegin({}) reached while the group is open",
g.0
),
TraceError::UnassignedRead(v) => write!(
f,
"sva trace interpreter: read of unassigned local variable {}",
v.0
),
}
}
}
impl std::error::Error for TraceError {}
pub fn read_local(frame: &[Option<i64>], var: LocalVarId) -> Result<i64, TraceError> {
frame
.get(var.0 as usize)
.copied()
.flatten()
.ok_or(TraceError::UnassignedRead(var))
}
type Config = (u32, u32, ClockId, Vec<(FirstMatchId, u32)>, Frame);
type Eval<'a> = &'a dyn Fn(LeafId, u32, &[Option<i64>]) -> Result<i64, TraceError>;
pub fn attempt(
au: &Automaton,
start: u32,
len: u32,
leaf: &dyn Fn(LeafId, u32) -> bool,
) -> Result<Attempt, TraceError> {
let matches = run(
au,
start,
len,
&|id, tick, _| Ok(i64::from(leaf(id, tick))),
false,
)?;
Ok(Attempt {
admits_empty: au.admits_empty,
ends: matches.0.into_iter().map(|(t, _)| t).collect(),
})
}
pub fn attempt_with_locals(
au: &Automaton,
start: u32,
len: u32,
eval: Eval<'_>,
) -> Result<LocalAttempt, TraceError> {
Ok(LocalAttempt {
admits_empty: au.admits_empty,
matches: run(au, start, len, eval, true)?.0,
})
}
pub fn attempt_with_calls(
au: &Automaton,
start: u32,
len: u32,
eval: Eval<'_>,
) -> Result<CallAttempt, TraceError> {
let (matches, calls) = run(au, start, len, eval, true)?;
Ok(CallAttempt {
attempt: LocalAttempt {
admits_empty: au.admits_empty,
matches,
},
calls,
})
}
pub fn attempt_with_clocks(
au: &Automaton,
start: u32,
clocks: &TraceClocks,
eval: Eval<'_>,
) -> Result<CallAttempt, TraceError> {
let strictly_increasing = |v: &[u64]| v.windows(2).all(|w| w[0] < w[1]);
if !strictly_increasing(&clocks.dest) || !strictly_increasing(&clocks.source) {
return Err(TraceError::BadClocks);
}
let mut runner = Runner {
eval,
locals: true,
len: clocks.dest.len() as u32,
clocks: Some(clocks),
timeline: None,
status: HashMap::new(),
calls: BTreeSet::new(),
word: None,
};
let matches = runner.attempt(au, start, Space::Dest)?;
Ok(CallAttempt {
attempt: LocalAttempt {
admits_empty: au.admits_empty,
matches,
},
calls: runner.calls,
})
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct TailWord {
pub real: u32,
pub mid: Vec<bool>,
pub tail_top: bool,
}
impl TailWord {
pub fn weak(real: u32) -> Self {
TailWord {
real,
mid: Vec::new(),
tail_top: true,
}
}
pub fn strong(real: u32) -> Self {
TailWord {
real,
mid: Vec::new(),
tail_top: false,
}
}
pub fn extent(&self) -> u32 {
self.real + self.mid.len() as u32
}
pub fn letter(&self, tick: u32) -> Option<bool> {
if tick < self.real {
None
} else {
Some(
self.mid
.get((tick - self.real) as usize)
.copied()
.unwrap_or(self.tail_top),
)
}
}
pub fn flip(&self) -> Self {
TailWord {
real: self.real,
mid: self.mid.iter().map(|t| !t).collect(),
tail_top: !self.tail_top,
}
}
pub fn cut_then_top(&self, n: u32) -> Self {
if n <= self.real {
return TailWord::weak(n);
}
let keep = (n - self.real) as usize;
let mid = (0..keep)
.map(|i| self.mid.get(i).copied().unwrap_or(self.tail_top))
.collect();
let mut w = TailWord {
real: self.real,
mid,
tail_top: true,
};
while w.mid.last() == Some(&true) {
w.mid.pop();
}
w
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Timeline {
real: u32,
ticks: Vec<Vec<u32>>,
}
impl Timeline {
pub fn new(real: u32, ticks: Vec<Vec<u32>>) -> Result<Self, TraceError> {
let ok = ticks
.iter()
.all(|l| l.windows(2).all(|w| w[0] < w[1]) && l.iter().all(|&t| t < real));
if ok {
Ok(Timeline { real, ticks })
} else {
Err(TraceError::BadTimeline)
}
}
pub fn from_rows(rows: &[&[bool]]) -> Self {
let real = rows.iter().map(|r| r.len() as u32).max().unwrap_or(0);
let ticks = rows
.iter()
.map(|r| {
r.iter()
.enumerate()
.filter(|&(_, &b)| b)
.map(|(t, _)| t as u32)
.collect()
})
.collect();
Timeline { real, ticks }
}
pub fn real(&self) -> u32 {
self.real
}
pub fn truncated(&self, n: u32) -> Timeline {
let real = n.min(self.real);
Timeline {
real,
ticks: self
.ticks
.iter()
.map(|l| l.iter().copied().take_while(|&t| t < real).collect())
.collect(),
}
}
fn list(&self, c: ClockId) -> Option<&[u32]> {
self.ticks.get(c.0 as usize).map(Vec::as_slice)
}
pub fn is_tick(&self, c: ClockId, p: u32) -> bool {
match self.list(c) {
_ if p >= self.real => true,
None => true,
Some(l) => l.binary_search(&p).is_ok(),
}
}
pub fn next_tick(&self, c: ClockId, p: u32) -> u32 {
match self.list(c) {
_ if p >= self.real => p,
None => p,
Some(l) => l
.get(l.partition_point(|&t| t < p))
.copied()
.unwrap_or(self.real),
}
}
pub fn kth_after(&self, c: ClockId, p: u32, k: u32) -> u32 {
if k == 0 {
return p;
}
match self.list(c) {
_ if p >= self.real => p + k,
None => p + k,
Some(l) => {
let at_or_before = l.partition_point(|&t| t <= p);
let j = at_or_before + k as usize - 1;
match l.get(j) {
Some(&t) => t,
None => self.real + (j - l.len()) as u32,
}
}
}
}
}
pub fn attempt_word(
au: &Automaton,
entry: ThreadStateId,
frame0: &[Option<i64>],
start: u32,
word: &TailWord,
horizon: u32,
eval: Eval<'_>,
) -> Result<BTreeSet<(u32, Frame)>, TraceError> {
attempt_word_in(
au,
entry,
frame0,
start,
word,
horizon,
eval,
None,
au.entry_clock,
)
}
#[allow(clippy::too_many_arguments)]
pub fn attempt_word_in(
au: &Automaton,
entry: ThreadStateId,
frame0: &[Option<i64>],
start: u32,
word: &TailWord,
horizon: u32,
eval: Eval<'_>,
timeline: Option<&Timeline>,
dom: ClockId,
) -> Result<BTreeSet<(u32, Frame)>, TraceError> {
attempt_word_calls(au, entry, frame0, start, word, horizon, eval, timeline, dom)
.map(|(matches, _)| matches)
}
#[allow(clippy::too_many_arguments)]
pub fn attempt_word_calls(
au: &Automaton,
entry: ThreadStateId,
frame0: &[Option<i64>],
start: u32,
word: &TailWord,
horizon: u32,
eval: Eval<'_>,
timeline: Option<&Timeline>,
dom: ClockId,
) -> Result<RunResult, TraceError> {
let mut runner = Runner {
eval,
locals: true,
len: horizon,
clocks: None,
timeline,
status: HashMap::new(),
calls: BTreeSet::new(),
word: Some(word),
};
let matches = runner.attempt_from(au, entry, frame0.to_vec(), start, dom, Space::Dest)?;
Ok((matches, runner.calls))
}
pub type RunResult = (BTreeSet<(u32, Frame)>, BTreeSet<CallEvent>);
fn run(
au: &Automaton,
start: u32,
len: u32,
eval: Eval<'_>,
locals: bool,
) -> Result<RunResult, TraceError> {
let mut runner = Runner {
eval,
locals,
len,
clocks: None,
timeline: None,
status: HashMap::new(),
calls: BTreeSet::new(),
word: None,
};
let matches = runner.attempt(au, start, Space::Dest)?;
Ok((matches, runner.calls))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TraceClocks {
pub dest: Vec<u64>,
pub source: Vec<u64>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
enum Space {
Dest,
Source,
}
type EndStatus = BTreeMap<u32, BTreeSet<Frame>>;
struct Runner<'a> {
eval: Eval<'a>,
locals: bool,
len: u32,
clocks: Option<&'a TraceClocks>,
timeline: Option<&'a Timeline>,
status: HashMap<(usize, Space), EndStatus>,
calls: BTreeSet<CallEvent>,
word: Option<&'a TailWord>,
}
impl Runner<'_> {
fn len_of(&self, space: Space) -> u32 {
match space {
Space::Dest => self.len,
Space::Source => self.clocks.map_or(0, |c| c.source.len() as u32),
}
}
fn free_run(&mut self, au: &Automaton, space: Space) -> Result<EndStatus, TraceError> {
let mut out = EndStatus::new();
for start in 0..self.len_of(space) {
if space == Space::Dest
&& self
.timeline
.is_some_and(|t| !t.is_tick(au.entry_clock, start))
{
continue;
}
for (end, frame) in self.attempt(au, start, space)? {
out.entry(end).or_default().insert(frame);
}
}
Ok(out)
}
fn end_status(&mut self, ep: &EndPoint, space: Space) -> Result<&EndStatus, TraceError> {
let key = (std::ptr::from_ref(ep) as usize, space);
if !self.status.contains_key(&key) {
let computed = self.compute_status(ep, space)?;
self.status.insert(key, computed);
}
Ok(&self.status[&key])
}
fn compute_status(&mut self, ep: &EndPoint, space: Space) -> Result<EndStatus, TraceError> {
match (ep.method, space) {
(EndMethod::Triggered, _) => self.free_run(&ep.automaton, space),
(EndMethod::Matched, Space::Source) => Err(TraceError::NestedMatched),
(EndMethod::Matched, Space::Dest) => {
let Some(clocks) = self.clocks else {
return self.free_run(&ep.automaton, Space::Dest);
};
let source = self.free_run(&ep.automaton, Space::Source)?;
let mut out = EndStatus::new();
for (d, &hi) in clocks.dest.iter().enumerate() {
let lo = d.checked_sub(1).map(|p| clocks.dest[p]);
let mut frames = BTreeSet::new();
for (&end, fs) in &source {
let t = clocks.source[end as usize];
if t <= hi && lo.is_none_or(|l| t > l) {
frames.extend(fs.iter().cloned());
}
}
if !frames.is_empty() {
out.insert(d as u32, frames);
}
}
Ok(out)
}
}
}
fn attempt(
&mut self,
au: &Automaton,
start: u32,
space: Space,
) -> Result<BTreeSet<(u32, Frame)>, TraceError> {
let frame = vec![None; au.local_vars.len()];
self.attempt_from(au, au.start, frame, start, au.entry_clock, space)
}
fn attempt_from(
&mut self,
au: &Automaton,
entry: ThreadStateId,
frame0: Frame,
start: u32,
dom0: ClockId,
space: Space,
) -> Result<BTreeSet<(u32, Frame)>, TraceError> {
let (raw_eval, locals, len, word) = (self.eval, self.locals, self.len_of(space), self.word);
let timeline = if space == Space::Dest {
self.timeline
} else {
None
};
let step = |dom: ClockId, from: u32, k: u32| -> u32 {
timeline.map_or(from + k, |t| t.kth_after(dom, from, k))
};
let start = timeline.map_or(start, |t| t.next_tick(dom0, start));
let letter = |tick: u32| -> Option<bool> {
if space == Space::Dest {
word.and_then(|w| w.letter(tick))
} else {
None
}
};
let eval = |leaf: LeafId, tick: u32, frame: &[Option<i64>]| -> Result<i64, TraceError> {
match letter(tick) {
Some(_) => Ok(0),
None => raw_eval(leaf, tick, frame),
}
};
let mut matches = BTreeSet::new();
let mut table: BTreeMap<(FirstMatchId, u32), u32> = BTreeMap::new();
let mut seen: BTreeSet<Config> = BTreeSet::new();
let mut work: BTreeSet<Config> = BTreeSet::new();
if start < len {
work.insert((start, entry.0, dom0, Vec::new(), frame0));
}
while let Some((tick, state, dom, tags, frame)) = work.pop_first() {
if !seen.insert((tick, state, dom, tags.clone(), frame.clone())) {
continue;
}
let st = au.state(ThreadStateId(state));
for edge in &st.edges {
if let Some(g) = edge.guard {
let holds = match letter(tick) {
Some(top) => top,
None => raw_eval(g, tick, &frame)? != 0,
};
if !holds {
continue;
}
}
let target = edge.target.0;
match &edge.label {
Transition::Epsilon => {
work.insert((tick, target, dom, tags.clone(), frame.clone()));
}
Transition::Delay { min, max } if timeline.is_none() => {
let room = len - 1 - tick;
let hi = max.map_or(room, |m| m.min(room));
for k in *min..=hi {
work.insert((tick + k, target, dom, tags.clone(), frame.clone()));
}
}
Transition::Delay { min, max } => {
let mut k = *min;
while max.is_none_or(|m| k <= m) {
let to = step(dom, tick, k);
if to >= len {
break;
}
work.insert((to, target, dom, tags.clone(), frame.clone()));
k += 1;
}
}
Transition::ClockSync { to, strict } => {
let at = if *strict {
step(*to, tick, 1)
} else {
timeline.map_or(tick, |t| t.next_tick(*to, tick))
};
if at < len {
work.insert((at, target, *to, tags.clone(), frame.clone()));
}
}
Transition::Match { .. } => {
matches.insert((tick, frame.clone()));
}
Transition::FirstMatchBegin { group } => {
if tags.iter().any(|(g, _)| g == group) {
return Err(TraceError::BeginWhileOpen(*group));
}
let mut t2 = tags.clone();
t2.push((*group, tick));
t2.sort();
work.insert((tick, target, dom, t2, frame.clone()));
}
Transition::FirstMatchEnd { group } => {
let Some(pos) = tags.iter().position(|(g, _)| g == group) else {
return Err(TraceError::EndWithoutBegin(*group));
};
let begin = tags[pos].1;
let earliest = *table.entry((*group, begin)).or_insert(tick);
if earliest == tick {
let mut t2 = tags.clone();
t2.remove(pos);
work.insert((tick, target, dom, t2, frame.clone()));
}
}
Transition::Abort { .. } => return Err(TraceError::UnsupportedEdge("Abort")),
Transition::LocalVarWrite { var, value } => {
if !locals {
return Err(TraceError::UnsupportedEdge("LocalVarWrite"));
}
let v = au.local_var(*var).cast(eval(*value, tick, &frame)?);
let mut f2 = frame.clone();
f2[var.0 as usize] = Some(v);
work.insert((tick, target, dom, tags.clone(), f2));
}
Transition::LocalVarCopy { dst, src } => {
if !locals {
return Err(TraceError::UnsupportedEdge("LocalVarCopy"));
}
let mut f2 = frame.clone();
wide::copy_local(
&mut f2,
*dst,
au.local_var(*dst),
&frame,
*src,
au.local_var(*src),
);
work.insert((tick, target, dom, tags.clone(), f2));
}
Transition::LocalVarStep { var, delta } => {
if !locals {
return Err(TraceError::UnsupportedEdge("LocalVarStep"));
}
let decl = au.local_var(*var);
let mut f2 = frame.clone();
if decl.is_wide() {
wide::step_local(&mut f2, *var, decl, *delta)
.ok_or(TraceError::UnassignedRead(*var))?;
} else {
let cur = read_local(&frame, *var)?;
f2[var.0 as usize] = Some(decl.cast(cur.wrapping_add(*delta)));
}
work.insert((tick, target, dom, tags.clone(), f2));
}
Transition::SubroutineCall { call, args } => {
if !locals {
return Err(TraceError::UnsupportedEdge("SubroutineCall"));
}
let mut vals = Vec::with_capacity(args.len());
for a in args {
vals.push(match a {
CallArg::Value(leaf) => Some(eval(*leaf, tick, &frame)?),
CallArg::Ref | CallArg::Const => None,
});
}
if letter(tick).is_none() {
self.calls.insert(CallEvent {
tick,
call: *call,
args: vals,
});
}
work.insert((tick, target, dom, tags.clone(), frame.clone()));
}
Transition::EndPoint { point, flag } => {
if !locals {
return Err(TraceError::UnsupportedEdge("EndPoint"));
}
if let Some(top) = letter(tick) {
if let Some(slot) = flag {
let mut f2 = frame.clone();
f2[slot.0 as usize] = Some(i64::from(top));
work.insert((tick, target, dom, tags.clone(), f2));
} else if top {
work.insert((tick, target, dom, tags.clone(), frame.clone()));
}
continue;
}
let ep = au.end_point(*point);
let handoff = (ep.method == EndMethod::Matched && self.clocks.is_none())
.then(|| {
(0..tick)
.rev()
.find(|&p| self.timeline.is_none_or(|t| t.is_tick(dom, p)))
.map_or(0, |p| p + 1)
});
let status = self.end_status(ep, space)?;
let frames: Vec<Frame> = match handoff {
Some(lo) => status
.range(lo..=tick)
.flat_map(|(_, s)| s.iter().cloned())
.collect::<BTreeSet<Frame>>()
.into_iter()
.collect(),
None => status
.get(&tick)
.map(|s| s.iter().cloned().collect())
.unwrap_or_default(),
};
if let Some(slot) = flag {
let mut f2 = frame.clone();
f2[slot.0 as usize] = Some(i64::from(!frames.is_empty()));
work.insert((tick, target, dom, tags.clone(), f2));
} else {
for sub in frames {
let mut f2 = frame.clone();
for &(from, to) in &ep.flow_out {
wide::copy_local(
&mut f2,
to,
au.local_var(to),
&sub,
from,
au.local_var(from),
);
}
work.insert((tick, target, dom, tags.clone(), f2));
}
}
}
}
}
}
Ok(matches)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sva::ir::{AbortMode, LocalVarId};
fn rows(table: &'static [&'static [bool]]) -> impl Fn(LeafId, u32) -> bool {
move |id, t| {
table[id.0 as usize]
.get(t as usize)
.copied()
.unwrap_or(false)
}
}
fn a_then_b() -> Automaton {
let mut a = Automaton::empty();
let s0 = a.start;
let (s1, s2, s3) = (a.new_state(), a.new_state(), a.new_state());
a.add_edge(s0, Some(LeafId(0)), Transition::Epsilon, s1);
a.add_edge(
s1,
None,
Transition::Delay {
min: 1,
max: Some(1),
},
s2,
);
a.add_edge(s2, Some(LeafId(1)), Transition::Epsilon, s3);
a.add_edge(s3, None, Transition::Match { actions: vec![] }, s3);
a
}
#[test]
fn concatenation_ends_one_tick_after_the_start() {
let a = a_then_b();
let tr = rows(&[&[true, false, true], &[false, true, false]]);
let at0 = attempt(&a, 0, 3, &tr).unwrap();
assert_eq!(at0.ends, BTreeSet::from([1]));
assert!(!at0.admits_empty);
assert!(attempt(&a, 1, 3, &tr).unwrap().ends.is_empty());
assert!(attempt(&a, 2, 3, &tr).unwrap().ends.is_empty());
}
#[test]
fn delay_window_yields_every_end_tick_in_range() {
let mut a = Automaton::empty();
let s0 = a.start;
let (s1, s2) = (a.new_state(), a.new_state());
a.add_edge(
s0,
None,
Transition::Delay {
min: 1,
max: Some(3),
},
s1,
);
a.add_edge(s1, Some(LeafId(0)), Transition::Epsilon, s2);
a.add_edge(s2, None, Transition::Match { actions: vec![] }, s2);
let tr = rows(&[&[false, true, false, true, true]]);
assert_eq!(
attempt(&a, 0, 5, &tr).unwrap().ends,
BTreeSet::from([1, 3]),
"tick 4 is outside the window, tick 2 fails the guard"
);
}
#[test]
fn unbounded_delay_stops_at_the_end_of_the_trace() {
let mut a = Automaton::empty();
let s0 = a.start;
let s1 = a.new_state();
a.add_edge(s0, None, Transition::Delay { min: 0, max: None }, s1);
a.add_edge(s1, None, Transition::Match { actions: vec![] }, s1);
let tr = rows(&[]);
assert_eq!(
attempt(&a, 1, 4, &tr).unwrap().ends,
BTreeSet::from([1, 2, 3])
);
}
fn first_of_window() -> Automaton {
let mut a = Automaton::empty();
let g = a.new_first_match_group();
let s0 = a.start;
let (s1, s2, s3, s4) = (a.new_state(), a.new_state(), a.new_state(), a.new_state());
a.add_edge(s0, None, Transition::FirstMatchBegin { group: g }, s1);
a.add_edge(
s1,
None,
Transition::Delay {
min: 0,
max: Some(2),
},
s2,
);
a.add_edge(s2, Some(LeafId(0)), Transition::Epsilon, s3);
a.add_edge(s3, None, Transition::FirstMatchEnd { group: g }, s4);
a.add_edge(s4, None, Transition::Match { actions: vec![] }, s4);
a
}
#[test]
fn first_match_keeps_only_the_earliest_end() {
let a = first_of_window();
let tr = rows(&[&[false, true, true, true]]);
assert_eq!(attempt(&a, 0, 4, &tr).unwrap().ends, BTreeSet::from([1]));
let mut plain = a.clone();
for st in &mut plain.states {
for e in &mut st.edges {
if matches!(
e.label,
Transition::FirstMatchBegin { .. } | Transition::FirstMatchEnd { .. }
) {
e.label = Transition::Epsilon;
}
}
}
assert_eq!(
attempt(&plain, 0, 4, &tr).unwrap().ends,
BTreeSet::from([1, 2]),
"window is ticks 0..=2 and the guard holds on 1 and 2"
);
}
#[test]
fn first_match_regions_beginning_on_different_ticks_are_independent() {
let mut a = Automaton::empty();
let g = a.new_first_match_group();
let s0 = a.start;
let (s1, s2, s3, s4, s5) = (
a.new_state(),
a.new_state(),
a.new_state(),
a.new_state(),
a.new_state(),
);
a.add_edge(
s0,
None,
Transition::Delay {
min: 0,
max: Some(1),
},
s1,
);
a.add_edge(s1, None, Transition::FirstMatchBegin { group: g }, s2);
a.add_edge(
s2,
None,
Transition::Delay {
min: 0,
max: Some(2),
},
s3,
);
a.add_edge(s3, Some(LeafId(0)), Transition::Epsilon, s4);
a.add_edge(s4, None, Transition::FirstMatchEnd { group: g }, s5);
a.add_edge(s5, None, Transition::Match { actions: vec![] }, s5);
let tr = rows(&[&[false, true, false, true, false]]);
assert_eq!(attempt(&a, 0, 5, &tr).unwrap().ends, BTreeSet::from([1]));
let tr = rows(&[&[true, true, false, false, false]]);
assert_eq!(attempt(&a, 0, 5, &tr).unwrap().ends, BTreeSet::from([0, 1]));
let tr = rows(&[&[false, false, true, true, false]]);
assert_eq!(attempt(&a, 0, 5, &tr).unwrap().ends, BTreeSet::from([2]));
let tr = rows(&[&[false, false, false, true, false]]);
assert_eq!(attempt(&a, 0, 5, &tr).unwrap().ends, BTreeSet::from([3]));
}
#[test]
fn first_match_end_without_begin_is_a_named_error() {
let mut a = Automaton::empty();
let s0 = a.start;
let s1 = a.new_state();
a.add_edge(
s0,
None,
Transition::FirstMatchEnd {
group: FirstMatchId(7),
},
s1,
);
let tr = rows(&[]);
assert_eq!(
attempt(&a, 0, 2, &tr),
Err(TraceError::EndWithoutBegin(FirstMatchId(7)))
);
}
#[test]
fn nested_begin_of_an_open_group_is_a_named_error() {
let mut a = Automaton::empty();
let g = FirstMatchId(0);
let s0 = a.start;
let (s1, s2) = (a.new_state(), a.new_state());
a.add_edge(s0, None, Transition::FirstMatchBegin { group: g }, s1);
a.add_edge(s1, None, Transition::FirstMatchBegin { group: g }, s2);
let tr = rows(&[]);
assert_eq!(attempt(&a, 0, 2, &tr), Err(TraceError::BeginWhileOpen(g)));
}
type Eval<'a> = &'a dyn Fn(LeafId, u32, &[Option<i64>]) -> Result<i64, TraceError>;
fn write_then_guard() -> Automaton {
let mut a = Automaton::empty();
let v0 = a.new_local_var(4, false);
let s0 = a.start;
let (s1, s2, s3) = (a.new_state(), a.new_state(), a.new_state());
a.add_edge(
s0,
None,
Transition::LocalVarWrite {
var: v0,
value: LeafId(0),
},
s1,
);
a.add_edge(
s1,
None,
Transition::Delay {
min: 1,
max: Some(1),
},
s2,
);
a.add_edge(s2, Some(LeafId(1)), Transition::Epsilon, s3);
a.add_edge(s3, None, Transition::Match { actions: vec![] }, s3);
a
}
#[test]
fn a_local_write_is_cast_to_the_slot_type_and_later_leaves_read_it() {
let a = write_then_guard();
let ev: Eval = &|id, _t, frame| match id.0 {
0 => Ok(0x1f),
1 => Ok(i64::from(read_local(frame, LocalVarId(0))? == 15)),
_ => unreachable!(),
};
let r = attempt_with_locals(&a, 0, 3, ev).unwrap();
assert_eq!(r.ends(), BTreeSet::from([1]));
assert_eq!(
r.matches,
BTreeSet::from([(1, vec![Some(0xf)])]),
"the frame at the match carries the cast value"
);
let ev: Eval = &|id, _t, frame| match id.0 {
0 => Ok(0x1f),
1 => Ok(i64::from(read_local(frame, LocalVarId(0))? == 0x1f)),
_ => unreachable!(),
};
assert!(
attempt_with_locals(&a, 0, 3, ev)
.unwrap()
.matches
.is_empty()
);
}
#[test]
fn reading_an_unassigned_slot_is_a_named_error() {
let mut a = write_then_guard();
a.states[0].edges[0].label = Transition::Epsilon;
let ev: Eval = &|id, _t, frame| match id.0 {
1 => Ok(i64::from(read_local(frame, LocalVarId(0))? == 15)),
_ => unreachable!(),
};
assert_eq!(
attempt_with_locals(&a, 0, 3, ev),
Err(TraceError::UnassignedRead(LocalVarId(0)))
);
let e = TraceError::UnassignedRead(LocalVarId(3));
assert!(e.to_string().contains("unassigned local variable 3"), "{e}");
}
#[test]
fn forked_threads_keep_independent_frames() {
let mut a = Automaton::empty();
let v0 = a.new_local_var(8, false);
let s0 = a.start;
let (l, r, end) = (a.new_state(), a.new_state(), a.new_state());
a.add_edge(s0, None, Transition::Epsilon, l);
a.add_edge(s0, None, Transition::Epsilon, r);
for (from, leaf) in [(l, 0), (r, 1)] {
a.add_edge(
from,
None,
Transition::LocalVarWrite {
var: v0,
value: LeafId(leaf),
},
end,
);
}
a.add_edge(end, None, Transition::Match { actions: vec![] }, end);
let ev: Eval = &|id, _t, _f| Ok(10 + i64::from(id.0));
let r = attempt_with_locals(&a, 0, 1, ev).unwrap();
assert_eq!(
r.matches,
BTreeSet::from([(0, vec![Some(10)]), (0, vec![Some(11)])])
);
assert_eq!(r.ends(), BTreeSet::from([0]));
}
#[test]
fn a_local_copy_casts_to_the_destination_and_keeps_unassigned() {
let mut a = Automaton::empty();
let wide = a.new_local_var(8, true);
let narrow = a.new_local_var(4, true);
let never = a.new_local_var(8, true);
let sink = a.new_local_var(8, true);
let s0 = a.start;
let (s1, s2, s3, s4) = (a.new_state(), a.new_state(), a.new_state(), a.new_state());
a.add_edge(
s0,
None,
Transition::LocalVarWrite {
var: wide,
value: LeafId(0),
},
s1,
);
a.add_edge(
s1,
None,
Transition::LocalVarCopy {
dst: narrow,
src: wide,
},
s2,
);
a.add_edge(
s2,
None,
Transition::LocalVarWrite {
var: sink,
value: LeafId(0),
},
s3,
);
a.add_edge(
s3,
None,
Transition::LocalVarCopy {
dst: sink,
src: never,
},
s4,
);
a.add_edge(s4, None, Transition::Match { actions: vec![] }, s4);
let ev: Eval = &|_, _, _| Ok(0x39);
let r = attempt_with_locals(&a, 0, 1, ev).unwrap();
assert_eq!(
r.matches,
BTreeSet::from([(0, vec![Some(0x39), Some(-7), None, None])])
);
}
#[test]
fn the_plain_attempt_refuses_local_edges() {
let a = write_then_guard();
let tr = rows(&[]);
assert_eq!(
attempt(&a, 0, 3, &tr),
Err(TraceError::UnsupportedEdge("LocalVarWrite"))
);
let mut b = Automaton::empty();
let (x, y) = (b.new_local_var(1, false), b.new_local_var(1, false));
let (s0, s1) = (b.start, b.new_state());
b.add_edge(s0, None, Transition::LocalVarCopy { dst: x, src: y }, s1);
assert_eq!(
attempt(&b, 0, 3, &tr),
Err(TraceError::UnsupportedEdge("LocalVarCopy"))
);
}
fn write_then_steps(width: u32, signed: bool, deltas: &[i64]) -> Automaton {
let mut a = Automaton::empty();
let v0 = a.new_local_var(width, signed);
let mut cur = a.start;
let next = a.new_state();
a.add_edge(
cur,
None,
Transition::LocalVarWrite {
var: v0,
value: LeafId(0),
},
next,
);
cur = next;
for d in deltas {
let next = a.new_state();
a.add_edge(
cur,
None,
Transition::LocalVarStep { var: v0, delta: *d },
next,
);
cur = next;
}
a.add_edge(cur, None, Transition::Match { actions: vec![] }, cur);
a
}
#[test]
fn a_step_adds_its_delta_and_wraps_at_the_slot_width() {
let ev: Eval = &|_, _, _| Ok(14);
let a = write_then_steps(4, false, &[1, 1, 1]);
assert_eq!(
attempt_with_locals(&a, 0, 1, ev).unwrap().matches,
BTreeSet::from([(0, vec![Some(1)])])
);
let a = write_then_steps(4, true, &[1, 1]);
assert_eq!(
attempt_with_locals(&a, 0, 1, ev).unwrap().matches,
BTreeSet::from([(0, vec![Some(0)])])
);
let a = write_then_steps(8, false, &[-1, -1]);
assert_eq!(
attempt_with_locals(&a, 0, 1, ev).unwrap().matches,
BTreeSet::from([(0, vec![Some(12)])])
);
}
#[test]
fn a_step_of_an_unassigned_slot_is_a_named_error() {
let mut a = write_then_steps(8, false, &[1]);
a.states[0].edges[0].label = Transition::Epsilon;
let ev: Eval = &|_, _, _| Ok(0);
assert_eq!(
attempt_with_locals(&a, 0, 1, ev),
Err(TraceError::UnassignedRead(LocalVarId(0)))
);
}
#[test]
fn a_wide_local_is_written_stepped_and_copied_as_a_whole() {
let mut a = Automaton::empty();
let w = a.new_local_var(130, false);
let w2 = a.new_local_var(130, false);
let n = a.new_local_var(8, false);
assert_eq!(
(w.0, w2.0, n.0, a.local_vars.len()),
(0, 6, 12, 13),
"three limbs of value and of unknown plane per wide variable"
);
let mut cur = a.start;
for slot in 0..6u32 {
let next = a.new_state();
a.add_edge(
cur,
None,
Transition::LocalVarWrite {
var: LocalVarId(slot),
value: LeafId(slot),
},
next,
);
cur = next;
}
for t in [
Transition::LocalVarStep { var: w, delta: 1 },
Transition::LocalVarCopy { dst: w2, src: w },
Transition::LocalVarCopy { dst: n, src: w },
] {
let next = a.new_state();
a.add_edge(cur, None, t, next);
cur = next;
}
a.add_edge(cur, None, Transition::Match { actions: vec![] }, cur);
let words = [-1i64, 1, 3, 0, 0, 0];
let ev: Eval = &|id, _t, _f| Ok(words[id.0 as usize]);
let r = attempt_with_locals(&a, 0, 1, ev).unwrap();
let limbs = [Some(0), Some(2), Some(3), Some(0), Some(0), Some(0)];
let mut frame = limbs.to_vec();
frame.extend(limbs);
frame.push(Some(0));
assert_eq!(
r.matches,
BTreeSet::from([(0, frame)]),
"!0 + 1 carries into limb 1; the copies take every limb, the narrow one the low byte"
);
}
#[test]
fn a_step_of_a_wide_local_with_an_unknown_bit_is_unknown() {
let mut a = Automaton::empty();
let w = a.new_local_var(70, false);
let mut cur = a.start;
for slot in 0..4u32 {
let next = a.new_state();
a.add_edge(
cur,
None,
Transition::LocalVarWrite {
var: LocalVarId(slot),
value: LeafId(slot),
},
next,
);
cur = next;
}
let next = a.new_state();
a.add_edge(
cur,
None,
Transition::LocalVarStep { var: w, delta: 1 },
next,
);
a.add_edge(next, None, Transition::Match { actions: vec![] }, next);
let words = [5i64, 0, 1, 0];
let ev: Eval = &|id, _t, _f| Ok(words[id.0 as usize]);
let r = attempt_with_locals(&a, 0, 1, ev).unwrap();
assert_eq!(
r.matches,
BTreeSet::from([(0, vec![Some(0), Some(0), Some(-1), Some(0x3f)])])
);
}
fn two_calls() -> Automaton {
let mut a = Automaton::empty();
let s0 = a.start;
let (s1, s2, s3, s4, s5) = (
a.new_state(),
a.new_state(),
a.new_state(),
a.new_state(),
a.new_state(),
);
a.add_edge(s0, Some(LeafId(0)), Transition::Epsilon, s1);
a.add_edge(
s1,
None,
Transition::SubroutineCall {
call: CallId(0),
args: vec![CallArg::Value(LeafId(1)), CallArg::Ref, CallArg::Const],
},
s2,
);
a.add_edge(
s2,
None,
Transition::Delay {
min: 1,
max: Some(1),
},
s3,
);
a.add_edge(s3, Some(LeafId(2)), Transition::Epsilon, s4);
a.add_edge(
s4,
None,
Transition::SubroutineCall {
call: CallId(1),
args: vec![],
},
s5,
);
a.add_edge(s5, None, Transition::Match { actions: vec![] }, s5);
a
}
#[test]
fn a_call_is_recorded_with_its_sampled_arguments() {
let a = two_calls();
let ev: Eval = &|id, t, _| match id.0 {
1 => Ok(100 + i64::from(t)),
_ => Ok(1),
};
let r = attempt_with_calls(&a, 0, 3, ev).unwrap();
assert_eq!(r.attempt.ends(), BTreeSet::from([1]));
assert_eq!(
r.calls,
BTreeSet::from([
CallEvent {
tick: 0,
call: CallId(0),
args: vec![Some(100), None, None]
},
CallEvent {
tick: 1,
call: CallId(1),
args: vec![]
},
])
);
}
#[test]
fn a_call_fires_even_when_the_longer_sequence_fails() {
let a = two_calls();
let ev: Eval = &|id, _, _| match id.0 {
2 => Ok(0),
_ => Ok(1),
};
let r = attempt_with_calls(&a, 0, 3, ev).unwrap();
assert!(r.attempt.matches.is_empty());
assert_eq!(
r.calls.iter().map(|c| c.call).collect::<Vec<_>>(),
vec![CallId(0)]
);
}
#[test]
fn calls_and_writes_run_in_list_order() {
let mut a = Automaton::empty();
let v0 = a.new_local_var(8, false);
let s0 = a.start;
let (s1, s2, s3, s4) = (a.new_state(), a.new_state(), a.new_state(), a.new_state());
a.add_edge(
s0,
None,
Transition::LocalVarWrite {
var: v0,
value: LeafId(0),
},
s1,
);
a.add_edge(
s1,
None,
Transition::SubroutineCall {
call: CallId(0),
args: vec![CallArg::Value(LeafId(1))],
},
s2,
);
a.add_edge(s2, None, Transition::LocalVarStep { var: v0, delta: 5 }, s3);
a.add_edge(
s3,
None,
Transition::SubroutineCall {
call: CallId(1),
args: vec![CallArg::Value(LeafId(1))],
},
s4,
);
a.add_edge(s4, None, Transition::Match { actions: vec![] }, s4);
let ev: Eval = &|id, _, frame| match id.0 {
0 => Ok(3),
_ => read_local(frame, LocalVarId(0)),
};
let r = attempt_with_calls(&a, 0, 1, ev).unwrap();
let by_call: Vec<_> = r.calls.iter().map(|c| (c.call.0, c.args[0])).collect();
assert_eq!(by_call, vec![(0, Some(3)), (1, Some(8))]);
assert_eq!(r.attempt.matches, BTreeSet::from([(0, vec![Some(8)])]));
}
#[test]
fn the_plain_attempt_refuses_call_edges() {
let a = two_calls();
let tr = rows(&[&[true], &[true], &[true]]);
assert_eq!(
attempt(&a, 0, 3, &tr),
Err(TraceError::UnsupportedEdge("SubroutineCall"))
);
let a = write_then_steps(4, false, &[1]);
assert_eq!(
attempt(&a, 0, 3, &tr),
Err(TraceError::UnsupportedEdge("LocalVarWrite"))
);
let mut b = Automaton::empty();
let v = b.new_local_var(1, false);
let (s0, s1) = (b.start, b.new_state());
b.add_edge(s0, None, Transition::LocalVarStep { var: v, delta: 1 }, s1);
assert_eq!(
attempt(&b, 0, 3, &tr),
Err(TraceError::UnsupportedEdge("LocalVarStep"))
);
}
#[test]
fn abort_edges_are_refused_not_ignored() {
let mut a = Automaton::empty();
let s0 = a.start;
let s1 = a.new_state();
a.add_edge(
s0,
None,
Transition::Abort {
leaf: LeafId(0),
mode: AbortMode::Accept,
},
s1,
);
let tr = rows(&[]);
assert_eq!(
attempt(&a, 0, 2, &tr),
Err(TraceError::UnsupportedEdge("Abort"))
);
let ev: Eval = &|_, _, _| Ok(1);
assert_eq!(
attempt_with_locals(&a, 0, 2, ev),
Err(TraceError::UnsupportedEdge("Abort"))
);
}
#[test]
fn start_outside_the_trace_matches_nothing_and_admits_empty_passes_through() {
let mut a = a_then_b();
a.admits_empty = true;
let tr = rows(&[&[true], &[true]]);
let r = attempt(&a, 5, 3, &tr).unwrap();
assert!(r.ends.is_empty());
assert!(r.admits_empty);
}
fn one_tick(leaf: u32) -> Automaton {
let mut a = Automaton::empty();
let s0 = a.start;
let s1 = a.new_state();
a.add_edge(s0, Some(LeafId(leaf)), Transition::Epsilon, s1);
a.add_edge(s1, None, Transition::Match { actions: vec![] }, s1);
a
}
fn tests_end_point(
method: EndMethod,
sub: Automaton,
flow_out: Vec<(LocalVarId, LocalVarId)>,
flag: Option<LocalVarId>,
then: Option<LeafId>,
) -> Automaton {
let mut a = Automaton::empty();
a.local_vars = sub.local_vars.clone();
let p = a.new_end_point(EndPoint {
method,
site: 0,
automaton: sub,
flow_out,
});
let s0 = a.start;
let (s1, s2) = (a.new_state(), a.new_state());
a.add_edge(s0, None, Transition::EndPoint { point: p, flag }, s1);
a.add_edge(s1, then, Transition::Epsilon, s2);
a.add_edge(s2, None, Transition::Match { actions: vec![] }, s2);
a
}
#[test]
fn triggered_holds_where_the_instance_ends_whatever_the_start() {
let a = tests_end_point(EndMethod::Triggered, a_then_b(), vec![], None, None);
let ev: Eval = &|id, t, _| {
let rows: [[i64; 4]; 2] = [[1, 0, 1, 0], [0, 1, 0, 0]];
Ok(rows[id.0 as usize][t as usize])
};
for start in 0..4 {
let r = attempt_with_locals(&a, start, 4, ev).unwrap();
let want: BTreeSet<u32> = if start == 1 {
BTreeSet::from([1])
} else {
BTreeSet::new()
};
assert_eq!(r.ends(), want, "start {start}");
}
}
#[test]
fn an_empty_match_never_triggers() {
let mut sub = one_tick(0);
sub.admits_empty = true;
let a = tests_end_point(EndMethod::Triggered, sub, vec![], None, None);
let never: Eval = &|_, _, _| Ok(0);
for start in 0..3 {
assert!(
attempt_with_locals(&a, start, 3, never)
.unwrap()
.matches
.is_empty(),
"empty match fired the end point at start {start}"
);
}
let once: Eval = &|_, t, _| Ok(i64::from(t == 1));
assert_eq!(
attempt_with_locals(&a, 1, 3, once).unwrap().ends(),
BTreeSet::from([1])
);
}
#[test]
fn the_flag_form_stores_the_status_and_never_blocks() {
let mut sub = one_tick(0);
let flag = sub.new_local_var(1, false);
let a = tests_end_point(
EndMethod::Triggered,
sub,
vec![],
Some(flag),
Some(LeafId(1)),
);
let ev: Eval = &|id, t, frame| match id.0 {
0 => Ok(i64::from(t == 0 || t == 2)),
_ => Ok(i64::from(read_local(frame, LocalVarId(0))? == 0)),
};
let r = attempt_with_locals(&a, 0, 4, ev).unwrap();
assert!(r.matches.is_empty(), "tick 0 is triggered, `!flag` fails");
let r = attempt_with_locals(&a, 1, 4, ev).unwrap();
assert_eq!(r.matches, BTreeSet::from([(1, vec![Some(0)])]));
let r = attempt_with_locals(&a, 3, 4, ev).unwrap();
assert_eq!(r.matches, BTreeSet::from([(3, vec![Some(0)])]));
let r = attempt_with_locals(&a, 2, 4, ev).unwrap();
assert!(r.matches.is_empty());
}
#[test]
fn a_maximal_application_forks_per_frame_and_flows_variables_out() {
let mut sub = Automaton::empty();
let dst = sub.new_local_var(8, false); let src = sub.new_local_var(8, false); let s0 = sub.start;
let (w1, w2, m) = (sub.new_state(), sub.new_state(), sub.new_state());
sub.add_edge(s0, None, Transition::Epsilon, w1);
sub.add_edge(s0, None, Transition::Epsilon, w2);
sub.add_edge(
w1,
None,
Transition::LocalVarWrite {
var: src,
value: LeafId(0),
},
m,
);
sub.add_edge(
w2,
None,
Transition::LocalVarWrite {
var: src,
value: LeafId(1),
},
m,
);
sub.add_edge(m, None, Transition::Match { actions: vec![] }, m);
let a = tests_end_point(EndMethod::Triggered, sub, vec![(src, dst)], None, None);
let ev: Eval = &|id, t, _| Ok(10 * (i64::from(id.0) + 1) + i64::from(t));
let r = attempt_with_locals(&a, 2, 3, ev).unwrap();
assert_eq!(
r.matches,
BTreeSet::from([(2, vec![Some(12), None]), (2, vec![Some(22), None]),]),
"two frames, two threads; the instance's own slot stays out of the thread"
);
}
#[test]
fn matched_hands_off_across_clocks() {
let a = tests_end_point(EndMethod::Matched, one_tick(0), vec![], None, None);
let clocks = TraceClocks {
dest: vec![10, 20, 30],
source: vec![5, 12, 18, 25, 31],
};
let ev: Eval = &|_, t, _| Ok(i64::from(matches!(t, 0..=2 | 4)));
let ends = |start| {
attempt_with_clocks(&a, start, &clocks, ev)
.unwrap()
.attempt
.ends()
};
assert_eq!(ends(0), BTreeSet::from([0]), "t=5 <= 10");
assert_eq!(ends(1), BTreeSet::from([1]), "t=12 and t=18 in (10, 20]");
assert!(
ends(2).is_empty(),
"t=25 does not hold, t=31 is past tick 30"
);
let at_tick = TraceClocks {
dest: vec![10, 20],
source: vec![10, 15],
};
let only_first: Eval = &|_, t, _| Ok(i64::from(t == 0));
let r = attempt_with_clocks(&a, 0, &at_tick, only_first).unwrap();
assert_eq!(r.attempt.ends(), BTreeSet::from([0]));
let r = attempt_with_clocks(&a, 1, &at_tick, only_first).unwrap();
assert!(r.attempt.matches.is_empty());
}
#[test]
fn matched_needs_a_clock_model_and_does_not_nest() {
let a = tests_end_point(EndMethod::Matched, one_tick(0), vec![], None, None);
let ev: Eval = &|_, _, _| Ok(1);
assert!(attempt_with_locals(&a, 0, 2, ev).is_ok());
let clocks = TraceClocks {
dest: vec![1, 2],
source: vec![1, 2],
};
let inner = tests_end_point(EndMethod::Matched, one_tick(0), vec![], None, None);
let outer = tests_end_point(EndMethod::Matched, inner, vec![], None, None);
assert_eq!(
attempt_with_clocks(&outer, 0, &clocks, ev),
Err(TraceError::NestedMatched)
);
let bad = TraceClocks {
dest: vec![2, 2],
source: vec![1],
};
assert_eq!(
attempt_with_clocks(&a, 0, &bad, ev),
Err(TraceError::BadClocks)
);
}
#[test]
fn calls_inside_an_instance_are_reported() {
let a = tests_end_point(EndMethod::Triggered, two_calls(), vec![], None, None);
let ev: Eval = &|_, _, _| Ok(1);
let r = attempt_with_calls(&a, 1, 3, ev).unwrap();
assert_eq!(r.attempt.ends(), BTreeSet::from([1]));
assert!(r.calls.iter().any(|c| c.call == CallId(1)));
}
#[test]
fn the_plain_attempt_refuses_end_point_edges() {
let a = tests_end_point(EndMethod::Triggered, one_tick(0), vec![], None, None);
let tr = rows(&[&[true]]);
assert_eq!(
attempt(&a, 0, 1, &tr),
Err(TraceError::UnsupportedEdge("EndPoint"))
);
}
#[test]
fn display_names_the_group() {
let e = TraceError::EndWithoutBegin(FirstMatchId(3));
assert!(e.to_string().contains("FirstMatchEnd(3)"), "{e}");
let e = TraceError::BeginWhileOpen(FirstMatchId(4));
assert!(e.to_string().contains("FirstMatchBegin(4)"), "{e}");
let e = TraceError::UnsupportedEdge("Abort");
assert!(e.to_string().contains("Abort"), "{e}");
}
#[test]
fn tail_word_letters_flip_and_cut() {
let w = TailWord::weak(2);
assert_eq!(w.letter(1), None);
assert_eq!(w.letter(2), Some(true));
assert_eq!(w.letter(99), Some(true));
assert_eq!(w.extent(), 2);
let s = w.flip();
assert_eq!(s, TailWord::strong(2));
assert_eq!(s.letter(5), Some(false));
let cut = s.cut_then_top(4);
assert_eq!(cut.letter(2), Some(false));
assert_eq!(cut.letter(3), Some(false));
assert_eq!(cut.letter(4), Some(true));
assert_eq!(cut.extent(), 4);
assert_eq!(cut.flip().letter(2), Some(true), "flip swaps mid letters");
assert_eq!(cut.flip().letter(4), Some(false));
assert_eq!(s.cut_then_top(1), TailWord::weak(1));
assert_eq!(w.cut_then_top(5), TailWord::weak(2));
}
#[test]
fn attempt_word_guards_follow_the_tail_letter() {
let a = a_then_b();
let table: &[&[bool]] = &[&[true], &[false]];
let eval = |l: LeafId, t: u32, _: &[Option<i64>]| -> Result<i64, TraceError> {
Ok(i64::from(
table[l.0 as usize]
.get(t as usize)
.copied()
.unwrap_or(false),
))
};
let run = |w: &TailWord| {
attempt_word(&a, a.start, &[], 0, w, 6, &eval)
.unwrap()
.into_iter()
.map(|(e, _)| e)
.collect::<Vec<_>>()
};
assert_eq!(run(&TailWord::weak(1)), vec![1]);
assert_eq!(run(&TailWord::strong(1)), Vec::<u32>::new());
let on_tail = attempt_word(&a, a.start, &[], 3, &TailWord::weak(1), 6, &eval).unwrap();
assert_eq!(
on_tail.into_iter().map(|(e, _)| e).collect::<Vec<_>>(),
vec![4]
);
}
#[test]
fn attempt_word_calls_records_real_ticks_only() {
let a = two_calls();
let eval = |l: LeafId, t: u32, _: &[Option<i64>]| -> Result<i64, TraceError> {
Ok(match l.0 {
1 => 100 + i64::from(t),
_ => 1,
})
};
let run = |real: u32| {
attempt_word_calls(
&a,
a.start,
&[],
0,
&TailWord::weak(real),
6,
&eval,
None,
a.entry_clock,
)
.unwrap()
};
let (matches, calls) = run(1);
assert_eq!(
matches.into_iter().map(|(e, _)| e).collect::<Vec<_>>(),
vec![1]
);
assert_eq!(
calls.into_iter().collect::<Vec<_>>(),
vec![CallEvent {
tick: 0,
call: CallId(0),
args: vec![Some(100), None, None]
}],
"the second call would fire on tick 1, a `⊤` letter"
);
let (_, calls) = run(3);
assert_eq!(
calls.iter().map(|c| (c.tick, c.call.0)).collect::<Vec<_>>(),
vec![(0, 0), (1, 1)]
);
let plain = attempt_word(&a, a.start, &[], 0, &TailWord::weak(3), 6, &eval).unwrap();
assert_eq!(plain, run(3).0);
}
#[test]
fn attempt_word_starts_at_the_given_entry_with_the_given_frame() {
let mut a = Automaton::empty();
let v = a.new_local_var(8, false);
let entry = a.new_state();
let end = a.new_state();
a.add_edge(entry, None, Transition::Match { actions: vec![] }, entry);
let _ = end;
let eval = |_: LeafId, _: u32, _: &[Option<i64>]| Ok(1);
let frame = vec![Some(7)];
let got = attempt_word(&a, entry, &frame, 0, &TailWord::weak(1), 3, &eval).unwrap();
assert_eq!(
got.into_iter().collect::<Vec<_>>(),
vec![(0, vec![Some(7)])]
);
assert_eq!(v, LocalVarId(0));
}
const C0: ClockId = ClockId(0);
const C1: ClockId = ClockId(1);
#[test]
fn timeline_counts_strictly_subsequent_ticks_and_continues_into_the_tail() {
let tl = Timeline::new(6, vec![vec![0, 2, 4], vec![1, 2]]).unwrap();
assert_eq!(tl.real(), 6);
assert!(tl.is_tick(C0, 2) && !tl.is_tick(C0, 3) && tl.is_tick(C1, 1));
assert_eq!(tl.kth_after(C1, 0, 1), 1);
assert_eq!(
tl.kth_after(C1, 1, 1),
2,
"strictly later: not tick 1 itself"
);
assert_eq!(
tl.kth_after(C1, 2, 1),
6,
"list exhausted: the tail starts at `real`"
);
assert_eq!(tl.kth_after(C0, 0, 2), 4);
assert_eq!(tl.kth_after(C0, 4, 3), 8, "every tail position is a tick");
assert_eq!(
tl.kth_after(C0, 9, 2),
11,
"past `real` every position ticks"
);
assert_eq!(tl.kth_after(C0, 3, 0), 3, "k = 0 is the position itself");
assert_eq!(tl.next_tick(C0, 2), 2);
assert_eq!(tl.next_tick(C0, 3), 4);
assert_eq!(tl.next_tick(C0, 5), 6);
assert_eq!(tl.next_tick(C1, 7), 7);
assert_eq!(tl.kth_after(ClockId(5), 1, 1), 2);
assert_eq!(tl.next_tick(ClockId(5), 3), 3);
let rows = Timeline::from_rows(&[&[true, false, true], &[false, true]]);
assert_eq!(rows.real(), 3);
assert!(rows.is_tick(C1, 1) && !rows.is_tick(C1, 0));
}
#[test]
fn a_malformed_timeline_is_a_named_error() {
assert_eq!(
Timeline::new(4, vec![vec![2, 2]]),
Err(TraceError::BadTimeline)
);
assert_eq!(
Timeline::new(4, vec![vec![1, 4]]),
Err(TraceError::BadTimeline),
"a tick must be a real step"
);
assert!(
TraceError::BadTimeline
.to_string()
.contains("strictly increasing")
);
}
fn cross_clock(strict: bool) -> Automaton {
let mut a = Automaton::empty();
let s0 = a.start;
let (s1, s2, s3) = (a.new_state(), a.new_state(), a.new_state());
a.add_edge(s0, Some(LeafId(0)), Transition::Epsilon, s1);
a.add_edge(s1, None, Transition::ClockSync { to: C1, strict }, s2);
a.add_edge(s2, Some(LeafId(1)), Transition::Epsilon, s3);
a.add_edge(s3, None, Transition::Match { actions: vec![] }, s3);
a
}
fn ends_on(
a: &Automaton,
start: u32,
tl: &Timeline,
leaves: &'static [&'static [bool]],
) -> Vec<u32> {
let ev = move |l: LeafId, t: u32, _: &[Option<i64>]| -> Result<i64, TraceError> {
Ok(i64::from(
leaves[l.0 as usize]
.get(t as usize)
.copied()
.unwrap_or(false),
))
};
attempt_word_in(
a,
a.start,
&[],
start,
&TailWord::weak(tl.real()),
tl.real(),
&ev,
Some(tl),
C0,
)
.unwrap()
.into_iter()
.map(|(e, _)| e)
.collect()
}
#[test]
fn strict_clock_sync_waits_for_the_nearest_strictly_later_tick() {
let tl = Timeline::from_rows(&[
&[true, false, false, false, true],
&[true, false, true, true, false],
]);
let leaves: &'static [&'static [bool]] = &[
&[true, false, false, false, false],
&[true, false, false, true, false],
];
assert!(
ends_on(&cross_clock(true), 0, &tl, leaves).is_empty(),
"tick 2 is the target and l1 is false there; tick 0 and 3 are not read"
);
let leaves: &'static [&'static [bool]] = &[
&[true, false, false, false, false],
&[true, false, true, true, false],
];
assert_eq!(ends_on(&cross_clock(true), 0, &tl, leaves), vec![2]);
}
#[test]
fn nonstrict_clock_sync_may_overlap_the_current_tick() {
let tl = Timeline::from_rows(&[
&[true, false, false, false, true],
&[true, false, true, true, false],
]);
let leaves: &'static [&'static [bool]] = &[
&[true, false, false, false, false],
&[true, false, false, false, false],
];
assert_eq!(
ends_on(&cross_clock(false), 0, &tl, leaves),
vec![0],
"tick 0 is a tick of both clocks: the match overlaps"
);
let tl = Timeline::from_rows(&[&[true, false, false, false], &[false, false, true, false]]);
let leaves: &'static [&'static [bool]] =
&[&[true, false, false, false], &[false, false, true, false]];
assert_eq!(ends_on(&cross_clock(false), 0, &tl, leaves), vec![2]);
assert_eq!(ends_on(&cross_clock(true), 0, &tl, leaves), vec![2]);
}
#[test]
fn an_attempt_aligns_its_start_to_the_entry_clock() {
let a = a_then_b();
let tl = Timeline::from_rows(&[&[false, false, true, false, true, false]]);
let leaves: &'static [&'static [bool]] = &[
&[false, false, true, false, false, false],
&[false, false, false, false, true, false],
];
assert_eq!(ends_on(&a, 0, &tl, leaves), vec![4]);
}
#[test]
fn delay_counts_ticks_of_the_threads_current_clock() {
let mut a = cross_clock(true);
let m = ThreadStateId(3);
let (d, g, e) = (a.new_state(), a.new_state(), a.new_state());
a.states[m.0 as usize].edges.clear();
a.add_edge(
m,
None,
Transition::Delay {
min: 2,
max: Some(2),
},
d,
);
a.add_edge(d, Some(LeafId(2)), Transition::Epsilon, g);
a.add_edge(g, None, Transition::Match { actions: vec![] }, g);
let _ = e;
let tl = Timeline::from_rows(&[
&[true, false, false, false, false, false, false],
&[false, true, false, true, true, false, true],
]);
let leaves: &'static [&'static [bool]] = &[
&[true, false, false, false, false, false, false],
&[false, true, false, false, false, false, false],
&[false, false, false, false, true, false, true],
];
assert_eq!(ends_on(&a, 0, &tl, leaves), vec![4]);
}
#[test]
fn without_a_timeline_a_clock_sync_is_plain_delay() {
let table: &'static [&'static [bool]] = &[&[true, false, false], &[false, true, true]];
let tr = rows(table);
let strict = attempt(&cross_clock(true), 0, 3, &tr).unwrap();
assert_eq!(strict.ends, BTreeSet::from([1]));
let loose = attempt(&cross_clock(false), 0, 3, &tr).unwrap();
assert!(
loose.ends.is_empty(),
"##0 reads l1 at tick 0, where it is false"
);
}
}