use std::collections::HashMap;
use sv_lang::kinds::{ExpressionKind, SyntaxKind};
use sv_lang::nodes::{
ConditionalPathDeclarationSyntax, IfNonePathDeclarationSyntax, PathDeclarationSyntax,
PathSuffixSyntax,
};
use sv_lang::{
ArgumentDirection, BinaryOp, Bit, ConstantValue, EvalSession, Expression, PulseStyleKind,
Symbol, SyntaxTree, TimingPathPolarity, UnaryOp,
};
use sv_lang_kinds::SymbolKind;
use vj_abi::bit4::Bit4;
use super::{ElabError, InstanceId};
use crate::symid::{SymbolId, SymbolUid, is_value_name};
pub use crate::timing_types::{
CondBinaryOp, CondUnaryOp, EdgeControl, EdgeSet, EdgeTransition, PathDelaySet, PulseStyle,
TRANSITIONS,
};
pub type TerminalRef = (SymbolId, Option<(u32, u32)>);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PathEdge {
None,
Any,
Specific(sv_lang::EdgeKind),
}
impl PathEdge {
fn from_accessors<'d>(path: &Symbol<'d>) -> (PathEdge, Option<Expression<'d>>) {
let identifier = path.timing_path_edge_identifier();
let source = path.timing_path_edge_source_expr();
let edge = if identifier != sv_lang::EdgeKind::None {
PathEdge::Specific(identifier)
} else if source.is_some() {
PathEdge::Any
} else {
PathEdge::None
};
(edge, source)
}
}
impl CondUnaryOp {
fn from_sv(op: UnaryOp) -> Option<CondUnaryOp> {
match op {
UnaryOp::BitwiseNot => Some(CondUnaryOp::BitwiseNot),
UnaryOp::BitwiseAnd => Some(CondUnaryOp::ReductionAnd),
UnaryOp::BitwiseOr => Some(CondUnaryOp::ReductionOr),
UnaryOp::BitwiseXor => Some(CondUnaryOp::ReductionXor),
UnaryOp::BitwiseNand => Some(CondUnaryOp::ReductionNand),
UnaryOp::BitwiseNor => Some(CondUnaryOp::ReductionNor),
UnaryOp::BitwiseXnor => Some(CondUnaryOp::ReductionXnor),
UnaryOp::LogicalNot => Some(CondUnaryOp::LogicalNot),
UnaryOp::Plus
| UnaryOp::Minus
| UnaryOp::Preincrement
| UnaryOp::Predecrement
| UnaryOp::Postincrement
| UnaryOp::Postdecrement => None,
}
}
}
impl CondBinaryOp {
fn from_sv_check(op: BinaryOp) -> Option<CondBinaryOp> {
match op {
BinaryOp::CaseEquality => Some(CondBinaryOp::CaseEquality),
BinaryOp::CaseInequality => Some(CondBinaryOp::CaseInequality),
other => CondBinaryOp::from_sv(other),
}
}
fn from_sv(op: BinaryOp) -> Option<CondBinaryOp> {
match op {
BinaryOp::BinaryAnd => Some(CondBinaryOp::BitwiseAnd),
BinaryOp::BinaryOr => Some(CondBinaryOp::BitwiseOr),
BinaryOp::BinaryXor => Some(CondBinaryOp::BitwiseXor),
BinaryOp::BinaryXnor => Some(CondBinaryOp::BitwiseXnor),
BinaryOp::Equality => Some(CondBinaryOp::Equality),
BinaryOp::Inequality => Some(CondBinaryOp::Inequality),
BinaryOp::LogicalAnd => Some(CondBinaryOp::LogicalAnd),
BinaryOp::LogicalOr => Some(CondBinaryOp::LogicalOr),
BinaryOp::Add
| BinaryOp::Subtract
| BinaryOp::Multiply
| BinaryOp::Divide
| BinaryOp::Mod
| BinaryOp::CaseEquality
| BinaryOp::CaseInequality
| BinaryOp::GreaterThanEqual
| BinaryOp::GreaterThan
| BinaryOp::LessThanEqual
| BinaryOp::LessThan
| BinaryOp::WildcardEquality
| BinaryOp::WildcardInequality
| BinaryOp::LogicalImplication
| BinaryOp::LogicalEquivalence
| BinaryOp::LogicalShiftLeft
| BinaryOp::LogicalShiftRight
| BinaryOp::ArithmeticShiftLeft
| BinaryOp::ArithmeticShiftRight
| BinaryOp::Power => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CondNode {
Signal(TerminalRef),
Specparam(SymbolId),
Const(Vec<Bit4>),
Unary(CondUnaryOp, Box<CondNode>),
Binary(CondBinaryOp, Box<CondNode>, Box<CondNode>),
Ternary(Box<CondNode>, Box<CondNode>, Box<CondNode>),
Concat(Vec<CondNode>),
Replication(u32, Box<CondNode>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PathCondition {
Unconditional,
If(CondNode),
IfNone,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PathEntry {
pub instance: InstanceId,
pub inputs: Vec<TerminalRef>,
pub outputs: Vec<TerminalRef>,
pub connection: sv_lang::TimingPathConnectionKind,
pub polarity: TimingPathPolarity,
pub path_uid: SymbolId,
pub delays: PathDelaySet,
pub edge: PathEdge,
pub edge_data_source_name: Option<String>,
pub condition: PathCondition,
pub reject_limit: PathDelaySet,
pub error_limit: PathDelaySet,
pub pulse_style: PulseStyle,
pub showcancelled: bool,
}
impl PathEntry {
pub fn debug_polarity(&self) -> TimingPathPolarity {
self.polarity
}
pub fn debug_edge_source_expr_name(&self) -> Option<&str> {
self.edge_data_source_name.as_deref()
}
}
impl EdgeControl {
pub fn opposite(self) -> Option<EdgeControl> {
match self {
EdgeControl::AnyChange => None,
EdgeControl::Edges(set) => Some(EdgeControl::Edges(set.reversed())),
}
}
pub fn from_syntax(edge: sv_lang::EdgeKind, descriptors: &[String]) -> Result<Self, String> {
use sv_lang::EdgeKind as K;
match (edge, descriptors.is_empty()) {
(K::None, true) => Ok(EdgeControl::AnyChange),
(K::PosEdge, true) => Ok(EdgeControl::Edges(EdgeSet::POSEDGE)),
(K::NegEdge, true) => Ok(EdgeControl::Edges(EdgeSet::NEGEDGE)),
(K::BothEdges, true) => Ok(EdgeControl::Edges(EdgeSet::ALL)),
(K::BothEdges, false) => {
let mut set = EdgeSet::EMPTY;
for d in descriptors {
let t = EdgeTransition::of_descriptor(d).ok_or_else(|| {
format!(
"edge descriptor `{d}` is not one of 01, 10, 0x, x0, 1x, x1 (z \
allowed for x, LRM 31.5)"
)
})?;
set = set.with(t);
}
Ok(EdgeControl::Edges(set))
}
(K::None | K::PosEdge | K::NegEdge, false) => Err(format!(
"an edge descriptor list {descriptors:?} is only allowed on `edge [...]`, not \
on {edge:?} (LRM 31.5)"
)),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CheckEvent {
pub terminal: TerminalRef,
pub edge: sv_lang::EdgeKind,
pub edge_descriptors: Vec<String>,
pub edge_control: EdgeControl,
pub condition: Option<CondNode>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct CheckLimit {
pub arg_index: u32,
pub ticks: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EventPairArgs {
pub reference: CheckEvent,
pub data: CheckEvent,
pub limit: CheckLimit,
pub notifier: Option<TerminalRef>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SetupHoldArgs {
pub reference: CheckEvent,
pub data: CheckEvent,
pub setup_limit: CheckLimit,
pub hold_limit: CheckLimit,
pub notifier: Option<TerminalRef>,
pub timestamp_cond: Option<CondNode>,
pub timecheck_cond: Option<CondNode>,
pub delayed_reference: Option<TerminalRef>,
pub delayed_data: Option<TerminalRef>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RecRemArgs {
pub reference: CheckEvent,
pub data: CheckEvent,
pub recovery_limit: CheckLimit,
pub removal_limit: CheckLimit,
pub notifier: Option<TerminalRef>,
pub timestamp_cond: Option<CondNode>,
pub timecheck_cond: Option<CondNode>,
pub delayed_reference: Option<TerminalRef>,
pub delayed_data: Option<TerminalRef>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TimeSkewArgs {
pub reference: CheckEvent,
pub data: CheckEvent,
pub limit: CheckLimit,
pub notifier: Option<TerminalRef>,
pub event_based: Option<bool>,
pub remain_active: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FullSkewArgs {
pub reference: CheckEvent,
pub data: CheckEvent,
pub reference_first_limit: CheckLimit,
pub data_first_limit: CheckLimit,
pub notifier: Option<TerminalRef>,
pub event_based: Option<bool>,
pub remain_active: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PeriodArgs {
pub reference: CheckEvent,
pub limit: CheckLimit,
pub notifier: Option<TerminalRef>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WidthArgs {
pub reference: CheckEvent,
pub limit: CheckLimit,
pub threshold: Option<CheckLimit>,
pub notifier: Option<TerminalRef>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NoChangeArgs {
pub reference: CheckEvent,
pub data: CheckEvent,
pub start_edge_offset: CheckLimit,
pub end_edge_offset: CheckLimit,
pub notifier: Option<TerminalRef>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CheckArgs {
Setup(EventPairArgs),
Hold(EventPairArgs),
SetupHold(SetupHoldArgs),
Recovery(EventPairArgs),
Removal(EventPairArgs),
RecRem(RecRemArgs),
Skew(EventPairArgs),
TimeSkew(TimeSkewArgs),
FullSkew(FullSkewArgs),
Period(PeriodArgs),
Width(WidthArgs),
NoChange(NoChangeArgs),
}
impl CheckArgs {
pub fn limits_mut(&mut self) -> Vec<&mut CheckLimit> {
match self {
CheckArgs::Setup(a)
| CheckArgs::Hold(a)
| CheckArgs::Recovery(a)
| CheckArgs::Removal(a)
| CheckArgs::Skew(a) => vec![&mut a.limit],
CheckArgs::SetupHold(a) => vec![&mut a.setup_limit, &mut a.hold_limit],
CheckArgs::RecRem(a) => vec![&mut a.recovery_limit, &mut a.removal_limit],
CheckArgs::TimeSkew(a) => vec![&mut a.limit],
CheckArgs::FullSkew(a) => {
vec![&mut a.reference_first_limit, &mut a.data_first_limit]
}
CheckArgs::Period(a) => vec![&mut a.limit],
CheckArgs::Width(a) => {
let mut v = vec![&mut a.limit];
v.extend(a.threshold.as_mut());
v
}
CheckArgs::NoChange(a) => vec![&mut a.start_edge_offset, &mut a.end_edge_offset],
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CheckEntry {
pub instance: InstanceId,
pub check_uid: SymbolId,
pub kind: sv_lang::SystemTimingCheckKind,
pub args: CheckArgs,
}
#[derive(Default)]
pub struct TimingModel {
specparams: HashMap<(InstanceId, SymbolId), ConstantValue>,
pub paths: Vec<PathEntry>,
pub timing_checks: Vec<CheckEntry>,
pulse_style_option: Option<PulseStyle>,
showcancelled_option: Option<bool>,
}
impl TimingModel {
pub fn new() -> TimingModel {
TimingModel::default()
}
pub fn seed_specparams(&mut self, infos: &[(InstanceId, Symbol<'_>)]) {
for &(id, body) in infos {
for m in super::module_items(&body) {
if m.kind() == SymbolKind::Specparam {
if let Some(v) = m.constant_value() {
self.specparams.insert((id, m.uid()), v);
}
continue;
}
if m.kind() != SymbolKind::SpecifyBlock {
continue;
}
for inner in m.members() {
if inner.kind() != SymbolKind::Specparam {
continue;
}
if let Some(v) = inner.constant_value() {
self.specparams.insert((id, inner.uid()), v);
}
}
}
}
}
pub fn specparam(&self, instance: InstanceId, uid: &SymbolId) -> Option<&ConstantValue> {
self.specparams.get(&(instance, uid.clone()))
}
pub fn set_specparam(&mut self, instance: InstanceId, uid: SymbolId, value: ConstantValue) {
self.specparams.insert((instance, uid), value);
}
pub fn set_path_delays(
&mut self,
instance: InstanceId,
path_uid: &SymbolId,
delays: PathDelaySet,
) {
for p in &mut self.paths {
if p.instance == instance && &p.path_uid == path_uid {
p.delays = delays;
p.reject_limit = delays;
p.error_limit = delays;
}
}
}
pub fn set_path_pulse_limits(
&mut self,
instance: InstanceId,
path_uid: &SymbolId,
reject: u64,
error: u64,
) {
let error = error.max(reject);
for p in &mut self.paths {
if p.instance == instance && &p.path_uid == path_uid {
p.reject_limit = PathDelaySet::uniform(reject);
p.error_limit = PathDelaySet::uniform(error);
}
}
}
pub fn apply_pulse_percent(&mut self, reject_pct: f64, error_pct: f64) {
let scale = |v: u64, pct: f64| -> u64 {
if pct == 100.0 {
v
} else {
(v as f64 * pct / 100.0).round() as u64
}
};
for p in &mut self.paths {
let delays = p.delays.transitions();
let reject = delays.map(|d| scale(d, reject_pct));
let error: [u64; 12] =
std::array::from_fn(|i| scale(delays[i], error_pct).max(reject[i]));
p.reject_limit = PathDelaySet::from_transitions(reject);
p.error_limit = PathDelaySet::from_transitions(error);
}
}
pub fn set_pulse_options(&mut self, style: Option<PulseStyle>, showcancelled: Option<bool>) {
self.pulse_style_option = style;
self.showcancelled_option = showcancelled;
}
pub fn discover<'d>(
&mut self,
eval: &EvalSession<'d>,
infos: &[(InstanceId, Symbol<'d>)],
) -> Result<(), ElabError> {
let trees = eval.design().syntax_trees();
for &(id, body) in infos {
let ports = port_directions(&body);
let mut declarations: Vec<(TerminalRef, PulseStyleKind)> = Vec::new();
for m in super::module_items(&body) {
if m.kind() != SymbolKind::SpecifyBlock {
continue;
}
for inner in m.members() {
match inner.kind() {
SymbolKind::Specparam => {}
SymbolKind::TimingPath => {
self.paths
.extend(discover_path(eval, id, &inner, &ports, &trees)?);
}
SymbolKind::PulseStyle => {
declarations.extend(discover_pulse_style(
eval,
id,
&inner,
&ports,
&self.paths,
)?);
}
SymbolKind::SystemTimingCheck => {
self.timing_checks.push(discover_check(eval, id, &inner)?);
}
other => {
return Err(ElabError::UnsupportedConstruct {
hier_path: inner.hierarchical_path(),
detail: format!(
"specify block member of kind `{other:?}` (LRM 30.3's own \
specify_item production only allows specparam_declaration, \
path_declaration, system_timing_check and \
pulsestyle_declaration/showcancelled_declaration — this is \
an unexpected AST shape, not a construct this task has \
chosen to defer)"
),
});
}
}
}
}
validate_state_dependent_groups(id, &self.paths)?;
apply_pulse_style_declarations(id, &mut self.paths, &declarations);
for p in self.paths.iter_mut().filter(|p| p.instance == id) {
if let Some(style) = self.pulse_style_option {
p.pulse_style = style;
}
if let Some(show) = self.showcancelled_option {
p.showcancelled = show;
}
}
}
Ok(())
}
}
fn strip_arg_wrappers<'d>(mut e: Expression<'d>) -> Expression<'d> {
loop {
let k = e.kind();
let next = if k == ExpressionKind::Conversion {
e.operand()
} else if k == ExpressionKind::Assignment {
e.left()
} else {
None
};
match next {
Some(n) => e = n,
None => return e,
}
}
}
struct CheckArgsView<'a, 'd> {
eval: &'a EvalSession<'d>,
check: &'a Symbol<'d>,
name: &'static str,
args: Vec<sv_lang::SystemTimingCheckArg<'d>>,
}
impl<'a, 'd> CheckArgsView<'a, 'd> {
fn err(&self, detail: String) -> ElabError {
unsupported(self.check, format!("`{}` {detail}", self.name))
}
fn arg(&self, i: usize) -> Option<&sv_lang::SystemTimingCheckArg<'d>> {
self.args.get(i).filter(|a| a.expr.is_some())
}
fn require(
&self,
i: usize,
what: &str,
) -> Result<&sv_lang::SystemTimingCheckArg<'d>, ElabError> {
self.arg(i).ok_or_else(|| {
self.err(format!(
"is missing its required {what} (argument {}; an empty argument is not allowed \
there)",
i + 1
))
})
}
fn signal_of(&self, e: Expression<'d>, what: &str) -> Result<TerminalRef, ElabError> {
let e = strip_arg_wrappers(e);
let (sym, bits) = resolve_cond_signal_ref(self.eval, self.check, e)?;
let modport_signal = sym.kind() == SymbolKind::ModportPort
&& sym.modport_port_explicit_connection().is_none()
&& sym
.modport_port_internal_symbol()
.is_some_and(|s| matches!(s.kind(), SymbolKind::Net | SymbolKind::Variable));
if !modport_signal && !matches!(sym.kind(), SymbolKind::Net | SymbolKind::Variable) {
return Err(self.err(format!(
"{what} `{}` is a {:?}, not a net or variable",
sym.name(),
sym.kind()
)));
}
Ok((sym.uid(), bits))
}
fn event(&self, i: usize, what: &str) -> Result<CheckEvent, ElabError> {
let a = self.require(i, what)?;
let expr = a
.expr
.expect("require() only returns arguments with an expr");
let condition = a
.condition
.map(|c| compile_cond_expr_in(self.eval, self.check, c, CondDialect::TimingCheck))
.transpose()?;
let edge_control = EdgeControl::from_syntax(a.edge, &a.edge_descriptors)
.map_err(|e| self.err(format!("{what}: {e}")))?;
Ok(CheckEvent {
terminal: self.signal_of(expr, what)?,
edge: a.edge,
edge_descriptors: a.edge_descriptors.clone(),
edge_control,
condition,
})
}
fn limit(&self, i: usize, what: &str) -> Result<CheckLimit, ElabError> {
self.require(i, what)?;
Ok(CheckLimit {
arg_index: i as u32,
ticks: 0,
})
}
fn opt_limit(&self, i: usize) -> Option<CheckLimit> {
self.arg(i).map(|_| CheckLimit {
arg_index: i as u32,
ticks: 0,
})
}
fn opt_signal(&self, i: usize, what: &str) -> Result<Option<TerminalRef>, ElabError> {
self.arg(i)
.map(|a| self.signal_of(a.expr.expect("arg() filters on expr"), what))
.transpose()
}
fn opt_cond(&self, i: usize) -> Result<Option<CondNode>, ElabError> {
self.arg(i)
.map(|a| {
compile_cond_expr_in(
self.eval,
self.check,
a.expr.expect("arg() filters on expr"),
CondDialect::TimingCheck,
)
})
.transpose()
}
fn opt_flag(&self, i: usize, what: &str) -> Result<Option<bool>, ElabError> {
self.arg(i)
.map(|a| {
const_i64(self.eval, a.expr.expect("arg() filters on expr"))
.map(|v| v != 0)
.ok_or_else(|| self.err(format!("{what} must be a constant expression")))
})
.transpose()
}
fn pair(&self, data_first: bool) -> Result<EventPairArgs, ElabError> {
let (r, d) = if data_first { (1, 0) } else { (0, 1) };
Ok(EventPairArgs {
reference: self.event(r, "reference event")?,
data: self.event(d, "data event")?,
limit: self.limit(2, "limit")?,
notifier: self.opt_signal(3, "notifier")?,
})
}
}
fn discover_check<'d>(
eval: &EvalSession<'d>,
instance: InstanceId,
check: &Symbol<'d>,
) -> Result<CheckEntry, ElabError> {
use sv_lang::SystemTimingCheckKind as K;
let kind = check.system_timing_check_kind();
let name = match kind {
K::Setup => "$setup",
K::Hold => "$hold",
K::SetupHold => "$setuphold",
K::Recovery => "$recovery",
K::Removal => "$removal",
K::RecRem => "$recrem",
K::Skew => "$skew",
K::TimeSkew => "$timeskew",
K::FullSkew => "$fullskew",
K::Period => "$period",
K::Width => "$width",
K::NoChange => "$nochange",
K::Unknown => {
return Err(unsupported(
check,
"system timing check of an unrecognized kind (unexpected AST shape)".to_string(),
));
}
};
let v = CheckArgsView {
eval,
check,
name,
args: check.system_timing_check_arguments().collect(),
};
let args = match kind {
K::Unknown => {
return Err(unsupported(
check,
"system timing check of an unrecognized kind (unexpected AST shape)".to_string(),
));
}
K::Setup => CheckArgs::Setup(v.pair(true)?),
K::Hold => CheckArgs::Hold(v.pair(false)?),
K::Recovery => CheckArgs::Recovery(v.pair(false)?),
K::Removal => CheckArgs::Removal(v.pair(false)?),
K::Skew => CheckArgs::Skew(v.pair(false)?),
K::SetupHold => CheckArgs::SetupHold(SetupHoldArgs {
reference: v.event(0, "reference event")?,
data: v.event(1, "data event")?,
setup_limit: v.limit(2, "setup limit")?,
hold_limit: v.limit(3, "hold limit")?,
notifier: v.opt_signal(4, "notifier")?,
timestamp_cond: v.opt_cond(5)?,
timecheck_cond: v.opt_cond(6)?,
delayed_reference: v.opt_signal(7, "delayed reference")?,
delayed_data: v.opt_signal(8, "delayed data")?,
}),
K::RecRem => CheckArgs::RecRem(RecRemArgs {
reference: v.event(0, "reference event")?,
data: v.event(1, "data event")?,
recovery_limit: v.limit(2, "recovery limit")?,
removal_limit: v.limit(3, "removal limit")?,
notifier: v.opt_signal(4, "notifier")?,
timestamp_cond: v.opt_cond(5)?,
timecheck_cond: v.opt_cond(6)?,
delayed_reference: v.opt_signal(7, "delayed reference")?,
delayed_data: v.opt_signal(8, "delayed data")?,
}),
K::TimeSkew => CheckArgs::TimeSkew(TimeSkewArgs {
reference: v.event(0, "reference event")?,
data: v.event(1, "data event")?,
limit: v.limit(2, "limit")?,
notifier: v.opt_signal(3, "notifier")?,
event_based: v.opt_flag(4, "event_based_flag")?,
remain_active: v.opt_flag(5, "remain_active_flag")?,
}),
K::FullSkew => CheckArgs::FullSkew(FullSkewArgs {
reference: v.event(0, "reference event")?,
data: v.event(1, "data event")?,
reference_first_limit: v.limit(2, "first limit")?,
data_first_limit: v.limit(3, "second limit")?,
notifier: v.opt_signal(4, "notifier")?,
event_based: v.opt_flag(5, "event_based_flag")?,
remain_active: v.opt_flag(6, "remain_active_flag")?,
}),
K::Period => {
let reference = v.event(0, "reference event")?;
if reference.edge == sv_lang::EdgeKind::None {
return Err(v.err(
"reference event must be an edge specification (`posedge`, `negedge` or \
`edge`) -- LRM 31.4.5: the period is measured between successive \
occurrences of that edge"
.to_string(),
));
}
CheckArgs::Period(PeriodArgs {
reference,
limit: v.limit(1, "limit")?,
notifier: v.opt_signal(2, "notifier")?,
})
}
K::Width => {
let reference = v.event(0, "reference event")?;
if reference.edge == sv_lang::EdgeKind::None {
return Err(v.err(
"reference event must be an edge specification (`posedge`, `negedge` or \
`edge`) -- LRM 31.4.4: the width is measured from that edge to the opposite \
one"
.to_string(),
));
}
if v.arg(3).is_some() && v.arg(2).is_none() {
return Err(v.err(
"has a notifier but a null threshold: LRM 31.4.4 allows the notifier only \
when the threshold argument is also given (write `0` for no threshold)"
.to_string(),
));
}
if let Some(t) = v.arg(2) {
let te = t.expr.expect("arg() filters on expr");
if te
.constant_value()
.or_else(|| eval.eval_constant(te))
.is_none()
{
return Err(v.err(
"threshold must be a constant expression (LRM 31.4.4) -- a notifier in \
the third position is the classic mistake; the notifier is the fourth \
argument"
.to_string(),
));
}
}
CheckArgs::Width(WidthArgs {
reference,
limit: v.limit(1, "limit")?,
threshold: v.opt_limit(2),
notifier: v.opt_signal(3, "notifier")?,
})
}
K::NoChange => {
let reference = v.event(0, "reference event")?;
let plain_edge = matches!(
reference.edge,
sv_lang::EdgeKind::PosEdge | sv_lang::EdgeKind::NegEdge
) && reference.edge_descriptors.is_empty();
if !plain_edge {
return Err(v.err(
"reference event must be `posedge` or `negedge` (LRM 31.4.6); an `edge` \
control specifier (with or without a descriptor list) or a bare signal is \
not allowed"
.to_string(),
));
}
CheckArgs::NoChange(NoChangeArgs {
reference,
data: v.event(1, "data event")?,
start_edge_offset: v.limit(2, "start edge offset")?,
end_edge_offset: v.limit(3, "end edge offset")?,
notifier: v.opt_signal(4, "notifier")?,
})
}
};
Ok(CheckEntry {
instance,
check_uid: check.uid(),
kind,
args,
})
}
fn terminals_overlap(a: &TerminalRef, b: &TerminalRef) -> bool {
if a.0 != b.0 {
return false;
}
match (a.1, b.1) {
(Some((alo, aw)), Some((blo, bw))) => alo < blo + bw && blo < alo + aw,
_ => true,
}
}
fn discover_pulse_style<'d>(
eval: &EvalSession<'d>,
instance: InstanceId,
decl: &Symbol<'d>,
ports: &HashMap<SymbolId, ArgumentDirection>,
paths: &[PathEntry],
) -> Result<Vec<(TerminalRef, PulseStyleKind)>, ElabError> {
let kind = decl.pulse_style_kind();
let (keyword, clause) = match kind {
PulseStyleKind::OnEvent => ("pulsestyle_onevent", "30.7.4.1"),
PulseStyleKind::OnDetect => ("pulsestyle_ondetect", "30.7.4.1"),
PulseStyleKind::ShowCancelled => ("showcancelled", "30.7.4.2"),
PulseStyleKind::NoShowCancelled => ("noshowcancelled", "30.7.4.2"),
};
let mut out = Vec::new();
for e in decl.pulse_style_terminals() {
let terminal = resolve_terminal(eval, decl, ports, e, false)?;
let already_used = paths.iter().any(|p| {
p.instance == instance && p.outputs.iter().any(|o| terminals_overlap(o, &terminal))
});
if already_used {
let full = terminal.0.to_string();
let name = full.rsplit('.').next().unwrap_or(&full);
return Err(unsupported(
decl,
format!(
"`{keyword} {name}` follows a module path declaration that already names \
output `{name}` (LRM {clause}: it is an error for a module path output to \
appear in this declaration after it has already appeared in a module path \
declaration — the output would have contradictory pulse behavior for the \
earlier path)"
),
));
}
out.push((terminal, kind));
}
Ok(out)
}
fn apply_pulse_style_declarations(
instance: InstanceId,
paths: &mut [PathEntry],
declarations: &[(TerminalRef, PulseStyleKind)],
) {
for (terminal, kind) in declarations {
for p in paths.iter_mut().filter(|p| {
p.instance == instance && p.outputs.iter().any(|o| terminals_overlap(o, terminal))
}) {
match kind {
PulseStyleKind::OnEvent => p.pulse_style = PulseStyle::OnEvent,
PulseStyleKind::OnDetect => p.pulse_style = PulseStyle::OnDetect,
PulseStyleKind::ShowCancelled => p.showcancelled = true,
PulseStyleKind::NoShowCancelled => p.showcancelled = false,
}
}
}
}
fn port_directions<'d>(body: &Symbol<'d>) -> HashMap<SymbolId, ArgumentDirection> {
let mut map = HashMap::new();
for m in body.members() {
if m.kind() != SymbolKind::Port {
continue;
}
let internal = m.port_internal_symbol().unwrap_or(m);
map.insert(internal.uid(), m.port_direction());
}
map
}
fn unsupported(sym: &Symbol<'_>, detail: String) -> ElabError {
ElabError::UnsupportedConstruct {
hier_path: sym.hierarchical_path(),
detail,
}
}
fn const_i64<'d>(eval: &EvalSession<'d>, e: Expression<'d>) -> Option<i64> {
e.constant_value()
.or_else(|| eval.eval_constant(e))
.and_then(|v| v.as_i64())
}
fn validate_direction<'d>(
path: &Symbol<'d>,
ports: &HashMap<SymbolId, ArgumentDirection>,
sym: Symbol<'d>,
want_input: bool,
) -> Result<(), ElabError> {
if sym.kind() == SymbolKind::ModportPort {
let internal = sym.modport_port_internal_symbol();
if sym.modport_port_explicit_connection().is_some()
|| !internal.is_some_and(|s| matches!(s.kind(), SymbolKind::Net | SymbolKind::Variable))
{
return Err(unsupported(
path,
format!(
"specify terminal `{}` is an interface-port signal bound to an expression \
or a non-signal (LRM 25.6): not lowered",
sym.name()
),
));
}
let dir = sym.modport_port_direction();
let ok = if want_input {
matches!(dir, ArgumentDirection::In | ArgumentDirection::InOut)
} else {
matches!(dir, ArgumentDirection::Out | ArgumentDirection::InOut)
};
if !ok {
return Err(unsupported(
path,
format!(
"specify path {} `{}` is a modport port with direction {:?}, not {} (LRM \
25.6, 30.4.1)",
if want_input { "source" } else { "destination" },
sym.name(),
dir,
if want_input {
"input or inout"
} else {
"output or inout"
}
),
));
}
return Ok(());
}
if want_input && sym.kind() != SymbolKind::Net {
return Err(unsupported(
path,
format!(
"specify path source `{}` is a {:?}, not a net (LRM 30.4.1: \"the module path \
source shall be a net\")",
sym.name(),
sym.kind()
),
));
}
if !want_input && !matches!(sym.kind(), SymbolKind::Net | SymbolKind::Variable) {
return Err(unsupported(
path,
format!(
"specify path destination `{}` is a {:?}, not a net or variable (LRM 30.4.1: \
\"the module path destination shall be a net or variable\")",
sym.name(),
sym.kind()
),
));
}
let dir = ports.get(&sym.uid()).ok_or_else(|| {
unsupported(
path,
format!(
"specify path terminal `{}` is not connected to any module port (LRM 30.4.1 \
requires the source to be connected to an input/inout port, and the \
destination to an output/inout port)",
sym.name()
),
)
})?;
let ok = if want_input {
matches!(dir, ArgumentDirection::In | ArgumentDirection::InOut)
} else {
matches!(dir, ArgumentDirection::Out | ArgumentDirection::InOut)
};
if !ok {
return Err(unsupported(
path,
format!(
"specify path {} `{}` is connected to a {:?} port, not {} (LRM 30.4.1)",
if want_input { "source" } else { "destination" },
sym.name(),
dir,
if want_input {
"an input or inout port"
} else {
"an output or inout port"
}
),
));
}
Ok(())
}
fn resolve_terminal<'d>(
eval: &EvalSession<'d>,
path: &Symbol<'d>,
ports: &HashMap<SymbolId, ArgumentDirection>,
e: Expression<'d>,
want_input: bool,
) -> Result<TerminalRef, ElabError> {
let e = if e.kind() == ExpressionKind::Conversion {
e.operand().unwrap_or(e)
} else {
e
};
let (sym, bits) = if is_value_name(e.kind()) {
let sym = e.referenced_symbol().ok_or_else(|| {
unsupported(
path,
"specify path terminal has no referenced symbol (unexpected AST shape)".to_string(),
)
})?;
(sym, None)
} else if e.kind() == ExpressionKind::ElementSelect {
let base = e.select_value().ok_or_else(|| {
unsupported(
path,
"bit-select specify path terminal missing its own base expression".to_string(),
)
})?;
if !is_value_name(base.kind()) {
return Err(unsupported(
path,
"bit-select specify path terminal's base is not a plain signal reference \
(unexpected AST shape)"
.to_string(),
));
}
let sym = base.referenced_symbol().ok_or_else(|| {
unsupported(
path,
"bit-select specify path terminal's base has no referenced symbol (unexpected \
AST shape)"
.to_string(),
)
})?;
let idx_expr = e.selector().ok_or_else(|| {
unsupported(
path,
"bit-select specify path terminal missing its own index".to_string(),
)
})?;
let idx = const_i64(eval, idx_expr).ok_or_else(|| {
unsupported(
path,
"bit-select specify path terminal index must be a compile-time constant"
.to_string(),
)
})?;
if idx < 0 {
return Err(unsupported(
path,
"bit-select specify path terminal index is negative".to_string(),
));
}
(sym, Some((idx as u32, 1)))
} else if e.kind() == ExpressionKind::RangeSelect {
let base = e.select_value().ok_or_else(|| {
unsupported(
path,
"part-select specify path terminal missing its own base expression".to_string(),
)
})?;
if !is_value_name(base.kind()) {
return Err(unsupported(
path,
"part-select specify path terminal's base is not a plain signal reference \
(unexpected AST shape)"
.to_string(),
));
}
let sym = base.referenced_symbol().ok_or_else(|| {
unsupported(
path,
"part-select specify path terminal's base has no referenced symbol (unexpected \
AST shape)"
.to_string(),
)
})?;
let kind = e.range_selection_kind().ok_or_else(|| {
unsupported(
path,
"part-select specify path terminal missing its own range_selection_kind"
.to_string(),
)
})?;
let l_expr = e.range_left().ok_or_else(|| {
unsupported(
path,
"simple part-select specify path terminal missing its left bound".to_string(),
)
})?;
let r_expr = e.range_right().ok_or_else(|| {
unsupported(
path,
"simple part-select specify path terminal missing its right bound".to_string(),
)
})?;
let l = const_i64(eval, l_expr).ok_or_else(|| {
unsupported(
path,
"simple part-select specify path terminal's left bound must be a compile-time \
constant"
.to_string(),
)
})?;
let r = const_i64(eval, r_expr).ok_or_else(|| {
unsupported(
path,
"simple part-select specify path terminal's right bound must be a \
compile-time constant"
.to_string(),
)
})?;
let (lo, hi) = match kind {
0 => (l.min(r), l.max(r)),
1 => (l, l + r - 1),
_ => (l - r + 1, l),
};
if lo < 0 || hi < lo {
return Err(unsupported(
path,
"part-select specify path terminal has a negative or empty bit window".to_string(),
));
}
(sym, Some((lo as u32, (hi - lo + 1) as u32)))
} else {
return Err(unsupported(
path,
format!(
"specify path terminal expression kind `{:?}` (unexpected AST shape)",
e.kind()
),
));
};
validate_direction(path, ports, sym, want_input)?;
Ok((sym.uid(), bits))
}
fn bit4_of(b: Bit) -> Bit4 {
match b {
Bit::Zero => Bit4::Zero,
Bit::One => Bit4::One,
Bit::X => Bit4::X,
Bit::Z => Bit4::Z,
}
}
fn resolve_cond_signal_ref<'d>(
eval: &EvalSession<'d>,
path: &Symbol<'d>,
e: Expression<'d>,
) -> Result<(Symbol<'d>, Option<(u32, u32)>), ElabError> {
if is_value_name(e.kind()) {
let sym = e.referenced_symbol().ok_or_else(|| {
unsupported(
path,
"state-dependent path condition operand has no referenced symbol (unexpected \
AST shape)"
.to_string(),
)
})?;
return Ok((sym, None));
}
if e.kind() == ExpressionKind::ElementSelect {
let base = e.select_value().ok_or_else(|| {
unsupported(
path,
"bit-select condition operand missing its own base expression".to_string(),
)
})?;
if !is_value_name(base.kind()) {
return Err(unsupported(
path,
"bit-select condition operand's base is not a plain signal reference \
(unexpected AST shape)"
.to_string(),
));
}
let sym = base.referenced_symbol().ok_or_else(|| {
unsupported(
path,
"bit-select condition operand's base has no referenced symbol (unexpected AST \
shape)"
.to_string(),
)
})?;
let idx_expr = e.selector().ok_or_else(|| {
unsupported(
path,
"bit-select condition operand missing its own index".to_string(),
)
})?;
let idx = const_i64(eval, idx_expr).ok_or_else(|| {
unsupported(
path,
"bit-select condition operand index must be a compile-time constant".to_string(),
)
})?;
if idx < 0 {
return Err(unsupported(
path,
"bit-select condition operand index is negative".to_string(),
));
}
return Ok((sym, Some((idx as u32, 1))));
}
if e.kind() == ExpressionKind::RangeSelect {
let base = e.select_value().ok_or_else(|| {
unsupported(
path,
"part-select condition operand missing its own base expression".to_string(),
)
})?;
if !is_value_name(base.kind()) {
return Err(unsupported(
path,
"part-select condition operand's base is not a plain signal reference \
(unexpected AST shape)"
.to_string(),
));
}
let sym = base.referenced_symbol().ok_or_else(|| {
unsupported(
path,
"part-select condition operand's base has no referenced symbol (unexpected AST \
shape)"
.to_string(),
)
})?;
let kind = e.range_selection_kind().ok_or_else(|| {
unsupported(
path,
"part-select condition operand missing its own range_selection_kind".to_string(),
)
})?;
let l_expr = e.range_left().ok_or_else(|| {
unsupported(
path,
"simple part-select condition operand missing its left bound".to_string(),
)
})?;
let r_expr = e.range_right().ok_or_else(|| {
unsupported(
path,
"simple part-select condition operand missing its right bound".to_string(),
)
})?;
let l = const_i64(eval, l_expr).ok_or_else(|| {
unsupported(
path,
"simple part-select condition operand's left bound must be a compile-time \
constant"
.to_string(),
)
})?;
let r = const_i64(eval, r_expr).ok_or_else(|| {
unsupported(
path,
"simple part-select condition operand's right bound must be a compile-time \
constant"
.to_string(),
)
})?;
let (lo, hi) = match kind {
0 => (l.min(r), l.max(r)),
1 => (l, l + r - 1),
_ => (l - r + 1, l),
};
if lo < 0 || hi < lo {
return Err(unsupported(
path,
"part-select condition operand has a negative or empty bit window".to_string(),
));
}
return Ok((sym, Some((lo as u32, (hi - lo + 1) as u32))));
}
Err(unsupported(
path,
format!(
"state-dependent path condition operand expression kind `{:?}` is neither a \
signal/specparam reference nor a bit-/part-select of one (unexpected AST shape)",
e.kind()
),
))
}
fn compile_cond_expr<'d>(
eval: &EvalSession<'d>,
path: &Symbol<'d>,
e: Expression<'d>,
) -> Result<CondNode, ElabError> {
compile_cond_expr_in(eval, path, e, CondDialect::ModulePath)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum CondDialect {
ModulePath,
TimingCheck,
}
fn compile_cond_expr_in<'d>(
eval: &EvalSession<'d>,
path: &Symbol<'d>,
e: Expression<'d>,
dialect: CondDialect,
) -> Result<CondNode, ElabError> {
let e = if e.kind() == ExpressionKind::Conversion {
e.operand().unwrap_or(e)
} else {
e
};
match e.kind() {
ExpressionKind::UnaryOp => {
let uop = e.unary_op().ok_or_else(|| {
unsupported(
path,
"state-dependent path condition's unary expression is missing its own \
operator (unexpected AST shape)"
.to_string(),
)
})?;
let cop = CondUnaryOp::from_sv(uop).ok_or_else(|| {
unsupported(
path,
format!(
"state-dependent path condition uses unary operator `{uop:?}`, which is \
not one of LRM Table 30-1's own eight valid conditional-expression \
operators"
),
)
})?;
let operand = e.operand().ok_or_else(|| {
unsupported(
path,
"state-dependent path condition's unary expression is missing its own \
operand (unexpected AST shape)"
.to_string(),
)
})?;
Ok(CondNode::Unary(
cop,
Box::new(compile_cond_expr_in(eval, path, operand, dialect)?),
))
}
ExpressionKind::BinaryOp => {
let bop = e.binary_op().ok_or_else(|| {
unsupported(
path,
"state-dependent path condition's binary expression is missing its own \
operator (unexpected AST shape)"
.to_string(),
)
})?;
let mapped = match dialect {
CondDialect::ModulePath => CondBinaryOp::from_sv(bop),
CondDialect::TimingCheck => CondBinaryOp::from_sv_check(bop),
};
let cop = mapped.ok_or_else(|| {
unsupported(
path,
format!(
"state-dependent path condition uses binary operator `{bop:?}`, which \
is not one of LRM Table 30-1's own eight valid conditional-expression \
operators"
),
)
})?;
let l = e.left().ok_or_else(|| {
unsupported(
path,
"state-dependent path condition's binary expression is missing its own \
left operand (unexpected AST shape)"
.to_string(),
)
})?;
let r = e.right().ok_or_else(|| {
unsupported(
path,
"state-dependent path condition's binary expression is missing its own \
right operand (unexpected AST shape)"
.to_string(),
)
})?;
Ok(CondNode::Binary(
cop,
Box::new(compile_cond_expr_in(eval, path, l, dialect)?),
Box::new(compile_cond_expr_in(eval, path, r, dialect)?),
))
}
ExpressionKind::ConditionalOp => {
let conds = e.conditions();
if conds.len() != 1 || conds[0].pattern().is_some() {
return Err(unsupported(
path,
"state-dependent path condition uses a pattern-matching conditional \
operator (`&&&`-chained or `matches`), which LRM Table 30-1 does not list"
.to_string(),
));
}
let c = compile_cond_expr_in(eval, path, conds[0].expr(), dialect)?;
let t = e.true_value().ok_or_else(|| {
unsupported(
path,
"state-dependent path condition's conditional operator is missing its own \
true-value operand (unexpected AST shape)"
.to_string(),
)
})?;
let f = e.false_value().ok_or_else(|| {
unsupported(
path,
"state-dependent path condition's conditional operator is missing its own \
false-value operand (unexpected AST shape)"
.to_string(),
)
})?;
Ok(CondNode::Ternary(
Box::new(c),
Box::new(compile_cond_expr_in(eval, path, t, dialect)?),
Box::new(compile_cond_expr_in(eval, path, f, dialect)?),
))
}
ExpressionKind::Concatenation => {
let operands = e.operands();
if operands.is_empty() {
return Err(unsupported(
path,
"state-dependent path condition's concatenation has no operands \
(unexpected AST shape)"
.to_string(),
));
}
let mut out = Vec::with_capacity(operands.len());
for o in operands {
out.push(compile_cond_expr_in(eval, path, o, dialect)?);
}
Ok(CondNode::Concat(out))
}
ExpressionKind::Replication => {
let count_expr = e.replication_count().ok_or_else(|| {
unsupported(
path,
"state-dependent path condition's replication is missing its own count \
(unexpected AST shape)"
.to_string(),
)
})?;
let count = const_i64(eval, count_expr).ok_or_else(|| {
unsupported(
path,
"state-dependent path condition's replication count must be a compile-time \
constant"
.to_string(),
)
})?;
if count < 0 {
return Err(unsupported(
path,
"state-dependent path condition's replication count is negative".to_string(),
));
}
let unit = e.replication_concat().ok_or_else(|| {
unsupported(
path,
"state-dependent path condition's replication is missing its own repeated \
operand (unexpected AST shape)"
.to_string(),
)
})?;
Ok(CondNode::Replication(
count as u32,
Box::new(compile_cond_expr_in(eval, path, unit, dialect)?),
))
}
ExpressionKind::NamedValue
| ExpressionKind::HierarchicalValue
| ExpressionKind::ElementSelect
| ExpressionKind::RangeSelect => {
let (sym, bits) = resolve_cond_signal_ref(eval, path, e)?;
if sym.kind() == SymbolKind::Specparam {
Ok(CondNode::Specparam(sym.uid()))
} else {
Ok(CondNode::Signal((sym.uid(), bits)))
}
}
ExpressionKind::IntegerLiteral | ExpressionKind::UnbasedUnsizedIntegerLiteral => {
let c = match e.constant_value().or_else(|| eval.eval_constant(e)) {
Some(c) => c,
None => {
return Err(unsupported(
path,
"state-dependent path condition's integer literal operand did not fold \
to a constant (unexpected AST shape)"
.to_string(),
));
}
};
let int = match c.as_integer() {
Some(int) => int,
None => {
return Err(unsupported(
path,
"state-dependent path condition's integer literal operand did not fold \
to an integer constant (unexpected AST shape)"
.to_string(),
));
}
};
let bits = int.bits();
if bits.is_empty() && int.bit_width() > 0 {
return Err(unsupported(
path,
"state-dependent path condition's integer literal operand is too wide to \
expand bit-by-bit"
.to_string(),
));
}
Ok(CondNode::Const(bits.iter().map(|&b| bit4_of(b)).collect()))
}
ExpressionKind::RealLiteral
| ExpressionKind::TimeLiteral
| ExpressionKind::NullLiteral
| ExpressionKind::UnboundedLiteral
| ExpressionKind::StringLiteral => Err(unsupported(
path,
format!(
"state-dependent path condition operand of kind `{:?}` is not an integer \
constant number -- LRM 30.4.4.1's own \"compile-time constants\" operand kind \
means a constant NUMBER, never a real/time/null/unbounded/string literal",
e.kind()
),
)),
_ => Err(unsupported(
path,
format!(
"state-dependent path condition operand expression kind `{:?}` is none of LRM \
30.4.4.1's own operand kinds (a module input/inout port, a locally defined \
variable or net, their bit-/part-selects, or a compile-time constant number) \
and is not one of the operator shapes this function otherwise handles \
(unexpected AST shape)",
e.kind()
),
)),
}
}
fn resolve_condition<'d>(
eval: &EvalSession<'d>,
path: &Symbol<'d>,
) -> Result<PathCondition, ElabError> {
if !path.timing_path_is_state_dependent() {
return Ok(PathCondition::Unconditional);
}
match path.timing_path_condition_expr() {
Some(cond) => Ok(PathCondition::If(compile_cond_expr(eval, path, cond)?)),
None => Ok(PathCondition::IfNone),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TerminalShape {
Whole,
BitSelect,
PartSelect,
}
impl TerminalShape {
fn of(bits: &Option<(u32, u32)>) -> TerminalShape {
match bits {
None => TerminalShape::Whole,
Some((_, 1)) => TerminalShape::BitSelect,
Some((_, _)) => TerminalShape::PartSelect,
}
}
}
fn validate_state_dependent_groups(
instance: InstanceId,
paths: &[PathEntry],
) -> Result<(), ElabError> {
let mut groups: HashMap<(SymbolId, SymbolId), Vec<&PathEntry>> = HashMap::new();
for p in paths.iter().filter(|p| p.instance == instance) {
let key = (p.inputs[0].0.clone(), p.outputs[0].0.clone());
groups.entry(key).or_default().push(p);
}
for ((in_uid, out_uid), group) in &groups {
let has_ifnone = group
.iter()
.any(|p| matches!(p.condition, PathCondition::IfNone));
let has_unconditional = group
.iter()
.any(|p| matches!(p.condition, PathCondition::Unconditional));
if has_ifnone && has_unconditional {
return Err(ElabError::UnsupportedConstruct {
hier_path: format!("{in_uid} -> {out_uid}"),
detail: "an `ifnone` condition and an unconditional module path both declared \
for the same module path source/destination pair (LRM 30.4.4.4: \"It \
is illegal to specify both an ifnone condition for a module path and \
an unconditional simple module path for the same module path.\")"
.to_string(),
});
}
let mut shape: Option<TerminalShape> = None;
for p in group
.iter()
.filter(|p| !matches!(p.condition, PathCondition::Unconditional))
{
let s = TerminalShape::of(&p.outputs[0].1);
match shape {
None => shape = Some(s),
Some(prev) if prev == s => {}
Some(_) => {
return Err(ElabError::UnsupportedConstruct {
hier_path: format!("{in_uid} -> {out_uid}"),
detail: "state-dependent module path declarations for the same path \
reference their destination port in different ways (whole \
port vs. bit-select vs. part-select) -- LRM 30.4.4.3: \"The \
port is referenced in the same way in all path declarations \
(entire port, bit-select, or part-select).\""
.to_string(),
});
}
}
}
}
Ok(())
}
fn discover_path<'d>(
eval: &EvalSession<'d>,
instance: InstanceId,
path: &Symbol<'d>,
ports: &HashMap<SymbolId, ArgumentDirection>,
trees: &[SyntaxTree],
) -> Result<Vec<PathEntry>, ElabError> {
let raw_inputs: Vec<_> = path.timing_path_inputs().collect();
let raw_outputs: Vec<_> = path.timing_path_outputs().collect();
if raw_inputs.is_empty() {
return Err(unsupported(
path,
"specify path has no input terminal sv-lang could resolve (LRM 30.4.1's source \
restriction already rejected it upstream, or it uses a terminal form this \
workspace does not resolve, e.g. `interface_identifier.port_identifier`)"
.to_string(),
));
}
if raw_outputs.is_empty() {
return Err(unsupported(
path,
"specify path has no output terminal sv-lang could resolve (LRM 30.4.1's \
destination restriction already rejected it upstream, or it uses a terminal form \
this workspace does not resolve)"
.to_string(),
));
}
let (syntax_input_count, syntax_output_count) = syntax_terminal_counts(path, trees)?;
if raw_inputs.len() != syntax_input_count {
return Err(unsupported(
path,
format!(
"specify path's `list_of_path_inputs` names {syntax_input_count} terminal(s) in \
the source, but only {} resolved (LRM 30.4.1: \"the module path source shall be \
a net ... connected to an input or inout port\") -- at least one sibling input \
terminal violates this restriction and was silently dropped by sv-lang's own \
per-terminal filtering rather than failing the whole declaration",
raw_inputs.len()
),
));
}
if raw_outputs.len() != syntax_output_count {
return Err(unsupported(
path,
format!(
"specify path's output terminal list names {syntax_output_count} terminal(s) in \
the source, but only {} resolved (LRM 30.4.1: \"the module path destination \
shall be a net or variable ... connected to an output or inout port\") -- at \
least one sibling output/destination terminal violates this restriction and \
was silently dropped by sv-lang's own per-terminal filtering rather than \
failing the whole declaration",
raw_outputs.len()
),
));
}
let inputs = raw_inputs
.into_iter()
.map(|e| resolve_terminal(eval, path, ports, e, true))
.collect::<Result<Vec<_>, _>>()?;
let outputs = raw_outputs
.into_iter()
.map(|e| resolve_terminal(eval, path, ports, e, false))
.collect::<Result<Vec<_>, _>>()?;
let connection = path.timing_path_connection_kind();
let polarity = path.timing_path_polarity();
let path_uid = path.uid();
let (edge, edge_source) = PathEdge::from_accessors(path);
let edge_data_source_name = edge_source
.and_then(|e| e.referenced_symbol())
.map(|s| s.name().to_string());
let condition = resolve_condition(eval, path)?;
let mut out = Vec::new();
match connection {
sv_lang::TimingPathConnectionKind::Full => {
for o in &outputs {
for i in &inputs {
out.push(PathEntry {
instance,
inputs: vec![i.clone()],
outputs: vec![o.clone()],
connection,
path_uid: path_uid.clone(),
delays: PathDelaySet::default(),
polarity,
edge,
edge_data_source_name: edge_data_source_name.clone(),
condition: condition.clone(),
reject_limit: PathDelaySet::default(),
error_limit: PathDelaySet::default(),
pulse_style: PulseStyle::default(),
showcancelled: false,
});
}
}
}
sv_lang::TimingPathConnectionKind::Parallel => {
if inputs.len() != 1 || outputs.len() != 1 {
return Err(unsupported(
path,
format!(
"parallel (`=>`) module path with {} input(s) and {} output(s) \
(unexpected AST shape — LRM Annex A.7.2's `parallel_path_description` \
grammar only allows a single terminal on each side)",
inputs.len(),
outputs.len()
),
));
}
out.push(PathEntry {
instance,
inputs,
outputs,
connection,
path_uid,
delays: PathDelaySet::default(),
polarity,
edge,
edge_data_source_name,
condition,
reject_limit: PathDelaySet::default(),
error_limit: PathDelaySet::default(),
pulse_style: PulseStyle::default(),
showcancelled: false,
});
}
}
Ok(out)
}
fn syntax_terminal_counts<'d>(
path: &Symbol<'d>,
trees: &[SyntaxTree],
) -> Result<(usize, usize), ElabError> {
let node = trees
.iter()
.find_map(|tree| path.syntax(tree))
.ok_or_else(|| {
unsupported(
path,
"specify path declaration has no syntax node in any of the design's syntax \
trees (unexpected AST shape)"
.to_string(),
)
})?;
let decl = match node.kind() {
SyntaxKind::PathDeclaration => <PathDeclarationSyntax as sv_lang::AstNode>::cast(node),
SyntaxKind::ConditionalPathDeclaration => {
<ConditionalPathDeclarationSyntax as sv_lang::AstNode>::cast(node).map(|n| n.path())
}
SyntaxKind::IfNonePathDeclaration => {
<IfNonePathDeclarationSyntax as sv_lang::AstNode>::cast(node).map(|n| n.path())
}
other => {
return Err(unsupported(
path,
format!(
"specify path declaration's own syntax node is of kind `{other:?}` \
(expected PathDeclaration, ConditionalPathDeclaration or \
IfNonePathDeclaration -- unexpected AST shape)"
),
));
}
}
.ok_or_else(|| {
unsupported(
path,
"specify path declaration's own syntax node failed to cast to its expected shape \
(unexpected AST shape)"
.to_string(),
)
})?;
let desc = decl.desc();
let input_count = desc.inputs().iter().count();
let output_count = match desc.suffix() {
PathSuffixSyntax::SimplePathSuffix(s) => s.outputs().iter().count(),
PathSuffixSyntax::EdgeSensitivePathSuffix(s) => s.outputs().iter().count(),
};
Ok((input_count, output_count))
}
#[cfg(test)]
mod tests {
use sv_lang::{Compilation, ConstantValue, Design, Session, Symbol};
use sv_lang_kinds::SymbolKind;
use vj_abi::bit4::Bit4;
use super::{PathDelaySet, PathEdge, PulseStyle, TimingModel};
use crate::netlist::InstanceId;
use crate::symid::SymbolUid;
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")
}
#[test]
fn transitions_array_round_trips_in_the_documented_order() {
let t = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21];
let set = PathDelaySet::from_transitions(t);
assert_eq!(set.transitions(), t);
for (i, &(from, to)) in super::TRANSITIONS.iter().enumerate() {
assert_eq!(set.of(from, to), t[i], "{from:?}->{to:?}");
}
assert_eq!(PathDelaySet::from_list(&t).unwrap(), set);
}
#[test]
fn at_least_raises_only_the_delays_below_the_floor() {
let set = PathDelaySet::from_list(&[2, 9, 4, 3, 12, 1]).expect("six values");
let raised = set.at_least(5);
for (from, to) in [
(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::X, Bit4::One),
(Bit4::One, Bit4::X),
] {
assert_eq!(
raised.of(from, to),
set.of(from, to).max(5),
"{from:?}->{to:?}"
);
}
assert_eq!(raised.of(Bit4::One, Bit4::Zero), 9);
assert_eq!(raised.of(Bit4::Zero, Bit4::One), 5);
assert_eq!(raised.min_all(), 5);
assert_eq!(set.at_least(0), set);
}
#[test]
fn seed_specparams_round_trips_constant_value() {
let design =
compile("module m;\n specparam tRise = 10;\n specparam tFall = 7;\nendmodule\n");
let body = top_body(&design);
let mut model = TimingModel::new();
model.seed_specparams(&[(InstanceId(0), body)]);
let rise_uid = body
.members()
.find(|s| s.name() == "tRise")
.expect("tRise member")
.uid();
let fall_uid = body
.members()
.find(|s| s.name() == "tFall")
.expect("tFall member")
.uid();
assert_eq!(
model
.specparam(InstanceId(0), &rise_uid)
.and_then(ConstantValue::as_i64),
Some(10)
);
assert_eq!(
model
.specparam(InstanceId(0), &fall_uid)
.and_then(ConstantValue::as_i64),
Some(7)
);
assert!(model.specparam(InstanceId(1), &rise_uid).is_none());
}
#[test]
fn set_specparam_overwrites_the_seeded_value() {
let design =
compile("module m;\n specparam tRise = 10;\n specparam tFall = 22;\nendmodule\n");
let body = top_body(&design);
let mut model = TimingModel::new();
model.seed_specparams(&[(InstanceId(0), body)]);
let uid = body
.members()
.find(|s| s.name() == "tRise")
.expect("tRise member")
.uid();
assert_eq!(
model
.specparam(InstanceId(0), &uid)
.and_then(ConstantValue::as_i64),
Some(10)
);
let new_value = body
.members()
.find(|s| s.name() == "tFall")
.expect("tFall member")
.constant_value()
.expect("tFall has a constant value");
model.set_specparam(InstanceId(0), uid.clone(), new_value);
assert_eq!(
model
.specparam(InstanceId(0), &uid)
.and_then(ConstantValue::as_i64),
Some(22)
);
}
#[test]
fn seed_specparams_finds_specparams_declared_inside_a_specify_block() {
let design = compile(
"module m(input a, output q);\n assign q = a;\n specify\n (a => q) = tRise;\n \
specparam tRise = 10;\n specparam PATHPULSE$ = 3;\n endspecify\nendmodule\n",
);
let body = top_body(&design);
let mut model = TimingModel::new();
model.seed_specparams(&[(InstanceId(0), body)]);
let specify = body
.members()
.find(|s| s.kind() == SymbolKind::SpecifyBlock)
.expect("specify block member");
let rise_uid = specify
.members()
.find(|s| s.kind() == SymbolKind::Specparam && s.name() == "tRise")
.expect("tRise member inside the specify block")
.uid();
let pulse_uid = specify
.members()
.find(|s| s.kind() == SymbolKind::Specparam && s.name() == "PATHPULSE$")
.expect("PATHPULSE$ member inside the specify block")
.uid();
assert_eq!(
model
.specparam(InstanceId(0), &rise_uid)
.and_then(ConstantValue::as_i64),
Some(10),
"a plain specparam declared inside the specify block must be seeded"
);
assert_eq!(
model
.specparam(InstanceId(0), &pulse_uid)
.and_then(ConstantValue::as_i64),
Some(3),
"a PATHPULSE$ specparam -- legal only inside a specify block (30.7.1) -- must be \
seeded too"
);
}
#[test]
fn from_list_one_value_gives_every_known_transition_the_same_delay() {
let d = PathDelaySet::from_list(&[10]).expect("a 1-value list is supported");
for (from, to) in [
(Bit4::Zero, Bit4::One),
(Bit4::One, Bit4::Zero),
(Bit4::Zero, Bit4::Z),
(Bit4::Z, Bit4::One),
(Bit4::One, Bit4::Z),
(Bit4::Z, Bit4::Zero),
] {
assert_eq!(d.of(from, to), 10, "{from:?} -> {to:?}");
}
}
#[test]
fn from_list_two_values_split_rise_and_fall_and_z_follows_the_matching_side() {
let d = PathDelaySet::from_list(&[3, 5]).expect("a 2-value list is supported");
assert_eq!(d.of(Bit4::Zero, Bit4::One), 3, "0->1 is trise");
assert_eq!(d.of(Bit4::Z, Bit4::One), 3, "z->1 is trise");
assert_eq!(d.of(Bit4::One, Bit4::Zero), 5, "1->0 is tfall");
assert_eq!(d.of(Bit4::Z, Bit4::Zero), 5, "z->0 is tfall");
assert_eq!(d.of(Bit4::Zero, Bit4::Z), 3, "0->z is trise");
assert_eq!(d.of(Bit4::One, Bit4::Z), 5, "1->z is tfall");
}
#[test]
fn from_list_three_values_give_z_its_own_delay_distinct_from_rise_and_fall() {
let d = PathDelaySet::from_list(&[3, 5, 7]).expect("a 3-value list is supported");
assert_eq!(d.of(Bit4::Zero, Bit4::One), 3);
assert_eq!(d.of(Bit4::One, Bit4::Zero), 5);
assert_eq!(d.of(Bit4::Zero, Bit4::Z), 7, "0->z is tz, not trise");
assert_eq!(d.of(Bit4::One, Bit4::Z), 7, "1->z is tz, not tfall");
assert_eq!(d.of(Bit4::Z, Bit4::One), 3);
assert_eq!(d.of(Bit4::Z, Bit4::Zero), 5);
}
#[test]
fn from_list_derives_x_transitions_via_table_30_3s_general_algorithm() {
let d = PathDelaySet::from_list(&[5, 12, 17]).expect("a 3-value list is supported");
assert_eq!(
d.of(Bit4::Zero, Bit4::X),
5,
"0->x = min(0->z=17, 0->1=5) = 5"
);
assert_eq!(
d.of(Bit4::One, Bit4::X),
12,
"1->x = min(1->z=17, 1->0=12) = 12"
);
assert_eq!(d.of(Bit4::Z, Bit4::X), 5, "z->x = min(z->1=5, z->0=12) = 5");
assert_eq!(
d.of(Bit4::X, Bit4::Zero),
12,
"x->0 = max(z->0=12, 1->0=12) = 12"
);
assert_eq!(
d.of(Bit4::X, Bit4::One),
5,
"x->1 = max(z->1=5, 0->1=5) = 5"
);
assert_eq!(
d.of(Bit4::X, Bit4::Z),
17,
"x->z = max(1->z=17, 0->z=17) = 17"
);
}
#[test]
fn from_list_one_value_gives_all_twelve_entries_the_same_delay() {
let d = PathDelaySet::from_list(&[10]).expect("a 1-value list is supported");
for (from, to) in [
(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),
] {
assert_eq!(d.of(from, to), 10, "{from:?} -> {to:?}");
}
}
#[test]
fn from_list_six_value_list_takes_each_known_transition_directly_no_grouping() {
let d = PathDelaySet::from_list(&[1, 2, 3, 4, 5, 6]).expect("a 6-value list is supported");
assert_eq!(d.of(Bit4::Zero, Bit4::One), 1, "t01");
assert_eq!(d.of(Bit4::One, Bit4::Zero), 2, "t10");
assert_eq!(d.of(Bit4::Zero, Bit4::Z), 3, "t0z");
assert_eq!(d.of(Bit4::Z, Bit4::One), 4, "tz1 -- independent of t01");
assert_eq!(d.of(Bit4::One, Bit4::Z), 5, "t1z -- independent of t0z");
assert_eq!(d.of(Bit4::Z, Bit4::Zero), 6, "tz0 -- independent of t10");
}
#[test]
fn from_list_six_value_reproduces_the_lrm_worked_example_exactly() {
let d =
PathDelaySet::from_list(&[5, 12, 17, 10, 6, 22]).expect("a 6-value list is supported");
assert_eq!(d.of(Bit4::Zero, Bit4::One), 5, "t01");
assert_eq!(d.of(Bit4::One, Bit4::Zero), 12, "t10");
assert_eq!(d.of(Bit4::Zero, Bit4::Z), 17, "t0z");
assert_eq!(d.of(Bit4::Z, Bit4::One), 10, "tz1");
assert_eq!(d.of(Bit4::One, Bit4::Z), 6, "t1z");
assert_eq!(d.of(Bit4::Z, Bit4::Zero), 22, "tz0");
assert_eq!(d.of(Bit4::Zero, Bit4::X), 5, "0->x = min(17, 5) = 5");
assert_eq!(d.of(Bit4::One, Bit4::X), 6, "1->x = min(6, 12) = 6");
assert_eq!(d.of(Bit4::Z, Bit4::X), 10, "z->x = min(10, 22) = 10");
assert_eq!(d.of(Bit4::X, Bit4::Zero), 22, "x->0 = max(22, 12) = 22");
assert_eq!(d.of(Bit4::X, Bit4::One), 10, "x->1 = max(10, 5) = 10");
assert_eq!(d.of(Bit4::X, Bit4::Z), 17, "x->z = max(6, 17) = 17");
}
#[test]
fn from_list_twelve_value_list_is_all_explicit_no_derivation() {
let d = PathDelaySet::from_list(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
.expect("a 12-value list is supported");
assert_eq!(d.of(Bit4::Zero, Bit4::One), 1, "t01");
assert_eq!(d.of(Bit4::One, Bit4::Zero), 2, "t10");
assert_eq!(d.of(Bit4::Zero, Bit4::Z), 3, "t0z");
assert_eq!(d.of(Bit4::Z, Bit4::One), 4, "tz1");
assert_eq!(d.of(Bit4::One, Bit4::Z), 5, "t1z");
assert_eq!(d.of(Bit4::Z, Bit4::Zero), 6, "tz0");
assert_eq!(
d.of(Bit4::Zero, Bit4::X),
7,
"t0x -- given directly, not derived"
);
assert_eq!(
d.of(Bit4::X, Bit4::One),
8,
"tx1 -- given directly, not derived"
);
assert_eq!(
d.of(Bit4::One, Bit4::X),
9,
"t1x -- given directly, not derived"
);
assert_eq!(
d.of(Bit4::X, Bit4::Zero),
10,
"tx0 -- given directly, not derived"
);
assert_eq!(
d.of(Bit4::X, Bit4::Z),
11,
"txz -- given directly, not derived"
);
assert_eq!(
d.of(Bit4::Z, Bit4::X),
12,
"tzx -- given directly, not derived"
);
}
#[test]
fn from_list_rejects_any_length_other_than_1_2_3_6_or_12() {
for len in [4, 5, 7, 8, 9, 10, 11, 13] {
let d = vec![1u64; len];
let err = PathDelaySet::from_list(&d).unwrap_err();
assert!(
err.contains(&len.to_string()),
"the error must name the bad length, not just say \"unsupported\": {err}"
);
}
}
#[test]
fn set_path_delays_attaches_to_every_desugared_entry_sharing_one_path_uid() {
let mut design = compile(
"module m(input a, input b, output q);\n assign q = a | b;\n specify\n (a, b \
*> q) = 10;\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let mut model = TimingModel::new();
model
.discover(&eval, &[(InstanceId(0), body)])
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(model.paths.len(), 2);
let path_uid = model.paths[0].path_uid.clone();
assert_eq!(
model.paths[1].path_uid, path_uid,
"both entries share one path_uid"
);
let delays = PathDelaySet::from_list(&[3, 5]).expect("a 2-value list is supported");
model.set_path_delays(InstanceId(0), &path_uid, delays);
assert_eq!(model.paths[0].delays, delays);
assert_eq!(model.paths[1].delays, delays);
}
const ALL_TRANSITIONS: [(Bit4, Bit4); 12] = [
(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),
];
#[test]
fn pulse_limits_default_to_each_transitions_own_delay() {
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 mut model = TimingModel::new();
model
.discover(&eval, &[(InstanceId(0), body)])
.unwrap_or_else(|e| panic!("discover failed: {e}"));
let path_uid = model.paths[0].path_uid.clone();
let delays = PathDelaySet::from_list(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
.expect("a 12-value list is supported");
model.set_path_delays(InstanceId(0), &path_uid, delays);
for (from, to) in ALL_TRANSITIONS {
let d = delays.of(from, to);
assert_eq!(
model.paths[0].reject_limit.of(from, to),
d,
"{from:?}->{to:?}"
);
assert_eq!(
model.paths[0].error_limit.of(from, to),
d,
"{from:?}->{to:?}"
);
}
assert_eq!(
model.paths[0].reject_limit.of(Bit4::Zero, Bit4::One),
1,
"twelve DISTINCT limits, not one copied value"
);
assert_eq!(model.paths[0].reject_limit.of(Bit4::Z, Bit4::X), 12);
}
#[test]
fn set_path_pulse_limits_reaches_every_entry_and_clamps_error_to_reject() {
let mut design = compile(
"module m(input a, input b, output q);\n assign q = a | b;\n specify\n (a, b \
*> q) = 10;\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let mut model = TimingModel::new();
model
.discover(&eval, &[(InstanceId(0), body)])
.unwrap_or_else(|e| panic!("discover failed: {e}"));
let path_uid = model.paths[0].path_uid.clone();
let delays = PathDelaySet::from_list(&[10]).expect("a 1-value list is supported");
model.set_path_delays(InstanceId(0), &path_uid, delays);
model.set_path_pulse_limits(InstanceId(0), &path_uid, 2, 6);
for p in &model.paths {
for (from, to) in ALL_TRANSITIONS {
assert_eq!(p.reject_limit.of(from, to), 2);
assert_eq!(p.error_limit.of(from, to), 6);
}
assert_eq!(p.delays, delays, "pulse limits never touch the delays");
}
model.set_path_pulse_limits(InstanceId(0), &path_uid, 6, 2);
for p in &model.paths {
assert_eq!(p.reject_limit.of(Bit4::Zero, Bit4::One), 6);
assert_eq!(
p.error_limit.of(Bit4::Zero, Bit4::One),
6,
"error < reject is raised to the reject limit"
);
}
let longer = PathDelaySet::from_list(&[20]).expect("a 1-value list is supported");
model.set_path_delays(InstanceId(0), &path_uid, longer);
assert_eq!(model.paths[0].reject_limit, longer);
assert_eq!(model.paths[1].error_limit, longer);
}
#[test]
fn apply_pulse_percent_scales_each_transitions_own_delay() {
let mut design = compile(
"module m(input a, input b, output q);\n assign q = a | b;\n specify\n (a, b \
*> q) = 10;\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let mut model = TimingModel::new();
model
.discover(&eval, &[(InstanceId(0), body)])
.unwrap_or_else(|e| panic!("discover failed: {e}"));
let path_uid = model.paths[0].path_uid.clone();
let delays = PathDelaySet::from_list(&[10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120])
.expect("a 12-value list is supported");
model.set_path_delays(InstanceId(0), &path_uid, delays);
model.apply_pulse_percent(50.0, 25.0);
for p in &model.paths {
assert_eq!(p.delays, delays, "limits never touch the delays");
for (from, to) in ALL_TRANSITIONS {
let d = delays.of(from, to);
assert_eq!(
p.reject_limit.of(from, to),
d / 2,
"reject {from:?}->{to:?}"
);
assert_eq!(
p.error_limit.of(from, to),
d / 2,
"error 25% < reject 50% is raised to the reject limit"
);
}
}
model.apply_pulse_percent(20.0, 90.0);
for p in &model.paths {
for (from, to) in ALL_TRANSITIONS {
let d = delays.of(from, to);
assert_eq!(p.reject_limit.of(from, to), d / 5);
assert_eq!(p.error_limit.of(from, to), d * 9 / 10);
}
}
model.apply_pulse_percent(100.0, 100.0);
assert_eq!(model.paths[0].reject_limit, delays);
assert_eq!(model.paths[1].error_limit, delays);
let odd = PathDelaySet::from_list(&[3]).expect("a 1-value list is supported");
model.set_path_delays(InstanceId(0), &path_uid, odd);
model.apply_pulse_percent(50.0, 100.0);
assert_eq!(model.paths[0].reject_limit.of(Bit4::Zero, Bit4::One), 2);
assert_eq!(model.paths[0].error_limit.of(Bit4::Zero, Bit4::One), 3);
}
#[test]
fn pulse_options_override_declarations_and_an_absent_option_leaves_them() {
let mut design = compile(
"module m(input a, output q, output r, output s);\n assign q = a;\n assign r = a;\n \
assign s = a;\n specify\n pulsestyle_ondetect q;\n showcancelled q;\n \
noshowcancelled r;\n (a => q) = 1;\n (a => r) = 1;\n (a => s) = 1;\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let settings = |model: &TimingModel| -> Vec<(PulseStyle, bool)> {
["q", "r", "s"]
.iter()
.map(|out| {
let p = model
.paths
.iter()
.find(|p| p.outputs[0].0.path().rsplit('.').next() == Some(*out))
.unwrap_or_else(|| panic!("no path to {out}"));
(p.pulse_style, p.showcancelled)
})
.collect()
};
let mut plain = TimingModel::new();
plain
.discover(&eval, &[(InstanceId(0), body)])
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(
settings(&plain),
vec![
(PulseStyle::OnDetect, true),
(PulseStyle::OnEvent, false),
(PulseStyle::OnEvent, false)
],
"declarations, then the built-in default: on-event, noshowcancelled"
);
let discovered = |style: Option<PulseStyle>, show: Option<bool>| {
let mut model = TimingModel::new();
model.set_pulse_options(style, show);
model
.discover(&eval, &[(InstanceId(0), body)])
.unwrap_or_else(|e| panic!("discover failed: {e}"));
settings(&model)
};
use PulseStyle::{OnDetect, OnEvent};
assert_eq!(
discovered(Some(OnDetect), Some(true)),
vec![(OnDetect, true), (OnDetect, true), (OnDetect, true)]
);
assert_eq!(
discovered(Some(OnEvent), None),
vec![(OnEvent, true), (OnEvent, false), (OnEvent, false)]
);
assert_eq!(
discovered(None, Some(false)),
vec![(OnDetect, false), (OnEvent, false), (OnEvent, false)]
);
assert_eq!(
discovered(None, Some(true)),
vec![(OnDetect, true), (OnEvent, true), (OnEvent, true)]
);
}
#[test]
fn discover_resolves_positive_polarity_on_a_full_connection_path() {
use sv_lang::TimingPathPolarity;
let mut design = compile(
"module m(input a, output q);\n assign q = a;\n specify\n (a +*> q) = 1;\n \
endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let mut model = TimingModel::new();
model
.discover(&eval, &[(InstanceId(0), body)])
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(model.paths.len(), 1);
assert_eq!(
model.paths[0].debug_polarity(),
TimingPathPolarity::Positive
);
}
#[test]
fn discover_resolves_negative_polarity_on_a_parallel_connection_path() {
use sv_lang::TimingPathPolarity;
let mut design = compile(
"module m(input a, output q);\n assign q = a;\n specify\n (a -=> q) = 1;\n \
endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let mut model = TimingModel::new();
model
.discover(&eval, &[(InstanceId(0), body)])
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(model.paths.len(), 1);
assert_eq!(
model.paths[0].debug_polarity(),
TimingPathPolarity::Negative
);
}
#[test]
fn discover_resolves_unknown_polarity_when_no_polarity_operator_is_written() {
use sv_lang::TimingPathPolarity;
let mut design = compile(
"module m(input a, output q);\n assign q = a;\n specify\n (a => q) = 1;\n \
endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let mut model = TimingModel::new();
model
.discover(&eval, &[(InstanceId(0), body)])
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(model.paths.len(), 1);
assert_eq!(model.paths[0].debug_polarity(), TimingPathPolarity::Unknown);
}
#[test]
fn discover_resolves_specific_posedge_on_an_edge_sensitive_path() {
use sv_lang::EdgeKind;
let mut design = compile(
"module m(input clock, in, output out);\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 mut model = TimingModel::new();
model
.discover(&eval, &[(InstanceId(0), body)])
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(model.paths.len(), 1);
assert_eq!(model.paths[0].edge, PathEdge::Specific(EdgeKind::PosEdge));
assert_eq!(
model.paths[0].debug_edge_source_expr_name(),
Some("in"),
"the `: in` data-source expression's referenced symbol name must round-trip"
);
}
#[test]
fn discover_resolves_specific_negedge_on_an_edge_sensitive_path() {
use sv_lang::EdgeKind;
let mut design = compile(
"module m(input clock, in, output out);\n specify\n (negedge clock => (out -: \
in)) = (10, 8);\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let mut model = TimingModel::new();
model
.discover(&eval, &[(InstanceId(0), body)])
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(model.paths.len(), 1);
assert_eq!(model.paths[0].edge, PathEdge::Specific(EdgeKind::NegEdge));
}
#[test]
fn discover_resolves_edge_sensitive_any_transition_when_no_edge_identifier_is_written() {
let mut design = compile(
"module m(input clock, in, output out);\n specify\n (clock => (out : in)) = \
(10, 8);\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let mut model = TimingModel::new();
model
.discover(&eval, &[(InstanceId(0), body)])
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(model.paths.len(), 1);
assert_eq!(model.paths[0].edge, PathEdge::Any);
}
#[test]
fn discover_resolves_no_edge_sensitivity_on_a_plain_simple_path() {
let mut design = compile(
"module m(input a, output q);\n assign q = a;\n specify\n (a => q) = 1;\n \
endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let mut model = TimingModel::new();
model
.discover(&eval, &[(InstanceId(0), body)])
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(model.paths.len(), 1);
assert_eq!(model.paths[0].edge, PathEdge::None);
assert_eq!(model.paths[0].debug_edge_source_expr_name(), None);
}
#[test]
fn discover_resolves_specific_posedge_even_with_the_simple_destination_suffix() {
use sv_lang::EdgeKind;
let mut design = compile(
"module m(input clk, output q);\n specify\n (posedge clk => q) = 1;\n \
endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let mut model = TimingModel::new();
model
.discover(&eval, &[(InstanceId(0), body)])
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(model.paths.len(), 1);
assert_eq!(model.paths[0].edge, PathEdge::Specific(EdgeKind::PosEdge));
assert_eq!(model.paths[0].debug_edge_source_expr_name(), None);
}
#[test]
fn discover_shares_one_edge_spec_across_every_desugared_entry_of_a_full_connection() {
let mut design = compile(
"module m(input clock, input a, input b, output q1, output q2);\n specify\n \
(posedge clock *> (q1, q2 : a)) = (10, 8);\n endspecify\nendmodule\n",
);
let eval = design.eval_session();
let body = top_body(eval.design());
let mut model = TimingModel::new();
model
.discover(&eval, &[(InstanceId(0), body)])
.unwrap_or_else(|e| panic!("discover failed: {e}"));
assert_eq!(
model.paths.len(),
2,
"two desugared (clock*>q1), (clock*>q2) entries"
);
for p in &model.paths {
assert_eq!(p.edge, PathEdge::Specific(sv_lang::EdgeKind::PosEdge));
assert_eq!(p.debug_edge_source_expr_name(), Some("a"));
}
}
}