use std::collections::{HashMap, HashSet};
use std::io::Write;
use std::path::Path;
use std::sync::{Arc, Mutex};
use sv_lang::Symbol;
use vj_codegen::process_lower::SignalSlot;
use vj_runtime::sdf_annotate::SdfAnnotateCall;
use vj_sdf::ast::{
Cell, CheckKind, DelayDef, DelayKind, Edge, Instance, IoPath, IoPathCond, LabelBlock,
PathPulse, Port, PortBit, PortTchk, SdfFile, Timescale, TimingCheck, TimingSpec,
};
use vj_sim::netlist::InstanceId;
use vj_sim::netlist::specify::{
CheckArgs, CheckEntry, CheckEvent, CondNode, PathCondition, PathDelaySet, PathEdge, PathEntry,
TerminalRef,
};
use vj_sim::process::cfg::shown_path;
use vj_sim::symid::SymbolId;
use crate::interconnect::{Node, SharedTable};
use crate::primitives::{Delays, DeviceDelays, PrimDelays};
use crate::sdf_checks::{self, SvCheck};
use crate::sdf_cond::{self, CondScope};
use crate::sdf_labels::LabelPlan;
use crate::sdf_values::{
Mtm, PulsePercent, Scale, Selector, SlotUpdate, apply_update, device_delays, iopath_update,
pathpulse_update, to_ticks,
};
use crate::specify::LiveTimes;
use crate::timing_options::{SdfMtm, TimingOptions};
use crate::timingcheck::{self, LimitNote, LiveChecks};
#[derive(Clone, Debug)]
pub(crate) struct SdfInstance {
pub(crate) id: InstanceId,
pub(crate) path: String,
pub(crate) module: String,
pub(crate) precision_exp: i32,
pub(crate) signals: HashMap<String, (SymbolId, u32)>,
pub(crate) slots: HashMap<String, u32>,
names: HashMap<SymbolId, String>,
}
pub(crate) fn instance_table<P>(
infos: &[(InstanceId, Symbol<'_>, Symbol<'_>, P)],
instance_signals: &HashMap<InstanceId, HashMap<SymbolId, SignalSlot>>,
) -> Vec<SdfInstance> {
infos
.iter()
.map(|(id, body, inst, _)| {
let prefix = format!("{}.", body.hierarchical_path());
let mut signals = HashMap::new();
let mut slot_of = HashMap::new();
let mut names = HashMap::new();
if let Some(slots) = instance_signals.get(id) {
for (uid, slot) in slots {
let Some(rel) = uid.path().strip_prefix(&prefix) else {
continue;
};
if rel.contains('.') {
continue;
}
signals.insert(rel.to_string(), (uid.clone(), slot.width));
slot_of.insert(rel.to_string(), slot.slot);
names.insert(uid.clone(), rel.to_string());
}
}
SdfInstance {
id: *id,
path: shown_path(inst),
module: inst
.instance_definition()
.map(|d| d.name().to_string())
.unwrap_or_default(),
precision_exp: vj_codegen::timescale::precision_exp(inst),
signals,
slots: slot_of,
names,
}
})
.collect()
}
pub(crate) fn condition_specparams(
model: &vj_sim::netlist::specify::TimingModel,
) -> HashMap<(InstanceId, SymbolId), u64> {
fn walk(
node: &CondNode,
inst: InstanceId,
model: &vj_sim::netlist::specify::TimingModel,
out: &mut HashMap<(InstanceId, SymbolId), u64>,
) {
match node {
CondNode::Specparam(uid) => {
let value = model
.specparam(inst, uid)
.and_then(|v| v.as_integer())
.and_then(|int| {
let mut value = 0u64;
let bits = int.bits();
if bits.len() > 64 {
return None;
}
for (i, b) in bits.iter().enumerate() {
match b {
sv_lang::Bit::One => value |= 1 << i,
sv_lang::Bit::Zero => {}
sv_lang::Bit::X | sv_lang::Bit::Z => return None,
}
}
Some(value)
});
if let Some(v) = value {
out.insert((inst, uid.clone()), v);
}
}
CondNode::Signal(_) | CondNode::Const(_) => {}
CondNode::Unary(_, a) | CondNode::Replication(_, a) => walk(a, inst, model, out),
CondNode::Binary(_, a, b) => {
walk(a, inst, model, out);
walk(b, inst, model, out);
}
CondNode::Ternary(a, b, c) => {
walk(a, inst, model, out);
walk(b, inst, model, out);
walk(c, inst, model, out);
}
CondNode::Concat(items) => items.iter().for_each(|i| walk(i, inst, model, out)),
}
}
let mut out = HashMap::new();
for p in &model.paths {
if let PathCondition::If(node) = &p.condition {
walk(node, p.instance, model, &mut out);
}
}
out
}
struct InstScope<'a> {
inst: &'a SdfInstance,
specparams: &'a HashMap<(InstanceId, SymbolId), u64>,
}
impl CondScope for InstScope<'_> {
fn width_of(&self, name: &str) -> Option<u32> {
self.inst.signals.get(name).map(|(_, w)| *w)
}
fn name_of(&self, uid: &SymbolId) -> Option<(String, u32)> {
let name = self.inst.names.get(uid)?;
Some((name.clone(), self.inst.signals[name].1))
}
fn specparam(&self, uid: &SymbolId) -> Option<u64> {
self.specparams.get(&(self.inst.id, uid.clone())).copied()
}
}
fn terminal_matches(inst: &SdfInstance, t: &TerminalRef, port: &Port) -> bool {
let [name] = port.path.as_slice() else {
return false;
};
if inst.names.get(&t.0) != Some(name) {
return false;
}
match (port.bit, t.1) {
(None, _) | (Some(_), None) => true,
(Some(PortBit::Bit(i)), Some((lo, width))) => width == 1 && i64::from(lo) == i,
(Some(PortBit::Range(a, b)), Some((lo, width))) => {
i64::from(lo) == a.min(b) && i64::from(width) == (a - b).abs() + 1
}
}
}
fn edge_matches(sdf: Option<Edge>, path: PathEdge) -> Result<bool, String> {
let want = match sdf {
None => return Ok(true),
Some(Edge::Posedge | Edge::T01) => sv_lang::EdgeKind::PosEdge,
Some(Edge::Negedge | Edge::T10) => sv_lang::EdgeKind::NegEdge,
Some(e @ (Edge::T0z | Edge::Tz1 | Edge::T1z | Edge::Tz0)) => {
return Err(format!(
"the edge {e:?} has no SystemVerilog edge identifier (posedge/negedge cover 01 \
and 10 only)"
));
}
};
Ok(path == PathEdge::Specific(want))
}
fn event_matches(
inst: &SdfInstance,
scope: &InstScope<'_>,
port: &PortTchk,
ev: &CheckEvent,
) -> Result<bool, String> {
if !terminal_matches(inst, &ev.terminal, &port.spec.port) {
return Ok(false);
}
if let Some(edge) = port.spec.edge
&& timingcheck::sdf_edge_control(edge)? != ev.edge_control
{
return Ok(false);
}
match (&port.cond, &ev.condition) {
(None, _) => Ok(true),
(Some(expr), Some(node)) => sdf_cond::equivalent(expr, node, scope),
(Some(_), None) => Ok(false),
}
}
fn check_events(args: &CheckArgs) -> (&CheckEvent, Option<&CheckEvent>) {
match args {
CheckArgs::Setup(a)
| CheckArgs::Hold(a)
| CheckArgs::Recovery(a)
| CheckArgs::Removal(a)
| CheckArgs::Skew(a) => (&a.reference, Some(&a.data)),
CheckArgs::SetupHold(a) => (&a.reference, Some(&a.data)),
CheckArgs::RecRem(a) => (&a.reference, Some(&a.data)),
CheckArgs::TimeSkew(a) => (&a.reference, Some(&a.data)),
CheckArgs::FullSkew(a) => (&a.reference, Some(&a.data)),
CheckArgs::NoChange(a) => (&a.reference, Some(&a.data)),
CheckArgs::Period(a) => (&a.reference, None),
CheckArgs::Width(a) => (&a.reference, None),
}
}
fn check_conditions(args: &CheckArgs) -> (Option<&CondNode>, Option<&CondNode>) {
match args {
CheckArgs::SetupHold(a) => (a.timestamp_cond.as_ref(), a.timecheck_cond.as_ref()),
CheckArgs::RecRem(a) => (a.timestamp_cond.as_ref(), a.timecheck_cond.as_ref()),
CheckArgs::Setup(_)
| CheckArgs::Hold(_)
| CheckArgs::Recovery(_)
| CheckArgs::Removal(_)
| CheckArgs::Skew(_)
| CheckArgs::TimeSkew(_)
| CheckArgs::FullSkew(_)
| CheckArgs::Period(_)
| CheckArgs::Width(_)
| CheckArgs::NoChange(_) => (None, None),
}
}
fn limit_note_warning(note: LimitNote, name: &str) -> Option<String> {
match note {
LimitNote::Exact => None,
LimitNote::NegativeToZero => Some(format!(
"the negative limit annotated to {name} cannot be negative (it takes no negative \
limit); set to 0"
)),
LimitNote::Unrealisable { negative_timing } => Some(format!(
"the limit annotated to {name} cannot be realised ({}); set to 0",
if negative_timing {
"the delay of the check's delayed signals, solved when the design was planned, \
cannot absorb it (LRM 31.9.1)"
} else {
"it is negative and negative timing checks are not enabled \
(--enable-negative-timing-checks)"
}
)),
}
}
fn condition_reads(node: &CondNode, uid: &SymbolId) -> bool {
match node {
CondNode::Specparam(u) => u == uid,
CondNode::Signal(_) | CondNode::Const(_) => false,
CondNode::Unary(_, a) | CondNode::Replication(_, a) => condition_reads(a, uid),
CondNode::Binary(_, a, b) => condition_reads(a, uid) || condition_reads(b, uid),
CondNode::Ternary(a, b, c) => {
condition_reads(a, uid) || condition_reads(b, uid) || condition_reads(c, uid)
}
CondNode::Concat(items) => items.iter().any(|i| condition_reads(i, uid)),
}
}
pub(crate) struct Installed;
impl Installed {
pub(crate) fn new(mut annotator: Annotator) -> Installed {
vj_runtime::specparam::clear();
vj_runtime::sdf_annotate::install_applier(Box::new(move |call| annotator.apply(call)));
vj_runtime::sdf_annotate::install_finisher(Box::new(|code| {
vj_sim::sched::real_sv_rt_finish(vj_abi::SimHandle(std::ptr::null_mut()), code);
}));
Installed
}
}
impl Drop for Installed {
fn drop(&mut self) {
vj_runtime::sdf_annotate::clear_applier();
vj_runtime::specparam::clear();
}
}
pub(crate) struct Annotator {
paths: Vec<PathEntry>,
instances: Vec<SdfInstance>,
specparams: HashMap<(InstanceId, SymbolId), u64>,
live: LiveTimes,
device: DeviceDelays,
prims: PrimDelays,
tick_exp: i32,
pulse: PulsePercent,
tool_control: Mtm,
logs: HashSet<String>,
checks: Vec<CheckEntry>,
live_checks: Arc<LiveChecks>,
labels: LabelPlan,
interconnect: Option<SharedTable>,
capture_log: bool,
captured_log: Option<(String, Vec<String>)>,
}
impl Annotator {
pub(crate) fn new(
paths: Vec<PathEntry>,
instances: Vec<SdfInstance>,
specparams: HashMap<(InstanceId, SymbolId), u64>,
live: LiveTimes,
device: DeviceDelays,
tick_exp: i32,
) -> Annotator {
Annotator {
paths,
instances,
specparams,
live,
device,
prims: PrimDelays::new(),
tick_exp,
pulse: PulsePercent::DEFAULT,
tool_control: Mtm::Typ,
logs: HashSet::new(),
checks: Vec::new(),
live_checks: Arc::new(LiveChecks::default()),
labels: LabelPlan::default(),
interconnect: None,
capture_log: false,
captured_log: None,
}
}
pub(crate) fn with_primitives(mut self, prims: PrimDelays) -> Annotator {
self.prims = prims;
self
}
pub(crate) fn capturing_logs(mut self) -> Annotator {
self.capture_log = true;
self
}
pub(crate) fn take_captured_log(&mut self) -> Option<(String, Vec<String>)> {
self.captured_log.take()
}
pub(crate) fn with_options(mut self, options: &TimingOptions) -> Annotator {
self.pulse = PulsePercent {
reject: options.reject_pct(),
error: options.error_pct(),
};
self.tool_control = match options.tool_control() {
SdfMtm::Minimum => Mtm::Min,
SdfMtm::Typical => Mtm::Typ,
SdfMtm::Maximum => Mtm::Max,
};
self
}
pub(crate) fn with_interconnect(mut self, table: SharedTable) -> Annotator {
self.interconnect = Some(table);
self
}
pub(crate) fn with_labels(mut self, labels: LabelPlan) -> Annotator {
self.labels = labels;
self
}
pub(crate) fn with_checks(
mut self,
checks: Vec<CheckEntry>,
live_checks: Arc<LiveChecks>,
) -> Annotator {
self.checks = checks;
self.live_checks = live_checks;
self
}
pub(crate) fn apply(&mut self, call: &SdfAnnotateCall) -> Result<Vec<String>, String> {
let mtm = Mtm::parse(call.mtm_spec.as_deref(), self.tool_control)?;
let scale = Scale::parse(call.scale_factors.as_deref(), call.scale_type.as_deref())?;
let sel = Selector { mtm, scale };
let mut warnings = Vec::new();
if let Some(cfg) = &call.config_file {
std::fs::read_to_string(cfg)
.map_err(|e| format!("cannot read the config_file \"{cfg}\": {e}"))?;
warnings.push(format!(
"the config_file \"{cfg}\" was read, but its directives are not interpreted \
(implementation-defined: the LRM does not define the file's format)"
));
}
let text = std::fs::read_to_string(&call.file)
.map_err(|e| format!("cannot read the SDF file \"{}\": {e}", call.file))?;
let mut log = Vec::new();
self.annotate_text(
&text,
&call.file,
&call.scope,
&sel,
&mut warnings,
&mut log,
)?;
if let Some(path) = &call.log_file {
if self.capture_log {
self.captured_log = Some((path.clone(), log));
} else {
self.write_log(path, &log)?;
}
}
Ok(warnings)
}
fn annotate_text(
&mut self,
text: &str,
name: &str,
scope_path: &str,
sel: &Selector,
warnings: &mut Vec<String>,
log: &mut Vec<String>,
) -> Result<(), String> {
let sdf = vj_sdf::parse(text)
.map_err(|e| format!("{name}:{}:{}: {}", e.line, e.col, e.message))?;
let scope = self
.instances
.iter()
.position(|i| i.path == scope_path)
.ok_or_else(|| {
format!("the scope \"{scope_path}\" is not an instance of the design")
})?;
log.push(format!(
"$sdf_annotate: file \"{name}\", scope \"{scope_path}\""
));
self.annotate_file(&sdf, scope, sel, warnings, log);
Ok(())
}
fn write_log(&mut self, path: &str, lines: &[String]) -> Result<(), String> {
let first = self.logs.insert(path.to_string());
let mut f = std::fs::OpenOptions::new()
.create(true)
.write(true)
.append(!first)
.truncate(first)
.open(Path::new(path))
.map_err(|e| format!("cannot open the log_file \"{path}\": {e}"))?;
for l in lines {
writeln!(f, "{l}").map_err(|e| format!("cannot write the log_file \"{path}\": {e}"))?;
}
Ok(())
}
fn annotate_file(
&mut self,
sdf: &SdfFile,
scope: usize,
sel: &Selector,
warnings: &mut Vec<String>,
log: &mut Vec<String>,
) {
let ts = sdf.header.timescale.unwrap_or(Timescale::DEFAULT);
for cell in &sdf.cells {
let prims = self.resolve_prims(scope, cell);
let targets = if prims.is_empty() {
self.resolve(scope, cell, warnings)
} else {
let mut ignored = Vec::new();
let modules = self.resolve(scope, cell, &mut ignored);
if cell.instance.wildcard {
modules
} else {
Vec::new()
}
};
for p in &prims {
self.annotate_prim(p, cell, ts, sel, warnings, log);
}
for spec in &cell.specs {
match spec {
TimingSpec::Delay(block) => {
for &t in &targets {
for def in &block.defs {
self.annotate_def(t, def, block.mode, sel, ts, warnings, log);
}
}
}
TimingSpec::TimingChecks(checks) => {
for &t in &targets {
for check in checks {
self.annotate_check(t, check, sel, ts, warnings, log);
}
}
}
TimingSpec::Labels(block) => {
for &t in &targets {
self.annotate_labels(t, block, sel, ts, cell.line, warnings, log);
}
}
}
}
}
}
fn resolve_prims(&self, scope: usize, cell: &Cell) -> Vec<String> {
let Instance { path, wildcard } = &cell.instance;
let mut target = self.instances[scope].path.clone();
for c in path {
target.push('.');
target.push_str(c);
}
let below = format!("{target}.");
let mut found: Vec<String> = self
.prims
.iter()
.filter(|(p, (ty, _))| {
*ty == cell.celltype && (**p == target || (*wildcard && p.starts_with(&below)))
})
.map(|(p, _)| p.clone())
.collect();
found.sort();
found
}
fn annotate_prim(
&mut self,
path: &str,
cell: &Cell,
ts: Timescale,
sel: &Selector,
warnings: &mut Vec<String>,
log: &mut Vec<String>,
) {
let precision = self
.instances
.iter()
.filter(|i| path.starts_with(&format!("{}.", i.path)))
.max_by_key(|i| i.path.len())
.map_or(0, |i| i.precision_exp);
let tick_exp = self.tick_exp;
let magnitude = f64::from(ts.magnitude);
let convert = move |v: f64| to_ticks(v * magnitude, ts.unit_exp10, precision, tick_exp);
for spec in &cell.specs {
let TimingSpec::Delay(block) = spec else {
warnings.push(format!(
"line {}: primitive \"{path}\": only IOPATH and DEVICE delays annotate a \
primitive; not annotated",
cell.line
));
continue;
};
for def in &block.defs {
let (desc, delays) = match &def.kind {
DelayKind::IoPath(io) => (describe_iopath(io), &io.delays),
DelayKind::Device { delays, .. } => ("DEVICE".to_string(), delays),
_ => {
warnings.push(format!(
"line {}: primitive \"{path}\": only IOPATH and DEVICE delays \
annotate a primitive; not annotated",
def.line
));
continue;
}
};
match iopath_update(delays, sel, self.pulse, &convert) {
Ok(update) => {
let cells = &self.prims[path].1;
for c in cells {
let mut d = c.lock().unwrap();
*d = Delays::from_triple(device_delays(
d.triple(),
&update.delay,
block.mode,
));
}
log.push(format!(
"line {}: {desc} annotated {} output(s) of primitive \"{path}\"",
def.line,
cells.len()
));
if delays.iter().any(|d| d.pulse.is_some()) {
warnings.push(format!(
"line {}: {desc}: the pulse limits of a primitive's delay are not annotated",
def.line
));
}
}
Err(msg) => warnings.push(format!("line {}: {desc}: {msg}", def.line)),
}
}
}
}
fn resolve(&self, scope: usize, cell: &Cell, warnings: &mut Vec<String>) -> Vec<usize> {
let Instance { path, wildcard } = &cell.instance;
let mut target = self.instances[scope].path.clone();
for c in path {
target.push('.');
target.push_str(c);
}
let shown = if path.is_empty() && !wildcard {
"(INSTANCE)".to_string()
} else {
format!(
"INSTANCE {}{}",
path.join("."),
if *wildcard { ".*" } else { "" }
)
};
if *wildcard {
let below = format!("{target}.");
let found: Vec<usize> = self
.instances
.iter()
.enumerate()
.filter(|(_, i)| {
(i.path == target || i.path.starts_with(&below)) && i.module == cell.celltype
})
.map(|(k, _)| k)
.collect();
if found.is_empty() {
warnings.push(format!(
"line {}: {shown} names no instance of {} at or below \"{target}\"",
cell.line, cell.celltype
));
}
return found;
}
let Some(k) = self.instances.iter().position(|i| i.path == target) else {
warnings.push(format!(
"line {}: {shown} names no instance (\"{target}\" is not in the design)",
cell.line
));
return Vec::new();
};
if self.instances[k].module != cell.celltype {
warnings.push(format!(
"line {}: CELLTYPE {} does not match the module {} of \"{target}\"; not annotated",
cell.line, cell.celltype, self.instances[k].module
));
return Vec::new();
}
vec![k]
}
#[allow(clippy::too_many_arguments)]
fn annotate_def(
&mut self,
inst: usize,
def: &DelayDef,
mode: vj_sdf::ast::DeltaMode,
sel: &Selector,
ts: Timescale,
warnings: &mut Vec<String>,
log: &mut Vec<String>,
) {
let (inst_path, precision) = {
let i = &self.instances[inst];
(i.path.clone(), i.precision_exp)
};
let tick_exp = self.tick_exp;
let magnitude = f64::from(ts.magnitude);
let convert = move |v: f64| to_ticks(v * magnitude, ts.unit_exp10, precision, tick_exp);
let line = def.line;
let warn = |w: &mut Vec<String>, msg: String| w.push(format!("line {line}: {msg}"));
match &def.kind {
DelayKind::IoPath(io) => {
let desc = describe_iopath(io);
let matched = match self.match_iopath(inst, io) {
Ok(m) => m,
Err(msg) => {
warn(warnings, format!("{desc} in \"{inst_path}\": {msg}"));
return;
}
};
if matched.is_empty() {
warn(
warnings,
format!("{desc}: no matching specify path in \"{inst_path}\""),
);
return;
}
match iopath_update(&io.delays, sel, self.pulse, &convert) {
Ok(update) => {
self.apply_to(&matched, &update, mode);
log.push(format!(
"line {line}: {desc} annotated {} specify path(s) of \"{inst_path}\"",
matched.len()
));
}
Err(msg) => warn(warnings, format!("{desc}: {msg}")),
}
}
DelayKind::Device { port, delays } => {
let desc = match port {
Some(p) => format!("DEVICE {}", port_text(p)),
None => "DEVICE".to_string(),
};
let (target, target_port) = self.device_target(inst, port.as_ref());
let (inst_path, target_precision) = {
let t = &self.instances[target];
(t.path.clone(), t.precision_exp)
};
let convert = move |v: f64| {
to_ticks(v * magnitude, ts.unit_exp10, target_precision, tick_exp)
};
let matched = self.match_device(target, target_port.as_ref());
let cells = if matched.is_empty() {
self.device_cells(target, target_port.as_ref())
} else {
Vec::new()
};
if matched.is_empty() && cells.is_empty() {
warn(
warnings,
format!(
"{desc}: no specify path to a module output and no primitive \
driving one in \"{inst_path}\""
),
);
return;
}
match iopath_update(delays, sel, self.pulse, &convert) {
Ok(update) if !matched.is_empty() => {
self.apply_to(&matched, &update, mode);
log.push(format!(
"line {line}: {desc} annotated {} specify path(s) of \"{inst_path}\"",
matched.len()
));
}
Ok(update) => {
for cell in &cells {
let mut d = cell.lock().unwrap();
*d =
Delays::from_triple(device_delays(d.triple(), &update.delay, mode));
}
log.push(format!(
"line {line}: {desc} annotated {} primitive output(s) of \
\"{inst_path}\"",
cells.len()
));
if delays.iter().any(|d| d.pulse.is_some()) {
warn(
warnings,
format!(
"{desc}: the pulse limits of a primitive's delay are not \
annotated (a primitive has no module path pulse limits)"
),
);
}
}
Err(msg) => warn(warnings, format!("{desc}: {msg}")),
}
}
DelayKind::PathPulse(pp) => {
let desc = describe_pathpulse(pp);
let matched = self.match_pathpulse(inst, pp);
if matched.is_empty() {
warn(
warnings,
format!("{desc}: no matching specify path in \"{inst_path}\""),
);
return;
}
let mut live = self.live.lock().unwrap();
for &p in &matched {
let update = pathpulse_update(pp, sel, &convert, &live[p].delays);
apply_update(&mut live[p], &update, mode);
}
log.push(format!(
"line {line}: {desc} annotated {} specify path(s) of \"{inst_path}\"",
matched.len()
));
}
DelayKind::Port { port, delays } => self.annotate_interconnect(
inst,
&InterconnectDef {
what: "PORT",
source: None,
load: port,
net: false,
delays,
},
mode,
sel,
ts,
line,
warnings,
log,
),
DelayKind::NetDelay { net, delays } => self.annotate_interconnect(
inst,
&InterconnectDef {
what: "NETDELAY",
source: None,
load: net,
net: true,
delays,
},
mode,
sel,
ts,
line,
warnings,
log,
),
DelayKind::Interconnect {
source,
load,
delays,
} => self.annotate_interconnect(
inst,
&InterconnectDef {
what: "INTERCONNECT",
source: Some(source),
load,
net: false,
delays,
},
mode,
sel,
ts,
line,
warnings,
log,
),
}
}
fn interconnect_node(&self, inst: usize, p: &Port) -> Option<(Node, usize)> {
let (name, rel) = p.path.split_last()?;
let base = &self.instances[inst].path;
let path = if rel.is_empty() {
base.clone()
} else {
format!("{base}.{}", rel.join("."))
};
let k = self.instances.iter().position(|i| i.path == path)?;
let slot = *self.instances[k].slots.get(name)?;
Some(((self.instances[k].id, slot), k))
}
#[allow(clippy::too_many_arguments)]
fn annotate_interconnect(
&mut self,
inst: usize,
def: &InterconnectDef<'_>,
mode: vj_sdf::ast::DeltaMode,
sel: &Selector,
ts: Timescale,
line: u32,
warnings: &mut Vec<String>,
log: &mut Vec<String>,
) {
let mut desc = def.what.to_string();
if let Some(s) = def.source {
desc.push(' ');
desc.push_str(&slash_text(s));
}
desc.push(' ');
desc.push_str(&slash_text(def.load));
let warn = |w: &mut Vec<String>, msg: String| w.push(format!("line {line}: {desc}: {msg}"));
let table = match &self.interconnect {
Some(t) => Arc::clone(t),
None => {
warn(
warnings,
"the design has no port connection wire to annotate".to_string(),
);
return;
}
};
let table = table.lock().unwrap();
if table.is_empty() {
warn(
warnings,
"the design has no port connection wire to annotate".to_string(),
);
return;
}
let Some((load, load_inst)) = self.interconnect_node(inst, def.load) else {
warn(
warnings,
format!(
"\"{}\" is not a signal of any instance of the design; not annotated",
slash_text(def.load)
),
);
return;
};
let bits = def.load.bit.as_ref().map(|b| match *b {
PortBit::Bit(i) => (i, i),
PortBit::Range(a, b) => (a, b),
});
let graph = table.graph();
let selection = if def.net && !graph.is_port(load) {
table.select_net_loads(load, bits)
} else if graph.is_port(load) {
table.select_feeders(load, bits)
} else {
Vec::new()
};
if selection.is_empty() {
warn(
warnings,
format!(
"\"{}\" has no port connection wire to delay (a top-level port, an output \
port, an inout port, an unpacked-array port, a port connected to an \
expression and a name that is not a port are not annotated); not annotated",
slash_text(def.load)
),
);
return;
}
if let Some((a, b)) = bits {
let covered: i64 = selection
.iter()
.map(|&(_, first, end)| i64::from(end - first))
.sum();
let wanted = (a - b).abs() + 1;
if covered < wanted {
warn(
warnings,
format!(
"only {covered} of the {wanted} selected bit(s) of \"{}\" are connected \
by a wire; the rest are not annotated",
slash_text(def.load)
),
);
}
}
let mut source_node: Option<(crate::interconnect::Node, Option<(i64, i64)>)> = None;
if let Some(src) = def.source {
match self.interconnect_node(inst, src) {
None => warn(
warnings,
format!(
"source port {} is not found; the delay to the load is annotated anyway",
slash_text(src)
),
),
Some((s, _)) => {
if !graph.net_of(load, Some(s)).source_on_net {
warn(
warnings,
format!(
"source port {} is not on the same net as the load; the delay \
to the load is annotated anyway",
slash_text(src)
),
);
}
let src_bits = src.bit.as_ref().map(|b| match *b {
PortBit::Bit(i) => (i, i),
PortBit::Range(a, b) => (a, b),
});
source_node = Some((s, src_bits));
}
}
}
let precision = self.instances[load_inst].precision_exp;
let (tick_exp, magnitude) = (self.tick_exp, f64::from(ts.magnitude));
let convert = |v: f64| to_ticks(v * magnitude, ts.unit_exp10, precision, tick_exp);
match iopath_update(def.delays, sel, self.pulse, &convert) {
Ok(update) => {
match source_node {
Some((s, src_bits)) => {
table.annotate_source(&selection, s, src_bits, &update, mode);
}
None => table.annotate(&selection, &update, mode),
}
log.push(format!(
"line {line}: {desc} annotated {} wire(s) of \"{}\"",
selection.len(),
self.instances[load_inst].path
));
}
Err(msg) => warn(warnings, msg),
}
}
#[allow(clippy::too_many_arguments)]
fn annotate_labels(
&mut self,
inst: usize,
block: &LabelBlock,
sel: &Selector,
ts: Timescale,
line: u32,
warnings: &mut Vec<String>,
log: &mut Vec<String>,
) {
let (id, inst_path, precision) = {
let i = &self.instances[inst];
(i.id, i.path.clone(), i.precision_exp)
};
let tick_exp = self.tick_exp;
let warn = |w: &mut Vec<String>, msg: String| w.push(format!("line {line}: {msg}"));
let Some(plan) = self.labels.get_mut(id) else {
warn(
warnings,
format!("LABEL: \"{inst_path}\" has no specparam information; not annotated"),
);
return;
};
let exp = ts.unit_exp10 - plan.unit_exp;
let to_module = |v: f64| {
let p = 10f64.powi(exp.abs());
let v = v * f64::from(ts.magnitude);
if exp >= 0 { v * p } else { v / p }
};
let mut touched: Vec<String> = Vec::new();
for (name, rv) in &block.labels {
let Some(sp) = plan.specparam_mut(name) else {
warn(
warnings,
format!("LABEL {name}: \"{inst_path}\" has no specparam `{name}`"),
);
continue;
};
let Some(v) = sel.value(*rv) else { continue };
match sp.annotate(block.mode, to_module(v)) {
Ok(_) => {
if !touched.contains(name) {
touched.push(name.clone());
}
}
Err(msg) => warn(warnings, format!("LABEL {name}: {msg}")),
}
}
if touched.is_empty() {
return;
}
for name in &touched {
let uid = plan
.specparams
.iter()
.find(|s| s.name == *name)
.map(|s| s.uid().clone());
if plan.read_by_planned_delay(name) {
warn(
warnings,
format!(
"LABEL {name}: a gate, continuous-assignment or net delay reads it, and \
those delays are fixed when the design is planned; not re-evaluated"
),
);
}
let in_condition = uid.is_some_and(|u| {
let in_path = self.paths.iter().any(|p| {
p.instance == id
&& matches!(&p.condition, PathCondition::If(n) if condition_reads(n, &u))
});
let in_check = self.checks.iter().filter(|c| c.instance == id).any(|c| {
let (reference, data) = check_events(&c.args);
let (stamp, check) = check_conditions(&c.args);
[
reference.condition.as_ref(),
data.and_then(|d| d.condition.as_ref()),
]
.into_iter()
.chain([stamp, check])
.flatten()
.any(|n| condition_reads(n, &u))
});
in_path || in_check
});
if in_condition {
warn(
warnings,
format!(
"LABEL {name}: a path or timing check condition reads it, and those \
conditions are fixed when the design is planned; not re-evaluated"
),
);
}
for derived in plan
.specparams
.iter()
.filter(|s| s.derived_from().contains(name))
{
warn(
warnings,
format!(
"LABEL {name}: the initializer of specparam `{}` reads it; that \
specparam is not re-derived",
derived.name
),
);
}
}
let paths: Vec<_> = plan
.paths
.iter()
.filter(|p| p.dep.reads_any(&touched))
.collect();
let limits: Vec<_> = plan
.limits
.iter()
.filter(|l| l.dep.reads_any(&touched))
.collect();
let mut jobs: Vec<&[String]> = Vec::new();
for dep in paths
.iter()
.map(|p| &p.dep)
.chain(limits.iter().map(|l| &l.dep))
{
match &dep.texts {
Ok(t) => jobs.push(t),
Err(_) => jobs.push(&[]),
}
}
let results = if jobs.is_empty() {
Vec::new()
} else {
match plan.evaluate_all(&jobs) {
Ok(r) => r,
Err(msg) => {
warn(
warnings,
format!("LABEL: cannot re-evaluate the expressions that read it: {msg}"),
);
return;
}
}
};
let unit_exp = plan.unit_exp;
let (mut n_paths, mut n_limits) = (0, 0);
let mut results = results.into_iter();
for p in &paths {
let result = results.next().expect("one result per job");
let desc = format!("a path delay reading {}", p.dep.refs.join(", "));
let delays = match (&p.dep.texts, result) {
(Err(why), _) => Err(why.clone()),
(Ok(_), Err(msg)) => Err(msg),
(Ok(_), Ok(values)) => values
.iter()
.map(|&v| {
if !v.is_finite() {
return Err(format!("the delay evaluates to {v}"));
}
Ok(to_ticks(v.max(0.0), unit_exp, precision, tick_exp) as u64)
})
.collect::<Result<Vec<u64>, String>>()
.and_then(|t| PathDelaySet::from_list(&t)),
};
match delays {
Ok(new) => {
let mut live = self.live.lock().unwrap();
for &k in &p.indices {
let t = &mut live[k];
let was_default = (t.reject == t.delays, t.error == t.delays);
let new = new.at_least(t.floor);
t.delays = new;
if was_default.0 {
t.reject = new;
}
if was_default.1 {
t.error = new;
}
}
n_paths += p.indices.len();
}
Err(msg) => warn(
warnings,
format!("LABEL: {desc} in \"{inst_path}\" is not re-evaluated: {msg}"),
),
}
}
for l in &limits {
let result = results.next().expect("one result per job");
let name = SvCheck::of(&self.checks[l.entry].args).name();
let desc = format!("a {name} limit reading {}", l.dep.refs.join(", "));
let Some(slot) = l.slot else {
warn(
warnings,
format!(
"LABEL: {desc} in \"{inst_path}\" is the threshold of a $width, which \
annotation does not reach; not re-evaluated"
),
);
continue;
};
let value = match (&l.dep.texts, result) {
(Err(why), _) => Err(why.clone()),
(Ok(_), Err(msg)) => Err(msg),
(Ok(_), Ok(values)) => match values.as_slice() {
[v] if v.is_finite() => Ok(*v),
[v] => Err(format!("the limit evaluates to {v}")),
_ => Err("a limit is one expression".to_string()),
},
};
match value {
Ok(v) => {
let ticks = to_ticks(v.abs(), unit_exp, precision, tick_exp);
let ticks = if v < 0.0 { -ticks } else { ticks };
match self.live_checks.set(l.entry, slot, ticks) {
Ok(note) => {
if let Some(msg) = limit_note_warning(note, name) {
warn(warnings, format!("LABEL: {desc}: {msg}"));
}
n_limits += 1;
}
Err(msg) => warn(warnings, format!("LABEL: {desc}: {msg}")),
}
}
Err(msg) => warn(
warnings,
format!("LABEL: {desc} in \"{inst_path}\" is not re-evaluated: {msg}"),
),
}
}
log.push(format!(
"line {line}: LABEL annotated specparam(s) {} of \"{inst_path}\"; re-evaluated {n_paths} \
specify path(s) and {n_limits} timing check limit(s)",
touched.join(", ")
));
}
fn annotate_check(
&mut self,
inst: usize,
check: &TimingCheck,
sel: &Selector,
ts: Timescale,
warnings: &mut Vec<String>,
log: &mut Vec<String>,
) {
let (inst_path, precision) = {
let i = &self.instances[inst];
(i.path.clone(), i.precision_exp)
};
let tick_exp = self.tick_exp;
let magnitude = f64::from(ts.magnitude);
let convert = move |v: f64| to_ticks(v * magnitude, ts.unit_exp10, precision, tick_exp);
let line = check.line;
let desc = describe_check(check);
let warn = |w: &mut Vec<String>, msg: String| w.push(format!("line {line}: {msg}"));
let want_ports = if matches!(check.kind, CheckKind::Width | CheckKind::Period) {
1
} else {
2
};
if check.ports.len() != want_ports
|| check.values.len() != sdf_checks::value_count(check.kind)
{
warn(
warnings,
format!(
"{desc}: {} takes {want_ports} port(s) and {} value(s); not annotated",
sdf_checks::sdf_name(check.kind),
sdf_checks::value_count(check.kind)
),
);
return;
}
let values: Vec<Option<i64>> = check
.values
.iter()
.map(|&rv| sel.value(rv).map(&convert))
.collect();
let matched = match self.match_checks(inst, check) {
Ok(m) => m,
Err(msg) => {
warn(warnings, format!("{desc} in \"{inst_path}\": {msg}"));
return;
}
};
if matched.is_empty() {
warn(
warnings,
format!("{desc}: no matching timing check in \"{inst_path}\""),
);
return;
}
let mut annotated = Vec::new();
let fan_out = sdf_checks::fan_out(check.kind);
for (entry, target) in matched {
let sv = fan_out[target].check;
for &(v, slot) in fan_out[target].slots {
let Some(ticks) = values[v] else { continue };
match self.live_checks.set(entry, slot, ticks) {
Ok(note) => {
if let Some(msg) = limit_note_warning(note, sv.name()) {
warn(warnings, format!("{desc}: {msg}"));
}
}
Err(msg) => warn(warnings, format!("{desc}: {msg}")),
}
}
annotated.push(sv.name());
}
log.push(format!(
"line {line}: {desc} annotated {} timing check(s) of \"{inst_path}\" ({})",
annotated.len(),
annotated.join(", ")
));
}
fn match_checks(
&self,
inst: usize,
check: &TimingCheck,
) -> Result<Vec<(usize, usize)>, String> {
let me = &self.instances[inst];
let scope = InstScope {
inst: me,
specparams: &self.specparams,
};
let (data_port, ref_port) = match check.ports.as_slice() {
[only] => (None, only),
[first, second] if sdf_checks::first_port_is_data(check.kind) => (Some(first), second),
[first, second] => (Some(second), first),
_ => return Err("a timing check takes one or two ports".to_string()),
};
let mut out = Vec::new();
for (k, entry) in self.checks.iter().enumerate() {
if entry.instance != me.id {
continue;
}
let sv = SvCheck::of(&entry.args);
let Some(target) = sdf_checks::fan_out(check.kind)
.iter()
.position(|t| t.check == sv)
else {
continue;
};
let (reference, data) = check_events(&entry.args);
if !event_matches(me, &scope, ref_port, reference)? {
continue;
}
match (data_port, data) {
(Some(p), Some(d)) => {
if !event_matches(me, &scope, p, d)? {
continue;
}
}
(None, None) => {}
(None, Some(_)) | (Some(_), None) => continue,
}
let (timestamp, timecheck) = check_conditions(&entry.args);
let mut conds_ok = true;
for (sdf, sv) in [(&check.scond, timestamp), (&check.ccond, timecheck)] {
if let Some(expr) = sdf {
conds_ok &= match sv {
Some(node) => sdf_cond::equivalent(expr, node, &scope)?,
None => false,
};
}
}
if conds_ok {
out.push((k, target));
}
}
Ok(out)
}
fn apply_to(&self, paths: &[usize], update: &SlotUpdate, mode: vj_sdf::ast::DeltaMode) {
let mut live = self.live.lock().unwrap();
for &p in paths {
apply_update(&mut live[p], update, mode);
}
}
fn paths_of(&self, inst: usize) -> impl Iterator<Item = (usize, &PathEntry)> {
let id = self.instances[inst].id;
self.paths
.iter()
.enumerate()
.filter(move |(_, p)| p.instance == id)
}
fn match_iopath(&self, inst: usize, io: &IoPath) -> Result<Vec<usize>, String> {
let me = &self.instances[inst];
let scope = InstScope {
inst: me,
specparams: &self.specparams,
};
let mut out = Vec::new();
for (k, p) in self.paths_of(inst) {
if !terminal_matches(me, &p.inputs[0], &io.input.port)
|| !terminal_matches(me, &p.outputs[0], &io.output)
{
continue;
}
if !edge_matches(io.input.edge, p.edge)? {
continue;
}
let cond_ok = match (&io.cond, &p.condition) {
(IoPathCond::None, _) => true,
(IoPathCond::Cond { expr, .. }, PathCondition::If(node)) => {
sdf_cond::equivalent(expr, node, &scope)?
}
(IoPathCond::Cond { .. }, _) => false,
(IoPathCond::Else, PathCondition::IfNone) => true,
(IoPathCond::Else, _) => false,
};
if cond_ok {
out.push(k);
}
}
Ok(out)
}
fn device_target(&self, inst: usize, port: Option<&Port>) -> (usize, Option<Port>) {
let Some(p) = port else {
return (inst, None);
};
let me = &self.instances[inst];
if let [name] = p.path.as_slice()
&& me.signals.contains_key(name)
{
return (inst, Some(p.clone()));
}
let child = |comps: &[String]| {
let want = format!("{}.{}", me.path, comps.join("."));
self.instances.iter().position(|i| i.path == want)
};
let mut whole = p.path.clone();
let whole_is_name = match (p.bit, whole.last_mut()) {
(None, _) => true,
(Some(PortBit::Bit(n)), Some(last)) => {
last.push_str(&format!("[{n}]"));
true
}
(Some(PortBit::Bit(_)), None) | (Some(PortBit::Range(..)), _) => false,
};
if whole_is_name && let Some(k) = child(&whole) {
return (k, None);
}
if let Some((last, parent)) = p.path.split_last()
&& !parent.is_empty()
&& let Some(k) = child(parent)
{
let port = Port {
path: vec![last.clone()],
bit: p.bit,
};
return (k, Some(port));
}
(inst, Some(p.clone()))
}
fn match_device(&self, inst: usize, port: Option<&Port>) -> Vec<usize> {
let me = &self.instances[inst];
self.paths_of(inst)
.filter(|(_, p)| port.is_none_or(|port| terminal_matches(me, &p.outputs[0], port)))
.map(|(k, _)| k)
.collect()
}
fn device_cells(&self, inst: usize, port: Option<&Port>) -> Vec<Arc<Mutex<Delays>>> {
let me = &self.instances[inst];
let only = match port {
None => None,
Some(p) => match p.path.as_slice() {
[name] => match me.signals.get(name) {
Some((uid, _)) => Some(uid),
None => return Vec::new(),
},
_ => return Vec::new(),
},
};
self.device
.iter()
.filter(|((id, uid), _)| *id == me.id && only.is_none_or(|o| o == uid))
.flat_map(|(_, cells)| cells.iter().cloned())
.collect()
}
fn match_pathpulse(&self, inst: usize, pp: &PathPulse) -> Vec<usize> {
let me = &self.instances[inst];
self.paths_of(inst)
.filter(|(_, p)| {
pp.input
.as_ref()
.is_none_or(|i| terminal_matches(me, &p.inputs[0], i))
&& pp
.output
.as_ref()
.is_none_or(|o| terminal_matches(me, &p.outputs[0], o))
})
.map(|(k, _)| k)
.collect()
}
}
struct InterconnectDef<'a> {
what: &'static str,
source: Option<&'a Port>,
load: &'a Port,
net: bool,
delays: &'a [vj_sdf::ast::DelVal],
}
fn slash_text(p: &Port) -> String {
let mut s = p.path.join("/");
match p.bit {
None => {}
Some(PortBit::Bit(i)) => s.push_str(&format!("[{i}]")),
Some(PortBit::Range(a, b)) => s.push_str(&format!("[{a}:{b}]")),
}
s
}
fn port_text(p: &Port) -> String {
let mut s = p.path.join(".");
match p.bit {
None => {}
Some(PortBit::Bit(i)) => s.push_str(&format!("[{i}]")),
Some(PortBit::Range(a, b)) => s.push_str(&format!("[{a}:{b}]")),
}
s
}
fn describe_iopath(io: &IoPath) -> String {
let kind = match io.cond {
IoPathCond::None => "IOPATH",
IoPathCond::Cond { .. } => "COND IOPATH",
IoPathCond::Else => "CONDELSE IOPATH",
};
format!(
"{kind} {} {}",
port_text(&io.input.port),
port_text(&io.output)
)
}
fn describe_check(check: &TimingCheck) -> String {
let ports: Vec<String> = check
.ports
.iter()
.map(|p| port_text(&p.spec.port))
.collect();
format!("{} {}", sdf_checks::sdf_name(check.kind), ports.join(" "))
}
fn describe_pathpulse(pp: &PathPulse) -> String {
let kind = if pp.percent {
"PATHPULSEPERCENT"
} else {
"PATHPULSE"
};
match (&pp.input, &pp.output) {
(Some(i), Some(o)) => format!("{kind} {} {}", port_text(i), port_text(o)),
_ => kind.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::specify::PathTimes;
use std::sync::{Arc, Mutex};
use vj_abi::bit4::Bit4;
use vj_sim::netlist::specify::{
CheckLimit, CondUnaryOp, EventPairArgs, FullSkewArgs, NoChangeArgs, PathDelaySet,
PeriodArgs, RecRemArgs, SetupHoldArgs, TimeSkewArgs, WidthArgs,
};
fn id(path: &str) -> SymbolId {
SymbolId::synthetic(path)
}
fn instance(
n: u32,
path: &str,
module: &str,
names: &[&str],
wide: &[(&str, u32)],
) -> SdfInstance {
let mut signals = HashMap::new();
let mut by_id = HashMap::new();
let all = names.iter().map(|n| (*n, 1)).chain(wide.iter().copied());
for (name, width) in all {
let uid = id(&format!("{path}.{name}"));
signals.insert(name.to_string(), (uid.clone(), width));
by_id.insert(uid, name.to_string());
}
SdfInstance {
id: InstanceId(n),
path: path.to_string(),
module: module.to_string(),
precision_exp: -12,
signals,
slots: HashMap::new(),
names: by_id,
}
}
fn term(inst: &str, name: &str) -> TerminalRef {
(id(&format!("{inst}.{name}")), None)
}
fn entry(
n: u32,
inst: &str,
from: TerminalRef,
to: TerminalRef,
edge: PathEdge,
condition: PathCondition,
) -> PathEntry {
PathEntry {
instance: InstanceId(n),
inputs: vec![from],
outputs: vec![to],
connection: sv_lang::TimingPathConnectionKind::Parallel,
polarity: sv_lang::TimingPathPolarity::Unknown,
path_uid: id(&format!("{inst}.path")),
delays: PathDelaySet::uniform(0),
edge,
edge_data_source_name: None,
condition,
reject_limit: PathDelaySet::uniform(0),
error_limit: PathDelaySet::uniform(0),
pulse_style: vj_sim::netlist::specify::PulseStyle::OnEvent,
showcancelled: false,
}
}
fn plain(from: &str, to: &str) -> PathEntry {
entry(
1,
"top.d",
term("top.d", from),
term("top.d", to),
PathEdge::None,
PathCondition::Unconditional,
)
}
struct World {
ann: Annotator,
live: LiveTimes,
checks: Arc<LiveChecks>,
}
fn world(paths: Vec<PathEntry>) -> World {
world_with(paths, HashMap::new())
}
fn world_with(paths: Vec<PathEntry>, device: DeviceDelays) -> World {
let names = ["sel", "mode", "zout", "a", "q"];
let instances = vec![
instance(0, "top", "top", &[], &[]),
instance(1, "top.d", "dut", &names, &[("bus", 4)]),
instance(2, "top.g", "wrap", &[], &[]),
instance(3, "top.g.d", "dut", &names, &[("bus", 4)]),
];
let times = PathTimes {
delays: PathDelaySet::uniform(0),
reject: PathDelaySet::uniform(0),
error: PathDelaySet::uniform(0),
floor: 0,
};
let live: LiveTimes = Arc::new(Mutex::new(vec![times; paths.len()]));
let ann = Annotator::new(
paths,
instances,
HashMap::new(),
Arc::clone(&live),
device,
-12,
);
World {
ann,
live,
checks: Arc::new(LiveChecks::default()),
}
}
fn world_checks(entries: Vec<CheckEntry>, delta: i64) -> World {
let mut w = world(Vec::new());
let live = Arc::new(timingcheck::live_checks_for_entries(&entries, delta, true));
w.ann = w.ann.with_checks(entries, Arc::clone(&live));
w.checks = live;
w
}
fn event(name: &str, edge: sv_lang::EdgeKind, cond: Option<CondNode>) -> CheckEvent {
timingcheck::test_event(term("top.d", name), edge, cond)
}
fn lim(arg_index: u32, ticks: i64) -> CheckLimit {
CheckLimit { arg_index, ticks }
}
fn check_entry(kind: sv_lang::SystemTimingCheckKind, args: CheckArgs) -> CheckEntry {
CheckEntry {
instance: InstanceId(1),
check_uid: id("top.d.chk"),
kind,
args,
}
}
fn setuphold(reference: CheckEvent, data: CheckEvent, setup: i64, hold: i64) -> CheckEntry {
check_entry(
sv_lang::SystemTimingCheckKind::SetupHold,
CheckArgs::SetupHold(SetupHoldArgs {
reference,
data,
setup_limit: lim(2, setup),
hold_limit: lim(3, hold),
notifier: None,
timestamp_cond: None,
timecheck_cond: None,
delayed_reference: None,
delayed_data: None,
}),
)
}
fn pair(reference: CheckEvent, data: CheckEvent, limit: i64) -> EventPairArgs {
EventPairArgs {
reference,
data,
limit: lim(2, limit),
notifier: None,
}
}
fn sdf_checks(body: &str) -> String {
format!(
"(DELAYFILE (TIMESCALE 1ns) (CELL (CELLTYPE \"dut\") (INSTANCE d) \
(TIMINGCHECK {body})))"
)
}
impl World {
fn run(&mut self, scope: &str, sdf: &str) -> (Vec<String>, Vec<String>) {
let sel = Selector {
mtm: Mtm::Typ,
scale: Scale::IDENTITY,
};
let (mut warnings, mut log) = (Vec::new(), Vec::new());
self.ann
.annotate_text(sdf, "t.sdf", scope, &sel, &mut warnings, &mut log)
.unwrap();
(warnings, log)
}
fn times(&self, i: usize) -> PathTimes {
self.live.lock().unwrap()[i]
}
fn rise_fall(&self, i: usize) -> (u64, u64) {
let t = self.times(i);
(
t.delays.of(Bit4::Zero, Bit4::One),
t.delays.of(Bit4::One, Bit4::Zero),
)
}
}
fn sdf(body: &str) -> String {
format!(
"(DELAYFILE (TIMESCALE 1ns) (CELL (CELLTYPE \"dut\") (INSTANCE d) \
(DELAY (ABSOLUTE {body}))))"
)
}
fn mode_cond(negated: bool) -> PathCondition {
let sig = CondNode::Signal(term("top.d", "mode"));
PathCondition::If(if negated {
CondNode::Unary(CondUnaryOp::LogicalNot, Box::new(sig))
} else {
sig
})
}
fn cond_path(negated: bool) -> PathEntry {
entry(
1,
"top.d",
term("top.d", "sel"),
term("top.d", "zout"),
PathEdge::None,
mode_cond(negated),
)
}
#[test]
fn lrm_32_4_1_example_1_the_matching_path_gets_the_rise_and_fall_times() {
let mut w = world(vec![plain("sel", "zout")]);
let (warnings, _) = w.run("top", &sdf("(IOPATH sel zout (1.3) (1.7))"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(w.rise_fall(0), (1300, 1700));
}
#[test]
fn lrm_32_4_1_example_2_a_conditional_iopath_annotates_only_the_same_condition() {
let mut w = world(vec![cond_path(true), cond_path(false)]);
let (warnings, _) = w.run("top", &sdf("(COND mode (IOPATH sel zout (1.3) (1.7)))"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(
w.rise_fall(0),
(0, 0),
"`if (!mode)` is a different condition"
);
assert_eq!(w.rise_fall(1), (1300, 1700), "`if (mode)`");
}
#[test]
fn lrm_32_4_1_example_3_a_nonconditional_iopath_annotates_every_path_between_the_ports() {
let mut w = world(vec![cond_path(true), cond_path(false)]);
let (warnings, _) = w.run("top", &sdf("(IOPATH sel zout (1.3) (1.7))"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(w.rise_fall(0), (1300, 1700));
assert_eq!(w.rise_fall(1), (1300, 1700));
}
#[test]
fn condelse_annotates_only_the_ifnone_path() {
let ifnone = entry(
1,
"top.d",
term("top.d", "sel"),
term("top.d", "zout"),
PathEdge::None,
PathCondition::IfNone,
);
let mut w = world(vec![cond_path(false), ifnone, plain("sel", "zout")]);
w.run("top", &sdf("(CONDELSE (IOPATH sel zout (4) (5)))"));
assert_eq!(w.rise_fall(0), (0, 0));
assert_eq!(w.rise_fall(1), (4000, 5000));
assert_eq!(
w.rise_fall(2),
(0, 0),
"an unconditional path is not an ifnone path"
);
w.run("top", &sdf("(COND mode (IOPATH sel zout (6) (7)))"));
assert_eq!(w.rise_fall(0), (6000, 7000));
assert_eq!(w.rise_fall(1), (4000, 5000), "COND never annotates ifnone");
}
#[test]
fn a_cond_matches_by_truth_table_not_by_spelling() {
let mut w = world(vec![cond_path(true)]);
w.run("top", &sdf("(COND mode == 1'b0 (IOPATH sel zout (2) (3)))"));
assert_eq!(w.rise_fall(0), (2000, 3000));
}
fn edge_paths() -> Vec<PathEntry> {
let mk = |edge| {
entry(
1,
"top.d",
term("top.d", "a"),
term("top.d", "q"),
edge,
PathCondition::Unconditional,
)
};
vec![
mk(PathEdge::Specific(sv_lang::EdgeKind::PosEdge)),
mk(PathEdge::Specific(sv_lang::EdgeKind::NegEdge)),
mk(PathEdge::None),
mk(PathEdge::Any),
]
}
#[test]
fn an_sdf_edge_selects_the_path_with_that_edge() {
let mut w = world(edge_paths());
w.run("top", &sdf("(IOPATH (posedge a) q (1) (1))"));
assert_eq!(
(0..4).map(|i| w.rise_fall(i).0).collect::<Vec<_>>(),
[1000, 0, 0, 0],
"posedge reaches only the posedge path (not none, any)"
);
w.run("top", &sdf("(IOPATH (10 a) q (2) (2))"));
assert_eq!(
(0..4).map(|i| w.rise_fall(i).0).collect::<Vec<_>>(),
[1000, 2000, 0, 0],
"10 is negedge"
);
w.run("top", &sdf("(IOPATH a q (3) (3))"));
assert_eq!(
(0..4).map(|i| w.rise_fall(i).0).collect::<Vec<_>>(),
[3000; 4],
"no edge in the SDF selects every path"
);
}
#[test]
fn a_z_edge_matches_nothing_and_warns() {
let mut w = world(edge_paths());
let (warnings, _) = w.run("top", &sdf("(IOPATH (0z a) q (1) (1))"));
assert_eq!((0..4).map(|i| w.rise_fall(i).0).sum::<u64>(), 0);
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(
warnings[0].contains("0z") || warnings[0].contains("T0z"),
"{warnings:?}"
);
}
#[test]
fn an_iopath_with_no_matching_path_warns_with_its_line() {
let mut w = world(vec![plain("sel", "zout")]);
let (warnings, _) = w.run("top", &sdf("(IOPATH a q (1) (1))"));
assert_eq!(w.rise_fall(0), (0, 0));
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("IOPATH a q"), "{warnings:?}");
assert!(warnings[0].starts_with("line 1:"), "{warnings:?}");
}
#[test]
fn a_port_bit_index_matches_the_whole_signal_or_that_bit() {
let bit = |lo| -> TerminalRef { (id("top.d.bus"), Some((lo, 1))) };
let paths = vec![
entry(
1,
"top.d",
bit(2),
term("top.d", "q"),
PathEdge::None,
PathCondition::Unconditional,
),
entry(
1,
"top.d",
bit(1),
term("top.d", "q"),
PathEdge::None,
PathCondition::Unconditional,
),
entry(
1,
"top.d",
term("top.d", "bus"),
term("top.d", "q"),
PathEdge::None,
PathCondition::Unconditional,
),
];
let mut w = world(paths);
w.run("top", &sdf("(IOPATH bus[2] q (1) (1))"));
assert_eq!(w.rise_fall(0).0, 1000, "the bit-2 window");
assert_eq!(w.rise_fall(1).0, 0, "the bit-1 window is another path");
assert_eq!(
w.rise_fall(2).0,
1000,
"a whole-signal terminal contains bit 2"
);
w.run("top", &sdf("(IOPATH bus q (7) (7))"));
assert_eq!(
(w.rise_fall(0).0, w.rise_fall(1).0, w.rise_fall(2).0),
(7000, 7000, 7000),
"no index matches any terminal of the signal"
);
}
#[test]
fn a_celltype_that_is_not_the_instances_module_warns_and_skips() {
let mut w = world(vec![plain("sel", "zout")]);
let text = "(DELAYFILE (CELL (CELLTYPE \"other\") (INSTANCE d) \
(DELAY (ABSOLUTE (IOPATH sel zout (1) (1))))))";
let (warnings, _) = w.run("top", text);
assert_eq!(w.rise_fall(0), (0, 0));
assert!(
warnings.iter().any(|m| m.contains("CELLTYPE other")),
"{warnings:?}"
);
}
#[test]
fn the_instance_path_is_relative_to_the_scope() {
let inner = entry(
3,
"top.g.d",
term("top.g.d", "sel"),
term("top.g.d", "zout"),
PathEdge::None,
PathCondition::Unconditional,
);
let mut w = world(vec![plain("sel", "zout"), inner]);
let (warnings, _) = w.run("top.g", &sdf("(IOPATH sel zout (1) (2))"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(w.rise_fall(0), (0, 0), "top.d is not under top.g");
assert_eq!(w.rise_fall(1), (1000, 2000));
}
#[test]
fn a_wildcard_instance_reaches_every_instance_of_the_celltype_below() {
let inner = entry(
3,
"top.g.d",
term("top.g.d", "sel"),
term("top.g.d", "zout"),
PathEdge::None,
PathCondition::Unconditional,
);
let mut w = world(vec![plain("sel", "zout"), inner]);
let text = "(DELAYFILE (CELL (CELLTYPE \"dut\") (INSTANCE *) \
(DELAY (ABSOLUTE (IOPATH sel zout (1) (2))))))";
let (warnings, _) = w.run("top", text);
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(w.rise_fall(0), (1000, 2000));
assert_eq!(w.rise_fall(1), (1000, 2000));
}
#[test]
fn an_unknown_instance_warns() {
let mut w = world(vec![plain("sel", "zout")]);
let text = "(DELAYFILE (CELL (CELLTYPE \"dut\") (INSTANCE nope) \
(DELAY (ABSOLUTE (IOPATH sel zout (1) (2))))))";
let (warnings, _) = w.run("top", text);
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("nope"), "{warnings:?}");
}
#[test]
fn an_unknown_scope_is_an_error() {
let mut w = world(vec![]);
let sel = Selector {
mtm: Mtm::Typ,
scale: Scale::IDENTITY,
};
let r = w.ann.annotate_text(
"(DELAYFILE)",
"t.sdf",
"top.zz",
&sel,
&mut vec![],
&mut vec![],
);
assert!(r.unwrap_err().contains("top.zz"));
}
#[test]
fn a_syntax_error_names_the_file_line_and_column() {
let mut w = world(vec![]);
let sel = Selector {
mtm: Mtm::Typ,
scale: Scale::IDENTITY,
};
let e = w
.ann
.annotate_text(
"(DELAYFILE\n (CELL",
"t.sdf",
"top",
&sel,
&mut vec![],
&mut vec![],
)
.unwrap_err();
assert!(e.starts_with("t.sdf:"), "{e}");
}
#[test]
fn device_annotates_every_path_to_an_output_or_to_the_named_one() {
let mut w = world(vec![plain("sel", "zout"), plain("a", "q")]);
w.run("top", &sdf("(DEVICE (3) (4))"));
assert_eq!(
(w.rise_fall(0), w.rise_fall(1)),
((3000, 4000), (3000, 4000))
);
w.run("top", &sdf("(DEVICE q (5) (6))"));
assert_eq!(w.rise_fall(0), (3000, 4000), "zout is not q");
assert_eq!(w.rise_fall(1), (5000, 6000));
}
fn prim_cell(v: [u64; 3]) -> Arc<Mutex<Delays>> {
Arc::new(Mutex::new(Delays::from_triple(v)))
}
fn device_world(paths: Vec<PathEntry>) -> (World, [Arc<Mutex<Delays>>; 3]) {
let cells = [
prim_cell([1, 1, 1]),
prim_cell([2, 2, 2]),
prim_cell([3, 3, 3]),
];
let mut device = DeviceDelays::new();
device.insert(
(InstanceId(1), id("top.d.zout")),
vec![cells[0].clone(), cells[1].clone()],
);
device.insert((InstanceId(1), id("top.d.q")), vec![cells[2].clone()]);
(world_with(paths, device), cells)
}
fn triples(cells: &[Arc<Mutex<Delays>>; 3]) -> [[u64; 3]; 3] {
[0, 1, 2].map(|i| cells[i].lock().unwrap().triple())
}
#[test]
fn device_with_no_specify_path_annotates_the_primitives_driving_the_outputs() {
let (mut w, cells) = device_world(vec![]);
let (warnings, log) = w.run("top", &sdf("(DEVICE (3) (4))"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(
triples(&cells),
[[3000, 4000, 3000]; 3],
"every primitive output, turn-off min(3, 4)"
);
assert!(
log.iter().any(|l| l.contains("3 primitive output(s)")),
"{log:?}"
);
w.run("top", &sdf("(DEVICE q (5) (6) (7))"));
assert_eq!(triples(&cells)[0], [3000, 4000, 3000], "zout is not q");
assert_eq!(triples(&cells)[2], [5000, 6000, 7000]);
}
#[test]
fn device_increment_on_primitives_adds_to_what_is_there() {
let (mut w, cells) = device_world(vec![]);
let text = "(DELAYFILE (TIMESCALE 1ns) (CELL (CELLTYPE \"dut\") (INSTANCE d) \
(DELAY (ABSOLUTE (DEVICE zout (5) (6) (7)))) \
(DELAY (INCREMENT (DEVICE zout (1) (1) (1))))))";
w.run("top", text);
assert_eq!(triples(&cells)[0], [6000, 7000, 8000]);
assert_eq!(triples(&cells)[1], [6000, 7000, 8000]);
assert_eq!(triples(&cells)[2], [3, 3, 3], "q was not named");
}
#[test]
fn device_prefers_specify_paths_over_primitives() {
let (mut w, cells) = device_world(vec![plain("sel", "zout")]);
let (warnings, _) = w.run("top", &sdf("(DEVICE (3) (4))"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(w.rise_fall(0), (3000, 4000));
assert_eq!(triples(&cells), [[1, 1, 1], [2, 2, 2], [3, 3, 3]]);
w.run("top", &sdf("(DEVICE q (8) (9))"));
assert_eq!(triples(&cells)[2], [8000, 9000, 8000]);
}
#[test]
fn device_with_nothing_to_annotate_warns() {
let (mut w, cells) = device_world(vec![]);
let (warnings, _) = w.run("top", &sdf("(DEVICE a (3) (4))"));
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("no primitive"), "{warnings:?}");
assert_eq!(triples(&cells), [[1, 1, 1], [2, 2, 2], [3, 3, 3]]);
}
#[test]
fn device_pulse_limits_on_a_primitive_are_reported() {
let (mut w, cells) = device_world(vec![]);
let (warnings, _) = w.run("top", &sdf("(DEVICE q ((3) (1) (2)) (4))"));
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("pulse limits"), "{warnings:?}");
assert_eq!(triples(&cells)[2], [3000, 4000, 3000]);
}
fn sdf_cell(celltype: &str, inst: &str, body: &str) -> String {
format!(
"(DELAYFILE (TIMESCALE 1ns) (CELL (CELLTYPE \"{celltype}\") (INSTANCE {inst}) \
(DELAY (ABSOLUTE {body}))))"
)
}
#[test]
fn device_naming_a_child_instance_annotates_that_modules_output_paths() {
let g_d = |from: &str, to: &str| {
entry(
3,
"top.g.d",
term("top.g.d", from),
term("top.g.d", to),
PathEdge::None,
PathCondition::Unconditional,
)
};
let mut w = world(vec![
plain("sel", "zout"),
plain("a", "q"),
g_d("sel", "zout"),
]);
let (warnings, _) = w.run("top", &sdf_cell("top", "", "(DEVICE d (3) (4))"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(w.rise_fall(0), (3000, 4000));
assert_eq!(w.rise_fall(1), (3000, 4000));
assert_eq!(w.rise_fall(2), (0, 0), "top.g.d is another instance");
let (warnings, _) = w.run("top", &sdf_cell("top", "", "(DEVICE d.q (5) (6))"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(w.rise_fall(0), (3000, 4000), "zout is not q");
assert_eq!(w.rise_fall(1), (5000, 6000));
w.run("top", &sdf_cell("top", "", "(DEVICE g.d (7) (8))"));
assert_eq!(w.rise_fall(2), (7000, 8000));
w.run("top", &sdf_cell("top", "", "(DEVICE g.d.zout (1) (2))"));
assert_eq!(w.rise_fall(2), (1000, 2000));
w.run("top", &sdf_cell("wrap", "g", "(DEVICE d (9) (9))"));
assert_eq!(w.rise_fall(2), (9000, 9000));
assert_eq!(w.rise_fall(0), (3000, 4000), "top.d untouched");
}
#[test]
fn device_naming_a_child_instance_falls_back_to_its_primitives() {
let (mut w, cells) = device_world(vec![]);
let (warnings, log) = w.run("top", &sdf_cell("top", "", "(DEVICE d.q (5) (6))"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(triples(&cells)[2], [5000, 6000, 5000]);
assert_eq!(triples(&cells)[0], [1, 1, 1], "zout is not q");
assert!(
log.iter()
.any(|l| l.contains("1 primitive output(s)") && l.contains("\"top.d\"")),
"the log names the annotated child: {log:?}"
);
w.run("top", &sdf_cell("top", "", "(DEVICE d (3) (4))"));
assert_eq!(triples(&cells), [[3000, 4000, 3000]; 3]);
}
#[test]
fn device_naming_an_instance_array_element() {
let mut w = world(vec![]);
let names = ["sel", "mode", "zout", "a", "q"];
w.ann
.instances
.push(instance(4, "top.arr[1]", "dut", &names, &[("bus", 4)]));
let arr1 = |from: &str, to: &str| {
entry(
4,
"top.arr[1]",
term("top.arr[1]", from),
term("top.arr[1]", to),
PathEdge::None,
PathCondition::Unconditional,
)
};
w.ann.paths = vec![arr1("sel", "zout"), arr1("a", "q")];
w.live.lock().unwrap().resize(
2,
PathTimes {
delays: PathDelaySet::uniform(0),
reject: PathDelaySet::uniform(0),
error: PathDelaySet::uniform(0),
floor: 0,
},
);
let (warnings, _) = w.run("top", &sdf_cell("top", "", "(DEVICE arr[1] (3) (4))"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(
(w.rise_fall(0), w.rise_fall(1)),
((3000, 4000), (3000, 4000))
);
let (warnings, _) = w.run("top", &sdf_cell("top", "", "(DEVICE arr\\[1\\].q (5) (6))"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(w.rise_fall(0), (3000, 4000));
assert_eq!(w.rise_fall(1), (5000, 6000));
}
#[test]
fn device_naming_nothing_warns() {
let mut w = world(vec![plain("sel", "zout")]);
let (warnings, _) = w.run("top", &sdf_cell("top", "", "(DEVICE nosuch (3) (4))"));
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("DEVICE nosuch"), "{warnings:?}");
let (warnings, _) = w.run("top", &sdf_cell("top", "", "(DEVICE d.nosuch (3) (4))"));
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("\"top.d\""), "{warnings:?}");
assert_eq!(w.rise_fall(0), (0, 0));
}
#[test]
fn annotations_apply_in_file_order() {
let mut w = world(vec![plain("sel", "zout")]);
let text = "(DELAYFILE (TIMESCALE 1ns) (CELL (CELLTYPE \"dut\") (INSTANCE d) \
(DELAY (ABSOLUTE (IOPATH sel zout (5) (5)))) \
(DELAY (INCREMENT (IOPATH sel zout (1) (2)))) \
(DELAY (ABSOLUTE (IOPATH sel zout (7) (8))))) \
(CELL (CELLTYPE \"dut\") (INSTANCE d) \
(DELAY (INCREMENT (IOPATH sel zout (1) (1))))))";
w.run("top", text);
assert_eq!(w.rise_fall(0), (8000, 9000), "7+1, 8+1");
}
#[test]
fn pathpulse_sets_limits_only_and_a_limit_above_the_delay_acts_as_the_delay() {
let mut w = world(vec![plain("sel", "zout")]);
w.run("top", &sdf("(IOPATH sel zout (50) (50))"));
w.run("top", &sdf("(PATHPULSE sel zout (20) (30))"));
let t = w.times(0);
assert_eq!(w.rise_fall(0), (50_000, 50_000), "the delay is untouched");
assert_eq!(t.limits(Bit4::Zero, Bit4::One), (50_000, 20_000, 30_000));
w.run("top", &sdf("(PATHPULSE sel zout (80) (90))"));
assert_eq!(
w.times(0).limits(Bit4::Zero, Bit4::One),
(50_000, 50_000, 50_000),
"limits above the delay act as the delay"
);
}
#[test]
fn pathpulsepercent_is_a_share_of_the_current_delay() {
let mut w = world(vec![plain("sel", "zout")]);
w.run("top", &sdf("(IOPATH sel zout (50) (50))"));
w.run("top", &sdf("(PATHPULSEPERCENT sel zout (20) (40))"));
assert_eq!(
w.times(0).limits(Bit4::Zero, Bit4::One),
(50_000, 10_000, 20_000)
);
}
#[test]
fn unannotated_sections_are_named_warnings() {
let mut w = world(vec![plain("sel", "zout")]);
let text = "(DELAYFILE (CELL (CELLTYPE \"dut\") (INSTANCE d) \
(DELAY (ABSOLUTE (INTERCONNECT a sel (1))))))";
let (warnings, _) = w.run("top", text);
let joined = warnings.join("\n");
assert!(joined.contains("INTERCONNECT"), "{joined}");
}
use crate::sdf_labels::InstLabels;
fn label_world(paths: Vec<PathEntry>, labels: InstLabels) -> World {
let mut plan = LabelPlan::default();
plan.insert(InstanceId(1), labels);
let mut w = world(paths);
w.ann = w.ann.with_labels(plan);
w
}
fn sdf_label(unit: &str, mode: &str, body: &str) -> String {
format!(
"(DELAYFILE (TIMESCALE {unit}) (CELL (CELLTYPE \"dut\") (INSTANCE d) \
(LABEL ({mode} {body}))))"
)
}
fn cap_path() -> InstLabels {
InstLabels::for_test(-9, &[("cap", 0.0)]).with_path(vec![0], &["1.4 * cap + 0.7"], &["cap"])
}
#[test]
fn lrm_32_4_3_a_label_reevaluates_the_path_delay_that_reads_the_specparam() {
let mut w = label_world(vec![plain("sel", "zout")], cap_path());
let (warnings, log) = w.run("top", &sdf_label("1ns", "ABSOLUTE", "(cap (2.5))"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(w.rise_fall(0), (4200, 4200));
assert!(
log.join("\n").contains("re-evaluated 1 specify path"),
"{log:?}"
);
vj_runtime::specparam::clear();
}
#[test]
fn a_label_increment_adds_to_the_annotated_value() {
let mut w = label_world(vec![plain("sel", "zout")], cap_path());
w.run("top", &sdf_label("1ns", "ABSOLUTE", "(cap (2.5))"));
w.run("top", &sdf_label("1ns", "INCREMENT", "(cap (1))"));
assert_eq!(w.rise_fall(0), (5600, 5600), "1.4 * 3.5 + 0.7");
w.run("top", &sdf_label("1ns", "ABSOLUTE", "(cap (1))"));
assert_eq!(
w.rise_fall(0),
(2100, 2100),
"ABSOLUTE replaces: 1.4 * 1 + 0.7"
);
vj_runtime::specparam::clear();
}
#[test]
fn a_first_increment_adds_to_the_source_value() {
let labels = InstLabels::for_test(-9, &[("cap", 2.0)]).with_path(
vec![0],
&["1.4 * cap + 0.7"],
&["cap"],
);
let mut w = label_world(vec![plain("sel", "zout")], labels);
w.run("top", &sdf_label("1ns", "INCREMENT", "(cap (0.5))"));
assert_eq!(w.rise_fall(0), (4200, 4200), "cap = 2 + 0.5");
vj_runtime::specparam::clear();
}
#[test]
fn a_label_value_is_converted_from_the_sdf_unit_to_the_module_unit() {
let mut w = label_world(vec![plain("sel", "zout")], cap_path());
w.run("top", &sdf_label("1ps", "ABSOLUTE", "(cap (2500))"));
assert_eq!(w.rise_fall(0), (4200, 4200));
assert_eq!(vj_runtime::specparam::get("test.cap"), Some(2.5));
vj_runtime::specparam::clear();
}
#[test]
fn an_empty_label_value_holds_and_a_triple_takes_the_selected_member() {
let mut w = label_world(vec![plain("sel", "zout")], cap_path());
w.run("top", &sdf_label("1ns", "ABSOLUTE", "(cap (1:2:3))"));
assert_eq!(
w.rise_fall(0),
(3500, 3500),
"typ is the default: 1.4 * 2 + 0.7"
);
let (warnings, _) = w.run("top", &sdf_label("1ns", "ABSOLUTE", "(cap ())"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(w.rise_fall(0), (3500, 3500), "held");
vj_runtime::specparam::clear();
}
#[test]
fn a_label_naming_no_specparam_warns_and_the_rest_apply() {
let mut w = label_world(vec![plain("sel", "zout")], cap_path());
let (warnings, _) = w.run(
"top",
&sdf_label("1ns", "ABSOLUTE", "(nosuch (1)) (cap (2.5))"),
);
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("LABEL nosuch"), "{warnings:?}");
assert_eq!(w.rise_fall(0), (4200, 4200));
vj_runtime::specparam::clear();
}
#[test]
fn a_label_in_an_instance_with_no_plan_warns() {
let mut w = world(vec![plain("sel", "zout")]);
let (warnings, _) = w.run("top", &sdf_label("1ns", "ABSOLUTE", "(cap (1))"));
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(
warnings[0].contains("no specparam information"),
"{warnings:?}"
);
}
#[test]
fn a_label_and_an_iopath_override_each_other_in_file_order() {
let mut w = label_world(vec![plain("sel", "zout")], cap_path());
w.run("top", &sdf("(IOPATH sel zout (9) (9))"));
assert_eq!(w.rise_fall(0), (9000, 9000));
w.run("top", &sdf_label("1ns", "ABSOLUTE", "(cap (2.5))"));
assert_eq!(w.rise_fall(0), (4200, 4200));
w.run("top", &sdf("(IOPATH sel zout (7) (7))"));
assert_eq!(w.rise_fall(0), (7000, 7000));
vj_runtime::specparam::clear();
}
#[test]
fn a_label_that_makes_a_path_delay_negative_clamps_it_to_zero() {
let labels =
InstLabels::for_test(-9, &[("cap", 0.0)]).with_path(vec![0], &["cap - 5"], &["cap"]);
let mut w = label_world(vec![plain("sel", "zout")], labels);
let (warnings, _) = w.run("top", &sdf_label("1ns", "ABSOLUTE", "(cap (2))"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(w.rise_fall(0), (0, 0));
vj_runtime::specparam::clear();
}
#[test]
fn a_two_value_delay_list_is_reevaluated_and_default_limits_follow() {
let labels = InstLabels::for_test(-9, &[("cap", 0.0)]).with_path(
vec![0, 1],
&["cap", "cap * 2"],
&["cap"],
);
let mut w = label_world(vec![plain("sel", "zout"), plain("sel", "zout")], labels);
{
let mut live = w.live.lock().unwrap();
live[1].reject = PathDelaySet::uniform(77);
}
w.run("top", &sdf_label("1ns", "ABSOLUTE", "(cap (3))"));
assert_eq!(w.rise_fall(0), (3000, 6000));
assert_eq!(
w.times(0).reject.of(Bit4::Zero, Bit4::One),
3000,
"followed"
);
assert_eq!(
w.times(1).reject.of(Bit4::Zero, Bit4::One),
77,
"explicit, kept"
);
vj_runtime::specparam::clear();
}
#[test]
fn a_failing_reevaluation_warns_and_keeps_the_delay() {
let labels = InstLabels::for_test(-9, &[("cap", 0.0)]).with_path(
vec![0],
&["cap + undeclared_name"],
&["cap"],
);
let mut w = label_world(vec![plain("sel", "zout")], labels);
w.run("top", &sdf("(IOPATH sel zout (9) (9))"));
let (warnings, _) = w.run("top", &sdf_label("1ns", "ABSOLUTE", "(cap (2))"));
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(
warnings[0].contains("cap + undeclared_name"),
"{warnings:?}"
);
assert_eq!(w.rise_fall(0), (9000, 9000));
vj_runtime::specparam::clear();
}
#[test]
fn a_label_with_no_readers_still_publishes_the_value() {
let labels = InstLabels::for_test(-9, &[("dhigh", 0.0)]);
let mut w = label_world(vec![plain("sel", "zout")], labels);
let (warnings, log) = w.run("top", &sdf_label("1ns", "ABSOLUTE", "(dhigh (60))"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(vj_runtime::specparam::get("test.dhigh"), Some(60.0));
assert_eq!(w.rise_fall(0), (0, 0));
assert!(log.join("\n").contains("dhigh"), "{log:?}");
vj_runtime::specparam::clear();
}
#[test]
fn a_label_reevaluates_a_timing_check_limit() {
let entries = vec![setuphold(clk(Ek::PosEdge, None), data(), 111, 222)];
let mut w = world_checks(entries, 0);
let labels =
InstLabels::for_test(-9, &[("cap", 0.0)]).with_limit(0, Some(1), "cap * 2", &["cap"]);
let mut plan = LabelPlan::default();
plan.insert(InstanceId(1), labels);
w.ann = w.ann.with_labels(plan);
let (warnings, _) = w.run("top", &sdf_label("1ns", "ABSOLUTE", "(cap (1.5))"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(sh(&w, 0), (111, 3000), "the setup is untouched");
vj_runtime::specparam::clear();
}
#[test]
fn a_label_that_makes_a_limit_negative_warns_where_it_cannot_be_kept() {
let setup = check_entry(
Sk::Setup,
CheckArgs::Setup(pair(clk(Ek::PosEdge, None), data(), 10)),
);
let mut w = world_checks(vec![setup], 0);
let labels =
InstLabels::for_test(-9, &[("cap", 0.0)]).with_limit(0, Some(0), "cap - 5", &["cap"]);
let mut plan = LabelPlan::default();
plan.insert(InstanceId(1), labels);
w.ann = w.ann.with_labels(plan);
let (warnings, _) = w.run("top", &sdf_label("1ns", "ABSOLUTE", "(cap (1))"));
assert_eq!(w.checks.declared(0, 0), Some(0));
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("$setup"), "{warnings:?}");
vj_runtime::specparam::clear();
}
#[test]
fn a_label_reaching_a_width_threshold_warns() {
let width = check_entry(
Sk::Width,
CheckArgs::Width(WidthArgs {
reference: clk(Ek::PosEdge, None),
limit: lim(1, 1),
threshold: Some(lim(2, 5)),
notifier: None,
}),
);
let mut w = world_checks(vec![width], 0);
let labels = InstLabels::for_test(-9, &[("cap", 0.0)]).with_limit(0, None, "cap", &["cap"]);
let mut plan = LabelPlan::default();
plan.insert(InstanceId(1), labels);
w.ann = w.ann.with_labels(plan);
let (warnings, _) = w.run("top", &sdf_label("1ns", "ABSOLUTE", "(cap (1))"));
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(
warnings[0].contains("threshold of a $width"),
"{warnings:?}"
);
vj_runtime::specparam::clear();
}
#[test]
fn a_label_a_gate_delay_reads_names_the_limitation() {
let labels = InstLabels::for_test(-9, &[("cap", 0.0)]).reading_in_planned_delay("cap");
let mut w = label_world(vec![plain("sel", "zout")], labels);
let (warnings, _) = w.run("top", &sdf_label("1ns", "ABSOLUTE", "(cap (1))"));
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(
warnings[0].contains("fixed when the design is planned"),
"{warnings:?}"
);
vj_runtime::specparam::clear();
}
#[test]
fn a_label_a_path_condition_reads_names_the_limitation() {
let labels = InstLabels::for_test(-9, &[("cap", 0.0)]);
let uid = labels.specparam_uid("cap");
let mut cond = plain("sel", "zout");
cond.condition = PathCondition::If(CondNode::Specparam(uid));
let mut w = label_world(vec![cond], labels);
let (warnings, _) = w.run("top", &sdf_label("1ns", "ABSOLUTE", "(cap (1))"));
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("condition"), "{warnings:?}");
vj_runtime::specparam::clear();
}
#[test]
fn a_label_a_specparam_initializer_reads_names_the_limitation() {
let labels =
InstLabels::for_test(-9, &[("cap", 0.0), ("twice", 0.0)]).derived("twice", &["cap"]);
let mut w = label_world(vec![plain("sel", "zout")], labels);
let (warnings, _) = w.run("top", &sdf_label("1ns", "ABSOLUTE", "(cap (1))"));
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("`twice`"), "{warnings:?}");
vj_runtime::specparam::clear();
}
#[test]
fn a_label_naming_a_refused_specparam_warns_with_the_reason() {
let labels = InstLabels::for_test(-9, &[("cap", 0.0)]).refused("cap", "is a string");
let mut w = label_world(vec![plain("sel", "zout")], labels);
let (warnings, _) = w.run("top", &sdf_label("1ns", "ABSOLUTE", "(cap (1))"));
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("`cap` is a string"), "{warnings:?}");
assert_eq!(vj_runtime::specparam::get("test.cap"), None);
}
#[test]
fn a_wildcard_label_annotates_every_matching_instance() {
let mut plan = LabelPlan::default();
plan.insert(
InstanceId(1),
InstLabels::for_test(-9, &[("cap", 0.0)]).keyed("top.d"),
);
plan.insert(
InstanceId(3),
InstLabels::for_test(-9, &[("cap", 0.0)]).keyed("top.g.d"),
);
let mut w = world(Vec::new());
w.ann = w.ann.with_labels(plan);
let text = "(DELAYFILE (TIMESCALE 1ns) (CELL (CELLTYPE \"dut\") (INSTANCE *) \
(LABEL (ABSOLUTE (cap (4))))))";
let (warnings, _) = w.run("top", text);
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(vj_runtime::specparam::get("top.d.cap"), Some(4.0));
assert_eq!(vj_runtime::specparam::get("top.g.d.cap"), Some(4.0));
vj_runtime::specparam::clear();
}
#[test]
fn a_delay_rounds_to_the_instances_precision() {
let mut w = world(vec![plain("sel", "zout")]);
w.ann.instances[1].precision_exp = -9;
w.run("top", &sdf("(IOPATH sel zout (1.3) (1.7))"));
assert_eq!(w.rise_fall(0), (1000, 2000));
}
#[test]
fn the_sdf_timescale_sets_the_unit() {
let mut w = world(vec![plain("sel", "zout")]);
let text = "(DELAYFILE (TIMESCALE 10 ps) (CELL (CELLTYPE \"dut\") (INSTANCE d) \
(DELAY (ABSOLUTE (IOPATH sel zout (3) (4))))))";
w.run("top", text);
assert_eq!(w.rise_fall(0), (30, 40));
}
use sv_lang::EdgeKind as Ek;
use sv_lang::SystemTimingCheckKind as Sk;
fn clk(edge: Ek, cond: Option<CondNode>) -> CheckEvent {
event("q", edge, cond)
}
fn data() -> CheckEvent {
event("a", Ek::None, None)
}
fn mode_sig() -> CondNode {
CondNode::Signal(term("top.d", "mode"))
}
fn not_mode() -> CondNode {
CondNode::Unary(CondUnaryOp::LogicalNot, Box::new(mode_sig()))
}
fn lrm_32_4_2_checks() -> Vec<CheckEntry> {
vec![
setuphold(clk(Ek::PosEdge, Some(mode_sig())), data(), 111, 222),
setuphold(clk(Ek::NegEdge, Some(not_mode())), data(), 111, 222),
setuphold(clk(Ek::BothEdges, None), data(), 111, 222),
]
}
fn sh(w: &World, k: usize) -> (i64, i64) {
(
w.checks.declared(k, 0).unwrap(),
w.checks.declared(k, 1).unwrap(),
)
}
#[test]
fn lrm_32_4_2_example_1_a_check_with_no_edge_or_condition_matches_every_condition() {
let mut w = world_checks(lrm_32_4_2_checks(), 0);
let (warnings, _) = w.run("top", &sdf_checks("(SETUPHOLD a q (3) (4))"));
assert_eq!(warnings, Vec::<String>::new());
for k in 0..3 {
assert_eq!(sh(&w, k), (3000, 4000), "check {k}");
}
}
#[test]
fn lrm_32_4_2_example_2_an_sdf_edge_matches_only_that_edge() {
let mut w = world_checks(lrm_32_4_2_checks(), 0);
let (warnings, _) = w.run("top", &sdf_checks("(SETUPHOLD a (posedge q) (3) (4))"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(sh(&w, 0), (3000, 4000));
assert_eq!(sh(&w, 1), (111, 222), "negedge clk &&& !mode");
assert_eq!(sh(&w, 2), (111, 222), "edge clk is not matched by posedge");
}
#[test]
fn lrm_32_4_2_example_3_an_sdf_edge_and_condition_must_both_match() {
let mut w = world_checks(lrm_32_4_2_checks(), 0);
let (warnings, _) = w.run(
"top",
&sdf_checks("(SETUPHOLD a (COND !mode (posedge q)) (3) (4))"),
);
for k in 0..3 {
assert_eq!(sh(&w, k), (111, 222), "check {k}");
}
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(
warnings[0].contains("no matching timing check"),
"{warnings:?}"
);
}
#[test]
fn a_matching_check_condition_annotates_that_check_only() {
let mut w = world_checks(lrm_32_4_2_checks(), 0);
w.run(
"top",
&sdf_checks(
"(SETUPHOLD a (COND mode (posedge q)) (3) (4)) \
(SETUPHOLD a (COND !mode (negedge q)) (5) (6))",
),
);
assert_eq!(sh(&w, 0), (3000, 4000));
assert_eq!(sh(&w, 1), (5000, 6000));
assert_eq!(sh(&w, 2), (111, 222), "edge clk has no condition to match");
}
#[test]
fn an_sdf_cond_does_not_match_an_unconditional_event() {
let mut w = world_checks(vec![setuphold(clk(Ek::PosEdge, None), data(), 111, 222)], 0);
let (warnings, _) = w.run(
"top",
&sdf_checks("(SETUPHOLD a (COND mode (posedge q)) (3) (4))"),
);
assert_eq!(sh(&w, 0), (111, 222));
assert_eq!(warnings.len(), 1, "{warnings:?}");
}
#[test]
fn scond_and_ccond_match_the_timestamp_and_timecheck_conditions() {
let mut with = setuphold(clk(Ek::PosEdge, None), data(), 111, 222);
let CheckArgs::SetupHold(a) = &mut with.args else {
unreachable!()
};
a.timestamp_cond = Some(mode_sig());
a.timecheck_cond = Some(not_mode());
let without = setuphold(clk(Ek::PosEdge, None), data(), 111, 222);
let mut w = world_checks(vec![with, without], 0);
let sdf_text = sdf_checks("(SETUPHOLD a (posedge q) (SCOND mode) (CCOND !mode) (3) (4))");
let (warnings, _) = w.run("top", &sdf_text);
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(sh(&w, 0), (3000, 4000));
assert_eq!(
sh(&w, 1),
(111, 222),
"no timestamp condition to be equal to"
);
let mut w2 = world_checks(vec![setuphold(clk(Ek::PosEdge, None), data(), 1, 2)], 0);
let (warnings, _) = w2.run(
"top",
&sdf_checks("(SETUPHOLD a (posedge q) (SCOND mode) (3) (4))"),
);
assert_eq!(sh(&w2, 0), (1, 2));
assert_eq!(warnings.len(), 1, "{warnings:?}");
}
#[test]
fn table_32_2_setup_leaves_the_setuphold_hold_limit_alone() {
let setup = check_entry(
Sk::Setup,
CheckArgs::Setup(pair(clk(Ek::PosEdge, None), data(), 10)),
);
let mut w = world_checks(
vec![setup, setuphold(clk(Ek::PosEdge, None), data(), 20, 30)],
0,
);
let (warnings, _) = w.run("top", &sdf_checks("(SETUP a (posedge q) (7))"));
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(w.checks.declared(0, 0), Some(7000));
assert_eq!(sh(&w, 1), (7000, 30), "x: the hold limit is untouched");
}
#[test]
fn table_32_2_hold_leaves_the_setuphold_setup_limit_alone() {
let hold = check_entry(
Sk::Hold,
CheckArgs::Hold(pair(clk(Ek::PosEdge, None), data(), 10)),
);
let mut w = world_checks(
vec![hold, setuphold(clk(Ek::PosEdge, None), data(), 20, 30)],
0,
);
w.run("top", &sdf_checks("(HOLD a (posedge q) (7))"));
assert_eq!(w.checks.declared(0, 0), Some(7000));
assert_eq!(sh(&w, 1), (20, 7000));
}
#[test]
fn table_32_2_setuphold_annotates_setup_hold_and_setuphold() {
let setup = check_entry(
Sk::Setup,
CheckArgs::Setup(pair(clk(Ek::PosEdge, None), data(), 10)),
);
let hold = check_entry(
Sk::Hold,
CheckArgs::Hold(pair(clk(Ek::PosEdge, None), data(), 11)),
);
let mut w = world_checks(
vec![
setup,
hold,
setuphold(clk(Ek::PosEdge, None), data(), 20, 30),
],
0,
);
w.run("top", &sdf_checks("(SETUPHOLD a (posedge q) (7) (8))"));
assert_eq!(w.checks.declared(0, 0), Some(7000), "$setup(v1)");
assert_eq!(w.checks.declared(1, 0), Some(8000), "$hold(v2)");
assert_eq!(sh(&w, 2), (7000, 8000));
}
#[test]
fn an_empty_value_holds_the_limit_and_a_triple_takes_the_typical_member() {
let mut w = world_checks(vec![setuphold(clk(Ek::PosEdge, None), data(), 20, 30)], 0);
w.run("top", &sdf_checks("(SETUPHOLD a (posedge q) () (1:2:3))"));
assert_eq!(sh(&w, 0), (20, 2000));
}
#[test]
fn table_32_2_recovery_removal_and_recrem() {
let rec = check_entry(
Sk::Recovery,
CheckArgs::Recovery(pair(clk(Ek::PosEdge, None), data(), 1)),
);
let rem = check_entry(
Sk::Removal,
CheckArgs::Removal(pair(clk(Ek::PosEdge, None), data(), 2)),
);
let recrem = check_entry(
Sk::RecRem,
CheckArgs::RecRem(RecRemArgs {
reference: clk(Ek::PosEdge, None),
data: data(),
recovery_limit: lim(2, 3),
removal_limit: lim(3, 4),
notifier: None,
timestamp_cond: None,
timecheck_cond: None,
delayed_reference: None,
delayed_data: None,
}),
);
let mut w = world_checks(vec![rec, rem, recrem], 0);
let at = |w: &World| {
(
w.checks.declared(0, 0).unwrap(),
w.checks.declared(1, 0).unwrap(),
sh(w, 2),
)
};
w.run("top", &sdf_checks("(RECOVERY a (posedge q) (5))"));
assert_eq!(
at(&w),
(5000, 2, (5000, 4)),
"$recovery(v1), $recrem(v1, x)"
);
w.run("top", &sdf_checks("(REMOVAL a (posedge q) (6))"));
assert_eq!(
at(&w),
(5000, 6000, (5000, 6000)),
"$removal(v1), $recrem(x, v1)"
);
w.run("top", &sdf_checks("(RECREM a (posedge q) (7) (8))"));
assert_eq!(at(&w), (7000, 8000, (7000, 8000)));
}
#[test]
fn table_32_2_skew_timeskew_and_fullskew() {
let skew = check_entry(
Sk::Skew,
CheckArgs::Skew(pair(
event("q", Ek::PosEdge, None),
event("a", Ek::PosEdge, None),
1,
)),
);
let timeskew = check_entry(
Sk::TimeSkew,
CheckArgs::TimeSkew(TimeSkewArgs {
reference: event("q", Ek::PosEdge, None),
data: event("a", Ek::PosEdge, None),
limit: lim(2, 2),
notifier: None,
event_based: None,
remain_active: None,
}),
);
let fullskew = check_entry(
Sk::FullSkew,
CheckArgs::FullSkew(FullSkewArgs {
reference: event("q", Ek::PosEdge, None),
data: event("a", Ek::PosEdge, None),
reference_first_limit: lim(2, 3),
data_first_limit: lim(3, 4),
notifier: None,
event_based: None,
remain_active: None,
}),
);
let mut w = world_checks(vec![skew, timeskew, fullskew], 0);
let at = |w: &World| {
(
w.checks.declared(0, 0).unwrap(),
w.checks.declared(1, 0).unwrap(),
sh(w, 2),
)
};
let (warnings, _) = w.run("top", &sdf_checks("(SKEW (posedge a) (posedge q) (5))"));
assert_eq!(at(&w), (1, 2, (3, 4)));
assert_eq!(warnings.len(), 1, "{warnings:?}");
w.run("top", &sdf_checks("(SKEW (posedge q) (posedge a) (5))"));
assert_eq!(at(&w), (5000, 5000, (3, 4)), "$skew(v1), $timeskew(v1)");
w.run(
"top",
&sdf_checks("(BIDIRECTSKEW (posedge q) (posedge a) (6) (7))"),
);
assert_eq!(at(&w), (5000, 5000, (6000, 7000)), "$fullskew(v1, v2)");
}
#[test]
fn table_32_2_width_period_and_nochange() {
let width = check_entry(
Sk::Width,
CheckArgs::Width(WidthArgs {
reference: clk(Ek::PosEdge, None),
limit: lim(1, 1),
threshold: Some(lim(2, 5)),
notifier: None,
}),
);
let period = check_entry(
Sk::Period,
CheckArgs::Period(PeriodArgs {
reference: clk(Ek::PosEdge, None),
limit: lim(1, 2),
notifier: None,
}),
);
let nochange = check_entry(
Sk::NoChange,
CheckArgs::NoChange(NoChangeArgs {
reference: clk(Ek::PosEdge, None),
data: data(),
start_edge_offset: lim(2, 3),
end_edge_offset: lim(3, 4),
notifier: None,
}),
);
let mut w = world_checks(vec![width, period, nochange], 0);
let (warnings, _) = w.run(
"top",
&sdf_checks(
"(WIDTH (posedge q) (6)) (PERIOD (posedge q) (7)) \
(NOCHANGE a (posedge q) (-1) (2))",
),
);
assert_eq!(warnings, Vec::<String>::new());
assert_eq!(w.checks.declared(0, 0), Some(6000));
assert_eq!(w.checks.declared(1, 0), Some(7000));
assert_eq!(
(w.checks.declared(2, 0), w.checks.declared(2, 1)),
(Some(-1000), Some(2000))
);
}
#[test]
fn a_check_of_another_instance_is_not_annotated() {
let mut other = setuphold(clk(Ek::PosEdge, None), data(), 111, 222);
other.instance = InstanceId(3);
let mut w = world_checks(vec![other], 0);
let (warnings, _) = w.run("top", &sdf_checks("(SETUPHOLD a (posedge q) (3) (4))"));
assert_eq!(sh(&w, 0), (111, 222));
assert_eq!(warnings.len(), 1, "{warnings:?}");
}
#[test]
fn a_negative_limit_is_warned_where_it_cannot_be_kept() {
let setup = check_entry(
Sk::Setup,
CheckArgs::Setup(pair(clk(Ek::PosEdge, None), data(), 10)),
);
let mut w = world_checks(
vec![setup, setuphold(clk(Ek::PosEdge, None), data(), 20, 30)],
0,
);
let (warnings, _) = w.run("top", &sdf_checks("(SETUP a (posedge q) (-2))"));
assert_eq!(
w.checks.declared(0, 0),
Some(0),
"$setup takes no negative limit"
);
assert_eq!(warnings.len(), 2, "{warnings:?}");
assert!(warnings[0].contains("$setup"), "{warnings:?}");
assert!(warnings[1].contains("$setuphold"), "{warnings:?}");
}
#[test]
fn a_z_edge_check_matches_nothing_and_warns() {
let mut w = world_checks(vec![setuphold(clk(Ek::PosEdge, None), data(), 111, 222)], 0);
let (warnings, _) = w.run("top", &sdf_checks("(SETUPHOLD a (0z q) (3) (4))"));
assert_eq!(sh(&w, 0), (111, 222));
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(
warnings[0].contains("0z") || warnings[0].contains("T0z"),
"{warnings:?}"
);
}
#[test]
fn a_bad_mtm_spec_is_an_error() {
let mut w = world(vec![]);
let call = SdfAnnotateCall {
file: "/nonexistent.sdf".into(),
scope: "top".into(),
config_file: None,
log_file: None,
mtm_spec: Some("BOGUS".into()),
scale_factors: None,
scale_type: None,
};
let e = w.ann.apply(&call).unwrap_err();
assert!(e.contains("BOGUS"), "{e}");
}
}