use crate::types::TypeWidth;
use std::collections::{HashMap, HashSet};
use std::fmt;
use sv_sim_sim::symid::SymbolUid;
use crate::process_lower::{SignalSlot, SignalSlotClass};
use sv_lang::kinds::{ExpressionKind, StatementKind, SymbolKind, TimingControlKind};
use sv_lang::{
AnalysisFlags, Design, EdgeKind, Expression, MethodFlags, ProceduralBlockKind, SemNode,
Statement, Symbol, TimingControl, UnaryOp, Walk,
};
use sv_sim_sim::symid::SymbolId;
use crate::error::LoweringError;
pub use crate::sva::emit::procedure_inferred_disable;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SliceProcKind {
Initial,
Always,
AlwaysComb,
AlwaysLatch,
AlwaysFF,
Final,
StaticAssertion,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiscoveredProcess<'d> {
pub sym: Symbol<'d>,
pub kind: SliceProcKind,
pub clock: Option<(Symbol<'d>, EdgeKind)>,
pub reset: Option<(Symbol<'d>, EdgeKind)>,
pub read_set: Vec<SymbolId>,
pub assertion: Option<crate::sva::proc::HostedAssertion<'d>>,
}
pub fn discover_processes<'d>(
module_body: &Symbol<'d>,
comb_sensitivity: &CombSensitivityIndex,
) -> Result<Vec<DiscoveredProcess<'d>>, LoweringError> {
discover_processes_dispatching(module_body, comb_sensitivity, None)
}
pub fn discover_processes_dispatching<'d>(
module_body: &Symbol<'d>,
comb_sensitivity: &CombSensitivityIndex,
virtual_tasks: Option<&sv_sim_sim::process::cfg::VirtualTasks<'d>>,
) -> Result<Vec<DiscoveredProcess<'d>>, LoweringError> {
let mut out = Vec::new();
let procedural_instances: HashSet<SymbolId> =
sv_sim_sim::netlist::procedural_checkers(module_body)
.iter()
.map(|c| c.instance.uid())
.collect();
for (ordinal, m) in sv_sim_sim::netlist::module_items(module_body)
.into_iter()
.enumerate()
{
if m.kind() != SymbolKind::ProceduralBlock {
continue;
}
let full_body = m.body().ok_or_else(|| {
LoweringError::unsupported_at("procedural block has no body statement", &m)
})?;
let full_body = sv_sim_sim::process::cfg::unwrap_invalid(full_body);
if is_procedural_instance_assertion(&m, &procedural_instances) {
if static_assertion_statement(full_body).is_none() {
reject_unsupported_in_body_with(&m, full_body, true)?;
}
continue;
}
if m.procedural_block_procedure_kind() == ProceduralBlockKind::Always
&& let Some(stmt) = static_assertion_statement(full_body)
{
validate_assertion_leaves(&m, stmt)?;
for action in [stmt.assertion_if_true(), stmt.assertion_if_false()]
.into_iter()
.flatten()
{
reject_unsupported_in_body(&m, action)?;
}
out.push(DiscoveredProcess {
sym: m,
kind: SliceProcKind::StaticAssertion,
clock: None,
reset: None,
read_set: Vec::new(),
assertion: Some(crate::sva::proc::HostedAssertion {
stmt,
procedural: false,
instance: None,
}),
});
continue;
}
let nested = collect_nested_assertions(&m, full_body, virtual_tasks)?;
reject_unsupported_in_body_with(&m, full_body, true)?;
let kind = if is_static_deferred_assertion(&m) {
SliceProcKind::AlwaysComb
} else {
classify_procedure_kind(&m, m.procedural_block_procedure_kind())?
};
let mut clock = None;
let mut reset = None;
let mut read_set = Vec::new();
match kind {
SliceProcKind::Initial | SliceProcKind::Final => {}
SliceProcKind::StaticAssertion => {
unreachable!("classify_procedure_kind never yields StaticAssertion")
}
SliceProcKind::Always => {
if is_implicit_or_delay_always_head(full_body) || body_suspends_inside(full_body) {
} else {
clock = Some(require_single_edge_head(&m, "always", full_body)?);
}
}
SliceProcKind::AlwaysFF => {
if full_body.kind() == StatementKind::Timed
&& (sv_sim_sim::sva::sampled::is_checker_always_ff(&m)
|| full_body
.timing_control()
.is_some_and(sv_sim_sim::sva::sampled::mentions_global_clock_event))
&& !is_clock_reset_idiom_head(full_body)
{
} else if full_body
.timing_control()
.is_some_and(has_value_change_entry)
|| is_wide_edge_list_head(full_body)
{
} else {
let in_checker = sv_sim_sim::sva::sampled::is_checker_always_ff(&m);
let (clk, rst) = require_clock_and_optional_reset(&m, full_body, in_checker)?;
clock = Some(clk);
reset = rst;
}
}
SliceProcKind::AlwaysComb | SliceProcKind::AlwaysLatch => {
read_set = comb_sensitivity
.read_set_of(&m.uid(), ordinal)
.map(<[_]>::to_vec)
.ok_or_else(|| {
LoweringError::unsupported_at(
"no precomputed read-set entry for this always_comb/always_latch \
block -- `build_comb_sensitivity_index` must be called over the \
WHOLE design (including this block's own instance) before \
`discover_processes` runs",
&m,
)
})?;
let locals = block_locals(&m);
for h in &nested {
for sym in crate::sva::proc::const_cast_reads(h.stmt) {
let id = sym.uid();
if !locals.contains(&id) && !read_set.contains(&id) {
read_set.push(id);
}
}
}
}
};
out.push(DiscoveredProcess {
sym: m,
kind,
clock,
reset,
read_set,
assertion: None,
});
for hosted in nested {
out.push(DiscoveredProcess {
sym: m,
kind: SliceProcKind::StaticAssertion,
clock: None,
reset: None,
read_set: Vec::new(),
assertion: Some(hosted),
});
}
}
Ok(out)
}
fn collect_nested_assertions<'d>(
m: &Symbol<'d>,
root: Statement<'d>,
virtual_tasks: Option<&sv_sim_sim::process::cfg::VirtualTasks<'d>>,
) -> Result<Vec<crate::sva::proc::HostedAssertion<'d>>, LoweringError> {
let mut out: Vec<crate::sva::proc::HostedAssertion<'d>> = Vec::new();
let mut stack = vec![(root.as_sem_node(), false)];
let mut visited = std::collections::HashSet::new();
while let Some((node, in_task)) = stack.pop() {
if let Some(e) = node.as_expression()
&& e.kind() == sv_lang::kinds::ExpressionKind::Call
&& e.is_system_call() != Some(true)
{
for callee in sv_sim_sim::netlist::itf::call_callees(&e) {
if callee.subroutine_flags().contains(MethodFlags::DPI_IMPORT) {
continue;
}
if let Some(body) = sv_sim_sim::process::cfg::proc_body(&callee)
&& visited.insert(callee.uid())
{
stack.push((body.as_sem_node(), true));
}
if let Some(vt) = virtual_tasks
&& callee.subroutine_this_var().is_some()
&& (callee.subroutine_flags().contains(MethodFlags::VIRTUAL)
|| callee.subroutine_override().is_some())
{
for (_, imp) in vt.implementations(&callee) {
if let Some(body) = sv_sim_sim::process::cfg::proc_body(&imp)
&& visited.insert(imp.uid())
{
stack.push((body.as_sem_node(), true));
}
}
}
}
}
if let Some(stmt) = node.as_statement()
&& stmt.kind() == StatementKind::ConcurrentAssertion
&& (!in_task || stmt.assertion_kind() == Some(sv_lang::AssertionKind::Expect))
{
if out
.iter()
.any(|h| h.stmt.source_range().start == stmt.source_range().start)
{
continue;
}
validate_assertion_leaves(m, stmt)?;
for action in [stmt.assertion_if_true(), stmt.assertion_if_false()]
.into_iter()
.flatten()
{
reject_unsupported_in_body(m, action)?;
}
out.push(crate::sva::proc::HostedAssertion {
stmt,
procedural: true,
instance: None,
});
continue;
}
if let Some(stmt) = node.as_statement()
&& stmt.kind() == StatementKind::ProceduralChecker
{
for instance in stmt.procedural_checker_instances() {
if instance.kind() != SymbolKind::CheckerInstance {
return Err(LoweringError::unsupported_at(
"an array of procedural checker instances: the grammar has no such \
construct (17.3 instantiates one checker per statement), so this is \
an internal error",
m,
));
}
for block in sv_sim_sim::netlist::module_items(&instance) {
let Some(assertion) = static_concurrent_assertion_of(&block) else {
continue;
};
validate_assertion_leaves(&block, assertion)?;
for action in [
assertion.assertion_if_true(),
assertion.assertion_if_false(),
]
.into_iter()
.flatten()
{
reject_unsupported_in_body(&block, action)?;
}
out.push(crate::sva::proc::HostedAssertion {
stmt: assertion,
procedural: true,
instance: Some(instance),
});
}
}
continue;
}
let mut kids = sv_sim_sim::process::cfg::live_children(&node);
kids.reverse();
stack.extend(kids.into_iter().map(|k| (k, in_task)));
}
Ok(out)
}
pub fn static_concurrent_assertion_of<'d>(block: &Symbol<'d>) -> Option<Statement<'d>> {
if block.kind() != SymbolKind::ProceduralBlock
|| block.procedural_block_procedure_kind() != ProceduralBlockKind::Always
{
return None;
}
static_assertion_statement(sv_sim_sim::process::cfg::proc_body(block)?)
}
fn is_procedural_instance_assertion(
block: &Symbol<'_>,
procedural_instances: &HashSet<SymbolId>,
) -> bool {
if procedural_instances.is_empty()
|| !(static_concurrent_assertion_of(block).is_some() || is_static_deferred_assertion(block))
{
return false;
}
let mut scope = block.parent();
while let Some(s) = scope {
if s.kind() == SymbolKind::CheckerInstanceBody {
return s
.checker_instance_body_parent_instance()
.is_some_and(|i| procedural_instances.contains(&i.uid()));
}
scope = s.parent();
}
false
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DiscoveredSubroutine<'d> {
pub sym: Symbol<'d>,
}
pub fn discover_subroutines<'d>(module_body: &Symbol<'d>) -> Vec<DiscoveredSubroutine<'d>> {
sv_sim_sim::netlist::module_items(module_body)
.into_iter()
.filter(|m| m.kind() == SymbolKind::Subroutine)
.filter(|m| !m.subroutine_flags().contains(MethodFlags::DPI_IMPORT))
.map(|sym| DiscoveredSubroutine { sym })
.collect()
}
pub fn discover_called_subroutines<'d>(
roots: impl IntoIterator<Item = SemNode<'d>>,
) -> Vec<DiscoveredSubroutine<'d>> {
discover_called_subroutines_seen(roots, &mut HashSet::new())
}
pub fn discover_called_subroutines_seen<'d>(
roots: impl IntoIterator<Item = SemNode<'d>>,
seen: &mut HashSet<SymbolId>,
) -> Vec<DiscoveredSubroutine<'d>> {
discover_called_subroutines_inlined(roots, seen).0
}
pub fn discover_called_subroutines_inlined<'d>(
roots: impl IntoIterator<Item = SemNode<'d>>,
seen: &mut HashSet<SymbolId>,
) -> (Vec<DiscoveredSubroutine<'d>>, Vec<Symbol<'d>>) {
let mut out = Vec::new();
let mut inlined = Vec::new();
let mut stack: Vec<SemNode<'d>> = roots.into_iter().collect();
while let Some(node) = stack.pop() {
let callees: Vec<Symbol<'d>> = match node.as_expression() {
Some(expr) => match sv_sim_sim::netlist::vif::call_targets(&expr) {
Some((_, targets)) => targets.into_iter().flatten().collect(),
None => sv_sim_sim::netlist::itf::call_callees(&expr),
},
None => Vec::new(),
};
for callee in callees {
if callee.subroutine_flags().contains(MethodFlags::DPI_IMPORT)
|| !seen.insert(callee.uid())
{
continue;
}
let is_class_method = callee.parent().is_some_and(|p| {
matches!(
p.kind(),
SymbolKind::ClassType
| SymbolKind::GenericClassDef
| SymbolKind::CovergroupBody
| SymbolKind::Coverpoint
| SymbolKind::CoverCross
)
});
if !is_class_method {
if sv_sim_sim::process::cfg::subroutine_can_suspend(&callee) {
inlined.push(callee);
} else {
out.push(DiscoveredSubroutine { sym: callee });
}
}
if let Some(body) = sv_sim_sim::process::cfg::proc_body(&callee) {
stack.push(body.as_sem_node());
}
for m in callee.members() {
if m.kind() == SymbolKind::Variable
&& let Some(init) = m.initializer()
{
stack.push(init.as_sem_node());
}
}
}
stack.extend(sv_sim_sim::process::cfg::live_children(&node));
}
(out, inlined)
}
#[derive(Debug, Clone)]
pub struct DiscoveredContAssign<'d> {
pub sym: Symbol<'d>,
pub assign_expr: Expression<'d>,
pub net_target: Option<Symbol<'d>>,
pub read_set: Vec<SymbolId>,
pub delay: Option<sv_lang::SemNode<'d>>,
}
pub fn discover_variable_initializers<'d>(
module_body: &Symbol<'d>,
) -> Vec<DiscoveredContAssign<'d>> {
let mut out = Vec::new();
let block_statics = sv_sim_sim::netlist::static_procedural_vars(module_body);
for m in sv_sim_sim::netlist::module_items(module_body)
.into_iter()
.chain(block_statics)
{
if m.kind() != SymbolKind::Variable {
continue;
}
let Some(init) = sv_sim_sim::netlist::variable_initializer(&m) else {
continue;
};
let Some(ty) = m.value_type() else {
continue;
};
if init.constant_value().is_some()
&& !sv_sim_sim::state::storage::contains_tagged_union(&ty)
&& !ty.is_event()
{
continue;
}
if sv_sim_sim::state::storage::is_collection(&ty) {
continue;
}
if crate::arrays::array_shape(&ty).is_some_and(|s| s.elem.is_virtual_interface()) {
continue;
}
out.push(DiscoveredContAssign {
sym: m,
assign_expr: init,
net_target: Some(m),
read_set: Vec::new(),
delay: None,
});
}
out
}
pub fn discover_cont_assigns<'d>(
module_body: &Symbol<'d>,
) -> Result<Vec<DiscoveredContAssign<'d>>, LoweringError> {
let mut out = Vec::new();
for m in sv_sim_sim::netlist::module_items(module_body) {
if m.kind() == SymbolKind::Net
&& let Some(rhs) = m.initializer()
{
out.push(DiscoveredContAssign {
sym: m,
assign_expr: rhs,
net_target: Some(m),
read_set: cont_assign_read_set(rhs),
delay: m.net_delay(),
});
continue;
}
if m.kind() != SymbolKind::ContinuousAssign {
continue;
}
let assign_expr = m.continuous_assign_assignment().ok_or_else(|| {
LoweringError::unsupported_at(
"ContinuousAssign symbol has no bound assignment expression (unexpected AST \
shape)",
&m,
)
})?;
if assign_expr.kind() != ExpressionKind::Assignment {
return Err(LoweringError::unsupported_at(
&format!(
"ContinuousAssign symbol's bound expression is `{:?}`, not `Assignment` \
(unexpected AST shape)",
assign_expr.kind()
),
&m,
));
}
if assign_expr.is_nonblocking() == Some(true) {
return Err(LoweringError::unsupported_at(
"nonblocking (`<=`) continuous assign is not legal SystemVerilog (unexpected \
AST shape)",
&m,
));
}
if assign_expr.left().is_none() {
return Err(LoweringError::unsupported_at(
"continuous assign missing lvalue",
&m,
));
}
let read_set = cont_assign_read_set(assign_expr);
out.push(DiscoveredContAssign {
sym: m,
assign_expr,
net_target: None,
read_set,
delay: m.continuous_assign_delay(),
});
}
Ok(out)
}
#[derive(Debug, Clone, Default)]
pub struct CombSensitivityIndex(HashMap<(SymbolId, usize), Vec<SymbolId>>);
impl CombSensitivityIndex {
pub fn read_set_of(&self, block_id: &SymbolId, ordinal: usize) -> Option<&[SymbolId]> {
self.0.get(&(block_id.clone(), ordinal)).map(Vec::as_slice)
}
}
fn push_lvalue_index_reads<'d>(lhs: Expression<'d>, stack: &mut Vec<SemNode<'d>>) {
let kind = lhs.kind();
if kind == ExpressionKind::ElementSelect || kind == ExpressionKind::RangeSelect {
for x in [lhs.selector(), lhs.range_left(), lhs.range_right()]
.into_iter()
.flatten()
{
stack.push(x.as_sem_node());
}
if let Some(b) = lhs.select_value() {
push_lvalue_index_reads(b, stack);
}
} else if kind == ExpressionKind::MemberAccess {
if let Some(o) = lhs.operand() {
push_lvalue_index_reads(o, stack);
}
} else if kind == ExpressionKind::Concatenation {
for o in lhs.operands() {
push_lvalue_index_reads(o, stack);
}
} else if !sv_sim_sim::symid::is_value_name(kind) {
stack.push(lhs.as_sem_node());
}
}
pub fn cont_assign_read_set(assign_expr: Expression<'_>) -> Vec<SymbolId> {
let mut order: Vec<SymbolId> = Vec::new();
let mut seen: HashSet<SymbolId> = HashSet::new();
let mut written: HashSet<SymbolId> = HashSet::new();
let mut stack = vec![assign_expr.as_sem_node()];
while let Some(n) = stack.pop() {
if let Some(e) = n.as_expression() {
if sv_sim_sim::symid::is_value_name(e.kind())
&& let Some(sym) = e.referenced_symbol()
&& matches!(
sym.kind(),
SymbolKind::Variable | SymbolKind::Net | SymbolKind::ModportPort
)
&& seen.insert(sym.uid())
{
order.push(sym.uid());
}
if e.kind() == ExpressionKind::Assignment
&& e.is_nonblocking() != Some(true)
&& let Some(lhs) = e.left()
&& sv_sim_sim::symid::is_value_name(lhs.kind())
&& let Some(sym) = lhs.referenced_symbol()
&& sym.kind() != SymbolKind::Net
{
written.insert(sym.uid());
}
for id in crate::func::callee_reads(&e) {
if seen.insert(id.clone()) {
order.push(id);
}
}
}
if let Some(e) = n.as_expression()
&& e.kind() == ExpressionKind::Assignment
&& let (Some(lhs), Some(rhs)) = (e.left(), e.right())
{
if sv_sim_sim::symid::is_value_name(lhs.kind()) {
if e.is_nonblocking() == Some(true) {
stack.extend(n.children());
continue;
}
stack.push(rhs.as_sem_node());
continue;
}
stack.push(rhs.as_sem_node());
push_lvalue_index_reads(lhs, &mut stack);
continue;
}
stack.extend(n.children());
}
order
.into_iter()
.filter(|id| !written.contains(id))
.collect()
}
pub fn build_comb_sensitivity_index(
design: &Design,
) -> Result<CombSensitivityIndex, LoweringError> {
let mut any_comb = false;
design.root().visit(|sym| {
if sym.kind() != SymbolKind::Instance {
return Walk::Continue;
}
if let Some(scope) = sym.instance_body() {
let in_body = sv_sim_sim::netlist::module_items(&scope)
.iter()
.any(has_comb_semantics);
let in_checker = sv_sim_sim::netlist::checker_instances(&scope)
.iter()
.filter_map(sv_sim_sim::netlist::checker_instance_body)
.any(|b| {
sv_sim_sim::netlist::module_items(&b)
.iter()
.any(has_comb_semantics)
});
any_comb |= in_body || in_checker;
}
Walk::Continue
});
if !any_comb {
return Ok(CombSensitivityIndex(HashMap::new()));
}
let analysis = design
.analyze(AnalysisFlags::NONE, 1)
.map_err(|e| LoweringError::unsupported(&format!("analysis error: {e}"), "<design>"))?;
let mut map: HashMap<(SymbolId, usize), Vec<SymbolId>> = HashMap::new();
type BodyReads<'d> = (Symbol<'d>, Vec<(usize, Vec<Symbol<'d>>)>);
let mut analyzed: Vec<BodyReads<'_>> = Vec::new();
let mut skipped: Vec<Symbol<'_>> = Vec::new();
design.root().visit(|sym| {
if sym.kind() != SymbolKind::Instance {
return Walk::Continue;
}
let Some(scope) = sym.instance_body() else {
return Walk::Continue;
};
let items = sv_sim_sim::netlist::module_items(&scope);
let scopes = std::iter::once(scope)
.chain(sv_sim_sim::netlist::generate_scopes(&scope))
.chain(
sv_sim_sim::netlist::checker_instances(&scope)
.iter()
.filter_map(sv_sim_sim::netlist::checker_instance_body)
.collect::<Vec<_>>(),
);
let mut body_reads = Vec::new();
for proc in scopes.flat_map(|s| analysis.procedures(s)) {
let psym = proc.symbol();
if !has_comb_semantics(&psym) {
continue;
}
let locals = block_locals(&psym);
let mut reads: Vec<Symbol<'_>> = proc
.read_set()
.map(|r| r.symbol())
.filter(|s| matches!(s.kind(), SymbolKind::Variable | SymbolKind::Net))
.filter(|s| !locals.contains(&s.uid()))
.collect();
for sym in assertion_action_reads(&psym) {
if !locals.contains(&sym.uid()) && !reads.contains(&sym) {
reads.push(sym);
}
}
reads.sort_by_cached_key(|s| s.uid());
let Some(ordinal) = items.iter().position(|m| *m == psym) else {
continue;
};
map.insert(
(psym.uid(), ordinal),
reads.iter().map(|s| s.uid()).collect(),
);
body_reads.push((ordinal, reads));
}
let has_comb = items.iter().any(has_comb_semantics);
if body_reads.is_empty() && has_comb {
skipped.push(scope);
} else if !body_reads.is_empty() {
analyzed.push((scope, body_reads));
}
Walk::Continue
});
for body in skipped {
let Some((twin, reads)) = analyzed
.iter()
.find(|(t, _)| body.instance_body_has_same_type(t))
else {
continue;
};
let items = sv_sim_sim::netlist::module_items(&body);
for (ordinal, twin_reads) in reads {
let Some(psym) = items.get(*ordinal) else {
continue;
};
let ids = twin_reads
.iter()
.map(|r| match member_path(r, twin) {
Some(path) => resolve_member_path(&body, &path).unwrap_or(*r).uid(),
None => r.uid(),
})
.collect();
map.insert((psym.uid(), *ordinal), ids);
}
}
Ok(CombSensitivityIndex(map))
}
fn assertion_action_reads<'d>(proc: &Symbol<'d>) -> Vec<Symbol<'d>> {
let mut out: Vec<Symbol<'d>> = Vec::new();
let Some(code) = sv_sim_sim::process::cfg::proc_body(proc) else {
return out;
};
let mut stack = vec![code.as_sem_node()];
let mut in_assertion = vec![false];
while let Some(n) = stack.pop() {
let inside = in_assertion.pop().unwrap_or(false);
let is_assertion = n
.as_statement()
.is_some_and(|s| s.kind() == StatementKind::ImmediateAssertion);
if inside
&& let Some(e) = n.as_expression()
&& sv_sim_sim::symid::is_value_name(e.kind())
&& let Some(sym) = e.referenced_symbol()
&& matches!(sym.kind(), SymbolKind::Variable | SymbolKind::Net)
&& !out.contains(&sym)
{
out.push(sym);
}
let target = n
.as_expression()
.filter(|e| e.kind() == ExpressionKind::Assignment)
.and_then(|e| e.left())
.filter(|l| sv_sim_sim::symid::is_value_name(l.kind()));
let mut skipped = target.is_none();
for c in sv_sim_sim::process::cfg::live_children(&n) {
if !skipped {
skipped = true;
continue;
}
if is_assertion && c.as_expression().is_some() {
continue;
}
stack.push(c);
in_assertion.push(inside || is_assertion);
}
}
out
}
pub(crate) fn member_path(sym: &Symbol<'_>, body: &Symbol<'_>) -> Option<Vec<usize>> {
let mut path = Vec::new();
let mut cur = *sym;
loop {
let parent = cur.parent()?;
let index = parent.members().position(|m| m == cur)?;
path.push(index);
if parent == *body {
path.reverse();
return Some(path);
}
cur = parent;
}
}
pub(crate) fn resolve_member_path<'d>(body: &Symbol<'d>, path: &[usize]) -> Option<Symbol<'d>> {
let mut cur = *body;
for &index in path {
let next = cur.members().nth(index)?;
cur = next;
}
Some(cur)
}
fn block_locals(proc: &Symbol<'_>) -> HashSet<SymbolId> {
let mut out = HashSet::new();
let Some(code) = sv_sim_sim::process::cfg::proc_body(proc) else {
return out;
};
let mut stack = vec![code.as_sem_node()];
while let Some(n) = stack.pop() {
if let Some(s) = n.as_statement() {
for block in sv_sim_sim::process::cfg::block_scopes(&s) {
out.extend(
block
.members()
.filter(|v| v.kind() == SymbolKind::Variable)
.map(|v| v.uid()),
);
}
}
stack.extend(sv_sim_sim::process::cfg::live_children(&n));
}
out
}
fn classify_procedure_kind<'d>(
_sym: &Symbol<'d>,
kind: ProceduralBlockKind,
) -> Result<SliceProcKind, LoweringError> {
match kind {
ProceduralBlockKind::Initial => Ok(SliceProcKind::Initial),
ProceduralBlockKind::Always => Ok(SliceProcKind::Always),
ProceduralBlockKind::AlwaysFF => Ok(SliceProcKind::AlwaysFF),
ProceduralBlockKind::AlwaysComb => Ok(SliceProcKind::AlwaysComb),
ProceduralBlockKind::AlwaysLatch => Ok(SliceProcKind::AlwaysLatch),
ProceduralBlockKind::Final => Ok(SliceProcKind::Final),
}
}
pub fn has_implicit_event_head(block: &Symbol<'_>) -> bool {
block.kind() == SymbolKind::ProceduralBlock
&& block.procedural_block_procedure_kind() == ProceduralBlockKind::Always
&& sv_sim_sim::process::cfg::proc_body(block).is_some_and(|body| {
body.kind() == StatementKind::Timed
&& body
.timing_control()
.is_some_and(|tc| tc.kind() == TimingControlKind::ImplicitEvent)
})
}
fn is_implicit_or_delay_always_head<'d>(full_body: Statement<'d>) -> bool {
if full_body.kind() != StatementKind::Timed {
return false;
}
full_body.timing_control().is_some_and(|tc| {
let kind = tc.kind();
kind == TimingControlKind::Delay
|| kind == TimingControlKind::ImplicitEvent
|| kind == TimingControlKind::EventList
|| (kind == TimingControlKind::SignalEvent && tc.edge() == sv_lang::EdgeKind::None)
|| (kind == TimingControlKind::SignalEvent && tc.edge() == sv_lang::EdgeKind::BothEdges)
|| (kind == TimingControlKind::SignalEvent
&& tc
.children()
.into_iter()
.find_map(|n| n.as_expression())
.is_some_and(|e| e.referenced_symbol().is_none()))
})
}
fn body_suspends_inside<'d>(full_body: Statement<'d>) -> bool {
if full_body.kind() == StatementKind::Timed {
return false;
}
let suspends = |s: &Statement<'d>| {
matches!(s.kind(), StatementKind::Timed | StatementKind::Wait)
|| (s.kind() == StatementKind::ExpressionStatement
&& s.expr().is_some_and(|e| {
(e.kind() == ExpressionKind::Assignment
&& e.is_nonblocking() != Some(true)
&& e.assignment_timing().is_some())
|| (e.kind() == ExpressionKind::Call
&& e.subroutine_kind() == Some(sv_lang::SubroutineKind::Task))
}))
};
if suspends(&full_body) {
return true;
}
let mut stack = sv_sim_sim::process::cfg::live_children(&full_body.as_sem_node());
while let Some(n) = stack.pop() {
if n.as_statement().is_some_and(|s| suspends(&s)) {
return true;
}
stack.extend(sv_sim_sim::process::cfg::live_children(&n));
}
false
}
fn require_single_edge_head<'d>(
sym: &Symbol<'d>,
what: &'static str,
full_body: Statement<'d>,
) -> Result<(Symbol<'d>, EdgeKind), LoweringError> {
if full_body.kind() == StatementKind::Invalid {
return Err(LoweringError::unsupported_at(
&format!(
"`{what}` block's body is an Invalid statement -- slang rejected something in \
it; an error inside a statically dead `if`/`case` arm (e.g. a reversed range \
select `v[7:8]` of a descending vector under a false parameter condition) is \
suppressed by slang but still invalidates the whole block"
),
sym,
));
}
if full_body.kind() != StatementKind::Timed {
return Err(LoweringError::unsupported_at(
&format!(
"`{what}` block has no `@(...)` timing control at its head -- a bare \
`{what}` whose body starts directly with a statement (no head timing \
control at all) is unsupported in the A2 slice"
),
sym,
));
}
let tc = full_body.timing_control().ok_or_else(|| {
LoweringError::unsupported_at(
&format!(
"`{what}` block's `Timed` statement has no timing control (unexpected AST shape)"
),
sym,
)
})?;
if tc.kind() != TimingControlKind::SignalEvent {
return Err(LoweringError::unsupported_at(
&format!(
"`{what}` block head timing control is `{:?}`, not a single `posedge`/\
`negedge` signal-event control -- `always @*`/implicit-sensitivity, \
event-list (`or`-joined), and level-sensitive heads are unsupported in \
the A2 slice (this is distinct from a `SignalEventControl` that merely \
lacks an edge -- see the sibling rejection below)",
tc.kind()
),
sym,
));
}
let edge = tc.edge();
if edge != EdgeKind::PosEdge && edge != EdgeKind::NegEdge {
return Err(LoweringError::unsupported_at(
&format!(
"`{what}` block's signal-event control has edge `{edge:?}`, not \
`posedge`/`negedge` -- a level-sensitive (no-edge) event control is \
unsupported in the A2 slice"
),
sym,
));
}
let clock_sym = tc
.children()
.into_iter()
.find_map(|n| n.as_expression())
.and_then(|e| e.referenced_symbol())
.ok_or_else(|| {
LoweringError::unsupported_at(
&format!(
"`{what}` block's edge control does not directly reference a plain \
signal (unsupported clock expression shape)"
),
sym,
)
})?;
Ok((clock_sym, edge))
}
type ClockAndOptionalReset<'d> = ((Symbol<'d>, EdgeKind), Option<(Symbol<'d>, EdgeKind)>);
fn require_clock_and_optional_reset<'d>(
sym: &Symbol<'d>,
full_body: Statement<'d>,
in_checker: bool,
) -> Result<ClockAndOptionalReset<'d>, LoweringError> {
if full_body.kind() != StatementKind::Timed {
return Err(LoweringError::unsupported_at(
"`always_ff` block has no `@(...)` timing control at its head -- a bare \
`always_ff` whose body starts directly with a statement (no head timing \
control at all) is unsupported in the A2 slice",
sym,
));
}
let tc = full_body.timing_control().ok_or_else(|| {
LoweringError::unsupported_at(
"`always_ff` block's `Timed` statement has no timing control (unexpected AST shape)",
sym,
)
})?;
if tc.kind() != TimingControlKind::EventList {
let clk = require_single_edge_head(sym, "always_ff", full_body)?;
return Ok((clk, None));
}
let subs: Vec<TimingControl<'d>> = tc
.children()
.into_iter()
.filter_map(|n| n.as_timing_control())
.collect();
if subs.len() != 2 {
return Err(LoweringError::unsupported_at(
&format!(
"`always_ff` block's event-list head has {} sub-control(s), not exactly 2 -- \
this task's scope is exactly one clock + one async reset \
(`@(posedge clk or negedge rst_n)`); an arbitrary N-signal event list for \
`always_ff` is unsupported",
subs.len()
),
sym,
));
}
let mut edges: Vec<(Symbol<'d>, EdgeKind)> = Vec::with_capacity(2);
for sub in &subs {
if sub.kind() != TimingControlKind::SignalEvent {
return Err(LoweringError::unsupported_at(
&format!(
"`always_ff` block's event-list head has a `{:?}` sub-control, not a \
`posedge`/`negedge` signal-event control",
sub.kind()
),
sym,
));
}
let edge = sub.edge();
if edge != EdgeKind::PosEdge && edge != EdgeKind::NegEdge {
return Err(LoweringError::unsupported_at(
&format!(
"`always_ff` block's event-list head has a sub-control with edge \
`{edge:?}`, not `posedge`/`negedge` -- a bare, level-sensitive entry in \
an `always_ff` event list is not a valid async-reset idiom"
),
sym,
));
}
let signal = sub
.children()
.into_iter()
.find_map(|n| n.as_expression())
.and_then(|e| e.referenced_symbol())
.ok_or_else(|| {
LoweringError::unsupported_at(
"`always_ff` block's event-list head has a sub-control that does not \
directly reference a plain signal (unsupported clock/reset expression \
shape)",
sym,
)
})?;
edges.push((signal, edge));
}
if let Some(pair) = identify_clock_and_reset(full_body, edges[0], edges[1]) {
return Ok(pair);
}
if in_checker {
return Ok((edges[0], None));
}
Err(LoweringError::unsupported_at(
"`always_ff` block's event-list head has a clock+async-reset pair, but its body's \
top-level `if` (looking only through `begin`/`end` wrapping) does not \
unambiguously identify exactly one of the two entries as the reset -- by testing \
a bare reference to it with a polarity matching its own declared edge -- so the \
clock and reset cannot be told apart by source order alone (LRM 1800-2023 \
imposes no ordering requirement on an event list's members)",
sym,
))
}
fn has_value_change_entry(tc: TimingControl<'_>) -> bool {
let entries: Vec<TimingControl<'_>> = if tc.kind() == TimingControlKind::SignalEvent {
vec![tc]
} else if tc.kind() == TimingControlKind::EventList {
tc.children()
.into_iter()
.filter_map(|n| n.as_timing_control())
.collect()
} else {
return false;
};
!entries.is_empty()
&& entries
.iter()
.all(|t| t.kind() == TimingControlKind::SignalEvent)
&& entries.iter().any(|t| t.edge() == EdgeKind::None)
}
fn is_wide_edge_list_head(full_body: Statement<'_>) -> bool {
let Some(tc) = full_body.timing_control() else {
return false;
};
if tc.kind() != TimingControlKind::EventList {
return false;
}
let subs: Vec<TimingControl<'_>> = tc
.children()
.into_iter()
.filter_map(|n| n.as_timing_control())
.collect();
subs.len() > 2
&& subs
.iter()
.all(|t| t.kind() == TimingControlKind::SignalEvent)
}
fn is_clock_reset_idiom_head(full_body: Statement<'_>) -> bool {
let Some(tc) = full_body.timing_control() else {
return false;
};
let edge_entry = |t: &TimingControl<'_>| {
t.kind() == TimingControlKind::SignalEvent
&& (t.edge() == EdgeKind::PosEdge || t.edge() == EdgeKind::NegEdge)
&& t.children()
.into_iter()
.find_map(|n| n.as_expression())
.and_then(|e| e.referenced_symbol())
.is_some()
};
if tc.kind() == TimingControlKind::SignalEvent {
return edge_entry(&tc);
}
if tc.kind() == TimingControlKind::EventList {
let subs: Vec<TimingControl<'_>> = tc
.children()
.into_iter()
.filter_map(|n| n.as_timing_control())
.collect();
return subs.len() == 2 && subs.iter().all(edge_entry);
}
false
}
fn identify_clock_and_reset<'d>(
full_body: Statement<'d>,
a: (Symbol<'d>, EdgeKind),
b: (Symbol<'d>, EdgeKind),
) -> Option<ClockAndOptionalReset<'d>> {
let cond = sv_sim_sim::process::cfg::live_statements(&full_body)
.into_iter()
.find_map(find_top_level_conditional)?;
let conditions = cond.conditions();
let polarity = if conditions.len() == 1 {
condition_polarity(conditions[0].expr())
} else {
None
};
let matches = |sig: Symbol<'d>, edge: EdgeKind| {
polarity.is_some_and(|(psym, active_high)| {
psym.uid() == sig.uid() && active_high == (edge == EdgeKind::PosEdge)
})
};
let a_matches = matches(a.0, a.1);
let b_matches = matches(b.0, b.1);
match (a_matches, b_matches) {
(true, false) => Some((b, Some(a))),
(false, true) => Some((a, Some(b))),
_ => None,
}
}
fn find_top_level_conditional<'d>(stmt: Statement<'d>) -> Option<Statement<'d>> {
let stmt = sv_sim_sim::process::cfg::unwrap_invalid(stmt);
if stmt.kind() == StatementKind::Conditional {
return Some(stmt);
}
if matches!(stmt.kind(), StatementKind::Block | StatementKind::List) {
return sv_sim_sim::process::cfg::live_statements(&stmt)
.into_iter()
.find_map(find_top_level_conditional);
}
None
}
fn condition_polarity<'d>(expr: Expression<'d>) -> Option<(Symbol<'d>, bool)> {
if sv_sim_sim::symid::is_value_name(expr.kind()) {
return expr.referenced_symbol().map(|s| (s, true));
}
if expr.kind() == ExpressionKind::UnaryOp
&& matches!(
expr.unary_op(),
Some(UnaryOp::LogicalNot | UnaryOp::BitwiseNot)
)
{
return expr
.operand()
.and_then(|op| op.referenced_symbol())
.map(|s| (s, false));
}
None
}
fn reject_unsupported_in_body<'d>(
block_sym: &Symbol<'d>,
root: Statement<'d>,
) -> Result<(), LoweringError> {
reject_unsupported_in_body_with(block_sym, root, false)
}
fn reject_unsupported_in_body_with<'d>(
block_sym: &Symbol<'d>,
root: Statement<'d>,
nested_assertions_ok: bool,
) -> Result<(), LoweringError> {
let mut stack = vec![root.as_sem_node()];
while let Some(node) = stack.pop() {
if let Some(stmt) = node.as_statement()
&& stmt.kind() == StatementKind::ConcurrentAssertion
{
if nested_assertions_ok {
continue;
}
validate_assertion_leaves(block_sym, stmt)?;
return Err(LoweringError::unsupported_at(
&format!(
"{:?} statement (SVA concurrent assertions are deferred, XC-007)",
stmt.kind()
),
block_sym,
));
}
stack.extend(sv_sim_sim::process::cfg::live_children(&node));
}
Ok(())
}
pub fn static_assertion_statement<'d>(full_body: Statement<'d>) -> Option<Statement<'d>> {
let stmt = through_single_statement_blocks(full_body)?;
(stmt.kind() == StatementKind::ConcurrentAssertion).then_some(stmt)
}
fn through_single_statement_blocks<'d>(mut stmt: Statement<'d>) -> Option<Statement<'d>> {
while stmt.kind() == StatementKind::Block {
let inner: Vec<Statement<'d>> = stmt
.as_sem_node()
.children()
.into_iter()
.filter_map(|n| n.as_statement())
.collect();
match inner.as_slice() {
[one] => stmt = *one,
_ => return None,
}
}
Some(stmt)
}
pub(crate) fn is_static_deferred_assertion(block: &Symbol<'_>) -> bool {
static_deferred_assertion_of(block).is_some()
}
pub fn static_deferred_assertion_of<'d>(block: &Symbol<'d>) -> Option<Statement<'d>> {
if block.kind() != SymbolKind::ProceduralBlock
|| block.procedural_block_procedure_kind() != ProceduralBlockKind::Always
{
return None;
}
block
.body()
.and_then(through_single_statement_blocks)
.filter(|s| {
s.kind() == StatementKind::ImmediateAssertion && s.is_deferred_assertion() == Some(true)
})
}
fn has_comb_semantics(block: &Symbol<'_>) -> bool {
block.kind() == SymbolKind::ProceduralBlock
&& (matches!(
block.procedural_block_procedure_kind(),
ProceduralBlockKind::AlwaysComb | ProceduralBlockKind::AlwaysLatch
) || is_static_deferred_assertion(block))
}
fn validate_assertion_leaves<'d>(
block_sym: &Symbol<'d>,
stmt: Statement<'d>,
) -> Result<(), LoweringError> {
if let Some(root) = stmt
.as_sem_node()
.children()
.into_iter()
.find_map(|n| n.as_assertion_expr())
{
let mut leaves = Vec::new();
crate::sva::collect_boolean_leaves(root, &mut leaves);
for leaf in leaves {
crate::sva::leaf::validate_boolean_leaf(leaf, block_sym)?;
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KindCountDrift {
pub kind_name: &'static str,
pub expected: u16,
pub actual: u16,
}
impl fmt::Display for KindCountDrift {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}::COUNT drifted: expected {} (last reviewed value), found {} -- re-review \
every exhaustive match over {} in crates/sv-sim-codegen/src/module.rs before \
updating the expected count",
self.kind_name, self.expected, self.actual, self.kind_name
)
}
}
impl std::error::Error for KindCountDrift {}
fn assert_kind_count(
kind_name: &'static str,
actual: u16,
expected: u16,
) -> Result<(), KindCountDrift> {
if actual == expected {
Ok(())
} else {
Err(KindCountDrift {
kind_name,
expected,
actual,
})
}
}
pub fn assert_kind_counts_match_pinned_slang() -> Result<(), KindCountDrift> {
assert_kind_count("TimingControlKind", TimingControlKind::COUNT, 10)?;
assert_kind_count("StatementKind", StatementKind::COUNT, 32)?;
assert_kind_count("ExpressionKind", ExpressionKind::COUNT, 41)?;
Ok(())
}
pub fn signal_slots_of<'d>(
body: &Symbol<'d>,
) -> Result<HashMap<SymbolId, SignalSlot>, LoweringError> {
let mut map = HashMap::new();
let mut slot_index: u32 = 0;
for member in sv_sim_sim::netlist::state_items(body) {
let kind = member.kind();
if !matches!(
kind,
SymbolKind::Variable | SymbolKind::Net | SymbolKind::Port | SymbolKind::FormalArgument
) {
continue;
}
if kind == SymbolKind::Port && member.port_internal_symbol().is_some() {
continue;
}
if kind == SymbolKind::Port && member.port_is_null_port() {
continue;
}
let ty = if kind == SymbolKind::Port {
member.port_type()
} else {
member.value_type()
};
let Some(ty) = ty else {
continue;
};
let (width, signed, class) = if kind == SymbolKind::Net
&& sv_sim_sim::state::storage::interconnect_dims(&ty).is_some()
{
(1, false, SignalSlotClass::Inline)
} else if sv_sim_sim::state::storage::is_collection(&ty) {
(64, false, SignalSlotClass::Inline)
} else if ty.is_unpacked_array() {
let shape = crate::arrays::array_shape(&ty).ok_or_else(|| {
LoweringError::unsupported_at("a dynamically sized unpacked array member", &member)
})?;
let elem_ty = shape.elem;
if !(elem_ty.is_integral()
|| elem_ty.is_floating()
|| elem_ty.is_string()
|| elem_ty.is_chandle()
|| elem_ty.is_covergroup()
|| elem_ty.is_class()
|| elem_ty.is_virtual_interface()
|| sv_sim_sim::state::storage::is_unpacked_aggregate(&elem_ty)
|| elem_ty.is_event()
|| sv_sim_sim::state::storage::is_collection(&elem_ty))
{
return Err(LoweringError::unsupported_at(
"unpacked-array element type is not a value type this simulator \
stores in a memory row",
&member,
));
}
let elem_width = if elem_ty.is_event() {
sv_sim_sim::state::EVENT_CELL_WIDTH
} else {
elem_ty.value_width()
};
let range = ty.fixed_unpacked_array_range();
(
elem_width,
elem_ty.is_signed(),
SignalSlotClass::Memory {
num_rows: shape.rows() as u32,
lower_index: i64::from(range.lower()),
},
)
} else if ty.is_event() {
(
sv_sim_sim::state::EVENT_CELL_WIDTH,
false,
SignalSlotClass::Wide,
)
} else {
let width = ty.value_width();
let class = if width > 64 {
SignalSlotClass::Wide
} else {
SignalSlotClass::Inline
};
(width, ty.is_signed(), class)
};
map.insert(
member.uid(),
SignalSlot {
slot: slot_index,
width,
signed,
class,
},
);
slot_index += 1;
}
for (cb, vars) in sv_sim_sim::state::clocking_blocks(body) {
map.insert(
cb.uid(),
SignalSlot {
slot: slot_index,
width: 32,
signed: false,
class: SignalSlotClass::Inline,
},
);
slot_index += 1;
for v in vars {
let ty = v.value_type().ok_or_else(|| {
LoweringError::unsupported_at("a clock variable with no type", &v)
})?;
let width = ty.value_width();
map.insert(
v.uid(),
SignalSlot {
slot: slot_index,
width,
signed: ty.is_signed(),
class: if width > 64 {
SignalSlotClass::Wide
} else {
SignalSlotClass::Inline
},
},
);
slot_index += 1;
}
}
let hidden = sv_sim_sim::sva::sampled::hidden_slots(body)
.map_err(|e| LoweringError::unsupported(&e.to_string(), &body.hierarchical_path()))?;
for cell in hidden.slots() {
let src = if let Some(shape) = &cell.foreign {
SignalSlot {
slot: 0,
width: shape.width,
signed: shape.signed,
class: if shape.width > 64 {
SignalSlotClass::Wide
} else {
SignalSlotClass::Inline
},
}
} else {
map.get(&cell.source).copied().ok_or_else(|| {
LoweringError::unsupported(
&format!(
"sampled value function over `{}`, which has no state slot",
cell.source
),
&body.hierarchical_path(),
)
})?
};
map.insert(
cell.id.clone(),
SignalSlot {
slot: slot_index,
..src
},
);
slot_index += 1;
}
for w in sv_sim_sim::sva::endpoint::watchers(body) {
map.insert(
w.id(),
SignalSlot {
slot: slot_index,
width: sv_sim_sim::state::EVENT_CELL_WIDTH,
signed: false,
class: SignalSlotClass::Wide,
},
);
slot_index += 1;
}
if let Some(pool) = sv_sim_sim::state::event_pool::event_pool(body) {
let entry = |num_rows: u32, lower_index: i64| SignalSlot {
slot: slot_index,
width: sv_sim_sim::state::EVENT_CELL_WIDTH,
signed: false,
class: SignalSlotClass::Memory {
num_rows,
lower_index,
},
};
map.insert(
sv_sim_sim::state::event_pool::pool_key(),
entry(pool.total_rows(), i64::from(pool.static_rows)),
);
for s in &pool.statics {
map.insert(
sv_sim_sim::state::event_pool::static_key(&s.var.uid()),
entry(s.count, i64::from(s.first)),
);
map.insert(
sv_sim_sim::state::event_pool::pool_of_key(&s.var.uid()),
entry(pool.total_rows(), i64::from(pool.static_rows)),
);
}
}
Ok(map)
}