use crate::symid::{SymbolId, SymbolUid};
use std::collections::BTreeSet;
use sv_lang::{EdgeKind, Expression, KnownSystemName, SemNode, Symbol, TimingControl};
use sv_lang_kinds::{ExpressionKind, SymbolKind, TimingControlKind};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SampledError(pub String);
impl core::fmt::Display for SampledError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.0)
}
}
fn err<T>(msg: impl Into<String>) -> Result<T, SampledError> {
Err(SampledError(msg.into()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SampledOp {
Sampled,
Rose,
Fell,
Stable,
Changed,
Past,
Future,
Rising,
Falling,
Steady,
Changing,
}
impl SampledOp {
pub fn is_future(self) -> bool {
matches!(
self,
Self::Future | Self::Rising | Self::Falling | Self::Steady | Self::Changing
)
}
pub fn needs_clock(self) -> bool {
self != Self::Sampled
}
pub fn depth(self, ticks: u32) -> u32 {
match self {
Self::Sampled
| Self::Future
| Self::Rising
| Self::Falling
| Self::Steady
| Self::Changing => 0,
Self::Past => ticks,
_ => 1,
}
}
}
pub fn op_of(id: KnownSystemName) -> Option<(SampledOp, bool)> {
use SampledOp as O;
Some(match id {
KnownSystemName::Sampled => (O::Sampled, false),
KnownSystemName::Rose => (O::Rose, false),
KnownSystemName::Fell => (O::Fell, false),
KnownSystemName::Stable => (O::Stable, false),
KnownSystemName::Changed => (O::Changed, false),
KnownSystemName::Past => (O::Past, false),
KnownSystemName::PastGclk => (O::Past, true),
KnownSystemName::RoseGclk => (O::Rose, true),
KnownSystemName::FellGclk => (O::Fell, true),
KnownSystemName::StableGclk => (O::Stable, true),
KnownSystemName::ChangedGclk => (O::Changed, true),
KnownSystemName::FutureGclk => (O::Future, true),
KnownSystemName::RisingGclk => (O::Rising, true),
KnownSystemName::FallingGclk => (O::Falling, true),
KnownSystemName::SteadyGclk => (O::Steady, true),
KnownSystemName::ChangingGclk => (O::Changing, true),
_ => return None,
})
}
#[derive(Clone, Copy)]
pub struct ClockEvent<'d> {
pub edge: EdgeKind,
pub signal: Expression<'d>,
pub iff: Option<Expression<'d>>,
}
pub fn clock_event_of<'d>(tc: TimingControl<'d>) -> Result<ClockEvent<'d>, SampledError> {
let ev = assertion_clock_event_of(tc)?;
if ev.signal.kind() == ExpressionKind::AssertionInstance {
return err(
"a sequence instance as a clocking event here (`@seq`, LRM 16.5.2) is only supported as the clocking event of an assertion"
.to_string(),
);
}
Ok(ev)
}
pub fn assertion_clock_event_of<'d>(tc: TimingControl<'d>) -> Result<ClockEvent<'d>, SampledError> {
if tc.kind() != TimingControlKind::SignalEvent {
return err(format!(
"a sampled value function's clocking event is a {:?} control; only a single \
`@(edge signal [iff cond])` event is supported",
tc.kind()
));
}
let children = tc.children();
let signal = children
.first()
.and_then(|c| c.as_expression())
.ok_or_else(|| SampledError("a clocking event with no signal expression".into()))?;
let iff = if children.len() == 2 {
children[1].as_expression()
} else {
None
};
Ok(ClockEvent {
edge: tc.edge(),
signal,
iff,
})
}
pub fn is_global_clock_event(tc: TimingControl<'_>) -> bool {
tc.kind() == TimingControlKind::SignalEvent
&& tc.edge() == EdgeKind::None
&& tc
.children()
.first()
.and_then(|c| c.as_expression())
.is_some_and(|x| {
x.kind() == ExpressionKind::Call
&& x.is_system_call() == Some(true)
&& x.system_subroutine()
.is_some_and(|s| s.known_name_id() == KnownSystemName::GlobalClock)
})
}
pub fn mentions_global_clock_event(tc: TimingControl<'_>) -> bool {
if tc.kind() == TimingControlKind::EventList {
return tc
.children()
.into_iter()
.filter_map(|c| c.as_timing_control())
.any(mentions_global_clock_event);
}
is_global_clock_event(tc)
}
pub fn is_future_call(e: Expression<'_>) -> bool {
e.kind() == ExpressionKind::Call
&& e.is_system_call() == Some(true)
&& e.system_subroutine()
.and_then(|s| op_of(s.known_name_id()))
.is_some_and(|(op, _)| op.is_future())
}
#[derive(Clone, Copy)]
pub struct SampledCall<'d> {
pub op: SampledOp,
pub global: bool,
pub call: Expression<'d>,
pub arg: Expression<'d>,
pub ticks: u32,
pub gate: Option<Expression<'d>>,
pub clock: Option<ClockEvent<'d>>,
}
fn is_empty_arg(e: &Expression<'_>) -> bool {
e.kind() == ExpressionKind::EmptyArgument
}
pub fn decode<'d>(e: Expression<'d>) -> Result<Option<SampledCall<'d>>, SampledError> {
if e.kind() != ExpressionKind::Call || e.is_system_call() != Some(true) {
return Ok(None);
}
let Some(sub) = e.system_subroutine() else {
return Ok(None);
};
let Some((op, mut global)) = op_of(sub.known_name_id()) else {
return Ok(None);
};
let operands = e.operands();
let name = sub.name();
let Some(&arg) = operands.first() else {
return err(format!("{name} with no argument expression"));
};
let mut ticks = 1u32;
let mut gate = None;
let mut clock = None;
for (i, o) in operands.iter().enumerate().skip(1) {
if o.kind() == ExpressionKind::ClockingEvent {
if clock.is_some() {
return err(format!("{name} with two clocking events"));
}
let tc = o
.as_sem_node()
.children()
.into_iter()
.find_map(|c| c.as_timing_control())
.ok_or_else(|| SampledError(format!("{name}: a clocking event with no control")))?;
if is_global_clock_event(tc) {
if global {
return err(format!("{name} with two clocking events"));
}
global = true;
} else {
clock = Some(clock_event_of(tc)?);
}
} else if is_empty_arg(o) {
} else if op == SampledOp::Past && i == 1 {
let n = o
.constant_value()
.and_then(|cv| cv.as_i64())
.ok_or_else(|| {
SampledError(
"$past's number_of_ticks is not an elaboration-time constant".into(),
)
})?;
if n < 1 {
return err(format!(
"$past's number_of_ticks is {n}; it shall be 1 or greater"
));
}
ticks = u32::try_from(n)
.map_err(|_| SampledError(format!("$past's number_of_ticks {n} is too large")))?;
} else if op == SampledOp::Past && i == 2 {
gate = Some(*o);
} else {
return err(format!("{name}: unexpected argument {i}"));
}
}
Ok(Some(SampledCall {
op,
global,
call: e,
arg,
ticks,
gate,
clock,
}))
}
pub type InferredDisable<'d> = Option<Option<(Symbol<'d>, bool)>>;
pub type InferredDisableResolver = for<'d> fn(&Symbol<'d>) -> Result<InferredDisable<'d>, String>;
thread_local! {
static INFERRED_DISABLE_RESOLVER: std::cell::Cell<Option<InferredDisableResolver>> =
const { std::cell::Cell::new(None) };
}
pub fn set_inferred_disable_resolver(
f: Option<InferredDisableResolver>,
) -> Option<InferredDisableResolver> {
INFERRED_DISABLE_RESOLVER.with(|c| c.replace(f))
}
pub fn resolve_inferred_disable<'d>(sym: &Symbol<'d>) -> Result<InferredDisable<'d>, String> {
match INFERRED_DISABLE_RESOLVER.with(std::cell::Cell::get) {
Some(f) => f(sym),
None => Err("the design's syntax is not available here".into()),
}
}
fn is_inferred_disable_call(e: &Expression<'_>) -> bool {
e.kind() == ExpressionKind::Call
&& e.call_subroutine_name().as_deref() == Some("$inferred_disable")
}
fn reads_inferred_disable(root: SemNode<'_>) -> bool {
let mut visited = BTreeSet::new();
let mut work = vec![root];
while let Some(node) = work.pop() {
let mut found = false;
walk(node, &mut |n| {
if let Some(e) = n.as_expression() {
if is_inferred_disable_call(&e) {
found = true;
} else if e.kind() == ExpressionKind::Call
&& e.is_system_call() != Some(true)
&& let Some(callee) = crate::netlist::itf::call_target(&e)
&& visited.insert(callee.uid())
&& let Some(body) = crate::process::cfg::proc_body(&callee)
{
work.push(body.as_sem_node());
}
}
!found
});
if found {
return true;
}
}
false
}
pub fn sampleable_symbol(sym: &Symbol<'_>) -> bool {
let kind = sym.kind();
if kind != SymbolKind::Net
&& !(kind == SymbolKind::Variable
&& sym.variable_lifetime() == sv_lang::VariableLifetime::Static)
{
return false;
}
sym.value_type().is_some_and(|t| t.is_integral())
}
fn is_integral_memory(sym: &Symbol<'_>) -> bool {
if sym.kind() != SymbolKind::Variable && sym.kind() != SymbolKind::Net {
return false;
}
let Some(ty) = sym.value_type() else {
return false;
};
if !ty.is_unpacked_array() || crate::state::storage::is_collection(&ty) {
return false;
}
let mut t = ty;
while t.is_unpacked_array() {
match t.element_type() {
Some(e) => t = e,
None => return false,
}
}
t.is_integral()
}
pub fn is_state_symbol(sym: &Symbol<'_>) -> bool {
let kind = sym.kind();
kind == SymbolKind::Net
|| (kind == SymbolKind::Variable
&& sym.variable_lifetime() == sv_lang::VariableLifetime::Static)
}
fn walk<'d>(node: SemNode<'d>, f: &mut impl FnMut(SemNode<'d>) -> bool) {
let mut stack = vec![node];
while let Some(n) = stack.pop() {
if f(n) {
let mut kids = crate::process::cfg::live_children(&n);
kids.reverse();
stack.extend(kids);
}
}
}
pub fn signals_in(node: SemNode<'_>) -> BTreeSet<SymbolId> {
let mut out = BTreeSet::new();
walk(node, &mut |n| {
if let Some(e) = n.as_expression()
&& e.kind() == ExpressionKind::NamedValue
&& let Some(sym) = e.referenced_symbol()
&& sampleable_symbol(&sym)
{
out.insert(sym.uid());
}
true
});
out
}
pub fn strict_signals_in(e: Expression<'_>) -> Result<BTreeSet<SymbolId>, SampledError> {
strict_signals(e, true, true)
}
pub fn strict_site_signals_in(e: Expression<'_>) -> Result<BTreeSet<SymbolId>, SampledError> {
strict_signals(e, true, true)
}
fn strict_hidden_signals_in(e: Expression<'_>) -> Result<BTreeSet<SymbolId>, SampledError> {
strict_signals(e, true, false)
}
fn strict_signals(
e: Expression<'_>,
allow_hier: bool,
allow_mem: bool,
) -> Result<BTreeSet<SymbolId>, SampledError> {
let mut failure = None;
let mut hier = BTreeSet::new();
walk(e.as_sem_node(), &mut |n| {
if failure.is_some() {
return false;
}
let Some(x) = n.as_expression() else {
return true;
};
let kind = x.kind();
if kind == ExpressionKind::HierarchicalValue {
match x.referenced_symbol() {
Some(sym) if allow_hier && sampleable_symbol(&sym) => {
hier.insert(sym.uid());
}
_ => {
failure = Some(SampledError(
"a hierarchical reference inside a sampled value function's expression \
is not supported unless it names an integral variable or net"
.into(),
));
}
}
} else if kind == ExpressionKind::NamedValue {
if let Some(sym) = x.referenced_symbol()
&& is_state_symbol(&sym)
&& !sampleable_symbol(&sym)
{
if allow_mem && is_integral_memory(&sym) {
hier.insert(sym.uid());
return true;
}
failure = Some(SampledError(format!(
"`{}` inside a sampled value function's expression has a type \
(real/string/array/event/collection/handle) whose sampled value is not \
supported",
sym.name()
)));
}
} else if kind == ExpressionKind::Call {
match decode(x) {
Ok(Some(_)) => {
failure = Some(SampledError(
"a sampled value function nested inside the expression of another is \
not supported"
.into(),
));
}
Ok(None) => {
if x.is_system_call() == Some(false) {
failure = Some(SampledError(
"a user subroutine call inside a sampled value function's expression \
is not supported"
.into(),
));
}
}
Err(e) => failure = Some(e),
}
}
failure.is_none()
});
match failure {
Some(e) => Err(e),
None => {
let mut out = signals_in(e.as_sem_node());
out.extend(hier);
Ok(out)
}
}
}
pub fn is_checker_always_ff(block: &Symbol<'_>) -> bool {
if block.kind() != SymbolKind::ProceduralBlock
|| block.procedural_block_procedure_kind() != sv_lang::ProceduralBlockKind::AlwaysFF
{
return false;
}
let mut scope = block.parent();
while let Some(s) = scope {
let kind = s.kind();
if kind == SymbolKind::CheckerInstanceBody {
return true;
}
if kind == SymbolKind::InstanceBody {
return false;
}
scope = s.parent();
}
false
}
pub fn checker_ff_sampled_signals(block: &Symbol<'_>) -> Result<BTreeSet<SymbolId>, SampledError> {
if !is_checker_always_ff(block) {
return Ok(BTreeSet::new());
}
let Some(body) = crate::process::cfg::proc_body(block) else {
return Ok(BTreeSet::new());
};
let mut event = BTreeSet::new();
let mut read = BTreeSet::new();
let mut failure = None;
walk(body.as_sem_node(), &mut |n| {
if failure.is_some() {
return false;
}
if n.as_assertion_expr().is_some() {
return false;
}
if let Some(tc) = n.as_timing_control() {
event.extend(signals_in(tc.as_sem_node()));
return false;
}
if let Some(e) = n.as_expression() {
let kind = e.kind();
if kind == ExpressionKind::Assignment && e.is_nonblocking() != Some(true) {
failure = Some(SampledError(
"a blocking assignment in a checker's `always_ff` (LRM 17.5 allows \
nonblocking assignments only there)"
.into(),
));
} else if kind == ExpressionKind::HierarchicalValue {
failure = Some(SampledError(
"a hierarchical reference inside a checker's `always_ff` cannot be \
sampled (LRM 17.5): its P cell lives in another instance"
.into(),
));
} else if kind == ExpressionKind::NamedValue
&& let Some(sym) = e.referenced_symbol()
{
if sampleable_symbol(&sym) {
read.insert(sym.uid());
} else if is_state_symbol(&sym) {
failure = Some(SampledError(format!(
"`{}` in a checker's `always_ff` has a type \
(real/string/array/event/collection/handle) whose sampled value (LRM \
17.5) is not supported",
sym.name()
)));
}
}
}
failure.is_none()
});
if let Some(e) = failure {
return Err(e);
}
let mut sampled = &read - &event;
if reads_inferred_disable(body.as_sem_node()) {
match resolve_inferred_disable(block) {
Ok(Some(Some((sig, _)))) if sampleable_symbol(&sig) => {
sampled.insert(sig.uid());
}
Ok(Some(None)) => {}
Ok(Some(Some((sig, _)))) => {
return err(format!(
"`$inferred_disable` in a checker's `always_ff` stands for `{}`, whose type \
has no sampled value (LRM 17.5)",
sig.name()
));
}
Ok(None) => {
return err(
"`$inferred_disable` in an `always_ff` of a checker that is not a static \
instance has no sampled `default disable iff` (LRM 17.4)",
);
}
Err(why) => {
return err(format!(
"`$inferred_disable` in a checker's `always_ff`: {why}"
));
}
}
}
Ok(sampled)
}
pub fn relocate_clock<'d>(
block: &Symbol<'d>,
ev: &ClockEvent<'_>,
) -> Result<ClockEvent<'d>, SampledError> {
let want = ev.signal.source_range();
let mut found = None;
let mut look = |root: SemNode<'d>| {
walk(root, &mut |n| {
if found.is_none()
&& let Some(tc) = n.as_timing_control()
&& tc.kind() == TimingControlKind::SignalEvent
&& let Ok(cand) = clock_event_of(tc)
&& cand.edge == ev.edge
&& cand.signal.source_range() == want
{
found = Some(cand);
}
found.is_none()
});
};
if let Some(body) = crate::process::cfg::proc_body(block) {
look(body.as_sem_node());
}
if let Some(scope) = block.parent() {
for cb in crate::netlist::module_items(&scope)
.into_iter()
.filter(|m| m.kind() == SymbolKind::ClockingBlock)
{
if let Some(cev) = cb.clocking_block_event() {
look(cev);
}
}
}
found.ok_or_else(|| {
SampledError(
"the inferred clocking event of another instance of this module has no counterpart \
in this instance"
.into(),
)
})
}
pub fn relocate_clock_node<'d>(block: &Symbol<'d>, tc: &TimingControl<'_>) -> Option<SemNode<'d>> {
if tc.kind() != TimingControlKind::SignalEvent {
return None;
}
let kids = tc.children();
let want = kids.first()?.as_expression()?.source_range();
let edge = tc.edge();
let arity = kids.len();
let mut found = None;
walk(
crate::process::cfg::proc_body(block)?.as_sem_node(),
&mut |n| {
if found.is_none()
&& let Some(cand) = n.as_timing_control()
&& cand.kind() == TimingControlKind::SignalEvent
&& cand.edge() == edge
&& cand.children().len() == arity
&& cand
.children()
.first()
.and_then(|c| c.as_expression())
.is_some_and(|e| e.source_range() == want)
{
found = Some(cand.as_sem_node());
}
found.is_none()
},
);
found
}
pub struct Site<'d> {
pub call: SampledCall<'d>,
pub arg_signals: BTreeSet<SymbolId>,
pub p_signals: BTreeSet<SymbolId>,
pub foreign: std::collections::BTreeMap<SymbolId, ForeignShape>,
}
pub fn site_loc(call: &Expression<'_>) -> String {
let start = call.source_range().start;
format!("{}.{}", start.buffer, start.offset)
}
pub fn sites_of_block<'d>(block: &Symbol<'d>) -> Result<Vec<Site<'d>>, SampledError> {
let Some(body) = crate::process::cfg::proc_body(block) else {
return Ok(Vec::new());
};
let mut calls = Vec::new();
let mut failure = None;
walk(body.as_sem_node(), &mut |n| {
if failure.is_some() {
return false;
}
if n.as_assertion_expr().is_some() {
return false;
}
if let Some(e) = n.as_expression() {
match decode(e) {
Ok(Some(c)) => {
calls.push(c);
return false;
}
Ok(None) => {}
Err(e) => {
failure = Some(e);
return false;
}
}
}
true
});
if let Some(e) = failure {
return Err(e);
}
let mut sites = Vec::new();
let own_body = crate::netlist::enclosing_instance_body(block).map(|b| b.uid());
for call in calls {
let arg_signals = strict_hidden_signals_in(call.arg)?;
let mut p_signals = arg_signals.clone();
let mut roots = vec![call.arg];
if let Some(g) = call.gate {
p_signals.extend(strict_hidden_signals_in(g)?);
roots.push(g);
}
if call.op.needs_clock() {
match call.clock {
Some(ev) => {
p_signals.extend(strict_hidden_signals_in(ev.signal)?);
roots.push(ev.signal);
if let Some(iff) = ev.iff {
p_signals.extend(strict_hidden_signals_in(iff)?);
roots.push(iff);
}
}
None => {
walk(body.as_sem_node(), &mut |n| {
if let Some(tc) = n.as_timing_control()
&& tc.kind() == TimingControlKind::SignalEvent
{
p_signals.extend(signals_in(tc.as_sem_node()));
}
true
});
if let Some(scope) = block.parent() {
for cb in crate::netlist::module_items(&scope)
.into_iter()
.filter(|m| m.kind() == SymbolKind::ClockingBlock)
{
if let Some(ev) = cb.clocking_block_event() {
p_signals.extend(signals_in(ev));
}
}
}
}
}
}
let mut foreign = std::collections::BTreeMap::new();
for root in roots {
walk(root.as_sem_node(), &mut |n| {
if let Some(x) = n.as_expression()
&& x.kind() == ExpressionKind::HierarchicalValue
&& let Some(sym) = x.referenced_symbol()
&& sampleable_symbol(&sym)
&& crate::netlist::enclosing_instance_body(&sym).map(|b| b.uid()) != own_body
&& let Some(shape) = foreign_shape(&sym)
{
foreign.insert(sym.uid(), shape);
}
true
});
}
sites.push(Site {
call,
arg_signals,
p_signals,
foreign,
});
}
Ok(sites)
}
pub fn p_id(sig: &SymbolId) -> SymbolId {
sig.derive("p")
}
pub fn n_id(sig: &SymbolId) -> SymbolId {
sig.derive("n")
}
pub fn s_id(owner: &SymbolId, loc: &str, sig: &SymbolId, k: u32) -> SymbolId {
owner.derive(&format!("s{loc}@{}#{k}", sig.key_string()))
}
fn fnv1a(s: &str) -> u64 {
let mut h = 0xcbf2_9ce4_8422_2325u64;
for b in s.bytes() {
h ^= u64::from(b);
h = h.wrapping_mul(0x0100_0000_01b3);
}
h
}
pub fn guard_name(owner: &SymbolId, loc: &str) -> String {
format!(
"vj_sva_guard_{:016x}",
fnv1a(&format!("{}@{loc}", owner.key_string()))
)
}
#[derive(Debug, Clone, PartialEq)]
pub struct HiddenSlot {
pub id: SymbolId,
pub source: SymbolId,
pub foreign: Option<ForeignShape>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ForeignShape {
pub width: u32,
pub signed: bool,
pub initial: Option<crate::state::FourStateValue>,
pub required: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShiftStep {
pub from: SymbolId,
pub to: SymbolId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SiteShift {
pub guard_name: String,
pub shifts: Vec<ShiftStep>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Hidden {
pub p: Vec<HiddenSlot>,
pub s: Vec<HiddenSlot>,
pub sites: Vec<SiteShift>,
}
impl Hidden {
pub fn slots(&self) -> impl Iterator<Item = &HiddenSlot> {
self.p.iter().chain(self.s.iter())
}
pub fn is_empty(&self) -> bool {
self.p.is_empty() && self.s.is_empty()
}
}
fn enclosing_clock_signals(body: &Symbol<'_>) -> Vec<(SymbolId, ForeignShape)> {
let mut out = Vec::new();
let mut cur = *body;
while let Some(parent) = super::global_clock::parent_body(&cur) {
for cb in crate::netlist::module_items(&parent)
.into_iter()
.filter(|m| m.kind() == SymbolKind::ClockingBlock)
{
let Some(ev) = cb.clocking_block_event() else {
continue;
};
let mut syms = Vec::new();
walk(ev, &mut |n| {
if let Some(e) = n.as_expression()
&& e.kind() == ExpressionKind::NamedValue
&& let Some(sym) = e.referenced_symbol()
&& sampleable_symbol(&sym)
{
syms.push(sym);
}
true
});
for sym in syms {
if let Some(shape) = foreign_shape(&sym) {
out.push((sym.uid(), shape));
}
}
}
cur = parent;
}
out
}
fn foreign_shape(sym: &Symbol<'_>) -> Option<ForeignShape> {
use crate::state::{FourStateValue, value};
let ty = sym.value_type()?;
let width = u32::try_from(ty.bit_width()).ok().filter(|w| *w > 0)?;
let signed = ty.is_signed();
let initial = sym
.initializer()
.and_then(|init| init.constant_value())
.and_then(|cv| value::four_state_of_constant(&cv))
.and_then(|fv| {
value::owned_svint_to_four_state(&value::four_state_to_owned_svint(&fv, width, signed))
})
.or_else(|| {
(!ty.is_four_state()).then(|| {
if width <= 64 {
FourStateValue::Narrow(0)
} else {
let words = width.div_ceil(64) as usize;
FourStateValue::Wide {
aval: vec![0; words],
bval: vec![0; words],
}
}
})
});
Some(ForeignShape {
width,
signed,
initial,
required: false,
})
}
pub fn hidden_slots(body: &Symbol<'_>) -> Result<Hidden, SampledError> {
let mut p: BTreeSet<SymbolId> = BTreeSet::new();
let mut foreign: std::collections::BTreeMap<SymbolId, ForeignShape> = Default::default();
let mut out = Hidden::default();
let mut seen = BTreeSet::new();
for item in crate::netlist::module_items(body) {
if !matches!(
item.kind(),
SymbolKind::ProceduralBlock | SymbolKind::Subroutine
) {
continue;
}
let owner = item.uid();
if item.kind() == SymbolKind::ProceduralBlock {
p.extend(checker_ff_sampled_signals(&item)?);
}
for site in sites_of_block(&item)? {
let loc = site_loc(&site.call.call);
if !seen.insert((owner.clone(), loc.clone())) {
return err(format!(
"two sampled value calls share one source position ({loc}); a macro that \
expands to several of them is not supported"
));
}
p.extend(site.p_signals.iter().cloned());
for (id, shape) in &site.foreign {
foreign.insert(
id.clone(),
ForeignShape {
required: true,
..shape.clone()
},
);
}
if site.call.global && site.call.clock.is_none() && !site.call.op.is_future() {
for (id, shape) in enclosing_clock_signals(body) {
p.insert(id.clone());
foreign.insert(id, shape);
}
}
let depth = site.call.op.depth(site.call.ticks);
if depth == 0 || site.call.op.is_future() {
continue;
}
let mut shifts = Vec::new();
for sig in &site.arg_signals {
for k in 0..depth {
out.s.push(HiddenSlot {
id: s_id(&owner, &loc, sig, k),
source: sig.clone(),
foreign: site.foreign.get(sig).cloned(),
});
}
for k in (1..depth).rev() {
shifts.push(ShiftStep {
from: s_id(&owner, &loc, sig, k - 1),
to: s_id(&owner, &loc, sig, k),
});
}
shifts.push(ShiftStep {
from: p_id(sig),
to: s_id(&owner, &loc, sig, 0),
});
}
out.sites.push(SiteShift {
guard_name: guard_name(&owner, &loc),
shifts,
});
}
}
out.p = p
.into_iter()
.map(|sig| HiddenSlot {
id: p_id(&sig),
foreign: foreign.get(&sig).cloned(),
source: sig,
})
.collect();
Ok(out)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct UseViews {
pub cur: Vec<(SymbolId, SymbolId)>,
pub prev: Vec<(SymbolId, SymbolId)>,
pub next: Vec<(SymbolId, SymbolId)>,
}
pub fn use_views(
owner: &SymbolId,
call: &SampledCall<'_>,
arg_signals: &BTreeSet<SymbolId>,
) -> UseViews {
let loc = site_loc(&call.call);
let mut v = UseViews::default();
for sig in arg_signals {
match call.op {
SampledOp::Sampled => v.cur.push((sig.clone(), p_id(sig))),
SampledOp::Rose | SampledOp::Fell | SampledOp::Stable | SampledOp::Changed => {
v.cur.push((sig.clone(), p_id(sig)));
v.prev.push((sig.clone(), s_id(owner, &loc, sig, 0)));
}
SampledOp::Past => v
.cur
.push((sig.clone(), s_id(owner, &loc, sig, call.ticks - 1))),
SampledOp::Future
| SampledOp::Rising
| SampledOp::Falling
| SampledOp::Steady
| SampledOp::Changing => {
v.cur.push((sig.clone(), p_id(sig)));
v.next.push((sig.clone(), n_id(sig)));
}
}
}
v
}
#[cfg(test)]
pub(crate) mod model {
pub struct Trace<'a> {
pub preponed: &'a [u8],
pub tick: &'a [bool],
pub init: u8,
}
impl Trace<'_> {
pub fn sampled(&self, t: usize) -> u8 {
self.preponed[t]
}
pub fn past(&self, t: usize, k: usize) -> u8 {
let ticks: Vec<usize> = (0..t).filter(|&s| self.tick[s]).collect();
if ticks.len() >= k {
self.preponed[ticks[ticks.len() - k]]
} else {
self.init
}
}
pub fn rose(&self, t: usize) -> bool {
self.sampled(t) == b'1' && self.past(t, 1) != b'1'
}
pub fn fell(&self, t: usize) -> bool {
self.sampled(t) == b'0' && self.past(t, 1) != b'0'
}
pub fn stable(&self, t: usize) -> bool {
self.sampled(t) == self.past(t, 1)
}
pub fn changed(&self, t: usize) -> bool {
!self.stable(t)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bits(s: &str) -> Vec<u8> {
s.bytes().collect()
}
#[test]
fn figure_16_3_value_change_expressions() {
let req = bits("00110001110000");
let ack = bits("00001000110000");
let tick = vec![true; 14];
let rq = Trace {
preponed: &req,
tick: &tick,
init: b'0',
};
let ak = Trace {
preponed: &ack,
tick: &tick,
init: b'0',
};
let e1: Vec<usize> = (0..14).filter(|&t| rq.rose(t)).map(|t| t + 1).collect();
let e2: Vec<usize> = (0..14).filter(|&t| ak.fell(t)).map(|t| t + 1).collect();
assert_eq!(e1, [3, 8]);
assert_eq!(e2, [6, 11]);
assert!(e1.contains(&3) && e2.contains(&6));
}
#[test]
fn value_change_compares_with_the_previous_tick_not_the_previous_step() {
let x = bits("00110");
let tick = [true, false, false, true, true];
let t = Trace {
preponed: &x,
tick: &tick,
init: b'0',
};
assert!(t.rose(3));
assert!(t.fell(4) && t.changed(4));
assert!(t.rose(2));
}
#[test]
fn before_the_first_tick_the_default_sampled_value_is_used() {
let x = bits("1x0");
let tick = [true, true, true];
let t = Trace {
preponed: &x,
tick: &tick,
init: b'x',
};
assert!(t.rose(0));
assert!(!t.stable(0));
assert!(!t.rose(1) && !t.fell(1) && t.changed(1));
assert!(t.fell(2));
}
#[test]
fn past_counts_gated_ticks() {
let q = bits("0101100101");
let tick = [
false, true, true, false, true, false, false, true, false, false,
];
let t = Trace {
preponed: &q,
tick: &tick,
init: b'x',
};
assert_eq!(t.past(9, 1), q[7]);
assert_eq!(t.past(9, 2), q[4]);
assert_eq!(t.past(9, 4), q[1]);
assert_eq!(t.past(9, 5), b'x');
assert_eq!(t.past(4, 1), q[2]);
assert_eq!(t.past(4, 3), b'x');
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::compile_source;
fn hidden_of(src: &str) -> Result<Hidden, SampledError> {
let design = compile_source(src).expect("valid SV source compiles");
let body = design
.top_instances()
.next()
.expect("one top instance")
.instance_body()
.expect("instance has a body");
hidden_slots(&body)
}
fn names(slots: &[HiddenSlot]) -> Vec<String> {
slots.iter().map(|s| s.id.path().to_owned()).collect()
}
#[test]
fn rose_gets_p_cells_for_arg_and_clock_and_one_s_cell() {
let h = hidden_of(
"module top; logic clk, b, r;\n\
always @(posedge clk) r <= $rose(b);\nendmodule\n",
)
.unwrap();
assert_eq!(names(&h.p), ["top.b$p", "top.clk$p"]);
assert_eq!(h.s.len(), 1);
assert_eq!(h.s[0].source.path(), "top.b");
assert_eq!(h.sites.len(), 1);
assert_eq!(h.sites[0].shifts.len(), 1);
assert_eq!(h.sites[0].shifts[0].from, p_id(&h.s[0].source));
assert_eq!(h.sites[0].shifts[0].to, h.s[0].id);
}
#[test]
fn past_depth_n_keeps_n_cells_shifted_oldest_first() {
let h = hidden_of(
"module top; logic clk; logic [3:0] x, y;\n\
always @(posedge clk) y <= $past(x, 3);\nendmodule\n",
)
.unwrap();
assert_eq!(h.s.len(), 3);
let sh = &h.sites[0].shifts;
assert_eq!(sh.len(), 3);
assert_eq!(sh[0].to, h.s[2].id);
assert_eq!(sh[0].from, h.s[1].id);
assert_eq!(sh[1].to, h.s[1].id);
assert_eq!(sh[1].from, h.s[0].id);
assert_eq!(sh[2].to, h.s[0].id);
assert_eq!(sh[2].from, p_id(&h.s[0].source));
}
#[test]
fn sampled_needs_only_a_p_cell() {
let h = hidden_of(
"module top; logic clk, a, r;\n\
always @(posedge clk) r <= $sampled(a);\nendmodule\n",
)
.unwrap();
assert_eq!(names(&h.p), ["top.a$p"]);
assert!(h.s.is_empty() && h.sites.is_empty());
}
#[test]
fn past_gate_signals_get_p_cells() {
let h = hidden_of(
"module top; logic clk, en, q, r;\n\
always @(posedge clk) r <= $past(q, 2, en);\nendmodule\n",
)
.unwrap();
assert_eq!(names(&h.p), ["top.clk$p", "top.en$p", "top.q$p"]);
}
#[test]
fn no_sampled_calls_no_hidden_cells() {
let h = hidden_of("module top; logic clk, a; always @(posedge clk) a <= ~a;\nendmodule\n")
.unwrap();
assert!(h.is_empty() && h.sites.is_empty());
}
#[test]
fn enumeration_is_deterministic() {
let src = "module top; logic clk, a, b, r;\n\
always @(posedge clk) r <= $rose(a) ^ $fell(b) ^ $past(a, 2);\nendmodule\n";
assert_eq!(hidden_of(src).unwrap(), hidden_of(src).unwrap());
}
#[test]
fn nested_sampled_calls_are_rejected() {
let e = hidden_of(
"module top; logic clk, a, r;\n\
always @(posedge clk) r <= $past($rose(a));\nendmodule\n",
)
.map(|_| ())
.unwrap_err();
assert!(e.0.contains("nested"), "{e}");
}
#[test]
fn automatic_loop_variable_gets_no_cell() {
let h = hidden_of(
"module top; logic clk; logic [3:0] b, r1;\n\
always @(posedge clk) for (int i = 0; i < 4; i++) r1[i] <= $past(b[i]);\n\
endmodule\n",
)
.unwrap();
assert_eq!(names(&h.p), ["top.b$p", "top.clk$p"]);
assert_eq!(h.s.len(), 1);
}
#[test]
fn past_zero_ticks_is_rejected() {
let e = hidden_of(
"module top; logic clk, a, r;\n\
always @(posedge clk) r <= $past(a, 0);\nendmodule\n",
)
.map(|_| ())
.unwrap_err();
assert!(e.0.contains("number_of_ticks"), "{e}");
}
#[test]
fn unsampleable_signal_types_are_rejected() {
for (decl, call) in [
("logic [3:0] m [0:3]; logic r;", "$past(m[1])"),
("real x; logic r;", "$rose(x > 1.0)"),
("string s; logic r;", "$changed(s == \"a\")"),
] {
let src = format!(
"module top; logic clk; {decl}\n\
always @(posedge clk) r <= {call};\nendmodule\n"
);
let e = hidden_of(&src).map(|_| ()).unwrap_err();
assert!(
e.0.contains("sampled value is not supported"),
"{call}: {e}"
);
}
}
#[test]
fn user_function_call_in_the_argument_is_rejected() {
let e = hidden_of(
"module top; logic clk, a, r;\n\
function logic f(logic x); return x; endfunction\n\
always @(posedge clk) r <= $rose(f(a));\nendmodule\n",
)
.map(|_| ())
.unwrap_err();
assert!(e.0.contains("user subroutine call"), "{e}");
}
#[test]
fn hierarchical_reference_in_the_argument_gets_foreign_cells() {
let h = hidden_of(
"module top; logic clk, r;\n\
sub u();\n\
always @(posedge clk) r <= $rose(u.v);\nendmodule\n\
module sub; logic v; endmodule\n",
)
.expect("accepted");
assert!(
h.p.iter()
.any(|c| c.foreign.as_ref().is_some_and(|f| f.required))
);
assert!(h.s.iter().all(|c| c.foreign.is_some()), "{:?}", h.s);
}
#[test]
fn use_views_pick_the_cell_each_function_reads() {
let design = compile_source(
"module top; logic clk, a, b, c, r;\n\
always @(posedge clk) begin\n\
r <= $sampled(a); r <= $rose(b); r <= $past(c, 3);\n\
end\nendmodule\n",
)
.expect("compiles");
let body = design
.top_instances()
.next()
.unwrap()
.instance_body()
.unwrap();
let block = crate::netlist::module_items(&body)
.into_iter()
.find(|m| m.kind() == SymbolKind::ProceduralBlock)
.expect("one procedural block");
let owner = block.uid();
let sites = sites_of_block(&block).unwrap();
assert_eq!(sites.len(), 3);
let views: Vec<UseViews> = sites
.iter()
.map(|s| use_views(&owner, &s.call, &s.arg_signals))
.collect();
let a = body.find("a").unwrap().uid();
let b = body.find("b").unwrap().uid();
let c = body.find("c").unwrap().uid();
let loc = |i: usize| site_loc(&sites[i].call.call);
assert_eq!(views[0].cur, [(a.clone(), p_id(&a))]);
assert!(views[0].prev.is_empty());
assert_eq!(views[1].cur, [(b.clone(), p_id(&b))]);
assert_eq!(views[1].prev, [(b.clone(), s_id(&owner, &loc(1), &b, 0))]);
assert_eq!(views[2].cur, [(c.clone(), s_id(&owner, &loc(2), &c, 2))]);
assert!(views[2].prev.is_empty());
}
#[test]
fn relocate_clock_finds_the_same_event_in_another_instance() {
let design = compile_source(
"module sub(input logic clk, input logic d, output logic r);\n\
always @(negedge clk) r <= $rose(d);\nendmodule\n\
module top; logic clk, d, r1, r2;\n\
sub u1(clk, d, r1); sub u2(clk, d, r2);\nendmodule\n",
)
.expect("compiles");
let top = design
.top_instances()
.next()
.unwrap()
.instance_body()
.unwrap();
let block_of = |inst: &str| {
let body = top.find(inst).unwrap().instance_body().unwrap();
crate::netlist::module_items(&body)
.into_iter()
.find(|m| m.kind() == SymbolKind::ProceduralBlock)
.unwrap()
};
let (b1, b2) = (block_of("u1"), block_of("u2"));
let ev1 = {
let mut found = None;
walk(
crate::process::cfg::proc_body(&b1).unwrap().as_sem_node(),
&mut |n| {
if found.is_none() {
found = n.as_timing_control();
}
found.is_none()
},
);
clock_event_of(found.expect("the always has an event control")).unwrap()
};
let ev2 = relocate_clock(&b2, &ev1).unwrap();
assert_eq!(ev2.edge, EdgeKind::NegEdge);
assert_eq!(
ev2.signal.referenced_symbol().unwrap().uid().path(),
"top.u2.clk"
);
assert_eq!(
ev1.signal.referenced_symbol().unwrap().uid().path(),
"top.u1.clk"
);
}
#[test]
fn gclk_spellings_and_explicit_global_clock_event_decode_alike() {
let design = compile_source(
"module top; logic gclk; logic [3:0] v, r;\n\
global clocking sys @(posedge gclk); endclocking\n\
always @(posedge gclk) begin\n\
r <= $past_gclk(v); r <= $past(v, , , @$global_clock);\n\
r <= $rose_gclk(v); r <= $rose(v, @$global_clock);\n\
r <= $past_gclk(v) + $past(v, , , @(posedge gclk));\n\
end\nendmodule\n",
)
.expect("compiles");
let body = design
.top_instances()
.next()
.unwrap()
.instance_body()
.unwrap();
let block = crate::netlist::module_items(&body)
.into_iter()
.find(|m| m.kind() == SymbolKind::ProceduralBlock)
.unwrap();
let sites = sites_of_block(&block).unwrap();
let shape: Vec<(SampledOp, bool, bool)> = sites
.iter()
.map(|s| (s.call.op, s.call.global, s.call.clock.is_some()))
.collect();
assert_eq!(
shape,
[
(SampledOp::Past, true, false),
(SampledOp::Past, true, false),
(SampledOp::Rose, true, false),
(SampledOp::Rose, true, false),
(SampledOp::Past, true, false),
(SampledOp::Past, false, true),
]
);
let h = hidden_slots(&body).unwrap();
assert!(names(&h.p).contains(&"top.gclk$p".to_owned()), "{:?}", h.p);
}
#[test]
fn explicit_clock_signals_get_p_cells() {
let h = hidden_of(
"module top; logic clk, clk2, b, r;\n\
always @(posedge clk) r <= $rose(b, @(posedge clk2));\nendmodule\n",
)
.unwrap();
assert!(names(&h.p).contains(&"top.clk2$p".to_owned()));
}
}