use std::collections::HashSet;
use sv_lang::kinds::{StatementKind, SymbolKind, TimingControlKind};
use sv_lang::{AnalyzedProcedure, EdgeKind, SemNode, Symbol, TimingControl};
use super::AnalysisError;
use crate::symid::{SymbolId, SymbolUid, is_value_name};
pub fn infer_structural_clock<'d>(
block: &Symbol<'d>,
) -> Result<Option<TimingControl<'d>>, AnalysisError> {
let unsupported = |what: &str| {
AnalysisError::Unsupported(format!(
"clock inference for procedure `{}` of a procedural checker instance: {what}",
block.hierarchical_path()
))
};
let Some(body) = crate::process::cfg::proc_body(block) else {
return Ok(None);
};
let mut referenced: HashSet<SymbolId> = HashSet::new();
let mut events: Vec<TimingControl<'d>> = Vec::new();
let mut stack: Vec<SemNode<'d>> = vec![body.as_sem_node()];
while let Some(n) = stack.pop() {
if n.as_timing_control().is_some() {
return Err(unsupported(
"a timing control that is not on a statement (an intra-assignment delay) is not \
modelled",
));
}
if let Some(e) = n.as_expression() {
if is_value_name(e.kind()) {
if let Some(sym) = e.referenced_symbol() {
referenced.insert(sym.uid());
}
continue;
}
} else if let Some(stmt) = n.as_statement() {
let kind = stmt.kind();
if kind == StatementKind::ConcurrentAssertion {
continue;
}
if kind == StatementKind::Wait
|| kind == StatementKind::WaitFork
|| kind == StatementKind::WaitOrder
{
return Ok(None);
}
if kind == StatementKind::Timed {
let tc = stmt
.timing_control()
.ok_or_else(|| unsupported("a timed statement with no timing control"))?;
if tc.kind() == TimingControlKind::SignalEvent
|| tc.kind() == TimingControlKind::EventList
{
events.push(tc);
} else {
return Ok(None);
}
stack.extend(
n.children()
.into_iter()
.filter(|c| c.as_timing_control().is_none()),
);
continue;
}
}
stack.extend(n.children());
}
let [control] = events.as_slice() else {
return Ok(None);
};
let candidates: Vec<TimingControl<'d>> = if control.kind() == TimingControlKind::EventList {
let mut out = Vec::new();
for c in control.children() {
let Some(tc) = c
.as_timing_control()
.filter(|t| t.kind() == TimingControlKind::SignalEvent)
else {
return Err(unsupported(
"an event list with a member that is not a signal event",
));
};
out.push(tc);
}
out
} else {
vec![*control]
};
let mut found: Option<TimingControl<'d>> = None;
for cand in candidates {
let Some(expr) = cand.children().first().and_then(|c| c.as_expression()) else {
return Err(unsupported("an event with no signal expression"));
};
if cand.edge() == EdgeKind::None {
let is_event_or_clocking = expr.referenced_symbol().is_some_and(|s| {
s.kind() == SymbolKind::ClockingBlock
|| expr.expr_type().is_some_and(|t| t.is_event())
});
if is_event_or_clocking && cand.children().len() == 1 {
return Err(unsupported(
"an event variable or clocking block as the clocking event is not modelled",
));
}
continue;
}
let mut shared = false;
let mut terms: Vec<SemNode<'d>> = vec![expr.as_sem_node()];
while let Some(t) = terms.pop() {
if let Some(e) = t.as_expression()
&& is_value_name(e.kind())
{
shared |= e
.referenced_symbol()
.is_some_and(|s| referenced.contains(&s.uid()));
continue;
}
terms.extend(t.children());
}
if !shared {
if found.is_some() {
return Ok(None);
}
found = Some(cand);
}
}
Ok(found)
}
pub fn infer_procedural_clock<'d>(
proc: &AnalyzedProcedure<'d>,
) -> Result<Option<TimingControl<'d>>, AnalysisError> {
match proc.inferred_clock() {
None => Ok(None),
Some(node) => node.as_timing_control().map(Some).ok_or_else(|| {
AnalysisError::Unsupported(format!(
"AnalyzedProcedure::inferred_clock() for procedure `{}` returned a SemNode of \
kind `{}` that is not a TimingControl -- sv-lang API shape assumption violated",
proc.symbol().hierarchical_path(),
node.kind_name(),
))
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use sv_lang::kinds::TimingControlKind;
use sv_lang::{AnalysisFlags, BinaryOp, Compilation, EdgeKind, Session};
fn compile(src: &str) -> sv_lang::Design {
let session = Session::new();
let mut comp = Compilation::new(&session).expect("session");
comp.add_source(src).expect("add_source");
comp.compile().expect("compiles")
}
fn the_one_procedure<'d>(
design: &'d sv_lang::Design,
analysis: &'d sv_lang::Analysis<'d>,
) -> AnalyzedProcedure<'d> {
let body = design
.top_instances()
.next()
.expect("one top instance")
.instance_body()
.expect("instance body");
let mut procs: Vec<_> = analysis.procedures(body).collect();
assert_eq!(procs.len(), 1, "fixture has exactly one procedure");
procs.remove(0)
}
#[test]
fn r1_p1_infers_posedge_mclk() {
let design = compile(
"module m;\n\
logic mclk, scanclk, q, d, d1;\n\
property r1; q != d; endproperty\n\
always @(posedge mclk) begin\n\
q <= d1;\n\
r1_p1: assert property (r1);\n\
r1_p2: assert property (@(posedge scanclk) r1);\n\
end\n\
endmodule\n",
);
let analysis = design.analyze(AnalysisFlags::NONE, 1).expect("analysis");
let proc = the_one_procedure(&design, &analysis);
let tc = infer_procedural_clock(&proc)
.expect("no API-shape error")
.expect("r1_p1's clock is inferred");
assert_eq!(tc.kind(), TimingControlKind::SignalEvent);
assert_eq!(tc.edge(), EdgeKind::PosEdge);
let children = tc.children();
assert_eq!(
children.len(),
1,
"no `iff` qualifier on r1's inferred clock"
);
let signal = children[0].as_expression().expect("event expression");
assert_eq!(
signal.referenced_symbol().map(|s| s.name().to_string()),
Some("mclk".to_string())
);
}
#[test]
fn r2_infers_posedge_clock_iff_reset_eq_0() {
let design = compile(
"module m;\n\
logic clock, reset, cnt, q, d1;\n\
property r2; q != d1; endproperty\n\
always_ff @(posedge clock iff reset == 0 or posedge reset) begin\n\
cnt <= reset ? 0 : cnt + 1;\n\
q <= $past(d1);\n\
r2_p: assert property (r2);\n\
end\n\
endmodule\n",
);
let analysis = design.analyze(AnalysisFlags::NONE, 1).expect("analysis");
let proc = the_one_procedure(&design, &analysis);
let tc = infer_procedural_clock(&proc)
.expect("no API-shape error")
.expect("r2's clock is inferred");
assert_eq!(tc.kind(), TimingControlKind::SignalEvent);
assert_eq!(tc.edge(), EdgeKind::PosEdge);
let children = tc.children();
assert_eq!(
children.len(),
2,
"r2's inferred clock keeps its `iff reset == 0` qualifier"
);
let signal = children[0].as_expression().expect("event expression");
assert_eq!(
signal.referenced_symbol().map(|s| s.name().to_string()),
Some("clock".to_string())
);
let iff = children[1].as_expression().expect("iff condition");
assert_eq!(iff.binary_op(), Some(BinaryOp::Equality));
let lhs = iff.left().expect("iff lhs");
assert_eq!(
lhs.referenced_symbol().map(|s| s.name().to_string()),
Some("reset".to_string()),
"the iff condition is `reset == 0`, not `posedge reset`'s own term"
);
}
#[test]
fn r3_infers_nothing_edge_identifier_missing() {
let design = compile(
"module m;\n\
logic clock, reset, cnt, q, d1;\n\
property r3; q != d1; endproperty\n\
always_ff @(clock iff reset == 0 or posedge reset) begin\n\
cnt <= reset ? 0 : cnt + 1;\n\
q <= $past(d1);\n\
r3_p: assert property (r3);\n\
end\n\
endmodule\n",
);
let analysis = design.analyze(AnalysisFlags::NONE, 1).expect("analysis");
let proc = the_one_procedure(&design, &analysis);
let inferred = infer_procedural_clock(&proc).expect("no API-shape error");
assert!(
inferred.is_none(),
"r3: no clock inferred, per the LRM's own text"
);
}
#[test]
fn default_clocking_fallback_is_indistinguishable_from_abc() {
let design = compile(
"module m;\n\
logic clock, reset, cnt, q, d1, other_sig;\n\
property r3; q != d1; endproperty\n\
clocking cb @(posedge other_sig);\n\
endclocking\n\
default clocking cb;\n\
always_ff @(clock iff reset == 0 or posedge reset) begin\n\
cnt <= reset ? 0 : cnt + 1;\n\
q <= $past(d1);\n\
r3_p: assert property (r3);\n\
end\n\
endmodule\n",
);
let analysis = design.analyze(AnalysisFlags::NONE, 1).expect("analysis");
let proc = the_one_procedure(&design, &analysis);
let tc = infer_procedural_clock(&proc)
.expect("no API-shape error")
.expect(
"the default-clocking fallback fires even though 16.14.6(a)-(c) fails exactly \
as it does in r3_infers_nothing_edge_identifier_missing",
);
assert_eq!(tc.kind(), TimingControlKind::SignalEvent);
assert_eq!(tc.edge(), EdgeKind::PosEdge);
let children = tc.children();
assert_eq!(
children.len(),
1,
"the default clocking block's own event carries no `iff` qualifier"
);
let signal = children[0].as_expression().expect("event expression");
assert_eq!(
signal.referenced_symbol().map(|s| s.name().to_string()),
Some("other_sig".to_string()),
"the returned clock is the DEFAULT CLOCKING's event, not derived from the \
procedure's own (a)-(c)-failing event control -- confirming the two sources are \
folded together and cannot be told apart from this function's return value"
);
}
#[test]
fn a_procedure_with_only_a_sampled_call_infers_its_clock() {
let design = compile(
"module m;\n\
logic clk, b, r;\n\
always @(posedge clk) r <= $rose(b);\n\
endmodule\n",
);
let analysis = design.analyze(AnalysisFlags::NONE, 1).expect("analysis");
let proc = the_one_procedure(&design, &analysis);
let tc = infer_procedural_clock(&proc)
.expect("no API-shape error")
.expect("the always block's clock is inferred");
assert_eq!(tc.edge(), EdgeKind::PosEdge);
}
#[test]
fn r4_infers_nothing_multiple_event_controls_and_delays() {
let design = compile(
"module m;\n\
logic mclk, q, q1, d1;\n\
property r4; q != d1; endproperty\n\
always @(posedge mclk) begin\n\
#10 q <= d1;\n\
@(negedge mclk)\n\
#10 q1 <= !d1;\n\
r4_p: assert property (r4);\n\
end\n\
endmodule\n",
);
let analysis = design.analyze(AnalysisFlags::NONE, 1).expect("analysis");
let proc = the_one_procedure(&design, &analysis);
let inferred = infer_procedural_clock(&proc).expect("no API-shape error");
assert!(
inferred.is_none(),
"r4: no clock inferred, per the LRM's own text"
);
}
fn structural(src: &str) -> Result<Option<(EdgeKind, String)>, AnalysisError> {
let design = compile(src);
let analysis = design.analyze(AnalysisFlags::NONE, 1).expect("analysis");
let proc = the_one_procedure(&design, &analysis);
let block = proc.symbol();
Ok(infer_structural_clock(&block)?.map(|tc| {
let signal = tc.children()[0].as_expression().expect("event expression");
let name = signal
.referenced_symbol()
.map(|s| s.name().to_string())
.expect("a named clock");
(tc.edge(), name)
}))
}
#[test]
fn structural_clock_is_the_single_edge_event() {
let inferred = structural(
"module m;\n\
logic mclk, q, d;\n\
always @(posedge mclk) begin\n\
q <= d;\n\
p: assert property (q != d);\n\
end\n\
endmodule\n",
)
.expect("no error");
assert_eq!(inferred, Some((EdgeKind::PosEdge, "mclk".to_string())));
}
#[test]
fn structural_clock_term_read_in_the_body_is_not_a_clock() {
let inferred = structural(
"module m;\n\
logic clk, q;\n\
always @(posedge clk) q <= clk;\n\
endmodule\n",
)
.expect("no error");
assert_eq!(inferred, None);
}
#[test]
fn structural_clock_blocking_delay_infers_nothing() {
let inferred = structural(
"module m;\n\
logic clk, q, d;\n\
always @(posedge clk) begin #1 q <= d; end\n\
endmodule\n",
)
.expect("no error");
assert_eq!(inferred, None);
}
#[test]
fn structural_clock_two_event_controls_infer_nothing() {
let inferred = structural(
"module m;\n\
logic mclk, q, q1, d1;\n\
always @(posedge mclk) begin\n\
@(negedge mclk) q1 <= !d1;\n\
q <= d1;\n\
end\n\
endmodule\n",
)
.expect("no error");
assert_eq!(inferred, None);
}
#[test]
fn structural_clock_event_variable_is_a_loud_error() {
let err = structural(
"module m;\n\
event e;\n\
logic q, d;\n\
always @(e) q <= d;\n\
endmodule\n",
)
.expect_err("an event-variable clock is refused");
assert!(
err.to_string().contains("event variable or clocking block"),
"{err}"
);
}
#[test]
fn structural_clock_intra_assignment_delay_is_a_loud_error() {
let err = structural(
"module m;\n\
logic clk, q, d;\n\
always @(posedge clk) q = #1 d;\n\
endmodule\n",
)
.expect_err("an intra-assignment delay is refused");
assert!(err.to_string().contains("intra-assignment delay"), "{err}");
}
}