use super::incremental::{Accel, automaton_span};
use super::ir::{AbortMode, CallId, ClockId, LeafId, LocalVarId};
use super::property::{Disposition, Evaluator, Prop, PropEnv, PropertyIr};
use super::trace::{Timeline, TraceError};
use crate::sched::assert_control::Q_ALL;
use crate::sched::with_sva_leaf_time;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AssertKind {
Assert,
Assume,
CoverProperty,
CoverSequence,
Restrict,
Expect,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EventKind {
Pass,
Fail,
Match,
Call,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CallFire {
pub call: CallId,
pub args: Vec<Option<i64>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Event {
pub kind: EventKind,
pub attempt: u64,
pub vacuous: bool,
pub values: Vec<i64>,
pub tag: u64,
pub call: Option<CallFire>,
pub flags: u32,
pub time: Option<u64>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StepOutcome {
Pending,
Accept,
Fail,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StepRecord {
pub attempt: u64,
pub index: u64,
pub time: u64,
pub matched: Vec<LeafId>,
pub outcome: StepOutcome,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Stats {
pub attempted: u64,
pub passed: u64,
pub vacuous: u64,
pub failed: u64,
pub disabled: u64,
pub matched: u64,
pub killed: u64,
}
pub trait Host {
fn clock_ticked(&self, clock: ClockId, prev: &[u128]) -> bool;
fn leaf(&self, leaf: LeafId, row: &[u128], frame: &[i64]) -> i64;
fn case_pair(&self, selector: LeafId, label: LeafId, row: &[u128], frame: &[i64]) -> bool;
fn live(&self, leaf: LeafId, sampled: &[u128]) -> bool;
fn live_with(&self, leaf: LeafId, sampled: &[u128], frame: &[i64]) -> bool {
let _ = frame;
self.live(leaf, sampled)
}
fn live_all(&self, leaves: &[LeafId], sampled: &[u128], out: &mut Vec<bool>) {
out.extend(leaves.iter().map(|&l| self.live(l, sampled)));
}
}
#[derive(Clone)]
pub struct EngineConfig {
pub ir: PropertyIr,
pub kind: AssertKind,
pub n_clocks: usize,
pub leaf_reads: Vec<Vec<LocalVarId>>,
pub leaf_writes: Vec<Option<LocalVarId>>,
pub activated: bool,
pub captured: Vec<LocalVarId>,
pub disable_mixed: bool,
pub history_depth: usize,
pub hist_clocks: usize,
pub future: bool,
pub future_clock: u32,
pub live_history: bool,
pub tick_history: bool,
}
pub fn live_leaves(ir: &PropertyIr) -> Vec<LeafId> {
let mut out: Vec<LeafId> = Vec::new();
let mut add = |l: LeafId| {
if !out.contains(&l) {
out.push(l);
}
};
if let Some(d) = ir.disable {
add(d);
}
for p in &ir.props {
if let Prop::Abort { cond, mode, .. } = p
&& matches!(mode, AbortMode::Accept | AbortMode::Reject)
{
add(*cond);
}
}
out
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EngineError(pub String);
impl std::fmt::Display for EngineError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for EngineError {}
#[derive(Clone)]
struct Pos {
time: u64,
row: Row,
ticked: smallvec::SmallVec<[bool; 4]>,
disable_cur: bool,
btw: Vec<bool>,
mixed_cur: Vec<bool>,
mixed_btw: Vec<bool>,
vals: std::cell::RefCell<smallvec::SmallVec<[Option<i64>; 4]>>,
}
type Row = smallvec::SmallVec<[u128; 8]>;
#[derive(Clone)]
struct Attempt {
id: u64,
start: u64,
from: u64,
values: Vec<i64>,
tag: u64,
called_from: u64,
flags: u32,
}
#[derive(Clone)]
struct Matured {
at: u64,
tag: u64,
values: Vec<i64>,
}
#[derive(Clone)]
pub struct Engine {
cfg: EngineConfig,
live: Vec<LeafId>,
disable_live: Option<usize>,
mixed_tuples: Vec<Vec<i64>>,
mixed_now: Vec<bool>,
last_end: Vec<u128>,
disabled_now: Vec<(u64, Option<u64>)>,
held: std::collections::VecDeque<(Pos, u64, bool)>,
live_row: Vec<u128>,
history: Vec<std::collections::VecDeque<Vec<u128>>>,
positions: Vec<Pos>,
base: u64,
attempts: Vec<Attempt>,
next_attempt: u64,
step_time: Option<u64>,
started: Vec<(u64, u64)>,
step_is_tick: bool,
step_starts: bool,
matured: Vec<Matured>,
dirty: bool,
stats: Stats,
span: Option<u32>,
lookback: Vec<(ClockId, Option<u32>)>,
has_calls: bool,
auto_span: u32,
scratch_live: Vec<bool>,
scratch_ticked: Vec<bool>,
spare: std::cell::RefCell<Vec<Attempt>>,
checking: bool,
flags: u32,
accel: Option<std::cell::RefCell<Accel>>,
tracing: Tracing,
}
#[derive(Clone, Default)]
struct Tracing {
stepped: std::collections::HashMap<u64, Option<u64>>,
fail_leaf: bool,
step_new: bool,
steps: Vec<StepRecord>,
fail_leaves: Vec<(u64, Option<LeafId>)>,
}
fn differential() -> bool {
static MODE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*MODE.get_or_init(|| match std::env::var("VJ_SVA_DIFF").as_deref() {
Ok("0") => false,
Ok(_) => true,
Err(_) => cfg!(debug_assertions),
})
}
fn accel_enabled() -> bool {
static MODE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*MODE.get_or_init(|| std::env::var("VJ_SVA_ACCEL").as_deref() != Ok("0"))
}
type Judged = Result<(Vec<Event>, Stats, Vec<Attempt>, Vec<(u64, Option<u64>)>), TraceError>;
fn assert_same_outcome(fast: &Judged, oracle: &Judged) {
match (fast, oracle) {
(Ok((ev, st, keep, dis)), Ok((ev0, st0, keep0, dis0))) => {
let key = |k: &[Attempt]| -> Vec<(u64, u64, u64, u64)> {
k.iter()
.map(|a| (a.id, a.start, a.from, a.called_from))
.collect()
};
assert_eq!(ev, ev0, "incremental SVA evaluation diverged: events");
assert_eq!(st, st0, "incremental SVA evaluation diverged: counters");
assert_eq!(
key(keep),
key(keep0),
"incremental SVA evaluation diverged: pending attempts"
);
assert_eq!(
dis, dis0,
"incremental SVA evaluation diverged: disabled attempts"
);
}
(Err(e), Err(e0)) => assert_eq!(e, e0, "incremental SVA evaluation diverged: error"),
(Ok(_), Err(e0)) => {
panic!("incremental SVA evaluation succeeded where the reference failed: {e0:?}")
}
(Err(e), Ok(_)) => {
panic!("incremental SVA evaluation failed where the reference succeeded: {e:?}")
}
}
}
impl Engine {
pub fn sampled_row(&self) -> &[u128] {
&self.last_end
}
pub fn tick_row(&self) -> std::borrow::Cow<'_, [u128]> {
if self.cfg.tick_history {
std::borrow::Cow::Owned(self.position_row().into_vec())
} else {
std::borrow::Cow::Borrowed(&self.last_end)
}
}
pub fn set_sampled_words(&mut self, words: &[(usize, u128)]) {
for &(i, w) in words {
self.last_end[i] = w;
}
}
pub fn new(cfg: EngineConfig, initial_row: Vec<u128>) -> Result<Self, EngineError> {
let history = (0..(cfg.n_clocks + cfg.hist_clocks).max(1))
.map(|_| std::iter::repeat_n(initial_row.clone(), cfg.history_depth).collect())
.collect();
let has_calls = cfg.ir.automaton.has_calls();
if cfg.ir.automaton.end_points_have_calls() {
return Err(EngineError(
"a subroutine call (16.11) inside a sequence instance tested with `.triggered` or \
`.matched` is not executable: the instance's calls would have to run at its \
own end points, which the end-point status handoff does not carry"
.into(),
));
}
if has_calls && cfg.kind == AssertKind::Expect {
return Err(EngineError(
"a subroutine call (16.11) in the property of an `expect` statement is not \
executable: the statement blocks on its single outcome and the call would run \
in the Reactive region of a process that is still waiting for it"
.into(),
));
}
if has_calls && cfg.ir.props.iter().any(|p| matches!(p, Prop::Abort { .. })) {
return Err(EngineError(
"a subroutine call (16.11) in a property under `accept_on`/`reject_on` is not \
executable: the abort re-judges the attempt on a word cut at the abort, so \
which end points the calls ran at is not defined by the attempt alone"
.into(),
));
}
let mut live = live_leaves(&cfg.ir);
if let Some(d) = cfg.ir.disable
&& !cfg.leaf_reads[d.0 as usize].is_empty()
&& !cfg
.ir
.props
.iter()
.any(|p| matches!(p, Prop::Abort { cond, .. } if *cond == d))
{
live.retain(|&l| l != d);
}
let disable_live = cfg
.ir
.disable
.and_then(|d| live.iter().position(|&l| l == d));
let span = cfg.ir.automaton.max_span();
let lookback = cfg.ir.automaton.end_point_lookback();
let auto_span = automaton_span(&cfg.ir.automaton);
let locals_read = cfg.leaf_reads.iter().any(|r| !r.is_empty());
let accel = if accel_enabled() && !cfg.disable_mixed {
Accel::new(&cfg.ir, cfg.n_clocks, has_calls, locals_read).map(std::cell::RefCell::new)
} else {
None
};
Ok(Engine {
accel,
tracing: Tracing::default(),
lookback,
cfg,
live,
disable_live,
mixed_tuples: Vec::new(),
mixed_now: Vec::new(),
last_end: initial_row,
disabled_now: Vec::new(),
held: Default::default(),
live_row: Vec::new(),
history,
positions: Vec::new(),
base: 0,
attempts: Vec::new(),
next_attempt: 0,
step_time: None,
started: Vec::new(),
step_is_tick: false,
step_starts: false,
matured: Vec::new(),
dirty: false,
stats: Stats::default(),
span,
has_calls,
auto_span,
scratch_live: Vec::new(),
scratch_ticked: Vec::new(),
spare: std::cell::RefCell::new(Vec::new()),
checking: true,
flags: Q_ALL,
})
}
pub fn set_checking(&mut self, on: bool) {
self.checking = on;
}
pub fn set_action_flags(&mut self, flags: u32) {
self.flags = flags;
}
pub fn kill(&mut self) {
self.stats.killed += self.attempts.len() as u64;
self.attempts.clear();
self.matured.clear();
self.trim();
}
pub fn kind(&self) -> AssertKind {
self.cfg.kind
}
pub fn stats(&self) -> Stats {
self.stats
}
pub fn live_set(&self) -> &[LeafId] {
&self.live
}
pub fn end_of_step(&mut self, row: Vec<u128>) {
self.last_end = row;
}
pub fn end_of_step_with(&mut self, fill: impl FnOnce(&mut [u128])) {
self.last_end.fill(0);
fill(&mut self.last_end);
}
pub fn activate(&mut self, now: u64, values: Vec<i64>) {
self.activate_tagged(now, values, 0);
}
pub fn activate_tagged(&mut self, now: u64, values: Vec<i64>, tag: u64) {
let strict = self.cfg.kind == AssertKind::Expect;
self.register_tuple(&values);
if !strict && !self.cfg.future && self.step_time == Some(now) && self.step_starts {
self.start_attempt(values, tag);
self.dirty = true;
} else {
self.matured.push(Matured {
at: now,
tag,
values,
});
}
}
fn register_tuple(&mut self, values: &[i64]) {
if self.cfg.disable_mixed && !self.mixed_tuples.iter().any(|t| t == values) {
self.mixed_tuples.push(values.to_vec());
self.dirty = true;
}
}
pub fn live_tuples(&self) -> usize {
self.mixed_tuples.len()
}
fn prune_tuples(&mut self) {
if self.mixed_tuples.is_empty() {
return;
}
let keep: Vec<bool> = self
.mixed_tuples
.iter()
.map(|t| {
self.attempts.iter().any(|a| a.values == *t)
|| self.matured.iter().any(|m| m.values == *t)
})
.collect();
if keep.iter().all(|&k| k) {
return;
}
let prune = |v: &mut Vec<bool>| {
let mut i = 0;
v.retain(|_| {
let k = keep.get(i).copied().unwrap_or(true);
i += 1;
k
});
};
let mut i = 0;
self.mixed_tuples.retain(|_| {
let k = keep[i];
i += 1;
k
});
prune(&mut self.mixed_now);
for p in &mut self.positions {
prune(&mut p.mixed_cur);
prune(&mut p.mixed_btw);
}
for (p, _, _) in &mut self.held {
prune(&mut p.mixed_cur);
prune(&mut p.mixed_btw);
}
}
fn start_attempt(&mut self, values: Vec<i64>, tag: u64) {
self.register_tuple(&values);
let start = self.base + self.positions.len() as u64 - 1;
self.attempts.push(Attempt {
id: self.next_attempt,
start,
from: start,
called_from: start,
values,
tag,
flags: self.flags,
});
if self.cfg.future {
let at = self.positions.last().map_or(0, |p| p.time);
self.started.push((self.next_attempt, at));
}
if self.tracing.step_new {
self.tracing.stepped.insert(self.next_attempt, None);
}
self.next_attempt += 1;
self.stats.attempted += 1;
}
pub fn set_step(&mut self, id: u64, on: bool) {
if on {
self.tracing.stepped.entry(id).or_insert(None);
} else {
self.tracing.stepped.remove(&id);
}
}
pub fn set_step_new(&mut self, on: bool) {
self.tracing.step_new = on;
}
pub fn set_fail_leaf(&mut self, on: bool) {
self.tracing.fail_leaf = on;
}
pub fn take_started(&mut self) -> Vec<(u64, u64)> {
std::mem::take(&mut self.started)
}
pub fn take_steps(&mut self) -> Vec<StepRecord> {
std::mem::take(&mut self.tracing.steps)
}
pub fn take_fail_leaves(&mut self) -> Vec<(u64, Option<LeafId>)> {
std::mem::take(&mut self.tracing.fail_leaves)
}
pub fn kill_attempt(&mut self, id: u64) -> bool {
let before = self.attempts.len();
self.attempts.retain(|a| a.id != id);
let killed = self.attempts.len() != before;
if killed {
self.stats.killed += (before - self.attempts.len()) as u64;
self.trim();
}
killed
}
pub fn disabled_attempts(&self) -> &[(u64, Option<u64>)] {
&self.disabled_now
}
pub fn fire(&mut self, host: &dyn Host, now: u64) -> Result<Vec<Event>, TraceError> {
self.disabled_now.clear();
if self.cfg.kind == AssertKind::Restrict {
return Ok(Vec::new());
}
let first = self.step_time != Some(now);
self.step_time = Some(now);
if first && self.cfg.live_history {
self.live_row = self.position_row().into_vec();
}
let mut live_now = std::mem::take(&mut self.scratch_live);
live_now.clear();
let mut live_ready = false;
let live_pending =
!self.attempts.is_empty() || !self.held.is_empty() || !self.matured.is_empty();
let mut pushed = false;
let mut live_hit = false;
self.mixed_now.clear();
if self.cfg.disable_mixed
&& let Some(d) = self.cfg.ir.disable
{
let mut frame = vec![0i64; self.cfg.ir.automaton.local_vars.len()];
for t in &self.mixed_tuples {
for (slot, v) in self.cfg.captured.iter().zip(t) {
frame[slot.0 as usize] = *v;
}
self.mixed_now
.push(host.live_with(d, &self.live_row, &frame));
}
}
let mut ticked = std::mem::take(&mut self.scratch_ticked);
if first {
self.step_is_tick = false;
self.step_starts = false;
ticked.clear();
let tick_row = self.tick_row();
ticked.extend(
(0..self.cfg.n_clocks + self.cfg.hist_clocks)
.map(|c| host.clock_ticked(ClockId(c as u32), &tick_row)),
);
drop(tick_row);
if ticked.iter().any(|&t| t) {
let own = &ticked[..self.cfg.n_clocks];
let entry = self.cfg.ir.automaton.entry_clock.0 as usize;
let starts = own.get(entry).copied().unwrap_or(false);
if !live_ready && !self.live.is_empty() {
host.live_all(&self.live, &self.live_row, &mut live_now);
live_ready = true;
}
let disable_cur = self.disable_live.is_some_and(|i| live_now[i]);
let pos = own.iter().any(|&t| t).then(|| Pos {
time: now,
row: self.position_row(),
ticked: own.iter().copied().collect(),
disable_cur,
btw: vec![false; self.live.len()],
mixed_cur: self.mixed_now.clone(),
mixed_btw: Vec::new(),
vals: Default::default(),
});
if self.cfg.history_depth > 0 {
for (c, clock) in self.history.iter_mut().enumerate() {
if ticked.get(c).copied().unwrap_or(false) {
clock.push_front(self.last_end.clone());
clock.truncate(self.cfg.history_depth);
}
}
}
if pos.is_none() {
if live_pending && !live_ready && !self.live.is_empty() {
host.live_all(&self.live, &self.live_row, &mut live_now);
}
live_hit |= self.record_between(&live_now);
}
if self.cfg.future
&& ticked
.get(self.cfg.future_clock as usize)
.copied()
.unwrap_or(false)
{
while let Some((mut prev, at, prev_starts)) = self.held.pop_front() {
prev.row.extend_from_slice(&self.last_end);
self.release(prev, at, prev_starts);
pushed = true;
}
}
if let Some(pos) = pos {
self.step_is_tick = true;
self.step_starts = starts;
if self.cfg.future {
self.held.push_back((pos, now, starts));
} else {
self.release(pos, now, starts);
pushed = true;
}
}
} else {
if live_pending && !live_ready && !self.live.is_empty() {
host.live_all(&self.live, &self.live_row, &mut live_now);
}
live_hit |= self.record_between(&live_now);
}
} else if self.step_is_tick {
if live_pending && !live_ready && !self.live.is_empty() {
host.live_all(&self.live, &self.live_row, &mut live_now);
}
if let Some(i) = self.disable_live
&& live_now.get(i).copied().unwrap_or(false)
&& let Some(last) = self.newest_mut()
&& !last.disable_cur
{
last.disable_cur = true;
live_hit = true;
}
let mixed = std::mem::take(&mut self.mixed_now);
if let Some(last) = self.newest_mut() {
for (i, &v) in mixed.iter().enumerate() {
if v && !last.mixed_cur.get(i).copied().unwrap_or(false) {
if last.mixed_cur.len() <= i {
last.mixed_cur.resize(i + 1, false);
}
last.mixed_cur[i] = true;
live_hit = true;
}
}
}
self.mixed_now = mixed;
} else {
if live_pending && !live_ready && !self.live.is_empty() {
host.live_all(&self.live, &self.live_row, &mut live_now);
}
live_hit |= self.record_between(&live_now);
}
self.scratch_live = live_now;
self.scratch_ticked = ticked;
if !pushed && !live_hit && !self.dirty {
return Ok(Vec::new());
}
self.dirty = false;
let events = self.evaluate(host)?;
self.trim();
Ok(events)
}
fn release(&mut self, pos: Pos, at: u64, starts: bool) {
self.positions.push(pos);
if !starts {
return;
}
if self.cfg.activated {
let strict = self.cfg.kind == AssertKind::Expect;
let (ready, later): (Vec<Matured>, Vec<Matured>) = std::mem::take(&mut self.matured)
.into_iter()
.partition(|m| if strict { m.at < at } else { m.at <= at });
self.matured = later;
for m in ready {
self.start_attempt(m.values, m.tag);
}
} else if self.checking {
self.start_attempt(Vec::new(), 0);
}
}
fn newest_mut(&mut self) -> Option<&mut Pos> {
match self.held.back_mut() {
Some((pos, _, _)) => Some(pos),
None => self.positions.last_mut(),
}
}
fn position_row(&self) -> Row {
let mut row = Row::from_slice(&self.last_end);
for clock in &self.history {
for earlier in clock {
row.extend_from_slice(earlier);
}
}
row
}
fn record_between(&mut self, live_now: &[bool]) -> bool {
let mixed = std::mem::take(&mut self.mixed_now);
let Some(last) = self.newest_mut() else {
self.mixed_now = mixed;
return false;
};
let mut hit = false;
for (i, &v) in live_now.iter().enumerate() {
if v && !last.btw[i] {
last.btw[i] = true;
hit = true;
}
}
for (i, &v) in mixed.iter().enumerate() {
if v && !last.mixed_btw.get(i).copied().unwrap_or(false) {
if last.mixed_btw.len() <= i {
last.mixed_btw.resize(i + 1, false);
}
last.mixed_btw[i] = true;
hit = true;
}
}
self.mixed_now = mixed;
hit
}
fn evaluate(&mut self, host: &dyn Host) -> Result<Vec<Event>, TraceError> {
if self.attempts.is_empty() {
return Ok(Vec::new());
}
let len = self.positions.len() as u32;
let timeline = if self.cfg.n_clocks > 1 {
let ticks = (0..self.cfg.n_clocks)
.map(|c| {
self.positions
.iter()
.enumerate()
.filter(|(_, p)| p.ticked[c])
.map(|(i, _)| i as u32)
.collect()
})
.collect();
Some(Timeline::new(len, ticks)?)
} else {
None
};
let attempts = std::mem::take(&mut self.attempts);
let oracle_input = (self.accel.is_some() && differential()).then(|| attempts.clone());
if let Some(accel) = &self.accel {
accel.borrow_mut().set_base(self.base);
}
let mut traced = Traced::default();
let ran = self.judge_all(
host,
timeline.as_ref(),
len,
attempts,
self.accel.as_ref(),
Some(&mut traced),
);
if let Some(input) = oracle_input {
let oracle = self.judge_all(host, timeline.as_ref(), len, input, None, None);
assert_same_outcome(&ran, &oracle);
}
let (events, stats, keep, disabled) = ran?;
self.record_traced(traced, len, &events);
self.stats = stats;
self.attempts = keep;
self.disabled_now.extend(disabled);
Ok(events)
}
fn judge_all(
&self,
host: &dyn Host,
timeline: Option<&Timeline>,
len: u32,
attempts: Vec<Attempt>,
accel: Option<&std::cell::RefCell<Accel>>,
mut traced: Option<&mut Traced>,
) -> Judged {
let strong = self.cfg.kind == AssertKind::CoverProperty;
let mut events = Vec::new();
let mut stats = self.stats;
let mut keep = std::mem::take(&mut *self.spare.borrow_mut());
keep.clear();
let mut disabled = Vec::new();
let env_for = |values: &[i64]| WindowEnv {
host,
positions: &self.positions,
disable: self.cfg.ir.disable,
live: &self.live,
leaf_reads: &self.cfg.leaf_reads,
leaf_writes: &self.cfg.leaf_writes,
timeline,
captured: &self.cfg.captured,
values: values.to_vec(),
mixed: self.cfg.disable_mixed.then_some(&self.mixed_tuples[..]),
log: None,
};
let judge = Judge {
kind: self.cfg.kind,
span: self.span,
multiclock: timeline.is_some(),
has_calls: self.has_calls,
base: self.base,
len,
times: if self.cfg.future {
self.positions.iter().map(|p| p.time).collect()
} else {
Vec::new()
},
};
let wants_trace =
|att: &Attempt| self.tracing.fail_leaf || self.tracing.stepped.contains_key(&att.id);
let (mut attempts, traced_atts): (Vec<Attempt>, Vec<Attempt>) = match traced.as_ref() {
Some(_) if self.tracing.fail_leaf || !self.tracing.stepped.is_empty() => {
attempts.into_iter().partition(|a| !wants_trace(a))
}
_ => (attempts, Vec::new()),
};
for att in traced_atts {
let mut env = env_for(&att.values);
env.log = Some(std::cell::RefCell::new(Vec::new()));
let (id, start) = (att.id, att.start);
{
let ev = Evaluator::new_with_span(&self.cfg.ir, &env, strong, self.auto_span)
.open_future();
judge.run(&ev, att, &mut events, &mut stats, &mut keep, &mut disabled)?;
}
if let (Some(t), Some(log)) = (traced.as_deref_mut(), env.log.take()) {
t.logs.push((id, start, log.into_inner()));
}
}
if self.cfg.captured.is_empty() {
let env = env_for(&[]);
let ev = Evaluator::new_with_span(&self.cfg.ir, &env, strong, self.auto_span)
.open_future()
.with_accel(accel);
for att in attempts.drain(..) {
judge.run(&ev, att, &mut events, &mut stats, &mut keep, &mut disabled)?;
}
} else {
for att in attempts.drain(..) {
let env = env_for(&att.values);
let ev = Evaluator::new_with_span(&self.cfg.ir, &env, strong, self.auto_span)
.open_future()
.with_accel(accel);
judge.run(&ev, att, &mut events, &mut stats, &mut keep, &mut disabled)?;
}
}
*self.spare.borrow_mut() = attempts;
Ok((events, stats, keep, disabled))
}
fn trim(&mut self) {
self.prune_tuples();
let newest = if self.cfg.activated && self.step_starts && !self.positions.is_empty() {
self.base + self.positions.len() as u64 - 1
} else {
self.base + self.positions.len() as u64
};
let keep_from = self
.attempts
.first()
.map_or(newest, |a| a.start.min(newest));
let mut keep_from = keep_from;
for &(clock, span) in &self.lookback {
let Some(n) = span else {
keep_from = self.base;
break;
};
let mut need = n + 1;
let mut p = keep_from;
while p > self.base && need > 0 {
p -= 1;
if self.positions[(p - self.base) as usize]
.ticked
.get(clock.0 as usize)
.copied()
.unwrap_or(true)
{
need -= 1;
}
}
keep_from = keep_from.min(p);
}
let drop_n = (keep_from - self.base) as usize;
if drop_n == 0 {
return;
}
self.positions.drain(..drop_n);
self.base = keep_from;
let base = self.base;
if let Some(accel) = &self.accel {
accel.borrow_mut().evict_before(base);
}
}
}
#[derive(Default)]
struct Traced {
logs: Vec<(u64, u64, LeafLog)>,
}
type LeafLog = Vec<(u32, LeafId, i64)>;
impl Engine {
fn record_traced(&mut self, traced: Traced, len: u32, events: &[Event]) {
if len == 0 {
return;
}
let newest = len - 1;
let abs_newest = self.base + u64::from(newest);
for (id, start, log) in traced.logs {
let failed = events
.iter()
.any(|e| e.attempt == id && e.kind == EventKind::Fail);
let passed = events
.iter()
.any(|e| e.attempt == id && e.kind == EventKind::Pass);
let fail_leaf = log
.iter()
.rev()
.find(|(t, _, v)| *t == newest && *v == 0)
.or_else(|| log.last())
.map(|&(_, l, _)| l);
if failed && self.tracing.fail_leaf {
self.tracing.fail_leaves.push((id, fail_leaf));
}
let Some(reported) = self.tracing.stepped.get_mut(&id) else {
continue;
};
let first = reported.map_or(start, |r| r + 1);
if first > abs_newest {
continue;
}
*reported = Some(abs_newest);
let outcome = if failed {
StepOutcome::Fail
} else if passed {
StepOutcome::Accept
} else {
StepOutcome::Pending
};
for abs in first.max(self.base)..=abs_newest {
let tick = (abs - self.base) as u32;
let last = abs == abs_newest;
let mut matched: Vec<LeafId> = Vec::new();
for &(t, l, v) in &log {
if t == tick && v != 0 && !matched.contains(&l) {
matched.push(l);
}
}
let outcome = if last { outcome } else { StepOutcome::Pending };
if outcome == StepOutcome::Fail
&& let Some(l) = fail_leaf
{
matched.retain(|&m| m != l);
matched.push(l);
}
self.tracing.steps.push(StepRecord {
attempt: id,
index: abs - start,
time: self.positions[tick as usize].time,
matched,
outcome,
});
}
if outcome != StepOutcome::Pending {
self.tracing.stepped.remove(&id);
}
}
}
}
struct Judge {
kind: AssertKind,
span: Option<u32>,
multiclock: bool,
has_calls: bool,
base: u64,
len: u32,
times: Vec<u64>,
}
impl Judge {
fn verdict_time(&self, ev: &Evaluator<'_>, rel: u32) -> Result<Option<u64>, TraceError> {
if self.times.is_empty() {
return Ok(None);
}
for l in rel + 1..self.len {
if !matches!(ev.attempt(l, rel)?, Disposition::Pending) {
return Ok(self.times.get(l as usize - 1).copied());
}
}
Ok(self.times.get(self.len as usize - 1).copied())
}
fn tick_time(&self, t: u32) -> Option<u64> {
self.times
.get(t as usize)
.or_else(|| self.times.last())
.copied()
}
fn run(
&self,
ev: &Evaluator<'_>,
mut att: Attempt,
events: &mut Vec<Event>,
stats: &mut Stats,
keep: &mut Vec<Attempt>,
disabled: &mut Vec<(u64, Option<u64>)>,
) -> Result<(), TraceError> {
let (len, kind) = (self.len, self.kind);
let rel = (att.start - self.base) as u32;
let event = |att: &Attempt, kind: EventKind| Event {
kind,
attempt: att.id,
vacuous: false,
values: att.values.clone(),
tag: att.tag,
call: None,
flags: att.flags,
time: None,
};
if self.has_calls {
let calls = match kind {
AssertKind::CoverSequence => ev.sequence_calls(len, rel)?,
AssertKind::Assert
| AssertKind::Assume
| AssertKind::CoverProperty
| AssertKind::Expect => ev.scheduled_calls(len, rel)?,
AssertKind::Restrict => unreachable!("a restrict property is never fired"),
};
for c in calls {
if self.base + u64::from(c.tick) >= att.called_from {
events.push(Event {
call: Some(CallFire {
call: c.call,
args: c.args,
}),
..event(&att, EventKind::Call)
});
}
}
att.called_from = self.base + u64::from(len);
}
match kind {
AssertKind::Assert
| AssertKind::Assume
| AssertKind::CoverProperty
| AssertKind::Expect => {
let report = kind != AssertKind::CoverProperty;
match ev.attempt(len, rel)? {
Disposition::Pending => keep.push(att),
Disposition::Passes => {
let vacuous = !ev.nonvacuous(len, rel)?;
stats.passed += 1;
stats.vacuous += u64::from(vacuous);
events.push(Event {
vacuous,
time: self.verdict_time(ev, rel)?,
..event(&att, EventKind::Pass)
});
}
Disposition::Fails => {
stats.failed += 1;
if report {
events.push(Event {
time: self.verdict_time(ev, rel)?,
..event(&att, EventKind::Fail)
});
}
}
Disposition::Disabled => {
stats.disabled += 1;
let at = ev.disabling_prefix(len, rel)?;
disabled.push((att.id, at.and_then(|p| self.tick_time(p))));
}
}
}
AssertKind::CoverSequence => {
let disabled_at = ev.disabling_prefix(len, rel)?;
let from = (att.from - self.base) as u32;
for (end, _) in ev.root_sequence_matches(len, rel)? {
if end >= from && disabled_at.is_none_or(|p| end < p) {
stats.matched += 1;
events.push(Event {
time: self.tick_time(end),
..event(&att, EventKind::Match)
});
}
}
att.from = self.base + u64::from(len);
if let Some(p) = disabled_at {
stats.disabled += 1;
disabled.push((att.id, self.tick_time(p)));
} else if self.multiclock {
if !ev.sequence_exhausted(len, rel)? {
keep.push(att);
}
} else if self.span.is_none_or(|s| len - 1 < rel.saturating_add(s)) {
keep.push(att);
}
}
AssertKind::Restrict => unreachable!("a restrict property is never fired"),
}
Ok(())
}
}
struct WindowEnv<'a> {
host: &'a dyn Host,
positions: &'a [Pos],
disable: Option<LeafId>,
live: &'a [LeafId],
leaf_reads: &'a [Vec<LocalVarId>],
leaf_writes: &'a [Option<LocalVarId>],
timeline: Option<&'a Timeline>,
captured: &'a [LocalVarId],
values: Vec<i64>,
mixed: Option<&'a [Vec<i64>]>,
log: Option<std::cell::RefCell<Vec<(u32, LeafId, i64)>>>,
}
impl WindowEnv<'_> {
fn note(&self, tick: u32, leaf: LeafId, v: i64) {
if let Some(log) = &self.log {
log.borrow_mut().push((tick, leaf, v));
}
}
fn host_frame(&self, leaves: &[LeafId], frame: &[Option<i64>]) -> Result<Vec<i64>, TraceError> {
for l in leaves {
for v in &self.leaf_reads[l.0 as usize] {
if frame.get(v.0 as usize).copied().flatten().is_none() {
return Err(TraceError::UnassignedRead(*v));
}
}
}
Ok(frame.iter().map(|v| v.unwrap_or(0)).collect())
}
fn mixed_index(&self) -> Option<usize> {
self.mixed?.iter().position(|t| *t == self.values)
}
fn reads_locals(&self, leaf: LeafId) -> bool {
!self.leaf_reads[leaf.0 as usize].is_empty() || self.leaf_writes[leaf.0 as usize].is_some()
}
}
impl WindowEnv<'_> {
fn value_of(&self, leaf: LeafId, tick: u32, frame: &[Option<i64>]) -> Result<i64, TraceError> {
let pos = &self.positions[tick as usize];
if Some(leaf) == self.disable
&& let Some(i) = self.mixed_index()
{
return Ok(i64::from(pos.mixed_cur.get(i).copied().unwrap_or(false)));
}
if Some(leaf) == self.disable && !self.reads_locals(leaf) {
return Ok(i64::from(pos.disable_cur));
}
if !self.reads_locals(leaf) {
let slot = leaf.0 as usize;
if let Some(Some(v)) = pos.vals.borrow().get(slot) {
return Ok(*v);
}
let v = with_sva_leaf_time(pos.time, || self.host.leaf(leaf, &pos.row, &[]));
let mut vals = pos.vals.borrow_mut();
if vals.is_empty() {
vals.resize(self.leaf_reads.len(), None);
}
vals[slot] = Some(v);
return Ok(v);
}
let f = self.host_frame(&[leaf], frame)?;
Ok(with_sva_leaf_time(pos.time, || {
self.host.leaf(leaf, &pos.row, &f)
}))
}
}
impl PropEnv for WindowEnv<'_> {
fn value(&self, leaf: LeafId, tick: u32, frame: &[Option<i64>]) -> Result<i64, TraceError> {
let v = self.value_of(leaf, tick, frame)?;
if Some(leaf) != self.disable {
self.note(tick, leaf, v);
}
Ok(v)
}
fn case_match(
&self,
selector: LeafId,
label: LeafId,
tick: u32,
frame: &[Option<i64>],
) -> Result<bool, TraceError> {
let f = self.host_frame(&[selector, label], frame)?;
let m = self
.host
.case_pair(selector, label, &self.positions[tick as usize].row, &f);
self.note(tick, selector, 1);
self.note(tick, label, i64::from(m));
Ok(m)
}
fn between(&self, leaf: LeafId, after: u32) -> Result<bool, TraceError> {
if Some(leaf) == self.disable
&& let Some(i) = self.mixed_index()
{
return Ok(self.positions[after as usize]
.mixed_btw
.get(i)
.copied()
.unwrap_or(false));
}
let Some(i) = self.live.iter().position(|&l| l == leaf) else {
return Ok(false);
};
Ok(self.positions[after as usize].btw[i])
}
fn timeline(&self) -> Option<&Timeline> {
self.timeline
}
fn initial_frame(&self) -> Vec<(LocalVarId, i64)> {
self.captured
.iter()
.copied()
.zip(self.values.iter().copied())
.collect()
}
}
#[cfg(test)]
mod tests;