Fix NFA assertion crash and mis-counted property if/case (#8074)

This commit is contained in:
Yilou Wang
2026-08-18 18:30:38 +02:00
committed by GitHub
parent b032379de2
commit 8a29360ade
12 changed files with 562 additions and 112 deletions
+232 -82
View File
@@ -21,6 +21,9 @@
// - Replace converted assertions with combinational match/reject checks
// so V3AssertPre sees no multi-cycle SExpr (unsupported ones fall through).
//
// Members marked OWNED hold an AST tree this pass allocated and must delete;
// they are not linked into the netlist.
//
//*************************************************************************
#include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT
@@ -55,7 +58,7 @@ struct SvaVertexData final {
AstVar* delayRingWrappedVarp = nullptr; // All slots written since the last clear
AstVar* doneLVarp = nullptr; // SAnd LHS done-latch
AstVar* doneRVarp = nullptr; // SAnd RHS done-latch
AstNodeExpr* stateSigp = nullptr; // Combinational state signal (owned during lowering)
AstNodeExpr* stateSigp = nullptr; // Combinational state signal; OWNED during lowering
bool needsReg = false; // True if vertex has incoming clocked edge
};
@@ -65,12 +68,14 @@ class SvaStateVertex final : public V3GraphVertex {
public:
// True if this is the sequence-match terminal vertex
bool m_isMatch = false;
// Owned throughout-guard condition clones; IEEE 1800-2023 16.9.9
// OWNED throughout-guard condition clones; IEEE 1800-2023 16.9.9
std::vector<AstNodeExpr*> m_throughoutConds;
// Nonzero for a bitset ring-buffer vertex for ## delays.
bool m_isFixedDelayRing = false;
int m_delayRingSize = 0; // Fixed delay cycles. Range: max-min+1.
AstNodeExpr* m_delayRingClearCondp = nullptr; // local RHS for pure-boolean range
// OWNED; enclosing-abort fire condition clearing in-flight ring bits
AstNodeExpr* m_abortClearp = nullptr;
// Liveness terminal (IEEE weak semantics): reject must not fire from this source
bool m_isUnbounded = false;
// Temporal sequence AND combiner; IEEE 1800-2023 16.9.5
@@ -95,6 +100,7 @@ public:
for (AstNodeExpr* cp : m_throughoutConds) VL_DO_DANGLING(cp->deleteTree(), cp);
if (m_delayRingClearCondp)
VL_DO_DANGLING(m_delayRingClearCondp->deleteTree(), m_delayRingClearCondp);
if (m_abortClearp) VL_DO_DANGLING(m_abortClearp->deleteTree(), m_abortClearp);
if (m_andLhsCondp) VL_DO_DANGLING(m_andLhsCondp->deleteTree(), m_andLhsCondp);
if (m_andRhsCondp) VL_DO_DANGLING(m_andRhsCondp->deleteTree(), m_andRhsCondp);
}
@@ -183,8 +189,8 @@ public:
std::vector<const SvaTransEdge*> allEdges() const {
std::vector<const SvaTransEdge*> result;
for (const V3GraphVertex& vtxr : m_graph.vertices()) {
for (const V3GraphEdge& er : vtxr.outEdges()) {
result.push_back(static_cast<const SvaTransEdge*>(&er));
for (const V3GraphEdge& edger : vtxr.outEdges()) {
result.push_back(static_cast<const SvaTransEdge*>(&edger));
}
}
return result;
@@ -208,6 +214,11 @@ struct BuildResult final {
static BuildResult failWithError() { return {nullptr, nullptr, {}, true}; }
};
// Parser-marked SAnd of overlapped implications: a property if/else/case.
static bool hasPropertyControlConjunction(const AstNodeExpr* nodep) {
return nodep->exists([](const AstSAnd* andp) { return andp->propertyControl(); });
}
static AstConst* newTypedConstp(FileLine* const flp, const AstNodeDType* const dtypep,
const uint32_t value) {
AstConst* const constp = new AstConst{flp, AstConst::DTyped{}, dtypep};
@@ -722,6 +733,7 @@ class SvaNfaBuilder final {
// Do not mark liveness sources: first boolean check is deferred.
edgep->m_rejectOnFail = true;
}
freeUnlinkedCondp(pre.finalCondp);
currentp = condVtxp;
} else {
currentp = pre.termVertexp;
@@ -965,12 +977,20 @@ class SvaNfaBuilder final {
return {mergeVtxp, nullptr, {}};
}
// Free a dropped sub-result condition that is not linked into the AST
// (abort folds synthesize unparented finalCondp trees).
static void freeUnlinkedCondp(AstNodeExpr* condp) {
if (condp && !condp->backp()) VL_DO_DANGLING(condp->deleteTree(), condp);
}
// Build merge vertex for SOr / LogOr: both branches feed into one vertex.
BuildResult buildOrMerge(AstNodeExpr* lhsp, AstNodeExpr* rhsp, SvaStateVertex* entryVtxp,
FileLine* flp) {
const BuildResult lhs = buildExpr(lhsp, entryVtxp);
const BuildResult rhs = buildExpr(rhsp, entryVtxp);
if (!lhs.valid() || !rhs.valid()) { // LCOV_EXCL_START -- sub-build fail bail
freeUnlinkedCondp(lhs.finalCondp);
freeUnlinkedCondp(rhs.finalCondp);
return BuildResult::fail(lhs.errorEmitted || rhs.errorEmitted);
} // LCOV_EXCL_STOP
// IEEE 1800-2023 16.14.3: a cover sequence counts every end-of-match. A
@@ -980,6 +1000,8 @@ class SvaNfaBuilder final {
// is handled by the OR-fold.
if (m_isCoverSeq && (lhs.termVertexp != entryVtxp || rhs.termVertexp != entryVtxp)) {
warnEndpointUnsupported(flp, "a sequence operand of 'or'");
freeUnlinkedCondp(lhs.finalCondp);
freeUnlinkedCondp(rhs.finalCondp);
return BuildResult::failWithError();
}
SvaStateVertex* const mergeVtxp = scopedCreateVertex();
@@ -995,6 +1017,8 @@ class SvaNfaBuilder final {
} else {
guardedLink(rhs.termVertexp, mergeVtxp, flp);
}
freeUnlinkedCondp(lhs.finalCondp);
freeUnlinkedCondp(rhs.finalCondp);
return {mergeVtxp, nullptr, {}};
}
@@ -1010,6 +1034,8 @@ class SvaNfaBuilder final {
const bool rhsScope = m_inUnboundedScope;
m_inUnboundedScope = savedScope || lhsScope || rhsScope;
if (!lhs.valid() || !rhs.valid()) { // LCOV_EXCL_START -- sub-build fail bail
freeUnlinkedCondp(lhs.finalCondp);
freeUnlinkedCondp(rhs.finalCondp);
return BuildResult::fail(lhs.errorEmitted || rhs.errorEmitted);
} // LCOV_EXCL_STOP
@@ -1021,12 +1047,20 @@ class SvaNfaBuilder final {
"Single-cycle SAnd operands must have finalCondp");
AstNodeExpr* const condp = new AstLogAnd{flp, lhs.finalCondp->cloneTreePure(false),
rhs.finalCondp->cloneTreePure(false)};
freeUnlinkedCondp(lhs.finalCondp);
freeUnlinkedCondp(rhs.finalCondp);
return {entryVtxp, condp, {}};
}
// Range-delay mid-window sources in either sub-branch would need
// to be folded into the latch's match-now signal, which the
// current combiner does not support. Defer (UNSUPPORTED).
if (!lhs.midSources.empty() || !rhs.midSources.empty()) return BuildResult::fail();
// current combiner does not support.
if (!lhs.midSources.empty() || !rhs.midSources.empty()) {
flp->v3warn(E_UNSUPPORTED,
"Unsupported: ranged cycle delay in an operand of property 'and'");
freeUnlinkedCondp(lhs.finalCondp);
freeUnlinkedCondp(rhs.finalCondp);
return BuildResult::failWithError();
}
SvaStateVertex* const combVtxp = scopedCreateVertex();
combVtxp->m_isAndCombiner = true;
combVtxp->m_andLhsTermp = lhs.termVertexp;
@@ -1064,6 +1098,8 @@ class SvaNfaBuilder final {
}
}
}
freeUnlinkedCondp(lhs.finalCondp);
freeUnlinkedCondp(rhs.finalCondp);
return {combVtxp, nullptr, {}};
}
@@ -1421,16 +1457,74 @@ class SvaNfaBuilder final {
return resultp;
}
// True when a same-tick Link chain already accounts the attempt: a
// required-step Link covers both outcomes; followed-by pairs both edges.
static bool chainAccountsSource(const SvaStateVertex* srcp,
const std::unordered_set<const V3GraphEdge*>& preEdges) {
bool plainNonSink = false;
bool markedSink = false;
for (const V3GraphEdge& edger : srcp->outEdges()) {
if (preEdges.count(&edger)) continue;
const SvaTransEdge& tedger = static_cast<const SvaTransEdge&>(edger);
if (tedger.m_consumesCycle) continue;
const bool sink = static_cast<const SvaStateVertex*>(tedger.toVtxp())->m_isRejectSink;
if (tedger.m_rejectOnFail) {
if (!sink) return true;
markedSink = true;
} else if (!sink) {
plainNonSink = true;
}
}
return plainNonSink && markedSink;
}
// Reject edge: fires when the source is live and the abort samples true.
void addAbortRejectEdge(SvaStateVertex* srcp, SvaStateVertex* sinkp, AstNodeExpr* condp,
FileLine* flp) {
AstNodeExpr* const notFirep = new AstLogNot{flp, sampled(abortFireExpr(condp, flp))};
m_graph.addLink(srcp, sinkp, notFirep)->m_rejectOnFail = true;
return;
}
// On the fire tick: kill body threads; accept kinds also forgive step misses.
void gateBodyEdgesOnAbort(const std::unordered_set<const V3GraphEdge*>& preEdges,
AstNodeExpr* condp, VAbortKind kind, FileLine* flp) {
for (V3GraphVertex& vtxr : m_graph.m_graph.vertices()) {
for (V3GraphEdge& edger : vtxr.outEdges()) {
if (preEdges.count(&edger)) continue;
SvaTransEdge* const tedgep = static_cast<SvaTransEdge*>(&edger);
if (tedgep->m_rejectOnFail) {
if (!kind.isAccept()) continue;
AstNodeExpr* const firep = sampled(abortFireExpr(condp, flp));
tedgep->m_condp
= tedgep->m_condp ? new AstLogOr{flp, tedgep->m_condp, firep} : firep;
} else if (tedgep->m_consumesCycle) {
AstNodeExpr* const notFirep
= new AstLogNot{flp, sampled(abortFireExpr(condp, flp))};
tedgep->m_condp = tedgep->m_condp
? new AstLogAnd{flp, tedgep->m_condp, notFirep}
: notFirep;
}
}
}
}
BuildResult buildAbortOn(AstNodeExpr* condp, AstNodeExpr* bodyp, SvaStateVertex* entryVtxp,
VAbortKind kind, FileLine* flp) {
// Snapshot pre-body vertices so post-build diff yields the body's sub-NFA.
VAbortKind kind, FileLine* flp, bool isTopLevelStep) {
// Snapshot pre-body vertices/edges so post-build diff yields the body's sub-NFA.
std::unordered_set<const V3GraphVertex*> preExisting;
for (const V3GraphVertex& vtxr : m_graph.m_graph.vertices()) preExisting.insert(&vtxr);
std::unordered_set<const V3GraphEdge*> preEdges;
for (V3GraphVertex& vtxr : m_graph.m_graph.vertices()) {
preExisting.insert(&vtxr);
for (V3GraphEdge& edger : vtxr.outEdges()) preEdges.insert(&edger);
}
m_outerAbortStack.push_back(condp);
const BuildResult bodyResult = buildExpr(bodyp, entryVtxp, /*isTopLevelStep=*/false);
const BuildResult bodyResult = buildExpr(bodyp, entryVtxp, isTopLevelStep);
m_outerAbortStack.pop_back();
UASSERT_OBJ(bodyResult.valid(), bodyp, "abort body must be a valid SVA expression");
if (!bodyResult.valid()) return bodyResult;
gateBodyEdgesOnAbort(preEdges, condp, kind, flp);
// Live-thread sources for the abort edge: entry + new body vertices,
// minus reject sinks (they carry reject fuel, not live-thread fuel).
@@ -1439,6 +1533,11 @@ class SvaNfaBuilder final {
for (V3GraphVertex& vtxr : m_graph.m_graph.vertices()) {
if (preExisting.count(&vtxr)) continue;
auto* const sp = static_cast<SvaStateVertex*>(&vtxr);
if (sp->m_delayRingSize) {
AstNodeExpr* const firep = abortFireExpr(condp, flp);
sp->m_abortClearp
= sp->m_abortClearp ? new AstLogOr{flp, sp->m_abortClearp, firep} : firep;
}
if (sp->m_isRejectSink) continue;
abortSources.push_back(sp);
}
@@ -1450,15 +1549,18 @@ class SvaNfaBuilder final {
if (kind.isAccept()) {
// Match-only sink fed by $sampled(abort-fire) from every live source;
// registered as midSource so it never contributes a reject. The body
// terminal is already in abortSources, so we don't fold abort-fire
// into bodyResult.finalCondp.
// registered as midSource so it never contributes a reject.
SvaStateVertex* const acceptSinkp = scopedCreateVertex();
for (SvaStateVertex* const srcp : abortSources)
guardedLink(srcp, acceptSinkp, sampledAbortFire(), flp);
std::vector<SvaStateVertex*> midSources = bodyResult.midSources;
midSources.push_back(acceptSinkp);
return {bodyResult.termVertexp, bodyResult.finalCondp, std::move(midSources)};
AstNodeExpr* finalCondp = bodyResult.finalCondp;
if (finalCondp) {
if (finalCondp->backp()) finalCondp = finalCondp->cloneTreePure(false);
finalCondp = new AstLogOr{flp, finalCondp, abortFireExpr(condp, flp)};
}
return {bodyResult.termVertexp, finalCondp, std::move(midSources)};
}
// rejectOnFail treats m_condp as the success condition and fires on
@@ -1466,10 +1568,15 @@ class SvaNfaBuilder final {
SvaStateVertex* const rejectSinkp = m_graph.createStateVertex();
rejectSinkp->m_isRejectSink = true;
for (SvaStateVertex* const srcp : abortSources)
m_graph.addLink(srcp, rejectSinkp, new AstLogNot{flp, sampledAbortFire()})
->m_rejectOnFail
= true;
return bodyResult;
if (!chainAccountsSource(srcp, preEdges))
addAbortRejectEdge(srcp, rejectSinkp, condp, flp);
AstNodeExpr* finalCondp = bodyResult.finalCondp;
if (finalCondp) {
if (finalCondp->backp()) finalCondp = finalCondp->cloneTreePure(false);
finalCondp
= new AstLogAnd{flp, finalCondp, new AstLogNot{flp, abortFireExpr(condp, flp)}};
}
return {bodyResult.termVertexp, finalCondp, bodyResult.midSources};
}
public:
@@ -1482,10 +1589,11 @@ public:
, m_isSeqEvent{isSeqEvent} {}
// Reset scope between antecedent and consequent: liveness must not leak.
// m_outerAbortStack survives: an abort wrapping the implication covers the
// consequent too (IEEE 1800-2023 16.12.14).
void resetScope() {
m_inUnboundedScope = false;
m_temporalGuardStack.clear();
m_outerAbortStack.clear();
}
BuildResult buildExpr(AstNodeExpr* nodep, SvaStateVertex* entryVtxp,
@@ -1540,7 +1648,8 @@ public:
return buildSWithin(withinp, entryVtxp, isTopLevelStep);
}
if (AstAbortOn* const ap = VN_CAST(nodep, AbortOn)) {
return buildAbortOn(ap->condp(), ap->propp(), entryVtxp, ap->kind(), ap->fileline());
return buildAbortOn(ap->condp(), ap->propp(), entryVtxp, ap->kind(), ap->fileline(),
isTopLevelStep);
}
if (VN_IS(nodep, SNonConsRep)) return BuildResult::fail();
if (AstImplication* const implp = VN_CAST(nodep, Implication)) {
@@ -1746,21 +1855,19 @@ class SvaNfaLowering final {
// latches the OR of its incoming contributions.
void emitStateRegisterNba(LowerCtx& c) {
AstNode* bodyp = nullptr;
bool hasDelayRing = false;
for (int i = 0; i < c.N; ++i) {
if (c.vtx[i]->datap()->delayRingVarp) hasDelayRing = true;
if (!c.vtx[i]->datap()->stateVarp) continue;
AstNodeExpr* nextStatep = nullptr;
for (const V3GraphEdge& er : c.vtx[i]->inEdges()) {
const SvaTransEdge& te = static_cast<const SvaTransEdge&>(er);
if (!te.m_consumesCycle) continue;
const int fromIdx = te.fromVtxp()->color();
UASSERT_OBJ(c.vtx[fromIdx]->datap()->stateSigp, te.fromVtxp(),
for (const V3GraphEdge& edger : c.vtx[i]->inEdges()) {
const SvaTransEdge& tedger = static_cast<const SvaTransEdge&>(edger);
if (!tedger.m_consumesCycle) continue;
const int fromIdx = tedger.fromVtxp()->color();
UASSERT_OBJ(c.vtx[fromIdx]->datap()->stateSigp, tedger.fromVtxp(),
"Clocked-edge source missing stateSig");
AstNodeExpr* srcSigp = c.vtx[fromIdx]->datap()->stateSigp->cloneTreePure(false);
srcSigp = andCond(c.flp, srcSigp, te.m_condp);
srcSigp = andCond(c.flp, srcSigp, tedger.m_condp);
if (c.disableExprp) {
AstNodeExpr* const notDisp
@@ -1781,8 +1888,8 @@ class SvaNfaLowering final {
}
// Capture disableCnt in Phase-2 NBA before any reactive re-evaluation.
// snapshotVarp and disableCntVarp are allocated together.
if (c.snapshotVarp && (bodyp || hasDelayRing)) {
// Emitted even for stateless graphs; snapshotOk gates rejects there too.
if (c.snapshotVarp) {
UASSERT_OBJ(c.disableCntVarp, c.senTreep, "snapshotVarp set without disableCntVarp");
// disable_snapshot <= disable_count;
AstAssignDly* const snapshotp
@@ -1807,15 +1914,15 @@ class SvaNfaLowering final {
const uint32_t size = static_cast<uint32_t>(vtxp->m_delayRingSize);
AstNodeExpr* incomingp = nullptr;
for (const SvaTransEdge* const tep : c.edges) {
if (static_cast<int>(tep->toVtxp()->color()) != ri) continue;
UASSERT_OBJ(tep->m_consumesCycle == vtxp->m_isFixedDelayRing, vtxp,
for (const SvaTransEdge* const tedgep : c.edges) {
if (static_cast<int>(tedgep->toVtxp()->color()) != ri) continue;
UASSERT_OBJ(tedgep->m_consumesCycle == vtxp->m_isFixedDelayRing, vtxp,
"Delay-ring incoming edge kind mismatch");
const int fi = tep->fromVtxp()->color();
const int fi = tedgep->fromVtxp()->color();
UASSERT_OBJ(c.vtx[fi]->datap()->stateSigp, c.vtx[fi],
"Delay-ring incoming source missing stateSig");
AstNodeExpr* contribp = c.vtx[fi]->datap()->stateSigp->cloneTreePure(false);
contribp = andCond(c.flp, contribp, tep->m_condp);
contribp = andCond(c.flp, contribp, tedgep->m_condp);
if (c.disableExprp) {
AstNodeExpr* const notDisp
= new AstLogNot{c.flp, c.disableExprp->cloneTreePure(false)};
@@ -1858,6 +1965,10 @@ class SvaNfaLowering final {
clearCondp = orExprs(c.flp, clearCondp,
sampled(vtxp->m_delayRingClearCondp->cloneTreePure(false)));
}
if (vtxp->m_abortClearp) {
clearCondp = orExprs(c.flp, clearCondp,
sampled(vtxp->m_abortClearp->cloneTreePure(false)));
}
if (c.disableExprp) {
clearCondp = orExprs(c.flp, clearCondp, c.disableExprp->cloneTreePure(false));
}
@@ -1960,14 +2071,14 @@ class SvaNfaLowering final {
// end-of-match fires the action independently, no OR-fold).
void computeTerminalMatchAndReject(LowerCtx& c, AstNodeExpr* snapshotOkp, SignalSet& sigs,
std::vector<AstNodeExpr*>* outPerMidSrcsp = nullptr) {
for (const SvaTransEdge* const tep : c.edges) {
if (tep->toVtxp() != c.graph.m_matchVertexp) continue;
const int fi = tep->fromVtxp()->color();
UASSERT_OBJ(c.vtx[fi]->datap()->stateSigp, tep->fromVtxp(),
for (const SvaTransEdge* const tedgep : c.edges) {
if (tedgep->toVtxp() != c.graph.m_matchVertexp) continue;
const int fi = tedgep->fromVtxp()->color();
UASSERT_OBJ(c.vtx[fi]->datap()->stateSigp, tedgep->fromVtxp(),
"Terminal-link source missing stateSig");
AstNodeExpr* srcSigp = c.vtx[fi]->datap()->stateSigp->cloneTreePure(false);
srcSigp = andCond(c.flp, srcSigp, tep->m_condp);
srcSigp = andCond(c.flp, srcSigp, tedgep->m_condp);
if (snapshotOkp) {
srcSigp = new AstLogAnd{c.flp, srcSigp, snapshotOkp->cloneTreePure(false)};
}
@@ -1984,19 +2095,19 @@ class SvaNfaLowering final {
outPerMidSrcsp->push_back(perMidp);
}
if (tep->fromVtxp()->m_delayRingSize && !tep->fromVtxp()->m_isFixedDelayRing) {
if (tedgep->fromVtxp()->m_delayRingSize && !tedgep->fromVtxp()->m_isFixedDelayRing) {
sigs.terminalActivep
= orExprs(c.flp, sigs.terminalActivep, srcSigp->cloneTreePure(false));
// reject |= ring[next_idx] && final_condition;
AstNodeExpr* expireContribp = delayRingOutput(c.flp, tep->fromVtxp());
expireContribp = andCond(c.flp, expireContribp, tep->m_condp);
AstNodeExpr* expireContribp = delayRingOutput(c.flp, tedgep->fromVtxp());
expireContribp = andCond(c.flp, expireContribp, tedgep->m_condp);
if (snapshotOkp) {
expireContribp
= new AstLogAnd{c.flp, expireContribp, snapshotOkp->cloneTreePure(false)};
}
sigs.rejectBasep = orExprs(c.flp, sigs.rejectBasep, expireContribp);
VL_DO_DANGLING(srcSigp->deleteTree(), srcSigp);
} else if (tep->fromVtxp()->m_isUnbounded || tep->fromVtxp()->m_isAndCombiner) {
} else if (tedgep->fromVtxp()->m_isUnbounded || tedgep->fromVtxp()->m_isAndCombiner) {
sigs.terminalActivep = orExprs(c.flp, sigs.terminalActivep, srcSigp);
} else {
sigs.terminalActivep
@@ -2075,21 +2186,24 @@ class SvaNfaLowering final {
// Phase 3a: required-step rejection.
// Builder only sets m_rejectOnFail on non-clocked Links with m_condp
// or m_condVtxp, and the source always has a resolved stateSig.
for (const SvaTransEdge* const tep : c.edges) {
if (!tep->m_rejectOnFail) continue;
const int fi = tep->fromVtxp()->color();
UASSERT_OBJ(c.vtx[fi]->datap()->stateSigp && (tep->m_condp || tep->m_condVtxp),
tep->fromVtxp(),
for (const SvaTransEdge* const tedgep : c.edges) {
if (!tedgep->m_rejectOnFail) continue;
const int fi = tedgep->fromVtxp()->color();
UASSERT_OBJ(c.vtx[fi]->datap()->stateSigp && (tedgep->m_condp || tedgep->m_condVtxp),
tedgep->fromVtxp(),
"rejectOnFail Link must have condp/condVtxp and source stateSig");
AstNodeExpr* const srcSigp = c.vtx[fi]->datap()->stateSigp->cloneTreePure(false);
AstNodeExpr* condp = nullptr;
if (tep->m_condVtxp) {
const int ci = tep->m_condVtxp->color();
UASSERT_OBJ(c.vtx[ci]->datap()->stateSigp, tep->m_condVtxp,
if (tedgep->m_condVtxp) {
const int ci = tedgep->m_condVtxp->color();
UASSERT_OBJ(c.vtx[ci]->datap()->stateSigp, tedgep->m_condVtxp,
"rejectOnFail condVtxp missing stateSig");
condp = c.vtx[ci]->datap()->stateSigp->cloneTreePure(false);
if (tedgep->m_condp) {
condp = new AstLogOr{c.flp, condp, tedgep->m_condp->cloneTreePure(false)};
}
} else {
condp = tep->m_condp->cloneTreePure(false);
condp = tedgep->m_condp->cloneTreePure(false);
}
AstNodeExpr* const notCondp = new AstLogNot{c.flp, condp};
AstNodeExpr* const rawFailp = new AstLogAnd{c.flp, srcSigp, notCondp};
@@ -2204,13 +2318,14 @@ class SvaNfaLowering final {
// Propagate Link edges
for (int fi = 0; fi < c.N; ++fi) {
if (!c.vtx[fi]->datap()->stateSigp) continue;
for (const V3GraphEdge& er : c.vtx[fi]->outEdges()) {
const SvaTransEdge& te = static_cast<const SvaTransEdge&>(er);
if (te.m_consumesCycle) continue;
const int ti = te.toVtxp()->color();
if (te.toVtxp()->m_isMatch || te.toVtxp()->m_isRejectSink) continue;
AstNodeExpr* const contributionp = andCond(
c.flp, c.vtx[fi]->datap()->stateSigp->cloneTreePure(false), te.m_condp);
for (const V3GraphEdge& edger : c.vtx[fi]->outEdges()) {
const SvaTransEdge& tedger = static_cast<const SvaTransEdge&>(edger);
if (tedger.m_consumesCycle) continue;
const int ti = tedger.toVtxp()->color();
if (tedger.toVtxp()->m_isMatch || tedger.toVtxp()->m_isRejectSink) continue;
AstNodeExpr* const contributionp
= andCond(c.flp, c.vtx[fi]->datap()->stateSigp->cloneTreePure(false),
tedger.m_condp);
if (!c.vtx[ti]->datap()->stateSigp) {
c.vtx[ti]->datap()->stateSigp = contributionp;
changed = true;
@@ -2364,10 +2479,11 @@ public:
// Identify registered vertices (targets of clocked edges).
for (int i = 0; i < N; ++i) {
for (const V3GraphEdge& er : vtx[i]->outEdges()) {
const SvaTransEdge& te = static_cast<const SvaTransEdge&>(er);
const int toIdx = te.toVtxp()->color();
if (te.m_consumesCycle && toIdx != matchIdx && !te.toVtxp()->m_isRejectSink) {
for (const V3GraphEdge& edger : vtx[i]->outEdges()) {
const SvaTransEdge& tedger = static_cast<const SvaTransEdge&>(edger);
const int toIdx = tedger.toVtxp()->color();
if (tedger.m_consumesCycle && toIdx != matchIdx
&& !tedger.toVtxp()->m_isRejectSink) {
vtx[toIdx]->datap()->needsReg = true;
}
}
@@ -3019,9 +3135,20 @@ class AssertNfaVisitor final : public VNVisitor {
return false;
}
void processAssertion(AstNodeCoverOrAssert* assertp) {
if (assertp->immediate()) return;
// Outcome counts for a property if/case are wrong in an outcome-multiplying
// context. Returns that context, or nullptr when the shape is supported.
static const char* unsupportedPropertyControl(const AstNodeCoverOrAssert* assertp,
const AstNodeExpr* seqBodyp, bool negated) {
if (!hasPropertyControlConjunction(seqBodyp)) return nullptr;
if (negated) return "negation";
if (VN_IS(assertp, Cover)) return "cover";
if (VN_AS(assertp, Assert)->passsp()) return "a pass action";
return nullptr;
}
// Inline property/sequence refs and reject unsupported shapes.
// Returns the PropSpec to lower, or nullptr when fully handled here.
AstPropSpec* prepareAssertionProp(AstNodeCoverOrAssert* assertp) {
if (AstPropSpec* const specp = VN_CAST(assertp->propp(), PropSpec)) {
if (AstFuncRef* const funcrefp = VN_CAST(specp->propp(), FuncRef)) {
if (const AstProperty* const propyp = VN_CAST(funcrefp->taskp(), Property)) {
@@ -3033,20 +3160,36 @@ class AssertNfaVisitor final : public VNVisitor {
inlineAllSequenceRefs(assertp->propp());
if (AstPropSpec* const specp = VN_CAST(assertp->propp(), PropSpec)) {
if (hoistClockedSeq(specp)) return;
if (hoistClockedSeq(specp)) return nullptr;
}
AstPropSpec* const propp = VN_AS(assertp->propp(), PropSpec);
const bool isCover = VN_IS(assertp, Cover);
if (!isCover && effectiveAssertPropStrength(propp) == VPropStrength::STRONG) {
if (!VN_IS(assertp, Cover)
&& effectiveAssertPropStrength(propp) == VPropStrength::STRONG) {
propp->v3warn(E_UNSUPPORTED,
"Unsupported: strong property in " + assertp->verilogKwd() + ".");
replaceBodyOnBuildError(assertp->fileline(), propp, /*errorEmitted=*/true);
return;
return nullptr;
}
if (!hasMultiCycleExpr(propp)) return;
if (isBareTopLevelUntil(propp)) return;
if (!hasMultiCycleExpr(propp)) return nullptr;
// A nested property instance keeps its body behind the call; lowering would drop it.
if (propp->exists([](const AstFuncRef* refp) { return VN_IS(refp->taskp(), Property); })) {
assertp->v3warn(E_UNSUPPORTED,
"Unsupported: property instance inside a multi-cycle property "
"expression");
VL_DO_DANGLING(pushDeletep(assertp->unlinkFrBack()), assertp);
return nullptr;
}
if (isBareTopLevelUntil(propp)) return nullptr;
return propp;
}
void processAssertion(AstNodeCoverOrAssert* assertp) {
if (assertp->immediate()) return;
AstPropSpec* const propp = prepareAssertionProp(assertp);
if (!propp) return;
PropertyParts parts = decomposeProperty(propp);
UASSERT_OBJ(parts.seqExprp, propp, "Property body must be an expression");
@@ -3059,6 +3202,9 @@ class AssertNfaVisitor final : public VNVisitor {
seqBodyp = notp->lhsp();
}
const char* const propertyControlp
= unsupportedPropertyControl(assertp, seqBodyp, negated);
// Substitute property-local match-item refs in consequent with
// $past(rhs, K) before NFA build (IEEE 1800-2023 16.10).
if (liftMatchItemSubstitutions(parts, seqBodyp)) {
@@ -3068,8 +3214,6 @@ class AssertNfaVisitor final : public VNVisitor {
return;
}
AstSenTree* senTreep = assertp->sentreep();
bool senTreeOwned = false; // True if we created senTreep locally
AstCover* const coverp = VN_CAST(assertp, Cover);
const bool isCoverSeq = coverp && coverp->isCoverSeq();
// A sequence event control is not an assertion directive; no default
@@ -3082,12 +3226,10 @@ class AssertNfaVisitor final : public VNVisitor {
if (!propp->disablep() && m_defaultDisablep && !isSeqEvent) {
propp->disablep(m_defaultDisablep->condp()->cloneTreePure(true));
}
if (!senTreep && propp->sensesp()) {
senTreep = new AstSenTree{propp->fileline(), propp->sensesp()->cloneTree(true)};
senTreeOwned = true;
}
if (!propp->sensesp()) return;
AstSenTree* senTreep
= new AstSenTree{propp->fileline(), propp->sensesp()->cloneTree(true)};
AstNodeExpr* disableExprp = propp->disablep();
if (!senTreep) return;
// NFA lowering clones repeated operands and may hoist them into an
// always_comb block. Resolve implicit sampled-value clocks first, while
@@ -3111,7 +3253,15 @@ class AssertNfaVisitor final : public VNVisitor {
// from this attempt become orphan MODULETEMPs; V3Dead removes
// them along with the dead always_comb driver.
replaceBodyOnBuildError(flp, propp, result.errorEmitted);
if (senTreeOwned) VL_DO_DANGLING(pushDeletep(senTreep), senTreep);
VL_DO_DANGLING(pushDeletep(senTreep), senTreep);
return;
}
// After the build, so a construct the builder rejects reports itself.
if (propertyControlp) {
seqBodyp->v3warn(E_UNSUPPORTED,
"Unsupported: temporal property if/case with " << propertyControlp);
replaceBodyOnBuildError(flp, propp, /*errorEmitted=*/true);
VL_DO_DANGLING(pushDeletep(senTreep), senTreep);
return;
}
@@ -3149,7 +3299,7 @@ class AssertNfaVisitor final : public VNVisitor {
AstSenTree* const threadFailReplaySenTreep
= signals.threadFailCountp ? senTreep->cloneTree(false) : nullptr;
if (senTreeOwned) VL_DO_DANGLING(pushDeletep(senTreep), senTreep);
VL_DO_DANGLING(pushDeletep(senTreep), senTreep);
if (disableExprUnlinked) VL_DO_DANGLING(pushDeletep(disableExprp), disableExprp);
if (result.finalCondp && !result.finalCondp->backp()) pushDeletep(result.finalCondp);