use std::collections::HashMap;
use std::path::Path;
#[cfg(test)]
use std::collections::HashSet;
use sv_lang::kinds::{ExpressionKind, SymbolKind};
use sv_lang::{EvalSession, Symbol};
#[cfg(test)]
use vj_abi::bit4::Bit4;
use vj_codegen::error::LoweringError;
use vj_codegen::process_lower::SignalSlot;
#[cfg(test)]
use vj_exec::timing::specify::CondEval;
use vj_sim::netlist::InstanceId;
#[cfg(test)]
use vj_sim::netlist::specify::CondBinaryOp;
use vj_sim::netlist::specify::EdgeTransition;
use vj_sim::netlist::specify::{CheckArgs, EdgeControl, EventPairArgs, TerminalRef, TimingModel};
use vj_sim::symid::{SymbolId, SymbolUid};
use crate::negtiming::{
Constraint, DelayTable, Explicit, Limit, NegativeCheck, Role, Solution, solve,
};
use crate::nets::Resolution;
use crate::primitives::{Loc, bank_slot, loc_of};
use crate::run::RunError;
use crate::specify::{CondPlan, compile_cond_plan};
use crate::timing_options::TimingOptions;
use vj_exec::timing::negtiming::LinePlan;
use vj_exec::timing::check::CheckPlan as ExecCheck;
pub(crate) use vj_exec::timing::check::*;
pub(crate) fn vpi_edge(a: &sv_lang::SystemTimingCheckArg<'_>) -> i32 {
let header = |n: &str| crate::vpi::consts::value_of(n).expect("a header constant") as i32;
match a.edge {
sv_lang::EdgeKind::PosEdge => header("vpiPosedge"),
sv_lang::EdgeKind::NegEdge => header("vpiNegedge"),
sv_lang::EdgeKind::BothEdges if a.edge_descriptors.is_empty() => header("vpiAnyEdge"),
sv_lang::EdgeKind::BothEdges => a
.edge_descriptors
.iter()
.map(|d| {
let d = d.to_ascii_lowercase().replace('z', "x");
header(match d.as_str() {
"01" => "vpiEdge01",
"10" => "vpiEdge10",
"0x" => "vpiEdge0x",
"x1" => "vpiEdgex1",
"1x" => "vpiEdge1x",
"x0" => "vpiEdgex0",
_ => "vpiNoEdge",
})
})
.fold(0, |acc, e| acc | e),
_ => header("vpiNoEdge"),
}
}
pub(crate) fn sdf_edge_control(edge: vj_sdf::ast::Edge) -> Result<EdgeControl, String> {
use vj_sdf::ast::Edge;
use vj_sim::netlist::specify::EdgeSet;
Ok(match edge {
Edge::Posedge => EdgeControl::Edges(EdgeSet::POSEDGE),
Edge::Negedge => EdgeControl::Edges(EdgeSet::NEGEDGE),
Edge::T01 => EdgeControl::Edges(EdgeSet::EMPTY.with(EdgeTransition::ZeroOne)),
Edge::T10 => EdgeControl::Edges(EdgeSet::EMPTY.with(EdgeTransition::OneZero)),
Edge::T0z | Edge::Tz1 | Edge::T1z | Edge::Tz0 => {
return Err(format!(
"the edge {edge:?} has no SystemVerilog edge identifier (posedge/negedge cover \
01 and 10 only)"
));
}
})
}
#[cfg(test)]
pub(crate) fn test_event(
terminal: TerminalRef,
edge: sv_lang::EdgeKind,
condition: Option<vj_sim::netlist::specify::CondNode>,
) -> vj_sim::netlist::specify::CheckEvent {
vj_sim::netlist::specify::CheckEvent {
terminal,
edge,
edge_descriptors: Vec::new(),
edge_control: EdgeControl::from_syntax(edge, &[]).unwrap(),
condition,
}
}
pub(crate) fn timing_check_symbols<'d>(
infos: &[(InstanceId, Symbol<'d>)],
) -> HashMap<SymbolId, Symbol<'d>> {
let mut out = HashMap::new();
for &(_, body) in infos {
for m in vj_sim::netlist::module_items(&body) {
if m.kind() != SymbolKind::SpecifyBlock {
continue;
}
for inner in m.members() {
if inner.kind() == SymbolKind::SystemTimingCheck {
out.insert(inner.uid(), inner);
}
}
}
}
out
}
fn limit_to_ticks(c: sv_lang::ConstantValue, scope: &Symbol<'_>, tick: i32) -> Result<i64, String> {
let v = match c {
sv_lang::ConstantValue::Real(f) => f,
c => c
.as_i64()
.ok_or_else(|| "a limit that is not a number".to_string())? as f64,
};
let ticks = vj_codegen::timescale::delay_ticks(v.abs(), scope, tick).map_err(|e| e.message)?;
let ticks = i64::try_from(ticks).map_err(|_| "a limit too large for 64 bits".to_string())?;
Ok(if v < 0.0 { -ticks } else { ticks })
}
pub(crate) fn attach_limits<'d>(
eval: &EvalSession<'d>,
model: &mut TimingModel,
infos: &[(InstanceId, Symbol<'d>)],
_path: &Path,
) -> Result<(), RunError> {
let symbols = timing_check_symbols(infos);
for entry in &mut model.timing_checks {
let sym = symbols.get(&entry.check_uid).unwrap_or_else(|| {
panic!(
"timing check {} was discovered but is not reachable from the same infos",
entry.check_uid
)
});
let args: Vec<_> = sym.system_timing_check_arguments().collect();
let tick = vj_codegen::timescale::design_tick_exp(sym);
for limit in entry.args.limits_mut() {
let fail = |m: String| {
RunError::Lowering(LoweringError::unsupported_at(
&format!("timing check limit: {m}"),
sym,
))
};
let e = args
.get(limit.arg_index as usize)
.and_then(|a| a.expr)
.ok_or_else(|| fail("a limit argument with no expression".to_string()))?;
let e = if e.kind() == ExpressionKind::MinTypMax {
e.children()
.into_iter()
.filter_map(|n| n.as_expression())
.nth(1)
.ok_or_else(|| fail("a min:typ:max limit with no typical value".to_string()))?
} else {
e
};
let c = e
.constant_value()
.or_else(|| eval.eval_constant(e))
.ok_or_else(|| fail("a limit that is not a constant".to_string()))?;
limit.ticks = limit_to_ticks(c, sym, tick).map_err(fail)?;
}
}
Ok(())
}
struct CheckPlan {
entry: usize,
spec: CheckSpec,
reference: WatchPlan,
data: WatchPlan,
trailing: Option<WatchPlan>,
notifier: Option<(Loc, u32)>,
reference_label: String,
data_label: String,
instance: InstanceId,
reference_signal: SymbolId,
data_signal: SymbolId,
timestamp: Option<CondPlan>,
timecheck: Option<CondPlan>,
}
#[derive(Default)]
pub(crate) struct Plans {
checks: Vec<CheckPlan>,
lines: Vec<LinePlan>,
live: Vec<LiveCheck>,
negative_timing: bool,
}
impl Plans {
pub(crate) fn spec(&self) -> ChecksPlan {
ChecksPlan {
checks: self
.checks
.iter()
.map(|c| ExecCheck {
entry: c.entry,
spec: c.spec,
reference: c.reference.clone(),
data: c.data.clone(),
trailing: c.trailing.clone(),
notifier: c.notifier,
reference_label: c.reference_label.clone(),
data_label: c.data_label.clone(),
timestamp: c.timestamp.clone(),
timecheck: c.timecheck.clone(),
})
.collect(),
lines: self.lines.clone(),
live: self.live.clone(),
negative_timing: self.negative_timing,
}
}
}
fn terminal_label(terminal: &TerminalRef) -> String {
match terminal.1 {
None => terminal.0.to_string(),
Some((lo, 1)) => format!("{}[{lo}]", terminal.0),
Some((lo, width)) => format!("{}[{}:{lo}]", terminal.0, lo + width - 1),
}
}
fn terminal_window(
terminal: &TerminalRef,
id: InstanceId,
scope: &Scope<'_>,
what: &str,
) -> Result<(InstanceId, SignalSlot, u32, u32), String> {
let (owner, slot) = scope.slot_of(id, &terminal.0).ok_or_else(|| {
format!(
"{what} `{}` has no resolved signal slot in its own instance (unexpected: discovery \
already validated this reference's existence)",
terminal.0
)
})?;
let (lo, width) = terminal.1.unwrap_or((0, slot.width));
if lo.checked_add(width).is_none_or(|end| end > slot.width) {
return Err(format!(
"{what} `{}` selects bits {lo}+:{width} of a {}-bit signal",
terminal.0, slot.width
));
}
Ok((owner, slot, lo, width))
}
pub(crate) struct Scope<'a> {
pub(crate) own: &'a HashMap<InstanceId, HashMap<SymbolId, SignalSlot>>,
pub(crate) iface: &'a HashMap<InstanceId, HashMap<SymbolId, (InstanceId, SignalSlot)>>,
}
impl Scope<'_> {
fn slot_of(&self, id: InstanceId, uid: &SymbolId) -> Option<(InstanceId, SignalSlot)> {
self.own
.get(&id)
.and_then(|m| m.get(uid))
.map(|&s| (id, s))
.or_else(|| self.iface.get(&id).and_then(|m| m.get(uid)).copied())
}
}
fn plan_event(
event: &vj_sim::netlist::specify::CheckEvent,
instance: InstanceId,
scope: &Scope<'_>,
own: &HashMap<SymbolId, SignalSlot>,
model: &TimingModel,
what: &str,
) -> Result<WatchPlan, String> {
let (owner, slot, lo, width) = terminal_window(&event.terminal, instance, scope, what)?;
let conditions = event
.condition
.as_ref()
.map(|c| compile_cond_plan(c, own, instance, model))
.transpose()
.map_err(|m| format!("{what}'s condition: {m}"))?
.into_iter()
.collect();
Ok(WatchPlan {
loc: loc_of(owner, slot),
lo,
width,
control: event.edge_control,
conditions,
})
}
fn plan_role_conditions(
args: &CheckArgs,
instance: InstanceId,
own: &HashMap<SymbolId, SignalSlot>,
model: &TimingModel,
name: &str,
) -> Result<(Option<CondPlan>, Option<CondPlan>), String> {
let (timestamp, timecheck) = match args {
CheckArgs::SetupHold(a) => (&a.timestamp_cond, &a.timecheck_cond),
CheckArgs::RecRem(a) => (&a.timestamp_cond, &a.timecheck_cond),
_ => return Ok((None, None)),
};
let compile = |c: &Option<_>, which: &str| {
c.as_ref()
.map(|c| compile_cond_plan(c, own, instance, model))
.transpose()
.map_err(|m| format!("{name}'s {which}: {m}"))
};
Ok((
compile(timestamp, "timestamp_condition")?,
compile(timecheck, "timecheck_condition")?,
))
}
pub(crate) fn negative_timing(
model: &mut TimingModel,
scope: &Scope<'_>,
options: &TimingOptions,
) -> Result<DelayTable, String> {
let mut table = DelayTable::new(options.negative_timing_checks);
let mut constraints: Vec<Constraint> = Vec::new();
let mut named: Vec<(usize, String, [&'static str; 2])> = Vec::new();
for (index, entry) in model.timing_checks.iter().enumerate() {
let (reference, data, first, second, delayed, limit_names, check) = match &entry.args {
CheckArgs::SetupHold(a) => (
&a.reference,
&a.data,
a.setup_limit.ticks,
a.hold_limit.ticks,
[&a.delayed_reference, &a.delayed_data],
["setup", "hold"],
"$setuphold",
),
CheckArgs::RecRem(a) => (
&a.reference,
&a.data,
a.removal_limit.ticks,
a.recovery_limit.ticks,
[&a.delayed_reference, &a.delayed_data],
["removal", "recovery"],
"$recrem",
),
_ => continue,
};
let name = format!("{check} {}", entry.check_uid);
if !scope.own.contains_key(&entry.instance) {
return Err(format!("{name} is in an instance with no signal table"));
}
let (r_owner, r_slot, r_lo, r_width) = terminal_window(
&reference.terminal,
entry.instance,
scope,
&format!("{name}'s reference terminal"),
)?;
let (d_owner, d_slot, d_lo, d_width) = terminal_window(
&data.terminal,
entry.instance,
scope,
&format!("{name}'s data terminal"),
)?;
let r_node = table.node(entry.instance, r_owner, &reference.terminal.0, r_slot);
let d_node = table.node(entry.instance, d_owner, &data.terminal.0, d_slot);
for (net, node, source_lo, source_width, what) in [
(delayed[0], r_node, r_lo, r_width, "delayed_reference"),
(delayed[1], d_node, d_lo, d_width, "delayed_data"),
] {
let Some(net) = net else { continue };
let (owner, slot, lo, width) =
terminal_window(net, entry.instance, scope, &format!("{name}'s {what}"))?;
table.add_explicit(
node,
Explicit {
loc: loc_of(owner, slot),
dst_lo: lo,
source_lo,
width: width.min(source_width),
},
);
}
constraints.push(Constraint {
reference: r_node,
data: d_node,
data_first: first,
data_second: second,
});
named.push((index, name, limit_names));
}
let solution = if table.enabled {
solve(table.signals.len(), &constraints)
} else {
Solution {
delays: vec![0; table.signals.len()],
effective: constraints
.iter()
.map(|c| (c.data_first.max(0), c.data_second.max(0)))
.collect(),
zeroed: Vec::new(),
}
};
for (signal, delay) in table.signals.iter_mut().zip(&solution.delays) {
signal.delay = *delay;
}
for z in &solution.zeroed {
let (_, name, limits) = &named[z.check];
let which = match z.limit {
Limit::DataFirst => limits[0],
Limit::DataSecond => limits[1],
};
table.warnings.push(format!(
"warning: {name}: the negative {which} limit {} cannot be met together with the \
other negative timing checks; it is set to 0 (LRM 31.9.1)",
z.was
));
}
for (i, c) in constraints.iter().enumerate() {
table.checks.push(NegativeCheck {
entry: named[i].0,
check: model.timing_checks[named[i].0].check_uid.clone(),
reference: c.reference,
data: c.data,
declared: (c.data_first, c.data_second),
effective: solution.effective[i],
});
}
for path in &mut model.paths {
let delay = path
.inputs
.first()
.map_or(0, |input| table.delay_of(path.instance, &input.0));
if delay > 0 {
path.delays = path.delays.at_least(delay.unsigned_abs());
}
}
Ok(table)
}
fn uses_delayed_signals(spec: CheckSpec) -> bool {
!matches!(
spec,
CheckSpec::Skew { .. } | CheckSpec::TimeSkew { .. } | CheckSpec::FullSkew { .. }
)
}
fn shadow_of(
table: &mut DelayTable,
res: &mut Resolution,
bank: InstanceId,
instance: InstanceId,
signal: &SymbolId,
lo: u32,
width: u32,
) -> Option<(Loc, u32)> {
let node = table.find(instance, signal)?;
if table.signals[node].delay == 0 {
return None;
}
if table.shadow_of(instance, signal, lo, width).is_none() {
let signal_width = table.signals[node].slot.width;
let slot = u32::try_from(res.bank_widths.len()).expect("bank cell count fits u32");
res.bank_widths.push(signal_width);
table.signals[node].implicit = Some(loc_of(bank, bank_slot(slot, signal_width)));
}
table.shadow_of(instance, signal, lo, width)
}
fn associate_conditions(check: &mut CheckPlan, table: &DelayTable, uid: &SymbolId) {
let Some(first) = table.first_of(uid) else {
return;
};
let timestamp = check.timestamp.take();
let timecheck = check.timecheck.take();
let (first_watch, second_watch) = match first {
Role::Reference => (&mut check.reference, &mut check.data),
Role::Data => (&mut check.data, &mut check.reference),
};
first_watch.conditions.extend(timestamp);
second_watch.conditions.extend(timecheck);
}
fn check_delta(table: &DelayTable, check: &CheckPlan) -> i64 {
if !uses_delayed_signals(check.spec) {
return 0;
}
table.delay_of(check.instance, &check.data_signal)
- table.delay_of(check.instance, &check.reference_signal)
}
fn delay_check(
check: &mut CheckPlan,
table: &mut DelayTable,
res: &mut Resolution,
bank: InstanceId,
) -> Option<String> {
if !uses_delayed_signals(check.spec) {
return None;
}
let delta = check_delta(table, check);
let (spec, clamped) = effective_spec(check.spec, delta);
let name = check.spec.name();
let warning = clamped.then(|| {
format!(
"warning: {name} of {} and {}: a limit adjusted by the {delta} tick delay between \
them became <= 0 and is set to 0 (LRM 31.9.1)",
check.reference_label, check.data_label
)
});
check.spec = spec;
let instance = check.instance;
for (watch, signal) in [
(&mut check.reference, &check.reference_signal),
(&mut check.data, &check.data_signal),
]
.into_iter()
.chain(
check
.trailing
.as_mut()
.map(|t| (t, &check.reference_signal)),
) {
if let Some((loc, lo)) =
shadow_of(table, res, bank, instance, signal, watch.lo, watch.width)
{
watch.loc = loc;
watch.lo = lo;
}
}
warning
}
pub(crate) fn plan(
model: &TimingModel,
scope: &Scope<'_>,
options: &TimingOptions,
table: &mut DelayTable,
res: &mut Resolution,
bank: InstanceId,
) -> Result<Plans, String> {
let mut plans = Plans::default();
let mut live_entries: Vec<LiveCheck> = Vec::new();
for (index, entry) in model.timing_checks.iter().enumerate() {
let uid = &entry.check_uid;
let (spec, reference, data, trailing, notifier): (CheckSpec, &_, _, Option<_>, _) =
match &entry.args {
CheckArgs::Setup(a) => (
window_spec(StabilityKind::Setup, a, uid, "31.3.1")?,
&a.reference,
a.data.clone(),
None,
&a.notifier,
),
CheckArgs::Hold(a) => (
window_spec(StabilityKind::Hold, a, uid, "31.3.2")?,
&a.reference,
a.data.clone(),
None,
&a.notifier,
),
CheckArgs::SetupHold(a) => (
setuphold_spec(a),
&a.reference,
a.data.clone(),
None,
&a.notifier,
),
CheckArgs::Recovery(a) => (
window_spec(StabilityKind::Recovery, a, uid, "31.3.5")?,
&a.reference,
a.data.clone(),
None,
&a.notifier,
),
CheckArgs::Removal(a) => (
window_spec(StabilityKind::Removal, a, uid, "31.3.4")?,
&a.reference,
a.data.clone(),
None,
&a.notifier,
),
CheckArgs::RecRem(a) => (
recrem_spec(a),
&a.reference,
a.data.clone(),
None,
&a.notifier,
),
CheckArgs::Width(a) => (
width_spec(a, uid)?,
&a.reference,
opposite_event(&a.reference, "$width", uid)?,
None,
&a.notifier,
),
CheckArgs::Period(a) => (
period_spec(a, uid)?,
&a.reference,
a.reference.clone(),
None,
&a.notifier,
),
CheckArgs::NoChange(a) => (
CheckSpec::NoChange {
start: a.start_edge_offset.ticks,
end: a.end_edge_offset.ticks,
},
&a.reference,
a.data.clone(),
Some(opposite_event(&a.reference, "$nochange", uid)?),
&a.notifier,
),
CheckArgs::Skew(a) => (
skew_spec(a, uid)?,
&a.reference,
a.data.clone(),
None,
&a.notifier,
),
CheckArgs::TimeSkew(a) => (
timeskew_spec(a, uid)?,
&a.reference,
a.data.clone(),
None,
&a.notifier,
),
CheckArgs::FullSkew(a) => (
fullskew_spec(a, uid)?,
&a.reference,
a.data.clone(),
None,
&a.notifier,
),
};
let spec = match (spec, table.checks.iter().find(|c| c.entry == index)) {
(CheckSpec::SetupHold { .. }, Some(c)) => CheckSpec::SetupHold {
setup: c.effective.0,
hold: c.effective.1,
},
(CheckSpec::RecRem { .. }, Some(c)) => CheckSpec::RecRem {
recovery: c.effective.1,
removal: c.effective.0,
},
(spec, _) => spec,
};
let own = scope.own.get(&entry.instance).ok_or_else(|| {
format!(
"{} {} is in an instance with no signal table",
spec.name(),
entry.check_uid
)
})?;
let name = format!("{} {}", spec.name(), entry.check_uid);
let notifier = notifier
.as_ref()
.map(|n| {
terminal_window(n, entry.instance, scope, &format!("{name}'s notifier"))
.map(|(owner, slot, lo, _)| (loc_of(owner, slot), lo))
})
.transpose()?;
let (timestamp, timecheck) =
plan_role_conditions(&entry.args, entry.instance, own, model, &name)?;
live_entries.push(LiveCheck {
declared: spec,
delta: 0,
});
let check = CheckPlan {
entry: index,
spec,
reference: plan_event(
reference,
entry.instance,
scope,
own,
model,
&format!("{name}'s reference terminal"),
)?,
data: plan_event(
&data,
entry.instance,
scope,
own,
model,
&format!("{name}'s data terminal"),
)?,
trailing: trailing
.as_ref()
.map(|t| {
plan_event(
t,
entry.instance,
scope,
own,
model,
&format!("{name}'s trailing reference edge"),
)
})
.transpose()?,
notifier,
reference_label: terminal_label(&reference.terminal),
data_label: terminal_label(&data.terminal),
instance: entry.instance,
reference_signal: reference.terminal.0.clone(),
data_signal: data.terminal.0.clone(),
timestamp,
timecheck,
};
let mut checks = if options.per_bit_vectors {
expand_per_bit(
check,
&reference.terminal.0.to_string(),
&data.terminal.0.to_string(),
)
} else {
vec![check]
};
live_entries[index].delta = checks.first().map_or(0, |c| check_delta(table, c));
for check in &mut checks {
associate_conditions(check, table, uid);
if let Some(warning) = delay_check(check, table, res, bank) {
if !table.warnings.contains(&warning) {
table.warnings.push(warning);
}
}
}
plans.checks.extend(checks);
}
for signal in &table.signals {
let mut outputs = signal.explicit.clone();
if let Some(loc) = signal.implicit {
outputs.push(Explicit {
loc,
dst_lo: 0,
source_lo: 0,
width: signal.slot.width,
});
}
if outputs.is_empty() {
continue;
}
plans.lines.push(LinePlan {
source: loc_of(signal.instance, signal.slot),
delay: signal.delay.unsigned_abs(),
outputs,
});
}
plans.live = live_entries;
plans.negative_timing = table.enabled;
Ok(plans)
}
fn bit_watches(watch: &WatchPlan, label: &str, base: &str) -> Vec<(WatchPlan, String)> {
if watch.width == 1 {
return vec![(watch.clone(), label.to_string())];
}
(watch.lo..watch.lo + watch.width)
.map(|bit| {
let mut single = watch.clone();
single.lo = bit;
single.width = 1;
(single, format!("{base}[{bit}]"))
})
.collect()
}
fn expand_per_bit(check: CheckPlan, reference_base: &str, data_base: &str) -> Vec<CheckPlan> {
let references = bit_watches(&check.reference, &check.reference_label, reference_base);
let datas = bit_watches(&check.data, &check.data_label, data_base);
let one_signal = matches!(
check.spec,
CheckSpec::Width { .. } | CheckSpec::Period { .. }
);
let trailing = |index: usize| {
check.trailing.as_ref().map(|t| {
bit_watches(t, "", reference_base)
.into_iter()
.nth(index)
.map_or_else(|| t.clone(), |(w, _)| w)
})
};
let make = |ri: usize, di: usize| CheckPlan {
entry: check.entry,
spec: check.spec,
reference: references[ri].0.clone(),
data: datas[di].0.clone(),
trailing: trailing(ri),
notifier: check.notifier,
reference_label: references[ri].1.clone(),
data_label: datas[di].1.clone(),
instance: check.instance,
reference_signal: check.reference_signal.clone(),
data_signal: check.data_signal.clone(),
timestamp: check.timestamp.clone(),
timecheck: check.timecheck.clone(),
};
if one_signal {
(0..references.len().min(datas.len()))
.map(|i| make(i, i))
.collect()
} else {
(0..references.len())
.flat_map(|r| (0..datas.len()).map(move |d| (r, d)))
.map(|(r, d)| make(r, d))
.collect()
}
}
fn window_spec(
kind: StabilityKind,
args: &EventPairArgs,
uid: &SymbolId,
lrm: &str,
) -> Result<CheckSpec, String> {
if args.limit.ticks < 0 {
return Err(format!(
"{} {uid}: the limit is {} ticks, but {}'s limit must be a non-negative constant \
expression (LRM {lrm})",
kind.name(),
args.limit.ticks,
kind.name(),
));
}
Ok(CheckSpec::Window {
kind,
limit: args.limit.ticks,
})
}
fn setuphold_spec(a: &vj_sim::netlist::specify::SetupHoldArgs) -> CheckSpec {
CheckSpec::SetupHold {
setup: a.setup_limit.ticks,
hold: a.hold_limit.ticks,
}
}
fn recrem_spec(a: &vj_sim::netlist::specify::RecRemArgs) -> CheckSpec {
CheckSpec::RecRem {
recovery: a.recovery_limit.ticks,
removal: a.removal_limit.ticks,
}
}
fn width_spec(
a: &vj_sim::netlist::specify::WidthArgs,
uid: &SymbolId,
) -> Result<CheckSpec, String> {
let threshold = a.threshold.as_ref().map_or(0, |t| t.ticks);
for (which, ticks) in [("limit", a.limit.ticks), ("threshold", threshold)] {
if ticks < 0 {
return Err(format!(
"$width {uid}: the {which} is {ticks} ticks, but $width's {which} must be a \
non-negative constant expression (LRM 31.4.4)"
));
}
}
Ok(CheckSpec::Width {
limit: a.limit.ticks,
threshold,
})
}
fn skew_spec(
a: &vj_sim::netlist::specify::EventPairArgs,
uid: &SymbolId,
) -> Result<CheckSpec, String> {
if a.limit.ticks < 0 {
return Err(format!(
"$skew {uid}: the limit is {} ticks, but $skew's limit must be a non-negative \
constant expression (LRM 31.4.1)",
a.limit.ticks
));
}
Ok(CheckSpec::Skew {
limit: a.limit.ticks,
})
}
fn timeskew_spec(
a: &vj_sim::netlist::specify::TimeSkewArgs,
uid: &SymbolId,
) -> Result<CheckSpec, String> {
if a.limit.ticks < 0 {
return Err(format!(
"$timeskew {uid}: the limit is {} ticks, but $timeskew's limit must be a \
non-negative constant expression (LRM 31.4.2)",
a.limit.ticks
));
}
Ok(CheckSpec::TimeSkew {
limit: a.limit.ticks,
flags: SkewFlags {
event_based: a.event_based.unwrap_or(false),
remain_active: a.remain_active.unwrap_or(false),
},
})
}
fn fullskew_spec(
a: &vj_sim::netlist::specify::FullSkewArgs,
uid: &SymbolId,
) -> Result<CheckSpec, String> {
for (which, ticks) in [
("first limit", a.reference_first_limit.ticks),
("second limit", a.data_first_limit.ticks),
] {
if ticks < 0 {
return Err(format!(
"$fullskew {uid}: the {which} is {ticks} ticks, but $fullskew's limits must be \
non-negative constant expressions (LRM 31.4.3)"
));
}
}
Ok(CheckSpec::FullSkew {
limit: a.reference_first_limit.ticks,
data_first_limit: a.data_first_limit.ticks,
flags: SkewFlags {
event_based: a.event_based.unwrap_or(false),
remain_active: a.remain_active.unwrap_or(false),
},
})
}
fn period_spec(
a: &vj_sim::netlist::specify::PeriodArgs,
uid: &SymbolId,
) -> Result<CheckSpec, String> {
if a.limit.ticks < 0 {
return Err(format!(
"$period {uid}: the limit is {} ticks, but $period's limit must be a non-negative \
constant expression (LRM 31.4.5)",
a.limit.ticks
));
}
Ok(CheckSpec::Period {
limit: a.limit.ticks,
})
}
fn opposite_control(control: EdgeControl, check: &str, what: &str) -> Result<EdgeControl, String> {
control.opposite().ok_or_else(|| {
format!(
"{what} has no edge control, so it has no opposite edge ({check} derives an event \
from its reference event's edge)"
)
})
}
fn opposite_event(
reference: &vj_sim::netlist::specify::CheckEvent,
check: &str,
uid: &SymbolId,
) -> Result<vj_sim::netlist::specify::CheckEvent, String> {
let mut event = reference.clone();
event.edge_control = opposite_control(
reference.edge_control,
check,
&format!("{check} {uid}'s reference event"),
)?;
Ok(event)
}
#[cfg(test)]
pub(crate) fn live_checks_for_entries(
entries: &[vj_sim::netlist::specify::CheckEntry],
delta: i64,
negative_timing: bool,
) -> LiveChecks {
let flags = SkewFlags {
event_based: false,
remain_active: false,
};
let live = entries
.iter()
.map(|e| {
let window = |kind, a: &vj_sim::netlist::specify::EventPairArgs| CheckSpec::Window {
kind,
limit: a.limit.ticks,
};
let declared = match &e.args {
CheckArgs::Setup(a) => window(StabilityKind::Setup, a),
CheckArgs::Hold(a) => window(StabilityKind::Hold, a),
CheckArgs::Recovery(a) => window(StabilityKind::Recovery, a),
CheckArgs::Removal(a) => window(StabilityKind::Removal, a),
CheckArgs::Skew(a) => CheckSpec::Skew {
limit: a.limit.ticks,
},
CheckArgs::SetupHold(a) => CheckSpec::SetupHold {
setup: a.setup_limit.ticks,
hold: a.hold_limit.ticks,
},
CheckArgs::RecRem(a) => CheckSpec::RecRem {
recovery: a.recovery_limit.ticks,
removal: a.removal_limit.ticks,
},
CheckArgs::TimeSkew(a) => CheckSpec::TimeSkew {
limit: a.limit.ticks,
flags,
},
CheckArgs::FullSkew(a) => CheckSpec::FullSkew {
limit: a.reference_first_limit.ticks,
data_first_limit: a.data_first_limit.ticks,
flags,
},
CheckArgs::Period(a) => CheckSpec::Period {
limit: a.limit.ticks,
},
CheckArgs::Width(a) => CheckSpec::Width {
limit: a.limit.ticks,
threshold: a.threshold.map_or(0, |t| t.ticks),
},
CheckArgs::NoChange(a) => CheckSpec::NoChange {
start: a.start_edge_offset.ticks,
end: a.end_edge_offset.ticks,
},
};
LiveCheck { declared, delta }
})
.collect();
LiveChecks::new(live, negative_timing)
}
#[cfg(test)]
mod tests {
use super::*;
use sv_lang::{Compilation, Design, Session};
use vj_sim::netlist::specify::{CheckArgs, CondUnaryOp, EdgeSet};
#[test]
fn notifier_toggle_matches_table_31_13_for_every_value() {
assert_eq!(notifier_toggle(Bit4::Zero), Bit4::One, "0 -> 1");
assert_eq!(notifier_toggle(Bit4::One), Bit4::Zero, "1 -> 0");
assert_eq!(notifier_toggle(Bit4::Z), Bit4::Z, "z -> z");
assert_eq!(notifier_toggle(Bit4::X), Bit4::Zero, "x -> 0");
}
#[test]
fn notifier_toggle_is_an_involution_on_known_values_only() {
for v in [Bit4::Zero, Bit4::One] {
assert_eq!(notifier_toggle(notifier_toggle(v)), v);
}
assert_eq!(notifier_toggle(notifier_toggle(Bit4::Z)), Bit4::Z);
assert_eq!(notifier_toggle(notifier_toggle(Bit4::X)), Bit4::One);
}
fn compile(src: &str) -> Design {
let session = Session::new();
let mut comp = Compilation::new(&session).expect("compilation created");
comp.add_source(src).expect("source added");
comp.compile().expect("design compiled")
}
fn discovered(src: &str) -> Result<TimingModel, RunError> {
let mut design = compile(src);
let eval = design.eval_session();
let body = eval
.design()
.top_instances()
.next()
.expect("one top instance")
.instance_body()
.expect("top instance has a body");
crate::specify::discover(&eval, &[(InstanceId(0), body)], Path::new("<test>"))
}
fn ticks_of(model: &mut TimingModel) -> Vec<i64> {
model.timing_checks[0]
.args
.limits_mut()
.into_iter()
.map(|l| l.ticks)
.collect()
}
#[test]
fn discover_converts_a_check_limit_with_the_timescale() {
let mut model = discovered(
"`timescale 1ns/1ps\nmodule m(input d, input clk);\n specify\n $setup(d, \
posedge clk, 2.5);\n endspecify\nendmodule\n",
)
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(ticks_of(&mut model), [2500]);
}
#[test]
fn discover_keeps_the_sign_of_a_negative_setuphold_limit() {
let mut model = discovered(
"`timescale 1ns/1ps\nmodule m(input d, input clk);\n specify\n $setuphold(posedge \
clk, d, -1, 3);\n endspecify\nendmodule\n",
)
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(ticks_of(&mut model), [-1000, 3000]);
}
#[test]
fn discover_evaluates_specparam_mintypmax_and_threshold_limits() {
let mut model = discovered(
"`timescale 1ns/1ps\nmodule m(input clk);\n specparam lim = 4;\n specify\n \
$width(posedge clk, lim, (1:2:3));\n endspecify\nendmodule\n",
)
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(ticks_of(&mut model), [4000, 2000]);
let mut model = discovered(
"`timescale 1ns/1ps\nmodule m(input clk);\n specify\n $width(posedge clk, \
5);\n endspecify\nendmodule\n",
)
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(ticks_of(&mut model), [5000]);
}
const FOUR: [Bit4; 4] = [Bit4::Zero, Bit4::One, Bit4::X, Bit4::Z];
fn control(edge: sv_lang::EdgeKind, ds: &[&str]) -> EdgeControl {
let ds: Vec<String> = ds.iter().map(|d| d.to_string()).collect();
EdgeControl::from_syntax(edge, &ds).unwrap_or_else(|e| panic!("{e}"))
}
fn accepted(control: EdgeControl) -> Vec<(Bit4, Bit4)> {
let mut out = Vec::new();
for from in FOUR {
for to in FOUR {
if edge_matches(control, from, to) {
out.push((from, to));
}
}
}
out
}
#[test]
fn posedge_and_negedge_are_exactly_their_edge_lists_for_every_transition() {
use sv_lang::EdgeKind::{BothEdges, NegEdge, PosEdge};
let pos = control(PosEdge, &[]);
let pos_explicit = control(BothEdges, &["01", "0x", "x1"]);
let neg = control(NegEdge, &[]);
let neg_explicit = control(BothEdges, &["10", "x0", "1x"]);
for from in FOUR {
for to in FOUR {
assert_eq!(
edge_matches(pos, from, to),
edge_matches(pos_explicit, from, to),
"posedge vs edge[01,0x,x1] on {from:?}->{to:?}"
);
assert_eq!(
edge_matches(neg, from, to),
edge_matches(neg_explicit, from, to),
"negedge vs edge[10,x0,1x] on {from:?}->{to:?}"
);
}
}
use Bit4::{One as I, X, Z, Zero as O};
assert_eq!(accepted(pos), [(O, I), (O, X), (O, Z), (X, I), (Z, I)]);
assert_eq!(accepted(neg), [(I, O), (I, X), (I, Z), (X, O), (Z, O)]);
}
#[test]
fn each_descriptor_matches_exactly_its_transition_with_z_as_x() {
use Bit4::{One as I, X, Z, Zero as O};
use sv_lang::EdgeKind::BothEdges;
for (d, want) in [
("01", vec![(O, I)]),
("10", vec![(I, O)]),
("0x", vec![(O, X), (O, Z)]),
("0z", vec![(O, X), (O, Z)]),
("x0", vec![(X, O), (Z, O)]),
("Z0", vec![(X, O), (Z, O)]),
("1x", vec![(I, X), (I, Z)]),
("1Z", vec![(I, X), (I, Z)]),
("x1", vec![(X, I), (Z, I)]),
("z1", vec![(X, I), (Z, I)]),
] {
assert_eq!(accepted(control(BothEdges, &[d])), want, "descriptor {d}");
}
}
#[test]
fn a_descriptor_list_is_the_union_of_its_descriptors() {
use Bit4::{One as I, X, Z, Zero as O};
let c = control(sv_lang::EdgeKind::BothEdges, &["01", "1x"]);
assert_eq!(accepted(c), [(O, I), (I, X), (I, Z)]);
}
#[test]
fn bare_edge_is_all_six_transitions_but_not_x_to_z() {
let all = accepted(control(sv_lang::EdgeKind::BothEdges, &[]));
assert_eq!(all.len(), 10, "01 10 0x 0z x0 z0 1x 1z x1 z1: {all:?}");
assert!(!all.contains(&(Bit4::X, Bit4::Z)) && !all.contains(&(Bit4::Z, Bit4::X)));
for v in FOUR {
assert!(!edge_matches(EdgeControl::Edges(EdgeSet::ALL), v, v));
}
}
#[test]
fn no_edge_control_matches_every_value_change_and_nothing_else() {
for from in FOUR {
for to in FOUR {
assert_eq!(
edge_matches(EdgeControl::AnyChange, from, to),
from != to,
"{from:?}->{to:?}"
);
}
}
}
fn lit(bits: &[Bit4]) -> CondEval {
CondEval::Const(bits.to_vec())
}
fn un(op: CondUnaryOp, a: CondEval) -> CondEval {
CondEval::Unary(op, Box::new(a))
}
fn bin(op: CondBinaryOp, a: CondEval, b: CondEval) -> CondEval {
CondEval::Binary(op, Box::new(a), Box::new(b))
}
#[test]
fn a_bare_condition_holds_only_for_a_known_one_31_7_example_1() {
assert!(condition_holds(&lit(&[Bit4::One])));
assert!(!condition_holds(&lit(&[Bit4::Zero])));
assert!(!condition_holds(&lit(&[Bit4::X])), "x must not enable");
assert!(!condition_holds(&lit(&[Bit4::Z])), "z is treated as x");
}
#[test]
fn tilde_clr_and_clr_case_equals_zero_agree_on_every_value_31_7_example_2() {
for clr in FOUR {
let tilde = condition_holds(&un(CondUnaryOp::BitwiseNot, lit(&[clr])));
let case_eq = condition_holds(&bin(
CondBinaryOp::CaseEquality,
lit(&[clr]),
lit(&[Bit4::Zero]),
));
assert_eq!(tilde, case_eq, "clr = {clr:?}");
assert_eq!(tilde, clr == Bit4::Zero, "clr = {clr:?}: only a known 0");
}
}
#[test]
fn the_external_and_gate_signal_is_an_ordinary_bare_condition_31_7_example_3() {
for clr in FOUR {
for set in FOUR {
let gate = bin(CondBinaryOp::BitwiseAnd, lit(&[clr]), lit(&[set]));
assert_eq!(
condition_holds(&gate),
clr == Bit4::One && set == Bit4::One,
"clr={clr:?} set={set:?}"
);
}
}
}
#[test]
fn x_enables_a_nondeterministic_comparison_only_31_7() {
use CondBinaryOp::{CaseEquality, CaseInequality, Equality, Inequality};
for sig in FOUR {
let unknown = matches!(sig, Bit4::X | Bit4::Z);
for k in [Bit4::Zero, Bit4::One] {
let holds = |op| condition_holds(&bin(op, lit(&[sig]), lit(&[k])));
assert_eq!(holds(Equality), unknown || sig == k, "{sig:?} == {k:?}");
assert_eq!(holds(Inequality), unknown || sig != k, "{sig:?} != {k:?}");
assert_eq!(
holds(CaseEquality),
!unknown && sig == k,
"{sig:?} === {k:?}"
);
assert_eq!(
holds(CaseInequality),
!unknown && sig != k,
"{sig:?} !== {k:?}"
);
}
}
}
#[test]
fn a_multibit_conditioning_signal_is_read_at_its_lsb_31_7() {
use Bit4::{One as I, X, Zero as O};
assert!(!condition_holds(&lit(&[O, I, O, O])));
assert!(condition_holds(&lit(&[I, O, O, O])));
assert!(!condition_holds(&lit(&[X, I, I, I])));
let eq1 = |v: &[Bit4]| condition_holds(&bin(CondBinaryOp::Equality, lit(v), lit(&[I])));
assert!(!eq1(&[O, I, I, I]));
assert!(eq1(&[I, O, O, O]));
assert!(eq1(&[X, O, O, O]));
let ceq0 =
|v: &[Bit4]| condition_holds(&bin(CondBinaryOp::CaseEquality, lit(v), lit(&[O; 32])));
assert!(ceq0(&[O, I, I, I]), "LSB 0 === 0");
assert!(!ceq0(&[I, O, O, O]));
let not = |v: &[Bit4]| condition_holds(&un(CondUnaryOp::BitwiseNot, lit(v)));
assert!(not(&[O, I, I, I]), "~4'b1110 = 4'b0001, LSB 1");
assert!(!not(&[I, O, O, O]), "~4'b0001 = 4'b1110, LSB 0");
assert!(!not(&[X, O, O, O]), "~4'b000x has LSB x");
}
#[test]
fn any_other_expression_is_a_bare_deterministic_condition_read_at_its_lsb() {
use Bit4::{One as I, X, Zero as O};
let and = |a, b| condition_holds(&bin(CondBinaryOp::LogicalAnd, lit(&[a]), lit(&[b])));
assert!(and(I, I));
assert!(!and(I, O));
assert!(!and(I, X), "1 && x = x does not enable");
let not = |a| condition_holds(&un(CondUnaryOp::LogicalNot, lit(&[a])));
assert!(not(O));
assert!(!not(X));
let eq = |a: &[Bit4]| condition_holds(&bin(CondBinaryOp::Equality, lit(a), lit(&[O, I])));
assert!(eq(&[O, I]));
assert!(!eq(&[I, I]));
assert!(eq(&[X, I]));
}
fn cond_with(
node: &vj_sim::netlist::specify::CondNode,
names: &[(String, SymbolId)],
value_of: &dyn Fn(&str) -> Vec<Bit4>,
) -> CondEval {
use vj_sim::netlist::specify::CondNode as N;
let go = |n: &N| Box::new(cond_with(n, names, value_of));
match node {
N::Signal((uid, bits)) => {
let name = &names
.iter()
.find(|(_, u)| u == uid)
.unwrap_or_else(|| panic!("unknown signal {uid}"))
.0;
let v = value_of(name);
CondEval::Const(match bits {
None => v,
Some((lo, w)) => v[*lo as usize..(*lo + *w) as usize].to_vec(),
})
}
N::Const(b) => CondEval::Const(b.clone()),
N::Unary(op, a) => CondEval::Unary(*op, go(a)),
N::Binary(op, a, b) => CondEval::Binary(*op, go(a), go(b)),
N::Specparam(_) | N::Ternary(..) | N::Concat(_) | N::Replication(..) => {
panic!("not used by these examples: {node:?}")
}
}
}
fn first_reference_condition(
call: &str,
) -> (vj_sim::netlist::specify::CondNode, Vec<(String, SymbolId)>) {
let src = format!(
"module m(input clk, input data, input clr, input [3:0] bus);\n specify\n {call}\n \
endspecify\nendmodule\n"
);
let mut design = compile(&src);
let eval = design.eval_session();
let body = eval
.design()
.top_instances()
.next()
.expect("one top instance")
.instance_body()
.expect("top instance has a body");
let names: Vec<(String, SymbolId)> = body
.members()
.filter(|s| matches!(s.kind(), SymbolKind::Net | SymbolKind::Variable))
.map(|s| (s.name().to_string(), s.uid()))
.collect();
let model = crate::specify::discover(&eval, &[(InstanceId(0), body)], Path::new("<test>"))
.unwrap_or_else(|e| panic!("discover failed: {e}"));
let CheckArgs::Setup(a) = &model.timing_checks[0].args else {
panic!("not $setup")
};
let cond = a.reference.condition.clone().expect("a conditioned event");
(cond, names)
}
fn holds_for(call: &str, clr: &[Bit4]) -> bool {
let (node, names) = first_reference_condition(call);
let value_of = |n: &str| match n {
"clr" => clr.to_vec(),
"bus" => clr.to_vec(),
other => panic!("unexpected signal {other}"),
};
condition_holds(&cond_with(&node, &names, &value_of))
}
#[test]
fn discovered_31_7_examples_enable_the_check_as_the_lrm_says() {
use Bit4::{One as I, X, Z, Zero as O};
let bare = "$setup(data, posedge clk &&& clr, 10);";
let tilde = "$setup(data, posedge clk &&& (~clr), 10);";
let case_eq = "$setup(data, posedge clk &&& (clr===0), 10);";
let eq = "$setup(data, posedge clk &&& (clr==0), 10);";
let ne = "$setup(data, posedge clk &&& (clr!=1'b1), 10);";
for (v, want_bare, want_low, want_eq) in [
(O, false, true, true),
(I, true, false, false),
(X, false, false, true),
(Z, false, false, true),
] {
assert_eq!(holds_for(bare, &[v]), want_bare, "&&& clr, clr={v:?}");
assert_eq!(holds_for(tilde, &[v]), want_low, "&&& (~clr), clr={v:?}");
assert_eq!(
holds_for(case_eq, &[v]),
want_low,
"&&& (clr===0), clr={v:?}"
);
assert_eq!(holds_for(eq, &[v]), want_eq, "&&& (clr==0), clr={v:?}");
assert_eq!(holds_for(ne, &[v]), want_eq, "&&& (clr!=1'b1), clr={v:?}");
}
let sel = "$setup(data, posedge clk &&& bus[1], 10);";
assert!(holds_for(sel, &[O, I, O, O]));
assert!(!holds_for(sel, &[I, O, I, I]));
}
#[test]
fn every_kind_exposes_its_limit_slots_in_argument_order() {
let model = discovered(
"`timescale 1ns/1ps\nmodule m(input d, input clk, input clr);\n reg n;\n specify\n \
$setup(d, posedge clk, 1);\n $hold(posedge clk, d, 2);\n $setuphold(posedge \
clk, d, 3, 4);\n $recovery(posedge clr, posedge clk, 5);\n $removal(posedge \
clr, posedge clk, 6);\n $recrem(posedge clr, posedge clk, 7, 8);\n \
$skew(posedge clk, d, 9);\n $timeskew(posedge clk, d, 10);\n \
$fullskew(posedge clk, d, 11, 12);\n $period(posedge clk, 13);\n \
$width(posedge clk, 14, 15);\n $nochange(posedge clk, d, 16, 17);\n \
endspecify\nendmodule\n",
)
.unwrap_or_else(|e| panic!("discover failed: {e}"));
let mut model = model;
let per_check: Vec<Vec<i64>> = model
.timing_checks
.iter_mut()
.map(|c| c.args.limits_mut().into_iter().map(|l| l.ticks).collect())
.collect();
assert_eq!(
per_check,
vec![
vec![1000],
vec![2000],
vec![3000, 4000],
vec![5000],
vec![6000],
vec![7000, 8000],
vec![9000],
vec![10000],
vec![11000, 12000],
vec![13000],
vec![14000, 15000],
vec![16000, 17000],
]
);
assert!(matches!(
model.timing_checks[2].args,
CheckArgs::SetupHold(_)
));
}
#[test]
fn window_violation_truth_tables_for_both_boundary_kinds() {
let table = [
(9, false, false), (10, false, true), (11, true, true), (15, true, true), (19, true, true), (20, false, false), (21, false, false), ];
for (t, open_both, closed_start) in table {
assert_eq!(
window_violation(BoundaryKind::OpenBoth, 10, 20, t),
open_both,
"OpenBoth at t={t}"
);
assert_eq!(
window_violation(BoundaryKind::ClosedStartOpenEnd, 10, 20, t),
closed_start,
"ClosedStartOpenEnd at t={t}"
);
}
}
#[test]
fn window_violation_truth_table_for_the_open_start_closed_end_kind() {
let table = [
(9, false, false, false), (10, false, false, true), (11, true, true, true), (15, true, true, true), (19, true, true, true), (20, true, false, false), (21, false, false, false), ];
for (t, open_start, open_both, closed_start) in table {
assert_eq!(
window_violation(BoundaryKind::OpenStartClosedEnd, 10, 20, t),
open_start,
"OpenStartClosedEnd at t={t}"
);
assert_eq!(
window_violation(BoundaryKind::OpenBoth, 10, 20, t),
open_both,
"OpenBoth at t={t}"
);
assert_eq!(
window_violation(BoundaryKind::ClosedStartOpenEnd, 10, 20, t),
closed_start,
"ClosedStartOpenEnd at t={t}"
);
}
}
#[test]
fn window_violation_of_an_empty_or_inverted_window_is_never_a_violation() {
for kind in [
BoundaryKind::OpenBoth,
BoundaryKind::ClosedStartOpenEnd,
BoundaryKind::OpenStartClosedEnd,
] {
for t in 8..=12 {
assert!(
!window_violation(kind, 10, 10, t),
"{kind:?} zero width t={t}"
);
assert!(!window_violation(kind, 12, 8, t), "{kind:?} inverted t={t}");
}
}
}
#[test]
fn a_zero_limit_short_circuits_both_boundary_kinds() {
for kind in [
BoundaryKind::OpenBoth,
BoundaryKind::ClosedStartOpenEnd,
BoundaryKind::OpenStartClosedEnd,
] {
for t in 8..=12 {
assert!(
!stability_violation(kind, 0, 10, 10, t),
"{kind:?} zero width"
);
}
assert!(
window_violation(kind, 0, 100, 50),
"the window itself contains 50"
);
assert!(
!stability_violation(kind, 0, 0, 100, 50),
"{kind:?}: limit 0 must not violate even if the window passed in is wide"
);
assert!(
stability_violation(kind, 1, 0, 100, 50),
"{kind:?}: limit 1 does"
);
}
}
const SETUP: StabilityKind = StabilityKind::Setup;
const HOLD: StabilityKind = StabilityKind::Hold;
const REMOVAL: StabilityKind = StabilityKind::Removal;
const RECOVERY: StabilityKind = StabilityKind::Recovery;
#[derive(Clone, Copy, Debug)]
enum Order {
DataFirst,
ReferenceFirst,
}
fn pair(kind: StabilityKind, limit: i64, data_t: i64, ref_t: i64, order: Order) -> Vec<i64> {
let mut w = WindowState::new(kind, limit);
let mut out = Vec::new();
let data_first = match data_t.cmp(&ref_t) {
std::cmp::Ordering::Less => true,
std::cmp::Ordering::Greater => false,
std::cmp::Ordering::Equal => matches!(order, Order::DataFirst),
};
if data_first {
out.extend(w.data_event(data_t));
out.extend(w.reference_event(ref_t));
} else {
out.extend(w.reference_event(ref_t));
out.extend(w.data_event(data_t));
}
out
}
#[test]
fn setup_boundary_sweep_is_open_at_both_ends() {
let limit = 5;
let t_ref = 100;
for off in -(limit + 1)..=(limit + 1) {
for order in [Order::DataFirst, Order::ReferenceFirst] {
let got = pair(SETUP, limit, t_ref + off, t_ref, order);
let want: Vec<i64> = if (-limit + 1..=-1).contains(&off) {
vec![-off]
} else {
vec![]
};
assert_eq!(got, want, "$setup, data at ref{off:+} ({order:?})");
}
}
assert!(
pair(SETUP, limit, 95, t_ref, Order::DataFirst).is_empty(),
"begin"
);
assert_eq!(
pair(SETUP, limit, 96, t_ref, Order::DataFirst),
[4],
"begin+1"
);
assert_eq!(
pair(SETUP, limit, 99, t_ref, Order::DataFirst),
[1],
"end-1"
);
assert!(
pair(SETUP, limit, 100, t_ref, Order::DataFirst).is_empty(),
"end"
);
assert!(
pair(SETUP, limit, 100, t_ref, Order::ReferenceFirst).is_empty(),
"end"
);
}
#[test]
fn hold_boundary_sweep_is_closed_at_the_start_and_open_at_the_end() {
let limit = 5;
let t_ref = 100;
for off in -(limit + 1)..=(limit + 1) {
for order in [Order::DataFirst, Order::ReferenceFirst] {
let got = pair(HOLD, limit, t_ref + off, t_ref, order);
let want: Vec<i64> = if (0..limit).contains(&off) {
vec![off]
} else {
vec![]
};
assert_eq!(got, want, "$hold, data at ref{off:+} ({order:?})");
}
}
for order in [Order::DataFirst, Order::ReferenceFirst] {
assert_eq!(
pair(HOLD, limit, 100, t_ref, order),
[0],
"start IS a violation"
);
assert_eq!(pair(HOLD, limit, 104, t_ref, order), [4], "end-1");
assert!(
pair(HOLD, limit, 105, t_ref, order).is_empty(),
"end is not"
);
assert!(
pair(HOLD, limit, 99, t_ref, order).is_empty(),
"before the window"
);
}
}
#[test]
fn a_zero_limit_never_violates_through_the_state_machine() {
for kind in [SETUP, HOLD] {
for off in -2..=2 {
for order in [Order::DataFirst, Order::ReferenceFirst] {
assert!(
pair(kind, 0, 100 + off, 100, order).is_empty(),
"{kind:?} off={off} ({order:?})"
);
}
}
}
}
#[test]
fn setup_sees_the_earlier_data_event_when_another_lands_at_the_reference_time() {
for order in [Order::DataFirst, Order::ReferenceFirst] {
let mut w = WindowState::new(SETUP, 5);
let mut got = Vec::new();
got.extend(w.data_event(98));
if matches!(order, Order::DataFirst) {
got.extend(w.data_event(100));
got.extend(w.reference_event(100));
} else {
got.extend(w.reference_event(100));
got.extend(w.data_event(100));
}
assert_eq!(got, [2], "({order:?})");
}
}
#[test]
fn setup_reports_once_per_reference_event() {
let mut w = WindowState::new(SETUP, 5);
assert_eq!(w.data_event(96), None);
assert_eq!(w.data_event(98), None);
assert_eq!(w.reference_event(100), Some(2), "nearest data event");
assert_eq!(
w.reference_event(101),
Some(3),
"next reference: its own window"
);
assert_eq!(w.reference_event(104), None, "98 is 6 before 104: outside");
}
#[test]
fn hold_uses_the_most_recent_reference_event() {
let mut w = WindowState::new(HOLD, 5);
assert_eq!(w.reference_event(100), None);
assert_eq!(w.reference_event(102), None);
assert_eq!(w.data_event(106), Some(4));
let mut w = WindowState::new(HOLD, 5);
assert_eq!(w.reference_event(100), None);
assert_eq!(w.data_event(103), Some(3));
assert_eq!(w.data_event(105), None, "end of the window");
}
#[test]
fn hold_reports_a_data_event_at_most_once() {
let mut w = WindowState::new(HOLD, 5);
assert_eq!(w.reference_event(100), None);
assert_eq!(w.data_event(102), Some(2), "against the 100 edge");
assert_eq!(
w.reference_event(102),
None,
"same data event, already reported"
);
}
#[test]
fn hold_before_any_reference_event_is_clean() {
let mut w = WindowState::new(HOLD, 5);
assert_eq!(w.data_event(3), None);
let mut w = WindowState::new(SETUP, 5);
assert_eq!(w.reference_event(3), None, "$setup: no data event yet");
}
#[test]
fn removal_boundary_sweep_is_open_at_both_ends() {
let limit = 5;
let t_ref = 100;
for off in -(limit + 1)..=(limit + 1) {
for order in [Order::DataFirst, Order::ReferenceFirst] {
let got = pair(REMOVAL, limit, t_ref + off, t_ref, order);
let want: Vec<i64> = if (-limit + 1..=-1).contains(&off) {
vec![-off]
} else {
vec![]
};
assert_eq!(got, want, "$removal, data at ref{off:+} ({order:?})");
}
}
for order in [Order::DataFirst, Order::ReferenceFirst] {
assert!(
pair(REMOVAL, limit, 95, t_ref, order).is_empty(),
"begin ({order:?})"
);
assert_eq!(
pair(REMOVAL, limit, 96, t_ref, order),
[4],
"begin+1 ({order:?})"
);
assert_eq!(
pair(REMOVAL, limit, 99, t_ref, order),
[1],
"end-1 ({order:?})"
);
assert!(
pair(REMOVAL, limit, 100, t_ref, order).is_empty(),
"end, simultaneous ({order:?})"
);
}
}
#[test]
fn recovery_boundary_sweep_is_closed_at_the_start_and_open_at_the_end() {
let limit = 5;
let t_ref = 100;
for off in -(limit + 1)..=(limit + 1) {
for order in [Order::DataFirst, Order::ReferenceFirst] {
let got = pair(RECOVERY, limit, t_ref + off, t_ref, order);
let want: Vec<i64> = if (0..limit).contains(&off) {
vec![off]
} else {
vec![]
};
assert_eq!(got, want, "$recovery, data at ref{off:+} ({order:?})");
}
}
for order in [Order::DataFirst, Order::ReferenceFirst] {
assert_eq!(
pair(RECOVERY, limit, 100, t_ref, order),
[0],
"start IS a violation ({order:?})"
);
assert_eq!(pair(RECOVERY, limit, 104, t_ref, order), [4], "end-1");
assert!(
pair(RECOVERY, limit, 105, t_ref, order).is_empty(),
"end is not ({order:?})"
);
assert!(
pair(RECOVERY, limit, 99, t_ref, order).is_empty(),
"before the window ({order:?})"
);
}
}
#[test]
fn removal_and_recovery_disagree_exactly_at_the_simultaneous_instant() {
for order in [Order::DataFirst, Order::ReferenceFirst] {
assert!(pair(REMOVAL, 5, 100, 100, order).is_empty(), "{order:?}");
assert_eq!(pair(RECOVERY, 5, 100, 100, order), [0], "{order:?}");
}
}
#[test]
fn removal_and_recovery_with_a_zero_limit_never_violate() {
for kind in [REMOVAL, RECOVERY] {
for off in -2..=2 {
for order in [Order::DataFirst, Order::ReferenceFirst] {
assert!(
pair(kind, 0, 100 + off, 100, order).is_empty(),
"{kind:?} off={off} ({order:?})"
);
}
}
}
}
#[test]
fn removal_reports_once_per_reference_event_and_sees_the_earlier_data_event() {
let mut w = WindowState::new(REMOVAL, 5);
assert_eq!(w.data_event(96), None);
assert_eq!(w.data_event(98), None);
assert_eq!(w.reference_event(100), Some(2), "nearest data event");
assert_eq!(w.reference_event(101), Some(3), "its own window");
assert_eq!(w.reference_event(104), None, "98 is 6 before 104");
for order in [Order::DataFirst, Order::ReferenceFirst] {
let mut w = WindowState::new(REMOVAL, 5);
let mut got = Vec::new();
got.extend(w.data_event(98));
if matches!(order, Order::DataFirst) {
got.extend(w.data_event(100));
got.extend(w.reference_event(100));
} else {
got.extend(w.reference_event(100));
got.extend(w.data_event(100));
}
assert_eq!(got, [2], "({order:?})");
}
}
#[test]
fn recovery_uses_the_most_recent_reference_event_and_flags_once() {
let mut w = WindowState::new(RECOVERY, 5);
assert_eq!(w.reference_event(100), None);
assert_eq!(w.reference_event(102), None);
assert_eq!(w.data_event(106), Some(4));
let mut w = WindowState::new(RECOVERY, 5);
assert_eq!(w.reference_event(100), None);
assert_eq!(w.data_event(102), Some(2), "against the 100 edge");
assert_eq!(w.reference_event(102), None, "already reported");
assert_eq!(w.data_event(105), Some(3), "measured from the 102 edge");
}
#[test]
fn removal_and_recovery_before_any_other_event_are_clean() {
let mut w = WindowState::new(RECOVERY, 5);
assert_eq!(w.data_event(3), None, "$recovery: no reference event yet");
let mut w = WindowState::new(REMOVAL, 5);
assert_eq!(w.reference_event(3), None, "$removal: no data event yet");
}
fn sh_pair(setup: i64, hold: i64, data_t: i64, ref_t: i64, order: Order) -> Vec<(i64, i64)> {
let mut s = SetupHoldState::setuphold(setup, hold);
let mut out = Vec::new();
let data_first = match data_t.cmp(&ref_t) {
std::cmp::Ordering::Less => true,
std::cmp::Ordering::Greater => false,
std::cmp::Ordering::Equal => matches!(order, Order::DataFirst),
};
if data_first {
out.extend(s.data_event(data_t));
out.extend(s.reference_event(ref_t));
} else {
out.extend(s.reference_event(ref_t));
out.extend(s.data_event(data_t));
}
out.into_iter().map(|v| (v.actual, v.limit)).collect()
}
#[test]
fn setuphold_boundary_sweep_covers_both_windows_and_the_simultaneous_point() {
for off in -6..=4_i64 {
for order in [Order::DataFirst, Order::ReferenceFirst] {
let got = sh_pair(5, 3, 100 + off, 100, order);
let want: Vec<(i64, i64)> = match off {
-4..=-1 => vec![(-off, 5)],
0 => vec![(0, 5)],
1..=2 => vec![(off, 3)],
_ => vec![],
};
assert_eq!(got, want, "$setuphold(5,3), data at ref{off:+} ({order:?})");
}
}
}
#[test]
fn setuphold_simultaneous_events_report_the_larger_limit() {
for order in [Order::DataFirst, Order::ReferenceFirst] {
assert_eq!(sh_pair(2, 7, 50, 50, order), [(0, 7)], "hold larger");
assert_eq!(sh_pair(7, 2, 50, 50, order), [(0, 7)], "setup larger");
}
}
#[test]
fn setuphold_with_one_zero_limit_keeps_only_the_other_window() {
for order in [Order::DataFirst, Order::ReferenceFirst] {
for off in -6..=3_i64 {
let got = sh_pair(5, 0, 100 + off, 100, order);
let want: Vec<(i64, i64)> = match off {
-4..=-1 => vec![(-off, 5)],
0 => vec![(0, 5)],
_ => vec![],
};
assert_eq!(got, want, "$setuphold(5,0), off {off:+} ({order:?})");
}
for off in -3..=6_i64 {
let got = sh_pair(0, 5, 100 + off, 100, order);
let want: Vec<(i64, i64)> = match off {
0..=4 => vec![(off, 5)],
_ => vec![],
};
assert_eq!(got, want, "$setuphold(0,5), off {off:+} ({order:?})");
}
}
assert!(pair(SETUP, 5, 100, 100, Order::DataFirst).is_empty());
assert!(!sh_pair(5, 0, 100, 100, Order::DataFirst).is_empty());
}
#[test]
fn setuphold_with_both_limits_zero_never_violates_through_the_state_machine() {
for off in -3..=3_i64 {
for order in [Order::DataFirst, Order::ReferenceFirst] {
assert!(
sh_pair(0, 0, 100 + off, 100, order).is_empty(),
"off {off:+} ({order:?})"
);
}
}
}
#[test]
fn setuphold_simultaneous_pair_is_reported_exactly_once() {
let v = |actual, limit| Violation { actual, limit };
let mut s = SetupHoldState::setuphold(5, 3);
let mut got = Vec::new();
got.extend(s.data_event(100));
got.extend(s.reference_event(100));
got.extend(s.data_event(100));
got.extend(s.reference_event(100));
assert_eq!(got, [v(0, 5)], "data first");
let mut s = SetupHoldState::setuphold(5, 3);
let mut got = Vec::new();
got.extend(s.reference_event(100));
got.extend(s.data_event(100));
got.extend(s.reference_event(100));
got.extend(s.data_event(100));
assert_eq!(got, [v(0, 5)], "reference first");
got.extend(s.data_event(200));
got.extend(s.reference_event(200));
assert_eq!(got, [v(0, 5), v(0, 5)], "next instant");
}
#[test]
fn setuphold_finds_the_earlier_data_event_beside_a_simultaneous_one() {
let v = |actual, limit| Violation { actual, limit };
let mut s = SetupHoldState::setuphold(5, 3);
let mut got = Vec::new();
got.extend(s.data_event(98));
got.extend(s.data_event(100));
got.extend(s.reference_event(100));
assert_eq!(got, [v(2, 5), v(0, 5)], "data, data, reference");
let mut s = SetupHoldState::setuphold(5, 3);
let mut got = Vec::new();
got.extend(s.data_event(98));
got.extend(s.reference_event(100));
got.extend(s.data_event(100));
assert_eq!(got, [v(2, 5), v(0, 5)], "data, reference, data");
}
#[test]
fn setuphold_hold_window_uses_the_latest_reference_and_flags_once() {
let v = |actual, limit| Violation { actual, limit };
let mut s = SetupHoldState::setuphold(2, 5);
assert!(s.reference_event(100).is_empty());
assert!(s.reference_event(102).is_empty());
assert_eq!(s.data_event(106), [v(4, 5)]);
let mut s = SetupHoldState::setuphold(2, 5);
assert!(s.reference_event(100).is_empty());
assert_eq!(s.data_event(102), [v(2, 5)], "against the 100 edge");
assert!(s.reference_event(102).is_empty());
assert_eq!(s.data_event(105), [v(3, 5)]);
assert!(s.data_event(107).is_empty(), "end of the 102 window");
}
#[test]
fn setuphold_before_any_other_event_is_clean() {
let mut s = SetupHoldState::setuphold(5, 3);
assert!(s.data_event(3).is_empty(), "no reference event yet");
let mut s = SetupHoldState::setuphold(5, 3);
assert!(s.reference_event(3).is_empty(), "no data event yet");
}
#[test]
fn setuphold_equals_setup_plus_hold_over_generated_sequences() {
let mut x: u64 = 0x5eed_5eed_5eed_5eed;
let mut next = move |n: u64| {
x = x
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(x >> 33) % n
};
for case in 0..3000 {
let setup = 1 + next(4) as i64;
let hold = 1 + next(4) as i64;
let mut both = SetupHoldState::setuphold(setup, hold);
let mut su = WindowState::new(SETUP, setup);
let mut ho = WindowState::new(HOLD, hold);
let mut t = 100_i64;
let mut trace = Vec::new();
for _ in 0..14 {
t += next(5) as i64;
let is_data = next(2) == 0;
trace.push((t, is_data));
let got: Vec<i64> = if is_data {
both.data_event(t).iter().map(|v| v.actual).collect()
} else {
both.reference_event(t).iter().map(|v| v.actual).collect()
};
let want: Vec<i64> = if is_data {
su.data_event(t)
.into_iter()
.chain(ho.data_event(t))
.collect()
} else {
su.reference_event(t)
.into_iter()
.chain(ho.reference_event(t))
.collect()
};
assert_eq!(
got, want,
"case {case}: $setuphold({setup},{hold}) vs $setup+$hold after \
{trace:?} (time, is_data)"
);
}
}
}
fn rr_pair(rec: i64, rem: i64, data_t: i64, ref_t: i64, order: Order) -> Vec<(i64, i64)> {
let mut s = SetupHoldState::recrem(rec, rem);
let mut out = Vec::new();
let data_first = match data_t.cmp(&ref_t) {
std::cmp::Ordering::Less => true,
std::cmp::Ordering::Greater => false,
std::cmp::Ordering::Equal => matches!(order, Order::DataFirst),
};
if data_first {
out.extend(s.data_event(data_t));
out.extend(s.reference_event(ref_t));
} else {
out.extend(s.reference_event(ref_t));
out.extend(s.data_event(data_t));
}
out.into_iter().map(|v| (v.actual, v.limit)).collect()
}
#[test]
fn recrem_boundary_sweep_sizes_data_first_by_removal_and_data_second_by_recovery() {
for off in -7..=5_i64 {
for order in [Order::DataFirst, Order::ReferenceFirst] {
let got = rr_pair(3, 5, 100 + off, 100, order);
let want: Vec<(i64, i64)> = match off {
-4..=-1 => vec![(-off, 5)],
0 => vec![(0, 5)],
1..=2 => vec![(off, 3)],
_ => vec![],
};
assert_eq!(got, want, "$recrem(3,5), data at ref{off:+} ({order:?})");
}
}
}
#[test]
fn recrem_is_setuphold_with_its_limits_the_other_way_round() {
for off in -7..=7_i64 {
for order in [Order::DataFirst, Order::ReferenceFirst] {
assert_eq!(
rr_pair(3, 5, 100 + off, 100, order),
sh_pair(5, 3, 100 + off, 100, order),
"off {off:+} ({order:?})"
);
}
}
assert_ne!(
rr_pair(3, 5, 96, 100, Order::DataFirst),
sh_pair(3, 5, 96, 100, Order::DataFirst),
"a data event 4 ticks before the reference is inside removal 5, outside setup 3"
);
}
#[test]
fn recrem_simultaneous_events_report_the_larger_limit_once() {
for order in [Order::DataFirst, Order::ReferenceFirst] {
assert_eq!(rr_pair(2, 7, 50, 50, order), [(0, 7)], "removal larger");
assert_eq!(rr_pair(7, 2, 50, 50, order), [(0, 7)], "recovery larger");
}
let mut s = SetupHoldState::recrem(3, 5);
let mut got = Vec::new();
got.extend(s.data_event(100));
got.extend(s.reference_event(100));
got.extend(s.data_event(100));
got.extend(s.reference_event(100));
assert_eq!(
got,
[Violation {
actual: 0,
limit: 5
}],
"once"
);
}
#[test]
fn recrem_with_a_zero_limit_keeps_only_the_other_window() {
for order in [Order::DataFirst, Order::ReferenceFirst] {
for off in -3..=6_i64 {
let want: Vec<(i64, i64)> = match off {
0..=3 => vec![(off, 4)],
_ => vec![],
};
assert_eq!(
rr_pair(4, 0, 100 + off, 100, order),
want,
"rec 4 rem 0, {off:+}"
);
}
for off in -6..=3_i64 {
let want: Vec<(i64, i64)> = match off {
-3..=-1 => vec![(-off, 4)],
0 => vec![(0, 4)],
_ => vec![],
};
assert_eq!(
rr_pair(0, 4, 100 + off, 100, order),
want,
"rec 0 rem 4, {off:+}"
);
}
for off in -3..=3_i64 {
assert!(
rr_pair(0, 0, 100 + off, 100, order).is_empty(),
"rec 0 rem 0, {off:+} ({order:?})"
);
}
}
}
#[test]
fn recrem_equals_removal_plus_recovery_over_generated_sequences() {
let mut x: u64 = 0x7e57_7e57_7e57_7e57;
let mut next = move |n: u64| {
x = x
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(x >> 33) % n
};
for case in 0..3000 {
let rec = 1 + next(4) as i64;
let rem = 1 + next(4) as i64;
let mut both = SetupHoldState::recrem(rec, rem);
let mut re = WindowState::new(REMOVAL, rem);
let mut rc = WindowState::new(RECOVERY, rec);
let mut t = 100_i64;
let mut trace = Vec::new();
for _ in 0..14 {
t += next(5) as i64;
let is_data = next(2) == 0;
trace.push((t, is_data));
let mut got: Vec<i64> = if is_data {
both.data_event(t).iter().map(|v| v.actual).collect()
} else {
both.reference_event(t).iter().map(|v| v.actual).collect()
};
let mut want: Vec<i64> = if is_data {
re.data_event(t)
.into_iter()
.chain(rc.data_event(t))
.collect()
} else {
re.reference_event(t)
.into_iter()
.chain(rc.reference_event(t))
.collect()
};
got.sort_unstable();
want.sort_unstable();
assert_eq!(
got, want,
"case {case}: $recrem({rec},{rem}) vs $removal+$recovery after {trace:?} \
(time, is_data)"
);
}
}
}
#[test]
fn split_setuphold_without_conditions_equals_the_single_machine() {
let mut x: u64 = 0x5917_5917_5917_5917;
let mut next = move |n: u64| {
x = x
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(x >> 33) % n
};
for case in 0..3000 {
let setup = 1 + next(4) as i64;
let hold = 1 + next(4) as i64;
let mut one = SetupHoldState::new(setup, hold);
let mut split = SplitSetupHold::new(setup, hold);
let mut t = 100_i64;
let mut trace = Vec::new();
for _ in 0..14 {
t += next(4) as i64;
let (d, r) = match next(3) {
0 => (true, false),
1 => (false, true),
_ => (true, true),
};
trace.push((t, d, r));
let mut want = Vec::new();
if d {
want.extend(one.data_event(t));
}
if r {
want.extend(one.reference_event(t));
}
let mut got = split.deliver(t, d.then_some(Roles::BOTH), r.then_some(Roles::BOTH));
got.sort_by_key(|v| (v.actual, v.limit));
want.sort_by_key(|v| (v.actual, v.limit));
assert_eq!(
got, want,
"case {case}: ({setup},{hold}) after {trace:?} (time, data, reference)"
);
}
}
}
#[test]
fn split_setuphold_roles_select_the_window() {
let ts = Roles {
timestamp: true,
timecheck: false,
};
let tc = Roles {
timestamp: false,
timecheck: true,
};
let none = Roles {
timestamp: false,
timecheck: false,
};
let sorted = |mut v: Vec<Violation>| {
v.sort_by_key(|v| (v.actual, v.limit));
v
};
let mut s = SplitSetupHold::new(3, 5);
assert!(s.deliver(100, Some(ts), None).is_empty());
assert_eq!(
s.deliver(102, None, Some(tc)),
[Violation {
actual: 2,
limit: 3
}]
);
let mut s = SplitSetupHold::new(3, 5);
assert!(s.deliver(100, Some(tc), None).is_empty());
assert!(s.deliver(102, None, Some(tc)).is_empty());
let mut s = SplitSetupHold::new(3, 5);
assert!(s.deliver(100, Some(ts), None).is_empty());
assert!(s.deliver(102, None, Some(ts)).is_empty());
let mut s = SplitSetupHold::new(3, 5);
assert!(s.deliver(200, None, Some(ts)).is_empty());
assert_eq!(
s.deliver(204, Some(tc), None),
[Violation {
actual: 4,
limit: 5
}]
);
let mut s = SplitSetupHold::new(3, 5);
assert!(s.deliver(200, None, Some(tc)).is_empty());
assert!(s.deliver(204, Some(tc), None).is_empty());
let mut s = SplitSetupHold::new(3, 5);
assert!(s.deliver(200, None, Some(ts)).is_empty());
assert!(s.deliver(204, Some(ts), None).is_empty());
let mut s = SplitSetupHold::new(3, 5);
assert!(s.deliver(300, Some(none), None).is_empty());
assert!(s.deliver(301, None, Some(none)).is_empty());
let mut s = SplitSetupHold::new(3, 5);
assert_eq!(
sorted(s.deliver(400, Some(Roles::BOTH), Some(Roles::BOTH))),
[Violation {
actual: 0,
limit: 5
}]
);
}
#[test]
fn event_occurred_reads_a_vector_edge_at_the_lsb_and_a_plain_change_at_any_bit() {
use Bit4::{One as I, Zero as O};
let pos = control(sv_lang::EdgeKind::PosEdge, &[]);
assert!(event_occurred(pos, &[O, O], &[I, O]), "LSB rises");
assert!(!event_occurred(pos, &[O, O], &[O, I]), "only the MSB rises");
assert!(!event_occurred(pos, &[I, O], &[O, O]), "LSB falls");
let any = EdgeControl::AnyChange;
assert!(event_occurred(any, &[O, O], &[O, I]), "any bit");
assert!(
event_occurred(any, &[O, O], &[I, I]),
"two bits, still one event"
);
assert!(!event_occurred(any, &[I, O], &[I, O]), "no change");
}
fn run_src(tag: &str, src: &str) -> String {
run_src_with(tag, src, negative_on())
}
fn check_design(check: &str, stim: &str) -> String {
format!(
"module chk(input clk, input d);\n reg ntf;\n specify\n {check}\n endspecify\n \
always @(ntf) $display(\"ntf=%b at %0t\", ntf, $time);\nendmodule\nmodule top;\n \
reg clk, d;\n chk u(.clk(clk), .d(d));\n initial begin\n clk = 0; d = 0;\n \
#100;\n {stim}\n #100 $finish;\n end\nendmodule\n"
)
}
fn violations(stdout: &str) -> Vec<&str> {
stdout.lines().filter(|l| l.contains("violation")).collect()
}
#[test]
fn run_design_setup_violation_toggles_the_notifier_and_prints_the_message() {
let stdout = run_src(
"setup-exact",
&check_design(
"$setup(d, posedge clk, 5, ntf);",
"#97 d = 1;\n #3 clk = 1;",
),
);
assert_eq!(
stdout,
"ntf=0 at 200\n200: $setup violation: data top.u.d vs reference top.u.clk, limit=5, \
actual=3\n"
);
}
#[test]
fn run_design_setup_sweep_is_open_at_both_ends() {
for (off, want) in [(6, 0), (5, 0), (4, 1), (1, 1), (0, 0)] {
let stim = if off == 0 {
"d = 1; clk = 1;".to_string()
} else {
format!("d = 1;\n #{off} clk = 1;")
};
let stdout = run_src(
&format!("setup-sweep-{off}"),
&check_design("$setup(d, posedge clk, 5, ntf);", &stim),
);
assert_eq!(violations(&stdout).len(), want, "offset {off}:\n{stdout}");
assert_eq!(stdout.contains("ntf="), want == 1, "notifier, offset {off}");
}
}
#[test]
fn run_design_hold_sweep_is_closed_at_the_start_and_open_at_the_end() {
for (off, want) in [(0, 1), (1, 1), (4, 1), (5, 0), (6, 0)] {
let stim = if off == 0 {
"clk = 1; d = 1;".to_string()
} else {
format!("clk = 1;\n #{off} d = 1;")
};
let stdout = run_src(
&format!("hold-sweep-{off}"),
&check_design("$hold(posedge clk, d, 5, ntf);", &stim),
);
assert_eq!(violations(&stdout).len(), want, "offset {off}:\n{stdout}");
assert_eq!(stdout.contains("ntf="), want == 1, "notifier, offset {off}");
}
}
#[test]
fn run_design_a_zero_limit_never_violates() {
for check in [
"$setup(d, posedge clk, 0, ntf);",
"$hold(posedge clk, d, 0, ntf);",
] {
let stdout = run_src("zero", &check_design(check, "d = 1; clk = 1;"));
assert!(violations(&stdout).is_empty(), "{check}:\n{stdout}");
assert!(!stdout.contains("ntf="), "{check}:\n{stdout}");
}
}
#[test]
fn run_design_a_check_without_a_notifier_still_reports() {
let stdout = run_src(
"no-ntf",
&check_design("$setup(d, posedge clk, 5);", "#97 d = 1;\n #3 clk = 1;"),
);
assert_eq!(violations(&stdout).len(), 1, "{stdout}");
assert!(!stdout.contains("ntf="));
}
#[test]
fn run_design_a_condition_gates_the_reference_and_the_data_event() {
let src = |en: u32, den: u32| {
format!(
"module chk(input clk, input d, input en, input den);\n reg ntf;\n specify\n \
$setup(d &&& den, posedge clk &&& en, 5, ntf);\n endspecify\nendmodule\n\
module top;\n reg clk, d, en, den;\n chk u(.clk(clk), .d(d), .en(en), \
.den(den));\n initial begin\n clk = 0; d = 0; en = {en}; den = {den};\n \
#100 d = 1;\n #3 clk = 1;\n #10 $finish;\n end\nendmodule\n"
)
};
assert_eq!(violations(&run_src("cond-11", &src(1, 1))).len(), 1);
assert!(
violations(&run_src("cond-01", &src(0, 1))).is_empty(),
"reference gated"
);
assert!(
violations(&run_src("cond-10", &src(1, 0))).is_empty(),
"data gated"
);
}
#[test]
fn run_design_a_vector_terminal_changing_in_many_bits_is_one_event() {
let src = "module chk(input clk, input [3:0] d);\n reg ntf;\n specify\n $setup(d, \
posedge clk, 5, ntf);\n endspecify\nendmodule\nmodule top;\n reg clk;\n \
reg [3:0] d;\n chk u(.clk(clk), .d(d));\n initial begin\n clk = 0; \
d = 0;\n #100 d = 4'b1111;\n #3 clk = 1;\n #10 $finish;\n \
end\nendmodule\n";
assert_eq!(violations(&run_src("vec", src)).len(), 1);
}
#[test]
fn run_design_each_instance_has_its_own_check_state() {
let src = "module chk(input clk, input d);\n specify\n $setup(d, posedge clk, 5);\n \
endspecify\nendmodule\nmodule top;\n reg clk1, d1, clk2, d2;\n chk \
a(.clk(clk1), .d(d1));\n chk b(.clk(clk2), .d(d2));\n initial begin\n \
clk1 = 0; d1 = 0; clk2 = 0; d2 = 0;\n #100 d1 = 1; d2 = 1;\n #3 clk1 \
= 1;\n #20 clk2 = 1;\n #10 $finish;\n end\nendmodule\n";
let stdout = run_src("two-inst", src);
let v = violations(&stdout);
assert_eq!(v.len(), 1, "{stdout}");
assert!(
v[0].contains("top.a") || v[0].contains(".a."),
"instance a: {}",
v[0]
);
}
fn sh_violations(setup: i64, hold: i64, stim: &str, tag: &str) -> Vec<String> {
let stdout = run_src(
tag,
&check_design(
&format!("$setuphold(posedge clk, d, {setup}, {hold}, ntf);"),
stim,
),
);
violations(&stdout).iter().map(|l| l.to_string()).collect()
}
#[test]
fn run_design_setuphold_data_first_window_is_open_at_the_start_closed_at_the_end() {
for (off, want) in [(7, 0), (6, 0), (5, 0), (4, 1), (1, 1), (0, 1)] {
let stim = if off == 0 {
"d = 1; clk = 1;".to_string()
} else {
format!("d = 1;\n #{off} clk = 1;")
};
let v = sh_violations(5, 3, &stim, &format!("sh-first-{off}"));
assert_eq!(v.len(), want, "data {off} before the edge: {v:?}");
if want == 1 {
assert!(
v[0].ends_with(&format!("limit=5, actual={off}")),
"{}",
v[0]
);
}
}
}
#[test]
fn run_design_setuphold_data_second_window_is_closed_at_the_start_open_at_the_end() {
for (off, want) in [(0, 1), (1, 1), (2, 1), (3, 0), (4, 0)] {
let stim = if off == 0 {
"clk = 1; d = 1;".to_string()
} else {
format!("clk = 1;\n #{off} d = 1;")
};
let v = sh_violations(5, 3, &stim, &format!("sh-second-{off}"));
assert_eq!(v.len(), want, "data {off} after the edge: {v:?}");
if want == 1 && off != 0 {
assert!(
v[0].ends_with(&format!("limit=3, actual={off}")),
"{}",
v[0]
);
}
}
}
#[test]
fn run_design_setuphold_reports_the_violated_windows_limit_and_toggles_the_notifier() {
let stdout = run_src(
"sh-exact",
&check_design(
"$setuphold(posedge clk, d, 5, 3, ntf);",
"#97 d = 1;\n #3 clk = 1;\n #1 d = 0;",
),
);
assert_eq!(
stdout,
"ntf=0 at 200\n200: $setuphold violation: data top.u.d vs reference top.u.clk, \
limit=5, actual=3\nntf=1 at 201\n201: $setuphold violation: data top.u.d vs \
reference top.u.clk, limit=3, actual=1\n"
);
}
#[test]
fn run_design_setuphold_with_both_limits_zero_never_violates() {
for stim in [
"d = 1; clk = 1;",
"clk = 1; d = 1;",
"d = 1;\n #1 clk = 1;",
"clk = 1;\n #1 d = 1;",
] {
let stdout = run_src(
"sh-zero",
&check_design("$setuphold(posedge clk, d, 0, 0, ntf);", stim),
);
assert!(violations(&stdout).is_empty(), "{stim}:\n{stdout}");
assert!(!stdout.contains("ntf="), "{stim}:\n{stdout}");
}
}
#[test]
fn run_design_setuphold_with_one_zero_limit_keeps_only_the_other_window() {
assert!(sh_violations(0, 3, "d = 1;\n #1 clk = 1;", "sh-s0-first").is_empty());
assert_eq!(
sh_violations(0, 3, "clk = 1;\n #1 d = 1;", "sh-s0-second").len(),
1
);
assert_eq!(
sh_violations(0, 3, "clk = 1; d = 1;", "sh-s0-simul").len(),
1
);
assert_eq!(
sh_violations(5, 0, "d = 1;\n #1 clk = 1;", "sh-h0-first").len(),
1
);
assert!(sh_violations(5, 0, "clk = 1;\n #1 d = 1;", "sh-h0-second").is_empty());
assert_eq!(
sh_violations(5, 0, "clk = 1; d = 1;", "sh-h0-simul").len(),
1
);
}
#[test]
fn run_design_setuphold_is_equivalent_to_a_setup_plus_hold_pair() {
let src = "module one(input clk, input d);\n specify\n $setuphold(posedge clk, d, 3, \
2);\n endspecify\nendmodule\nmodule two(input clk, input d);\n specify\n \
$setup(d, posedge clk, 3);\n $hold(posedge clk, d, 2);\n endspecify\n\
endmodule\nmodule top;\n reg clk, d;\n one a(.clk(clk), .d(d));\n two \
b(.clk(clk), .d(d));\n initial begin\n clk = 0; d = 0;\n #10 d = 1;\n \
#2 clk = 1;\n #10 clk = 0;\n #1 d = 0;\n #7 d = 1;\n #3 clk = 1;\n \
#1 d = 0;\n #5 clk = 0;\n #1 d = 1;\n #5 clk = 1;\n #2 d = 0;\n \
#1 clk = 0;\n #4 begin clk = 1; d = 1; end\n #20 $finish;\n end\n\
endmodule\n";
let stdout = run_src("sh-equiv", src);
let key = |l: &str| {
let now = l.split(':').next().unwrap_or("").to_string();
let actual = l.rsplit("actual=").next().unwrap_or("").to_string();
(now, actual)
};
let of = |inst: &str| -> Vec<(String, String)> {
violations(&stdout)
.into_iter()
.filter(|l| l.contains(&format!("top.{inst}.")))
.map(key)
.collect()
};
let one = of("a");
let two = of("b");
assert_eq!(one.len(), 3, "{stdout}");
assert_eq!(one, two, "{stdout}");
}
#[test]
fn run_design_setuphold_is_closed_at_its_setup_end_where_setup_is_open() {
let both = |check: &str| {
let stdout = run_src("sh-vs-setup", &check_design(check, "d = 1; clk = 1;"));
violations(&stdout).len()
};
assert_eq!(both("$setuphold(posedge clk, d, 5, 0, ntf);"), 1);
assert_eq!(both("$setup(d, posedge clk, 5, ntf);"), 0);
}
#[test]
fn run_design_setuphold_conditions_gate_the_reference_and_the_data_event() {
let src = |en: u32, den: u32| {
format!(
"module chk(input clk, input d, input en, input den);\n reg ntf;\n specify\n \
$setuphold(posedge clk &&& en, d &&& den, 5, 3, ntf);\n endspecify\nendmodule\n\
module top;\n reg clk, d, en, den;\n chk u(.clk(clk), .d(d), .en(en), \
.den(den));\n initial begin\n clk = 0; d = 0; en = {en}; den = {den};\n \
#100 d = 1;\n #3 clk = 1;\n #10 $finish;\n end\nendmodule\n"
)
};
assert_eq!(violations(&run_src("shc-11", &src(1, 1))).len(), 1);
assert!(violations(&run_src("shc-01", &src(0, 1))).is_empty());
assert!(violations(&run_src("shc-10", &src(1, 0))).is_empty());
}
#[test]
fn run_design_setuphold_vector_terminal_is_one_event() {
let src = "module chk(input clk, input [3:0] d);\n specify\n $setuphold(posedge clk, d, \
5, 3);\n endspecify\nendmodule\nmodule top;\n reg clk;\n reg [3:0] d;\n chk \
u(.clk(clk), .d(d));\n initial begin\n clk = 0; d = 0;\n #100 d = \
4'b1111;\n #3 clk = 1;\n #10 $finish;\n end\nendmodule\n";
assert_eq!(violations(&run_src("sh-vec", src)).len(), 1);
}
fn run_src_err(tag: &str, src: &str) -> String {
let file = std::env::temp_dir().join(format!("vj-sdf-t16-{tag}-{}.sv", std::process::id()));
std::fs::write(&file, src).expect("write temp fixture");
let mut out = Vec::new();
let r = crate::run::run_design(&file, &mut out);
let _ = std::fs::remove_file(&file);
match r {
Ok(code) => panic!("expected an error, run finished with {code}:\n{out:?}"),
Err(e) => e.to_string(),
}
}
fn cond_gate(check: &str, first: &str, second: &str, x: u8, y: u8, tag: &str) -> usize {
let src = format!(
"module chk(input clk, input d, input c);\n reg ntf;\n specify\n {check}\n \
endspecify\nendmodule\nmodule top;\n reg clk, d, c;\n chk u(.clk(clk), .d(d), \
.c(c));\n initial begin\n clk = 0; d = 0; c = 0;\n #100 c = {x};\n #1 \
{first} = 1;\n #1 c = {y};\n #1 {second} = 1;\n #100 $finish;\n end\n\
endmodule\n"
);
violations(&run_src(tag, &src)).len()
}
#[test]
fn run_design_setuphold_conditions_gate_each_window_by_event_order() {
for (order, first, second) in [("setup", "d", "clk"), ("hold", "clk", "d")] {
for (what, check, wants) in [
(
"ts",
"$setuphold(posedge clk, d, 3, 3, ntf, c);",
(true, false),
),
(
"tc",
"$setuphold(posedge clk, d, 3, 3, ntf, , c);",
(false, true),
),
(
"both",
"$setuphold(posedge clk, d, 3, 3, ntf, c, c);",
(true, true),
),
] {
for x in 0..2u8 {
for y in 0..2u8 {
let want = (!wants.0 || x == 1) && (!wants.1 || y == 1);
let n = cond_gate(
check,
first,
second,
x,
y,
&format!("shc-{order}-{what}-{x}{y}"),
);
assert_eq!(n, usize::from(want), "{order} {what} c={x}->{y}");
}
}
}
}
}
fn nt_violations(limits: &str, stim: &str, tag: &str) -> Vec<String> {
let check = format!("$setuphold(posedge clk, d, {limits}, ntf);");
let stdout = run_src(tag, &check_design(&check, stim));
violations(&stdout).iter().map(|l| l.to_string()).collect()
}
#[test]
fn run_design_negative_setup_window_is_shifted_after_the_edge() {
let limits = "-10, 20";
for (off, violates) in [
(1, false),
(5, false),
(9, false),
(10, false),
(11, true),
(15, true),
(19, true),
(20, false),
(21, false),
(30, false),
] {
let v = nt_violations(
limits,
&format!("clk = 1;\n #{off} d = 1;"),
&format!("nt-setup-{off}"),
);
assert_eq!(
v.len(),
usize::from(violates),
"data {off} after clk: {v:?}"
);
}
let v = nt_violations(limits, "clk = 1;\n #15 d = 1;", "nt-setup-exact");
assert_eq!(
v,
["115: $setuphold violation: data top.u.d vs reference top.u.clk, limit=9, actual=4"]
);
}
#[test]
fn run_design_negative_hold_window_is_shifted_before_the_edge() {
let limits = "20, -10";
for (off, violates) in [
(1, false),
(9, false),
(10, false),
(11, true),
(15, true),
(19, true),
(20, false),
(25, false),
] {
let v = nt_violations(
limits,
&format!("d = 1;\n #{off} clk = 1;"),
&format!("nt-hold-{off}"),
);
assert_eq!(
v.len(),
usize::from(violates),
"data {off} before clk: {v:?}"
);
}
let v = nt_violations(limits, "d = 1;\n #15 clk = 1;", "nt-hold-exact");
assert_eq!(
v,
["115: $setuphold violation: data top.u.d vs reference top.u.clk, limit=9, actual=4"]
);
}
fn rr_design(check: &str, stim: &str) -> String {
format!(
"module chk(input rst, input clk);\n reg ntf;\n specify\n {check}\n endspecify\n \
always @(ntf) $display(\"ntf=%b at %0t\", ntf, $time);\nendmodule\nmodule top;\n \
reg rst, clk;\n chk u(.rst(rst), .clk(clk));\n initial begin\n rst = 0; clk = 0;\n \
#100;\n {stim}\n #100 $finish;\n end\nendmodule\n"
)
}
fn rr_violations(check: &str, stim: &str, tag: &str) -> Vec<String> {
let stdout = run_src(tag, &rr_design(check, stim));
violations(&stdout).iter().map(|l| l.to_string()).collect()
}
#[test]
fn run_design_removal_violation_prints_the_message_and_toggles_the_notifier() {
let stdout = run_src(
"rem-exact",
&rr_design(
"$removal(posedge rst, posedge clk, 5, ntf);",
"clk = 1;\n #3 rst = 1;\n #1 clk = 0;\n #1 clk = 1;\n #1 rst = 0;\n #1 rst = 1;",
),
);
assert_eq!(
stdout,
"ntf=0 at 103\n103: $removal violation: data top.u.clk vs reference top.u.rst, \
limit=5, actual=3\nntf=1 at 107\n107: $removal violation: data top.u.clk vs reference \
top.u.rst, limit=5, actual=2\n"
);
}
#[test]
fn run_design_recovery_violation_prints_the_message_and_toggles_the_notifier() {
let stdout = run_src(
"rec-exact",
&rr_design(
"$recovery(posedge rst, posedge clk, 5, ntf);",
"rst = 1;\n #3 clk = 1;\n #1 clk = 0;\n #1 clk = 1;",
),
);
assert_eq!(
stdout,
"ntf=0 at 103\n103: $recovery violation: data top.u.clk vs reference top.u.rst, \
limit=5, actual=3\n"
);
}
#[test]
fn run_design_removal_sweep_is_open_at_both_ends() {
let check = "$removal(posedge rst, posedge clk, 5, ntf);";
for (off, want) in [(6, 0), (5, 0), (4, 1), (1, 1)] {
let v = rr_violations(
check,
&format!("clk = 1;\n #{off} rst = 1;"),
&format!("rem-sweep-{off}"),
);
assert_eq!(v.len(), want, "clk {off} before the release: {v:?}");
if want == 1 {
assert!(v[0].ends_with(&format!("limit=5, actual={off}")), "{v:?}");
}
}
for (stim, tag) in [
("clk = 1; rst = 1;", "rem-simul-a"),
("rst = 1; clk = 1;", "rem-simul-b"),
] {
let v = rr_violations(check, stim, tag);
assert!(
v.is_empty(),
"{stim}: the end point is not a violation: {v:?}"
);
}
}
#[test]
fn run_design_recovery_sweep_is_closed_at_the_start_and_open_at_the_end() {
let check = "$recovery(posedge rst, posedge clk, 5, ntf);";
for (off, want) in [(1, 1), (4, 1), (5, 0), (6, 0)] {
let v = rr_violations(
check,
&format!("rst = 1;\n #{off} clk = 1;"),
&format!("rec-sweep-{off}"),
);
assert_eq!(v.len(), want, "clk {off} after the release: {v:?}");
if want == 1 {
assert!(v[0].ends_with(&format!("limit=5, actual={off}")), "{v:?}");
}
}
for (stim, tag) in [
("rst = 1; clk = 1;", "rec-simul-a"),
("clk = 1; rst = 1;", "rec-simul-b"),
] {
let v = rr_violations(check, stim, tag);
assert_eq!(v.len(), 1, "{stim}: the start IS a violation: {v:?}");
assert!(v[0].ends_with("limit=5, actual=0"), "{v:?}");
}
}
#[test]
fn run_design_removal_looks_before_the_release_and_recovery_after_it() {
let rem = "$removal(posedge rst, posedge clk, 5, ntf);";
let rec = "$recovery(posedge rst, posedge clk, 5, ntf);";
let clk_then_rst = "clk = 1;\n #2 rst = 1;";
let rst_then_clk = "rst = 1;\n #2 clk = 1;";
assert_eq!(rr_violations(rem, clk_then_rst, "role-rem-a").len(), 1);
assert!(rr_violations(rem, rst_then_clk, "role-rem-b").is_empty());
assert!(rr_violations(rec, clk_then_rst, "role-rec-a").is_empty());
assert_eq!(rr_violations(rec, rst_then_clk, "role-rec-b").len(), 1);
}
#[test]
fn run_design_removal_and_recovery_with_a_zero_limit_never_violate() {
for check in [
"$removal(posedge rst, posedge clk, 0, ntf);",
"$recovery(posedge rst, posedge clk, 0, ntf);",
] {
for stim in ["clk = 1; rst = 1;", "rst = 1; clk = 1;"] {
let stdout = run_src("rr-zero", &rr_design(check, stim));
assert!(
violations(&stdout).is_empty(),
"{check} / {stim}:\n{stdout}"
);
assert!(!stdout.contains("ntf="), "{check} / {stim}:\n{stdout}");
}
}
}
#[test]
fn run_design_removal_and_recovery_ignore_the_other_edge() {
let rec = "$recovery(posedge rst, posedge clk, 5);";
let v = rr_violations(
rec,
"rst = 1;\n #10 rst = 0;\n #1 clk = 1;",
"edge-rec-rst",
);
assert!(v.is_empty(), "{v:?}");
let v = rr_violations(
rec,
"clk = 1;\n #1 rst = 1;\n #1 clk = 0;\n #20 clk = 1;",
"edge-rec-clk",
);
assert!(v.is_empty(), "{v:?}");
let rem = "$removal(posedge rst, posedge clk, 5);";
let v = rr_violations(
rem,
"clk = 1;\n #20 clk = 0;\n #1 rst = 1;",
"edge-rem-clk",
);
assert!(v.is_empty(), "{v:?}");
let v = rr_violations(
rem,
"rst = 1;\n #1 clk = 1;\n #1 rst = 0;",
"edge-rem-rst",
);
assert!(v.is_empty(), "{v:?}");
}
#[test]
fn run_design_removal_and_recovery_conditions_gate_both_events() {
let src = |kind: &str, stim: &str, en: u32, den: u32| {
let args = "posedge rst &&& en, posedge clk &&& den";
format!(
"module chk(input rst, input clk, input en, input den);\n reg ntf;\n specify\n \
{kind}({args}, 5, ntf);\n endspecify\nendmodule\nmodule top;\n reg rst, \
clk, en, den;\n chk u(.rst(rst), .clk(clk), .en(en), .den(den));\n initial \
begin\n rst = 0; clk = 0; en = {en}; den = {den};\n #100;\n {stim}\n \
#10 $finish;\n end\nendmodule\n"
)
};
for (kind, stim) in [
("$removal", "clk = 1;\n #3 rst = 1;"),
("$recovery", "rst = 1;\n #3 clk = 1;"),
] {
let n = |en, den, tag: &str| violations(&run_src(tag, &src(kind, stim, en, den))).len();
assert_eq!(n(1, 1, "rrc-11"), 1, "{kind}");
assert_eq!(n(0, 1, "rrc-01"), 0, "{kind}: reference gated");
assert_eq!(n(1, 0, "rrc-10"), 0, "{kind}: data gated");
}
}
#[test]
fn run_design_removal_and_recovery_in_one_module_are_independent() {
let stdout = run_src(
"rr-both",
&rr_design(
"$removal(posedge rst, posedge clk, 2, ntf);\n $recovery(posedge rst, posedge \
clk, 3, ntf);",
"clk = 1;\n #1 begin clk = 0; rst = 1; end\n #6 clk = 1;",
),
);
let v = violations(&stdout);
assert_eq!(v.len(), 1, "{stdout}");
assert!(
v[0].contains("$removal") && v[0].ends_with("limit=2, actual=1"),
"{v:?}"
);
}
#[test]
fn run_design_removal_and_recovery_reject_a_negative_limit_by_name() {
for (check, clause) in [
("$removal(posedge rst, posedge clk, -1);", "31.3.4"),
("$recovery(posedge rst, posedge clk, -1);", "31.3.5"),
] {
let e = run_src_err("rr-neg", &rr_design(check, "clk = 1;"));
let name = check.split('(').next().unwrap_or("");
assert!(
e.contains(name) && e.contains("non-negative") && e.contains(clause),
"{check}: unexpected error: {e}"
);
}
}
fn rcr_violations(rec: i64, rem: i64, stim: &str, tag: &str) -> Vec<String> {
rr_violations(
&format!("$recrem(posedge rst, posedge clk, {rec}, {rem}, ntf);"),
stim,
tag,
)
}
#[test]
fn run_design_recrem_reports_the_violated_windows_limit_and_toggles_the_notifier() {
let stdout = run_src(
"rcr-exact",
&rr_design(
"$recrem(posedge rst, posedge clk, 3, 5, ntf);",
"clk = 1;\n #2 rst = 1;\n #1 clk = 0;\n #1 clk = 1;",
),
);
assert_eq!(
stdout,
"ntf=0 at 102\n102: $recrem violation: data top.u.clk vs reference top.u.rst, \
limit=5, actual=2\nntf=1 at 104\n104: $recrem violation: data top.u.clk vs \
reference top.u.rst, limit=3, actual=2\n"
);
}
#[test]
fn run_design_recrem_data_first_window_uses_the_removal_limit_and_is_closed_at_the_end() {
for (off, want) in [(7, 0), (6, 0), (5, 0), (4, 1), (1, 1), (0, 1)] {
let stim = if off == 0 {
"clk = 1; rst = 1;".to_string()
} else {
format!("clk = 1;\n #{off} rst = 1;")
};
let v = rcr_violations(3, 5, &stim, &format!("rcr-first-{off}"));
assert_eq!(v.len(), want, "clock {off} before the release: {v:?}");
if want == 1 {
assert!(
v[0].ends_with(&format!("limit=5, actual={off}")),
"{}",
v[0]
);
}
}
}
#[test]
fn run_design_recrem_data_second_window_uses_the_recovery_limit_and_is_open_at_the_end() {
for (off, want) in [(0, 1), (1, 1), (2, 1), (3, 0), (4, 0), (5, 0)] {
let stim = if off == 0 {
"rst = 1; clk = 1;".to_string()
} else {
format!("rst = 1;\n #{off} clk = 1;")
};
let v = rcr_violations(3, 5, &stim, &format!("rcr-second-{off}"));
assert_eq!(v.len(), want, "clock {off} after the release: {v:?}");
if want == 1 && off != 0 {
assert!(
v[0].ends_with(&format!("limit=3, actual={off}")),
"{}",
v[0]
);
}
}
}
#[test]
fn run_design_recrem_takes_recovery_first_and_removal_second() {
let before = "clk = 1;\n #4 rst = 1;";
let after = "rst = 1;\n #4 clk = 1;";
assert_eq!(rcr_violations(3, 5, before, "rcr-ord-a").len(), 1);
assert!(rcr_violations(3, 5, after, "rcr-ord-b").is_empty());
assert!(rcr_violations(5, 3, before, "rcr-ord-c").is_empty());
assert_eq!(rcr_violations(5, 3, after, "rcr-ord-d").len(), 1);
}
#[test]
fn run_design_recrem_with_both_limits_zero_never_violates() {
for stim in [
"clk = 1; rst = 1;",
"rst = 1; clk = 1;",
"clk = 1;\n #1 rst = 1;",
"rst = 1;\n #1 clk = 1;",
] {
let stdout = run_src(
"rcr-zero",
&rr_design("$recrem(posedge rst, posedge clk, 0, 0, ntf);", stim),
);
assert!(violations(&stdout).is_empty(), "{stim}:\n{stdout}");
assert!(!stdout.contains("ntf="), "{stim}:\n{stdout}");
}
}
#[test]
fn run_design_recrem_with_one_zero_limit_keeps_only_the_other_window() {
let before = "clk = 1;\n #1 rst = 1;";
let after = "rst = 1;\n #1 clk = 1;";
let same = "clk = 1; rst = 1;";
assert_eq!(rcr_violations(0, 3, before, "rcr-r0-first").len(), 1);
assert!(rcr_violations(0, 3, after, "rcr-r0-second").is_empty());
assert_eq!(rcr_violations(0, 3, same, "rcr-r0-simul").len(), 1);
assert!(rcr_violations(3, 0, before, "rcr-m0-first").is_empty());
assert_eq!(rcr_violations(3, 0, after, "rcr-m0-second").len(), 1);
assert_eq!(rcr_violations(3, 0, same, "rcr-m0-simul").len(), 1);
}
#[test]
fn run_design_recrem_is_equivalent_to_a_removal_plus_recovery_pair() {
let src = "module one(input rst, input clk);\n specify\n $recrem(posedge rst, \
posedge clk, 2, 3);\n endspecify\nendmodule\nmodule two(input rst, input \
clk);\n specify\n $removal(posedge rst, posedge clk, 3);\n \
$recovery(posedge rst, posedge clk, 2);\n endspecify\nendmodule\nmodule \
top;\n reg rst, clk;\n one a(.rst(rst), .clk(clk));\n two b(.rst(rst), \
.clk(clk));\n initial begin\n rst = 0; clk = 0;\n #10 clk = 1;\n \
#2 rst = 1;\n #1 clk = 0;\n #1 clk = 1;\n #1 clk = 0;\n #1 clk = 1;\n \
#1 begin clk = 0; rst = 0; end\n #6 clk = 1;\n #1 clk = 0;\n #3 rst = 1;\n \
#1 clk = 1;\n #1 clk = 0;\n #1 rst = 0;\n #7 begin rst = 1; clk = 1; end\n \
#1 clk = 0;\n #1 clk = 1;\n #1 clk = 0;\n #1 rst = 0;\n #1 clk = 1;\n #1 clk = 0;\n #1 rst = 1;\n #1 clk = 1;\n #20 $finish;\n end\nendmodule\n";
let stdout = run_src("rcr-equiv", src);
let key = |l: &str| {
let now = l.split(':').next().unwrap_or("").to_string();
let actual = l.rsplit("actual=").next().unwrap_or("").to_string();
(now, actual)
};
let of = |inst: &str| -> Vec<(String, String)> {
violations(&stdout)
.into_iter()
.filter(|l| l.contains(&format!("top.{inst}.")))
.map(key)
.collect()
};
let one = of("a");
let two = of("b");
assert_eq!(one.len(), 5, "{stdout}");
assert_eq!(one, two, "{stdout}");
}
#[test]
fn run_design_recrem_is_closed_at_its_removal_end_where_removal_is_open() {
let n = |check: &str| {
let stdout = run_src("rcr-vs-rem", &rr_design(check, "clk = 1; rst = 1;"));
violations(&stdout).len()
};
assert_eq!(n("$recrem(posedge rst, posedge clk, 0, 5, ntf);"), 1);
assert_eq!(n("$removal(posedge rst, posedge clk, 5, ntf);"), 0);
}
#[test]
fn run_design_recrem_conditions_gate_the_reference_and_the_data_event() {
let src = |stim: &str, en: u32, den: u32| {
format!(
"module chk(input rst, input clk, input en, input den);\n reg ntf;\n specify\n \
$recrem(posedge rst &&& en, posedge clk &&& den, 3, 5, ntf);\n endspecify\n\
endmodule\nmodule top;\n reg rst, clk, en, den;\n chk u(.rst(rst), .clk(clk), \
.en(en), .den(den));\n initial begin\n rst = 0; clk = 0; en = {en}; \
den = {den};\n #100;\n {stim}\n #10 $finish;\n end\nendmodule\n"
)
};
for (stim, what) in [
("clk = 1;\n #2 rst = 1;", "data first"),
("rst = 1;\n #2 clk = 1;", "data second"),
] {
let n = |en, den, tag: &str| violations(&run_src(tag, &src(stim, en, den))).len();
assert_eq!(n(1, 1, "rcrc-11"), 1, "{what}");
assert_eq!(n(0, 1, "rcrc-01"), 0, "{what}: reference gated");
assert_eq!(n(1, 0, "rcrc-10"), 0, "{what}: data gated");
}
}
#[test]
fn run_design_recrem_ignores_the_other_edge() {
let v = rcr_violations(
3,
5,
"rst = 1;\n #5 rst = 0;\n #1 clk = 1;",
"rcr-edge-rst",
);
assert!(v.is_empty(), "{v:?}");
let v = rcr_violations(
3,
5,
"clk = 1;\n #20 clk = 0;\n #1 rst = 1;",
"rcr-edge-clk",
);
assert!(v.is_empty(), "{v:?}");
}
#[test]
fn run_design_recrem_vector_terminal_is_one_event() {
let src = "module chk(input rst, input [3:0] d);\n specify\n $recrem(posedge rst, d, 3, \
5);\n endspecify\nendmodule\nmodule top;\n reg rst;\n reg [3:0] d;\n chk \
u(.rst(rst), .d(d));\n initial begin\n rst = 0; d = 0;\n #100 d = \
4'b1111;\n #2 rst = 1;\n #10 $finish;\n end\nendmodule\n";
assert_eq!(violations(&run_src("rcr-vec", src)).len(), 1);
}
#[test]
fn run_design_recrem_conditions_gate_each_window_by_event_order() {
for (order, first, second) in [("removal", "clk", "d"), ("recovery", "d", "clk")] {
for (what, check, wants) in [
(
"ts",
"$recrem(posedge clk, posedge d, 3, 3, ntf, c);",
(true, false),
),
(
"tc",
"$recrem(posedge clk, posedge d, 3, 3, ntf, , c);",
(false, true),
),
] {
for x in 0..2u8 {
for y in 0..2u8 {
let want = (!wants.0 || x == 1) && (!wants.1 || y == 1);
let n = cond_gate(
check,
first,
second,
x,
y,
&format!("rcc-{order}-{what}-{x}{y}"),
);
assert_eq!(n, usize::from(want), "{order} {what} c={x}->{y}");
}
}
}
}
}
#[test]
fn run_design_recrem_each_check_has_its_own_state() {
let src = "module chk(input rst, input clk, input rst2, input clk2);\n specify\n \
$recrem(posedge rst, posedge clk, 3, 5);\n $recrem(posedge rst2, posedge \
clk2, 3, 5);\n endspecify\nendmodule\nmodule top;\n reg rst, clk, rst2, \
clk2;\n chk u(.rst(rst), .clk(clk), .rst2(rst2), .clk2(clk2));\n initial \
begin\n rst = 0; clk = 0; rst2 = 0; clk2 = 0;\n #100 clk = 1;\n #2 \
rst = 1;\n #20 clk2 = 1;\n #20 rst2 = 1;\n #10 $finish;\n end\n\
endmodule\n";
let stdout = run_src("rcr-two", src);
let v = violations(&stdout);
assert_eq!(v.len(), 1, "{stdout}");
assert!(v[0].starts_with("102: $recrem"), "{}", v[0]);
}
#[test]
fn width_state_sweep_matches_the_inequality() {
let mut hits = 0;
for threshold in 0..=3 {
for limit in 0..=6 {
for width in 0..=8 {
let mut s = WidthState::new(limit, threshold);
s.reference_event(100);
let got = s.data_event(100 + width);
let want = (threshold < width && width < limit).then_some(width);
assert_eq!(
got, want,
"threshold {threshold}, limit {limit}, width {width}"
);
hits += usize::from(got.is_some());
}
}
}
assert!(
hits > 0,
"the sweep must contain violations or it proves nothing"
);
}
#[test]
fn width_state_needs_a_timestamp_and_spends_it() {
let mut s = WidthState::new(10, 0);
assert_eq!(s.data_event(5), None, "no timestamp yet");
s.reference_event(10);
assert_eq!(s.data_event(14), Some(4));
assert_eq!(s.data_event(15), None, "the timestamp was spent");
s.reference_event(20);
assert_eq!(s.data_event(21), Some(1));
}
#[test]
fn width_state_measures_from_the_latest_timestamp() {
let mut s = WidthState::new(10, 0);
s.reference_event(10);
s.reference_event(14);
assert_eq!(s.data_event(17), Some(3));
}
#[test]
fn period_state_sweep_matches_the_inequality() {
let mut hits = 0;
for limit in 0..=6 {
for period in 1..=8 {
let mut s = PeriodState::new(limit);
assert_eq!(s.data_event(100), None, "the first edge has no period");
s.reference_event(100);
let got = s.data_event(100 + period);
s.reference_event(100 + period);
let want = (period < limit).then_some(period);
assert_eq!(got, want, "limit {limit}, period {period}");
hits += usize::from(got.is_some());
}
}
assert!(
hits > 0,
"the sweep must contain violations or it proves nothing"
);
}
#[test]
fn period_state_measures_consecutive_edges() {
let mut s = PeriodState::new(10);
let mut got = Vec::new();
for now in [0, 12, 14, 30, 39] {
got.push(s.data_event(now));
s.reference_event(now);
}
assert_eq!(got, [None, None, Some(2), None, Some(9)]);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Nc {
Lead,
Trail,
Data,
}
fn nc_run(start: i64, end: i64, events: &[(i64, Nc)]) -> Vec<(i64, i64)> {
let mut s = NoChangeState::new(start, end);
let mut out = Vec::new();
for &(now, e) in events {
let found = match e {
Nc::Lead => s.reference_event(now),
Nc::Trail => s.trailing_event(now),
Nc::Data => s.data_event(now),
};
out.extend(found.into_iter().map(|actual| (now, actual)));
}
out
}
#[test]
fn nochange_zero_offsets_flag_a_change_strictly_inside_the_high_period() {
use Nc::{Data, Lead, Trail};
for (t, want) in [
(9, false),
(10, false),
(11, true),
(19, true),
(20, false),
(21, false),
] {
let got = if t < 10 {
nc_run(0, 0, &[(t, Data), (10, Lead), (20, Trail)])
} else if t <= 20 {
nc_run(0, 0, &[(10, Lead), (t, Data), (20, Trail)])
} else {
nc_run(0, 0, &[(10, Lead), (20, Trail), (t, Data)])
};
assert_eq!(!got.is_empty(), want, "data change at {t}");
if want {
assert_eq!(got, [(20, t - 10)], "data change at {t}");
}
}
}
#[test]
fn nochange_zero_offsets_ignore_a_change_at_either_reference_edge() {
use Nc::{Data, Lead, Trail};
for first_is_data in [true, false] {
let at = |t: i64, edge: Nc| {
if first_is_data {
[(t, Data), (t, edge)]
} else {
[(t, edge), (t, Data)]
}
};
let mut events = Vec::new();
events.extend(at(10, Lead));
events.extend(at(20, Trail));
let got = nc_run(0, 0, &events);
assert!(got.is_empty(), "data-first {first_is_data}: {got:?}");
}
}
#[test]
fn nochange_offsets_extend_and_shrink_the_window() {
use Nc::{Data, Lead, Trail};
for (start, end, t, want) in [
(3, 0, 96, false),
(3, 0, 97, false),
(3, 0, 98, true),
(3, 0, 100, true),
(-3, 0, 103, false),
(-3, 0, 104, true),
(0, 4, 113, true),
(0, 4, 114, false),
(0, -4, 105, true),
(0, -4, 106, false),
] {
let events = if t < 100 {
[(t, Data), (100, Lead), (110, Trail)]
} else if t <= 110 {
[(100, Lead), (t, Data), (110, Trail)]
} else {
[(100, Lead), (110, Trail), (t, Data)]
};
let got = nc_run(start, end, &events);
assert_eq!(
!got.is_empty(),
want,
"$nochange(.., {start}, {end}), data at {t}: {got:?}"
);
if want {
assert_eq!(got.len(), 1);
assert_eq!(got[0].1, t - 100, "actual is relative to the leading edge");
}
}
}
#[test]
fn nochange_the_fixtures_two_checks_in_both_same_instant_orders() {
use Nc::{Data, Lead, Trail};
let script: [(i64, Option<Nc>, bool); 14] = [
(0, Some(Trail), true),
(11, None, true),
(12, Some(Lead), false),
(16, None, true),
(20, Some(Trail), false),
(22, None, true),
(30, Some(Lead), true),
(36, Some(Trail), true),
(40, None, true),
(47, None, true),
(48, Some(Lead), false),
(50, None, true),
(52, Some(Trail), false),
(57, None, true),
];
for data_first in [true, false] {
let mut events = Vec::new();
for (t, edge, data) in script {
let mut pair: Vec<(i64, Nc)> = Vec::new();
if data {
pair.push((t, Data));
}
if let Some(e) = edge {
pair.push((t, e));
}
if !data_first {
pair.reverse();
}
events.extend(pair);
}
assert_eq!(
nc_run(0, 0, &events),
[(20, 4), (52, 2)],
"(0, 0), data first {data_first}"
);
let mut got = nc_run(2, 3, &events);
got.sort_unstable();
assert_eq!(
got,
[
(12, -1),
(16, 4),
(22, 10),
(30, 0),
(36, 6),
(48, -1),
(50, 2)
],
"(2, 3), data first {data_first}"
);
}
}
#[test]
fn nochange_ignores_a_second_leading_edge_and_an_unmatched_trailing_edge() {
use Nc::{Data, Lead, Trail};
assert_eq!(
nc_run(
0,
0,
&[(5, Trail), (10, Lead), (12, Lead), (14, Data), (20, Trail)]
),
[(20, 4)],
"actual is from the first leading edge (10), not the second (12)"
);
assert!(
nc_run(0, 0, &[(10, Lead), (20, Trail), (22, Trail), (25, Data)]).is_empty(),
"a second trailing edge does not reopen the window"
);
}
#[test]
fn nochange_a_window_still_open_at_the_end_does_not_report_its_pending_change() {
use Nc::{Data, Lead};
assert!(nc_run(0, 0, &[(10, Lead), (14, Data)]).is_empty());
assert_eq!(nc_run(0, 1, &[(10, Lead), (14, Data)]), [(14, 4)]);
}
#[test]
fn nochange_reports_a_data_change_once_across_overlapping_windows() {
use Nc::{Data, Lead, Trail};
let got = nc_run(
0,
10,
&[
(10, Lead),
(12, Trail),
(14, Lead),
(15, Data),
(16, Trail),
(24, Data),
],
);
assert_eq!(got, [(15, 5), (24, 10)]);
}
fn lcg(seed: &mut u64) -> u64 {
*seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
*seed >> 33
}
#[test]
fn nochange_state_matches_the_lrm_windows_on_random_traces_in_every_event_order() {
let mut violations_seen = 0;
for start in -2..=3 {
for end in -2..=3 {
for seed0 in 0..120u64 {
let mut seed = seed0 * 7919 + 13;
let mut trace: Vec<(i64, Option<Nc>, bool)> = Vec::new();
let mut t = 0;
for _ in 0..8 {
t += 1 + (lcg(&mut seed) % 3) as i64;
let edge = match lcg(&mut seed) % 4 {
0 => Some(Nc::Lead),
1 => Some(Nc::Trail),
_ => None,
};
trace.push((t, edge, lcg(&mut seed).is_multiple_of(2)));
}
trace.push((t + 20, Some(Nc::Trail), false));
let mut windows: Vec<(i64, i64)> = Vec::new();
let mut open = None;
for &(t, edge, _) in &trace {
match edge {
Some(Nc::Lead) if open.is_none() => open = Some(t),
Some(Nc::Trail) => {
if let Some(l) = open.take() {
windows.push((l, t));
}
}
_ => {}
}
}
let mut want: Vec<i64> = trace
.iter()
.filter(|&&(_, _, data)| data)
.filter_map(|&(t, _, _)| {
windows
.iter()
.find(|&&(l, tt)| l - start < t && t < tt + end)
.map(|&(l, _)| t - l)
})
.collect();
want.sort_unstable();
violations_seen += want.len();
let instants_with_two =
trace.iter().filter(|&&(_, e, d)| e.is_some() && d).count();
let mut canonical: Option<Vec<(i64, i64)>> = None;
for mask in 0..1u32 << instants_with_two {
let mut events = Vec::new();
let mut bit = 0;
for &(t, edge, data) in &trace {
let mut pair = Vec::new();
if data {
pair.push((t, Nc::Data));
}
if let Some(e) = edge {
pair.push((t, e));
}
if pair.len() == 2 {
if mask >> bit & 1 == 1 {
pair.reverse();
}
bit += 1;
}
events.extend(pair);
}
let mut got = nc_run(start, end, &events);
got.sort_unstable();
let mut actuals: Vec<i64> = got.iter().map(|g| g.1).collect();
actuals.sort_unstable();
assert_eq!(
actuals, want,
"$nochange(.., {start}, {end}) on {trace:?}, order mask {mask:b}"
);
match &canonical {
None => canonical = Some(got),
Some(c) => assert_eq!(
&got, c,
"report times differ with the order of same-instant events: \
$nochange(.., {start}, {end}) on {trace:?}, mask {mask:b}"
),
}
}
}
}
}
assert!(
violations_seen > 1000,
"only {violations_seen} violations in the sweep"
);
}
#[test]
fn opposite_event_controls_follow_the_reference_edge() {
let pos = control(sv_lang::EdgeKind::PosEdge, &[]);
let neg = control(sv_lang::EdgeKind::NegEdge, &[]);
assert_eq!(opposite_control(pos, "$width", "x"), Ok(neg));
assert_eq!(opposite_control(neg, "$width", "x"), Ok(pos));
let e = opposite_control(EdgeControl::AnyChange, "$width", "x").unwrap_err();
assert!(e.contains("$width") && e.contains("edge"), "{e}");
}
fn pulse_design(check: &str, stim: &str) -> String {
format!(
"module chk(input clr, input en);\n reg ntf;\n specparam lim = 10, thresh = 3;\n \
specify\n {check}\n endspecify\n always @(ntf) $display(\"ntf=%b at %0t\", ntf, \
$time);\nendmodule\nmodule top;\n reg clr, en;\n chk u(.clr(clr), .en(en));\n \
initial begin\n clr = 1; en = 1;\n #100;\n {stim}\n #100 $finish;\n \
end\nendmodule\n"
)
}
fn low_pulse(check: &str, w: i64, tag: &str) -> (String, Vec<String>) {
let stdout = run_src(
&format!("{tag}-{w}"),
&pulse_design(check, &format!("clr = 0;\n #{w} clr = 1;")),
);
let v = violations(&stdout).iter().map(|l| l.to_string()).collect();
(stdout, v)
}
#[test]
fn run_design_width_call_shape_limit_only() {
for (w, want) in [(1, true), (5, true), (9, true), (10, false), (11, false)] {
let (stdout, v) = low_pulse("$width(negedge clr, lim);", w, "w1");
assert_eq!(v.len(), usize::from(want), "width {w}:\n{stdout}");
assert!(!stdout.contains("ntf="), "no notifier is declared");
}
let (_, v) = low_pulse("$width(negedge clr, lim);", 4, "w1x");
assert_eq!(
v,
["104: $width violation: data top.u.clr vs reference top.u.clr, limit=10, actual=4"]
);
}
#[test]
fn run_design_width_call_shape_limit_threshold_notifier() {
for (w, want) in [(2, false), (3, false), (4, true), (9, true), (10, false)] {
let (stdout, v) = low_pulse("$width(negedge clr, lim, thresh, ntf);", w, "w2");
assert_eq!(v.len(), usize::from(want), "width {w}:\n{stdout}");
assert_eq!(stdout.contains("ntf=0 at"), want, "notifier, width {w}");
}
}
#[test]
fn run_design_width_call_shape_zero_threshold_with_notifier() {
for (w, want) in [(1, true), (9, true), (10, false)] {
let (stdout, v) = low_pulse("$width(negedge clr, lim, 0, ntf);", w, "w3");
assert_eq!(v.len(), usize::from(want), "width {w}:\n{stdout}");
assert_eq!(stdout.contains("ntf=0 at"), want, "notifier, width {w}");
}
}
#[test]
fn run_design_width_posedge_reference_measures_the_high_pulse() {
for (w, want) in [(5, true), (6, false)] {
let stdout = run_src(
&format!("w-pos-{w}"),
&pulse_design(
"$width(posedge clr, 6, 0, ntf);",
&format!("clr = 0;\n #20 clr = 1;\n #{w} clr = 0;"),
),
);
assert_eq!(violations(&stdout).len(), usize::from(want), "{stdout}");
}
}
#[test]
fn run_design_width_illegal_call_shapes_are_named_errors() {
for (tag, call, what) in [
(
"ill1",
"$width(negedge clr, lim, , ntf);",
["$width", "threshold"],
),
(
"ill2",
"$width(negedge clr, lim, ntf);",
["ntf", "constant"],
),
] {
let e = run_src_err(tag, &pulse_design(call, "clr = 0;"));
assert!(what.iter().all(|w| e.contains(w)), "{tag}: {e}");
}
}
#[test]
fn run_design_width_and_period_reject_a_negative_limit_or_threshold_by_name() {
for (tag, call, what) in [
("neg-wl", "$width(negedge clr, -1);", "$width"),
("neg-wt", "$width(negedge clr, 5, -1, ntf);", "threshold"),
("neg-pl", "$period(posedge clr, -1);", "$period"),
] {
let e = run_src_err(tag, &pulse_design(call, "clr = 0;"));
assert!(
e.contains(what) && e.contains("non-negative"),
"{tag}: unexpected error: {e}"
);
}
}
#[test]
fn run_design_width_derived_data_event_is_gated_by_the_reference_condition() {
let check = "$width(posedge clr &&& en, 10);";
let pulse = |en_at_fall: &str, tag: &str| {
let stdout = run_src(
tag,
&pulse_design(
check,
&format!(
"clr = 0;\n #10 clr = 1;\n #3 en = {en_at_fall};\n #2 clr = 0;"
),
),
);
violations(&stdout).len()
};
assert_eq!(pulse("1", "w-cond-1"), 1, "en high at the falling edge");
assert_eq!(pulse("0", "w-cond-0"), 0, "en low at the falling edge");
}
#[test]
fn run_design_period_reports_a_short_period_and_not_one_equal_to_the_limit() {
let run = |period: i64, tag: &str| {
run_src(
tag,
&pulse_design(
"$period(posedge clr, lim, ntf);",
&format!(
"clr = 0;\n #5 clr = 1;\n #{} clr = 0;\n #1 clr = 1;",
period - 1
),
),
)
};
let stdout = run(7, "p-short");
assert_eq!(
stdout,
"ntf=0 at 112\n112: $period violation: data top.u.clr vs reference top.u.clr, \
limit=10, actual=7\n"
);
assert_eq!(violations(&run(10, "p-eq")).len(), 0, "period == limit");
assert_eq!(violations(&run(11, "p-long")).len(), 0, "period > limit");
assert_eq!(violations(&run(9, "p-9")).len(), 1, "period < limit");
}
#[test]
fn run_design_period_edges_while_the_condition_is_false_are_not_events() {
let stim = "clr = 0;\n #5 clr = 1;\n #2 clr = 0;\n #1 en = 0;\n #1 clr = 1;\n \
#1 clr = 0;\n #1 en = 1;\n #1 clr = 1;";
let stdout = run_src(
"p-cond",
&pulse_design("$period(posedge clr &&& en, lim);", stim),
);
let v = violations(&stdout);
assert_eq!(v.len(), 1, "{stdout}");
assert!(v[0].starts_with("112: $period"), "{}", v[0]);
assert!(v[0].ends_with("actual=7"), "{}", v[0]);
}
fn nochange_design(check: &str, stim: &str) -> String {
format!(
"module chk(input clk, input d);\n reg ntf;\n specify\n {check}\n endspecify\n \
always @(ntf) $display(\"ntf=%b at %0t\", ntf, $time);\nendmodule\nmodule top;\n \
reg clk, d;\n chk u(.clk(clk), .d(d));\n initial begin\n clk = 0; d = 0;\n \
#100;\n {stim}\n #100 $finish;\n end\nendmodule\n"
)
}
#[test]
fn run_design_nochange_negative_offsets_shrink_the_window() {
for (off, want) in [
(1, false),
(2, false),
(3, true),
(8, true),
(9, false),
(10, false),
] {
let stdout = run_src(
&format!("nc-shrink-{off}"),
&nochange_design(
"$nochange(posedge clk, d, -2, -1, ntf);",
&format!("clk = 1;\n #{off} d = 1;\n #{} clk = 0;", 10 - off),
),
);
assert_eq!(
violations(&stdout).len(),
usize::from(want),
"d at +{off}:\n{stdout}"
);
assert_eq!(stdout.contains("ntf="), want, "notifier, d at +{off}");
}
}
#[test]
fn run_design_nochange_start_offset_flags_a_change_before_the_leading_edge() {
let stdout = run_src(
"nc-early",
&nochange_design(
"$nochange(posedge clk, d, 3, 0, ntf);",
"#7 d = 1;\n #2 clk = 1;\n #5 clk = 0;",
),
);
assert_eq!(
stdout,
"ntf=0 at 109\n109: $nochange violation: data top.u.d vs reference top.u.clk, \
limit=3,0, actual=-2\n"
);
let stdout = run_src(
"nc-early-edge",
&nochange_design(
"$nochange(posedge clk, d, 3, 0, ntf);",
"#6 d = 1;\n #3 clk = 1;\n #5 clk = 0;",
),
);
assert_eq!(violations(&stdout).len(), 0, "{stdout}");
}
#[test]
fn run_design_nochange_negedge_reference_watches_the_low_period() {
let run = |at: &str, tag: &str| {
let stdout = run_src(
tag,
&nochange_design(
"$nochange(negedge clk, d, 0, 0);",
&format!("clk = 1;\n #10 clk = 0;\n {at}\n #10 clk = 1;"),
),
);
violations(&stdout).len()
};
assert_eq!(
run("#4 d = 1;", "nc-neg-in"),
1,
"d changes while clk is low"
);
assert_eq!(
run("#0 d = 1;", "nc-neg-edge"),
0,
"d changes AT the negedge"
);
}
fn skew_measure(s: &mut SkewState, at: i64) -> Option<i64> {
s.data_event(at);
let n = s.pending();
s.settle(n).first().copied()
}
#[test]
fn skew_state_sweep_matches_the_inequality() {
let mut hits = 0;
for limit in 0..=6 {
for skew in 0..=8 {
let mut s = SkewState::new(limit);
s.reference_event(100);
let got = skew_measure(&mut s, 100 + skew);
let want = (skew > limit).then_some(skew);
assert_eq!(got, want, "limit {limit}, skew {skew}");
hits += usize::from(got.is_some());
}
}
assert!(
hits > 0,
"the sweep must contain violations or it proves nothing"
);
}
#[test]
fn skew_state_reports_every_late_data_event() {
let mut s = SkewState::new(5);
s.reference_event(10);
let got: Vec<_> = [15, 16, 18, 30, 31].map(|t| skew_measure(&mut s, t)).into();
assert_eq!(got, [None, Some(6), Some(8), Some(20), Some(21)]);
}
#[test]
fn skew_state_a_second_reference_event_replaces_the_first() {
let mut s = SkewState::new(5);
s.reference_event(10);
s.reference_event(20);
assert_eq!(skew_measure(&mut s, 22), None);
assert_eq!(skew_measure(&mut s, 26), Some(6));
}
#[test]
fn skew_state_simultaneous_events_never_violate_even_with_limit_zero() {
let mut s = SkewState::new(0);
s.reference_event(10);
assert_eq!(skew_measure(&mut s, 10), None);
assert_eq!(skew_measure(&mut s, 11), Some(1));
}
#[test]
fn skew_state_a_data_event_waits_for_a_later_reference_event_of_its_instant() {
let mut s = SkewState::new(0);
s.reference_event(10);
s.data_event(30);
assert_eq!(s.pending(), 1);
s.reference_event(30);
assert_eq!(s.settle(1), Vec::<i64>::new());
assert_eq!(s.pending(), 0);
s.data_event(40);
s.data_event(41);
assert_eq!(s.settle(1), [10]);
assert_eq!(s.pending(), 1);
assert_eq!(s.settle(1), [11]);
}
#[test]
fn skew_state_is_never_evaluated_without_a_reference_and_a_data_event() {
let mut s = SkewState::new(0);
assert_eq!(skew_measure(&mut s, 5), None, "no reference event yet");
assert_eq!(s.evaluations(), 0);
s.reference_event(10);
s.reference_event(20);
assert_eq!(s.evaluations(), 0, "reference events evaluate nothing");
assert_eq!(skew_measure(&mut s, 21), Some(1));
assert_eq!(s.evaluations(), 1);
}
#[test]
fn deliver_defers_a_skew_data_event_and_only_a_skew_data_event() {
let mut skew = Machine::Skew(SkewState::new(0));
assert!(skew.deliver(10, false, true, false).is_empty());
assert!(skew.deliver(25, true, true, false).is_empty());
assert_eq!(skew.pending(), 1);
assert!(
skew.settle(1).is_empty(),
"the simultaneous reference event is the one the data event measures"
);
assert!(skew.deliver(26, true, false, false).is_empty());
assert_eq!(skew.settle(1).len(), 1);
let mut width = Machine::Width(WidthState::new(10, 0));
assert!(width.deliver(10, false, true, false).is_empty());
let v = width.deliver(14, true, true, false);
assert_eq!(v.len(), 1, "{v:?}");
assert_eq!(v[0].actual, 4);
assert_eq!(width.pending(), 0);
}
#[test]
fn run_design_skew_rejects_a_negative_limit_by_name() {
let e = run_src_err(
"skew-neg",
&check_design("$skew(posedge clk, d, -1, ntf);", "clk = 1;"),
);
assert!(
e.contains("$skew") && e.contains("non-negative") && e.contains("31.4.1"),
"unexpected error: {e}"
);
}
#[test]
fn run_design_skew_violation_toggles_the_notifier_and_prints_the_message() {
let stdout = run_src(
"skew-exact",
&check_design("$skew(posedge clk, d, 5, ntf);", "clk = 1;\n #8 d = 1;"),
);
assert_eq!(
stdout,
"ntf=0 at 108\n108: $skew violation: data top.u.d vs reference top.u.clk, \
limit=5, actual=8\n"
);
}
#[test]
fn run_design_skew_sweep_is_closed_at_the_limit() {
for (at, want) in [(0, false), (4, false), (5, false), (6, true), (9, true)] {
let stdout = run_src(
&format!("skew-sweep-{at}"),
&check_design(
"$skew(posedge clk, d, 5, ntf);",
&format!("clk = 1;\n #{at} d = 1;"),
),
);
assert_eq!(violations(&stdout).len(), usize::from(want), "at +{at}");
assert_eq!(stdout.contains("ntf="), want, "notifier at +{at}");
}
}
#[test]
fn run_design_skew_reports_every_data_event_and_none_before_a_reference() {
let stdout = run_src(
"skew-every",
&check_design(
"$skew(posedge clk, d, 5, ntf);",
"d = 1;\n #5 d = 0;\n #5 clk = 1;\n #7 d = 1;\n #1 d = 0;\n \
#1 d = 1;",
),
);
let v = violations(&stdout);
assert_eq!(v.len(), 3, "{stdout}");
assert!(
v[0].starts_with("117:") && v[0].ends_with("actual=7"),
"{}",
v[0]
);
assert!(
v[1].starts_with("118:") && v[1].ends_with("actual=8"),
"{}",
v[1]
);
assert!(
v[2].starts_with("119:") && v[2].ends_with("actual=9"),
"{}",
v[2]
);
}
#[test]
fn run_design_skew_a_second_reference_edge_cancels_the_old_wait() {
let stdout = run_src(
"skew-cancel",
&check_design(
"$skew(posedge clk, d, 5, ntf);",
"clk = 1;\n #3 clk = 0;\n #3 clk = 1;\n #3 d = 1;",
),
);
assert_eq!(stdout, "", "{stdout}");
let stdout = run_src(
"skew-never",
&check_design(
"$skew(posedge clk, d, 0, ntf);",
"clk = 1;\n #3 clk = 0;",
),
);
assert_eq!(stdout, "");
}
#[test]
fn run_design_skew_simultaneous_transitions_do_not_violate_with_limit_zero() {
let stdout = run_src(
"skew-simul",
&check_design(
"$skew(posedge clk, d, 0, ntf);",
"clk = 1;\n #20 clk = 0;\n #20 begin clk = 1; d = 1; end",
),
);
assert_eq!(stdout, "", "{stdout}");
let stdout = run_src(
"skew-apart",
&check_design(
"$skew(posedge clk, d, 0, ntf);",
"clk = 1;\n #20 clk = 0;\n #20 clk = 1;\n #1 d = 1;",
),
);
assert_eq!(violations(&stdout).len(), 1, "{stdout}");
}
fn skew_late_reference_design(limit: i64, depth: usize, stim: &str) -> String {
let mut wires = String::new();
let mut prev = "r0".to_string();
for i in 1..=depth {
wires.push_str(&format!(" wire r{i};\n assign r{i} = {prev};\n"));
prev = format!("r{i}");
}
format!(
"module chk(input r, input d);\n reg ntf;\n specify\n $skew(posedge r, d, {limit}, \
ntf);\n endspecify\n always @(ntf) $display(\"ntf=%b at %0t\", ntf, $time);\n\
endmodule\nmodule top;\n reg r0, d;\n{wires} chk u(.r({prev}), .d(d));\n initial \
begin\n r0 = 0; d = 0;\n {stim}\n #100 $finish;\n end\nendmodule\n"
)
}
#[test]
fn run_design_skew_a_reference_a_delta_late_is_still_simultaneous() {
for depth in 1..=3 {
let stdout = run_src(
&format!("skew-delta-{depth}"),
&skew_late_reference_design(
0,
depth,
"#10 r0 = 1;\n #10 r0 = 0;\n #10 begin d = 1; r0 = 1; end",
),
);
assert_eq!(stdout, "", "reference {depth} delta(s) late: {stdout}");
let stdout = run_src(
&format!("skew-delta-rf-{depth}"),
&skew_late_reference_design(
0,
depth,
"#10 r0 = 1;\n #10 r0 = 0;\n #10 begin r0 = 1; d = 1; end",
),
);
assert_eq!(
stdout, "",
"reference first, {depth} delta(s) late: {stdout}"
);
}
}
#[test]
fn run_design_skew_a_delta_late_reference_still_reports_real_violations() {
let stdout = run_src(
"skew-delta-real",
&skew_late_reference_design(
0,
2,
"#10 r0 = 1;\n #1 d = 1;\n #5 r0 = 0;\n #5 begin d = 0; r0 = 1; end\n \
#5 d = 1;",
),
);
assert_eq!(
stdout,
"ntf=0 at 11\n11: $skew violation: data top.u.d vs reference top.u.r, limit=0, \
actual=1\nntf=1 at 26\n26: $skew violation: data top.u.d vs reference top.u.r, \
limit=0, actual=5\n"
);
}
#[test]
fn run_design_skew_edge_and_condition_select_the_events() {
let stdout = run_src(
"skew-edge",
&check_design(
"$skew(posedge clk, d, 5, ntf);",
"clk = 1;\n #10 clk = 0;\n #10 d = 1;",
),
);
assert_eq!(violations(&stdout).len(), 1, "d at +20 from the posedge");
let stdout = run_src(
"skew-neg-ref",
&check_design(
"$skew(negedge clk, d, 5, ntf);",
"clk = 1;\n #10 clk = 0;\n #3 d = 1;\n #10 d = 0;",
),
);
let v = violations(&stdout);
assert_eq!(v.len(), 1, "{stdout}");
assert!(
v[0].starts_with("123:") && v[0].ends_with("actual=13"),
"{}",
v[0]
);
let stdout = run_src(
"skew-cond",
&check_design(
"$skew(posedge clk, d &&& clk, 5, ntf);",
"clk = 1;\n #10 d = 1;\n #10 clk = 0;\n #10 d = 0;",
),
);
assert_eq!(violations(&stdout).len(), 1, "{stdout}");
}
#[derive(Clone, Copy, Debug)]
enum Ev {
Ref,
RefFalse,
Data,
DataFalse,
}
fn skew_replay(mut s: TimeSkewState, events: &[(i64, Ev)]) -> Vec<(i64, i64, i64)> {
let mut hits = Vec::new();
let mut i = 0;
loop {
let now = match (events.get(i).map(|e| e.0), s.deadline()) {
(None, None) => break,
(Some(t), None) | (None, Some(t)) => t,
(Some(t), Some(d)) => t.min(d),
};
let from = i;
while events.get(i).is_some_and(|e| e.0 == now) {
i += 1;
}
let batch = &events[from..i];
for (_, ev) in batch {
match ev {
Ev::Ref => s.reference_event(now),
Ev::RefFalse => s.reference_suppressed(now),
Ev::DataFalse => s.data_suppressed(now),
Ev::Data => {}
}
}
for _ in batch.iter().filter(|(_, ev)| matches!(ev, Ev::Data)) {
s.data_event(now);
}
let n = s.pending();
hits.extend(s.settle_limited(n).into_iter().map(|(a, l)| (now, a, l)));
hits.extend(s.expire_limited(now).map(|(a, l)| (now, a, l)));
}
hits
}
fn timeskew_replay(limit: i64, flags: SkewFlags, events: &[(i64, Ev)]) -> Vec<(i64, i64)> {
skew_replay(TimeSkewState::new(limit, flags), events)
.into_iter()
.map(|(t, actual, _)| (t, actual))
.collect()
}
fn flags(event_based: bool, remain_active: bool) -> SkewFlags {
SkewFlags {
event_based,
remain_active,
}
}
fn figure_31_1_events() -> Vec<(i64, Ev)> {
let mut e = vec![(100, Ev::Ref)];
e.extend([160, 170, 180].map(|t| (t, Ev::Data)));
e.push((185, Ev::RefFalse));
e.extend([190, 200, 210, 220].map(|t| (t, Ev::Data)));
e
}
fn figure_31_2_events() -> Vec<(i64, Ev)> {
let mut e = vec![(100, Ev::Ref), (120, Ev::RefFalse)];
e.extend([160, 170, 180, 190].map(|t| (t, Ev::Data)));
e
}
fn timeskew_cases() -> [SkewFlags; 4] {
[
flags(false, false),
flags(true, false),
flags(true, true),
flags(false, true),
]
}
#[test]
fn timeskew_state_replays_figure_31_1_for_all_four_cases() {
let want: [Vec<(i64, i64)>; 4] = [
vec![(150, 50)],
vec![(160, 60)],
[160, 170, 180, 190, 200, 210, 220]
.map(|t| (t, t - 100))
.to_vec(),
vec![(150, 50)],
];
for (f, want) in timeskew_cases().into_iter().zip(want) {
assert_eq!(timeskew_replay(50, f, &figure_31_1_events()), want, "{f:?}");
}
}
#[test]
fn timeskew_state_replays_figure_31_2_for_all_four_cases() {
let want: [Vec<(i64, i64)>; 4] = [
vec![],
vec![],
vec![(160, 60), (170, 70), (180, 80), (190, 90)],
vec![(150, 50)],
];
for (f, want) in timeskew_cases().into_iter().zip(want) {
assert_eq!(timeskew_replay(50, f, &figure_31_2_events()), want, "{f:?}");
}
}
#[test]
fn timeskew_state_a_true_conditioned_reference_restarts_the_wait_in_every_case() {
let mut events = figure_31_2_events();
events[1].1 = Ev::Ref;
let want: [Vec<(i64, i64)>; 4] = [
vec![],
vec![(180, 60)],
vec![(180, 60), (190, 70)],
vec![],
];
for (f, want) in timeskew_cases().into_iter().zip(want) {
assert_eq!(timeskew_replay(50, f, &events), want, "{f:?}");
}
}
#[test]
fn timeskew_state_sweep_is_closed_at_the_limit_in_both_modes() {
let mut violations = 0;
for limit in 0..=5 {
for skew in 0..=8 {
let events = [(100, Ev::Ref), (100 + skew, Ev::Data)];
let timer = timeskew_replay(limit, flags(false, false), &events);
let event = timeskew_replay(limit, flags(true, false), &events);
if skew > limit {
assert_eq!(
timer,
[(100 + limit, limit)],
"timer: limit {limit}, skew {skew}"
);
assert_eq!(
event,
[(100 + skew, skew)],
"event: limit {limit}, skew {skew}"
);
violations += 2;
} else {
assert_eq!(timer, [], "timer: limit {limit}, skew {skew}");
assert_eq!(event, [], "event: limit {limit}, skew {skew}");
}
}
}
assert!(violations > 0, "a sweep with no violation proves nothing");
}
#[test]
fn timeskew_state_reports_a_missing_data_event_only_when_timer_based() {
let events = [(100, Ev::Ref)];
assert_eq!(timeskew_replay(7, flags(false, false), &events), [(107, 7)]);
assert_eq!(timeskew_replay(7, flags(false, true), &events), [(107, 7)]);
assert_eq!(timeskew_replay(7, flags(true, false), &events), []);
assert_eq!(timeskew_replay(7, flags(true, true), &events), []);
}
#[test]
fn timeskew_state_event_based_and_remain_active_is_skew() {
let stimulus = [
(3, Ev::Ref),
(5, Ev::Data),
(12, Ev::Data),
(12, Ev::Ref),
(14, Ev::Data),
(20, Ev::Data),
(20, Ev::Ref),
(31, Ev::Data),
(31, Ev::Data),
(40, Ev::RefFalse),
(47, Ev::Data),
];
for limit in [0, 1, 2, 5] {
let got = timeskew_replay(limit, flags(true, true), &stimulus);
let mut skew = SkewState::new(limit);
let mut want = Vec::new();
let mut i = 0;
while i < stimulus.len() {
let now = stimulus[i].0;
let batch: Vec<Ev> = stimulus[i..]
.iter()
.take_while(|e| e.0 == now)
.map(|e| e.1)
.collect();
i += batch.len();
if batch.iter().any(|e| matches!(e, Ev::Ref)) {
skew.reference_event(now);
}
for _ in batch.iter().filter(|e| matches!(e, Ev::Data)) {
skew.data_event(now);
}
let n = skew.pending();
want.extend(skew.settle(n).into_iter().map(|a| (now, a)));
}
assert_eq!(got, want, "limit {limit}");
}
}
#[test]
fn timeskew_state_event_based_goes_dormant_only_after_a_violation_or_a_false_reference() {
let f = flags(true, false);
let events = [
(100, Ev::Ref),
(103, Ev::Data), (105, Ev::Data), (106, Ev::Data), (107, Ev::Data),
(110, Ev::Ref), (118, Ev::Data),
(120, Ev::RefFalse), (130, Ev::Data),
(140, Ev::Ref),
(146, Ev::Data),
];
assert_eq!(
timeskew_replay(5, f, &events),
[(106, 6), (118, 8), (146, 6)]
);
}
#[test]
fn timeskew_state_timer_based_goes_dormant_at_an_in_limit_data_event_until_the_next_reference()
{
let events = [
(100, Ev::Ref),
(102, Ev::Data),
(104, Ev::Data),
(110, Ev::Data),
(200, Ev::Ref),
];
assert_eq!(timeskew_replay(5, flags(false, false), &events), [(205, 5)]);
}
#[test]
fn timeskew_state_a_false_reference_arms_nothing_and_moves_nothing() {
for ra in [false, true] {
let f = flags(false, ra);
let mut s = TimeSkewState::new(10, f);
s.reference_suppressed(0);
assert_eq!(s.deadline(), None, "idle, remain_active {ra}");
s.reference_event(50);
assert_eq!(s.deadline(), Some(60));
s.reference_suppressed(55);
assert_eq!(
s.deadline(),
ra.then_some(60),
"armed at 50, remain_active {ra}: kept (not moved) iff the flag is set"
);
}
}
#[test]
fn timeskew_state_deadline_follows_the_active_timer_based_wait() {
let mut s = TimeSkewState::new(10, flags(false, false));
assert_eq!(s.deadline(), None, "no reference event yet");
s.reference_event(5);
assert_eq!(s.deadline(), Some(15));
s.reference_event(8);
assert_eq!(s.deadline(), Some(18), "the second reference restarts it");
assert_eq!(s.expire(17), None, "not yet");
assert_eq!(s.deadline(), Some(18));
assert_eq!(s.expire(18), Some(10));
assert_eq!(s.deadline(), None, "dormant after the violation");
assert_eq!(s.expire(18), None, "reported once");
let mut e = TimeSkewState::new(10, flags(true, true));
e.reference_event(5);
assert_eq!(e.deadline(), None, "event-based: no timer");
assert_eq!(e.expire(1000), None);
}
#[test]
fn timeskew_state_expiry_waits_for_the_pending_data_events() {
let mut s = TimeSkewState::new(5, flags(false, false));
s.reference_event(100);
s.data_event(105);
assert_eq!(s.expire(105), None, "a data event is still to be measured");
assert_eq!(s.settle(1), Vec::<i64>::new(), "skew 5 = limit: within it");
assert_eq!(s.expire(105), None, "dormant: nothing to report");
assert_eq!(s.deadline(), None);
}
#[test]
fn timeskew_state_a_late_data_event_with_no_expiry_still_violates_once() {
let mut s = TimeSkewState::new(5, flags(false, false));
s.reference_event(100);
s.data_event(120);
s.data_event(121);
assert_eq!(s.settle(2), [20], "reported once; dormant for the second");
assert_eq!(s.deadline(), None);
}
#[test]
fn timeskew_state_data_events_without_a_wait_are_ignored() {
let mut s = TimeSkewState::new(0, flags(true, true));
s.data_event(5);
assert_eq!(s.pending(), 1);
assert_eq!(s.settle(1), Vec::<i64>::new(), "no reference event yet");
assert_eq!(s.pending(), 0);
}
#[test]
fn timeskew_state_a_data_event_waits_for_a_later_reference_event_of_its_instant() {
for event_based in [false, true] {
let mut s = TimeSkewState::new(0, flags(event_based, true));
s.reference_event(10);
s.data_event(30);
s.reference_event(30);
assert_eq!(s.settle(1), Vec::<i64>::new(), "event_based {event_based}");
}
}
#[test]
fn machine_time_skew_routes_every_entry_point() {
let spec = CheckSpec::TimeSkew {
limit: 5,
flags: flags(true, false),
};
assert_eq!(spec.name(), "$timeskew");
assert_eq!(spec.limit_label(), None);
let mut m = spec.machine();
assert!(m.deliver(10, false, true, false).is_empty());
assert!(m.deliver(20, true, false, false).is_empty(), "pended");
assert_eq!(m.pending(), 1);
let v = m.settle(1);
assert_eq!(v.len(), 1);
assert_eq!((v[0].actual, v[0].limit), (10, 5));
assert!(m.deliver(30, true, false, false).is_empty());
assert!(m.settle(1).is_empty());
let mut t = CheckSpec::TimeSkew {
limit: 5,
flags: flags(false, false),
}
.machine();
assert_eq!(t.deadline(), None);
t.deliver(100, false, true, false);
assert_eq!(t.deadline(), Some(105));
let v = t.expire(105).expect("the timer expires");
assert_eq!((v.actual, v.limit), (5, 5));
assert_eq!(t.deadline(), None);
t.deliver(110, false, true, false);
t.reference_suppressed(110);
assert_eq!(
t.deadline(),
None,
"dormant by the false-condition reference"
);
for other in [
Machine::Skew(SkewState::new(1)),
Machine::Period(PeriodState::new(1)),
] {
let mut other = other;
assert_eq!(other.deadline(), None);
assert!(other.expire(1000).is_none());
other.reference_suppressed(0);
assert_eq!(other.pending(), 0);
}
}
#[test]
fn timeskew_spec_rejects_a_negative_limit_and_decodes_the_flags() {
let src = "`timescale 1ns/1ps\nmodule m(input d, input clk);\n reg n;\n specify\n \
$timeskew(posedge clk, d, 5, n);\n $timeskew(posedge clk, d, 6, n, 1);\n \
$timeskew(posedge clk, d, 7, n, 0, 1);\n $timeskew(posedge clk, d, 8, n, \
, 1);\n endspecify\nendmodule\n";
let model = discovered(src).unwrap_or_else(|e| panic!("discover failed: {e}"));
let specs: Vec<CheckSpec> = model
.timing_checks
.iter()
.map(|c| {
let CheckArgs::TimeSkew(a) = &c.args else {
panic!("not $timeskew: {:?}", c.args)
};
timeskew_spec(a, &c.check_uid).expect("valid")
})
.collect();
let want = [
(5, false, false),
(6, true, false),
(7, false, true),
(8, false, true),
]
.map(|(limit, event_based, remain_active)| CheckSpec::TimeSkew {
limit: limit * 1000,
flags: flags(event_based, remain_active),
});
assert_eq!(specs, want, "elided flags are 0, the LRM default");
}
fn timeskew_design(check: &str, events: &[(i64, &str)]) -> String {
skew_design(check, events, 300)
}
fn skew_design(check: &str, events: &[(i64, &str)], finish: i64) -> String {
let mut stim = String::new();
let mut at = 100;
for &(t, stmt) in events {
assert!(t >= at, "events must be in time order: {t} after {at}");
if t > at {
stim.push_str(&format!("#{} ", t - at));
}
stim.push_str(stmt);
stim.push('\n');
at = t;
}
format!(
"module chk(input cp, input cpn, input mode);\n reg ntf;\n specify\n {check}\n \
endspecify\n always @(ntf) $display(\"ntf=%b at %0t\", ntf, $time);\nendmodule\n\
module top;\n reg cp, cpn, mode;\n chk u(.cp(cp), .cpn(cpn), .mode(mode));\n \
initial begin\n cp = 0; cpn = 1; mode = 1;\n #100;\n {stim} #{} $finish;\n \
end\nendmodule\n",
finish - at
)
}
fn timeskew_hits(stdout: &str) -> Vec<(i64, i64)> {
violations(stdout)
.into_iter()
.map(|l| {
assert!(l.contains("$timeskew violation"), "unexpected line: {l}");
let time = l.split(':').next().unwrap().parse().expect("time");
let actual = l.rsplit("actual=").next().unwrap().parse().expect("actual");
(time, actual)
})
.collect()
}
fn cpn_fall(events: &mut Vec<(i64, &'static str)>, t: i64) {
events.push((t, "cpn = 0;"));
events.push((t + 1, "cpn = 1;"));
}
fn figure_31_1() -> Vec<(i64, &'static str)> {
let mut e = vec![(100, "cp = 1;"), (110, "cp = 0;")];
for t in [160, 170, 180] {
cpn_fall(&mut e, t);
}
e.push((183, "mode = 0;"));
e.push((185, "cp = 1;"));
for t in [190, 200, 210, 220] {
cpn_fall(&mut e, t);
}
e.sort_by_key(|&(t, _)| t);
e
}
fn figure_31_2() -> Vec<(i64, &'static str)> {
let mut e = vec![
(100, "cp = 1;"),
(110, "cp = 0;"),
(115, "mode = 0;"),
(120, "cp = 1;"),
];
for t in [160, 170, 180, 190] {
cpn_fall(&mut e, t);
}
e.sort_by_key(|&(t, _)| t);
e
}
const TIMESKEW_CASES: [(u8, u8); 4] = [(0, 0), (1, 0), (1, 1), (0, 1)];
fn figure_hits(
tag: &str,
events: &[(i64, &str)],
event_based: u8,
remain_active: u8,
) -> Vec<(i64, i64)> {
let check = format!(
"$timeskew(posedge cp &&& mode, negedge cpn, 50, ntf, {event_based}, {remain_active});"
);
let stdout = run_src(tag, ×kew_design(&check, events));
let hits = timeskew_hits(&stdout);
assert_eq!(
stdout.matches("ntf=").count(),
hits.len(),
"one notifier toggle per violation: {stdout}"
);
hits
}
#[test]
fn run_design_timeskew_figure_31_1_cases_1_to_4() {
let all_late = vec![
(160, 60),
(170, 70),
(180, 80),
(190, 90),
(200, 100),
(210, 110),
(220, 120),
];
let want: [(&str, Vec<(i64, i64)>); 4] = [
("case 1", vec![(150, 50)]),
("case 2", vec![(160, 60)]),
("case 3", all_late),
("case 4", vec![(150, 50)]),
];
for (i, ((eb, ra), (name, hits))) in TIMESKEW_CASES.into_iter().zip(want).enumerate() {
let got = figure_hits(&format!("ts-f1-{i}"), &figure_31_1(), eb, ra);
assert_eq!(got, hits, "{name} (event_based {eb}, remain_active {ra})");
}
}
#[test]
fn run_design_timeskew_figure_31_2_remain_active_decides_the_violation_at_b() {
let want: [Vec<(i64, i64)>; 4] = [
vec![],
vec![],
vec![(160, 60), (170, 70), (180, 80), (190, 90)],
vec![(150, 50)],
];
for (i, ((eb, ra), hits)) in TIMESKEW_CASES.into_iter().zip(want).enumerate() {
let got = figure_hits(&format!("ts-f2-{i}"), &figure_31_2(), eb, ra);
assert_eq!(
got,
hits,
"case {} (event_based {eb}, remain_active {ra})",
i + 1
);
}
}
#[test]
fn run_design_timeskew_accepts_an_empty_notifier_slot_and_elided_flags() {
let stdout = run_src(
"ts-nontf",
×kew_design(
"$timeskew(posedge cp &&& mode, negedge cpn, 50, , 1, 1);",
&figure_31_1(),
),
);
assert_eq!(timeskew_hits(&stdout).len(), 7, "{stdout}");
assert!(!stdout.contains("ntf="), "no notifier is toggled: {stdout}");
let stdout = run_src(
"ts-elided",
×kew_design(
"$timeskew(posedge cp &&& mode, negedge cpn, 50);",
&figure_31_1(),
),
);
assert_eq!(timeskew_hits(&stdout), [(150, 50)], "{stdout}");
let stdout = run_src(
"ts-one-flag",
×kew_design(
"$timeskew(posedge cp &&& mode, negedge cpn, 50, ntf, 1);",
&figure_31_1(),
),
);
assert_eq!(timeskew_hits(&stdout), [(160, 60)], "{stdout}");
}
#[test]
fn run_design_timeskew_flags_are_constant_expressions() {
let src = timeskew_design(
"$timeskew(posedge cp &&& mode, negedge cpn, 50, ntf, EB, RA);",
&figure_31_1(),
)
.replacen(
" reg ntf;\n",
" parameter EB = 1;\n localparam RA = 1 + 0;\n reg ntf;\n",
1,
);
assert_eq!(timeskew_hits(&run_src("ts-param", &src)).len(), 7);
let stdout = run_src(
"ts-mtm",
×kew_design(
"$timeskew(posedge cp &&& mode, negedge cpn, 50, ntf, 1, (1:1:1));",
&figure_31_1(),
),
);
assert_eq!(timeskew_hits(&stdout).len(), 7, "{stdout}");
}
#[test]
fn run_design_timeskew_timer_reports_a_missing_data_event_exactly() {
let stdout = run_src(
"ts-exact",
×kew_design(
"$timeskew(posedge cp, negedge cpn, 5, ntf);",
&[(100, "cp = 1;")],
),
);
assert_eq!(
stdout,
"ntf=0 at 105\n105: $timeskew violation: data top.u.cpn vs reference top.u.cp, \
limit=5, actual=5\n"
);
}
#[test]
fn run_design_timeskew_a_data_event_at_the_expiry_is_within_the_limit() {
for (at, want) in [(103, vec![]), (105, vec![]), (106, vec![(105, 5)])] {
let stdout = run_src(
&format!("ts-edge-{at}"),
×kew_design(
"$timeskew(posedge cp, negedge cpn, 5, ntf);",
&[(100, "cp = 1;"), (at, "cpn = 0;")],
),
);
assert_eq!(timeskew_hits(&stdout), want, "data at {at}: {stdout}");
}
}
#[test]
fn run_design_timeskew_goes_dormant_after_an_in_limit_data_event_until_the_next_reference() {
let stdout = run_src(
"ts-dormant",
×kew_design(
"$timeskew(posedge cp, negedge cpn, 5, ntf);",
&[
(100, "cp = 1;"),
(103, "cpn = 0;"),
(104, "cpn = 1;"),
(108, "cpn = 0;"),
(120, "cp = 0;"),
(130, "cp = 1;"),
],
),
);
assert_eq!(timeskew_hits(&stdout), [(135, 5)], "{stdout}");
}
#[test]
fn run_design_timeskew_a_new_reference_at_the_expiry_cancels_the_violation() {
let stdout = run_src(
"ts-cancel",
×kew_design(
"$timeskew(posedge cp, negedge cpn, 5, ntf);",
&[(100, "cp = 1;"), (102, "cp = 0;"), (105, "cp = 1;")],
),
);
assert_eq!(timeskew_hits(&stdout), [(110, 5)], "{stdout}");
}
#[test]
fn run_design_timeskew_simultaneous_transitions_do_not_violate_with_limit_zero() {
for (tag, stmt) in [
("ts-simul-rd", "begin cp = 1; cpn = 0; end"),
("ts-simul-dr", "begin cpn = 0; cp = 1; end"),
] {
let stdout = run_src(
tag,
×kew_design(
"$timeskew(posedge cp, negedge cpn, 0, ntf);",
&[(100, stmt)],
),
);
assert_eq!(stdout, "", "{tag}: {stdout}");
}
let stdout = run_src(
"ts-simul-none",
×kew_design(
"$timeskew(posedge cp, negedge cpn, 0, ntf);",
&[(100, "cp = 1;")],
),
);
assert_eq!(timeskew_hits(&stdout), [(100, 0)], "{stdout}");
}
fn timeskew_late_design(check: &str, depth: usize, stim: &str) -> String {
let (mut wires, mut cp, mut dn) = (String::new(), "cp0".to_string(), "dn0".to_string());
for i in 1..=depth {
wires.push_str(&format!(
" wire cp{i}, dn{i};\n assign cp{i} = {cp};\n assign dn{i} = {dn};\n"
));
(cp, dn) = (format!("cp{i}"), format!("dn{i}"));
}
format!(
"module chk(input cp, input cpn);\n reg ntf;\n specify\n {check}\n endspecify\n \
always @(ntf) $display(\"ntf=%b at %0t\", ntf, $time);\nendmodule\nmodule top;\n \
reg cp0, dn0;\n{wires} chk u(.cp({cp}), .cpn({dn}));\n initial begin\n cp0 = 0; \
dn0 = 1;\n #100;\n {stim}\n #200 $finish;\n end\nendmodule\n"
)
}
#[test]
fn run_design_timeskew_a_data_event_a_delta_late_at_the_expiry_still_cancels() {
for depth in 1..=3 {
let stdout = run_src(
&format!("ts-late-data-{depth}"),
×kew_late_design(
"$timeskew(posedge cp, negedge cpn, 5, ntf);",
depth,
"cp0 = 1;\n #5 dn0 = 0;",
),
);
assert_eq!(stdout, "", "data {depth} delta(s) late: {stdout}");
let stdout = run_src(
&format!("ts-late-data-after-{depth}"),
×kew_late_design(
"$timeskew(posedge cp, negedge cpn, 5, ntf);",
depth,
"cp0 = 1;\n #6 dn0 = 0;",
),
);
assert_eq!(timeskew_hits(&stdout), [(105, 5)], "{stdout}");
}
}
#[test]
fn run_design_timeskew_a_reference_a_delta_late_at_the_expiry_still_cancels() {
for depth in 1..=3 {
let stdout = run_src(
&format!("ts-late-ref-{depth}"),
×kew_late_design(
"$timeskew(posedge cp, negedge cpn, 5, ntf);",
depth,
"cp0 = 1;\n #2 cp0 = 0;\n #3 cp0 = 1;",
),
);
assert_eq!(timeskew_hits(&stdout), [(110, 5)], "{stdout}");
}
}
#[test]
fn run_design_timeskew_a_false_conditioned_reference_never_arms_the_check() {
for ra in [0, 1] {
let stdout = run_src(
&format!("ts-false-ref-{ra}"),
×kew_design(
&format!("$timeskew(posedge cp &&& mode, negedge cpn, 5, ntf, 0, {ra});"),
&[(100, "mode = 0;"), (101, "cp = 1;")],
),
);
assert_eq!(stdout, "", "remain_active {ra}: {stdout}");
}
}
#[test]
fn run_design_timeskew_a_simulation_that_ends_inside_the_limit_reports_nothing() {
let src = "module chk(input cp, input cpn);\n reg ntf;\n specify\n \
$timeskew(posedge cp, negedge cpn, 5, ntf);\n endspecify\nendmodule\nmodule \
top;\n reg cp, cpn;\n chk u(.cp(cp), .cpn(cpn));\n initial begin\n cp = 0; \
cpn = 1;\n #100 cp = 1;\n #3 $finish;\n end\nendmodule\n";
assert_eq!(run_src("ts-early-finish", src), "");
}
#[test]
fn run_design_timeskew_rejects_a_negative_limit_by_name() {
let e = run_src_err(
"ts-neg",
×kew_design("$timeskew(posedge cp, negedge cpn, -1, ntf);", &[]),
);
assert!(
e.contains("$timeskew") && e.contains("non-negative") && e.contains("31.4.2"),
"unexpected error: {e}"
);
}
fn fullskew_replay(
l1: i64,
l2: i64,
f: SkewFlags,
events: &[(i64, Ev)],
) -> Vec<(i64, i64, i64)> {
skew_replay(TimeSkewState::fullskew(l1, l2, f), events)
}
fn v(time: i64, actual: i64, limit: i64) -> (i64, i64, i64) {
(time, actual, limit)
}
fn figure_31_3_events() -> Vec<(i64, Ev)> {
let mut e = vec![(100, Ev::Ref)];
e.extend([170, 260].map(|t| (t, Ev::Data)));
e.push((340, Ev::RefFalse));
e.extend([350, 430, 510, 590].map(|t| (t, Ev::Data)));
e.push((620, Ev::Ref));
e
}
#[test]
fn fullskew_state_replays_figure_31_3_case_1() {
let want = [
v(150, 50, 50),
v(240, 70, 70),
v(330, 70, 70),
v(420, 70, 70),
v(500, 70, 70),
v(580, 70, 70),
];
for ra in [false, true] {
assert_eq!(
fullskew_replay(50, 70, flags(false, ra), &figure_31_3_events()),
want,
"remain_active {ra}"
);
}
}
#[test]
fn fullskew_state_replays_figure_31_3_case_2() {
for ra in [false, true] {
assert_eq!(
fullskew_replay(50, 70, flags(true, ra), &figure_31_3_events()),
[v(170, 70, 50)],
"remain_active {ra}"
);
}
}
#[test]
fn fullskew_state_a_false_reference_inside_a_data_window_needs_remain_active() {
let events = [(260, Ev::Data), (300, Ev::RefFalse)];
assert_eq!(fullskew_replay(50, 70, flags(false, false), &events), []);
assert_eq!(
fullskew_replay(50, 70, flags(false, true), &events),
[v(330, 70, 70)]
);
}
#[test]
fn fullskew_state_with_an_unreachable_second_limit_agrees_with_timeskew() {
const FAR: i64 = 1_000_000;
let waveforms = [figure_31_1_events(), figure_31_2_events()];
let mut compared = 0;
for events in &waveforms {
for (case, f) in timeskew_cases().into_iter().enumerate() {
let skew = timeskew_replay(50, f, events);
let full: Vec<(i64, i64)> = fullskew_replay(50, FAR, f, events)
.into_iter()
.filter(|&(_, _, limit)| limit == 50)
.map(|(t, actual, _)| (t, actual))
.collect();
let want: Vec<(i64, i64)> = if case == 2 {
skew.into_iter().take(1).collect()
} else {
skew
};
assert_eq!(full, want, "case {} {f:?}", case + 1);
compared += want.len();
}
}
assert!(
compared >= 6,
"a cross-check that compares nothing proves nothing"
);
}
#[test]
fn fullskew_state_sweep_uses_the_limit_of_the_first_event() {
let (l1, l2) = (3, 7);
let mut violations = 0;
for skew in 0..=10 {
for (first, second, limit, other) in
[(Ev::Ref, Ev::Data, l1, l2), (Ev::Data, Ev::Ref, l2, l1)]
{
let events = [(100, first), (100 + skew, second)];
let timer = fullskew_replay(l1, l2, flags(false, false), &events);
let event = fullskew_replay(l1, l2, flags(true, false), &events);
if skew > limit {
assert_eq!(
timer,
[
v(100 + limit, limit, limit),
v(100 + skew + other, other, other)
],
"timer: {first:?} first, skew {skew}"
);
assert_eq!(
event,
[v(100 + skew, skew, limit)],
"event: {first:?} first, skew {skew}"
);
violations += 2;
} else {
assert_eq!(timer, [], "timer: {first:?} first, skew {skew}");
assert_eq!(event, [], "event: {first:?} first, skew {skew}");
}
}
}
assert!(violations > 0, "a sweep with no violation proves nothing");
}
#[test]
fn fullskew_state_simultaneous_events_never_violate_and_leave_the_check_dormant() {
for eb in [false, true] {
let mut s = TimeSkewState::fullskew(0, 0, flags(eb, false));
s.reference_event(100);
s.data_event(100);
let n = s.pending();
assert_eq!(s.settle_limited(n), [], "event_based {eb}");
assert_eq!(s.deadline(), None, "event_based {eb}: dormant");
assert_eq!(s.expire_limited(100), None);
}
}
#[test]
fn fullskew_state_a_lone_event_with_limit_zero_violates_at_its_instant() {
assert_eq!(
fullskew_replay(0, 9, flags(false, false), &[(100, Ev::Ref)]),
[v(100, 0, 0)]
);
assert_eq!(
fullskew_replay(9, 0, flags(false, false), &[(100, Ev::Data)]),
[v(100, 0, 0)]
);
}
#[test]
fn fullskew_state_a_new_timestamp_at_the_expiry_cancels_the_violation() {
let t = flags(false, false);
assert_eq!(
fullskew_replay(5, 9, t, &[(100, Ev::Ref), (105, Ev::Ref)]),
[v(110, 5, 5)]
);
assert_eq!(
fullskew_replay(5, 7, t, &[(100, Ev::Data), (107, Ev::Data)]),
[v(114, 7, 7)]
);
assert_eq!(
fullskew_replay(5, 9, t, &[(100, Ev::Ref), (106, Ev::Ref)]),
[v(105, 5, 5), v(111, 5, 5)]
);
}
#[test]
fn fullskew_state_event_based_chains_windows_through_late_timechecks() {
let events = [
(100, Ev::Ref),
(110, Ev::Data), (125, Ev::Ref), (128, Ev::Data), (140, Ev::Data), (160, Ev::Ref), ];
assert_eq!(
fullskew_replay(5, 7, flags(true, false), &events),
[v(110, 10, 5), v(125, 15, 7), v(160, 20, 7)]
);
}
#[test]
fn fullskew_state_a_false_edge_of_either_event_follows_remain_active() {
for (open, edge, l1, l2) in [
(Ev::Ref, Ev::DataFalse, 5, 9),
(Ev::Ref, Ev::RefFalse, 5, 9),
(Ev::Data, Ev::RefFalse, 9, 5),
(Ev::Data, Ev::DataFalse, 9, 5),
] {
let events = [(100, open), (102, edge)];
assert_eq!(
fullskew_replay(l1, l2, flags(false, false), &events),
[],
"{open:?} then {edge:?}: dormant"
);
assert_eq!(
fullskew_replay(l1, l2, flags(false, true), &events),
[v(105, 5, 5)],
"{open:?} then {edge:?}: ignored"
);
}
for ra in [false, true] {
for edge in [Ev::RefFalse, Ev::DataFalse] {
assert_eq!(
fullskew_replay(5, 5, flags(false, ra), &[(100, edge)]),
[],
"an idle check is not armed by {edge:?}"
);
}
}
}
#[test]
fn fullskew_state_a_false_edge_is_applied_after_the_events_of_its_instant() {
let a = [(100, Ev::Ref), (100, Ev::DataFalse)];
assert_eq!(fullskew_replay(5, 5, flags(false, false), &a), []);
assert_eq!(
fullskew_replay(5, 5, flags(false, true), &a),
[v(105, 5, 5)]
);
}
#[test]
fn fullskew_state_deadline_follows_the_active_windows_limit() {
let mut s = TimeSkewState::fullskew(5, 9, flags(false, false));
assert_eq!(s.deadline(), None);
s.reference_event(100);
let n = s.pending();
s.settle_limited(n);
assert_eq!(s.deadline(), Some(105));
s.data_event(103); let n = s.pending();
s.settle_limited(n);
assert_eq!(s.deadline(), None);
s.data_event(200);
let n = s.pending();
s.settle_limited(n);
assert_eq!(s.deadline(), Some(209));
s.reference_event(209);
assert_eq!(s.expire_limited(209), None);
let n = s.pending();
assert_eq!(s.settle_limited(n), [], "209 is within the 9 of 200");
let mut e = TimeSkewState::fullskew(5, 9, flags(true, true));
e.reference_event(100);
let n = e.pending();
e.settle_limited(n);
assert_eq!(e.deadline(), None, "event-based has no timer");
}
#[test]
fn fullskew_state_both_kinds_in_an_instant_check_the_open_window_then_go_dormant() {
let events = [
(100, Ev::Ref),
(120, Ev::Ref),
(120, Ev::Data),
(130, Ev::Data),
];
assert_eq!(
fullskew_replay(5, 5, flags(true, false), &events),
[v(120, 20, 5)]
);
assert_eq!(fullskew_replay(50, 50, flags(true, false), &events), []);
}
#[test]
fn fullskew_state_does_not_depend_on_the_arrival_order_within_an_instant() {
let kinds = [Ev::Ref, Ev::Data, Ev::RefFalse, Ev::DataFalse];
let mut checked = 0;
for eb in [false, true] {
for ra in [false, true] {
for open in [Ev::Ref, Ev::Data] {
for mask in 1u32..16 {
let set: Vec<Ev> = (0..4)
.filter(|i| mask & (1 << i) != 0)
.map(|i| kinds[i])
.collect();
let mut first: Option<Vec<(i64, i64)>> = None;
for perm in permutations(set.len()) {
let mut s = TimeSkewState::fullskew(4, 6, flags(eb, ra));
match open {
Ev::Ref => s.reference_event(100),
_ => s.data_event(100),
}
let n = s.pending();
s.settle_limited(n);
for &i in &perm {
match set[i] {
Ev::Ref => s.reference_event(110),
Ev::Data => s.data_event(110),
Ev::RefFalse => s.reference_suppressed(110),
Ev::DataFalse => s.data_suppressed(110),
}
}
let n = s.pending();
let mut got = s.settle_limited(n);
got.extend(s.expire_limited(150));
match &first {
None => first = Some(got),
Some(f) => assert_eq!(
&got, f,
"eb {eb} ra {ra} open {open:?} set {set:?} perm {perm:?}"
),
}
checked += 1;
}
}
}
}
}
assert_eq!(checked, 512);
}
fn permutations(n: usize) -> Vec<Vec<usize>> {
fn go(rest: &mut Vec<usize>, cur: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
if rest.is_empty() {
out.push(cur.clone());
return;
}
for i in 0..rest.len() {
let x = rest.remove(i);
cur.push(x);
go(rest, cur, out);
cur.pop();
rest.insert(i, x);
}
}
let mut out = Vec::new();
go(&mut (0..n).collect(), &mut Vec::new(), &mut out);
out
}
#[test]
fn fullskew_state_is_symmetric_under_swapping_the_two_events_and_limits() {
fn mirror(e: Ev) -> Ev {
match e {
Ev::Ref => Ev::Data,
Ev::Data => Ev::Ref,
Ev::RefFalse => Ev::DataFalse,
Ev::DataFalse => Ev::RefFalse,
}
}
let mut seed = 0x2545_f491_u64;
let mut next = move |bound: u64| {
seed = seed
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
(seed >> 33) % bound
};
let (mut runs, mut hits) = (0, 0);
for _ in 0..400 {
let mut t = 100;
let mut events = Vec::new();
for _ in 0..(3 + next(10)) {
t += next(6) as i64;
events.push((
t,
[
Ev::Ref,
Ev::Data,
Ev::Ref,
Ev::Data,
Ev::RefFalse,
Ev::DataFalse,
][next(6) as usize],
));
}
let mirrored: Vec<(i64, Ev)> = events.iter().map(|&(t, e)| (t, mirror(e))).collect();
let (l1, l2) = (next(5) as i64, next(5) as i64);
for f in timeskew_cases() {
let a = fullskew_replay(l1, l2, f, &events);
let b = fullskew_replay(l2, l1, f, &mirrored);
assert_eq!(a, b, "limits ({l1}, {l2}) {f:?} events {events:?}");
runs += 1;
hits += a.len();
}
}
assert!(runs == 1600 && hits > 200, "{runs} runs, {hits} violations");
}
#[test]
fn fullskew_spec_rejects_a_negative_limit_and_decodes_both_limits_and_the_flags() {
let src = "`timescale 1ns/1ps\nmodule m(input d, input clk);\n reg n;\n specify\n \
$fullskew(posedge clk, d, 5, 6, n);\n $fullskew(posedge clk, d, 7, 8, n, 1);\n \
$fullskew(posedge clk, d, 9, 10, n, 0, 1);\n $fullskew(posedge clk, d, 11, \
12);\n endspecify\nendmodule\n";
let model = discovered(src).unwrap_or_else(|e| panic!("discover failed: {e}"));
let specs: Vec<CheckSpec> = model
.timing_checks
.iter()
.map(|c| {
let CheckArgs::FullSkew(a) = &c.args else {
panic!("not $fullskew: {:?}", c.args)
};
fullskew_spec(a, &c.check_uid).expect("valid")
})
.collect();
let want = [
(5, 6, false, false),
(7, 8, true, false),
(9, 10, false, true),
(11, 12, false, false),
]
.map(|(l1, l2, event_based, remain_active)| CheckSpec::FullSkew {
limit: l1 * 1000,
data_first_limit: l2 * 1000,
flags: flags(event_based, remain_active),
});
assert_eq!(
specs, want,
"limit 1 = reference first, limit 2 = data first"
);
}
fn fullskew_hits(stdout: &str) -> Vec<(i64, i64, i64)> {
violations(stdout)
.into_iter()
.map(|l| {
assert!(l.contains("$fullskew violation"), "unexpected line: {l}");
let time = l.split(':').next().unwrap().parse().expect("time");
let actual = l.rsplit("actual=").next().unwrap().parse().expect("actual");
let limit = l
.split("limit=")
.nth(1)
.and_then(|r| r.split(',').next())
.unwrap()
.parse()
.expect("limit");
(time, actual, limit)
})
.collect()
}
fn fullskew_run(
tag: &str,
check: &str,
events: &[(i64, &str)],
finish: i64,
) -> Vec<(i64, i64, i64)> {
let stdout = run_src(tag, &skew_design(check, events, finish));
let hits = fullskew_hits(&stdout);
assert_eq!(
stdout.matches("ntf=").count(),
hits.len(),
"one notifier toggle per violation: {stdout}"
);
hits
}
fn figure_31_3() -> Vec<(i64, &'static str)> {
let mut e = vec![(100, "cp = 1;"), (110, "cp = 0;")];
for t in [170, 260] {
cpn_fall(&mut e, t);
}
e.extend([(335, "mode = 0;"), (340, "cp = 1;"), (345, "cp = 0;")]);
for t in [350, 430, 510, 590] {
cpn_fall(&mut e, t);
}
e.extend([(595, "mode = 1;"), (620, "cp = 1;")]);
e.sort_by_key(|&(t, _)| t);
e
}
fn figure_31_3_hits(tag: &str, event_based: u8, remain_active: u8) -> Vec<(i64, i64, i64)> {
let check = format!(
"$fullskew(posedge cp &&& mode, negedge cpn, 50, 70, ntf, {event_based}, \
{remain_active});"
);
fullskew_run(tag, &check, &figure_31_3(), 700)
}
#[test]
fn run_design_fullskew_figure_31_3_case_1() {
for ra in [0, 1] {
assert_eq!(
figure_31_3_hits(&format!("fs-f3-c1-{ra}"), 0, ra),
[
v(150, 50, 50),
v(240, 70, 70),
v(330, 70, 70),
v(420, 70, 70),
v(500, 70, 70),
v(580, 70, 70),
],
"remain_active {ra}"
);
}
}
#[test]
fn run_design_fullskew_figure_31_3_case_2() {
for ra in [0, 1] {
assert_eq!(
figure_31_3_hits(&format!("fs-f3-c2-{ra}"), 1, ra),
[v(170, 70, 50)],
"remain_active {ra}"
);
}
}
#[test]
fn run_design_fullskew_with_an_unreachable_second_limit_agrees_with_timeskew() {
let mut compared = 0;
for (fig, events) in [("1", figure_31_1()), ("2", figure_31_2())] {
for (i, (eb, ra)) in TIMESKEW_CASES.into_iter().enumerate() {
let skew = figure_hits(&format!("fs-x-ts-{fig}-{i}"), &events, eb, ra);
let check = format!(
"$fullskew(posedge cp &&& mode, negedge cpn, 50, 100000, ntf, {eb}, {ra});"
);
let full: Vec<(i64, i64)> =
fullskew_run(&format!("fs-x-fs-{fig}-{i}"), &check, &events, 300)
.into_iter()
.filter(|&(_, _, limit)| limit == 50)
.map(|(t, a, _)| (t, a))
.collect();
let want: Vec<(i64, i64)> = if i == 2 {
skew.into_iter().take(1).collect()
} else {
skew
};
assert_eq!(full, want, "figure 31-{fig} case {}", i + 1);
compared += want.len();
}
}
assert!(compared >= 6, "{compared}");
}
#[test]
fn run_design_fullskew_timer_reports_each_order_with_its_own_limit_exactly() {
let reference_first = run_src(
"fs-exact-ref",
&skew_design(
"$fullskew(posedge cp, negedge cpn, 5, 9, ntf);",
&[(100, "cp = 1;")],
300,
),
);
assert_eq!(
reference_first,
"ntf=0 at 105\n105: $fullskew violation: data top.u.cpn vs reference top.u.cp, \
limit=5, actual=5\n"
);
let data_first = run_src(
"fs-exact-data",
&skew_design(
"$fullskew(posedge cp, negedge cpn, 5, 9, ntf);",
&[(100, "cpn = 0;")],
300,
),
);
assert_eq!(
data_first,
"ntf=0 at 109\n109: $fullskew violation: data top.u.cpn vs reference top.u.cp, \
limit=9, actual=9\n"
);
}
#[test]
fn run_design_fullskew_event_based_boundaries_in_both_orders() {
let check = "$fullskew(posedge cp, negedge cpn, 5, 9, ntf, 1);";
for (tag, first, second, at, want) in [
("a", "cp = 1;", "cpn = 0;", 105, vec![]),
("b", "cp = 1;", "cpn = 0;", 106, vec![v(106, 6, 5)]),
("c", "cpn = 0;", "cp = 1;", 109, vec![]),
("d", "cpn = 0;", "cp = 1;", 110, vec![v(110, 10, 9)]),
] {
let got = fullskew_run(
&format!("fs-eb-{tag}"),
check,
&[(100, first), (at, second)],
300,
);
assert_eq!(got, want, "{tag}");
}
}
#[test]
fn run_design_fullskew_simultaneous_transitions_do_not_violate_with_limit_zero() {
for eb in [0, 1] {
for (tag, stmt) in [
("rd", "begin cp = 1; cpn = 0; end"),
("dr", "begin cpn = 0; cp = 1; end"),
] {
let got = fullskew_run(
&format!("fs-simul-{tag}-{eb}"),
&format!("$fullskew(posedge cp, negedge cpn, 0, 0, ntf, {eb});"),
&[(100, stmt)],
300,
);
assert_eq!(got, [], "{tag} event_based {eb}");
}
}
for (tag, stmt) in [("ref", "cp = 1;"), ("data", "cpn = 0;")] {
let got = fullskew_run(
&format!("fs-lone0-{tag}"),
"$fullskew(posedge cp, negedge cpn, 0, 0, ntf);",
&[(100, stmt)],
300,
);
assert_eq!(got, [v(100, 0, 0)], "{tag}");
}
}
#[test]
fn run_design_fullskew_a_false_data_condition_follows_remain_active() {
for (ra, want) in [(0, vec![]), (1, vec![v(105, 5, 5)])] {
let got = fullskew_run(
&format!("fs-false-data-{ra}"),
&format!("$fullskew(posedge cp, negedge cpn &&& mode, 5, 5, ntf, 0, {ra});"),
&[(100, "cp = 1;"), (101, "mode = 0;"), (102, "cpn = 0;")],
300,
);
assert_eq!(got, want, "remain_active {ra}");
}
}
#[test]
fn run_design_fullskew_accepts_an_empty_notifier_slot_and_elided_flags() {
let stdout = run_src(
"fs-nontf",
&skew_design(
"$fullskew(posedge cp &&& mode, negedge cpn, 50, 70, , 1, 1);",
&figure_31_3(),
700,
),
);
assert_eq!(fullskew_hits(&stdout), [v(170, 70, 50)], "{stdout}");
assert!(!stdout.contains("ntf="), "{stdout}");
let stdout = run_src(
"fs-elided",
&skew_design(
"$fullskew(posedge cp &&& mode, negedge cpn, 50, 70);",
&figure_31_3(),
700,
),
);
assert_eq!(fullskew_hits(&stdout).len(), 6, "{stdout}");
}
#[test]
fn run_design_fullskew_a_timecheck_a_delta_late_at_the_expiry_still_cancels() {
let check = "$fullskew(posedge cp, negedge cpn, 5, 5, ntf);";
for depth in 1..=3 {
for (tag, first, second) in [
("ref", "cp0 = 1;", "dn0 = 0;"),
("data", "dn0 = 0;", "cp0 = 1;"),
] {
let on_time = fullskew_run_late(
&format!("fs-late-{tag}-{depth}"),
check,
depth,
&format!("{first}\n #5 {second}"),
);
assert_eq!(on_time, [], "{tag}, {depth} delta(s) late");
let late = fullskew_run_late(
&format!("fs-late-after-{tag}-{depth}"),
check,
depth,
&format!("{first}\n #6 {second}"),
);
assert_eq!(
late,
[v(105, 5, 5), v(111, 5, 5)],
"{tag}, {depth} delta(s)"
);
}
}
}
#[test]
fn run_design_fullskew_a_timestamp_a_delta_late_at_the_expiry_still_replaces() {
let check = "$fullskew(posedge cp, negedge cpn, 5, 7, ntf);";
for depth in 1..=3 {
let got = fullskew_run_late(
&format!("fs-late-ts-ref-{depth}"),
check,
depth,
"cp0 = 1;\n #2 cp0 = 0;\n #3 cp0 = 1;",
);
assert_eq!(got, [v(110, 5, 5)], "reference, {depth} delta(s)");
let got = fullskew_run_late(
&format!("fs-late-ts-data-{depth}"),
check,
depth,
"dn0 = 0;\n #2 dn0 = 1;\n #5 dn0 = 0;",
);
assert_eq!(got, [v(114, 7, 7)], "data, {depth} delta(s)");
}
}
fn fullskew_run_late(tag: &str, check: &str, depth: usize, stim: &str) -> Vec<(i64, i64, i64)> {
fullskew_hits(&run_src(tag, ×kew_late_design(check, depth, stim)))
}
#[test]
fn run_design_fullskew_rejects_a_negative_limit_by_name() {
for check in [
"$fullskew(posedge cp, negedge cpn, -1, 5, ntf);",
"$fullskew(posedge cp, negedge cpn, 5, -1, ntf);",
] {
let e = run_src_err("fs-neg", &skew_design(check, &[], 300));
assert!(
e.contains("$fullskew") && e.contains("non-negative") && e.contains("31.4.3"),
"unexpected error for {check}: {e}"
);
}
}
fn per_bit() -> TimingOptions {
TimingOptions {
per_bit_vectors: true,
..TimingOptions::default()
}
}
fn run_src_with(tag: &str, src: &str, options: TimingOptions) -> String {
let file = std::env::temp_dir().join(format!("vj-sdf-t22-{tag}-{}.sv", std::process::id()));
std::fs::write(&file, src).expect("write temp fixture");
let mut out = Vec::new();
let code = crate::run::run_design_with_options(&file, &[], options, &mut out)
.unwrap_or_else(|e| panic!("run_design_with_options failed: {e}"));
let _ = std::fs::remove_file(&file);
assert_eq!(code, 0);
String::from_utf8(out).expect("stdout is valid UTF-8")
}
fn dff_design(check: &str) -> String {
format!(
"module DFF(input CLK, input [7:0] DAT);\n reg ntf;\n specify\n {check}\n \
endspecify\n always @(ntf) $display(\"ntf=%b at %0t\", ntf, $time);\nendmodule\n\
module top;\n reg CLK;\n reg [7:0] DAT;\n DFF u(.CLK(CLK), .DAT(DAT));\n \
initial begin\n CLK = 0; DAT = 8'b00101110;\n #100 DAT = 8'b01010011;\n \
#5 CLK = 1;\n #100 $finish;\n end\nendmodule\n"
)
}
fn data_labels(stdout: &str) -> Vec<String> {
let mut labels: Vec<String> = violations(stdout)
.into_iter()
.map(|l| {
l.split("data ")
.nth(1)
.and_then(|r| r.split(' ').next())
.expect("a data label")
.to_string()
})
.collect();
labels.sort();
labels
}
#[test]
fn vector_data_is_one_check_by_default() {
let out = run_src(
"vec-default",
&dff_design("$setup(DAT, posedge CLK, 10, ntf);"),
);
assert_eq!(violations(&out).len(), 1, "{out}");
assert!(
out.contains("data top.u.DAT vs reference top.u.CLK"),
"{out}"
);
assert!(
out.contains("limit=10, actual=5"),
"the vector's one event: {out}"
);
assert_eq!(out.matches("ntf=").count(), 1, "{out}");
}
#[test]
fn per_bit_option_reports_one_violation_per_changed_bit() {
let out = run_src_with(
"vec-per-bit",
&dff_design("$setup(DAT, posedge CLK, 10, ntf);"),
per_bit(),
);
assert_eq!(
data_labels(&out),
[
"top.u.DAT[0]",
"top.u.DAT[2]",
"top.u.DAT[3]",
"top.u.DAT[4]",
"top.u.DAT[5]",
"top.u.DAT[6]"
],
"{out}"
);
assert!(
violations(&out)
.iter()
.all(|l| l.starts_with("105: $setup violation") && l.contains("limit=10, actual=5")),
"{out}"
);
assert!(
out.contains("ntf=1 at 105"),
"six toggles of one notifier: {out}"
);
}
#[test]
fn per_bit_option_changes_nothing_for_scalar_terminals() {
let src = check_design(
"$setup(d, posedge clk, 5, ntf);",
"#2 d = 1;\n #2 clk = 1;",
);
let on = run_src_with("vec-scalar-on", &src, per_bit());
let off = run_src("vec-scalar-off", &src);
assert_eq!(on, off);
assert_eq!(violations(&on).len(), 1, "{on}");
assert_eq!(run_src_with("vec-off", &src, TimingOptions::default()), off);
}
#[test]
fn per_bit_option_expands_a_part_select_to_its_own_bits() {
let out = run_src_with(
"vec-part",
&dff_design("$setup(DAT[3:1], posedge CLK, 10, ntf);"),
per_bit(),
);
assert_eq!(data_labels(&out), ["top.u.DAT[2]", "top.u.DAT[3]"], "{out}");
let out = run_src_with(
"vec-select",
&dff_design("$setup(DAT[4], posedge CLK, 10, ntf);"),
per_bit(),
);
assert_eq!(data_labels(&out), ["top.u.DAT[4]"], "{out}");
let out = run_src(
"vec-part-default",
&dff_design("$setup(DAT[3:1], posedge CLK, 10, ntf);"),
);
assert_eq!(data_labels(&out), ["top.u.DAT[3:1]"], "{out}");
}
#[test]
fn per_bit_option_pairs_every_reference_bit_with_every_data_bit() {
let src = "module chk(input [1:0] r, input [2:0] d);\n specify\n $hold(posedge r, d, \
10);\n endspecify\nendmodule\nmodule top;\n reg [1:0] r;\n reg [2:0] d;\n \
chk u(.r(r), .d(d));\n initial begin\n r = 2'b00; d = 3'b000;\n #100 \
r = 2'b10;\n #1 d = 3'b101;\n #100 $finish;\n end\nendmodule\n";
let out = run_src_with("vec-mn", src, per_bit());
let lines = violations(&out);
assert_eq!(lines.len(), 2, "{out}");
assert!(
lines.iter().all(|l| l.contains("reference top.u.r[1]")),
"only the bit that rose: {out}"
);
assert_eq!(data_labels(&out), ["top.u.d[0]", "top.u.d[2]"], "{out}");
let out = run_src("vec-mn-default", src);
assert_eq!(violations(&out).len(), 0, "{out}");
}
fn vec_watch(width: u32) -> WatchPlan {
WatchPlan {
loc: loc_of(InstanceId(0), crate::primitives::bank_slot(0, width)),
lo: 0,
width,
control: EdgeControl::AnyChange,
conditions: Vec::new(),
}
}
fn vec_plan(spec: CheckSpec, m: u32, n: u32, trailing: bool) -> CheckPlan {
CheckPlan {
entry: 0,
spec,
reference: vec_watch(m),
data: vec_watch(n),
trailing: trailing.then(|| vec_watch(m)),
notifier: Some((loc_of(InstanceId(0), crate::primitives::bank_slot(1, 1)), 0)),
reference_label: format!("r[{}:0]", m - 1),
data_label: format!("d[{}:0]", n - 1),
instance: InstanceId(0),
reference_signal: SymbolId::synthetic("r"),
data_signal: SymbolId::synthetic("d"),
timestamp: None,
timecheck: None,
}
}
#[test]
fn per_bit_expansion_counts_follow_the_lrm() {
let window = |kind| CheckSpec::Window { kind, limit: 1 };
let two = [
window(StabilityKind::Setup),
window(StabilityKind::Hold),
window(StabilityKind::Recovery),
window(StabilityKind::Removal),
CheckSpec::SetupHold { setup: 1, hold: 1 },
CheckSpec::RecRem {
recovery: 1,
removal: 1,
},
CheckSpec::Skew { limit: 1 },
CheckSpec::TimeSkew {
limit: 1,
flags: SkewFlags {
event_based: false,
remain_active: false,
},
},
CheckSpec::FullSkew {
limit: 1,
data_first_limit: 1,
flags: SkewFlags {
event_based: false,
remain_active: false,
},
},
];
for spec in two {
let expanded = expand_per_bit(vec_plan(spec, 3, 4, false), "r", "d");
assert_eq!(expanded.len(), 12, "{}: M*N", spec.name());
let pairs: HashSet<(u32, u32)> = expanded
.iter()
.map(|c| (c.reference.lo, c.data.lo))
.collect();
assert_eq!(pairs.len(), 12, "{}: every pair once", spec.name());
assert!(
expanded.iter().all(|c| {
c.reference.width == 1 && c.data.width == 1 && c.notifier.is_some()
})
);
}
let nc = vec_plan(CheckSpec::NoChange { start: 0, end: 0 }, 3, 4, true);
let expanded = expand_per_bit(nc, "r", "d");
assert_eq!(expanded.len(), 12);
assert!(
expanded
.iter()
.all(|c| c.trailing.as_ref().map(|t| t.lo) == Some(c.reference.lo))
);
for spec in [
CheckSpec::Width {
limit: 1,
threshold: 0,
},
CheckSpec::Period { limit: 1 },
] {
let expanded = expand_per_bit(vec_plan(spec, 5, 5, false), "r", "r");
assert_eq!(expanded.len(), 5, "{}: N", spec.name());
assert!(
expanded.iter().all(|c| c.reference.lo == c.data.lo),
"{}",
spec.name()
);
}
}
#[test]
fn per_bit_expansion_labels_and_scalars() {
let one = expand_per_bit(
vec_plan(CheckSpec::Skew { limit: 1 }, 1, 1, false),
"r",
"d",
);
assert_eq!(one.len(), 1);
assert_eq!(
(one[0].reference_label.as_str(), one[0].data_label.as_str()),
("r[0:0]", "d[0:0]"),
"a width-1 terminal keeps the label it was planned with"
);
let mut p = vec_plan(CheckSpec::Skew { limit: 1 }, 1, 3, false);
p.data.lo = 4; p.data_label = "d[6:4]".into();
let labels: Vec<_> = expand_per_bit(p, "r", "d")
.into_iter()
.map(|c| c.data_label)
.collect();
assert_eq!(labels, ["d[4]", "d[5]", "d[6]"]);
}
const BANK: InstanceId = InstanceId(u32::MAX - 7);
struct NegPlan {
model: TimingModel,
table: DelayTable,
res: Resolution,
plans: Plans,
}
fn neg_plan(src: &str) -> NegPlan {
neg_plan_with(src, &negative_on())
}
fn negative_on() -> TimingOptions {
TimingOptions {
negative_timing_checks: true,
..TimingOptions::default()
}
}
fn neg_plan_with(src: &str, options: &TimingOptions) -> NegPlan {
let mut model = discovered(src).unwrap_or_else(|e| panic!("discover failed: {e}"));
let mut own: HashMap<SymbolId, SignalSlot> = HashMap::new();
let mut name = |t: &TerminalRef| {
let next = own.len() as u32;
own.entry(t.0.clone()).or_insert(SignalSlot {
slot: next,
width: 1,
signed: false,
class: vj_codegen::process_lower::SignalSlotClass::Inline,
});
};
for entry in &model.timing_checks {
let (reference, data, notifier, delayed) = match &entry.args {
CheckArgs::SetupHold(a) => (
&a.reference,
&a.data,
&a.notifier,
[&a.delayed_reference, &a.delayed_data],
),
CheckArgs::RecRem(a) => (
&a.reference,
&a.data,
&a.notifier,
[&a.delayed_reference, &a.delayed_data],
),
CheckArgs::Setup(a) | CheckArgs::Hold(a) => {
(&a.reference, &a.data, &a.notifier, [&None, &None])
}
other => panic!("neg_plan does not name the terminals of {other:?}"),
};
name(&reference.terminal);
name(&data.terminal);
for t in [notifier, delayed[0], delayed[1]].into_iter().flatten() {
name(t);
}
}
let signals = HashMap::from([(InstanceId(0), own)]);
let no_iface = HashMap::new();
let scope = Scope {
own: &signals,
iface: &no_iface,
};
let mut table = negative_timing(&mut model, &scope, options).expect("solve");
let mut res = Resolution::default();
let plans = plan(&model, &scope, options, &mut table, &mut res, BANK).expect("plan");
NegPlan {
model,
table,
res,
plans,
}
}
fn neg_src(checks: &str) -> String {
format!(
"module m(input clk, input d1, input d2, input d3);\n reg ntf;\n wire dclk, dd1, \
dd2;\n specify\n {checks}\n endspecify\nendmodule\n"
)
}
#[test]
fn example_1_creates_one_implicit_delayed_clock() {
let p = neg_plan(&neg_src("$setuphold(posedge clk, d1, -10, 20, ntf);"));
assert_eq!(p.res.bank_widths, [1], "one implicit copy (CLK)");
assert_eq!(p.plans.lines.len(), 1);
assert_eq!(p.plans.lines[0].delay, 11);
assert_eq!(p.plans.lines[0].outputs.len(), 1);
let c = &p.plans.checks[0];
assert_eq!(
c.reference.loc.instance(),
BANK,
"the check reads the delayed clock"
);
assert_eq!(
c.data.loc.instance(),
InstanceId(0),
"the undelayed data is read as is"
);
assert_eq!(c.spec, CheckSpec::SetupHold { setup: 1, hold: 9 });
}
#[test]
fn example_2_shares_one_implicit_copy_of_the_common_clock() {
let p = neg_plan(&neg_src(
"$setuphold(posedge clk, d1, -10, 20, ntf);\n $setuphold(posedge clk, d2, -15, \
18, ntf);",
));
assert_eq!(
p.res.bank_widths,
[1],
"one copy for CLK, not one per check"
);
assert_eq!(p.plans.lines.len(), 1);
assert_eq!(p.plans.lines[0].delay, 16);
let [a, b] = &p.plans.checks[..] else {
panic!("two checks")
};
assert_eq!(a.reference.loc.slot.slot, b.reference.loc.slot.slot);
assert_eq!(a.reference.loc.instance(), BANK);
assert_eq!(a.spec, CheckSpec::SetupHold { setup: 6, hold: 4 });
assert_eq!(b.spec, CheckSpec::SetupHold { setup: 1, hold: 2 });
}
#[test]
fn example_3_uses_the_explicit_net_in_both_checks() {
let p = neg_plan(&neg_src(
"$setuphold(posedge clk, d1, -10, 20, ntf, , , dclk, dd1);\n \
$setuphold(posedge clk, d2, -15, 18, ntf);",
));
assert!(
p.res.bank_widths.is_empty(),
"no implicit copy: dclk covers CLK"
);
let [a, b] = &p.plans.checks[..] else {
panic!("two checks")
};
assert_eq!(a.reference.loc.instance(), InstanceId(0));
assert_eq!(
a.reference.loc.slot.slot, b.reference.loc.slot.slot,
"both read dclk"
);
assert_ne!(
a.reference.loc.slot.slot, a.data.loc.slot.slot,
"the check reads a net of its own"
);
let lines: Vec<(u64, usize)> = p
.plans
.lines
.iter()
.map(|l| (l.delay, l.outputs.len()))
.collect();
assert_eq!(lines, [(16, 1), (0, 1)]);
}
#[test]
fn positive_checks_create_nothing() {
let p = neg_plan(&neg_src("$setuphold(posedge clk, d1, 5, 7, ntf);"));
assert!(p.res.bank_widths.is_empty());
assert!(p.plans.lines.is_empty());
assert_eq!(
p.plans.checks[0].spec,
CheckSpec::SetupHold { setup: 5, hold: 7 }
);
}
#[test]
fn a_hold_on_a_delayed_clock_is_clamped_to_zero_with_a_warning() {
let p = neg_plan(&neg_src(
"$setuphold(posedge clk, d1, -10, 20, ntf);\n $hold(posedge clk, d2, 5, ntf);\n \
$setup(d3, posedge clk, 5, ntf);",
));
let specs: Vec<CheckSpec> = p.plans.checks.iter().map(|c| c.spec).collect();
assert_eq!(
specs,
[
CheckSpec::SetupHold { setup: 1, hold: 9 },
CheckSpec::Window {
kind: StabilityKind::Hold,
limit: 0
},
CheckSpec::Window {
kind: StabilityKind::Setup,
limit: 16
},
]
);
assert_eq!(p.table.warnings.len(), 1, "{:?}", p.table.warnings);
assert!(
p.table.warnings[0].contains("$hold"),
"{:?}",
p.table.warnings
);
}
#[test]
fn inconsistent_negative_limits_warn_and_run_with_the_smallest_zeroed() {
let p = neg_plan(&neg_src(
"$setuphold(posedge clk, d1, -9, 12, ntf);\n $setuphold(posedge d1, clk, -1, 0, \
ntf);",
));
assert_eq!(p.table.warnings.len(), 2, "{:?}", p.table.warnings);
assert!(
p.table.warnings[0].contains("setup limit -1"),
"{:?}",
p.table.warnings
);
assert!(
p.table.warnings[1].contains("set to 0"),
"{:?}",
p.table.warnings
);
assert_eq!(
p.table
.checks
.iter()
.map(|c| c.effective)
.collect::<Vec<_>>(),
[(-9, 12), (0, 0)]
);
}
#[test]
fn paths_from_a_delayed_signal_are_raised_to_its_delay() {
let p = neg_plan(
"module m(input clk, input d, output q);\n reg ntf;\n assign q = clk & d;\n \
specify\n (clk => q) = 6;\n (d => q) = 3;\n $setuphold(posedge clk, d, -7, \
13, ntf);\n endspecify\nendmodule\n",
);
let ticks: Vec<u64> = p
.model
.paths
.iter()
.map(|path| path.delays.min_all())
.collect();
assert_eq!(ticks, [8, 3]);
}
#[test]
fn the_skew_checks_never_read_delayed_signals() {
assert!(!uses_delayed_signals(CheckSpec::Skew { limit: 1 }));
assert!(!uses_delayed_signals(CheckSpec::TimeSkew {
limit: 1,
flags: SkewFlags {
event_based: false,
remain_active: false
}
}));
assert!(!uses_delayed_signals(CheckSpec::FullSkew {
limit: 1,
data_first_limit: 1,
flags: SkewFlags {
event_based: false,
remain_active: false
}
}));
assert!(uses_delayed_signals(CheckSpec::Width {
limit: 1,
threshold: 0
}));
assert!(uses_delayed_signals(CheckSpec::NoChange {
start: 0,
end: 0
}));
}
#[test]
fn adjust_spec_moves_each_kind_by_its_own_sign() {
let w = |kind, limit| CheckSpec::Window { kind, limit };
assert_eq!(
adjust_spec(w(StabilityKind::Setup, 10), 4),
(w(StabilityKind::Setup, 6), false)
);
assert_eq!(
adjust_spec(w(StabilityKind::Removal, 10), 4),
(w(StabilityKind::Removal, 6), false)
);
assert_eq!(
adjust_spec(w(StabilityKind::Hold, 10), 4),
(w(StabilityKind::Hold, 14), false)
);
assert_eq!(
adjust_spec(w(StabilityKind::Recovery, 10), 4),
(w(StabilityKind::Recovery, 14), false)
);
assert_eq!(
adjust_spec(w(StabilityKind::Setup, 4), 4),
(w(StabilityKind::Setup, 0), true)
);
assert_eq!(
adjust_spec(
CheckSpec::RecRem {
recovery: 3,
removal: 8
},
5
),
(
CheckSpec::RecRem {
recovery: 8,
removal: 3
},
false
)
);
assert_eq!(
adjust_spec(CheckSpec::NoChange { start: 2, end: -1 }, 3),
(CheckSpec::NoChange { start: -1, end: 2 }, false)
);
let width = CheckSpec::Width {
limit: 5,
threshold: 1,
};
assert_eq!(adjust_spec(width, 4), (width, false));
assert_eq!(
adjust_spec(w(StabilityKind::Hold, 0), 0),
(w(StabilityKind::Hold, 0), false)
);
}
fn neg_cond_gate(args: &str, first: &str, second: &str, x: u8, y: u8, tag: &str) -> usize {
let src = format!(
"module chk(input clk, input d, input c);\n reg ntf;\n wire dclk, dd;\n specify\n \
$setuphold(posedge clk, d, {args}, dclk, dd);\n endspecify\nendmodule\nmodule \
top;\n reg clk, d, c;\n chk u(.clk(clk), .d(d), .c(c));\n initial begin\n clk \
= 0; d = 0; c = 0;\n #100 c = {x}; {first} = 1;\n #13 c = {y};\n #2 \
{second} = 1;\n #100 $finish;\n end\nendmodule\n"
);
violations(&run_src(tag, &src)).len()
}
#[test]
fn run_design_negative_setup_conditions_follow_the_delayed_signals() {
for (what, args, wants) in [
("ts", "-10, 20, ntf, c, ", (true, false)),
("tc", "-10, 20, ntf, , c", (false, true)),
("both", "-10, 20, ntf, c, c", (true, true)),
] {
for x in 0..2u8 {
for y in 0..2u8 {
let want = (!wants.0 || x == 1) && (!wants.1 || y == 1);
let n = neg_cond_gate(args, "clk", "d", x, y, &format!("ncs-{what}-{x}{y}"));
assert_eq!(n, usize::from(want), "negative setup {what} c={x}->{y}");
}
}
}
}
#[test]
fn run_design_negative_hold_conditions_follow_the_delayed_signals() {
for (what, args, wants) in [
("ts", "20, -10, ntf, c, ", (true, false)),
("tc", "20, -10, ntf, , c", (false, true)),
("both", "20, -10, ntf, c, c", (true, true)),
] {
for x in 0..2u8 {
for y in 0..2u8 {
let want = (!wants.0 || x == 1) && (!wants.1 || y == 1);
let n = neg_cond_gate(args, "d", "clk", x, y, &format!("nch-{what}-{x}{y}"));
assert_eq!(n, usize::from(want), "negative hold {what} c={x}->{y}");
}
}
}
}
#[test]
fn run_design_negative_check_notifier_toggles_at_the_delayed_violation() {
let stdout = run_src(
"neg-ntf",
&check_design(
"$setuphold(posedge clk, d, -10, 20, ntf);",
"clk = 1;\n #15 d = 1;",
),
);
let ntf: Vec<&str> = stdout.lines().filter(|l| l.starts_with("ntf=")).collect();
assert_eq!(ntf, ["ntf=0 at 115"], "{stdout}");
assert_eq!(violations(&stdout).len(), 1, "{stdout}");
}
#[test]
fn option_off_plans_no_delay_and_clamps_the_negative_limit() {
let off = TimingOptions::default();
let p = neg_plan_with(&neg_src("$setuphold(posedge clk, d1, -10, 20, ntf);"), &off);
assert!(p.res.bank_widths.is_empty());
assert!(p.plans.lines.is_empty());
let c = &p.plans.checks[0];
assert_eq!(
c.reference.loc.instance(),
InstanceId(0),
"the original clock"
);
assert_eq!(c.spec, CheckSpec::SetupHold { setup: 0, hold: 20 });
assert!(p.table.warnings.is_empty());
let on = neg_plan(&neg_src("$setuphold(posedge clk, d1, -10, 20, ntf);"));
assert_eq!(on.plans.lines.len(), 1);
assert_eq!(
on.plans.checks[0].spec,
CheckSpec::SetupHold { setup: 1, hold: 9 }
);
}
#[test]
fn option_off_drives_declared_nets_as_zero_delay_copies() {
let off = TimingOptions::default();
let p = neg_plan_with(
&neg_src("$setuphold(posedge clk, d1, -10, 20, ntf, , , dclk, dd1);"),
&off,
);
let lines: Vec<(u64, usize)> = p
.plans
.lines
.iter()
.map(|l| (l.delay, l.outputs.len()))
.collect();
assert_eq!(lines, [(0, 1), (0, 1)]);
assert!(
p.table
.checks
.iter()
.all(|c| p.table.first_of(&c.check).is_none())
);
}
#[test]
fn run_design_option_off_delayed_nets_are_copies() {
let src = "module chk(input clk, input d);\n reg ntf;\n wire dclk, dd;\n specify\n \
$setuphold(posedge clk, d, -10, 20, ntf, , , dclk, dd);\n endspecify\n \
always @(dclk) if ($time >= 100) $display(\"dclk=%b at %0t\", dclk, $time);\n \
always @(dd) if ($time >= 100) $display(\"dd=%b at %0t\", dd, $time);\n\
endmodule\nmodule top;\n reg clk, d;\n chk u(.clk(clk), .d(d));\n initial \
begin\n clk = 0; d = 0;\n #100 clk = 1;\n #3 d = 1;\n #100 \
$finish;\n end\nendmodule\n";
let off = run_src_with("neg-off-nets", src, TimingOptions::default());
let lines: Vec<&str> = off.lines().filter(|l| !l.contains("violation")).collect();
assert_eq!(lines, ["dclk=1 at 100", "dd=1 at 103"], "{off}");
assert_eq!(violations(&off).len(), 1, "{off}");
}
#[test]
fn run_design_option_off_leaves_the_module_path_alone() {
let src = "module dut(input clk, input d, output reg q);\n reg ntf;\n wire dclk, dd;\n \
always @(posedge clk) q <= d;\n specify\n (clk => q) = 6;\n \
$setuphold (posedge clk, posedge d, -3, 8, ntf, , , dclk, dd);\n \
$setuphold (posedge clk, negedge d, -7, 13, ntf, , , dclk, dd);\n \
endspecify\nendmodule\nmodule top;\n reg clk, d;\n wire q;\n dut u(.clk(clk), \
.d(d), .q(q));\n initial begin\n clk = 0; d = 0;\n #10 d = 1;\n #10 \
clk = 1;\n #6.5 $display(\"q=%b\", q);\n $finish;\n end\nendmodule\n";
let off = run_src_with("neg-off-path", src, TimingOptions::default());
assert_eq!(off, "q=1\n", "{off}");
let on = run_src("neg-on-path", src);
assert_eq!(on, "q=x\n", "{on}");
}
#[test]
fn option_does_not_change_a_positive_limit_check() {
let src = check_design(
"$setuphold(posedge clk, d, 5, 7, ntf);",
"d = 1;\n #3 clk = 1;\n #4 d = 0;",
);
let off = run_src_with("neg-opt-pos-off", &src, TimingOptions::default());
let on = run_src_with("neg-opt-pos-on", &src, negative_on());
assert_eq!(off, on);
assert_eq!(violations(&off).len(), 2, "{off}");
}
#[test]
fn run_design_explicit_delayed_nets_follow_their_signals() {
let src = "module chk(input clk, input d);\n reg ntf;\n wire dclk, dd;\n specify\n \
$setuphold(posedge clk, d, -10, 20, ntf, , , dclk, dd);\n endspecify\n \
always @(dclk) if ($time >= 100) $display(\"dclk=%b at %0t\", dclk, $time);\n \
always @(dd) if ($time >= 100) $display(\"dd=%b at %0t\", dd, $time);\n\
endmodule\nmodule top;\n reg clk, d;\n chk u(.clk(clk), .d(d));\n initial \
begin\n clk = 0; d = 0;\n #100 clk = 1;\n #3 d = 1;\n #100 $finish;\n \
end\nendmodule\n";
let stdout = run_src("neg-explicit", src);
let lines: Vec<&str> = stdout
.lines()
.filter(|l| !l.contains("violation"))
.collect();
assert_eq!(lines, ["dd=1 at 103", "dclk=1 at 111"], "{stdout}");
}
}