use std::collections::HashMap;
use std::path::Path;
use sv_lang::kinds::{ExpressionKind, SymbolKind};
use sv_lang::{EvalSession, Expression, SemNode, Symbol};
use vj_abi::bit4::Bit4;
use vj_codegen::error::LoweringError;
use vj_codegen::process_lower::SignalSlot;
use vj_sim::netlist::InstanceId;
use vj_sim::netlist::specify::{
CondNode, PathCondition, PathDelaySet, PathEdge, PathEntry, TimingModel,
};
use vj_sim::symid::{SymbolId, SymbolUid};
use crate::nets::{DriverKey, Resolution};
use crate::primitives::{Delays, Loc, bank_slot, loc_of};
use crate::run::{RunError, elab_to_run_error};
use crate::timing_options::TimingOptions;
#[cfg(test)]
pub(crate) use vj_exec::timing::specify::{CondEval, cond_eval_of};
pub(crate) use vj_exec::timing::specify::{CondPlan, LiveTimes, PathTimes};
use vj_exec::timing::specify::{
EdgeSel, Gate, Member, PathEdge as ExecEdge, RetimerPlan, SpecifyPlan,
};
pub(crate) fn timing_path_symbols<'d>(
infos: &[(InstanceId, Symbol<'d>)],
) -> Vec<(InstanceId, Symbol<'d>)> {
let mut out = Vec::new();
for &(id, 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::TimingPath {
out.push((id, inner));
}
}
}
}
out
}
fn path_delays_of(eval: &EvalSession<'_>, path: &Symbol<'_>) -> Result<PathDelaySet, String> {
let values = path_delay_values_of(eval, path.timing_path_delays(), path)?;
PathDelaySet::from_list(&values)
}
fn path_delay_values_of<'d>(
eval: &EvalSession<'_>,
exprs: impl Iterator<Item = Expression<'d>>,
scope: &Symbol<'_>,
) -> Result<Vec<u64>, String> {
let tick = vj_codegen::timescale::design_tick_exp(scope);
let mut out = Vec::new();
for e in exprs {
let e = if e.kind() == ExpressionKind::MinTypMax {
e.children()
.into_iter()
.filter_map(|n| n.as_expression())
.nth(1)
.ok_or_else(|| "a min:typ:max delay with no typical value".to_string())?
} else {
e
};
let c = e
.constant_value()
.or_else(|| eval.eval_constant(e))
.ok_or_else(|| "a delay that is not a constant".to_string())?;
out.push(constant_to_ticks(c, scope, tick)?);
}
Ok(out)
}
fn constant_to_ticks(
c: sv_lang::ConstantValue,
scope: &Symbol<'_>,
tick: i32,
) -> Result<u64, String> {
let v = match c {
sv_lang::ConstantValue::Real(f) => f,
c => c
.as_i64()
.ok_or_else(|| "a delay that is not a number".to_string())? as f64,
};
let v = if v.is_finite() && v < 0.0 { 0.0 } else { v };
vj_codegen::timescale::delay_ticks(v, scope, tick).map_err(|e| e.message)
}
#[cfg(test)]
pub(crate) fn discover<'d>(
eval: &EvalSession<'d>,
infos: &[(InstanceId, Symbol<'d>)],
path: &Path,
) -> Result<TimingModel, RunError> {
discover_with(eval, infos, path, &TimingOptions::default())
}
pub(crate) fn discover_with<'d>(
eval: &EvalSession<'d>,
infos: &[(InstanceId, Symbol<'d>)],
path: &Path,
options: &TimingOptions,
) -> Result<TimingModel, RunError> {
let mut model = TimingModel::new();
model.set_pulse_options(options.pulse_style, options.showcancelled);
model.seed_specparams(infos);
model
.discover(eval, infos)
.map_err(|e| elab_to_run_error(e, path))?;
for (id, sym) in timing_path_symbols(infos) {
let delays = path_delays_of(eval, &sym).map_err(|m| {
RunError::Lowering(LoweringError::unsupported_at(
&format!("specify path: {m}"),
&sym,
))
})?;
model.set_path_delays(id, &sym.uid(), delays);
}
model.apply_pulse_percent(options.reject_pct(), options.error_pct());
for (id, uid, reject, error) in pulse_limits_of(eval, &model, infos)? {
model.set_path_pulse_limits(id, &uid, reject, error);
}
crate::timingcheck::attach_limits(eval, &mut model, infos, path)?;
Ok(model)
}
fn terminal_name(e: Expression<'_>) -> Option<String> {
let e = if e.kind() == ExpressionKind::Conversion {
e.operand().unwrap_or(e)
} else {
e
};
let e = match e.kind() {
ExpressionKind::ElementSelect | ExpressionKind::RangeSelect => e.select_value()?,
_ => e,
};
if !vj_sim::symid::is_value_name(e.kind()) {
return None;
}
Some(e.referenced_symbol()?.name().to_string())
}
fn reject_truncated_literals<'d>(
root: &Symbol<'d>,
named: &HashMap<String, Symbol<'d>>,
trees: &[sv_lang::SyntaxTree],
) -> Result<(), String> {
use sv_lang::kinds::TokenKind;
let mut seen = std::collections::HashSet::new();
let mut work = vec![*root];
while let Some(sym) = work.pop() {
if !seen.insert(sym.name().to_string()) {
continue;
}
let Some(node) = trees.iter().find_map(|t| sym.syntax(t)) else {
continue;
};
let mut bad: Option<String> = None;
let mut refs: Vec<String> = Vec::new();
let mut after_base = false;
node.visit_with_tokens(|c| {
if let sv_lang::Child::Token(t) = c {
let based_digits = std::mem::replace(&mut after_base, false);
match t.kind() {
TokenKind::IntegerBase => after_base = true,
TokenKind::IntegerLiteral if based_digits => {}
TokenKind::IntegerLiteral => {
let digits = t.text().replace('_', "");
let unsized_decimal = digits.bytes().all(|b| b.is_ascii_digit());
if unsized_decimal
&& digits.parse::<u64>().map_or(true, |v| v > i32::MAX as u64)
{
bad = Some(t.text().to_string());
return sv_lang::Walk::Break;
}
}
TokenKind::Identifier => refs.push(t.text().to_string()),
_ => {}
}
}
sv_lang::Walk::Continue
});
if let Some(lit) = bad {
return Err(format!(
"`{}` uses the unsized decimal literal `{lit}`, which sv-lang truncates to 32 \
bits (the value would silently wrap); write it sized, e.g. `64'd{lit}`",
sym.name()
));
}
work.extend(refs.iter().filter_map(|r| named.get(r).copied()));
}
Ok(())
}
fn pulse_limits_of_specparam(
model: &TimingModel,
id: InstanceId,
sym: &Symbol<'_>,
plain: &[(String, sv_lang::ConstantValue)],
named: &HashMap<String, Symbol<'_>>,
trees: &[sv_lang::SyntaxTree],
) -> Result<(u64, u64), String> {
reject_truncated_literals(sym, named, trees)?;
let tick = vj_codegen::timescale::design_tick_exp(sym);
let first = model
.specparam(id, &sym.uid())
.cloned()
.or_else(|| sym.constant_value())
.ok_or_else(|| format!("`{}` has no constant value", sym.name()))?;
let reject = constant_to_ticks(first, sym, tick)?;
let node = trees
.iter()
.find_map(|t| sym.syntax(t))
.ok_or_else(|| format!("`{}` has no syntax node", sym.name()))?;
let decl = <sv_lang::nodes::SpecparamDeclaratorSyntax as sv_lang::AstNode>::cast(node)
.ok_or_else(|| {
format!(
"`{}`'s syntax node is `{:?}`, not a specparam declarator",
sym.name(),
node.kind()
)
})?;
let Some(second) = decl.value2() else {
return Ok((reject, reject));
};
let mut session = sv_lang::ScriptSession::new().map_err(|e| format!("{e:?}"))?;
for (name, v) in plain {
let decl = match v {
sv_lang::ConstantValue::Real(f) if f.is_finite() => format!("real {name} = {f:?};"),
other => match other.as_i64() {
Some(n) => format!("longint {name} = 64'sh{:x};", n as u64),
None => continue,
},
};
session.eval(&decl);
}
let value = session
.eval_expression(sv_lang::AstNode::syntax(&second))
.ok_or_else(|| {
format!(
"the error limit of `{}` is not a constant expression over literals, \
specparams and parameters",
sym.name()
)
})?;
Ok((reject, constant_to_ticks(value, sym, tick)?))
}
fn pulse_limits_of<'d>(
eval: &EvalSession<'d>,
model: &TimingModel,
infos: &[(InstanceId, Symbol<'d>)],
) -> Result<Vec<(InstanceId, SymbolId, u64, u64)>, RunError> {
let trees = eval.design().syntax_trees();
let paths = timing_path_symbols(infos);
let mut out = Vec::new();
for &(id, body) in infos {
let mut pulse: HashMap<String, Symbol<'d>> = HashMap::new();
let mut plain: Vec<(String, sv_lang::ConstantValue)> = Vec::new();
let mut params: Vec<(String, sv_lang::ConstantValue)> = Vec::new();
let mut note = |s: Symbol<'d>| {
let name = s.name().to_string();
if name.starts_with("PATHPULSE$") {
pulse.insert(name, s);
} else if let Some(v) = model.specparam(id, &s.uid()) {
plain.push((name, v.clone()));
}
};
for m in vj_sim::netlist::module_items(&body) {
match m.kind() {
SymbolKind::Specparam => note(m),
SymbolKind::Parameter => {
if let Some(v) = m.constant_value() {
params.push((m.name().to_string(), v));
}
}
SymbolKind::SpecifyBlock => {
for inner in m.members() {
if inner.kind() == SymbolKind::Specparam {
note(inner);
}
}
}
_ => {}
}
}
if pulse.is_empty() {
continue;
}
plain.extend(params);
let mut named: HashMap<String, Symbol<'d>> = HashMap::new();
for m in vj_sim::netlist::module_items(&body) {
match m.kind() {
SymbolKind::Specparam | SymbolKind::Parameter => {
named.insert(m.name().to_string(), m);
}
SymbolKind::SpecifyBlock => {
for inner in m.members() {
if inner.kind() == SymbolKind::Specparam {
named.insert(inner.name().to_string(), inner);
}
}
}
_ => {}
}
}
for &(pid, path) in &paths {
if pid != id {
continue;
}
let (Some(src), Some(dst)) = (
path.timing_path_inputs().next().and_then(terminal_name),
path.timing_path_outputs().next().and_then(terminal_name),
) else {
continue;
};
let Some(sym) = pulse
.get(&format!("PATHPULSE${src}${dst}"))
.or_else(|| pulse.get("PATHPULSE$"))
else {
continue;
};
let (reject, error) = pulse_limits_of_specparam(model, id, sym, &plain, &named, &trees)
.map_err(|m| {
RunError::Lowering(LoweringError::unsupported_at(
&format!("specify PATHPULSE$: {m}"),
sym,
))
})?;
out.push((id, path.uid(), reject, error));
}
}
Ok(out)
}
pub(crate) fn compile_cond_plan(
node: &CondNode,
own: &HashMap<SymbolId, SignalSlot>,
instance: InstanceId,
model: &TimingModel,
) -> Result<CondPlan, String> {
Ok(match node {
CondNode::Signal((uid, bits)) => {
let slot = *own.get(uid).ok_or_else(|| {
format!(
"state-dependent path condition references signal `{uid}`, which has no \
resolved signal slot in its own instance (unexpected: discovery already \
validated this reference's existence)"
)
})?;
CondPlan::Signal(loc_of(instance, slot), *bits)
}
CondNode::Specparam(uid) => {
let v = model.specparam(instance, uid).ok_or_else(|| {
format!(
"state-dependent path condition references specparam `{uid}`, which has no \
value in this design's TimingModel (unexpected: discovery already \
validated this reference's existence)"
)
})?;
let int = v.as_integer().ok_or_else(|| {
format!(
"state-dependent path condition's specparam `{uid}` is not an integer \
constant (e.g. a real)"
)
})?;
let bits = int.bits();
if bits.is_empty() && int.bit_width() > 0 {
return Err(format!(
"state-dependent path condition's specparam `{uid}` is too wide to expand \
bit-by-bit"
));
}
CondPlan::Const(bits.iter().map(|&b| bit4_of(b)).collect())
}
CondNode::Const(bits) => CondPlan::Const(bits.clone()),
CondNode::Unary(op, a) => {
CondPlan::Unary(*op, Box::new(compile_cond_plan(a, own, instance, model)?))
}
CondNode::Binary(op, a, b) => CondPlan::Binary(
*op,
Box::new(compile_cond_plan(a, own, instance, model)?),
Box::new(compile_cond_plan(b, own, instance, model)?),
),
CondNode::Ternary(c, t, f) => CondPlan::Ternary(
Box::new(compile_cond_plan(c, own, instance, model)?),
Box::new(compile_cond_plan(t, own, instance, model)?),
Box::new(compile_cond_plan(f, own, instance, model)?),
),
CondNode::Concat(items) => CondPlan::Concat(
items
.iter()
.map(|n| compile_cond_plan(n, own, instance, model))
.collect::<Result<_, _>>()?,
),
CondNode::Replication(n, item) => {
CondPlan::Replication(*n, Box::new(compile_cond_plan(item, own, instance, model)?))
}
})
}
fn compile_ifnone_siblings(
siblings: &[&CondNode],
own: &HashMap<SymbolId, SignalSlot>,
instance: InstanceId,
model: &TimingModel,
) -> Result<Option<Vec<CondPlan>>, String> {
if siblings.is_empty() {
return Ok(None);
}
Ok(Some(
siblings
.iter()
.map(|node| compile_cond_plan(node, own, instance, model))
.collect::<Result<Vec<_>, _>>()?,
))
}
fn bit4_of(b: sv_lang::Bit) -> Bit4 {
match b {
sv_lang::Bit::Zero => Bit4::Zero,
sv_lang::Bit::One => Bit4::One,
sv_lang::Bit::X => Bit4::X,
sv_lang::Bit::Z => Bit4::Z,
}
}
#[derive(Debug, Default)]
pub(crate) struct Plans {
pub(crate) retimers: Vec<RetimerPlan>,
pub(crate) paths: Vec<PathTimes>,
pub(crate) staged: HashMap<DriverKey, u32>,
}
impl Plans {
pub(crate) fn apply_floors(&mut self, floors: &[u64]) {
assert_eq!(
self.paths.len(),
floors.len(),
"one negative-timing floor per module path"
);
for (times, &floor) in self.paths.iter_mut().zip(floors) {
times.floor = floor;
}
}
}
fn exec_edge(edge: PathEdge) -> ExecEdge {
match edge {
PathEdge::None => ExecEdge::None,
PathEdge::Any => ExecEdge::Any,
PathEdge::Specific(sv_lang::EdgeKind::PosEdge) => ExecEdge::Specific(EdgeSel::Pos),
PathEdge::Specific(sv_lang::EdgeKind::NegEdge) => ExecEdge::Specific(EdgeSel::Neg),
PathEdge::Specific(sv_lang::EdgeKind::BothEdges) => ExecEdge::Specific(EdgeSel::Both),
PathEdge::Specific(sv_lang::EdgeKind::None) => unreachable!(
"a PathEdge::Specific is never constructed with EdgeKind::None -- \
PathEdge::from_accessors's own body is the only constructor, and it never does"
),
}
}
impl Plans {
pub(crate) fn spec(&self) -> SpecifyPlan {
SpecifyPlan {
retimers: self.retimers.clone(),
paths: self.paths.clone(),
}
}
}
fn destination_signal_kind<'d>(body: &Symbol<'d>, dst_uid: &SymbolId) -> Option<SymbolKind> {
for m in body.members() {
if m.kind() != SymbolKind::Port {
continue;
}
let internal = m.port_internal_symbol().unwrap_or(m);
if &internal.uid() == dst_uid {
return Some(internal.kind());
}
}
modport_port_in(body, dst_uid).and_then(|m| m.modport_port_internal_symbol().map(|s| s.kind()))
}
pub(crate) fn modport_terminals<'d>(
body: &Symbol<'d>,
signals: &HashMap<InstanceId, HashMap<SymbolId, SignalSlot>>,
body_instances: &HashMap<SymbolId, InstanceId>,
) -> HashMap<SymbolId, (InstanceId, SignalSlot)> {
let mut out = HashMap::new();
for m in body.members() {
let ip = if m.kind() == SymbolKind::Port {
m.port_internal_symbol().unwrap_or(m)
} else {
m
};
if ip.kind() != SymbolKind::InterfacePort {
continue;
}
let (_, Some(modport)) = ip.interface_port_connection() else {
continue;
};
for mp in modport.members() {
if mp.kind() != SymbolKind::ModportPort {
continue;
}
let Some(internal) = mp.modport_port_internal_symbol() else {
continue;
};
let Some(owner) = internal
.parent()
.and_then(|p| p.containing_instance())
.and_then(|b| body_instances.get(&b.uid()).copied())
else {
continue;
};
if let Some(&slot) = signals.get(&owner).and_then(|m| m.get(&internal.uid())) {
out.insert(mp.uid(), (owner, slot));
}
}
}
out
}
fn modport_port_in<'d>(body: &Symbol<'d>, uid: &SymbolId) -> Option<Symbol<'d>> {
let mut stack: Vec<SemNode<'d>> = Vec::new();
for m in body.members() {
if m.kind() == SymbolKind::ProceduralBlock
&& let Some(b) = vj_sim::process::cfg::proc_body(&m)
{
stack.push(b.as_sem_node());
}
}
while let Some(n) = stack.pop() {
if let Some(e) = n.as_expression()
&& vj_sim::symid::is_value_name(e.kind())
&& let Some(sym) = e.referenced_symbol()
&& sym.kind() == SymbolKind::ModportPort
&& &sym.uid() == uid
{
return Some(sym);
}
stack.extend(vj_sim::process::cfg::live_children(&n));
}
None
}
fn destination_is_inout<'d>(body: &Symbol<'d>, dst_uid: &SymbolId) -> bool {
body.members().any(|m| {
m.kind() == SymbolKind::Port
&& &m.port_internal_symbol().unwrap_or(m).uid() == dst_uid
&& m.port_direction() == sv_lang::ArgumentDirection::InOut
})
}
fn procedural_writes_of<'d>(body: &Symbol<'d>, dst_uid: &SymbolId) -> Vec<(Expression<'d>, bool)> {
let mut out = Vec::new();
let mut stack: Vec<SemNode<'d>> = Vec::new();
for m in body.members() {
if m.kind() == SymbolKind::ProceduralBlock
&& let Some(b) = vj_sim::process::cfg::proc_body(&m)
{
stack.push(b.as_sem_node());
}
}
while let Some(n) = stack.pop() {
if let Some(e) = n.as_expression()
&& e.kind() == ExpressionKind::Assignment
&& let Some(lhs) = e.left()
&& crate::nets::lvalue_roots(lhs)
.iter()
.any(|s| &s.uid() == dst_uid)
{
out.push((e, e.is_nonblocking() == Some(true)));
}
stack.extend(vj_sim::process::cfg::live_children(&n));
}
out
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn plan<'d>(
insts: &[(InstanceId, Symbol<'d>)],
signals: &HashMap<InstanceId, HashMap<SymbolId, SignalSlot>>,
res: &mut Resolution,
bank: InstanceId,
model: &TimingModel,
natural: &HashMap<(InstanceId, SymbolId), Delays>,
body_instances: &HashMap<SymbolId, InstanceId>,
prim_staging: &HashMap<(InstanceId, SymbolId), Loc>,
driven: &crate::nets::DrivenWindows,
) -> Result<Plans, String> {
let mut plans = Plans {
paths: model
.paths
.iter()
.map(|p| PathTimes {
delays: p.delays,
reject: p.reject_limit,
error: p.error_limit,
floor: 0,
})
.collect(),
..Plans::default()
};
let alloc = |res: &mut Resolution, width: u32| {
let slot = res.bank_widths.len() as u32;
res.bank_widths.push(width);
loc_of(bank, bank_slot(slot, width))
};
for &(id, body) in insts {
let own = &signals[&id];
let iface = modport_terminals(&body, signals, body_instances);
let cont_assigns =
vj_codegen::module::discover_cont_assigns(&body).map_err(|e| e.message)?;
let mut groups: Vec<(SymbolId, Vec<(usize, &PathEntry)>)> = Vec::new();
for (index, p) in model
.paths
.iter()
.enumerate()
.filter(|(_, p)| p.instance == id)
{
if p.inputs.len() != 1 || p.outputs.len() != 1 {
return Err(format!(
"a module path with {} input(s)/{} output(s) reached `specify::plan` \
un-desugared (unexpected: `TimingModel::discover` desugars every multi-\
terminal declaration into one-input/one-output entries before this ever \
runs)",
p.inputs.len(),
p.outputs.len()
));
}
let dst_uid = &p.outputs[0].0;
if !own.contains_key(dst_uid) && !iface.contains_key(dst_uid) {
continue;
}
match groups.iter_mut().find(|(u, _)| u == dst_uid) {
Some((_, members)) => members.push((index, p)),
None => groups.push((dst_uid.clone(), vec![(index, p)])),
}
}
for (dst_uid, group) in groups {
let (dst_owner, dst_slot) = own
.get(&dst_uid)
.map(|&s| (id, s))
.unwrap_or_else(|| iface[&dst_uid]);
let width = dst_slot.width;
if destination_signal_kind(&body, &dst_uid) == Some(SymbolKind::Net)
&& !destination_is_inout(&body, &dst_uid)
&& !driven.contains_key(&(id, dst_uid.clone()))
&& !prim_staging.contains_key(&(id, dst_uid.clone()))
&& !cont_assigns.iter().any(|ca| {
ca.net_target.is_some_and(|n| n.uid() == dst_uid)
|| ca
.assign_expr
.left()
.map(crate::nets::lvalue_roots)
.unwrap_or_default()
.iter()
.any(|r| r.uid() == dst_uid)
})
{
continue;
}
let mut bit_members: Vec<Vec<Member>> = vec![Vec::new(); width as usize];
for &(path_index, p) in &group {
let (in_uid, in_bits) = &p.inputs[0];
let (in_owner, in_slot) = own
.get(in_uid)
.map(|&s| (id, s))
.or_else(|| iface.get(in_uid).copied())
.ok_or_else(|| {
format!(
"specify path input `{in_uid}` has no resolved signal slot in its own \
instance (unexpected: discovery already validated this terminal's \
existence)"
)
})?;
let (src_lo, src_w) = in_bits.unwrap_or((0, in_slot.width));
if src_w == 0 || u64::from(src_lo) + u64::from(src_w) > u64::from(in_slot.width) {
return Err(format!(
"specify path input `{in_uid}` selects bits {src_lo}+{src_w} of a \
{}-bit net (out of range)",
in_slot.width
));
}
let (dst_lo, dst_w) = p.outputs[0].1.unwrap_or((0, width));
if dst_w == 0 || u64::from(dst_lo) + u64::from(dst_w) > u64::from(width) {
return Err(format!(
"specify path destination `{dst_uid}` selects bits {dst_lo}+{dst_w} of a \
{width}-bit net (out of range)"
));
}
let parallel = p.connection == sv_lang::TimingPathConnectionKind::Parallel;
if parallel && src_w != dst_w {
return Err(format!(
"parallel (`=>`) specify path `{in_uid}` ({src_w} bits) to `{dst_uid}` \
({dst_w} bits): LRM 30.4.2 requires the source and destination of a \
parallel connection to have the same number of bits"
));
}
let condition = match &p.condition {
PathCondition::Unconditional => None,
PathCondition::If(node) => {
Some(Gate::If(compile_cond_plan(node, own, id, model)?))
}
PathCondition::IfNone => {
let siblings: Vec<&CondNode> = group
.iter()
.filter(|(_, q)| {
q.inputs[0] == p.inputs[0] && q.outputs[0] == p.outputs[0]
})
.filter_map(|(_, q)| match &q.condition {
PathCondition::If(node) => Some(node),
_ => None,
})
.collect();
compile_ifnone_siblings(&siblings, own, id, model)?.map(Gate::IfNone)
}
};
for d in 0..dst_w {
let srcs = if parallel { d..d + 1 } else { 0..src_w };
for s in srcs {
bit_members[(dst_lo + d) as usize].push(Member {
input: loc_of(in_owner, in_slot),
input_lo: src_lo + s,
path: path_index,
style: p.pulse_style,
showcancelled: p.showcancelled,
edge: exec_edge(p.edge),
condition: condition.clone(),
});
}
}
}
let dst_kind = destination_signal_kind(&body, &dst_uid);
if iface.contains_key(&dst_uid) && dst_kind != Some(SymbolKind::Variable) {
return Err(format!(
"specify path destination `{dst_uid}` is a net (or is never written \
procedurally) of an interface port (LRM 25.6): only a variable the module \
writes is retimed through an interface port"
));
}
if dst_kind == Some(SymbolKind::Variable) {
let writes = procedural_writes_of(&body, &dst_uid);
if writes.is_empty() {
return Err(format!(
"specify path destination `{dst_uid}` is a variable with no procedural \
write the specify retimer could redirect"
));
}
for &(write, _) in &writes {
let lhs = write.left().ok_or_else(|| {
"internal: a procedural assignment resolved with no lvalue".to_string()
})?;
if !matches!(
lhs.kind(),
ExpressionKind::NamedValue
| ExpressionKind::HierarchicalValue
| ExpressionKind::ElementSelect
| ExpressionKind::RangeSelect
) {
return Err(format!(
"specify path destination `{dst_uid}`'s procedural driver writes a \
concatenation or member lvalue, not a whole variable or a bit/part \
select of it (not supported)"
));
}
}
let staging = alloc(res, width);
plans
.staged
.insert(DriverKey::ProcVar(id, dst_uid.clone()), staging.slot.slot);
let key = DriverKey::SpecifyPath(id, dst_uid.clone());
let dst = match res.slot(&key) {
Some(s) => loc_of(bank, bank_slot(s, dst_slot.width)),
None => loc_of(dst_owner, dst_slot),
};
for (bit, members) in bit_members.into_iter().enumerate() {
let kick = alloc(res, 32);
plans.retimers.push(RetimerPlan {
staging,
staging_lo: bit as u32,
dst,
dst_lo: bit as u32,
width: 1,
members,
kick,
natural: Delays::default(),
});
}
continue;
}
let mut drivers: Vec<DriverKey> = Vec::new();
for ca in &cont_assigns {
let roots: Vec<Symbol<'_>> = match ca.net_target {
Some(net) => vec![net],
None => ca
.assign_expr
.left()
.map(crate::nets::lvalue_roots)
.unwrap_or_default(),
};
if roots.iter().any(|r| r.uid() == dst_uid) {
drivers.push(DriverKey::Assign(id, ca.sym.uid(), dst_uid.clone()));
}
}
let prim = prim_staging.get(&(id, dst_uid.clone())).copied();
let mut inout_driver_slot = None;
let staging = match (drivers.len(), prim) {
(0, Some(staging)) => staging,
(1, None) => {
let staging = alloc(res, width);
let driver_key = drivers.into_iter().next().expect("one driver");
inout_driver_slot = res.slot(&driver_key);
plans.staged.insert(driver_key, staging.slot.slot);
staging
}
(n, p) => {
return Err(format!(
"specify path destination `{dst_uid}` has {n} plain-continuous-assign \
driver(s){} — a destination is retimed when it has EXACTLY one \
driver, a continuous assign or a primitive (a multi-driver net needs \
its own driver-contention design, not yet implemented)",
if p.is_some() {
" and a primitive driver"
} else {
""
}
));
}
};
let key = DriverKey::SpecifyPath(id, dst_uid.clone());
let dst = match res.slot(&key) {
Some(s) => loc_of(bank, bank_slot(s, dst_slot.width)),
None if destination_is_inout(&body, &dst_uid) => {
if let Some(s) = inout_driver_slot {
loc_of(bank, bank_slot(s, dst_slot.width))
} else {
return Err(format!(
"specify path destination `{dst_uid}` is an `inout` port, which is joined \
with the parent's net; retiming a bidirectional port's driver (LRM 30.4) \
is not implemented (make the destination an `output` port)"
));
}
}
None => loc_of(id, dst_slot),
};
let dest_natural = natural
.get(&(id, dst_uid.clone()))
.copied()
.unwrap_or_default();
for (bit, members) in bit_members.into_iter().enumerate() {
let kick = alloc(res, 32);
plans.retimers.push(RetimerPlan {
staging,
staging_lo: bit as u32,
dst,
dst_lo: bit as u32,
width: 1,
members,
kick,
natural: dest_natural,
});
}
}
}
Ok(plans)
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::path::Path;
use sv_lang::{Compilation, Design, Session, Symbol};
use vj_abi::bit4::Bit4;
use vj_codegen::process_lower::{SignalSlot, SignalSlotClass};
use vj_sim::netlist::InstanceId;
use vj_sim::symid::SymbolUid;
use super::{discover, plan};
use crate::nets::Resolution;
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 top_body(design: &Design) -> Symbol<'_> {
design
.top_instances()
.next()
.expect("one top instance")
.instance_body()
.expect("top instance has a body")
}
fn one_bit_slot(slot: u32) -> SignalSlot {
SignalSlot {
slot,
width: 1,
signed: false,
class: SignalSlotClass::Inline,
}
}
#[test]
fn discover_wraps_timing_model_discovery_through_run_error() {
let mut design = compile(
"module m(input a, output q);\n assign q = a;\n specify\n (a => q) = 10;\n \
endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let model = discover(&eval, &[(InstanceId(0), body)], Path::new("<test>"))
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(model.paths.len(), 1);
}
#[test]
fn discover_reports_a_direction_violation_as_a_lowering_run_error() {
let mut design = compile(
"module m(input b, input q3_in);\n specify\n (b => q3_in) = 3;\n endspecify\n\
endmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
match discover(&eval, &[(InstanceId(0), body)], Path::new("<test>")) {
Err(super::RunError::Lowering(_)) => {}
Err(other) => panic!("expected RunError::Lowering, got {other}"),
Ok(_) => panic!("a direction violation must surface as a named RunError"),
}
}
#[test]
fn discover_attaches_the_evaluated_delay_list_to_every_path() {
let mut design = compile(
"module m(input a, output q);\n assign q = a;\n specify\n (a => q) = (3, \
5);\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let model = discover(&eval, &[(InstanceId(0), body)], Path::new("<test>"))
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(model.paths.len(), 1);
let d = model.paths[0].delays;
assert_eq!(d.of(vj_abi::bit4::Bit4::Zero, vj_abi::bit4::Bit4::One), 3);
assert_eq!(d.of(vj_abi::bit4::Bit4::One, vj_abi::bit4::Bit4::Zero), 5);
}
#[test]
fn discover_resolves_an_edge_sensitive_path_to_a_procedural_reg_destination() {
let mut design = compile(
"module m(input clk, input d, output reg q);\n always @(posedge clk) q <= d;\n \
specify\n (posedge clk => (q +: d)) = (6, 8);\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let model = discover(&eval, &[(InstanceId(0), body)], Path::new("<test>"))
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(model.paths.len(), 1, "{:?}", model.paths);
assert_eq!(model.paths[0].inputs.len(), 1);
assert_eq!(model.paths[0].outputs.len(), 1);
let q_uid = body
.members()
.find(|s| s.name() == "q")
.expect("reg q")
.uid();
assert_eq!(model.paths[0].outputs[0].0, q_uid);
}
#[test]
fn discover_clamps_a_negative_path_delay_to_zero_per_30_5_1() {
let mut design = compile(
"module m(input a, output q);\n assign q = a;\n specify\n specparam NEG = \
-3;\n (a => q) = NEG;\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let model = discover(&eval, &[(InstanceId(0), body)], Path::new("<test>"))
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(model.paths.len(), 1);
let d = model.paths[0].delays;
for (from, to) in [
(vj_abi::bit4::Bit4::Zero, vj_abi::bit4::Bit4::One),
(vj_abi::bit4::Bit4::One, vj_abi::bit4::Bit4::Zero),
(vj_abi::bit4::Bit4::Zero, vj_abi::bit4::Bit4::Z),
] {
assert_eq!(
d.of(from, to),
0,
"{from:?} -> {to:?}: a negative path delay must clamp to 0, not error"
);
}
}
#[test]
fn plan_builds_one_retimer_for_a_simple_net_destination_path() {
let mut design = compile(
"module m(input a, output q);\n assign q = a;\n specify\n (a => q) = (3, \
5);\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let infos = [(InstanceId(0), body)];
let model = discover(&eval, &infos, Path::new("<test>"))
.unwrap_or_else(|e| panic!("discover failed: {e}"));
let q_uid = body
.members()
.find(|s| s.name() == "q")
.expect("net q")
.uid();
let a_uid = body
.members()
.find(|s| s.name() == "a")
.expect("net a")
.uid();
let mut own = std::collections::HashMap::new();
own.insert(q_uid, one_bit_slot(0));
own.insert(a_uid, one_bit_slot(1));
let mut signals = std::collections::HashMap::new();
signals.insert(InstanceId(0), own);
let mut res = Resolution::default();
let bank = InstanceId(u32::MAX);
let plans = plan(
&infos,
&signals,
&mut res,
bank,
&model,
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
)
.unwrap_or_else(|e| panic!("plan failed: {e}"));
assert_eq!(plans.retimers.len(), 1, "exactly one retimer");
assert_eq!(plans.staged.len(), 1, "the real driver is redirected");
assert_eq!(res.bank_widths.len(), 2);
}
#[test]
fn plan_builds_one_retimer_for_a_procedural_variable_destination_path() {
let mut design = compile(
"module m(input clk, input d, output reg q);\n always @(posedge clk) q <= d;\n \
specify\n (posedge clk => (q +: d)) = (6, 8);\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let infos = [(InstanceId(0), body)];
let model = discover(&eval, &infos, Path::new("<test>"))
.unwrap_or_else(|e| panic!("discover failed: {e}"));
let q_uid = body
.members()
.find(|s| s.name() == "q")
.expect("reg q")
.uid();
let clk_uid = body
.members()
.find(|s| s.name() == "clk")
.expect("net clk")
.uid();
let mut own = std::collections::HashMap::new();
own.insert(q_uid.clone(), one_bit_slot(0));
own.insert(clk_uid, one_bit_slot(1));
let mut signals = std::collections::HashMap::new();
signals.insert(InstanceId(0), own);
let mut res = Resolution::default();
let bank = InstanceId(u32::MAX);
let plans = plan(
&infos,
&signals,
&mut res,
bank,
&model,
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
)
.unwrap_or_else(|e| panic!("plan failed: {e}"));
assert_eq!(plans.retimers.len(), 1, "exactly one retimer");
assert_eq!(
plans
.staged
.get(&super::DriverKey::ProcVar(InstanceId(0), q_uid)),
Some(&0),
"the nonblocking write's own staging slot is keyed by DriverKey::ProcVar"
);
assert_eq!(res.bank_widths.len(), 2);
}
#[test]
fn plan_ignores_a_blocking_write_in_a_statically_dead_arm() {
let mut design = compile(
"module m #(parameter S = 1, parameter CL = 0)(input clk, input [7:0] d, output reg \
[7:0] q);\n always @(posedge clk) begin\n if (S > 1) q[7:8-CL] = d;\n else \
q <= d;\n end\n specify\n (posedge clk => (q +: d)) = (6, 8);\n endspecify\n\
endmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let infos = [(InstanceId(0), body)];
let model = discover(&eval, &infos, Path::new("<test>"))
.unwrap_or_else(|e| panic!("discover failed: {e}"));
let uid_of = |n: &str| {
body.members()
.find(|s| s.name() == n)
.expect("signal")
.uid()
};
let mut own = std::collections::HashMap::new();
own.insert(uid_of("q"), one_bit_slot(0));
own.insert(uid_of("clk"), one_bit_slot(1));
let mut signals = std::collections::HashMap::new();
signals.insert(InstanceId(0), own);
let mut res = Resolution::default();
let plans = plan(
&infos,
&signals,
&mut res,
InstanceId(u32::MAX),
&model,
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
)
.unwrap_or_else(|e| panic!("plan failed: {e}"));
assert_eq!(plans.retimers.len(), 1, "exactly one retimer");
}
#[test]
fn plan_redirects_a_variable_destination_written_by_a_blocking_assignment() {
let mut design = compile(
"module m(input clk, input d, output reg q);\n always @(posedge clk) q = d;\n \
specify\n (posedge clk => (q +: d)) = (6, 8);\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let infos = [(InstanceId(0), body)];
let model = discover(&eval, &infos, Path::new("<test>"))
.unwrap_or_else(|e| panic!("discover failed: {e}"));
let q_uid = body
.members()
.find(|s| s.name() == "q")
.expect("reg q")
.uid();
let clk_uid = body
.members()
.find(|s| s.name() == "clk")
.expect("net clk")
.uid();
let mut own = std::collections::HashMap::new();
own.insert(q_uid.clone(), one_bit_slot(0));
own.insert(clk_uid, one_bit_slot(1));
let mut signals = std::collections::HashMap::new();
signals.insert(InstanceId(0), own);
let mut res = Resolution::default();
let bank = InstanceId(u32::MAX);
let plans = plan(
&infos,
&signals,
&mut res,
bank,
&model,
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
)
.unwrap_or_else(|e| panic!("plan failed: {e}"));
assert_eq!(plans.retimers.len(), 1, "exactly one retimer");
assert!(
plans
.staged
.contains_key(&super::DriverKey::ProcVar(InstanceId(0), q_uid)),
"the blocking write's own staging slot is keyed by DriverKey::ProcVar"
);
}
#[test]
fn plan_redirects_a_variable_destination_written_by_two_nonblocking_statements() {
let mut design = compile(
"module m(input clk, input d, input e, output reg q);\n always @(posedge clk) \
q <= d;\n always @(posedge clk) q <= e;\n specify\n (posedge clk => (q +: \
d)) = (6, 8);\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let infos = [(InstanceId(0), body)];
let model = discover(&eval, &infos, Path::new("<test>"))
.unwrap_or_else(|e| panic!("discover failed: {e}"));
let q_uid = body
.members()
.find(|s| s.name() == "q")
.expect("reg q")
.uid();
let clk_uid = body
.members()
.find(|s| s.name() == "clk")
.expect("net clk")
.uid();
let mut own = std::collections::HashMap::new();
own.insert(q_uid.clone(), one_bit_slot(0));
own.insert(clk_uid, one_bit_slot(1));
let mut signals = std::collections::HashMap::new();
signals.insert(InstanceId(0), own);
let mut res = Resolution::default();
let bank = InstanceId(u32::MAX);
let plans = plan(
&infos,
&signals,
&mut res,
bank,
&model,
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
)
.unwrap_or_else(|e| panic!("plan failed: {e}"));
assert_eq!(plans.retimers.len(), 1, "exactly one retimer");
assert!(
plans
.staged
.contains_key(&super::DriverKey::ProcVar(InstanceId(0), q_uid)),
"the two writes' ONE shared staging slot is keyed by DriverKey::ProcVar"
);
}
#[test]
fn plan_mixes_a_module_path_delay_with_a_distributed_delay() {
let mut design = compile(
"module m(input a, output q);\n assign #2 q = a;\n specify\n (a => q) = (3, \
5);\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let infos = [(InstanceId(0), body)];
let model = discover(&eval, &infos, Path::new("<test>"))
.unwrap_or_else(|e| panic!("discover failed: {e}"));
let q_uid = body
.members()
.find(|s| s.name() == "q")
.expect("net q")
.uid();
let a_uid = body
.members()
.find(|s| s.name() == "a")
.expect("net a")
.uid();
let mut own = HashMap::new();
own.insert(q_uid.clone(), one_bit_slot(0));
own.insert(a_uid, one_bit_slot(1));
let mut signals = HashMap::new();
signals.insert(InstanceId(0), own);
let mut res = Resolution::default();
let bank = InstanceId(u32::MAX);
let trees = eval.design().syntax_trees();
let specify_dests = std::collections::HashSet::from([(InstanceId(0), q_uid.clone())]);
let prim_plans = crate::primitives::plan(
&eval,
&infos,
&signals,
&trees,
&mut res,
bank,
&specify_dests,
)
.unwrap_or_else(|e| panic!("primitives::plan failed: {e}"));
assert!(
prim_plans.staged.is_empty(),
"a specify-claimed destination's own `#delay` driver must not ALSO get \
primitives.rs's own delay line (it would race `PathRetimer`'s own commit): {:?}",
prim_plans.staged
);
let natural = prim_plans
.natural
.get(&(InstanceId(0), q_uid.clone()))
.unwrap_or_else(|| panic!("primitives::plan must expose this destination's own delay"));
assert_eq!(natural.of(&[Bit4::One]), 2, "the assign's own `#2` delay");
assert_eq!(
natural.of(&[Bit4::Zero]),
2,
"a single-value delay list: rise == fall"
);
let plans = plan(
&infos,
&signals,
&mut res,
bank,
&model,
&prim_plans.natural,
&HashMap::new(),
&prim_plans.specify_staging,
&HashMap::new(),
)
.unwrap_or_else(|e| panic!("plan failed: {e}"));
assert_eq!(plans.retimers.len(), 1, "exactly one retimer");
assert_eq!(
plans.staged.len(),
1,
"specify::plan, not primitives::plan, redirects the real driver"
);
assert_eq!(
plans.retimers[0].natural.of(&[Bit4::One]),
2,
"the distributed delay must reach the retimer unchanged"
);
}
#[test]
fn plan_merges_two_module_paths_sharing_one_destination_into_one_retimer() {
let mut design = compile(
"module m(input a, input b, output q);\n assign q = a | b;\n specify\n (a => \
q) = 10;\n (b => q) = 20;\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let infos = [(InstanceId(0), body)];
let model = discover(&eval, &infos, Path::new("<test>"))
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(model.paths.len(), 2, "two separate TimingPath declarations");
let q_uid = body
.members()
.find(|s| s.name() == "q")
.expect("net q")
.uid();
let a_uid = body
.members()
.find(|s| s.name() == "a")
.expect("net a")
.uid();
let b_uid = body
.members()
.find(|s| s.name() == "b")
.expect("net b")
.uid();
let mut own = std::collections::HashMap::new();
own.insert(q_uid, one_bit_slot(0));
own.insert(a_uid, one_bit_slot(1));
own.insert(b_uid, one_bit_slot(2));
let mut signals = std::collections::HashMap::new();
signals.insert(InstanceId(0), own);
let mut res = Resolution::default();
let bank = InstanceId(u32::MAX);
let plans = plan(
&infos,
&signals,
&mut res,
bank,
&model,
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
)
.unwrap_or_else(|e| panic!("plan failed: {e}"));
assert_eq!(
plans.retimers.len(),
1,
"one shared retiming stage, not two independent ones"
);
assert_eq!(plans.retimers[0].members.len(), 2, "both paths merged in");
assert_eq!(
res.bank_widths.len(),
2,
"still exactly one staging + one kick cell (shared, not duplicated per path)"
);
assert_eq!(
plans.staged.len(),
1,
"the one real driver, redirected once"
);
}
#[test]
fn plan_merges_a_fan_in_declaration_targeting_one_destination_too() {
let mut design = compile(
"module m(input a, input b, output q);\n assign q = a | b;\n specify\n (a, b \
*> q) = (3, 5);\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let infos = [(InstanceId(0), body)];
let model = discover(&eval, &infos, Path::new("<test>"))
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(
model.paths.len(),
2,
"one multi-input declaration desugars into two PathEntrys (30.4.6)"
);
let q_uid = body
.members()
.find(|s| s.name() == "q")
.expect("net q")
.uid();
let a_uid = body
.members()
.find(|s| s.name() == "a")
.expect("net a")
.uid();
let b_uid = body
.members()
.find(|s| s.name() == "b")
.expect("net b")
.uid();
let mut own = std::collections::HashMap::new();
own.insert(q_uid, one_bit_slot(0));
own.insert(a_uid, one_bit_slot(1));
own.insert(b_uid, one_bit_slot(2));
let mut signals = std::collections::HashMap::new();
signals.insert(InstanceId(0), own);
let mut res = Resolution::default();
let bank = InstanceId(u32::MAX);
let plans = plan(
&infos,
&signals,
&mut res,
bank,
&model,
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
)
.unwrap_or_else(|e| panic!("plan failed: {e}"));
assert_eq!(plans.retimers.len(), 1, "one shared retiming stage");
assert_eq!(
plans.retimers[0].members.len(),
2,
"both desugared paths merged in"
);
}
#[test]
fn run_design_lrm_30_5_3_example_1_rise_delay_selection() {
let src = "module dut(input a, input b, input trig, output y);\n assign y = \
trig;\n specify\n (a => y) = (6, 9);\n (b => y) = (5, 11);\n \
endspecify\nendmodule\n\nmodule top;\n reg a, b, trig;\n wire y;\n dut \
d(.a(a), .b(b), .trig(trig), .y(y));\n always @(y) $display(\"t=%0t \
y=%b\", $time, y);\n initial begin\n a = 0; b = 0; trig = 0;\n // \
t=5: A-recent scenario -- B transitions first, A later.\n #5 b = 1;\n \
#5 a = 1;\n #5 trig = 1;\n // rise, A most recent -> delay 6, commits \
at t=15+6=21.\n #15 trig = 0; a = 0; b = 0;\n // fall (prev scenario's \
tie-break state is irrelevant to a fall from an already-known 1), settle, \
then start the B-recent scenario fresh.\n #10 a = 1;\n #5 b = 1;\n \
#5 trig = 1;\n // rise, B most recent -> delay 5, commits at \
t(trig=1)+5.\n #15 trig = 0; a = 0; b = 0;\n #10;\n // tie \
scenario: a and b transition at the SAME time.\n begin a = 1; b = 1; \
end\n #5 trig = 1;\n // rise, tied -> smallest of (6,5) = 5.\n \
#20 $finish;\n end\nendmodule\n";
let file =
std::env::temp_dir().join(format!("vj-sdf-t6-example1-{}.sv", std::process::id()));
std::fs::write(&file, src).expect("write temp fixture");
let mut out = Vec::new();
let code = crate::run::run_design(&file, &mut out)
.unwrap_or_else(|e| panic!("run_design failed: {e}"));
let _ = std::fs::remove_file(&file);
assert_eq!(code, 0);
let stdout = String::from_utf8(out).expect("stdout is valid UTF-8");
assert_eq!(
stdout, "t=9 y=0\nt=21 y=1\nt=39 y=0\nt=55 y=1\nt=74 y=0\nt=85 y=1\n",
"A-recent rise=6, tied fall=min(9,11)=9, B-recent rise=5, tied fall=min(9,11)=9, \
tied rise=min(6,5)=5 -- LRM 30.5.3 Example 1's own stated selection numbers: {stdout}"
);
}
#[test]
fn run_design_state_dependent_path_indexed_part_select_conditions() {
let src = "module dut(input [7:0] s, input a, output y);\n assign y = a;\n specify\n \
if (s[3 +: 2] == 2'b10) (a => y) = 3;\n if (s[5 -: 2] == 2'b11) (a => y) \
= 7;\n endspecify\nendmodule\n\nmodule top;\n reg [7:0] s; reg a; wire y;\n \
dut d(.s(s), .a(a), .y(y));\n always @(y) $display(\"t=%0t y=%b\", $time, \
y);\n initial begin\n s = 8'b0001_0000; a = 0;\n #50 a = 1;\n #50 a \
= 0;\n #50 s = 8'b0011_1000;\n #50 a = 1;\n #50 a = 0;\n #50 \
$finish;\n end\nendmodule\n";
let file =
std::env::temp_dir().join(format!("vj-specify-indexed-cond-{}.sv", std::process::id()));
std::fs::write(&file, src).expect("write temp fixture");
let mut out = Vec::new();
let code = crate::run::run_design(&file, &mut out)
.unwrap_or_else(|e| panic!("run_design failed: {e}"));
let _ = std::fs::remove_file(&file);
assert_eq!(code, 0);
let stdout = String::from_utf8(out).expect("stdout is valid UTF-8");
assert_eq!(
stdout, "t=3 y=0\nt=53 y=1\nt=103 y=0\nt=207 y=1\nt=257 y=0\n",
"`+:` condition selects delay 3, then `-:` condition delay 7: {stdout}"
);
}
#[test]
fn run_design_path_terminal_indexed_part_select() {
let src = "module dut(input [4:0] a, output y);\n assign y = a[3] ^ a[4];\n specify\n \
(a[3 +: 1] => y) = 5;\n (a[4 -: 1] => y) = 9;\n endspecify\nendmodule\n\n\
module top;\n reg [4:0] a; wire y;\n dut d(.a(a), .y(y));\n always @(y) \
$display(\"t=%0t y=%b\", $time, y);\n initial begin\n a = 5'b00000;\n #10 \
a = 5'b01000;\n #20 a = 5'b11000;\n #30 $finish;\n end\nendmodule\n";
let file =
std::env::temp_dir().join(format!("vj-specify-indexed-term-{}.sv", std::process::id()));
std::fs::write(&file, src).expect("write temp fixture");
let mut out = Vec::new();
let code = crate::run::run_design(&file, &mut out)
.unwrap_or_else(|e| panic!("run_design failed: {e}"));
let _ = std::fs::remove_file(&file);
assert_eq!(code, 0);
let stdout = String::from_utf8(out).expect("stdout is valid UTF-8");
let lines: Vec<&str> = stdout.lines().collect();
for want in ["t=15 y=1", "t=39 y=0"] {
assert!(lines.contains(&want), "missing `{want}` in: {stdout}");
}
}
fn run_src_checked(tag: &str, src: &str) -> Result<String, String> {
let file = std::env::temp_dir().join(format!("vj-{tag}-{}.sv", std::process::id()));
std::fs::write(&file, src).expect("write temp fixture");
let mut out = Vec::new();
let res = crate::run::run_design(&file, &mut out);
let _ = std::fs::remove_file(&file);
res.map_err(|e| e.to_string())?;
Ok(String::from_utf8(out).expect("stdout is valid UTF-8"))
}
#[test]
fn run_design_path_vector_destination_parallel_part_select_source() {
let src = "module dut(input [4:0] a, input c, output [1:0] y);\n assign y = {a[4], \
a[3] ^ c};\n specify\n (a[3 +: 2] => y[1:0]) = 5;\n (c => y[0]) = \
9;\n endspecify\nendmodule\nmodule top;\n reg [4:0] a; reg c; wire [1:0] \
y;\n dut d(.a(a), .c(c), .y(y));\n always @(y) $display(\"t=%0t y=%b\", \
$time, y);\n initial begin\n a = 0; c = 0;\n #10 a = 5'b01000;\n \
#20 c = 1;\n #20 a = 5'b11000;\n #30 $finish;\n end\nendmodule\n";
let stdout = run_src_checked("specify-vec-par", src).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(stdout, "t=5 y=00\nt=15 y=01\nt=39 y=00\nt=55 y=10\n");
}
#[test]
fn run_design_path_vector_destination_full_connection_pairs_every_bit() {
let src = "module dut(input [1:0] a, input c, output [1:0] y);\n assign y = {a[1], \
a[0] ^ c};\n specify\n (a[1:0] *> y) = 3;\n (c => y[0]) = 8;\n \
endspecify\nendmodule\nmodule top;\n reg [1:0] a; reg c; wire [1:0] y;\n \
dut d(.a(a), .c(c), .y(y));\n always @(y) $display(\"t=%0t y=%b\", $time, \
y);\n initial begin\n a = 0; c = 0;\n #10 a = 2'b10;\n #20 begin c \
= 1; a = 2'b00; end\n #30 begin c = 0; a = 2'b10; end\n #30 \
$finish;\n end\nendmodule\n";
let stdout = run_src_checked("specify-vec-full", src).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(stdout, "t=3 y=00\nt=13 y=10\nt=33 y=01\nt=63 y=10\n");
}
#[test]
fn run_design_path_vector_destination_mismatches_are_refused() {
let unequal = "module dut(input [3:0] a, output [2:0] y);\n assign y = a[2:0];\n \
specify\n (a[3:2] => y) = 5;\n endspecify\nendmodule\nmodule \
top;\n reg [3:0] a; wire [2:0] y;\n dut d(.a(a), .y(y));\n initial \
#1 $finish;\nendmodule\n";
let err = run_src_checked("specify-vec-neq", unequal).expect_err("unequal widths");
assert!(err.contains("different width than destination"), "{err}");
let partial = "module dut(input [3:0] a, output [2:0] y);\n assign y = a[2:0];\n \
specify\n (a[0] => y[0]) = 5;\n endspecify\nendmodule\nmodule \
top;\n reg [3:0] a; wire [2:0] y;\n dut d(.a(a), .y(y));\n initial \
#1 $finish;\nendmodule\n";
run_src_checked("specify-vec-partial", partial).unwrap_or_else(|e| panic!("{e}"));
}
#[test]
fn run_design_lrm_30_5_3_example_2_adapted_three_way_tie() {
let src = "module dut(input a, input trig, output y);\n assign y = trig;\n \
specify\n (a => y) = (5, 9);\n (a => y) = (4, 8);\n (a => y) = \
(6, 5);\n endspecify\nendmodule\n\nmodule top;\n reg a, trig;\n wire \
y;\n dut d(.a(a), .trig(trig), .y(y));\n always @(y) $display(\"t=%0t \
y=%b\", $time, y);\n initial begin\n a = 0; trig = 0;\n #5 a = 1;\n \
#5 trig = 1;\n // rise, all three tied -> smallest of (5,4,6) = 4.\n \
#5 a = 0;\n #5 trig = 0;\n // fall, all three tied -> smallest of \
(9,8,5) = 5.\n #20 $finish;\n end\nendmodule\n";
let file =
std::env::temp_dir().join(format!("vj-sdf-t6-example2-{}.sv", std::process::id()));
std::fs::write(&file, src).expect("write temp fixture");
let mut out = Vec::new();
let code = crate::run::run_design(&file, &mut out)
.unwrap_or_else(|e| panic!("run_design failed: {e}"));
let _ = std::fs::remove_file(&file);
assert_eq!(code, 0);
let stdout = String::from_utf8(out).expect("stdout is valid UTF-8");
assert_eq!(
stdout, "t=5 y=0\nt=14 y=1\nt=25 y=0\n",
"LRM 30.5.3 Example 2's own stated numbers (rise=4, fall=5) among three paths tied \
on the same input: {stdout}"
);
}
#[test]
fn run_design_leaves_an_ordinary_sibling_nonblocking_write_unaffected_end_to_end() {
let src = "module dut(input clk, input d, output reg q, output reg q2);\n always \
@(posedge clk) begin\n q <= d;\n q2 <= d;\n end\n specify\n \
(posedge clk => (q +: d)) = (6, 8);\n endspecify\nendmodule\n\nmodule \
top;\n reg clk, d;\n wire q, q2;\n dut u(.clk(clk), .d(d), .q(q), \
.q2(q2));\n initial begin\n clk = 0;\n d = 0;\n #10 d = 1;\n \
#10 clk = 1;\n #0 $display(\"t=%0t q=%b q2=%b\", $time, q, q2);\n \
#5 $display(\"t=%0t q=%b q2=%b\", $time, q, q2);\n #2 $display(\"t=%0t \
q=%b q2=%b\", $time, q, q2);\n $finish;\n end\nendmodule\n";
let file = std::env::temp_dir().join(format!(
"vj-sdf-t4b-sibling-write-{}.sv",
std::process::id()
));
std::fs::write(&file, src).expect("write temp fixture");
let mut out = Vec::new();
let code = crate::run::run_design(&file, &mut out)
.unwrap_or_else(|e| panic!("run_design failed: {e}"));
let _ = std::fs::remove_file(&file);
assert_eq!(code, 0);
let stdout = String::from_utf8(out).expect("stdout is valid UTF-8");
assert_eq!(
stdout, "t=20 q=x q2=x\nt=25 q=x q2=1\nt=27 q=1 q2=1\n",
"q2 (ordinary) must commit immediately in the clock edge's own NBA region, while \
q (the specify-path destination) must stay x until its own retimed commit lands: \
{stdout}"
);
}
#[test]
fn path_retimer_never_reads_polarity_in_its_delay_computation() {
let tb = |path_decl: &str| -> String {
format!(
"module dut(input a, output q);\n assign q = a;\n specify\n {path_decl}\n \
endspecify\nendmodule\n\nmodule top;\n reg a;\n wire q;\n dut d(.a(a), \
.q(q));\n initial begin\n a = 0;\n #10 $display(\"t=%0t q=%b\", $time, \
q);\n a = 1;\n #2 $display(\"t=%0t q=%b\", $time, q);\n #2 \
$display(\"t=%0t q=%b\", $time, q);\n a = 0;\n #3 $display(\"t=%0t \
q=%b\", $time, q);\n #3 $display(\"t=%0t q=%b\", $time, q);\n \
$finish;\n end\nendmodule\n"
)
};
let run = |src: &str, tag: &str| -> String {
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let file = std::env::temp_dir().join(format!(
"vj-sdf-t4a-polarity-{}-{tag}-{n}.sv",
std::process::id()
));
std::fs::write(&file, src).expect("write temp fixture");
let mut out = Vec::new();
let code = crate::run::run_design(&file, &mut out)
.unwrap_or_else(|e| panic!("run_design({tag}) failed: {e}"));
let _ = std::fs::remove_file(&file);
assert_eq!(code, 0, "run_design({tag}) exited {code}");
String::from_utf8(out).expect("stdout is valid UTF-8")
};
let no_polarity = run(&tb("(a => q) = (3, 5);"), "none");
let positive = run(&tb("(a +=> q) = (3, 5);"), "pos");
let negative = run(&tb("(a -=> q) = (3, 5);"), "neg");
assert_eq!(
no_polarity, positive,
"a `+` polarity path must schedule identically to one with no polarity operator"
);
assert_eq!(
no_polarity, negative,
"a `-` polarity path must schedule identically to one with no polarity operator"
);
}
#[test]
fn plan_copies_the_discovered_edge_spec_into_every_member() {
let mut design = compile(
"module m(input clock, in, output out);\n assign out = in;\n specify\n \
(posedge clock => (out +: in)) = (10, 8);\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let infos = [(InstanceId(0), body)];
let model = discover(&eval, &infos, Path::new("<test>"))
.unwrap_or_else(|e| panic!("discover failed: {e}"));
let out_uid = body
.members()
.find(|s| s.name() == "out")
.expect("net out")
.uid();
let clock_uid = body
.members()
.find(|s| s.name() == "clock")
.expect("net clock")
.uid();
let mut own = std::collections::HashMap::new();
own.insert(out_uid, one_bit_slot(0));
own.insert(clock_uid, one_bit_slot(1));
let mut signals = std::collections::HashMap::new();
signals.insert(InstanceId(0), own);
let mut res = Resolution::default();
let bank = InstanceId(u32::MAX);
let plans = plan(
&infos,
&signals,
&mut res,
bank,
&model,
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
)
.unwrap_or_else(|e| panic!("plan failed: {e}"));
assert_eq!(plans.retimers.len(), 1);
assert_eq!(plans.retimers[0].members.len(), 1);
assert_eq!(
plans.retimers[0].members[0].edge,
vj_exec::timing::specify::PathEdge::Specific(vj_exec::timing::specify::EdgeSel::Pos)
);
}
#[test]
fn edge_source_expr_text_never_affects_scheduled_timing() {
let tb = |data_name: &str| -> String {
format!(
"module dut(input clock, input {data_name}, output out);\n assign out = \
{data_name};\n specify\n (posedge clock => (out +: {data_name})) = (10, \
8);\n endspecify\nendmodule\n\nmodule top;\n reg clock, d;\n wire out;\n \
dut u(.clock(clock), .{data_name}(d), .out(out));\n initial begin\n \
clock = 0;\n d = 0;\n #10 d = 1;\n #10 clock = 1;\n #0 \
$display(\"t=%0t out=%b\", $time, out);\n #9 $display(\"t=%0t out=%b\", \
$time, out);\n #2 $display(\"t=%0t out=%b\", $time, out);\n \
$finish;\n end\nendmodule\n"
)
};
let run = |src: &str, tag: &str| -> String {
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let file = std::env::temp_dir().join(format!(
"vj-sdf-t7-edge-source-expr-{}-{tag}-{n}.sv",
std::process::id()
));
std::fs::write(&file, src).expect("write temp fixture");
let mut out = Vec::new();
let code = crate::run::run_design(&file, &mut out)
.unwrap_or_else(|e| panic!("run_design({tag}) failed: {e}"));
let _ = std::fs::remove_file(&file);
assert_eq!(code, 0, "run_design({tag}) exited {code}");
String::from_utf8(out).expect("stdout is valid UTF-8")
};
let named_in = run(&tb("in"), "in");
let named_in2 = run(&tb("in2"), "in2");
assert_eq!(
named_in, named_in2,
"renaming the edge-sensitive suffix's own data-source expression's referenced \
signal must not change scheduled timing at all"
);
}
#[test]
fn run_design_edge_sensitive_path_gates_on_the_specified_edge() {
let src = "module dutP(input clock, input in, output out);\n assign out = in;\n \
specify\n (posedge clock => (out +: in)) = (10, 8);\n endspecify\n\
endmodule\n\nmodule dutN(input clock, input in, output out);\n assign out \
= in;\n specify\n (negedge clock => (out -: in)) = (10, 8);\n \
endspecify\nendmodule\n\nmodule dutA(input clock, input in, output \
out);\n assign out = in;\n specify\n (clock => (out : in)) = (10, \
8);\n endspecify\nendmodule\n\nmodule top;\n reg clock, in;\n wire outP, \
outN, outA;\n dutP dp(.clock(clock), .in(in), .out(outP));\n dutN \
dn(.clock(clock), .in(in), .out(outN));\n dutA da(.clock(clock), .in(in), \
.out(outA));\n always @(outP) $display(\"t=%0t outP=%b\", $time, \
outP);\n always @(outN) $display(\"t=%0t outN=%b\", $time, outN);\n \
always @(outA) $display(\"t=%0t outA=%b\", $time, outA);\n initial \
begin\n clock = 0; in = 0;\n #20 in = 1;\n #20 in = 0; clock = \
1;\n #20 in = 1; clock = 0;\n #20 $finish;\n end\nendmodule\n";
let file =
std::env::temp_dir().join(format!("vj-sdf-t7-edge-gate-{}.sv", std::process::id()));
std::fs::write(&file, src).expect("write temp fixture");
let mut out = Vec::new();
let code = crate::run::run_design(&file, &mut out)
.unwrap_or_else(|e| panic!("run_design failed: {e}"));
let _ = std::fs::remove_file(&file);
assert_eq!(code, 0);
let stdout = String::from_utf8(out).expect("stdout is valid UTF-8");
assert_eq!(
stdout,
"t=0 outP=0\nt=8 outN=0\nt=8 outA=0\nt=20 outP=1\nt=20 outN=1\nt=20 outA=1\nt=40 \
outN=0\nt=48 outP=0\nt=48 outA=0\nt=60 outP=1\nt=70 outN=1\nt=70 outA=1\n",
"LRM 30.4.3's own edge-gating rule, worked through this fixture's own timeline: \
{stdout}"
);
}
fn bits1(b: Bit4) -> Vec<Bit4> {
vec![b]
}
#[test]
fn eval_unary_logical_not_treats_x_and_z_as_true_per_30_4_4_1() {
use super::CondEval;
use vj_sim::netlist::specify::CondUnaryOp;
for (v, want) in [
(Bit4::Zero, Bit4::One),
(Bit4::One, Bit4::Zero),
(Bit4::X, Bit4::X),
(Bit4::Z, Bit4::X),
] {
let e = CondEval::Unary(CondUnaryOp::LogicalNot, Box::new(CondEval::Const(bits1(v))));
assert_eq!(e.eval(), bits1(want), "!{v:?}");
}
}
#[test]
fn eval_binary_logical_and_or_follow_the_4_state_absorbing_element_rule() {
use super::CondEval;
use vj_sim::netlist::specify::CondBinaryOp;
let c = |b: Bit4| CondEval::Const(bits1(b));
assert_eq!(
CondEval::Binary(
CondBinaryOp::LogicalAnd,
Box::new(c(Bit4::Zero)),
Box::new(c(Bit4::X))
)
.eval(),
bits1(Bit4::Zero)
);
assert_eq!(
CondEval::Binary(
CondBinaryOp::LogicalAnd,
Box::new(c(Bit4::One)),
Box::new(c(Bit4::X))
)
.eval(),
bits1(Bit4::X)
);
assert_eq!(
CondEval::Binary(
CondBinaryOp::LogicalOr,
Box::new(c(Bit4::One)),
Box::new(c(Bit4::X))
)
.eval(),
bits1(Bit4::One)
);
assert_eq!(
CondEval::Binary(
CondBinaryOp::LogicalOr,
Box::new(c(Bit4::Zero)),
Box::new(c(Bit4::X))
)
.eval(),
bits1(Bit4::X)
);
}
#[test]
fn eval_binary_equality_is_x_on_any_unknown_bit_else_a_real_bit_comparison() {
use super::CondEval;
use vj_sim::netlist::specify::CondBinaryOp;
let a = CondEval::Const(vec![Bit4::Zero, Bit4::Zero]);
let b = CondEval::Const(vec![Bit4::Zero, Bit4::Zero]);
assert_eq!(
CondEval::Binary(CondBinaryOp::Equality, Box::new(a), Box::new(b)).eval(),
bits1(Bit4::One)
);
let a = CondEval::Const(vec![Bit4::Zero, Bit4::Zero]);
let b = CondEval::Const(vec![Bit4::One, Bit4::Zero]);
assert_eq!(
CondEval::Binary(CondBinaryOp::Equality, Box::new(a), Box::new(b)).eval(),
bits1(Bit4::Zero)
);
let a = CondEval::Const(vec![Bit4::X, Bit4::Zero]);
let b = CondEval::Const(vec![Bit4::Zero, Bit4::Zero]);
assert_eq!(
CondEval::Binary(CondBinaryOp::Equality, Box::new(a), Box::new(b)).eval(),
bits1(Bit4::X)
);
}
#[test]
fn is_condition_true_treats_x_and_z_as_true_and_uses_the_lsb_of_a_vector() {
use super::CondEval;
assert!(CondEval::Const(bits1(Bit4::One)).is_condition_true());
assert!(!CondEval::Const(bits1(Bit4::Zero)).is_condition_true());
assert!(CondEval::Const(bits1(Bit4::X)).is_condition_true());
assert!(CondEval::Const(bits1(Bit4::Z)).is_condition_true());
assert!(!CondEval::Const(vec![Bit4::Zero, Bit4::One]).is_condition_true());
assert!(CondEval::Const(vec![Bit4::One, Bit4::Zero]).is_condition_true());
}
#[test]
fn eval_concat_is_msb_first_source_order_over_lsb_first_storage() {
use super::CondEval;
let msb_part = CondEval::Const(vec![Bit4::Zero, Bit4::One]);
let lsb_part = CondEval::Const(vec![Bit4::One]);
let e = CondEval::Concat(vec![msb_part, lsb_part]);
assert_eq!(e.eval(), vec![Bit4::One, Bit4::Zero, Bit4::One]);
}
#[test]
fn eval_replication_repeats_the_unit_n_times_lsb_first() {
use super::CondEval;
let unit = CondEval::Concat(vec![
CondEval::Const(bits1(Bit4::One)),
CondEval::Const(bits1(Bit4::Zero)),
]);
let e = CondEval::Replication(3, Box::new(unit));
assert_eq!(
e.eval(),
vec![
Bit4::Zero,
Bit4::One,
Bit4::Zero,
Bit4::One,
Bit4::Zero,
Bit4::One
]
);
}
#[test]
fn eval_ternary_selects_the_true_or_false_branch_and_merges_on_an_ambiguous_condition() {
use super::CondEval;
let t = CondEval::Const(bits1(Bit4::One));
let f = CondEval::Const(bits1(Bit4::Zero));
assert_eq!(
CondEval::Ternary(
Box::new(CondEval::Const(bits1(Bit4::One))),
Box::new(t.clone()),
Box::new(f.clone())
)
.eval(),
bits1(Bit4::One)
);
assert_eq!(
CondEval::Ternary(
Box::new(CondEval::Const(bits1(Bit4::Zero))),
Box::new(t.clone()),
Box::new(f.clone())
)
.eval(),
bits1(Bit4::Zero)
);
assert_eq!(
CondEval::Ternary(
Box::new(CondEval::Const(bits1(Bit4::X))),
Box::new(t.clone()),
Box::new(t)
)
.eval(),
bits1(Bit4::One)
);
assert_eq!(
CondEval::Ternary(
Box::new(CondEval::Const(bits1(Bit4::X))),
Box::new(f),
Box::new(CondEval::Const(bits1(Bit4::One)))
)
.eval(),
bits1(Bit4::X)
);
}
#[test]
fn ifnone_gate_is_false_whenever_any_sibling_if_condition_is_true() {
use std::collections::HashMap;
use super::{TimingModel, compile_ifnone_siblings, cond_eval_of};
use vj_exec::timing::specify::RuntimeGate;
use vj_sim::netlist::specify::CondNode;
let own = HashMap::new();
let instance = InstanceId(0);
let model = TimingModel::new();
for (siblings_true, want_eligible, label) in [
(
[Bit4::Zero, Bit4::Zero],
true,
"every sibling false -> ifnone is the default -> eligible",
),
(
[Bit4::One, Bit4::Zero],
false,
"first sibling true -> ifnone must be excluded",
),
(
[Bit4::Zero, Bit4::One],
false,
"second sibling true -> ifnone must be excluded (ANY \
sibling firing excludes it, not just the first)",
),
(
[Bit4::One, Bit4::One],
false,
"both siblings true -> still excluded",
),
] {
let siblings: Vec<CondNode> = siblings_true
.iter()
.map(|&b| CondNode::Const(vec![b]))
.collect();
let refs: Vec<&CondNode> = siblings.iter().collect();
let compiled = compile_ifnone_siblings(&refs, &own, instance, &model)
.expect("const-only siblings compile")
.expect("two `If` siblings -> Some per 30.4.4.4");
let gate = RuntimeGate::IfNone(
compiled
.iter()
.map(|c| cond_eval_of(c, &HashMap::new()))
.collect(),
);
assert_eq!(
gate.is_true(),
want_eligible,
"{label}: siblings={siblings_true:?}"
);
}
assert!(
compile_ifnone_siblings(&[], &own, instance, &model)
.unwrap()
.is_none()
);
}
#[test]
fn select_delay_ifnone_gate_uses_each_siblings_own_lsb_rule_not_raw_or_truthiness() {
use super::CondEval;
use vj_exec::timing::specify::RuntimeGate;
let sel = CondEval::Const(vec![Bit4::Zero, Bit4::One]);
assert!(
!sel.is_condition_true(),
"sel=2'b10's own `if (sel)` condition must be false per the LSB rule"
);
let gate = RuntimeGate::IfNone(vec![sel]);
assert!(
gate.is_true(),
"ifnone must fire when its one sibling's own LSB-rule condition is \
false, even though that sibling's raw value has a set bit"
);
}
#[test]
fn run_design_ifnone_fires_when_a_raw_multibit_sibling_condition_has_lsb_zero() {
let src = "module m(input in1, output out, input [1:0] sel);\n assign out = \
in1;\n specify\n if (sel) (in1=>out) = (5,5);\n ifnone \
(in1=>out) = (40,40);\n endspecify\nendmodule\nmodule top;\n reg \
in1;\n reg [1:0] sel;\n wire out;\n m dut(.in1(in1), .out(out), \
.sel(sel));\n always @(out) $display(\"t=%0t out=%b\", $time, \
out);\n initial begin\n in1 = 0; sel = 2'b10;\n #100 in1 = \
1;\n #50 $finish;\n end\nendmodule\n";
let file =
std::env::temp_dir().join(format!("vj-sdf-t8-ifnone-lsb-{}.sv", std::process::id()));
std::fs::write(&file, src).expect("write temp fixture");
let mut out = Vec::new();
let code = crate::run::run_design(&file, &mut out)
.unwrap_or_else(|e| panic!("run_design failed: {e}"));
let _ = std::fs::remove_file(&file);
assert_eq!(code, 0);
let stdout = String::from_utf8(out).expect("stdout is valid UTF-8");
assert_eq!(
stdout, "t=40 out=0\nt=140 out=1\n",
"ifnone must fire on sel=2'b10 (if(sel)'s own LSB is 0) for EVERY out \
transition, including the initial x->0 settle: {stdout}"
);
}
#[test]
fn run_design_if_and_ifnone_gate_delay_selection_like_the_lrm_alu_example() {
let src = "module m(input i1, input i2, output o1, input [1:0] opcode);\n assign o1 = \
(opcode == 2'b01) ? i1 : (opcode == 2'b10) ? i2 : (i1 ^ i2);\n specify\n \
if (opcode == 2'b01) (i1 => o1) = (5, 9);\n if (opcode == 2'b10) (i2 => \
o1) = (5, 9);\n ifnone (i2 => o1) = (15, 15);\n endspecify\nendmodule\n\
module top;\n reg i1, i2;\n reg [1:0] opcode;\n wire o1;\n m dut(.i1(i1), \
.i2(i2), .o1(o1), .opcode(opcode));\n always @(o1) $display(\"t=%0t \
o1=%b\", $time, o1);\n initial begin\n i1 = 0; i2 = 0; opcode = 2'b00;\n \
#10 opcode = 2'b10; i2 = 1;\n #20 opcode = 2'b00; i2 = 0;\n #30 i2 = \
1;\n #50 $finish;\n end\nendmodule\n";
let file =
std::env::temp_dir().join(format!("vj-sdf-t8-alu-ifnone-{}.sv", std::process::id()));
std::fs::write(&file, src).expect("write temp fixture");
let mut out = Vec::new();
let code = crate::run::run_design(&file, &mut out)
.unwrap_or_else(|e| panic!("run_design failed: {e}"));
let _ = std::fs::remove_file(&file);
assert_eq!(code, 0);
let stdout = String::from_utf8(out).expect("stdout is valid UTF-8");
assert_eq!(
stdout, "t=15 o1=1\nt=45 o1=0\nt=75 o1=1\n",
"If/ifnone gating must select the condition-appropriate delay at each \
transition: {stdout}"
);
}
fn run_src(tag: &str, src: &str) -> String {
let file = std::env::temp_dir().join(format!("vj-sdf-t10-{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(&file, &mut out)
.unwrap_or_else(|e| panic!("run_design failed: {e}"));
let _ = std::fs::remove_file(&file);
assert_eq!(code, 0);
String::from_utf8(out).expect("stdout is valid UTF-8")
}
#[test]
fn run_design_an_unmatched_pathpulse_leaves_the_default_limits_alone() {
let stdout = run_src(
"unmatched",
"module m(input a, input b, output y);\n assign y = a;\n specify\n (a => y) = \
10;\n specparam PATHPULSE$b$y = (2, 6);\n endspecify\nendmodule\nmodule top;\n \
reg a, b;\n wire y;\n m dut(.a(a), .b(b), .y(y));\n always @(y) \
$display(\"t=%0t y=%b\", $time, y);\n initial begin\n a = 0; b = 0;\n #50 \
a = 1;\n #4 a = 0;\n #50 $finish;\n end\nendmodule\n",
);
assert_eq!(
stdout, "t=10 y=0\n",
"default full-inertial rejection: {stdout}"
);
}
#[test]
fn run_design_a_multi_path_declaration_is_named_by_its_first_terminal_only() {
let stdout = run_src(
"multi",
"module m(input clr, input pre, output q);\n assign q = clr | pre;\n specify\n \
(clr, pre *> q) = 10;\n specparam PATHPULSE$clr$q = (2, 6);\n specparam \
PATHPULSE$pre$q = (0, 0);\n endspecify\nendmodule\nmodule top;\n reg clr, pre;\n \
wire q;\n m dut(.clr(clr), .pre(pre), .q(q));\n always @(q) $display(\"t=%0t \
q=%b\", $time, q);\n initial begin\n clr = 0; pre = 0;\n #50 clr = 1;\n #4 \
clr = 0;\n #46 pre = 1;\n #4 pre = 0;\n #50 $finish;\n end\nendmodule\n",
);
assert_eq!(
stdout, "t=10 q=0\nt=60 q=x\nt=64 q=0\nt=110 q=x\nt=114 q=0\n",
"both desugared paths share (2, 6); the (0, 0) naming `pre` is ignored: {stdout}"
);
}
#[test]
fn run_design_pulse_width_boundaries_follow_the_reject_and_error_limits() {
let stdout = run_src(
"bounds",
"module m(input a, output y);\n assign y = a;\n specify\n (a => y) = 10;\n \
specparam PATHPULSE$a$y = (3, 7);\n endspecify\nendmodule\nmodule top;\n reg a;\n \
wire y;\n m dut(.a(a), .y(y));\n always @(y) $display(\"t=%0t y=%b\", $time, \
y);\n initial begin\n a = 0;\n #100 a = 1; #2 a = 0;\n #100 a = 1; #3 a = \
0;\n #100 a = 1; #6 a = 0;\n #100 a = 1; #7 a = 0;\n #100 $finish;\n \
end\nendmodule\n",
);
assert_eq!(
stdout,
"t=10 y=0\n\
t=212 y=x\nt=215 y=0\n\
t=315 y=x\nt=321 y=0\n\
t=421 y=1\nt=428 y=0\n",
"width 2 vanishes; 3 and 6 go through X; 7 passes: {stdout}"
);
}
#[test]
fn run_design_a_longer_distributed_delay_floors_both_pulse_limits() {
for pulse in ["", "specparam PATHPULSE$a$y = (2, 6);"] {
let stdout = run_src(
"floor",
&format!(
"module m(input a, output y);\n assign #20 y = a;\n specify\n (a => y) = \
10;\n {pulse}\n endspecify\nendmodule\nmodule top;\n reg a;\n wire \
y;\n m dut(.a(a), .y(y));\n always @(y) $display(\"t=%0t y=%b\", $time, \
y);\n initial begin\n a = 0;\n #100 a = 1;\n #12 a = 0;\n #100 \
a = 1;\n #25 a = 0;\n #100 $finish;\n end\nendmodule\n"
),
);
assert_eq!(
stdout, "t=20 y=0\nt=232 y=1\nt=257 y=0\n",
"width 12 < 20 vanishes, width 25 passes ({pulse:?}): {stdout}"
);
}
}
fn uniform_limits(
model: &vj_sim::netlist::specify::TimingModel,
body: Symbol<'_>,
input: &str,
) -> (u64, u64) {
let uid = body
.members()
.find(|s| s.name() == input)
.unwrap_or_else(|| panic!("no signal `{input}`"))
.uid();
let entry = model
.paths
.iter()
.find(|p| p.inputs[0].0 == uid)
.unwrap_or_else(|| panic!("no path from `{input}`"));
let pairs = [
(Bit4::Zero, Bit4::One),
(Bit4::One, Bit4::Zero),
(Bit4::Zero, Bit4::Z),
(Bit4::Z, Bit4::One),
(Bit4::One, Bit4::Z),
(Bit4::Z, Bit4::Zero),
(Bit4::Zero, Bit4::X),
(Bit4::X, Bit4::One),
(Bit4::One, Bit4::X),
(Bit4::X, Bit4::Zero),
(Bit4::X, Bit4::Z),
(Bit4::Z, Bit4::X),
];
let (r, e) = (
entry.reject_limit.of(pairs[0].0, pairs[0].1),
entry.error_limit.of(pairs[0].0, pairs[0].1),
);
for (from, to) in pairs {
assert_eq!(entry.reject_limit.of(from, to), r, "{from:?}->{to:?}");
assert_eq!(entry.error_limit.of(from, to), e, "{from:?}->{to:?}");
}
(r, e)
}
fn discover_src(src: &str) -> Result<(), super::RunError> {
let mut design = compile(src);
let eval = design.eval_session();
let body = top_body(eval.design());
let result = discover(&eval, &[(InstanceId(0), body)], Path::new("<test>"));
result.map(|_| ())
}
fn limits_in(src: &str, inputs: &[&str]) -> Vec<(u64, u64)> {
let mut design = compile(src);
let eval = design.eval_session();
let body = top_body(eval.design());
let model = discover(&eval, &[(InstanceId(0), body)], Path::new("<test>"))
.unwrap_or_else(|e| panic!("discover failed: {e}"));
inputs
.iter()
.map(|i| uniform_limits(&model, body, i))
.collect()
}
#[test]
fn discover_reproduces_the_lrm_30_7_1_worked_example() {
let limits = limits_in(
"module m(input clk, input data, input clr, input pre, output q);\n assign q = \
clk | data | clr | pre;\n specify\n (clk => q) = 12;\n (data => q) = 10;\n \
(clr, pre *> q) = 4;\n\n specparam\n PATHPULSE$clk$q = (2,9),\n \
PATHPULSE$clr$q = (0,4),\n PATHPULSE$ = 3;\n endspecify\nendmodule\n",
&["clk", "clr", "pre", "data"],
);
assert_eq!(limits, [(2, 9), (0, 4), (0, 4), (3, 3)]);
}
#[test]
fn discover_ignores_a_pathpulse_naming_a_non_first_terminal() {
let limits = limits_in(
"module m(input clr, input pre, input a, output q, output r);\n assign q = clr | \
pre | a;\n assign r = a;\n specify\n (clr, pre *> q) = 4;\n (a *> q, r) = 6;\n \
specparam PATHPULSE$pre$q = (0, 1);\n specparam PATHPULSE$a$r = (1, 2);\n \
endspecify\nendmodule\n",
&["clr", "pre", "a"],
);
assert_eq!(limits, [(4, 4), (4, 4), (6, 6)]);
}
#[test]
fn discover_path_specific_pathpulse_wins_over_module_wide_in_either_order() {
for decls in [
"specparam PATHPULSE$ = (1, 2); specparam PATHPULSE$a$y = 5;",
"specparam PATHPULSE$a$y = 5; specparam PATHPULSE$ = (1, 2);",
] {
let limits = limits_in(
&format!(
"module m(input a, input b, output y);\n assign y = a & b;\n specify\n \
(a => y) = 10;\n (b => y) = 10;\n {decls}\n endspecify\nendmodule\n"
),
&["a", "b"],
);
assert_eq!(limits, [(5, 5), (1, 2)], "{decls}");
}
}
#[test]
fn discover_clamps_inverted_and_negative_pulse_limits() {
let limits = limits_in(
"module m(input a, input b, output y);\n assign y = a & b;\n specify\n (a => y) = \
10;\n (b => y) = 10;\n specparam NEG = -3;\n specparam PATHPULSE$a$y = (6, \
2);\n specparam PATHPULSE$b$y = (NEG, 4);\n endspecify\nendmodule\n",
&["a", "b"],
);
assert_eq!(limits, [(6, 6), (0, 4)]);
}
#[test]
fn discover_converts_pulse_limits_with_the_timescale_and_evaluates_the_error_limit() {
let limits = limits_in(
"`timescale 1ns/1ps\nmodule m(input a, output y);\n assign y = a;\n specify\n \
specparam hi = 0.5;\n (a => y) = 2;\n specparam PATHPULSE$a$y = (0.25, hi + \
1.0);\n endspecify\nendmodule\n",
&["a"],
);
assert_eq!(limits, [(250, 1500)]);
}
#[test]
fn discover_evaluates_an_error_limit_over_parameters_and_localparams() {
let limits = limits_in(
"module m(input a, input b, input c, output y);\n parameter P = 7;\n localparam L = \
3;\n assign y = a & b & c;\n specify\n (a => y) = 10;\n (b => y) = 10;\n \
(c => y) = 10;\n specparam PATHPULSE$a$y = (2, P);\n specparam PATHPULSE$b$y = \
(2, P + 2);\n specparam PATHPULSE$c$y = (2, L * 2);\n endspecify\nendmodule\n",
&["a", "b", "c"],
);
assert_eq!(limits, [(2, 7), (2, 9), (2, 6)]);
}
#[test]
fn discover_evaluates_an_error_limit_above_two_pow_31_without_wrapping() {
let limits = limits_in(
"module m(input a, input b, input c, output y);\n parameter P = 64'd5000000000;\n \
specparam big = 64'd3000000000;\n assign y = a & b & c;\n specify\n (a => y) = \
10;\n (b => y) = 10;\n (c => y) = 10;\n specparam PATHPULSE$a$y = (1, \
big);\n specparam PATHPULSE$b$y = (1, big + 1000000000 + 2);\n specparam \
PATHPULSE$c$y = (1, P);\n endspecify\nendmodule\n",
&["a", "b", "c"],
);
assert_eq!(
limits,
[(1, 3_000_000_000), (1, 4_000_000_002), (1, 5_000_000_000)]
);
}
#[test]
fn run_design_an_error_limit_above_two_pow_31_filters_to_x() {
let stdout = run_src(
"bigerr",
"module m(input a, output y);\n assign y = a;\n specify\n specparam big = \
64'd3000000000;\n (a => y) = 10;\n specparam PATHPULSE$a$y = (1, big);\n \
endspecify\nendmodule\nmodule top;\n reg a;\n wire y;\n m dut(.a(a), .y(y));\n \
always @(y) $display(\"t=%0t y=%b\", $time, y);\n initial begin\n a = 0;\n \
#100 a = 1;\n #4 a = 0;\n #100 $finish;\n end\nendmodule\n",
);
assert_eq!(stdout, "t=10 y=0\nt=110 y=x\nt=114 y=0\n", "{stdout}");
}
#[test]
fn discover_rejects_an_unsized_literal_above_i32_max_in_a_pulse_limit_loudly() {
for limit in [
"3000000000",
"big",
"P",
"big + 1",
"Q",
"3_000_000_000",
"99999999999999999999",
] {
let src = format!(
"module m(input a, output y);\n parameter P = 5000000000;\n parameter Q = P;\n \
specparam big = 3000000000;\n assign y = a;\n specify\n (a => y) = \
10;\n specparam PATHPULSE$a$y = (1, {limit});\n endspecify\nendmodule\n"
);
match discover_src(&src) {
Err(super::RunError::Lowering(e)) => {
assert!(
e.message.contains("PATHPULSE$") && e.message.contains("truncates"),
"{limit}: {}",
e.message
);
}
Err(other) => panic!("{limit}: expected RunError::Lowering, got {other}"),
Ok(_) => panic!("{limit}: a truncated limit must not be accepted"),
}
}
let limits = limits_in(
"module m(input a, output y);\n specparam unused = 3000000000;\n assign y = a;\n \
specify\n (a => y) = 10;\n specparam PATHPULSE$a$y = (1, 2147483647);\n \
endspecify\nendmodule\n",
&["a"],
);
assert_eq!(limits, [(1, 2_147_483_647)]);
}
#[test]
fn run_design_an_error_limit_over_a_parameter_follows_the_instance_override() {
let stdout = run_src(
"param",
"module m #(parameter P = 7)(input a, output y);\n assign y = a;\n specify\n (a => \
y) = 20;\n specparam PATHPULSE$a$y = (2, P);\n endspecify\nendmodule\nmodule \
top;\n reg a;\n wire y1, y2, y3;\n m #(.P(9)) d1(.a(a), .y(y1));\n m d2(.a(a), \
.y(y2));\n m d3(.a(a), .y(y3));\n defparam d3.P = 5;\n always @(y1) \
$display(\"t=%0t y1=%b\", $time, y1);\n always @(y2) $display(\"t=%0t y2=%b\", \
$time, y2);\n always @(y3) $display(\"t=%0t y3=%b\", $time, y3);\n initial begin\n \
a = 0;\n #100 a = 1;\n #8 a = 0;\n #100 $finish;\n end\nendmodule\n",
);
let mut lines: Vec<&str> = stdout.lines().collect();
lines.sort();
assert_eq!(
lines,
[
"t=120 y1=x",
"t=120 y2=1",
"t=120 y3=1",
"t=128 y1=0",
"t=128 y2=0",
"t=128 y3=0",
"t=20 y1=0",
"t=20 y2=0",
"t=20 y3=0",
],
"{stdout}"
);
}
#[test]
fn discover_rejects_an_unevaluable_error_limit_loudly() {
match discover_src(
"module m(input a, output y);\n function integer f(input integer x);\n f = x + \
1;\n endfunction\n assign y = a;\n specify\n (a => y) = 10;\n specparam \
PATHPULSE$a$y = (2, f(6));\n endspecify\nendmodule\n",
) {
Err(super::RunError::Lowering(e)) => {
assert!(e.message.contains("PATHPULSE$"), "{}", e.message);
}
Err(other) => panic!("expected RunError::Lowering, got {other}"),
Ok(_) => panic!("an error limit that cannot be evaluated must not be dropped"),
}
}
}