From f694e9e1e8c6fd308391d701b435385ae14d2eda Mon Sep 17 00:00:00 2001 From: Wilson Snyder Date: Tue, 1 Sep 2026 11:15:05 -0400 Subject: [PATCH] Revert NFA assertion outcomes (#7946 revert) (#8258) --- Changes | 1 - src/V3Assert.cpp | 60 +- src/V3Assert.h | 3 - src/V3AssertNfa.cpp | 3374 +++++------------ src/V3AssertPre.cpp | 6 - src/V3AstAttr.h | 2 - src/V3AstNodeStmt.h | 4 - src/V3AstNodes.cpp | 2 - src/V3Delayed.cpp | 119 +- test_regress/t/t_assert_always_unbounded.v | 9 +- test_regress/t/t_assert_consec_rep_large.v | 2 +- test_regress/t/t_assert_ctl_lock.v | 2 +- test_regress/t/t_assert_seq_event_unsup.out | 9 - test_regress/t/t_assert_seq_event_unsup.v | 20 - test_regress/t/t_cover_property.v | 2 +- test_regress/t/t_cover_sequence.v | 8 +- test_regress/t/t_cover_sequence_unsup.out | 24 +- test_regress/t/t_cover_sequence_unsup.v | 12 +- test_regress/t/t_prop_always.v | 20 +- test_regress/t/t_prop_always_wide.v | 19 +- test_regress/t/t_prop_followed_by.v | 5 +- test_regress/t/t_prop_s_always_eos_count.out | 37 +- test_regress/t/t_prop_s_always_eos_count.py | 2 +- test_regress/t/t_prop_s_always_eos_count.v | 2 +- test_regress/t/t_prop_s_always_liveness.out | 4 +- test_regress/t/t_prop_s_always_liveness.v | 8 +- test_regress/t/t_property_abort_implication.v | 10 +- test_regress/t/t_property_accept_reject_on.v | 2 +- test_regress/t/t_property_if_else.v | 9 +- test_regress/t/t_property_nfa_abort_counts.py | 18 - test_regress/t/t_property_nfa_abort_counts.v | 108 - test_regress/t/t_property_nfa_bbox_unsup.py | 16 - test_regress/t/t_property_nfa_bbox_unsup.v | 32 - test_regress/t/t_property_nfa_composites.v | 4 +- test_regress/t/t_property_nfa_counts.v | 22 +- test_regress/t/t_property_nfa_edge_shapes.out | 4 + test_regress/t/t_property_nfa_edge_shapes.py | 2 +- test_regress/t/t_property_nfa_edge_shapes.v | 47 +- test_regress/t/t_property_nfa_error_limit.out | 14 +- test_regress/t/t_property_nfa_error_limit.py | 2 +- test_regress/t/t_property_nfa_error_limit.v | 7 +- test_regress/t/t_property_nfa_msgs_unsup.out | 283 +- test_regress/t/t_property_nfa_msgs_unsup.py | 1 - test_regress/t/t_property_nfa_msgs_unsup.v | 160 +- test_regress/t/t_property_nfa_reactive_nba.py | 24 - test_regress/t/t_property_nfa_reactive_nba.v | 204 - ...property_nfa_stop_error_limit_crossing.out | 1 + .../t/t_property_nfa_sync_abort_counts.py | 18 - .../t/t_property_nfa_sync_abort_counts.v | 75 - .../t/t_property_nfa_throughout_count.v | 8 +- test_regress/t/t_property_s_until_nfa.v | 12 +- test_regress/t/t_property_sexpr.out | 74 + test_regress/t/t_property_sexpr.v | 4 +- test_regress/t/t_sequence_bool_ops.v | 64 +- test_regress/t/t_sequence_sexpr_throughout.v | 5 +- test_regress/t/t_sequence_within.v | 18 +- 56 files changed, 1224 insertions(+), 3780 deletions(-) delete mode 100755 test_regress/t/t_property_nfa_abort_counts.py delete mode 100644 test_regress/t/t_property_nfa_abort_counts.v delete mode 100755 test_regress/t/t_property_nfa_bbox_unsup.py delete mode 100644 test_regress/t/t_property_nfa_bbox_unsup.v create mode 100644 test_regress/t/t_property_nfa_edge_shapes.out delete mode 100755 test_regress/t/t_property_nfa_reactive_nba.py delete mode 100644 test_regress/t/t_property_nfa_reactive_nba.v delete mode 100755 test_regress/t/t_property_nfa_sync_abort_counts.py delete mode 100644 test_regress/t/t_property_nfa_sync_abort_counts.v diff --git a/Changes b/Changes index c03db891e..53896a42c 100644 --- a/Changes +++ b/Changes @@ -99,7 +99,6 @@ Verilator 5.051 devel * Fix use-after-free in V3LinkDotIfaceCapture (#7943). [Nick Brereton] * Fix sampled-value clocks in repeated assertions (#7944). [Artur Bieniek, Antmicro Ltd.] * Fix queued $finish/$stop request thread ordering (#7946 prep) (#7952). [Yilou Wang] -* Fix NFA assertion scheduling to fix outcome counts (#7946). [Yilou Wang] * Fix DFG nested shifts with overflowing shift amount (#7955) (#7977). * Fix timing controls in interface tasks called via virtual interface (#7959). [Yilou Wang] * Fix lambda parameter types in queue min and max (#7962). [Bartosz Skorowski] diff --git a/src/V3Assert.cpp b/src/V3Assert.cpp index e60aefa83..acddfdfb7 100644 --- a/src/V3Assert.cpp +++ b/src/V3Assert.cpp @@ -149,24 +149,6 @@ void V3AssertCommon::collectDefaultDisable(AstNetlist* nodep) { { DefaultDisablePropagateVisitor{nodep}; } } -AstNode* V3AssertCommon::repeatLoop(FileLine* flp, AstVar* counterp, AstNodeExpr* countp, - AstNode* actionp) { - AstAssign* const initp - = new AstAssign{flp, new AstVarRef{flp, counterp, VAccess::WRITE}, countp}; - AstLoop* const loopp = new AstLoop{flp}; - loopp->addStmtsp( - new AstLoopTest{flp, loopp, - new AstGt{flp, new AstVarRef{flp, counterp, VAccess::READ}, - new AstConst{flp, AstConst::WidthedValue{}, 32, 0}}}); - loopp->addStmtsp(actionp); - AstSub* const decrementp = new AstSub{flp, new AstVarRef{flp, counterp, VAccess::READ}, - new AstConst{flp, AstConst::WidthedValue{}, 32, 1}}; - decrementp->dtypeFrom(counterp); - loopp->addStmtsp(new AstAssign{flp, new AstVarRef{flp, counterp, VAccess::WRITE}, decrementp}); - AstNode::addNext(initp, loopp); - return initp; -} - void V3AssertCommon::lowerSequenceEvents(AstNetlist* nodep) { { SeqEventLowerVisitor{nodep}; } V3Global::dumpCheckGlobalTree("assertseqevent", 0, dumpTreeEitherLevel() >= 3); @@ -342,8 +324,6 @@ class AssertVisitor final : public VNVisitor { VDouble0 m_statLiftedCaseExprs; // Count of purified case expressions AstNodeFTask* m_ftaskp = nullptr; // Current function/task V3UniqueNames m_caseTempNames{"__VCase"}; - V3UniqueNames m_actionCountNames{"__VassertActionCount"}; // Action repeat count temps - bool m_inReactiveAssertionAction = false; // Action will execute after NBA // Maps from (expression, senTree) to the AstAlways that computes its delayed values. std::unordered_map, std::unordered_map, AstAlways*>> m_modExpr2Sen2DelayedAlwaysp; @@ -449,16 +429,6 @@ class AssertVisitor final : public VNVisitor { AstSampled* newSampledExpr(AstNodeExpr* nodep) { return new AstSampled{nodep->fileline(), nodep, nodep->dtypep(), true}; } - - AstNode* repeatAction(FileLine* flp, AstNodeExpr* countp, AstNode* actionp) { - AstNodeDType* const u32p = m_modp->findBasicDType(VBasicDTypeKwd::UINT32); - AstVar* const counterp - = new AstVar{flp, VVarType::BLOCKTEMP, m_actionCountNames.get(""), u32p}; - counterp->lifetime(VLifetime::AUTOMATIC_EXPLICIT); - AstBegin* const beginp = new AstBegin{flp, "", counterp, true}; - beginp->addStmtsp(V3AssertCommon::repeatLoop(flp, counterp, countp, actionp)); - return beginp; - } AstVarRef* newMonitorNumVarRefp(const AstNode* nodep, VAccess access) { if (!m_monitorNumVarp) { m_monitorNumVarp = new AstVar{nodep->fileline(), VVarType::MODULETEMP, "__VmonitorNum", @@ -662,31 +632,12 @@ class AssertVisitor final : public VNVisitor { return varpr; } - AstNode* wrapAssertionInSensitivity(AstNodeCoverOrAssert* nodep, AstSenTree* sentreep, - AstNode* bodysp) { - if (!sentreep) return bodysp; - FileLine* const flp = nodep->fileline(); - if (nodep->nfaLowered()) { - // Impure: a $finish in an earlier same-slot region must be seen live - AstNodeExpr* const notFinishp - = new AstLogNot{flp, new AstCExpr{flp, - "(vlSymsp->_vm_contextp__->finishPending()" - " || vlSymsp->_vm_contextp__->gotFinish())", - 1}}; - bodysp = new AstIf{flp, notFinishp, bodysp}; - return new AstAlwaysReactive{flp, sentreep, bodysp}; - } - return new AstAlways{flp, VAlwaysKwd::ALWAYS, sentreep, bodysp}; - } - void visitAssertionIterate(AstNodeCoverOrAssert* nodep, AstNode* failsp) { if (m_beginp && nodep->name() == "") nodep->name(m_beginp->name()); { AssertDeFutureVisitor{nodep->propp(), m_modp, m_modPastNum++}; } iterateAndNextNull(nodep->sentreep()); - VL_RESTORER(m_inReactiveAssertionAction); - m_inReactiveAssertionAction = nodep->nfaLowered(); if (AstAssert* const assertp = VN_CAST(nodep, Assert)) { iterateAndNextNull(assertp->failsp()); } else if (AstAssertIntrinsic* const assertp = VN_CAST(nodep, AssertIntrinsic)) { @@ -740,8 +691,6 @@ class AssertVisitor final : public VNVisitor { // V3Coverage assigned us a bucket to increment. AstCoverInc* const covincp = VN_AS(coverp->coverincsp(), CoverInc); UASSERT_OBJ(covincp, coverp, "Missing AstCoverInc under assertion"); - AstNodeExpr* const multiplicityp = covincp->multiplicityp(); - if (multiplicityp) multiplicityp->unlinkFrBack(); covincp->unlinkFrBackWithNext(); // next() might have AstAssign for trace if (message != "") covincp->declp()->comment(message); if (passsp) { @@ -752,7 +701,6 @@ class AssertVisitor final : public VNVisitor { } else { passsp = covincp; } - if (multiplicityp) passsp = repeatAction(nodep->fileline(), multiplicityp, passsp); } } else if (VN_IS(nodep, Assert) || VN_IS(nodep, AssertIntrinsic)) { if (nodep->immediate()) { @@ -822,7 +770,7 @@ class AssertVisitor final : public VNVisitor { if (disablep) bodysp = new AstIf{flp, new AstLogNot{flp, disablep}, bodysp}; // Add assertOn check last, for better combining if (!seqEvent) bodysp = newIfAssertOn(bodysp, nodep->directive(), nodep->userType()); - bodysp = wrapAssertionInSensitivity(nodep, sentreep, bodysp); + if (sentreep) bodysp = new AstAlways{flp, VAlwaysKwd::ALWAYS, sentreep, bodysp}; if (passsp && !passsp->backp()) VL_DO_DANGLING(pushDeletep(passsp), passsp); if (failsp && !failsp->backp()) VL_DO_DANGLING(pushDeletep(failsp), failsp); @@ -1151,11 +1099,7 @@ class AssertVisitor final : public VNVisitor { condp->dtypeFrom(onTickp); inp = condp; } else { - // Post-NBA readers use the pipeline's next stage after this tick's shift. - const bool procedureRunsPostNba = VN_IS(m_procedurep, AlwaysObserved); - const uint32_t pipelineTicks - = ticks + (procedureRunsPostNba || m_inReactiveAssertionAction ? 1 : 0); - inp = getPastValue(exprp, initp, senTreep, pipelineTicks); + inp = getPastValue(exprp, initp, senTreep, ticks); } nodep->replaceWith(inp); VL_DO_DANGLING(pushDeletep(nodep), nodep); diff --git a/src/V3Assert.h b/src/V3Assert.h index 8cafdc1d6..3bda2528e 100644 --- a/src/V3Assert.h +++ b/src/V3Assert.h @@ -27,9 +27,6 @@ class V3AssertCommon final { public: static void collectDefaultDisable(AstNetlist* nodep) VL_MT_DISABLED; - // counter = count; while (counter > 0) { action; counter--; } - static AstNode* repeatLoop(FileLine* flp, AstVar* counterp, AstNodeExpr* countp, - AstNode* actionp) VL_MT_DISABLED; static void lowerSequenceEvents(AstNetlist* nodep) VL_MT_DISABLED; }; diff --git a/src/V3AssertNfa.cpp b/src/V3AssertNfa.cpp index 65b637ad4..c7cf50804 100644 --- a/src/V3AssertNfa.cpp +++ b/src/V3AssertNfa.cpp @@ -16,12 +16,10 @@ // V3AssertNfa's Transformations: // // - Convert multi-cycle SVA sequences/properties into NFA graphs. -// - Lower each graph into one AlwaysObserved process: latch the Preponed -// samples, derive the verdict, commit the registered NFA state. -// - Materialize per-attempt pass/fail/match, abort, and strong pending -// counts (a state bit per start depth, a shift ring per ranged window); -// V3Assert runs the actions in Reactive once per counted attempt. -// - Reject shapes whose overlapping attempts cannot keep their identity. +// - Attach inherited assertion clocks before moving sampled-value functions. +// - Emit module-level state registers driven by AstAlways blocks. +// - 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. @@ -39,16 +37,12 @@ #include "V3Task.h" #include "V3UniqueNames.h" -#include -#include +#include #include #include VL_DEFINE_DEBUG_FUNCTIONS; -// Fixed-trace conjunction and strong-pending expansion cap (sites / ring slots) -static constexpr uint64_t FIXED_TRACE_SITE_LIMIT = 1024; - //###################################################################### // NFA Graph Data Structures (V3Graph-derived per upstream convention) @@ -58,12 +52,11 @@ class SvaStateVertex; // Per-vertex algorithm data, stored via V3GraphVertex::userp() during lowering struct SvaVertexData final { - AstVar* stateVarp = nullptr; // Live state register for this vertex - AstVar* evalStateVarp = nullptr; // Old state used for the current verdict - AstVar* delayRingVarp = nullptr; // Live bitset ring buffer - AstVar* evalDelayRingVarp = nullptr; // Old ring used for the current verdict - AstVar* delayRingIdxVarp = nullptr; // Next live slot written in the ring - AstVar* evalDelayRingIdxVarp = nullptr; // Old ring index used for the verdict + AstVar* stateVarp = nullptr; // NBA state register for this vertex + AstVar* delayRingVarp = nullptr; // Bitset ring buffer + AstVar* delayRingIdxVarp = nullptr; // Next slot written in delayRingVarp + AstVar* delayRingLiveCountVarp = nullptr; // Number of set bits in delayRingVarp + 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 @@ -91,10 +84,8 @@ public: AstNodeExpr* m_abortRejectp = nullptr; // Liveness terminal (IEEE weak semantics): reject must not fire from this source bool m_isUnbounded = false; - // Same-end sequence intersect combiner; IEEE 1800-2023 16.9.6 + // Temporal sequence AND combiner; IEEE 1800-2023 16.9.5 bool m_isAndCombiner = false; - // Temporal 'and' permits different end cycles and therefore needs done latches. - bool m_andNeedsDoneLatches = false; SvaStateVertex* m_andLhsTermp = nullptr; // LHS sub-NFA terminal vertex SvaStateVertex* m_andRhsTermp = nullptr; // RHS sub-NFA terminal vertex AstNodeExpr* m_andLhsCondp = nullptr; // OWNED; LHS final condition (may be nullptr) @@ -105,7 +96,8 @@ public: // end-of-simulation the universal-quantifier window never completed, which is // a liveness failure (IEEE 1800-2023 16.12.11 strong semantics). bool m_strongPending = false; - int m_strongPendingGroup = -1; // One group per lexical s_always operator + // Ring introduced for the checked portion of a bounded strong s_always window. + bool m_strongAlwaysRing = false; // CONSTRUCTORS explicit SvaStateVertex(V3Graph* graphp) @@ -181,8 +173,6 @@ public: V3Graph m_graph; // Owns all vertices and edges SvaStateVertex* m_startVertexp = nullptr; // Trigger/start vertex SvaStateVertex* m_matchVertexp = nullptr; // Sequence-match terminal vertex - bool m_hasOrMerge = false; // At least one temporal/property OR was lowered - bool m_hasAndCombiner = false; // At least one same-end intersect combiner was lowered // Create a new state vertex SvaStateVertex* createStateVertex() { return new SvaStateVertex{&m_graph}; } @@ -238,21 +228,17 @@ static bool hasPropertyControlConjunction(const AstNodeExpr* nodep) { return nodep->exists([](const AstSAnd* andp) { return andp->propertyControl(); }); } -// A peeled top-level abort expression remains owned by its source AstAbortOn. -struct AbortSpec final { - VAbortKind kind; // Accept/reject and sync/async flavor - AstNodeExpr* condp; // Abort condition (owned by nodep) - AstAbortOn* nodep; // Source node, deleted after lowering -}; +static AstConst* newTypedConstp(FileLine* const flp, const AstNodeDType* const dtypep, + const uint32_t value) { + AstConst* const constp = new AstConst{flp, AstConst::DTyped{}, dtypep}; + constp->num().setLong(value); + return constp; +} static AstNodeExpr* sampled(AstNodeExpr* exprp) { return new AstSampled{exprp->fileline(), exprp, exprp->dtypep(), true}; } -static bool containsMultiCycleSva(const AstNodeExpr* nodep) { - return nodep->exists([](const AstNodeExpr* ep) { return ep->isMultiCycleSva(); }); -} - static string assertCtlGetCall(const char* query, VAssertType type, VAssertDirectiveType directiveType) { return "vlSymsp->_vm_contextp__->assertCtlGet(VerilatedAssertCtlQuery::"s + query + ", "s @@ -265,16 +251,17 @@ static const char* assertPassOnQuery(bool vacuous) { return queries[vacuous]; } -// Observed captures impure assertion-control queries once; action gates read them live. static AstNodeExpr* assertOnCond(FileLine* flp, VAssertType type, VAssertDirectiveType directiveType) { if (!v3Global.opt.assertOn()) { return new AstConst{flp, AstConst::BitFalse{}}; } - return new AstCExpr{flp, assertCtlGetCall("ASSERT_CTL_ON", type, directiveType), 1}; + return new AstCExpr{flp, AstCExpr::Pure{}, + assertCtlGetCall("ASSERT_CTL_ON", type, directiveType), 1}; } static AstNodeExpr* assertKillGet(FileLine* flp, VAssertType type, VAssertDirectiveType directiveType) { - return new AstCExpr{flp, assertCtlGetCall("ASSERT_CTL_KILL", type, directiveType), 32}; + return new AstCExpr{flp, AstCExpr::Pure{}, + assertCtlGetCall("ASSERT_CTL_KILL", type, directiveType), 32}; } static string assertActionControlPrefix(VAssertDirectiveType directiveType) { @@ -289,7 +276,7 @@ static string assertActionControlPrefix(VAssertDirectiveType directiveType) { static AstNodeExpr* assertPassOnCond(FileLine* flp, VAssertType type, VAssertDirectiveType directiveType, bool vacuous) { - return new AstCExpr{flp, + return new AstCExpr{flp, AstCExpr::Pure{}, assertActionControlPrefix(directiveType) + assertCtlGetCall(assertPassOnQuery(vacuous), type, directiveType) + "))"s, @@ -298,7 +285,7 @@ static AstNodeExpr* assertPassOnCond(FileLine* flp, VAssertType type, static AstNodeExpr* assertFailOnCond(FileLine* flp, VAssertType type, VAssertDirectiveType directiveType) { - return new AstCExpr{flp, + return new AstCExpr{flp, AstCExpr::Pure{}, assertActionControlPrefix(directiveType) + assertCtlGetCall("ASSERT_CTL_FAIL_ON", type, directiveType) + "))"s, 1}; @@ -314,6 +301,16 @@ static AstIf* newPassOnIf(FileLine* flp, AstNodeExpr* firep, AstNode* bodyp, VAs return ifp; } +static AstNodeStmt* newIfAssertFailOn(AstNode* bodyp, VAssertDirectiveType directiveType, + VAssertType type) { + FileLine* const flp = bodyp->fileline(); + AstNodeExpr* const condp = assertFailOnCond(flp, type, directiveType); + AstIf* const ifp = new AstIf{flp, condp, bodyp}; + ifp->isBoundsCheck(true); + ifp->user1(true); + return ifp; +} + //###################################################################### // NFA Builder @@ -327,25 +324,11 @@ class SvaNfaBuilder final { std::vector m_outerAbortStack; bool m_inUnboundedScope = false; // Sticky: nodes created after inherit liveness bool m_markStrongPending = false; // Mark new vertices as strong s_always in-window - int m_strongPendingGroup = -1; // Group currently being built, or -1 - int m_nextStrongPendingGroup = 0; // Unique group for each strong s_always bool m_isCover = false; // Cover directives do not fail at end-of-simulation // IEEE 1800-2023 16.14.3 cover sequence: each end-of-match fires the action, // not just the first. Builder builds parallel-branch (no first-match-wins) // topology when true. Default false preserves cover_property semantics. bool m_isCoverSeq = false; - // Assertions and negated covers need exact per-attempt reject outcomes. - bool m_needsRejectVerdict = true; - - bool mayEmitLocalReject(bool isTopLevelStep) const { - return isTopLevelStep && m_needsRejectVerdict && !m_inUnboundedScope; - } - - static void cleanupProbeResult(const BuildResult& result) { - if (result.finalCondp && !result.finalCondp->backp()) { - VL_DO_DANGLING(result.finalCondp->deleteTree(), result.finalCondp); - } - } // Unsupported endpoint topology must reject, not ignore, or the wait hangs bool m_isSeqEvent = false; @@ -389,7 +372,19 @@ class SvaNfaBuilder final { return val; } - // Return a fixed clock-tick length, or -1 for variable/unbounded operands. + // Static fixed-length analysis: clock ticks from entry to terminal, or -1. + // Used by SIntersect to verify IEEE 1800-2023 16.9.6 equal-length precondition. + // + // Supported: + // - Boolean leaf (default) -> 0 + // - AstSExpr with fixed cycle delay -> pre + N + body + // - AstSExpr with range delay M==N -> pre + N + body + // - AstSThroughout -> length of rhsp (the seq) + // Unsupported (returns -1): + // - Range delays with M != N -> variable + // - Unbounded waits ##[M:$] -> infinite/variable + // - ConsRep / SGotoRep / SAnd / SOr -> defer (rare in intersect) + // - SIntersect nested in SIntersect -> defer static int fixedLength(AstNodeExpr* nodep) { if (AstSExpr* const sexprp = VN_CAST(nodep, SExpr)) { AstDelay* const delayp = VN_CAST(sexprp->delayp(), Delay); @@ -416,24 +411,6 @@ class SvaNfaBuilder final { if (AstSThroughout* const throughp = VN_CAST(nodep, SThroughout)) { return fixedLength(throughp->rhsp()); } - if (AstPropAlways* const alwaysp = VN_CAST(nodep, PropAlways)) { - if (VN_IS(alwaysp->hiBoundp(), Unbounded)) return -1; - return static_cast(getConstUInt(alwaysp->hiBoundp())); - } - if (AstAbortOn* const abortp = VN_CAST(nodep, AbortOn)) - return fixedLength(abortp->propp()); - if (AstSConsRep* const repp = VN_CAST(nodep, SConsRep)) { - if (repp->unbounded() || repp->exprp()->isMultiCycleSva()) return -1; - const unsigned minN = getConstUInt(repp->countp()); - if (repp->maxCountp() && getConstUInt(repp->maxCountp()) != minN) return -1; - return minN ? static_cast(minN - 1) : 0; - } - if (AstSAnd* const andp = VN_CAST(nodep, SAnd)) { - const int lhsLen = fixedLength(andp->lhsp()); - const int rhsLen = fixedLength(andp->rhsp()); - if (lhsLen < 0 || rhsLen < 0) return -1; - return std::max(lhsLen, rhsLen); - } if (AstSOr* const orp = VN_CAST(nodep, SOr)) { // Alternatives must share one end cycle; buildSWithin relies on // this to pair the OR with an SIntersect. @@ -453,29 +430,17 @@ class SvaNfaBuilder final { // nested in an intersect arm before fixedLength runs (clock-context // resolution fails). Kept as a guard in case future parser relaxations // permit it. - if (nodep->exists([](const AstNodeExpr* ep) { return ep->isMultiCycleSva(); })) return -1; + if (nodep->isMultiCycleSva()) return -1; // LCOV_EXCL_STOP // Plain boolean expression (no SVA constructs) -- 0 cycles. return 0; } - // Operators that can reject before their fixed endpoint (a later deadline double-rejects). - static bool mayRejectBeforeEnd(AstNodeExpr* nodep) { - return nodep->exists([](const AstSThroughout*) { return true; }) - || nodep->exists([](const AstAbortOn*) { return true; }) - || nodep->exists([](const AstPropAlways* const alwaysp) { - return !VN_IS(alwaysp->propp(), Const); - }); - } - - static bool containsImpureExpr(AstNode* nodep) { - return nodep->exists([](AstNode* const np) { - // $random reports pure but advances RNG state; must not be duplicated. - return VN_IS(np, Rand) || !np->isPure(); - }); - } - - // Return [lo,hi] for one ranged delay, or {-1,-1} otherwise (IEEE 16.9.6). + // Contiguous match-length range [lo,hi] for an operand whose length varies + // from AT MOST ONE ranged cycle delay; {-1,-1} otherwise. Drives the + // variable-length `intersect` lowering (IEEE 1800-2023 16.9.6): more than + // one ranged delay would make the per-length realization ambiguous, so it + // is reported unsupported rather than mis-paired. static std::pair lengthRange(AstNodeExpr* nodep) { if (AstSExpr* const sexprp = VN_CAST(nodep, SExpr)) { AstDelay* const delayp = VN_CAST(sexprp->delayp(), Delay); @@ -567,10 +532,7 @@ class SvaNfaBuilder final { vtxp->m_throughoutConds.push_back(cp->cloneTreePure(false)); } if (m_inUnboundedScope) vtxp->m_isUnbounded = true; - if (m_markStrongPending) { - vtxp->m_strongPending = true; - vtxp->m_strongPendingGroup = m_strongPendingGroup; - } + if (m_markStrongPending) vtxp->m_strongPending = true; return vtxp; } @@ -748,14 +710,11 @@ class SvaNfaBuilder final { SvaStateVertex* const condVtxp = scopedCreateVertex(); SvaTransEdge* const edgep = guardedLink( pre.termVertexp, condVtxp, sampled(pre.finalCondp->cloneTreePure(false)), flp); - if (mayEmitLocalReject(isTopLevelStep)) { - UASSERT_OBJ( - !pre.termVertexp->m_isUnbounded, preExprp, - "Deferred boolean after an unbounded terminal at a top-level step"); + if (isTopLevelStep && !pre.termVertexp->m_isUnbounded && !m_inUnboundedScope) { // Do not mark liveness sources: first boolean check is deferred. edgep->m_rejectOnFail = true; } - cleanupProbeResult(pre); + freeUnlinkedCondp(pre.finalCondp); currentp = condVtxp; } else { currentp = pre.termVertexp; @@ -765,7 +724,7 @@ class SvaNfaBuilder final { // Handle delay std::vector rangeMidSources; RangeDelayRejectInfo rangeRejectInfo; - const bool addRangeReject = mayEmitLocalReject(isTopLevelStep); + const bool addRangeReject = isTopLevelStep && !m_inUnboundedScope; if (delayp->isRangeDelay()) { bool errorEmitted = false; if (!applyRangeDelay(delayp, sexprp->exprp(), currentp, rangeMidSources, flp, @@ -830,7 +789,9 @@ class SvaNfaBuilder final { SvaStateVertex* const condVtxp = scopedCreateVertex(); SvaTransEdge* const linkp = guardedLink(currentp, condVtxp, sampledRefOrClone(hoistVarp, exprp, flp), flp); - if (mayEmitLocalReject(isTopLevelStep) && i == 0) linkp->m_rejectOnFail = true; + // Only an outermost required step rejects the property. Its first check is explicit, + // later required checks reject through the ring's throughout guard. + if (isTopLevelStep && i == 0) linkp->m_rejectOnFail = true; currentp = condVtxp; } // After minN: currentp is the first valid end-of-match position for [*m:n]. @@ -923,7 +884,7 @@ class SvaNfaBuilder final { sinkp->m_isRejectSink = true; SvaTransEdge* const rejEdgep = guardedLink(livep, sinkp, sampled(propp->cloneTreePure(false)), flp); - if (mayEmitLocalReject(isTopLevelStep)) rejEdgep->m_rejectOnFail = true; + if (isTopLevelStep) rejEdgep->m_rejectOnFail = true; return {livep, nullptr, {}}; } const unsigned hi = getConstUInt(nodep->hiBoundp()); @@ -934,19 +895,18 @@ class SvaNfaBuilder final { // not flagged; an attempt whose final tick coincides with $finish is still // flagged, matching the strong reference. Weak always[m:n] is not marked. VL_RESTORER(m_markStrongPending); - VL_RESTORER(m_strongPendingGroup); m_markStrongPending = nodep->isStrong(); - m_strongPendingGroup = nodep->isStrong() ? m_nextStrongPendingGroup++ : -1; // Check the first in-window tick, then reuse a guarded fixed-delay ring // for the remaining ticks instead of creating one state per cycle. SvaStateVertex* currentp = addDelayChain(entryVtxp, lo, flp); SvaStateVertex* const checkp = scopedCreateVertex(); SvaTransEdge* const linkp = guardedLink(currentp, checkp, sampled(propp->cloneTreePure(false)), flp); - if (mayEmitLocalReject(isTopLevelStep)) linkp->m_rejectOnFail = true; + if (isTopLevelStep) linkp->m_rejectOnFail = true; currentp = checkp; m_temporalGuardStack.push_back(propp); currentp = addDelayChain(currentp, hi - lo, flp); + if (nodep->isStrong() && currentp->m_delayRingSize) currentp->m_strongAlwaysRing = true; m_temporalGuardStack.pop_back(); return {currentp, propp, {}}; } @@ -1008,130 +968,55 @@ class SvaNfaBuilder final { return {mergeVtxp, nullptr, {}}; } - void linkBranchSuccess(const BuildResult& branch, SvaStateVertex* sourcep, - SvaStateVertex* mergeVtxp, FileLine* flp, bool rejectOnMiss, - bool isTopLevelStep) { - AstNodeExpr* const condp - = branch.finalCondp ? sampled(branch.finalCondp->cloneTreePure(false)) : nullptr; - SvaTransEdge* const edgep = guardedLink(sourcep, mergeVtxp, condp, flp); - if (rejectOnMiss && condp && mayEmitLocalReject(isTopLevelStep)) { - edgep->m_rejectOnFail = true; - } - } - - void linkOrBranch(const BuildResult& branch, SvaStateVertex* mergeVtxp, FileLine* flp) { - if (branch.finalCondp) { - guardedLink(branch.termVertexp, mergeVtxp, - sampled(branch.finalCondp->cloneTreePure(false)), flp); - } else { - guardedLink(branch.termVertexp, mergeVtxp, flp); - } - } - - // Constant truth of an operand: 1 true, 0 false, -1 not a two-state constant - static int constTruth(AstNodeExpr* exprp) { - bool strong = false; - if (const AstPropAlways* const alwaysp = VN_CAST(exprp, PropAlways)) { - strong = alwaysp->isStrong(); - exprp = alwaysp->propp(); - } - const AstConst* const constp = VN_CAST(exprp, Const); - if (!constp || constp->num().isFourState()) return -1; - if (constp->num().isEqZero()) return 0; - return strong ? -1 : 1; + // 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, bool isTopLevelStep) { - const int lhsConst = constTruth(lhsp); - const int rhsConst = constTruth(rhsp); - if (m_isSeqEvent && (containsMultiCycleSva(lhsp) || containsMultiCycleSva(rhsp))) { - warnEndpointUnsupported(flp, "a sequence operand of 'or'"); - return BuildResult::failWithError(); - } - // Cover sequence must count a temporal sibling's later ends; do not fold to true. - if (m_isCoverSeq - && ((lhsConst == 1 && containsMultiCycleSva(rhsp)) - || (rhsConst == 1 && containsMultiCycleSva(lhsp)))) { - flp->v3warn(COVERIGN, - "Ignoring unsupported: cover sequence with a sequence operand of 'or'"); - return BuildResult::failWithError(); - } - if (lhsConst == 1) return buildExpr(lhsp, entryVtxp, isTopLevelStep); - if (rhsConst == 1) return buildExpr(rhsp, entryVtxp, isTopLevelStep); - if (lhsConst == 0) return buildExpr(rhsp, entryVtxp, isTopLevelStep); - if (rhsConst == 0) return buildExpr(lhsp, entryVtxp, isTopLevelStep); - - const int lhsLen = fixedLength(lhsp); - const int rhsLen = fixedLength(rhsp); - const bool sameFixedEnd = lhsLen >= 0 && lhsLen == rhsLen; - if (!sameFixedEnd) { - // Diagnose an unsupported child before rejecting the OR (better source location). - const BuildResult lhs = buildExpr(lhsp, entryVtxp); - if (!lhs.valid() && lhs.errorEmitted) return BuildResult::failWithError(); - const BuildResult rhs = buildExpr(rhsp, entryVtxp); - if (!rhs.valid() && rhs.errorEmitted) { - cleanupProbeResult(lhs); - return BuildResult::failWithError(); - } - cleanupProbeResult(lhs); - cleanupProbeResult(rhs); - flp->v3warn(E_UNSUPPORTED, - "Unsupported: unequal/variable-end temporal 'or' cannot preserve " - "overlapping assertion attempt identity"); - return BuildResult::failWithError(); - } - // A side-effecting operand would be evaluated twice (state + count channel); reject it. - if (containsImpureExpr(lhsp) || containsImpureExpr(rhsp)) { - flp->v3warn(E_UNSUPPORTED, - "Unsupported: impure expression in a temporal 'or' composite"); - return BuildResult::failWithError(); - } - if (mayEmitLocalReject(isTopLevelStep) - && (mayRejectBeforeEnd(lhsp) || mayRejectBeforeEnd(rhsp))) { - flp->v3warn(E_UNSUPPORTED, - "Unsupported: temporal 'or' endpoint deadline after an operand that can " - "reject earlier"); - return BuildResult::failWithError(); - } - m_graph.m_hasOrMerge = true; + FileLine* flp) { const BuildResult lhs = buildExpr(lhsp, entryVtxp); const BuildResult rhs = buildExpr(rhsp, entryVtxp); - if (!lhs.valid() || !rhs.valid()) { - cleanupProbeResult(lhs); - cleanupProbeResult(rhs); + 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); - } - // Reject cover-seq 'or' operands whose earlier endpoints bypass this merge. + } // LCOV_EXCL_STOP + // IEEE 1800-2023 16.14.3: a cover sequence counts every end-of-match. A + // sequence operand of 'or' can end more than once, but only its final + // end reaches the merge vertex below, so reject sequence operands rather + // than under-count. Plain boolean disjunction has one end per cycle and + // is handled by the OR-fold. if (m_isCoverSeq && (lhs.termVertexp != entryVtxp || rhs.termVertexp != entryVtxp)) { - cleanupProbeResult(lhs); - cleanupProbeResult(rhs); warnEndpointUnsupported(flp, "a sequence operand of 'or'"); + freeUnlinkedCondp(lhs.finalCondp); + freeUnlinkedCondp(rhs.finalCondp); return BuildResult::failWithError(); } SvaStateVertex* const mergeVtxp = scopedCreateVertex(); - linkOrBranch(lhs, mergeVtxp, flp); - linkOrBranch(rhs, mergeVtxp, flp); - cleanupProbeResult(lhs); - cleanupProbeResult(rhs); - - // One endpoint verdict: reject once only if neither branch reached the merge. - if (mayEmitLocalReject(isTopLevelStep)) { - SvaStateVertex* const deadlineVtxp = addDelayChain(entryVtxp, lhsLen, flp); - SvaStateVertex* const sinkVtxp = m_graph.createStateVertex(); - sinkVtxp->m_isRejectSink = true; - SvaTransEdge* const rejectp = m_graph.addLink(deadlineVtxp, sinkVtxp); - rejectp->m_rejectOnFail = true; - rejectp->m_condVtxp = mergeVtxp; + if (lhs.finalCondp) { + guardedLink(lhs.termVertexp, mergeVtxp, sampled(lhs.finalCondp->cloneTreePure(false)), + flp); + } else { + guardedLink(lhs.termVertexp, mergeVtxp, flp); } - + if (rhs.finalCondp) { + guardedLink(rhs.termVertexp, mergeVtxp, sampled(rhs.finalCondp->cloneTreePure(false)), + flp); + } else { + guardedLink(rhs.termVertexp, mergeVtxp, flp); + } + freeUnlinkedCondp(lhs.finalCondp); + freeUnlinkedCondp(rhs.finalCondp); return {mergeVtxp, nullptr, {}}; } - // A done latch retains the first temporal-AND endpoint until its sibling arrives. + // Build done-latch combiner for SAnd/SIntersect (IEEE 1800-2023 16.9.5). BuildResult buildAndCombiner(AstNodeExpr* lhsExprp, AstNodeExpr* rhsExprp, SvaStateVertex* entryVtxp, FileLine* flp) { + // Snapshot-restore scope so LHS liveness does not leak into RHS. const bool savedScope = m_inUnboundedScope; const BuildResult lhs = buildExpr(lhsExprp, entryVtxp); const bool lhsScope = m_inUnboundedScope; @@ -1139,11 +1024,11 @@ class SvaNfaBuilder final { const BuildResult rhs = buildExpr(rhsExprp, entryVtxp); const bool rhsScope = m_inUnboundedScope; m_inUnboundedScope = savedScope || lhsScope || rhsScope; - if (!lhs.valid() || !rhs.valid()) { - cleanupProbeResult(lhs); - cleanupProbeResult(rhs); + 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 // Single-cycle operands: use boolean AND (done-latch would fire across cycles). // If both operands stayed at entry, they must be boolean leaves which @@ -1153,24 +1038,22 @@ class SvaNfaBuilder final { "Single-cycle SAnd operands must have finalCondp"); AstNodeExpr* const condp = new AstLogAnd{flp, lhs.finalCondp->cloneTreePure(false), rhs.finalCondp->cloneTreePure(false)}; - cleanupProbeResult(lhs); - cleanupProbeResult(rhs); + 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. if (!lhs.midSources.empty() || !rhs.midSources.empty()) { - cleanupProbeResult(lhs); - cleanupProbeResult(rhs); - UASSERT_OBJ(!m_isSeqEvent, flp, "Seq events reject variable 'and' operands first"); 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_andNeedsDoneLatches = true; combVtxp->m_andLhsTermp = lhs.termVertexp; combVtxp->m_andRhsTermp = rhs.termVertexp; if (lhs.finalCondp) combVtxp->m_andLhsCondp = lhs.finalCondp->cloneTreePure(false); @@ -1178,11 +1061,19 @@ class SvaNfaBuilder final { if (lhs.termVertexp->m_isUnbounded || rhs.termVertexp->m_isUnbounded) { combVtxp->m_isUnbounded = true; } + // Wire terminal-boolean rejects to a dedicated sink so each side can fail + // the AND independently. Skip for liveness or single-cycle operands + // (single-cycle termVertexp == entryVtxp would fire every cycle). if (!combVtxp->m_isUnbounded) { - const bool lhsMultiCycle = lhs.termVertexp != entryVtxp; - const bool rhsMultiCycle = rhs.termVertexp != entryVtxp; - const bool needSink - = (lhs.finalCondp && lhsMultiCycle) || (rhs.finalCondp && rhsMultiCycle); + bool needSink = false; + const bool lhsMultiCycle = (lhs.termVertexp != entryVtxp); + const bool rhsMultiCycle = (rhs.termVertexp != entryVtxp); + if (lhs.finalCondp && lhsMultiCycle && !lhs.termVertexp->m_isUnbounded) { + needSink = true; + } + if (rhs.finalCondp && rhsMultiCycle && !rhs.termVertexp->m_isUnbounded) { + needSink = true; + } if (needSink) { SvaStateVertex* const sinkVtxp = m_graph.createStateVertex(); sinkVtxp->m_isRejectSink = true; @@ -1198,67 +1089,17 @@ class SvaNfaBuilder final { } } } - cleanupProbeResult(lhs); - cleanupProbeResult(rhs); + freeUnlinkedCondp(lhs.finalCondp); + freeUnlinkedCondp(rhs.finalCondp); return {combVtxp, nullptr, {}}; } - // Equal-length intersect operands combine terminal matches without persistent state. - BuildResult buildSameEndIntersectCombiner(AstNodeExpr* lhsExprp, AstNodeExpr* rhsExprp, - SvaStateVertex* entryVtxp, FileLine* flp, - int sameEndLength, bool isTopLevelStep = false) { - UASSERT_OBJ(sameEndLength >= 0, lhsExprp, - "Same-end intersect combiner needs a fixed endpoint"); - if (mayEmitLocalReject(isTopLevelStep) - && (mayRejectBeforeEnd(lhsExprp) || mayRejectBeforeEnd(rhsExprp))) { - flp->v3warn( - E_UNSUPPORTED, - "Unsupported: intersect/within endpoint deadline after an operand that can " - "reject earlier"); - return BuildResult::failWithError(); - } - m_graph.m_hasAndCombiner = true; - // Snapshot-restore scope so LHS liveness does not leak into RHS. - const bool savedScope = m_inUnboundedScope; - const BuildResult lhs = buildExpr(lhsExprp, entryVtxp); - m_inUnboundedScope = savedScope; - const BuildResult rhs = buildExpr(rhsExprp, entryVtxp); - m_inUnboundedScope = savedScope; - if (!lhs.valid() || !rhs.valid()) { - cleanupProbeResult(lhs); - cleanupProbeResult(rhs); - UASSERT_OBJ(lhs.errorEmitted || rhs.errorEmitted, lhsExprp, - "Same-end intersect operand failed without a diagnostic"); - return BuildResult::failWithError(); - } - // Both-boolean operands have fixed length 0 and never route here (conjoined instead). - UASSERT_OBJ(lhs.termVertexp != entryVtxp || rhs.termVertexp != entryVtxp, lhsExprp, - "Intersect combiner requires a non-fixed operand"); - // Fixed-length operands (or-merge, bounded always, throughout of those) have no - // mid-window endpoints. - UASSERT_OBJ(lhs.midSources.empty() && rhs.midSources.empty(), lhsExprp, - "Same-end intersect operands cannot have mid-window sources"); - SvaStateVertex* const combVtxp = scopedCreateVertex(); - combVtxp->m_isAndCombiner = true; - combVtxp->m_andLhsTermp = lhs.termVertexp; - combVtxp->m_andRhsTermp = rhs.termVertexp; - if (lhs.finalCondp) combVtxp->m_andLhsCondp = lhs.finalCondp->cloneTreePure(false); - if (rhs.finalCondp) combVtxp->m_andRhsCondp = rhs.finalCondp->cloneTreePure(false); - cleanupProbeResult(lhs); - cleanupProbeResult(rhs); - // One endpoint verdict: reject once if the same-end combiner did not match. - if (mayEmitLocalReject(isTopLevelStep)) { - SvaStateVertex* const deadlineVtxp = addDelayChain(entryVtxp, sameEndLength, flp); - SvaStateVertex* const sinkVtxp = m_graph.createStateVertex(); - sinkVtxp->m_isRejectSink = true; - SvaTransEdge* const rejectp = m_graph.addLink(deadlineVtxp, sinkVtxp); - rejectp->m_rejectOnFail = true; - rejectp->m_condVtxp = combVtxp; - } - return {combVtxp, nullptr, {}}; - } - - // Lower fixed-length `seq1 within seq2` by aligning each legal seq1 placement. + // Lower `seq1 within seq2` (IEEE 1800-2023 16.9.10) as: + // (OR_{i in 0..slack} 1 ##i seq1 ##(slack-i) 1) intersect seq2 + // Both operands must have fixed length (no ranged cycle delays). + // The OR lives inside a single SIntersect so one AndCombiner done-latch + // serves all offsets; lifting it out would double-count matches where + // multiple offsets accept on the same seq2 end cycle. BuildResult buildSWithin(AstSWithin* nodep, SvaStateVertex* entryVtxp, bool isTopLevelStep = false) { const int innerLen = fixedLength(nodep->lhsp()); @@ -1301,8 +1142,10 @@ class SvaNfaBuilder final { AstNodeExpr* const combinedp = new AstSIntersect{flp, innerOrp, outerClonep}; BuildResult result = buildExpr(combinedp, entryVtxp, isTopLevelStep); VL_DO_DANGLING(combinedp->deleteTree(), combinedp); - // A conjoined boolean intersect returns a freshly-allocated finalCondp with no - // parent; callers clone-and-discard finalCondp, so anchor it in the graph via an edge. + // When both operands are plain booleans, buildAndCombiner returns a + // freshly-allocated AstAnd as finalCondp with no parent. Callers up + // the chain clone-and-discard finalCondp, so leave it parent-less and + // it leaks; anchor it in the graph now via an edge. if (result.valid() && result.finalCondp && !result.finalCondp->backp()) { SvaStateVertex* const wrapVtxp = scopedCreateVertex(); guardedLink(result.termVertexp, wrapVtxp, sampled(result.finalCondp), flp); @@ -1311,64 +1154,9 @@ class SvaNfaBuilder final { return result; } - static bool reserveFixedTraceSites(AstNode* nodep, uint64_t& sites, uint64_t increment) { - if (increment <= FIXED_TRACE_SITE_LIMIT - sites) { - sites += increment; - return true; - } - nodep->v3warn(E_UNSUPPORTED, - "Unsupported: concurrent assertion fixed-trace expansion exceeds " - << FIXED_TRACE_SITE_LIMIT << " sites"); - return false; - } - - // Bound fixed-sequence expansion and reject leaves whose effects would be duplicated. - static bool validateFixedTrace(AstNodeExpr* nodep, uint64_t& sites) { - if (AstSExpr* const sexprp = VN_CAST(nodep, SExpr)) { - if (AstNodeExpr* const prep = sexprp->preExprp()) { - if (!validateFixedTrace(prep, sites)) return false; - } - return validateFixedTrace(sexprp->exprp(), sites); - } - if (AstSConsRep* const repp = VN_CAST(nodep, SConsRep)) { - const unsigned minN = getConstUInt(repp->countp()); - if (containsImpureExpr(repp->exprp())) { - repp->v3warn( - E_UNSUPPORTED, - "Unsupported: impure expression in a flattened consecutive repetition"); - return false; - } - return reserveFixedTraceSites(repp, sites, static_cast(minN)); - } - if (AstSAnd* const andp = VN_CAST(nodep, SAnd)) { - return validateFixedTrace(andp->lhsp(), sites) - && validateFixedTrace(andp->rhsp(), sites); - } - if (AstSThroughout* const throughoutp = VN_CAST(nodep, SThroughout)) { - const int rhsLen = fixedLength(throughoutp->rhsp()); - UASSERT_OBJ(rhsLen >= 0, throughoutp, "Fixed-trace throughout has a fixed body"); - if (containsImpureExpr(throughoutp->lhsp())) { - throughoutp->v3warn( - E_UNSUPPORTED, - "Unsupported: impure guard in a flattened throughout composite"); - return false; - } - if (!reserveFixedTraceSites(throughoutp, sites, static_cast(rhsLen) + 1)) { - return false; - } - return validateFixedTrace(throughoutp->rhsp(), sites); - } - if (nodep->exists([](const AstNodeExpr* ep) { return ep->isMultiCycleSva(); })) - return true; - if (containsImpureExpr(nodep)) { - nodep->v3warn(E_UNSUPPORTED, - "Unsupported: impure expression in a flattened temporal composite"); - return false; - } - return reserveFixedTraceSites(nodep, sites, 1); - } - - // Collect boolean leaf checks of a fixed-length match, keyed by clock offset. + // Collect the boolean leaf checks of a fixed-length sequence keyed by their + // clock offset from the start. Returns false for anything other than nested + // AstSExpr with fixed cycle delays over boolean leaves (e.g. throughout). static bool flattenFixedSeq(AstNodeExpr* nodep, int baseOffset, std::map>& out) { if (AstSExpr* const sexprp = VN_CAST(nodep, SExpr)) { @@ -1385,36 +1173,15 @@ class SvaNfaBuilder final { } return flattenFixedSeq(sexprp->exprp(), baseOffset + preLen + delayCycles, out); } - if (AstSConsRep* const repp = VN_CAST(nodep, SConsRep)) { - UASSERT_OBJ(!repp->unbounded() && !repp->exprp()->isMultiCycleSva(), repp, - "Fixed-trace repetition is a bounded boolean"); - const unsigned minN = getConstUInt(repp->countp()); - for (unsigned i = 0; i < minN; ++i) { - out[baseOffset + static_cast(i)].push_back(repp->exprp()); - } - return true; - } - if (AstSAnd* const andp = VN_CAST(nodep, SAnd)) { - return flattenFixedSeq(andp->lhsp(), baseOffset, out) - && flattenFixedSeq(andp->rhsp(), baseOffset, out); - } - if (AstSThroughout* const throughoutp = VN_CAST(nodep, SThroughout)) { - const int rhsLen = fixedLength(throughoutp->rhsp()); - UASSERT_OBJ(rhsLen >= 0, throughoutp, "Fixed-trace throughout has a fixed body"); - if (!flattenFixedSeq(throughoutp->rhsp(), baseOffset, out)) return false; - // IEEE 16.9.9 covers the start tick, the end tick, and every gap tick between. - for (int offset = 0; offset <= rhsLen; ++offset) { - out[baseOffset + offset].push_back(throughoutp->lhsp()); - } - return true; - } - if (nodep->exists([](const AstNodeExpr* ep) { return ep->isMultiCycleSva(); })) - return false; + if (nodep->isMultiCycleSva()) return false; out[baseOffset].push_back(nodep); return true; } - // Conjoin two fixed sequences into one, AND-ing leaf checks at each offset. + // Conjoin two equal-length fixed sequences into one: at each clock offset + // AND the boolean checks of both operands (IEEE 1800-2023 16.9.6 -- both + // operands match the same window). Returns null if either operand is not a + // plain fixed sequence of boolean leaves. static AstNodeExpr* conjoinFixedSeqs(AstNodeExpr* lhsp, AstNodeExpr* rhsp, FileLine* flp) { std::map> checks; if (!flattenFixedSeq(lhsp, 0, checks) || !flattenFixedSeq(rhsp, 0, checks)) return nullptr; @@ -1455,112 +1222,9 @@ class SvaNfaBuilder final { return resultp; } - static void collectPropertyControlBranches(AstNodeExpr* nodep, - std::vector& branches) { - if (AstSAnd* const andp = VN_CAST(nodep, SAnd)) { - UASSERT_OBJ(andp->propertyControl(), andp, - "Property-control branch tree lost parser provenance"); - collectPropertyControlBranches(andp->lhsp(), branches); - collectPropertyControlBranches(andp->rhsp(), branches); - return; - } - branches.push_back(VN_AS(nodep, Implication)); - } - - // Property-control branches retain independent state, rejects, and failure depths. - BuildResult buildPropertyControlAnd(AstSAnd* nodep, SvaStateVertex* entryVtxp, - bool isTopLevelStep) { - if (m_inUnboundedScope) { - nodep->v3warn(E_UNSUPPORTED, - "Unsupported: property if/case inside a variable-end temporal window"); - return BuildResult::failWithError(); - } - - std::vector branches; - collectPropertyControlBranches(nodep, branches); - const auto impureIt - = std::find_if(branches.begin(), branches.end(), - [](AstImplication* bp) { return containsImpureExpr(bp->lhsp()); }); - if (impureIt != branches.end()) { - (*impureIt)->lhsp()->v3warn( - E_UNSUPPORTED, - "Unsupported: impure property if/case selector cannot be sampled once"); - return BuildResult::failWithError(); - } - - SvaStateVertex* const mergeVtxp = scopedCreateVertex(); - m_graph.m_hasOrMerge = true; - bool errorEmitted = false; - const bool linked - = std::all_of(branches.begin(), branches.end(), [&](AstImplication* const branchp) { - return linkPropertyControlBranch(branchp, entryVtxp, mergeVtxp, isTopLevelStep, - errorEmitted); - }); - if (!linked) return BuildResult::fail(errorEmitted); - return {mergeVtxp, nullptr, {}}; - } - - // Build one if/case branch and link its endpoints into the merge vertex. - bool linkPropertyControlBranch(AstImplication* branchp, SvaStateVertex* entryVtxp, - SvaStateVertex* mergeVtxp, bool isTopLevelStep, - bool& errorEmittedr) { - const bool savedScope = m_inUnboundedScope; - m_inUnboundedScope = false; - BuildResult branch = buildImplicationEdges( - branchp->lhsp(), branchp->rhsp(), entryVtxp, /*isOverlapped=*/true, - /*isFollowedBy=*/false, branchp->lhsp(), branchp->fileline()); - m_inUnboundedScope = savedScope; - if (!branch.valid()) { - errorEmittedr = branch.errorEmitted; - return false; - } - for (SvaStateVertex* const sourcep : branch.midSources) { - linkBranchSuccess(branch, sourcep, mergeVtxp, branchp->fileline(), false, - isTopLevelStep); - } - linkBranchSuccess(branch, branch.termVertexp, mergeVtxp, branchp->fileline(), - !branch.termVertexp->m_isUnbounded, isTopLevelStep); - if (branch.finalCondp && !branch.finalCondp->backp()) branch.finalCondp->deleteTree(); - branch.finalCondp = nullptr; - return true; - } - - BuildResult buildSAnd(AstSAnd* nodep, SvaStateVertex* entryVtxp, bool isTopLevelStep) { - if (nodep->propertyControl()) { - return buildPropertyControlAnd(nodep, entryVtxp, isTopLevelStep); - } - const int lhsLen = fixedLength(nodep->lhsp()); - const int rhsLen = fixedLength(nodep->rhsp()); - const bool hasAbort = nodep->exists([](const AstAbortOn*) { return true; }); - if (lhsLen >= 0 && rhsLen >= 0 && !hasAbort) { - uint64_t traceSites = 0; - if (!validateFixedTrace(nodep->lhsp(), traceSites) - || !validateFixedTrace(nodep->rhsp(), traceSites)) { - return BuildResult::failWithError(); - } - if (AstNodeExpr* const conjp - = conjoinFixedSeqs(nodep->lhsp(), nodep->rhsp(), nodep->fileline())) { - return buildFromLoweringTree(conjp, entryVtxp, isTopLevelStep); - } - nodep->v3warn(E_UNSUPPORTED, - "Unsupported: bounded temporal 'and' operand cannot be represented as a " - "single fixed match trace"); - return BuildResult::failWithError(); - } - if (!m_needsRejectVerdict) { - if (m_isSeqEvent) { - warnEndpointUnsupported(nodep->fileline(), "a variable/unbounded temporal 'and'"); - } else { - nodep->v3warn(E_UNSUPPORTED, - "Unsupported: variable/unbounded temporal 'and' cannot preserve " - "overlapping assertion attempt identity"); - } - return BuildResult::failWithError(); - } - return buildAndCombiner(nodep->lhsp(), nodep->rhsp(), entryVtxp, nodep->fileline()); - } - - // A simple ranged sequence `start ##[m:n] end` (start optional). + // `seq` is a simple ranged sequence `start ##[m:n] end` (start/end boolean, + // start may be absent). Used to collapse a both-variable intersect to one + // ranged delay. struct SimpleRanged final { bool ok = false; AstNodeExpr* startp = nullptr; // may be null (absent start) @@ -1579,7 +1243,10 @@ class SvaNfaBuilder final { return {true, prep, sexprp->exprp()}; } - // Build the NFA for a synthesized lowering tree, cloning finalCondp before freeing it. + // Build the NFA for a synthesized intersect lowering tree, then free it. + // buildExpr returns the terminal condition (finalCondp) by reference into the + // tree; detach a clone so the tree can be freed here. The graph already holds + // clones/hoists of every edge condition, so nothing else dangles. BuildResult buildFromLoweringTree(AstNodeExpr* treep, SvaStateVertex* entryVtxp, bool isTopLevelStep) { BuildResult result = buildExpr(treep, entryVtxp, isTopLevelStep); @@ -1590,7 +1257,11 @@ class SvaNfaBuilder final { return result; } - // No common length: the intersect never matches (legal, 16.9.6) -> constant false. + // Empty common-length intersection -- unequal fixed lengths, or disjoint + // ranged lengths. IEEE 1800-2023 16.9.6 requires both operands to match + // over a window of the same length, so with no common length the intersect + // simply never matches. This is legal (matching nothing), not an error, so + // lower to a constant false rather than rejecting legal code. BuildResult buildNeverMatchIntersect(AstNodeExpr* nodep, SvaStateVertex* entryVtxp, bool isTopLevelStep, const char* reason) { nodep->v3warn(NEVERMATCH, "Sequence can never match because " << reason << "."); @@ -1598,7 +1269,19 @@ class SvaNfaBuilder final { return buildFromLoweringTree(falsep, entryVtxp, isTopLevelStep); } - // Lower supported variable-length intersect forms under IEEE 1800-2023 16.9.6. + // Lower `seq1 intersect seq2` when an operand's match length varies + // (IEEE 1800-2023 16.9.6: both match over one window, equal start and end). + // The common length range is [lo,hi] = intersection of the two operands' + // achievable lengths. The equal-length combiner is avoided -- it mis-handles + // operands with an internal boolean check -- by lowering to plain sequences: + // - lo == hi (one shared length, e.g. one fixed + one ranged operand): + // pin each operand to that length and conjoin them cycle-by-cycle into a + // single fixed sequence. + // - lo < hi with simple `bool ##[m:n] bool` operands: collapse to one + // ranged delay `(start1 & start2) ##[lo:hi] (end1 & end2)`. (An OR of + // per-length branches cannot reject correctly -- a single missed length + // would fail the whole intersect, cf. Lesson 48.) + // - otherwise unsupported (clean error, not the legacy fall-through crash). BuildResult buildVarLenIntersect(AstSIntersect* nodep, SvaStateVertex* entryVtxp, bool isTopLevelStep) { const std::pair lhsRange = lengthRange(nodep->lhsp()); @@ -1617,12 +1300,6 @@ class SvaNfaBuilder final { } FileLine* const flp = nodep->fileline(); if (lo == hi) { - // Pinning a ranged operand to one length supports only plain boolean traces. - if (nodep->exists([](const AstSThroughout*) { return true; })) { - nodep->v3warn(E_UNSUPPORTED, - "Unsupported: intersect operand is not a plain boolean sequence"); - return BuildResult::failWithError(); - } AstNodeExpr* const lp = realizeAtLength(nodep->lhsp(), lo, lhsRange.first); AstNodeExpr* const rp = realizeAtLength(nodep->rhsp(), lo, rhsRange.first); AstNodeExpr* const conjp = conjoinFixedSeqs(lp, rp, flp); @@ -1666,13 +1343,7 @@ class SvaNfaBuilder final { // Mark entryVtxp so "cond false at tick 0" is detected as throughout-drop. entryVtxp->m_throughoutConds.push_back(nodep->lhsp()->cloneTreePure(false)); m_temporalGuardStack.push_back(nodep->lhsp()); - BuildResult result = buildExpr(nodep->rhsp(), entryVtxp, isTopLevelStep); - if (result.valid()) { - // Fold active throughout guards into the boolean terminal tick too. - AstNodeExpr* finalp = result.finalCondp; - if (finalp && finalp->backp()) finalp = finalp->cloneTreePure(false); - result.finalCondp = throughoutCond(finalp, nodep->fileline()); - } + const BuildResult result = buildExpr(nodep->rhsp(), entryVtxp, isTopLevelStep); m_temporalGuardStack.pop_back(); return result; } @@ -1703,21 +1374,20 @@ class SvaNfaBuilder final { << "' in complex property expression"); return BuildResult::failWithError(); } - AstNodeExpr* const lhsBitp = nodep->lhsp(); - AstNodeExpr* const rhsBitp = nodep->rhsp(); - if (containsMultiCycleSva(lhsBitp) || containsMultiCycleSva(rhsBitp)) { + AstNodeExpr* const lhsp = nodep->lhsp(); + AstNodeExpr* const rhsp = nodep->rhsp(); + const auto hasSeq = [](const AstNodeExpr* ep) { + return ep->exists([](const AstNodeExpr* np) { return np->isMultiCycleSva(); }); + }; + if (hasSeq(lhsp) || hasSeq(rhsp)) { nodep->v3warn(E_UNSUPPORTED, "Unsupported: '" << nodep->verilogKwd() << "' in complex property expression"); return BuildResult::failWithError(); } - if (m_inUnboundedScope) { - nodep->v3warn(E_UNSUPPORTED, "Unsupported: '" - << nodep->verilogKwd() - << "' inside a variable-length property window"); - return BuildResult::failWithError(); - } const bool ov = nodep->isOverlapping(); + AstNodeExpr* const lhsBitp = nodep->lhsp(); + AstNodeExpr* const rhsBitp = nodep->rhsp(); // p hoist count: continue, require (ov: 1 use; nov: 1 use). At least 2 uses. AstVar* const pHoistp = tryHoistSampled(lhsBitp, flp, 2); // q hoist count: continue (1) + require nov (1) = 2; ov: continue only (1). @@ -1726,10 +1396,7 @@ class SvaNfaBuilder final { SvaStateVertex* const waitCp = scopedCreateVertex(); SvaStateVertex* const waitRp = scopedCreateVertex(); waitCp->m_isUnbounded = true; - if (nodep->isStrong() && !m_isCover) { - waitRp->m_strongPending = true; - waitRp->m_strongPendingGroup = m_nextStrongPendingGroup++; - } + waitRp->m_strongPending = nodep->isStrong() && !m_isCover; // Entry and back-loop Links carry no condition; throughout-folding still applies. guardedLink(entryVtxp, waitCp, flp); @@ -1752,7 +1419,7 @@ class SvaNfaBuilder final { sampledRefOrClone(qHoistp, rhsBitp, flp)}; } SvaTransEdge* const rejEdgep = m_graph.addLink(waitCp, sinkVtxp, requireCondp); - if (mayEmitLocalReject(isTopLevelStep)) rejEdgep->m_rejectOnFail = true; + rejEdgep->m_rejectOnFail = true; // Accept condition rides via finalCondp; assembleResult $sampled-wraps it. AstNodeExpr* acceptCondp; @@ -1919,14 +1586,12 @@ class SvaNfaBuilder final { public: SvaNfaBuilder(SvaGraph& graph, AstNodeModule* modp, V3UniqueNames& propTempNames, - bool isCoverSeq = false, bool needsRejectVerdict = true, bool isSeqEvent = false, - bool isCover = false) + bool isCoverSeq = false, bool isSeqEvent = false, bool isCover = false) : m_graph{graph} , m_modp{modp} , m_propTempNames{propTempNames} , m_isCover{isCover} , m_isCoverSeq{isCoverSeq} - , m_needsRejectVerdict{needsRejectVerdict} , m_isSeqEvent{isSeqEvent} {} // Reset scope between antecedent and consequent: liveness must not leak. @@ -1937,7 +1602,6 @@ public: m_temporalGuardStack.clear(); } - // Build the NFA for one operator; fixed-length operands conjoin, others combine. BuildResult buildExpr(AstNodeExpr* nodep, SvaStateVertex* entryVtxp, bool isTopLevelStep = false) { if (AstSExpr* const sexprp = VN_CAST(nodep, SExpr)) { @@ -1956,20 +1620,21 @@ public: return buildThroughout(throughoutp, entryVtxp, isTopLevelStep); } if (AstSOr* const orp = VN_CAST(nodep, SOr)) { - return buildOrMerge(orp->lhsp(), orp->rhsp(), entryVtxp, orp->fileline(), - isTopLevelStep); + return buildOrMerge(orp->lhsp(), orp->rhsp(), entryVtxp, orp->fileline()); } if (AstLogOr* const orp = VN_CAST(nodep, LogOr)) { - // A plain logical OR is one sampled boolean, not a temporal merge. - UASSERT_OBJ(!orp->exists([](const AstNodeExpr* ep) { return ep->isMultiCycleSva(); }), - orp, "Grammar forbids temporal '||' operands"); - return {entryVtxp, orp, {}}; + return buildOrMerge(orp->lhsp(), orp->rhsp(), entryVtxp, orp->fileline()); } if (AstSAnd* const andp = VN_CAST(nodep, SAnd)) { - return buildSAnd(andp, entryVtxp, isTopLevelStep); + return buildAndCombiner(andp->lhsp(), andp->rhsp(), entryVtxp, andp->fileline()); } if (AstSIntersect* const intp = VN_CAST(nodep, SIntersect)) { - // Conjoin equal-length intersect checks, retaining a non-flattened fallback. + // IEEE 1800-2023 16.9.6: both operands match over one window with + // equal start and end (equal length). Lower to a single sequence + // that conjoins both operands' per-cycle checks -- correct under + // concurrent attempts, where the done-latch combiner conflates the + // two operands' start times and over-accepts. The combiner remains + // only as a fallback for operands that do not flatten. const int lhsLen = fixedLength(intp->lhsp()); const int rhsLen = fixedLength(intp->rhsp()); if (lhsLen >= 0 && rhsLen >= 0) { @@ -1982,8 +1647,7 @@ public: = conjoinFixedSeqs(intp->lhsp(), intp->rhsp(), intp->fileline())) { return buildFromLoweringTree(conjp, entryVtxp, isTopLevelStep); } - return buildSameEndIntersectCombiner(intp->lhsp(), intp->rhsp(), entryVtxp, - intp->fileline(), lhsLen, isTopLevelStep); + return buildAndCombiner(intp->lhsp(), intp->rhsp(), entryVtxp, intp->fileline()); } return buildVarLenIntersect(intp, entryVtxp, isTopLevelStep); } @@ -2003,20 +1667,22 @@ public: if (AstUntil* const untilp = VN_CAST(nodep, Until)) { return buildUntil(untilp, entryVtxp, isTopLevelStep); } - // Leave unsupported temporal operators to V3AssertPre to avoid duplicate diagnostics. - if (nodep->exists([](const AstNodeExpr* ep) { return ep->isMultiCycleSva(); })) { - return BuildResult::fail(); - } // Boolean leaf (including LogAnd): return as finalCond return {entryVtxp, nodep, {}}; } - // Wire antecedent, match/reject links, delay, and body for implication/followed-by. + // Wire an implication / followed-by from `entryVtxp`: builds the antecedent, + // emits the match-link (and for followed-by the reject-sink edge), inserts a + // delay vertex for non-overlapped forms, and builds the body. Used both for + // nested AstImplication in pexpr position and for the top-level assertion + // antecedent -- `errorNodep` anchors the "unsupported sequence antecedent" + // error, which differs between the two call sites. BuildResult buildImplicationEdges(AstNodeExpr* antExprp, AstNodeExpr* bodyExprp, SvaStateVertex* entryVtxp, bool isOverlapped, bool isFollowedBy, AstNode* errorNodep, FileLine* flp) { const BuildResult antResult = buildExpr(antExprp, entryVtxp); if (!antResult.valid()) return antResult; + // Followed-by requires pure-boolean antecedent for non-vacuous-fail at // the attempt-start cycle. IEEE 1800-2023 16.12.9 permits a multi-cycle // sequence LHS, so this is an implementation gap rather than illegal SV. @@ -2024,7 +1690,6 @@ public: errorNodep->v3warn(E_UNSUPPORTED, "Unsupported: sequence expression as antecedent of followed-by" " (#-# / #=#) (IEEE 1800-2023 16.12.9)"); - cleanupProbeResult(antResult); return BuildResult::failWithError(); } UASSERT_OBJ(!isFollowedBy || antResult.finalCondp, errorNodep, @@ -2073,95 +1738,35 @@ public: }; //###################################################################### -// NFA Lowering (Observed evaluation/state update, Reactive action dispatch) +// NFA Lowering (converts NFA graph to synthesizable AstAlways blocks) class SvaNfaLowering final { -public: - // Inputs for lower(). disableExprp ownership transfers to the lowering. - struct LowerRequest final { - AstNodeExpr* triggerExprp = nullptr; // Runtime assertion-on gate - AstSenTree* senTreep = nullptr; // Clock sensitivity tree - AstNodeExpr* matchCondp = nullptr; // Final boolean match condition - AstNodeExpr* disableExprp = nullptr; // Normalized disable iff (consumed) - const std::vector* abortSpecsp = nullptr; // Peeled abort prefix - AstVar* disableCntVarp = nullptr; // Disable posedge epoch counter - AstVar* snapshotVarp = nullptr; // Disable epoch snapshot - bool isCover = false; // Cover directive: count matches, no rejects - bool negated = false; // Property under not(): swap match/reject roles - VAssertType assertType = VAssertType::INTERNAL; // For assertion-control gating - VAssertDirectiveType directiveType = VAssertDirectiveType::INTERNAL; // Likewise - // Requested optional outputs; unset ones stay empty in LowerResult - bool wantPerSrcFail = false; // Per-depth failure sources - // The synthesized default handler only needs a count for true multiplicity. - bool pruneSingleFailSource = false; - bool wantPerSrcMatch = false; // Per-depth match sources - bool wantAbortPassCount = false; // Forced-accept attempt count - bool wantAbortFailCount = false; // Forced-reject attempt count - bool wantStrongPending = false; // End-of-sim pending count - bool wantPerMid = false; // Per-end cover sequence signals - }; - // Outputs of lower(); all expressions are freshly built (caller owns) - struct LowerResult final { - AstNodeExpr* outputExprp = nullptr; // Materialized !reject / match verdict - AstNodeExpr* abortAnyp = nullptr; // Any abort fired this evaluation - AstNodeExpr* disableRefp = nullptr; // Observed disable variable reference - AstNodeExpr* failCountp = nullptr; // Extra dynamically counted failures - AstNodeExpr* matchCountp = nullptr; // Extra range-ring match multiplicity - AstNodeExpr* abortPassCountp = nullptr; // Forced-accept attempt count - AstNodeExpr* abortFailCountp = nullptr; // Forced-reject attempt count - AstNodeExpr* strongPendingCountp = nullptr; // End-of-sim pending attempts - std::vector failAttemptSrcs; // Per-depth failure outcomes - std::vector matchAttemptSrcs; // Per-depth match outcomes - std::vector perMidSrcs; // Per-end cover sequence signals - std::vector perMidCounts; // Ring occupants per perMidSrcs entry, or null - }; - -private: AstNodeModule* const m_modp; // Module to add state vars and always blocks to AstNodeDType* const m_u32DTypep; // Shared unsigned counter dtype - V3UniqueNames m_names{"__Vnfa"}; // Generated state variable names + V3UniqueNames m_names{"__Vnfa"}; size_t m_statDelayRingEdgeVisits = 0; // Delay-ring incoming edges visited // Per-lowering shared context (passed to phase sub-functions) // Per-vertex lowering state is stored in SvaVertexData and accessed via // V3GraphVertex::userp() (see vtx[i]->datap()). struct LowerCtx final { - FileLine* const flp; // Source location for generated AST - SvaGraph& graph; // NFA graph - int N = 0; // Number of vertices + FileLine* flp; // Source location for generated AST + int N; // Number of vertices std::vector vtx; // Color-indexed vertex lookup std::vector edges; // All edges (flat) - int startIdx = 0; // Start vertex color index - int matchIdx = -1; // Match vertex color index (-1 if none) - AstSenTree* senTreep = nullptr; // Clock sensitivity tree - AstNodeExpr* disableExprp = nullptr; // disable iff expression (may be nullptr) - AstNodeExpr* matchCondp = nullptr; // Final boolean match condition (may be nullptr) - AstVar* disableCntVarp = nullptr; // disable counter var (may be nullptr) - AstVar* snapshotVarp = nullptr; // disable snapshot var (may be nullptr) - VAssertType assertType = VAssertType::INTERNAL; // Assertion type for control tasks - VAssertDirectiveType directiveType - = VAssertDirectiveType::INTERNAL; // Directive type for control tasks - AstVar* killVarp = nullptr; // Last observed kill generation - AstVar* evalKillVarp = nullptr; // Pre-update kill generation used for the verdict - AstVar* ctlKillVarp = nullptr; // Kill query captured once at transaction entry - AstVar* abortAnyVarp = nullptr; // Any outer-priority abort condition fired - AstVar* abortAcceptVarp = nullptr; // Winning abort is accept_on - AstVar* abortRejectVarp = nullptr; // Winning abort is reject_on - AstNode* snapshotBodyp = nullptr; // Observed old-state snapshot statements - AstNode* updateBodyp = nullptr; // Observed live-state update statements - LowerCtx(FileLine* fl, SvaGraph& g) - : flp{fl} - , graph{g} {} + int startIdx; // Start vertex color index + int matchIdx; // Match vertex color index (-1 if none) + AstSenTree* senTreep; // Clock sensitivity tree + AstNodeExpr* disableExprp; // disable iff expression (may be nullptr) + AstNodeExpr* matchCondp; // Final boolean match condition (may be nullptr) + AstVar* disableCntVarp; // disable counter var (may be nullptr) + AstVar* snapshotVarp; // disable snapshot var (may be nullptr) + VAssertType assertType; // Assertion type for control tasks + VAssertDirectiveType directiveType; // Directive type for control tasks + AstVar* killVarp; // Last observed kill generation + SvaGraph& graph; // NFA graph }; - static void appendStmt(AstNode*& bodypr, AstNode* stmtp) { - if (bodypr) { - bodypr->addNext(stmtp); - } else { - bodypr = stmtp; - } - } - // Build a match-now expression: stateSig[i] && $sampled(condp) static AstNodeExpr* buildMatchNow(FileLine* flp, AstNodeExpr* stateExprp, AstNodeExpr* condp) { AstNodeExpr* const statep = stateExprp->cloneTreePure(false); @@ -2177,22 +1782,27 @@ private: if (!ap) return bp; return new AstLogOr{flp, ap, bp}; } + static AstNodeExpr* addThreadFailCountp(FileLine* const flp, + AstNodeExpr* const totalThreadFailCountp, + AstNodeExpr* const contributionp, + AstNodeExpr* const enablep = nullptr) { + // contribution = enable ? contribution : 0; + AstNodeExpr* const enabledContributionp + = enablep ? new AstCond{flp, enablep, contributionp, + newTypedConstp(flp, contributionp->dtypep(), 0)} + : contributionp; + if (!totalThreadFailCountp) return enabledContributionp; + return new AstAdd{flp, totalThreadFailCountp, enabledContributionp}; + } static AstNodeExpr* killActive(LowerCtx& c) { - return new AstNeq{c.flp, new AstVarRef{c.flp, c.evalKillVarp, VAccess::READ}, - new AstVarRef{c.flp, c.ctlKillVarp, VAccess::READ}}; + return new AstNeq{c.flp, new AstVarRef{c.flp, c.killVarp, VAccess::READ}, + assertKillGet(c.flp, c.assertType, c.directiveType)}; } static AstNodeExpr* notKillActive(LowerCtx& c) { return new AstLogNot{c.flp, killActive(c)}; } static AstNodeExpr* gateNotKill(LowerCtx& c, AstNodeExpr* exprp) { if (!exprp) return nullptr; return new AstLogAnd{c.flp, exprp, notKillActive(c)}; } - static AstNodeExpr* abortActive(LowerCtx& c) { - return new AstVarRef{c.flp, c.abortAnyVarp, VAccess::READ}; - } - static AstNodeExpr* gateNotAbort(LowerCtx& c, AstNodeExpr* exprp) { - if (!exprp || !c.abortAnyVarp) return exprp; - return new AstLogAnd{c.flp, exprp, new AstLogNot{c.flp, abortActive(c)}}; - } static AstNodeExpr* nextRingIndex(FileLine* flp, AstVar* idxp, uint32_t size) { const auto u32Const = [flp](uint32_t value) { return new AstConst{flp, AstConst::WidthedValue{}, 32, value}; @@ -2213,393 +1823,47 @@ private: // ring[idx] return new AstSel{flp, new AstVarRef{flp, ringp, access}, idxExprp, 1}; } - static AstNodeExpr* ringIndexOffset(FileLine* flp, AstVar* idxp, uint32_t size, - uint32_t offset) { - if (!offset) return new AstVarRef{flp, idxp, VAccess::READ}; - AstAdd* const addp = new AstAdd{flp, new AstVarRef{flp, idxp, VAccess::READ}, - new AstConst{flp, AstConst::WidthedValue{}, 32, offset}}; - addp->dtypeFrom(idxp); - AstModDiv* const modp - = new AstModDiv{flp, addp, new AstConst{flp, AstConst::WidthedValue{}, 32, size}}; - modp->dtypeFrom(idxp); - return modp; + static AstNodeExpr* delayRingAtLastIndex(FileLine* const flp, AstVar* const idxp, + const uint32_t size) { + return new AstEq{flp, new AstVarRef{flp, idxp, VAccess::READ}, + new AstConst{flp, AstConst::WidthedValue{}, 32, size - 1}}; } - static AstNodeExpr* currentEntryAlive(LowerCtx& c) { - if (!c.disableExprp) return nullptr; - return new AstLogNot{c.flp, c.disableExprp->cloneTreePure(false)}; - } - static AstNodeExpr* oldAttemptAlive(LowerCtx& c) { - if (!c.snapshotVarp) return nullptr; - AstNodeExpr* const epochOkp - = new AstEq{c.flp, new AstVarRef{c.flp, c.snapshotVarp, VAccess::READ}, - new AstVarRef{c.flp, c.disableCntVarp, VAccess::READ}}; - AstNodeExpr* const gatep = currentEntryAlive(c); - if (!gatep) return epochOkp; - return new AstLogAnd{c.flp, gatep, epochOkp}; - } - static AstNodeExpr* gateOldAttempt(LowerCtx& c, AstNodeExpr* exprp) { - AstNodeExpr* const gatep = oldAttemptAlive(c); - if (!gatep) return exprp; - return new AstLogAnd{c.flp, exprp, gatep}; - } - - static void clearStateSignals(LowerCtx& c) { - for (int i = 0; i < c.N; ++i) { - AstNodeExpr*& sigp = c.vtx[i]->datap()->stateSigp; - if (sigp) VL_DO_DANGLING(sigp->deleteTree(), sigp); + static AstNodeExpr* delayRingOutput(FileLine* const flp, SvaStateVertex* const vtxp) { + AstVar* const idxp = vtxp->datap()->delayRingIdxVarp; + const uint32_t size = vtxp->m_delayRingSize; + AstNodeExpr* const outgoingIdxp = vtxp->m_isFixedDelayRing + ? new AstVarRef{flp, idxp, VAccess::READ} + : nextRingIndex(flp, idxp, size); + AstNodeExpr* outgoingValidp + = new AstVarRef{flp, vtxp->datap()->delayRingWrappedVarp, VAccess::READ}; + if (!vtxp->m_isFixedDelayRing) { + outgoingValidp + = new AstLogOr{flp, outgoingValidp, delayRingAtLastIndex(flp, idxp, size)}; } + return new AstLogAnd{flp, outgoingValidp, + delayRingBit(flp, vtxp->datap()->delayRingVarp, outgoingIdxp)}; } - - void emitAbortCapture(LowerCtx& c, const std::string& baseName, - const std::vector* abortSpecsp) { - if (!abortSpecsp) return; - - std::vector condVars; - condVars.reserve(abortSpecsp->size()); - for (size_t i = 0; i < abortSpecsp->size(); ++i) { - const AbortSpec& spec = abortSpecsp->at(i); - AstVar* const varp - = new AstVar{c.flp, VVarType::MODULETEMP, - baseName + "__abortCond" + std::to_string(i), m_modp->findBitDType()}; - varp->lifetime(VLifetime::STATIC_EXPLICIT); - m_modp->addStmtsp(varp); - condVars.push_back(varp); - // Sample abort once per clock; supported async forms have no live inter-clock window. - AstNodeExpr* valuep = sampled(spec.condp->cloneTreePure(false)); - appendStmt(c.snapshotBodyp, - new AstAssign{c.flp, new AstVarRef{c.flp, varp, VAccess::WRITE}, valuep}); - } - - c.abortAnyVarp = newAbortVar(c, baseName + "__abortAny"); - c.abortAcceptVarp = newAbortVar(c, baseName + "__abortAccept"); - c.abortRejectVarp = newAbortVar(c, baseName + "__abortReject"); - - AstNodeExpr* anyp = nullptr; - AstNodeExpr* acceptp = nullptr; - AstNodeExpr* rejectp = nullptr; - AstNodeExpr* remainingp = new AstConst{c.flp, AstConst::BitTrue{}}; - for (size_t i = 0; i < abortSpecsp->size(); ++i) { - AstNodeExpr* const condp = new AstVarRef{c.flp, condVars[i], VAccess::READ}; - anyp = orExprs(c.flp, anyp, condp->cloneTreePure(false)); - AstNodeExpr* const effectivep = new AstLogAnd{c.flp, condp->cloneTreePure(false), - remainingp->cloneTreePure(false)}; - if (abortSpecsp->at(i).kind.isAccept()) { - acceptp = orExprs(c.flp, acceptp, effectivep); - } else { - rejectp = orExprs(c.flp, rejectp, effectivep); - } - remainingp = new AstLogAnd{c.flp, remainingp, new AstLogNot{c.flp, condp}}; - } - VL_DO_DANGLING(remainingp->deleteTree(), remainingp); - if (!acceptp) acceptp = new AstConst{c.flp, AstConst::BitFalse{}}; - if (!rejectp) rejectp = new AstConst{c.flp, AstConst::BitFalse{}}; - appendStmt( - c.snapshotBodyp, - new AstAssign{c.flp, new AstVarRef{c.flp, c.abortAnyVarp, VAccess::WRITE}, anyp}); - appendStmt(c.snapshotBodyp, - new AstAssign{c.flp, new AstVarRef{c.flp, c.abortAcceptVarp, VAccess::WRITE}, - acceptp}); - appendStmt(c.snapshotBodyp, - new AstAssign{c.flp, new AstVarRef{c.flp, c.abortRejectVarp, VAccess::WRITE}, - rejectp}); - } - - AstVar* newAbortVar(LowerCtx& c, const std::string& name) { - AstVar* const varp = new AstVar{c.flp, VVarType::MODULETEMP, name, m_modp->findBitDType()}; - varp->lifetime(VLifetime::STATIC_EXPLICIT); - m_modp->addStmtsp(varp); - return varp; - } - static void addSnapshot(LowerCtx& c, AstNode*& bodyp, AstVar* evalp, AstVar* livep) { - if (!evalp) return; - UASSERT_OBJ(livep, evalp, "Evaluation snapshot missing live state"); - AstAssign* const assignp - = new AstAssign{c.flp, new AstVarRef{c.flp, evalp, VAccess::WRITE}, - new AstVarRef{c.flp, livep, VAccess::READ}}; - appendStmt(bodyp, assignp); - } - // Latch the sampled operands, abort conditions, and control state the verdict reads. - void emitEvaluationSnapshots(LowerCtx& c) { - AstNode* bodyp = c.snapshotBodyp; - addSnapshot(c, bodyp, c.evalKillVarp, c.killVarp); - for (int i = 0; i < c.N; ++i) { - SvaVertexData* const datap = c.vtx[i]->datap(); - addSnapshot(c, bodyp, datap->evalStateVarp, datap->stateVarp); - addSnapshot(c, bodyp, datap->evalDelayRingVarp, datap->delayRingVarp); - addSnapshot(c, bodyp, datap->evalDelayRingIdxVarp, datap->delayRingIdxVarp); - } - c.snapshotBodyp = bodyp; + static AstNodeExpr* delayRingHasLiveBitsp(FileLine* const flp, AstVar* const liveCountVarp) { + // active = live_count != 0; + return new AstNeq{flp, new AstVarRef{flp, liveCountVarp, VAccess::READ}, + newTypedConstp(flp, liveCountVarp->dtypep(), 0)}; } // Phase 3 output signals struct SignalSet final { AstNodeExpr* terminalActivep = nullptr; // OR of all successful terminal matches - AstNodeExpr* matchCountp = nullptr; // Additional range-ring match multiplicity - AstNodeExpr* failCountp = nullptr; // Additional dynamically counted failures + AstNodeExpr* matchCountp = nullptr; // NFA paths completing the sequence this tick AstNodeExpr* rejectBasep = nullptr; // Reject when a terminal match fails AstNodeExpr* requiredStepRejectp = nullptr; // Per-source reject from rejectOnFail Links AstNodeExpr* throughoutRejectp = nullptr; // Reject when a throughout guard drops + AstNodeExpr* threadFailCountp = nullptr; // Number of threads rejected on this tick }; - // Sentinels stored in the attempt-depth vector - static constexpr int DEPTH_UNREACHABLE = -1; - static constexpr int DEPTH_AMBIGUOUS = -2; - - using OutcomeBuckets = std::map; - - static AstNodeExpr* boolToCount(LowerCtx& c, AstNodeExpr* condp) { - AstCond* const resultp - = new AstCond{c.flp, condp, new AstConst{c.flp, AstConst::WidthedValue{}, 32, 1}, - new AstConst{c.flp, AstConst::WidthedValue{}, 32, 0}}; - resultp->dtypeFrom(c.killVarp); - return resultp; - } - - static AstNodeExpr* addCounts(LowerCtx& c, AstNodeExpr* lhsp, AstNodeExpr* rhsp) { - if (!lhsp) return rhsp; - AstAdd* const resultp = new AstAdd{c.flp, lhsp, rhsp}; - resultp->dtypeFrom(c.killVarp); - return resultp; - } - - static AstNodeExpr* gateCount(LowerCtx& c, AstNodeExpr* gatep, AstNodeExpr* countp) { - AstCond* const resultp = new AstCond{c.flp, gatep, countp, - new AstConst{c.flp, AstConst::WidthedValue{}, 32, 0}}; - resultp->dtypeFrom(c.killVarp); - return resultp; - } - - // Start depth of the attempt reaching each vertex; negative when unreachable or ambiguous. - static std::vector computeAttemptDepths(const LowerCtx& c) { - std::vector depths(c.N, DEPTH_UNREACHABLE); - depths[c.startIdx] = 0; - for (int pass = 0;; ++pass) { - UASSERT_OBJ(pass < 2 * c.N + 2, c.graph.m_startVertexp, - "Attempt depth propagation did not converge"); - bool changed = false; - for (const SvaTransEdge* const tep : c.edges) { - const int fi = tep->fromVtxp()->color(); - const int ti = tep->toVtxp()->color(); - if (depths[fi] == DEPTH_UNREACHABLE || ti == c.startIdx) continue; - int edgeDepth = tep->m_consumesCycle ? 1 : 0; - if (tep->toVtxp()->m_isFixedDelayRing) { - edgeDepth = tep->toVtxp()->m_delayRingSize; - } - const int candidate - = depths[fi] == DEPTH_AMBIGUOUS ? DEPTH_AMBIGUOUS : depths[fi] + edgeDepth; - if (depths[ti] == DEPTH_UNREACHABLE) { - depths[ti] = candidate; - changed = true; - } else if (depths[ti] != candidate && depths[ti] != DEPTH_AMBIGUOUS) { - depths[ti] = DEPTH_AMBIGUOUS; - changed = true; - } - } - if (!changed) break; - } - return depths; - } - - AstNodeExpr* strongPendingFastCount(LowerCtx& c) { - AstNodeExpr* countp = nullptr; - for (int i = 0; i < c.N; ++i) { - if (!c.vtx[i]->m_strongPending) continue; - AstNodeExpr* itemCountp = nullptr; - if (c.vtx[i]->datap()->stateVarp) { - itemCountp = boolToCount( - c, new AstVarRef{c.flp, c.vtx[i]->datap()->stateVarp, VAccess::READ}); - } else if (c.vtx[i]->datap()->delayRingVarp) { - AstCountOnes* const ringCountp = new AstCountOnes{ - c.flp, new AstVarRef{c.flp, c.vtx[i]->datap()->delayRingVarp, VAccess::READ}}; - ringCountp->dtypeFrom(c.killVarp); - itemCountp = ringCountp; - } - if (itemCountp) countp = addCounts(c, countp, itemCountp); - } - return countp; - } - - // Bucket strong pending state by attempt depth. - OutcomeBuckets bucketStrongByDepth(LowerCtx& c, const std::vector& depths) { - OutcomeBuckets depthBuckets; - for (int i = 0; i < c.N; ++i) { - if (!c.vtx[i]->m_strongPending) continue; - if (AstVar* const statep = c.vtx[i]->datap()->stateVarp) { - AstNodeExpr*& bucketpr = depthBuckets[depths[i]]; - bucketpr = orExprs(c.flp, bucketpr, new AstVarRef{c.flp, statep, VAccess::READ}); - continue; - } - AstVar* const ringp = c.vtx[i]->datap()->delayRingVarp; - if (!ringp) continue; - UASSERT_OBJ(c.vtx[i]->m_isFixedDelayRing, c.vtx[i], - "Strong pending range ring is unsupported"); - AstVar* const idxp = c.vtx[i]->datap()->delayRingIdxVarp; - const uint32_t size = static_cast(c.vtx[i]->m_delayRingSize); - for (uint32_t offset = 0; offset < size; ++offset) { - AstNodeExpr* const bitp - = delayRingBit(c.flp, ringp, ringIndexOffset(c.flp, idxp, size, offset)); - AstNodeExpr*& bucketpr = depthBuckets[depths[i] - offset]; - bucketpr = orExprs(c.flp, bucketpr, bitp); - } - } - return depthBuckets; - } - - AstNodeExpr* buildStrongPendingCount(LowerCtx& c, bool trackResolved, - bool ambiguousResolvedDepth) { - std::unordered_set groups; - uint64_t ringSlots = 0; - for (int i = 0; i < c.N; ++i) { - if (!c.vtx[i]->m_strongPending) continue; - UASSERT_OBJ(c.vtx[i]->m_strongPendingGroup >= 0, c.vtx[i], - "Strong pending vertex has no group"); - groups.insert(c.vtx[i]->m_strongPendingGroup); - if (c.vtx[i]->datap()->delayRingVarp) { - ringSlots += static_cast(c.vtx[i]->m_delayRingSize); - } - } - - if (groups.empty()) return nullptr; - - if (c.graph.m_hasAndCombiner) { - c.flp->v3warn(E_UNSUPPORTED, - "Unsupported: strong s_always in a temporal AND/intersect " - "composite cannot preserve resolved attempt identity"); - return strongPendingFastCount(c); - } - - // Linear strong properties use an exact O(1) ring count; OR needs depth buckets. - if (!trackResolved && groups.size() <= 1) return strongPendingFastCount(c); - - const std::vector depths = computeAttemptDepths(c); - for (int i = 0; i < c.N; ++i) { - if (!c.vtx[i]->m_strongPending) continue; - if (depths[i] < 0) { - c.flp->v3warn(E_UNSUPPORTED, - "Unsupported: end-of-simulation attempt counting for multiple " - "strong operators with an ambiguous temporal depth"); - return strongPendingFastCount(c); - } - } - if (ringSlots > FIXED_TRACE_SITE_LIMIT) { - c.flp->v3warn(E_UNSUPPORTED, - "Unsupported: end-of-simulation attempt counting for multiple strong " - "operators requires expanding " - << ringSlots << " ring slots (limit " << FIXED_TRACE_SITE_LIMIT - << ")"); - return strongPendingFastCount(c); - } - - if (trackResolved && ambiguousResolvedDepth) { - c.flp->v3warn(E_UNSUPPORTED, - "Unsupported: end-of-simulation attempt counting for multiple " - "strong operators with an ambiguous temporal depth"); - return strongPendingFastCount(c); - } - - OutcomeBuckets depthBuckets = bucketStrongByDepth(c, depths); - if (depthBuckets.empty()) { - c.flp->v3warn(E_UNSUPPORTED, "Unsupported: strong s_always pending state has a " - "non-positive temporal depth"); - return strongPendingFastCount(c); - } - AstNodeExpr* countp = nullptr; - for (auto& pair : depthBuckets) { - countp = addCounts(c, countp, boolToCount(c, pair.second)); - } - return countp; - } - - // Count the attempts alive this tick. - static AstNodeExpr* computeActiveAttemptCount(LowerCtx& c) { - const std::vector depths = computeAttemptDepths(c); - // Abort priority starts the current attempt before implication filters it. - UASSERT_OBJ(c.vtx[c.startIdx]->datap()->stateSigp, c.vtx[c.startIdx], - "Abort attempt root signal was not resolved"); - AstNodeExpr* currentp = c.vtx[c.startIdx]->datap()->stateSigp->cloneTreePure(false); - currentp = gateNotKill(c, currentp); - AstNodeExpr* countp = boolToCount(c, currentp); - - OutcomeBuckets scalarRoots; - for (int i = 0; i < c.N; ++i) { - SvaVertexData* const datap = c.vtx[i]->datap(); - if (datap->evalStateVarp) { - UASSERT_OBJ(depths[i] >= 0, c.vtx[i], - "Linear abort body implies a unique registered-vertex depth"); - AstNodeExpr* rootp = new AstVarRef{c.flp, datap->evalStateVarp, VAccess::READ}; - rootp = gateOldAttempt(c, rootp); - rootp = gateNotKill(c, rootp); - AstNodeExpr*& bucketpr = scalarRoots[depths[i]]; - bucketpr = orExprs(c.flp, bucketpr, rootp); - } - if (datap->evalDelayRingVarp) { - AstCountOnes* const ringCountp = new AstCountOnes{ - c.flp, new AstVarRef{c.flp, datap->evalDelayRingVarp, VAccess::READ}}; - ringCountp->dtypeFrom(c.killVarp); - AstNodeExpr* gatep = oldAttemptAlive(c); - gatep = gatep ? static_cast( - new AstLogAnd{c.flp, gatep, notKillActive(c)}) - : notKillActive(c); - countp = addCounts(c, countp, gateCount(c, gatep, ringCountp)); - } - } - for (auto& pair : scalarRoots) { - countp = addCounts(c, countp, boolToCount(c, pair.second)); - } - return countp; - } - - // Guard drops are counted by the throughout path - static AstNodeExpr* gateThroughoutGuards(LowerCtx& c, const SvaStateVertex* vtxp, - AstNodeExpr* exprp) { - for (AstNodeExpr* const condp : vtxp->m_throughoutConds) { - exprp = new AstLogAnd{c.flp, exprp, sampled(condp->cloneTreePure(false))}; - } - return exprp; - } - - static AstNodeExpr* gateAttemptOutcome(LowerCtx& c, AstNodeExpr* exprp) { - exprp = gateNotKill(c, exprp); - exprp = gateNotAbort(c, exprp); - if (c.disableExprp) { - exprp = new AstLogAnd{c.flp, exprp, - new AstLogNot{c.flp, c.disableExprp->cloneTreePure(false)}}; - } - return exprp; - } - - static void addAttemptOutcome(LowerCtx& c, OutcomeBuckets* bucketsp, - const std::vector& depths, int vertexIdx, - AstNodeExpr* exprp, int extraDepth = 0) { - if (!bucketsp) { - VL_DO_DANGLING(exprp->deleteTree(), exprp); - return; - } - int depth = depths[vertexIdx]; - if (depth >= 0) depth += extraDepth; - AstNodeExpr*& bucketpr = (*bucketsp)[depth]; - bucketpr = orExprs(c.flp, bucketpr, gateAttemptOutcome(c, exprp)); - } - - static void finishAttemptOutcomes(LowerCtx& c, OutcomeBuckets& buckets, - std::vector* outAttemptSrcsp) { - if (!outAttemptSrcsp) { - for (auto& pair : buckets) VL_DO_DANGLING(pair.second->deleteTree(), pair.second); - return; - } - AstNodeExpr* fallbackp = nullptr; - for (auto& pair : buckets) { - if (pair.first < 0) { - fallbackp = orExprs(c.flp, fallbackp, pair.second); - } else { - outAttemptSrcsp->push_back(pair.second); - } - } - if (fallbackp) outAttemptSrcsp->push_back(fallbackp); - } - - // Phase 2 updates live registered state after snapshotting incoming contributions. - void emitStateUpdate(LowerCtx& c) { + // Phase 2/2b/2c: Emit NBA state-update always blocks for registered vertices, + // delay rings, and SAnd combiner done-latches. + // Phase 2: State register NBA always block. Each clocked-edge target + // latches the OR of its incoming contributions. + void emitStateRegisterNba(LowerCtx& c) { AstNode* bodyp = nullptr; for (int i = 0; i < c.N; ++i) { if (!c.vtx[i]->datap()->stateVarp) continue; @@ -2614,32 +1878,49 @@ private: AstNodeExpr* srcSigp = c.vtx[fromIdx]->datap()->stateSigp->cloneTreePure(false); srcSigp = andCond(c.flp, srcSigp, tedger.m_condp); + + if (c.disableExprp) { + AstNodeExpr* const notDisp + = new AstLogNot{c.flp, c.disableExprp->cloneTreePure(false)}; + srcSigp = new AstLogAnd{c.flp, srcSigp, notDisp}; + } nextStatep = orExprs(c.flp, nextStatep, srcSigp); } UASSERT_OBJ(nextStatep, c.vtx[i], "Registered vertex has no clocked incoming contribution"); nextStatep = gateNotKill(c, nextStatep); - nextStatep = gateNotAbort(c, nextStatep); - AstAssign* const assignp = new AstAssign{ + AstAssignDly* const assignp = new AstAssignDly{ c.flp, new AstVarRef{c.flp, c.vtx[i]->datap()->stateVarp, VAccess::WRITE}, nextStatep}; - appendStmt(bodyp, assignp); + bodyp = AstNode::addNextNull(bodyp, assignp); } - if (bodyp) appendStmt(c.updateBodyp, bodyp); + // Capture disableCnt in Phase-2 NBA before any reactive re-evaluation. + // 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 + = new AstAssignDly{c.flp, new AstVarRef{c.flp, c.snapshotVarp, VAccess::WRITE}, + new AstVarRef{c.flp, c.disableCntVarp, VAccess::READ}}; + bodyp = AstNode::addNextNull(bodyp, snapshotp); + } + if (!bodyp) return; + m_modp->addStmtsp( + new AstAlways{c.flp, VAlwaysKwd::ALWAYS, c.senTreep->cloneTree(false), bodyp}); } - // Phase 2b: Bitset ring-buffer delay update. - void emitDelayRingUpdate(LowerCtx& c) { + // Phase 2b: Bitset ring-buffer delay always block. + void emitDelayRingNba(LowerCtx& c) { for (int ri = 0; ri < c.N; ++ri) { SvaStateVertex* const vtxp = c.vtx[ri]; + if (!vtxp->datap()->delayRingVarp) continue; AstVar* const ringp = vtxp->datap()->delayRingVarp; - if (!ringp) continue; AstVar* const idxp = vtxp->datap()->delayRingIdxVarp; - AstVar* const evalRingp = vtxp->datap()->evalDelayRingVarp; - AstVar* const evalIdxp = vtxp->datap()->evalDelayRingIdxVarp; + AstVar* const liveCountVarp = vtxp->datap()->delayRingLiveCountVarp; + AstVar* const wrappedp = vtxp->datap()->delayRingWrappedVarp; const uint32_t size = static_cast(vtxp->m_delayRingSize); AstNodeExpr* incomingp = nullptr; @@ -2653,44 +1934,59 @@ private: "Delay-ring incoming source missing stateSig"); AstNodeExpr* contribp = c.vtx[fi]->datap()->stateSigp->cloneTreePure(false); contribp = andCond(c.flp, contribp, tedger.m_condp); + if (c.disableExprp) { + AstNodeExpr* const notDisp + = new AstLogNot{c.flp, c.disableExprp->cloneTreePure(false)}; + contribp = new AstLogAnd{c.flp, contribp, notDisp}; + } incomingp = orExprs(c.flp, incomingp, contribp); } UASSERT_OBJ(incomingp, vtxp, "Delay ring has no incoming edge"); - incomingp = gateNotKill(c, incomingp); - - AstNode* updatep = nullptr; - if (vtxp->m_isFixedDelayRing) { - updatep = new AstAssign{c.flp, - delayRingBit(c.flp, ringp, - new AstVarRef{c.flp, evalIdxp, VAccess::READ}, - VAccess::WRITE), - incomingp}; - } else { - AstAssign* const clearExpirep = new AstAssign{ - c.flp, - delayRingBit(c.flp, ringp, nextRingIndex(c.flp, evalIdxp, size), - VAccess::WRITE), - new AstConst{c.flp, AstConst::BitFalse{}}}; - clearExpirep->addNext(new AstAssign{ - c.flp, - delayRingBit(c.flp, ringp, new AstVarRef{c.flp, evalIdxp, VAccess::READ}, - VAccess::WRITE), - incomingp}); - updatep = clearExpirep; + // ring[idx] <= incoming; + AstAssignDly* const writeIncomingp = new AstAssignDly{ + c.flp, + delayRingBit(c.flp, ringp, new AstVarRef{c.flp, idxp, VAccess::READ}, + VAccess::WRITE), + incomingp}; + AstNode* updateBodyp = writeIncomingp; + // live_count <= live_count + incoming_bit - outgoing_bit; + const int liveCountWidth = liveCountVarp->dtypep()->width(); + AstNodeExpr* const incomingIncrementp + = new AstExtend{c.flp, incomingp->cloneTreePure(false), liveCountWidth}; + AstNodeExpr* const outgoingp = delayRingOutput(c.flp, vtxp); + AstSub* const nextLiveCountp + = new AstSub{c.flp, + new AstAdd{c.flp, new AstVarRef{c.flp, liveCountVarp, VAccess::READ}, + incomingIncrementp}, + new AstExtend{c.flp, outgoingp, liveCountWidth}}; + updateBodyp->addNext(new AstAssignDly{ + c.flp, new AstVarRef{c.flp, liveCountVarp, VAccess::WRITE}, nextLiveCountp}); + // wrapped <= wrapped || idx == size - 1; + updateBodyp->addNext( + new AstAssignDly{c.flp, new AstVarRef{c.flp, wrappedp, VAccess::WRITE}, + new AstLogOr{c.flp, new AstVarRef{c.flp, wrappedp, VAccess::READ}, + delayRingAtLastIndex(c.flp, idxp, size)}}); + // idx <= next_idx; + updateBodyp->addNext(new AstAssignDly{c.flp, + new AstVarRef{c.flp, idxp, VAccess::WRITE}, + nextRingIndex(c.flp, idxp, size)}); + if (vtxp->m_delayRingAdvanceCondp) { + updateBodyp = new AstIf{ + c.flp, sampled(vtxp->m_delayRingAdvanceCondp->cloneTreePure(false)), + updateBodyp}; } - AstNodeExpr* clearp = killActive(c); - if (c.abortAnyVarp) clearp = orExprs(c.flp, clearp, abortActive(c)); + AstNodeExpr* clearCondp = killActive(c); if (vtxp->m_delayRingClearCondp) { - clearp = orExprs(c.flp, clearp, - sampled(vtxp->m_delayRingClearCondp->cloneTreePure(false))); + clearCondp = orExprs(c.flp, clearCondp, + sampled(vtxp->m_delayRingClearCondp->cloneTreePure(false))); } if (vtxp->m_abortClearp) { - clearp - = orExprs(c.flp, clearp, sampled(vtxp->m_abortClearp->cloneTreePure(false))); + clearCondp = orExprs(c.flp, clearCondp, + sampled(vtxp->m_abortClearp->cloneTreePure(false))); } - if (AstNodeExpr* const alivep = oldAttemptAlive(c)) { - clearp = orExprs(c.flp, clearp, new AstLogNot{c.flp, alivep}); + if (c.disableExprp) { + clearCondp = orExprs(c.flp, clearCondp, c.disableExprp->cloneTreePure(false)); } AstNodeExpr* guardp = nullptr; for (AstNodeExpr* const cp : vtxp->m_throughoutConds) { @@ -2698,212 +1994,142 @@ private: guardp = guardp ? static_cast(new AstLogAnd{c.flp, guardp, sampledp}) : sampledp; } - if (guardp) clearp = orExprs(c.flp, clearp, new AstLogNot{c.flp, guardp}); - AstConst* const zerop = new AstConst{c.flp, AstConst::DTyped{}, ringp->dtypep()}; - zerop->num().setAllBits0(); - AstNode* stepp = updatep; - AstNode* idxStepp = new AstAssign{c.flp, new AstVarRef{c.flp, idxp, VAccess::WRITE}, - nextRingIndex(c.flp, evalIdxp, size)}; - if (AstNodeExpr* const advancep = vtxp->m_delayRingAdvanceCondp) { - stepp = new AstIf{c.flp, sampled(advancep->cloneTreePure(false)), stepp}; - idxStepp = new AstIf{c.flp, sampled(advancep->cloneTreePure(false)), idxStepp}; - } - updatep = new AstIf{ - c.flp, clearp, - new AstAssign{c.flp, new AstVarRef{c.flp, ringp, VAccess::WRITE}, zerop}, stepp}; - appendStmt(c.updateBodyp, updatep); - appendStmt(c.updateBodyp, idxStepp); + if (guardp) clearCondp = orExprs(c.flp, clearCondp, new AstLogNot{c.flp, guardp}); + // Logically clear the ring without touching its wide storage. + AstAssignDly* const clearCountp + = new AstAssignDly{c.flp, new AstVarRef{c.flp, liveCountVarp, VAccess::WRITE}, + newTypedConstp(c.flp, liveCountVarp->dtypep(), 0)}; + clearCountp->addNext(new AstAssignDly{c.flp, + new AstVarRef{c.flp, wrappedp, VAccess::WRITE}, + new AstConst{c.flp, AstConst::BitFalse{}}}); + clearCountp->addNext(new AstAssignDly{c.flp, + new AstVarRef{c.flp, idxp, VAccess::WRITE}, + newTypedConstp(c.flp, idxp->dtypep(), 0)}); + updateBodyp = new AstIf{c.flp, clearCondp, clearCountp, updateBodyp}; - UASSERT_OBJ(evalRingp, vtxp, "Delay ring missing evaluation snapshot"); + m_modp->addStmtsp(new AstAlways{c.flp, VAlwaysKwd::ALWAYS, + c.senTreep->cloneTree(false), updateBodyp}); } } - // Done latches retain early AND endpoints after outcome expressions are captured. - void emitAndCombinerDoneUpdate(LowerCtx& c) { + // Phase 2c: SAnd combiner done-latch always block. + // NBA semantics ensure doneL/doneR read pre-update values (IEEE 16.9.5). + void emitAndCombinerDoneLatchNba(LowerCtx& c) { for (int ai = 0; ai < c.N; ++ai) { - SvaStateVertex* const vtxp = c.vtx[ai]; - if (!vtxp->datap()->doneLVarp) continue; - UASSERT_OBJ(vtxp->m_andLhsTermp && vtxp->m_andRhsTermp, vtxp, - "And-combiner vertex missing LHS/RHS terminal"); - const int l = vtxp->m_andLhsTermp->color(); - const int r = vtxp->m_andRhsTermp->color(); + if (!c.vtx[ai]->datap()->doneLVarp) continue; + // doneLVars is non-null only for AndCombiner vertices, which always + // have both m_andLhsTermp and m_andRhsTermp set at build time. + const SvaStateVertex* const avp = c.vtx[ai]; + UASSERT_OBJ(avp->m_andLhsTermp && avp->m_andRhsTermp, avp, + "AndCombiner vertex missing LHS/RHS terminal"); + const int l = avp->m_andLhsTermp->color(); + const int r = avp->m_andRhsTermp->color(); + // resolveLinks' 2*N+2 fixed-point pass is guaranteed to populate + // stateSigp on every AndCombiner and its LHS/RHS terminals. UASSERT_OBJ(c.vtx[l]->datap()->stateSigp && c.vtx[r]->datap()->stateSigp - && vtxp->datap()->stateSigp, - vtxp, "And-combiner signals unresolved"); + && c.vtx[ai]->datap()->stateSigp, + avp, "AndCombiner stateSigp chain unresolved after resolveLinks"); - AstNodeExpr* matchLp - = buildMatchNow(c.flp, c.vtx[l]->datap()->stateSigp, vtxp->m_andLhsCondp); - AstNodeExpr* matchRp - = buildMatchNow(c.flp, c.vtx[r]->datap()->stateSigp, vtxp->m_andRhsCondp); - matchLp = gateNotKill(c, matchLp); - matchRp = gateNotKill(c, matchRp); - matchLp = gateNotAbort(c, matchLp); - matchRp = gateNotAbort(c, matchRp); - - AstAssign* const clearLp = new AstAssign{ - c.flp, new AstVarRef{c.flp, vtxp->datap()->doneLVarp, VAccess::WRITE}, + AstAssignDly* const clearLp = new AstAssignDly{ + c.flp, new AstVarRef{c.flp, c.vtx[ai]->datap()->doneLVarp, VAccess::WRITE}, new AstConst{c.flp, AstConst::BitFalse{}}}; - clearLp->addNext(new AstAssign{ - c.flp, new AstVarRef{c.flp, vtxp->datap()->doneRVarp, VAccess::WRITE}, - new AstConst{c.flp, AstConst::BitFalse{}}}); + AstAssignDly* const clearRp = new AstAssignDly{ + c.flp, new AstVarRef{c.flp, c.vtx[ai]->datap()->doneRVarp, VAccess::WRITE}, + new AstConst{c.flp, AstConst::BitFalse{}}}; + clearLp->addNext(clearRp); - AstAssign* const setLp = new AstAssign{ - c.flp, new AstVarRef{c.flp, vtxp->datap()->doneLVarp, VAccess::WRITE}, - new AstConst{c.flp, AstConst::BitTrue{}}}; - AstIf* const setLIfp = new AstIf{c.flp, matchLp, setLp}; - setLIfp->addNext(new AstIf{ - c.flp, matchRp, - new AstAssign{c.flp, - new AstVarRef{c.flp, vtxp->datap()->doneRVarp, VAccess::WRITE}, - new AstConst{c.flp, AstConst::BitTrue{}}}}); - - AstNodeExpr* clearp - = orExprs(c.flp, killActive(c), vtxp->datap()->stateSigp->cloneTreePure(false)); - if (AstNodeExpr* const alivep = oldAttemptAlive(c)) { - clearp = orExprs(c.flp, clearp, new AstLogNot{c.flp, alivep}); + AstNodeExpr* const matchLNowp + = buildMatchNow(c.flp, c.vtx[l]->datap()->stateSigp, avp->m_andLhsCondp); + AstNodeExpr* const matchRNowp + = buildMatchNow(c.flp, c.vtx[r]->datap()->stateSigp, avp->m_andRhsCondp); + AstNodeExpr* gateLp = matchLNowp; + AstNodeExpr* gateRp = matchRNowp; + if (c.disableExprp) { + AstNodeExpr* const notDisLp + = new AstLogNot{c.flp, c.disableExprp->cloneTreePure(false)}; + gateLp = new AstLogAnd{c.flp, gateLp, notDisLp}; + AstNodeExpr* const notDisRp + = new AstLogNot{c.flp, c.disableExprp->cloneTreePure(false)}; + gateRp = new AstLogAnd{c.flp, gateRp, notDisRp}; } - appendStmt(c.updateBodyp, new AstIf{c.flp, clearp, clearLp, setLIfp}); + AstAssignDly* const setLp = new AstAssignDly{ + c.flp, new AstVarRef{c.flp, c.vtx[ai]->datap()->doneLVarp, VAccess::WRITE}, + new AstConst{c.flp, AstConst::BitTrue{}}}; + AstIf* const setLIfp = new AstIf{c.flp, gateLp, setLp, nullptr}; + AstAssignDly* const setRp = new AstAssignDly{ + c.flp, new AstVarRef{c.flp, c.vtx[ai]->datap()->doneRVarp, VAccess::WRITE}, + new AstConst{c.flp, AstConst::BitTrue{}}}; + AstIf* const setRIfp = new AstIf{c.flp, gateRp, setRp, nullptr}; + setLIfp->addNext(setRIfp); + + AstNodeExpr* const clearCondp = new AstLogOr{ + c.flp, killActive(c), c.vtx[ai]->datap()->stateSigp->cloneTreePure(false)}; + AstIf* const topp = new AstIf{c.flp, clearCondp, clearLp, setLIfp}; + m_modp->addStmtsp( + new AstAlways{c.flp, VAlwaysKwd::ALWAYS, c.senTreep->cloneTree(false), topp}); } } - static void emitKillAckUpdate(LowerCtx& c) { - AstAssign* const ackp - = new AstAssign{c.flp, new AstVarRef{c.flp, c.killVarp, VAccess::WRITE}, - new AstVarRef{c.flp, c.ctlKillVarp, VAccess::READ}}; - appendStmt(c.updateBodyp, new AstIf{c.flp, killActive(c), ackp, nullptr}); - } - - static void emitDisableEpochUpdate(LowerCtx& c) { - if (!c.snapshotVarp) return; - UASSERT_OBJ(c.disableCntVarp, c.senTreep, "snapshotVarp set without disableCntVarp"); - appendStmt(c.updateBodyp, - new AstAssign{c.flp, new AstVarRef{c.flp, c.snapshotVarp, VAccess::WRITE}, - new AstVarRef{c.flp, c.disableCntVarp, VAccess::READ}}); - } - - // Per-attempt match multiplicity contributed by a ranged-delay ring terminal. - void emitRangeRingMatchCount(LowerCtx& c, const SvaTransEdge* tep, int fi, SignalSet& sigs) { - AstCountOnes* const oldCountp = new AstCountOnes{ - c.flp, new AstVarRef{c.flp, c.vtx[fi]->datap()->evalDelayRingVarp, VAccess::READ}}; - oldCountp->dtypeFrom(c.killVarp); - AstNodeExpr* oldMatchCountp = oldCountp; - if (AstNodeExpr* const alivep = oldAttemptAlive(c)) { - oldMatchCountp = gateCount(c, alivep, oldMatchCountp); - } - - AstNodeExpr* incomingp = nullptr; - for (const V3GraphEdge& er : tep->fromVtxp()->inEdges()) { - const SvaTransEdge& inp = static_cast(er); - UASSERT_OBJ(!inp.m_consumesCycle, tep->fromVtxp(), "Range ring in-edge is a link"); - const int incomingFrom = inp.fromVtxp()->color(); - UASSERT_OBJ(c.vtx[incomingFrom]->datap()->stateSigp, inp.fromVtxp(), - "Range-ring incoming source missing stateSig"); - AstNodeExpr* contributionp - = c.vtx[incomingFrom]->datap()->stateSigp->cloneTreePure(false); - contributionp = andCond(c.flp, contributionp, inp.m_condp); - incomingp = orExprs(c.flp, incomingp, contributionp); - } - UASSERT_OBJ(incomingp, tep->fromVtxp(), "Range ring has no incoming link"); - AstNodeExpr* countp = addCounts(c, oldMatchCountp, boolToCount(c, incomingp)); - - AstNodeExpr* gatep = notKillActive(c); - UASSERT_OBJ(!tep->m_condp && c.matchCondp, tep->fromVtxp(), - "Range terminal condition rides matchCondp"); - gatep = new AstLogAnd{c.flp, gatep, sampled(c.matchCondp->cloneTreePure(false))}; - if (c.disableExprp) { - gatep = new AstLogAnd{c.flp, gatep, - new AstLogNot{c.flp, c.disableExprp->cloneTreePure(false)}}; - } - gatep = gateNotAbort(c, gatep); - countp = gateCount(c, gatep, countp); - sigs.matchCountp = addCounts(c, sigs.matchCountp, countp); + void emitKillAckNba(LowerCtx& c) { + AstAssignDly* const ackp + = new AstAssignDly{c.flp, new AstVarRef{c.flp, c.killVarp, VAccess::WRITE}, + assertKillGet(c.flp, c.assertType, c.directiveType)}; + m_modp->addStmtsp( + new AstAlways{c.flp, VAlwaysKwd::ALWAYS, c.senTreep->cloneTree(false), ackp}); } // Phase 3/3a/3b: Compute terminal match/reject signals, required-step reject, // throughout-drop reject; clean up intermediate state signals. // Phase 3: terminalActive and rejectBase from Links to matchVertex. // Builder only adds Links (non-clocked) to matchVertex via addLink in - // A checked match fed by a compressed repetition ring counts every ring occupant. - static AstNodeExpr* ringOccupantCount(LowerCtx& c, const SvaStateVertex* checkVtxp, - AstNodeExpr* firedp) { - const SvaStateVertex* const ringp = checkVtxp->m_matchCountRingp; - if (!ringp) return nullptr; - AstCountOnes* const onesp = new AstCountOnes{ - c.flp, new AstVarRef{c.flp, c.vtx[ringp->color()]->datap()->evalDelayRingVarp, - VAccess::READ}}; - onesp->dtypeFrom(c.killVarp); - return gateCount(c, firedp->cloneTreePure(false), onesp); - } - - // Terminal match/reject signals from the Links into matchVertex; per-end sources for covers. - void computeTerminalMatchAndReject(LowerCtx& c, SignalSet& sigs, OutcomeBuckets* failBucketsp, - OutcomeBuckets* matchBucketsp, - const std::vector& depths, - std::vector* outPerMidSrcsp = nullptr, - std::vector* outPerMidCountsp = nullptr) { + // wireMatchAndMidSources. For cover sequence, also count each end-of-match + // so the action can be replayed without unrolling endpoints. + void computeTerminalMatchAndReject(LowerCtx& c, AstNodeExpr* snapshotOkp, SignalSet& sigs, + const bool needMatchCount) { 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"); - const bool isRangeRing - = tedgep->fromVtxp()->m_delayRingSize && !tedgep->fromVtxp()->m_isFixedDelayRing; AstNodeExpr* srcSigp = c.vtx[fi]->datap()->stateSigp->cloneTreePure(false); srcSigp = andCond(c.flp, srcSigp, tedgep->m_condp); - if (matchBucketsp && !isRangeRing) { - AstNodeExpr* matchp = srcSigp->cloneTreePure(false); - if (c.matchCondp) { - matchp = new AstLogAnd{c.flp, matchp, - sampled(c.matchCondp->cloneTreePure(false))}; - } - // Count simultaneous sibling matches once in their shared depth bucket. - addAttemptOutcome(c, matchBucketsp, depths, tedgep->fromVtxp()->color(), matchp); + if (snapshotOkp) { + srcSigp = new AstLogAnd{c.flp, srcSigp, snapshotOkp->cloneTreePure(false)}; } - if (matchBucketsp && isRangeRing) emitRangeRingMatchCount(c, tedgep, fi, sigs); - if (outPerMidSrcsp) { - // Gate per-mid matches with matchCondp like the collapsed terminal signal. - AstNodeExpr* perMidp = srcSigp->cloneTreePure(false); - if (c.matchCondp) { - perMidp = new AstLogAnd{c.flp, perMidp, - sampled(c.matchCondp->cloneTreePure(false))}; + if (needMatchCount) { + AstNodeExpr* contributionp = nullptr; + SvaStateVertex* const countRingp = tedgep->fromVtxp()->m_matchCountRingp; + if (countRingp) { + AstVar* const liveCountVarp + = c.vtx[countRingp->color()]->datap()->delayRingLiveCountVarp; + contributionp = new AstCond{c.flp, srcSigp->cloneTreePure(false), + new AstVarRef{c.flp, liveCountVarp, VAccess::READ}, + newTypedConstp(c.flp, liveCountVarp->dtypep(), 0)}; + } else { + contributionp = new AstExtend{c.flp, srcSigp->cloneTreePure(false), + m_u32DTypep->width()}; } - perMidp = gateNotKill(c, perMidp); - outPerMidSrcsp->push_back(perMidp); - UASSERT_OBJ(outPerMidCountsp, tedgep->fromVtxp(), - "Per-mid counts travel with per-mid sources"); - outPerMidCountsp->push_back(ringOccupantCount(c, tedgep->fromVtxp(), perMidp)); + sigs.matchCountp = addThreadFailCountp(c.flp, sigs.matchCountp, contributionp); } - if (isRangeRing) { + if (tedgep->fromVtxp()->m_delayRingSize && !tedgep->fromVtxp()->m_isFixedDelayRing) { sigs.terminalActivep = orExprs(c.flp, sigs.terminalActivep, srcSigp->cloneTreePure(false)); - AstVar* const ringp = c.vtx[fi]->datap()->evalDelayRingVarp; - AstVar* const idxp = c.vtx[fi]->datap()->evalDelayRingIdxVarp; - const uint32_t size = static_cast(c.vtx[fi]->m_delayRingSize); - AstNodeExpr* expirep = gateOldAttempt( - c, delayRingBit(c.flp, ringp, nextRingIndex(c.flp, idxp, size))); - expirep = andCond(c.flp, expirep, tedgep->m_condp); - UASSERT_OBJ(c.matchCondp, tedgep->fromVtxp(), "Range ring terminal has a boolean"); - AstNodeExpr* const failp = new AstLogAnd{ - c.flp, expirep->cloneTreePure(false), - new AstLogNot{c.flp, sampled(c.matchCondp->cloneTreePure(false))}}; - addAttemptOutcome(c, failBucketsp, depths, fi, failp, - tedgep->fromVtxp()->m_delayRingSize - 1); - sigs.rejectBasep = orExprs(c.flp, sigs.rejectBasep, expirep); + // reject |= ring[next_idx] && final_condition; + 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 (tedgep->fromVtxp()->m_isUnbounded || tedgep->fromVtxp()->m_isAndCombiner) { sigs.terminalActivep = orExprs(c.flp, sigs.terminalActivep, srcSigp); } else { sigs.terminalActivep = orExprs(c.flp, sigs.terminalActivep, srcSigp->cloneTreePure(false)); - if (c.matchCondp) { - AstNodeExpr* failp = new AstLogAnd{ - c.flp, srcSigp->cloneTreePure(false), - new AstLogNot{c.flp, sampled(c.matchCondp->cloneTreePure(false))}}; - failp = gateThroughoutGuards(c, tedgep->fromVtxp(), failp); - addAttemptOutcome(c, failBucketsp, depths, fi, failp); - } sigs.rejectBasep = orExprs(c.flp, sigs.rejectBasep, srcSigp); } } @@ -2913,21 +2139,32 @@ private: "No terminal edge to match vertex"); } - // Phase 3b: Throughout-drop rejection (IEEE 16.9.9). - void computeThroughoutReject(LowerCtx& c, SignalSet& sigs, OutcomeBuckets* failBucketsp, - const std::vector& depths) { + AstNodeExpr* newThroughoutThreadFailCountp(LowerCtx& c, AstVar* const delayRingLiveCountVarp, + AstNodeExpr* const stateExprp, + AstNodeExpr* const enablep) { + AstNodeExpr* const activeThreadCountp + = delayRingLiveCountVarp + ? static_cast( + new AstVarRef{c.flp, delayRingLiveCountVarp, VAccess::READ}) + : new AstExtend{c.flp, stateExprp->cloneTreePure(false), m_u32DTypep->width()}; + return addThreadFailCountp(c.flp, nullptr, activeThreadCountp, enablep); + } + + // Phase 3b: Throughout-drop and ring-wide abort rejection. + void computeThroughoutReject(LowerCtx& c, SignalSet& sigs, const bool needThreadFailCount) { for (int i = 0; i < c.N; ++i) { const auto& conds = c.vtx[i]->m_throughoutConds; if (conds.empty() && !c.vtx[i]->m_abortRejectp) continue; if (c.vtx[i]->m_isAndCombiner) continue; - UASSERT_OBJ(c.vtx[i]->datap()->stateSigp, c.vtx[i], - "Throughout-conds vertex missing state representation"); - AstVar* const evalRingp = c.vtx[i]->datap()->evalDelayRingVarp; AstNodeExpr* stateExprp = nullptr; - if (evalRingp && c.vtx[i]->m_isFixedDelayRing) { - stateExprp = gateOldAttempt( - c, new AstRedOr{c.flp, new AstVarRef{c.flp, evalRingp, VAccess::READ}}); + if (c.vtx[i]->datap()->stateVarp) { + stateExprp = new AstVarRef{c.flp, c.vtx[i]->datap()->stateVarp, VAccess::READ}; + } else if (c.vtx[i]->datap()->delayRingVarp && c.vtx[i]->m_isFixedDelayRing) { + stateExprp + = delayRingHasLiveBitsp(c.flp, c.vtx[i]->datap()->delayRingLiveCountVarp); } else { + UASSERT_OBJ(c.vtx[i]->datap()->stateSigp, c.vtx[i], + "Throughout-conds vertex missing state representation"); stateExprp = c.vtx[i]->datap()->stateSigp->cloneTreePure(false); } AstNodeExpr* guardp = nullptr; @@ -2935,56 +2172,64 @@ private: AstNodeExpr* const sp = sampled(cp->cloneTreePure(false)); guardp = guardp ? static_cast(new AstLogAnd{c.flp, guardp, sp}) : sp; } - if (AstNodeExpr* const abortRejectp = c.vtx[i]->m_abortRejectp) { - AstNodeExpr* const notAbortp - = new AstLogNot{c.flp, sampled(abortRejectp->cloneTreePure(false))}; + if (c.vtx[i]->m_abortRejectp) { + AstNodeExpr* const notAbortp = new AstLogNot{ + c.flp, sampled(c.vtx[i]->m_abortRejectp->cloneTreePure(false))}; guardp = guardp ? static_cast(new AstLogAnd{c.flp, guardp, notAbortp}) : notAbortp; } AstNodeExpr* rejectCondp = new AstLogNot{c.flp, guardp}; - if (AstNodeExpr* const abortClearp = c.vtx[i]->m_abortClearp) { - // An accept abort clears the ring without a failure; a reject abort still rejects. - rejectCondp = new AstLogAnd{ - c.flp, rejectCondp, - new AstLogNot{c.flp, sampled(abortClearp->cloneTreePure(false))}}; - if (AstNodeExpr* const abortRejectp = c.vtx[i]->m_abortRejectp) { - rejectCondp = new AstLogOr{c.flp, rejectCondp, - sampled(abortRejectp->cloneTreePure(false))}; + if (c.vtx[i]->m_abortClearp) { + // Any abort clears the ring. Accept aborts suppress a simultaneous guard failure; + // reject aborts are restored below so they still force rejection. + AstNodeExpr* const notAbortClearp + = new AstLogNot{c.flp, sampled(c.vtx[i]->m_abortClearp->cloneTreePure(false))}; + rejectCondp = new AstLogAnd{c.flp, rejectCondp, notAbortClearp}; + if (c.vtx[i]->m_abortRejectp) { + rejectCondp + = new AstLogOr{c.flp, rejectCondp, + sampled(c.vtx[i]->m_abortRejectp->cloneTreePure(false))}; } } - AstNodeExpr* const failp = new AstLogAnd{c.flp, stateExprp, rejectCondp}; - if (evalRingp && failBucketsp) { - // Preserve one count per live ring attempt for actions and negated outcomes. - AstCountOnes* const ringCountp - = new AstCountOnes{c.flp, new AstVarRef{c.flp, evalRingp, VAccess::READ}}; - ringCountp->dtypeFrom(c.killVarp); - AstNodeExpr* countGatep = gateOldAttempt(c, rejectCondp->cloneTreePure(false)); - countGatep = gateAttemptOutcome(c, countGatep); - sigs.failCountp - = addCounts(c, sigs.failCountp, gateCount(c, countGatep, ringCountp)); - } else { - addAttemptOutcome(c, failBucketsp, depths, i, failp->cloneTreePure(false)); + if (needThreadFailCount) { + AstNodeExpr* const contributionp + = newThroughoutThreadFailCountp(c, c.vtx[i]->datap()->delayRingLiveCountVarp, + stateExprp, rejectCondp->cloneTreePure(false)); + sigs.threadFailCountp + = addThreadFailCountp(c.flp, sigs.threadFailCountp, contributionp); + if (c.vtx[i]->m_abortRejectp && c.vtx[i]->m_delayRingAdvanceCondp) { + // An advancing ring thread also occupies its same-tick match vertex in the + // unrolled NFA, so abort rejection must replay both fail actions. + AstNodeExpr* const abortAndAdvancep = new AstLogAnd{ + c.flp, sampled(c.vtx[i]->m_abortRejectp->cloneTreePure(false)), + sampled(c.vtx[i]->m_delayRingAdvanceCondp->cloneTreePure(false))}; + AstNodeExpr* const matchContributionp = newThroughoutThreadFailCountp( + c, c.vtx[i]->datap()->delayRingLiveCountVarp, stateExprp, + abortAndAdvancep); + sigs.threadFailCountp + = addThreadFailCountp(c.flp, sigs.threadFailCountp, matchContributionp); + } } - sigs.throughoutRejectp = orExprs(c.flp, sigs.throughoutRejectp, failp); + sigs.throughoutRejectp = orExprs(c.flp, sigs.throughoutRejectp, + new AstLogAnd{c.flp, stateExprp, rejectCondp}); } } - SignalSet computeSignals(LowerCtx& c, std::vector* outFailAttemptSrcsp, - std::vector* outMatchAttemptSrcsp, - bool* outAmbiguousResolvedDepthp = nullptr, - std::vector* outPerMidSrcsp = nullptr, - std::vector* outPerMidCountsp = nullptr) { + SignalSet computeSignals(LowerCtx& c, const bool needThreadFailCount, + const bool needThroughoutThreadFailCount, const bool needMatchCount) { SignalSet sigs; - const std::vector depths = computeAttemptDepths(c); - OutcomeBuckets failBuckets; - OutcomeBuckets matchBuckets; - OutcomeBuckets* const failBucketsp = outFailAttemptSrcsp ? &failBuckets : nullptr; - OutcomeBuckets* const matchBucketsp - = (outMatchAttemptSrcsp || outAmbiguousResolvedDepthp) ? &matchBuckets : nullptr; - computeTerminalMatchAndReject(c, sigs, failBucketsp, matchBucketsp, depths, outPerMidSrcsp, - outPerMidCountsp); + // Snapshot comparison expression for disable-iff counter. + // snapshotVarp and disableCntVarp are allocated together. + AstNodeExpr* snapshotOkp = nullptr; + if (c.snapshotVarp) { + UASSERT_OBJ(c.disableCntVarp, c.senTreep, "snapshotVarp set without disableCntVarp"); + snapshotOkp = new AstEq{c.flp, new AstVarRef{c.flp, c.snapshotVarp, VAccess::READ}, + new AstVarRef{c.flp, c.disableCntVarp, VAccess::READ}}; + } + + computeTerminalMatchAndReject(c, snapshotOkp, sigs, needMatchCount); // Phase 3a: required-step rejection. // Builder only sets m_rejectOnFail on non-clocked Links with m_condp @@ -3010,22 +2255,42 @@ private: } AstNodeExpr* const notCondp = new AstLogNot{c.flp, condp}; AstNodeExpr* const rawFailp = new AstLogAnd{c.flp, srcSigp, notCondp}; - addAttemptOutcome(c, failBucketsp, depths, fi, rawFailp->cloneTreePure(false)); - AstNodeExpr* const failp = gateAttemptOutcome(c, rawFailp); + if (needThreadFailCount) { + // thread_fail_count += fail; + sigs.threadFailCountp = addThreadFailCountp( + c.flp, sigs.threadFailCountp, + new AstExtend{c.flp, rawFailp->cloneTreePure(false), m_u32DTypep->width()}); + } + AstNodeExpr* const failp = gateNotKill(c, rawFailp); sigs.requiredStepRejectp = orExprs(c.flp, sigs.requiredStepRejectp, failp); } - computeThroughoutReject(c, sigs, failBucketsp, depths); + computeThroughoutReject(c, sigs, needThroughoutThreadFailCount); + if (sigs.threadFailCountp) { + sigs.threadFailCountp + = addThreadFailCountp(c.flp, nullptr, sigs.threadFailCountp, notKillActive(c)); + } + if (sigs.matchCountp) { + if (c.matchCondp) { + sigs.matchCountp = addThreadFailCountp( + c.flp, nullptr, sigs.matchCountp, sampled(c.matchCondp->cloneTreePure(false))); + } + sigs.matchCountp + = addThreadFailCountp(c.flp, nullptr, sigs.matchCountp, notKillActive(c)); + } sigs.terminalActivep = gateNotKill(c, sigs.terminalActivep); sigs.rejectBasep = gateNotKill(c, sigs.rejectBasep); sigs.throughoutRejectp = gateNotKill(c, sigs.throughoutRejectp); - sigs.terminalActivep = gateNotAbort(c, sigs.terminalActivep); - sigs.rejectBasep = gateNotAbort(c, sigs.rejectBasep); - sigs.throughoutRejectp = gateNotAbort(c, sigs.throughoutRejectp); - // Free the orphan intermediate state signals (lifetime ends this scope). - clearStateSignals(c); - // Fire-edge disable uses the current value; earlier window hops are gated above. + // Clean up intermediate state signals. These are orphan subtrees + // (never linked into the enclosing AST); deleteTree() is immediate + // which is what we want since stateSigp lifetime ends with this scope. + for (int i = 0; i < c.N; ++i) { + AstNodeExpr*& sigp = c.vtx[i]->datap()->stateSigp; + if (sigp) VL_DO_DANGLING(sigp->deleteTree(), sigp); + } + // Disable iff gating (IEEE 1800-2023 16.12). The edge counter misses a + // continuously-true disable, so gate on the current level value too. if (c.disableExprp) { // terminalActivep is always set, so gate it unconditionally. AstNodeExpr* const notTermp @@ -3041,145 +2306,112 @@ private: = new AstLogNot{c.flp, c.disableExprp->cloneTreePure(false)}; sigs.throughoutRejectp = new AstLogAnd{c.flp, sigs.throughoutRejectp, notDisp}; } - } - - if (c.disableExprp) { - VL_DO_DANGLING(c.disableExprp->deleteTree(), c.disableExprp); - c.disableExprp = nullptr; - } - - if (outAmbiguousResolvedDepthp) { - for (const auto& pair : matchBuckets) { - if (pair.first < 0) *outAmbiguousResolvedDepthp = true; + if (sigs.requiredStepRejectp) { + AstNodeExpr* const notDisp + = new AstLogNot{c.flp, c.disableExprp->cloneTreePure(false)}; + sigs.requiredStepRejectp = new AstLogAnd{c.flp, sigs.requiredStepRejectp, notDisp}; + } + if (sigs.matchCountp) { + sigs.matchCountp = addThreadFailCountp( + c.flp, nullptr, sigs.matchCountp, + new AstLogNot{c.flp, c.disableExprp->cloneTreePure(false)}); } } - finishAttemptOutcomes(c, failBuckets, outFailAttemptSrcsp); - finishAttemptOutcomes(c, matchBuckets, outMatchAttemptSrcsp); + if (snapshotOkp) { + VL_DO_DANGLING(snapshotOkp->deleteTree(), snapshotOkp); + snapshotOkp = nullptr; + } return sigs; } - // Phase 1 seeds: start trigger, registered state reads, delay-ring reads. - void seedLinkBaseSignals(LowerCtx& c, AstVar* triggerVarp) { + // Phase 1: Resolve combinational Links via fixed-point propagation. + void resolveLinks(LowerCtx& c, AstNodeExpr* triggerExprp) { // datap() was freshly allocated in lower() -- all stateSigp start null. - AstNodeExpr* startp = new AstVarRef{c.flp, triggerVarp, VAccess::READ}; - if (AstNodeExpr* const alivep = currentEntryAlive(c)) { - startp = new AstLogAnd{c.flp, startp, alivep}; - } - c.vtx[c.startIdx]->datap()->stateSigp = startp; + c.vtx[c.startIdx]->datap()->stateSigp = triggerExprp->cloneTreePure(false); for (int i = 0; i < c.N; ++i) { if (c.vtx[i]->datap()->stateVarp) { - AstVar* const statep = c.vtx[i]->datap()->evalStateVarp; c.vtx[i]->datap()->stateSigp - = gateOldAttempt(c, new AstVarRef{c.flp, statep, VAccess::READ}); + = new AstVarRef{c.flp, c.vtx[i]->datap()->stateVarp, VAccess::READ}; } else if (c.vtx[i]->datap()->delayRingVarp) { - AstVar* const ringp = c.vtx[i]->datap()->evalDelayRingVarp; - AstVar* const idxp = c.vtx[i]->datap()->evalDelayRingIdxVarp; - AstNodeExpr* ringStatep = nullptr; if (c.vtx[i]->m_isFixedDelayRing) { - ringStatep - = delayRingBit(c.flp, ringp, new AstVarRef{c.flp, idxp, VAccess::READ}); + // state = ring[idx]; + c.vtx[i]->datap()->stateSigp = delayRingOutput(c.flp, c.vtx[i]); } else { - ringStatep = new AstRedOr{c.flp, new AstVarRef{c.flp, ringp, VAccess::READ}}; + c.vtx[i]->datap()->stateSigp + = delayRingHasLiveBitsp(c.flp, c.vtx[i]->datap()->delayRingLiveCountVarp); } - c.vtx[i]->datap()->stateSigp = gateOldAttempt(c, ringStatep); } } - } - - // OR every in-Link contribution (and combiner terminal match) onto the seed. - void finalizeLinkTarget(LowerCtx& c, int ti) { - SvaStateVertex* const vtxp = c.vtx[ti]; - AstNodeExpr* sigp = vtxp->datap()->stateSigp; - if (vtxp->m_isAndCombiner) { - const int l = vtxp->m_andLhsTermp->color(); - const int r = vtxp->m_andRhsTermp->color(); - UASSERT_OBJ(c.vtx[l]->datap()->stateSigp && c.vtx[r]->datap()->stateSigp, vtxp, - "Combiner terminals resolve before the combiner"); - { + // Fixed-point propagation along zero-delay (Link) edges. Rebuild each + // derived signal from its incoming edges on every pass; appending the same + // contributions repeatedly makes merge expressions grow exponentially. + for (int pass = 0; pass < 2 * c.N + 2; ++pass) { + // Rebuild SAnd combiners once both sub-NFA terminals are available. + for (int i = 0; i < c.N; ++i) { + if (!c.vtx[i]->m_isAndCombiner) continue; + // AndCombiner vertices always have both terminal pointers set. + UASSERT_OBJ(c.vtx[i]->m_andLhsTermp && c.vtx[i]->m_andRhsTermp, c.vtx[i], + "AndCombiner vertex missing LHS/RHS terminal"); + const int l = c.vtx[i]->m_andLhsTermp->color(); + const int r = c.vtx[i]->m_andRhsTermp->color(); + if (!c.vtx[l]->datap()->stateSigp || !c.vtx[r]->datap()->stateSigp) continue; AstNodeExpr* const matchLp - = buildMatchNow(c.flp, c.vtx[l]->datap()->stateSigp, vtxp->m_andLhsCondp); + = buildMatchNow(c.flp, c.vtx[l]->datap()->stateSigp, c.vtx[i]->m_andLhsCondp); AstNodeExpr* const matchRp - = buildMatchNow(c.flp, c.vtx[r]->datap()->stateSigp, vtxp->m_andRhsCondp); - AstNodeExpr* matchp = nullptr; - if (vtxp->m_andNeedsDoneLatches) { - UASSERT_OBJ(vtxp->datap()->doneLVarp && vtxp->datap()->doneRVarp, vtxp, - "Temporal-and combiner missing done latches"); - AstNodeExpr* const doneLOrp = new AstLogOr{ - c.flp, new AstVarRef{c.flp, vtxp->datap()->doneLVarp, VAccess::READ}, - matchLp}; - AstNodeExpr* const doneROrp = new AstLogOr{ - c.flp, new AstVarRef{c.flp, vtxp->datap()->doneRVarp, VAccess::READ}, - matchRp}; - AstNodeExpr* const bothp = new AstLogAnd{c.flp, doneLOrp, doneROrp}; - AstNodeExpr* const oneNowp = new AstLogOr{c.flp, matchLp->cloneTreePure(false), - matchRp->cloneTreePure(false)}; - matchp = new AstLogAnd{c.flp, bothp, oneNowp}; - } else { - matchp = new AstLogAnd{c.flp, matchLp, matchRp}; + = buildMatchNow(c.flp, c.vtx[r]->datap()->stateSigp, c.vtx[i]->m_andRhsCondp); + AstNodeExpr* const doneLOrp = new AstLogOr{ + c.flp, new AstVarRef{c.flp, c.vtx[i]->datap()->doneLVarp, VAccess::READ}, + matchLp}; + AstNodeExpr* const doneROrp = new AstLogOr{ + c.flp, new AstVarRef{c.flp, c.vtx[i]->datap()->doneRVarp, VAccess::READ}, + matchRp}; + AstNodeExpr* const bothp = new AstLogAnd{c.flp, doneLOrp, doneROrp}; + AstNodeExpr* const oneNowp = new AstLogOr{c.flp, matchLp->cloneTreePure(false), + matchRp->cloneTreePure(false)}; + if (c.vtx[i]->datap()->stateSigp) { + VL_DO_DANGLING(c.vtx[i]->datap()->stateSigp->deleteTree(), + c.vtx[i]->datap()->stateSigp); } - sigp = orExprs(c.flp, sigp, matchp); + c.vtx[i]->datap()->stateSigp = new AstLogAnd{c.flp, bothp, oneNowp}; } - } - for (const V3GraphEdge& er : vtxp->inEdges()) { - const SvaTransEdge& te = static_cast(er); - UASSERT_OBJ(!te.m_consumesCycle, vtxp, "Link target has no clocked in-edge"); - AstNodeExpr* const srcSigp = te.fromVtxp()->datap()->stateSigp; - UASSERT_OBJ(srcSigp, te.fromVtxp(), "Link source resolves before its target"); - sigp = orExprs(c.flp, sigp, andCond(c.flp, srcSigp->cloneTreePure(false), te.m_condp)); - } - vtxp->datap()->stateSigp = sigp; - } - // Phase 1 finalizes each combinational Link after all dependency sources. - void resolveLinks(LowerCtx& c, AstVar* triggerVarp) { - seedLinkBaseSignals(c, triggerVarp); - std::vector pendingDeps(c.N, 0); - std::vector> dependents(c.N); - for (const SvaTransEdge* const tep : c.edges) { - if (tep->m_consumesCycle) continue; - const SvaStateVertex* const top = tep->toVtxp(); - if (top->m_isMatch || top->m_isRejectSink || top->datap()->needsReg) continue; - pendingDeps[top->color()]++; - dependents[tep->fromVtxp()->color()].push_back(top->color()); - } - for (int i = 0; i < c.N; ++i) { - if (!c.vtx[i]->m_isAndCombiner) continue; - // Same-end combiner vertices always have both terminal pointers set. - UASSERT_OBJ(c.vtx[i]->m_andLhsTermp && c.vtx[i]->m_andRhsTermp, c.vtx[i], - "Same-end combiner vertex missing LHS/RHS terminal"); - pendingDeps[i] += 2; - dependents[c.vtx[i]->m_andLhsTermp->color()].push_back(i); - dependents[c.vtx[i]->m_andRhsTermp->color()].push_back(i); - } - std::vector worklist; - std::vector finalized(c.N, false); - for (int i = 0; i < c.N; ++i) { - if (!pendingDeps[i]) { - finalized[i] = true; - worklist.push_back(i); + for (int ti = 0; ti < c.N; ++ti) { + if (ti == c.startIdx || c.vtx[ti]->datap()->stateVarp + || c.vtx[ti]->datap()->delayRingVarp || c.vtx[ti]->m_isAndCombiner + || c.vtx[ti]->m_isMatch || c.vtx[ti]->m_isRejectSink) { + continue; + } + AstNodeExpr* nextStatep = nullptr; + for (const V3GraphEdge& er : c.vtx[ti]->inEdges()) { + const SvaTransEdge& te = static_cast(er); + const int fi = te.fromVtxp()->color(); + if (!c.vtx[fi]->datap()->stateSigp) continue; + AstNodeExpr* const contributionp = andCond( + c.flp, c.vtx[fi]->datap()->stateSigp->cloneTreePure(false), te.m_condp); + nextStatep = orExprs(c.flp, nextStatep, contributionp); + } + if (c.vtx[ti]->datap()->stateSigp) { + VL_DO_DANGLING(c.vtx[ti]->datap()->stateSigp->deleteTree(), + c.vtx[ti]->datap()->stateSigp); + } + c.vtx[ti]->datap()->stateSigp = nextStatep; } } - while (!worklist.empty()) { - const int u = worklist.back(); - worklist.pop_back(); - for (const int d : dependents[u]) { - if (--pendingDeps[d]) continue; - finalizeLinkTarget(c, d); - finalized[d] = true; - worklist.push_back(d); - } - } - for (int i = 0; i < c.N; ++i) { - UASSERT_OBJ(finalized[i], c.vtx[i], "Combinational Link dependency cycle"); - } } +public: // Combine terminal/reject signals into final output expression. - AstNodeExpr* assembleResult(FileLine* flp, bool isCover, bool negated, AstNodeExpr* matchCondp, - AstNodeExpr* terminalActivep, AstNodeExpr* rejectBasep, - AstNodeExpr* throughoutRejectp, AstNodeExpr* requiredStepRejectp) { + static AstNodeExpr* assembleResult(AstNodeCoverOrAssert* const assertp, const bool negated, + AstNodeExpr* const matchCondp, const SignalSet& sigs, + AstNodeExpr** const outMatchpp) { + FileLine* const flp = assertp->fileline(); + const bool isCover = VN_IS(assertp, Cover); + AstNodeExpr* const terminalActivep = sigs.terminalActivep; + AstNodeExpr* const rejectBasep = sigs.rejectBasep; + AstNodeExpr* const throughoutRejectp = sigs.throughoutRejectp; + AstNodeExpr* const requiredStepRejectp = sigs.requiredStepRejectp; // Property negation (IEEE 1800-2023 16.12.1 `not`): invert match/reject. if (negated) { if (isCover) { @@ -3204,6 +2436,22 @@ private: AstNodeExpr* const sampledCondp = sampled(matchCondp->cloneTreePure(false)); matchp = new AstLogAnd{flp, matchp, sampledCondp}; } + if (outMatchpp) { + AstNodeExpr* notPMatchp = nullptr; + if (matchCondp && rejectBasep) { + AstNodeExpr* const sampledCondp = sampled(matchCondp->cloneTreePure(false)); + notPMatchp = new AstLogAnd{flp, rejectBasep->cloneTreePure(false), + new AstLogNot{flp, sampledCondp}}; + } else if (rejectBasep) { + notPMatchp = rejectBasep->cloneTreePure(false); + } + if (throughoutRejectp) + notPMatchp = orExprs(flp, notPMatchp, throughoutRejectp->cloneTreePure(false)); + if (requiredStepRejectp) + notPMatchp + = orExprs(flp, notPMatchp, requiredStepRejectp->cloneTreePure(false)); + *outMatchpp = notPMatchp; + } if (throughoutRejectp) VL_DO_DANGLING(throughoutRejectp->deleteTree(), throughoutRejectp); if (rejectBasep) VL_DO_DANGLING(rejectBasep->deleteTree(), rejectBasep); @@ -3232,6 +2480,14 @@ private: } else if (rejectBasep) { VL_DO_DANGLING(rejectBasep->deleteTree(), rejectBasep); } + if (outMatchpp) { + AstNodeExpr* matchExprp = terminalActivep->cloneTreePure(false); + if (matchCondp) { + AstNodeExpr* const sp = sampled(matchCondp->cloneTreePure(false)); + matchExprp = new AstLogAnd{flp, matchExprp, sp}; + } + *outMatchpp = matchExprp; + } if (terminalActivep) VL_DO_DANGLING(terminalActivep->deleteTree(), terminalActivep); if (throughoutRejectp) rejectp = orExprs(flp, rejectp, throughoutRejectp); if (requiredStepRejectp) rejectp = orExprs(flp, rejectp, requiredStepRejectp); @@ -3240,149 +2496,66 @@ private: return resultExprp; } - // Capture an impure control query once at transaction entry; readers use the var. - AstVar* emitCtlCapture(LowerCtx& c, const std::string& name, AstNodeExpr* valuep, - AstNodeDType* dtypep) { - AstVar* const varp = new AstVar{c.flp, VVarType::MODULETEMP, name, - dtypep ? dtypep : m_modp->findBitDType()}; - varp->lifetime(VLifetime::STATIC_EXPLICIT); - m_modp->addStmtsp(varp); - appendStmt(c.snapshotBodyp, - new AstAssign{c.flp, new AstVarRef{c.flp, varp, VAccess::WRITE}, valuep}); - return varp; + explicit SvaNfaLowering(AstNodeModule* modp) + : m_modp{modp} + , m_u32DTypep{modp->findBasicDType(VBasicDTypeKwd::UINT32)} {} + ~SvaNfaLowering() { + V3Stats::addStatSum("Assertions, NFA delay ring edge visits", m_statDelayRingEdgeVisits); } - AstNodeExpr* materializeObserved(LowerCtx& c, const std::string& name, AstNodeExpr* exprp, - AstNode*& bodypr, AstNodeDType* dtypep = nullptr) { - if (!exprp) return nullptr; - AstVar* const varp = new AstVar{c.flp, VVarType::MODULETEMP, name, - dtypep ? dtypep : m_modp->findBitDType()}; - varp->lifetime(VLifetime::STATIC_EXPLICIT); - m_modp->addStmtsp(varp); - appendStmt(bodypr, - new AstAssign{c.flp, new AstVarRef{c.flp, varp, VAccess::WRITE}, exprp}); - return new AstVarRef{c.flp, varp, VAccess::READ}; - } + // Lower NFA graph to synthesizable AstAlways blocks and raw result signals. + // Links are combinational; Edges are registered (NBA). + SignalSet lower(AstNodeCoverOrAssert* const assertp, SvaGraph& graph, + AstSenTree* const senTreep, AstNodeExpr* const matchCondp, + AstNodeExpr* const disableExprp, AstVar* const disableCntVarp, + AstVar* const snapshotVarp, const bool needThreadFailCount, + const bool needThroughoutThreadFailCount) { + FileLine* const flp = assertp->fileline(); + AstCover* const coverp = VN_CAST(assertp, Cover); + const bool isSeqEvent = coverp && coverp->isSeqEvent(); + const VAssertType assertType + = isSeqEvent ? VAssertType{VAssertType::INTERNAL} : assertp->userType(); + const VAssertDirectiveType directiveType + = isSeqEvent ? VAssertDirectiveType{VAssertDirectiveType::INTERNAL} + : assertp->directive(); + const std::string baseName = m_names.get(""); - // Requested-output pointers into a LowerResult; null = not requested - struct LowerOutputs final { - AstNodeExpr** abortAnypp = nullptr; // Any abort fired - AstNodeExpr** disablepp = nullptr; // Observed disable reference - std::vector* failAttemptSrcsp = nullptr; // Per-depth failures - std::vector* matchAttemptSrcsp = nullptr; // Per-depth matches - AstNodeExpr** failCountpp = nullptr; // Extra counted failures - AstNodeExpr** matchCountpp = nullptr; // Extra counted matches - AstNodeExpr** abortPassCountpp = nullptr; // Forced-accept count - AstNodeExpr** abortFailCountpp = nullptr; // Forced-reject count - AstNodeExpr** strongPendingCountpp = nullptr; // End-of-sim pending count - std::vector* perMidSrcsp = nullptr; // Per-end cover signals - std::vector* perMidCountsp - = nullptr; // Ring occupants per perMidSrcsp entry - }; - static LowerOutputs bindLowerOutputs(const LowerRequest& req, LowerResult& res) { - LowerOutputs o; - o.abortAnypp = req.abortSpecsp ? &res.abortAnyp : nullptr; - o.disablepp = req.disableExprp ? &res.disableRefp : nullptr; - o.failAttemptSrcsp = req.wantPerSrcFail ? &res.failAttemptSrcs : nullptr; - o.matchAttemptSrcsp = req.wantPerSrcMatch ? &res.matchAttemptSrcs : nullptr; - o.failCountpp = req.wantPerSrcFail ? &res.failCountp : nullptr; - o.matchCountpp = req.wantPerSrcMatch ? &res.matchCountp : nullptr; - o.abortPassCountpp = req.wantAbortPassCount ? &res.abortPassCountp : nullptr; - o.abortFailCountpp = req.wantAbortFailCount ? &res.abortFailCountp : nullptr; - o.strongPendingCountpp = req.wantStrongPending ? &res.strongPendingCountp : nullptr; - o.perMidSrcsp = req.wantPerMid ? &res.perMidSrcs : nullptr; - o.perMidCountsp = req.wantPerMid ? &res.perMidCounts : nullptr; - return o; - } - static bool anyStrongPending(const std::vector& vtx) { - for (const SvaStateVertex* const vtxp : vtx) { - if (vtxp->m_strongPending) return true; + // Number vertices with sequential colors for array indexing. + int N = 0; + for (V3GraphVertex& vtxr : graph.m_graph.vertices()) { vtxr.color(N++); } + std::vector vtx(N, nullptr); + for (V3GraphVertex& vtxr : graph.m_graph.vertices()) { + vtx[vtxr.color()] = static_cast(&vtxr); } - return false; - } + const int startIdx = graph.m_startVertexp->color(); + const int matchIdx = graph.m_matchVertexp ? graph.m_matchVertexp->color() : -1; + const std::vector edges = graph.allEdges(); - static void pruneSingleFailSource(const LowerRequest& req, const LowerOutputs& o, - const SignalSet& sigs, LowerResult& res) { - if (!req.pruneSingleFailSource) return; - if (res.failAttemptSrcs.size() > 1) return; - UASSERT_OBJ(!sigs.failCountp, req.senTreep, - "Single-source prune with a counted fail channel"); - for (AstNodeExpr* const srcp : res.failAttemptSrcs) { - VL_DO_DANGLING(srcp->deleteTree(), srcp); + // Allocate per-vertex lowering data (stored via V3GraphVertex::userp()). + std::vector> vertexData(N); + for (int i = 0; i < N; ++i) { + vertexData[i] = std::make_unique(); + vtx[i]->userp(vertexData[i].get()); } - res.failAttemptSrcs.clear(); - } - // Turn the requested counts and sources into Observed module temporaries. - void materializeLoweringOutputs(LowerCtx& c, const std::string& baseName, SignalSet& sigs, - const LowerOutputs& o, AstNodeExpr* abortPassCountp, - AstNodeExpr* abortFailCountp, AstNode*& captureBodyp) { - if (o.abortAnypp) { - *o.abortAnypp = materializeObserved(c, baseName + "__abortAnyOutcome", abortActive(c), - captureBodyp); - } - if (o.failAttemptSrcsp) { - for (size_t i = 0; i < o.failAttemptSrcsp->size(); ++i) { - AstNodeExpr*& exprpr = o.failAttemptSrcsp->at(i); - exprpr = materializeObserved(c, baseName + "__fail" + std::to_string(i), exprpr, - captureBodyp); + // Identify registered vertices (targets of clocked edges). + for (int i = 0; i < N; ++i) { + for (const V3GraphEdge& edger : vtx[i]->outEdges()) { + const SvaTransEdge& tedger = static_cast(edger); + const int toIdx = tedger.toVtxp()->color(); + if (tedger.m_consumesCycle && toIdx != matchIdx + && !tedger.toVtxp()->m_isRejectSink) { + vtx[toIdx]->datap()->needsReg = true; + } } } - if (o.matchAttemptSrcsp) { - for (size_t i = 0; i < o.matchAttemptSrcsp->size(); ++i) { - AstNodeExpr*& exprpr = o.matchAttemptSrcsp->at(i); - exprpr = materializeObserved(c, baseName + "__matchAttempt" + std::to_string(i), - exprpr, captureBodyp); - } - } - if (o.matchCountpp) { - *o.matchCountpp = materializeObserved(c, baseName + "__matchCount", sigs.matchCountp, - captureBodyp, m_u32DTypep); - } else { - UASSERT_OBJ(!sigs.matchCountp, c.flp, "Match count built without a requested output"); - } - if (o.failCountpp) { - *o.failCountpp = materializeObserved(c, baseName + "__failCount", sigs.failCountp, - captureBodyp, m_u32DTypep); - } else { - UASSERT_OBJ(!sigs.failCountp, c.flp, "Fail count built without a requested output"); - } - if (o.abortPassCountpp) { - *o.abortPassCountpp = materializeObserved(c, baseName + "__abortPassCount", - abortPassCountp, captureBodyp, m_u32DTypep); - } else if (abortPassCountp) { - VL_DO_DANGLING(abortPassCountp->deleteTree(), abortPassCountp); - } - if (o.abortFailCountpp) { - *o.abortFailCountpp = materializeObserved(c, baseName + "__abortFailCount", - abortFailCountp, captureBodyp, m_u32DTypep); - } else if (abortFailCountp) { - VL_DO_DANGLING(abortFailCountp->deleteTree(), abortFailCountp); - } - if (o.perMidSrcsp) { - for (size_t i = 0; i < o.perMidSrcsp->size(); ++i) { - AstNodeExpr*& exprpr = o.perMidSrcsp->at(i); - exprpr = materializeObserved(c, baseName + "__mid" + std::to_string(i), exprpr, - captureBodyp); - } - } - } - struct LowerVars final { - int N = 0; // Number of vertices - std::vector vtx; // Color-indexed vertex lookup - std::vector> vertexData; // Per-vertex lowering data - int startIdx = 0; // Start vertex color index - int matchIdx = -1; // Match vertex color index - std::vector edges; // All edges (flat) - AstVar* killVarp = nullptr; // Last observed kill generation - AstVar* evalKillVarp = nullptr; // Pre-update kill generation used for the verdict - AstNode* disableCapturep = nullptr; // Observed disable capture statement - }; - void allocateVertexStateVars(FileLine* flp, const std::string& baseName, LowerVars& lv) { - std::vector& vtx = lv.vtx; - for (int i = 0; i < lv.N; ++i) { - if (vtx[i]->m_andNeedsDoneLatches) { + AstVar* const killVarp + = new AstVar{flp, VVarType::MODULETEMP, baseName + "__kill", m_u32DTypep}; + killVarp->lifetime(VLifetime::STATIC_EXPLICIT); + m_modp->addStmtsp(killVarp); + for (int i = 0; i < N; ++i) { + if (vtx[i]->m_isAndCombiner) { const std::string base = baseName + "__a" + std::to_string(i); AstVar* const lp = new AstVar{flp, VVarType::MODULETEMP, base + "_doneL", m_modp->findBitDType()}; @@ -3398,273 +2571,106 @@ private: } if (vtx[i]->m_delayRingSize) { const std::string base = baseName + "__d" + std::to_string(i); - AstNodeDType* const ringDTypep = m_modp->findLogicDType( + // bit [size-1:0] ring; + AstNodeDType* const ringDTypep = m_modp->findBitDType( vtx[i]->m_delayRingSize, vtx[i]->m_delayRingSize, VSigning::UNSIGNED); AstVar* const ringp = new AstVar{flp, VVarType::MODULETEMP, base + "_ring", ringDTypep}; ringp->lifetime(VLifetime::STATIC_EXPLICIT); m_modp->addStmtsp(ringp); vtx[i]->datap()->delayRingVarp = ringp; - AstVar* const evalRingp - = new AstVar{flp, VVarType::MODULETEMP, base + "_ringEval", ringDTypep}; - evalRingp->lifetime(VLifetime::STATIC_EXPLICIT); - m_modp->addStmtsp(evalRingp); - vtx[i]->datap()->evalDelayRingVarp = evalRingp; + // int unsigned idx; AstVar* const idxp = new AstVar{flp, VVarType::MODULETEMP, base + "_idx", m_u32DTypep}; idxp->lifetime(VLifetime::STATIC_EXPLICIT); m_modp->addStmtsp(idxp); vtx[i]->datap()->delayRingIdxVarp = idxp; - AstVar* const evalIdxp - = new AstVar{flp, VVarType::MODULETEMP, base + "_idxEval", m_u32DTypep}; - evalIdxp->lifetime(VLifetime::STATIC_EXPLICIT); - m_modp->addStmtsp(evalIdxp); - vtx[i]->datap()->evalDelayRingIdxVarp = evalIdxp; + // int unsigned live_count; + AstVar* const liveCountVarp + = new AstVar{flp, VVarType::MODULETEMP, base + "_liveCount", m_u32DTypep}; + liveCountVarp->lifetime(VLifetime::STATIC_EXPLICIT); + m_modp->addStmtsp(liveCountVarp); + vtx[i]->datap()->delayRingLiveCountVarp = liveCountVarp; + AstVar* const wrappedp = new AstVar{flp, VVarType::MODULETEMP, base + "_wrapped", + m_modp->findBitDType()}; + wrappedp->lifetime(VLifetime::STATIC_EXPLICIT); + m_modp->addStmtsp(wrappedp); + vtx[i]->datap()->delayRingWrappedVarp = wrappedp; continue; } if (!vtx[i]->datap()->needsReg) continue; - if (i == lv.startIdx) continue; + if (i == startIdx || vtx[i]->m_isMatch) continue; const std::string varName = baseName + "__s" + std::to_string(i); AstVar* const varp = new AstVar{flp, VVarType::MODULETEMP, varName, m_modp->findBitDType()}; varp->lifetime(VLifetime::STATIC_EXPLICIT); m_modp->addStmtsp(varp); vtx[i]->datap()->stateVarp = varp; - AstVar* const evalVarp - = new AstVar{flp, VVarType::MODULETEMP, varName + "Eval", m_modp->findBitDType()}; - evalVarp->lifetime(VLifetime::STATIC_EXPLICIT); - m_modp->addStmtsp(evalVarp); - vtx[i]->datap()->evalStateVarp = evalVarp; } - } - LowerVars allocateLoweringVars(FileLine* flp, SvaGraph& graph, const std::string& baseName, - AstNodeExpr*& disableExprp, AstNodeExpr** outDisablepp) { - LowerVars lv; - int& N = lv.N; - std::vector& vtx = lv.vtx; - for (V3GraphVertex& vtxr : graph.m_graph.vertices()) { - SvaStateVertex* const svtxp = static_cast(&vtxr); - svtxp->color(vtx.size()); - vtx.push_back(svtxp); - } - N = static_cast(vtx.size()); - lv.startIdx = graph.m_startVertexp->color(); - lv.matchIdx = graph.m_matchVertexp->color(); - lv.edges = graph.allEdges(); - - lv.vertexData.resize(N); - for (int i = 0; i < N; ++i) { - lv.vertexData[i] = std::make_unique(); - vtx[i]->userp(lv.vertexData[i].get()); - } - - for (int i = 0; i < N; ++i) { - for (const V3GraphEdge& er : vtx[i]->outEdges()) { - const SvaTransEdge& te = static_cast(er); - const int toIdx = te.toVtxp()->color(); - if (te.m_consumesCycle) { - UASSERT_OBJ(toIdx != lv.matchIdx && !te.toVtxp()->m_isRejectSink, te.toVtxp(), - "Clocked edge into a terminal vertex"); - vtx[toIdx]->datap()->needsReg = true; - } - } - } - - lv.killVarp = new AstVar{flp, VVarType::MODULETEMP, baseName + "__kill", m_u32DTypep}; - lv.killVarp->lifetime(VLifetime::STATIC_EXPLICIT); - m_modp->addStmtsp(lv.killVarp); - lv.evalKillVarp - = new AstVar{flp, VVarType::MODULETEMP, baseName + "__killEval", m_u32DTypep}; - lv.evalKillVarp->lifetime(VLifetime::STATIC_EXPLICIT); - m_modp->addStmtsp(lv.evalKillVarp); - allocateVertexStateVars(flp, baseName, lv); - - if (disableExprp) { - AstVar* const disableObservedp = new AstVar{ - flp, VVarType::MODULETEMP, baseName + "__disable", m_modp->findBitDType()}; - disableObservedp->lifetime(VLifetime::STATIC_EXPLICIT); - m_modp->addStmtsp(disableObservedp); - lv.disableCapturep = new AstAssign{ - flp, new AstVarRef{flp, disableObservedp, VAccess::WRITE}, disableExprp}; - disableExprp = new AstVarRef{flp, disableObservedp, VAccess::READ}; - *outDisablepp = new AstVarRef{flp, disableObservedp, VAccess::READ}; - } - return lv; - } - - void finalizeStrongPending(LowerCtx& c, bool trackStrongResolved, bool ambiguousResolvedDepth, - AstNodeExpr** outStrongPendingCountpp) { - FileLine* const flp = c.flp; - AstNodeExpr* pendingCountp - = buildStrongPendingCount(c, trackStrongResolved, ambiguousResolvedDepth); - if (pendingCountp) { - AstNodeExpr* gatep = oldAttemptAlive(c); - AstNodeExpr* const notKilledp - = new AstEq{flp, new AstVarRef{flp, c.killVarp, VAccess::READ}, - assertKillGet(flp, c.assertType, c.directiveType)}; - gatep = gatep ? static_cast(new AstLogAnd{flp, gatep, notKilledp}) - : notKilledp; - pendingCountp = gateCount(c, gatep, pendingCountp); - } - if (outStrongPendingCountpp) { - *outStrongPendingCountpp = pendingCountp; - } else if (pendingCountp) { - VL_DO_DANGLING(pendingCountp->deleteTree(), pendingCountp); - } - } - - AstNodeExpr* applyAbortToResult(LowerCtx& c, AstNodeExpr* activeAttemptCountp, bool isCover, - bool negated, AstNodeExpr* matchCondp, SignalSet& sigs, - AstNodeExpr*& abortPassCountp, AstNodeExpr*& abortFailCountp) { - FileLine* const flp = c.flp; - AstNodeExpr* abortPassp = nullptr; - AstNodeExpr* abortFailp = nullptr; - abortPassCountp = nullptr; - abortFailCountp = nullptr; - if (activeAttemptCountp) { - abortPassCountp = gateCount(c, new AstVarRef{flp, c.abortAcceptVarp, VAccess::READ}, - activeAttemptCountp->cloneTreePure(false)); - abortFailCountp = gateCount(c, new AstVarRef{flp, c.abortRejectVarp, VAccess::READ}, - activeAttemptCountp->cloneTreePure(false)); - abortPassp = new AstNeq{flp, abortPassCountp->cloneTreePure(false), - new AstConst{flp, AstConst::WidthedValue{}, 32, 0}}; - abortFailp = new AstNeq{flp, abortFailCountp->cloneTreePure(false), - new AstConst{flp, AstConst::WidthedValue{}, 32, 0}}; - VL_DO_DANGLING(activeAttemptCountp->deleteTree(), activeAttemptCountp); - } - - AstNodeExpr* resultp - = assembleResult(flp, isCover, negated, matchCondp, sigs.terminalActivep, - sigs.rejectBasep, sigs.throughoutRejectp, sigs.requiredStepRejectp); - if (abortPassp) { - UASSERT_OBJ(abortFailp, c.graph.m_startVertexp, - "Abort pass verdict without fail verdict"); - if (isCover) { - resultp = new AstLogOr{flp, abortPassp->cloneTreePure(false), resultp}; - } else { - resultp = new AstLogOr{ - flp, abortPassp->cloneTreePure(false), - new AstLogAnd{flp, new AstLogNot{flp, abortFailp->cloneTreePure(false)}, - resultp}}; - } - VL_DO_DANGLING(abortPassp->deleteTree(), abortPassp); - VL_DO_DANGLING(abortFailp->deleteTree(), abortFailp); - } - return resultp; - } - -public: - explicit SvaNfaLowering(AstNodeModule* modp) - : m_modp{modp} - , m_u32DTypep{modp->findBasicDType(VBasicDTypeKwd::UINT32)} {} - ~SvaNfaLowering() { - V3Stats::addStatSum("Assertions, NFA delay ring edge visits", m_statDelayRingEdgeVisits); - } - - // Lower snapshot, verdict, and commit into Observed; actions execute in Reactive. - LowerResult lower(FileLine* flp, SvaGraph& graph, const LowerRequest& req) { - LowerResult res; - const LowerOutputs o = bindLowerOutputs(req, res); - AstNodeExpr* disableExprp = req.disableExprp; - - const std::string baseName = m_names.get(""); - LowerVars lv = allocateLoweringVars(flp, graph, baseName, disableExprp, o.disablepp); - const int N = lv.N; - std::vector& vtx = lv.vtx; // Build lowering context for phase sub-functions. - LowerCtx c{flp, graph}; - c.N = N; - c.vtx = vtx; - c.edges = lv.edges; - c.startIdx = lv.startIdx; - c.matchIdx = lv.matchIdx; - c.senTreep = req.senTreep; - c.disableExprp = disableExprp; - c.matchCondp = req.matchCondp; - c.disableCntVarp = req.disableCntVarp; - c.snapshotVarp = req.snapshotVarp; - c.assertType = req.assertType; - c.directiveType = req.directiveType; - c.killVarp = lv.killVarp; - c.evalKillVarp = lv.evalKillVarp; - c.snapshotBodyp = lv.disableCapturep; - - c.ctlKillVarp - = emitCtlCapture(c, baseName + "__ctlKill", - assertKillGet(flp, req.assertType, req.directiveType), m_u32DTypep); - AstVar* const ctlOnVarp - = emitCtlCapture(c, baseName + "__ctlOn", req.triggerExprp->cloneTree(false), nullptr); - emitAbortCapture(c, baseName, req.abortSpecsp); - emitEvaluationSnapshots(c); + LowerCtx c{flp, N, vtx, edges, startIdx, + matchIdx, senTreep, disableExprp, matchCondp, disableCntVarp, + snapshotVarp, assertType, directiveType, killVarp, graph}; // Phase 1: Resolve combinational Links via fixed-point propagation. - resolveLinks(c, ctlOnVarp); + AstNodeExpr* const triggerExprp + = isSeqEvent ? new AstConst{flp, AstConst::BitTrue{}} + : assertOnCond(flp, assertp->userType(), assertp->directive()); + resolveLinks(c, triggerExprp); + VL_DO_DANGLING(triggerExprp->deleteTree(), triggerExprp); - AstNodeExpr* const activeAttemptCountp - = o.abortAnypp ? computeActiveAttemptCount(c) : nullptr; - - // Phase 2: update registered state, delay rings, endpoint latches, and epochs. - emitAndCombinerDoneUpdate(c); - emitStateUpdate(c); - emitDelayRingUpdate(c); - emitKillAckUpdate(c); - emitDisableEpochUpdate(c); - - const bool trackStrongResolved - = o.strongPendingCountpp && anyStrongPending(vtx) && graph.m_hasOrMerge; - if (trackStrongResolved && o.matchAttemptSrcsp) { - flp->v3warn(E_UNSUPPORTED, - "Unsupported: pass-action multiplicity for strong s_always in a " - "temporal OR composite cannot preserve resolved attempts"); - } - bool ambiguousResolvedDepth = false; + // Phase 2/2b/2c: Emit NBA state-update, delay-ring, and SAnd done-latch logic. + emitStateRegisterNba(c); + emitDelayRingNba(c); + emitAndCombinerDoneLatchNba(c); + emitKillAckNba(c); // Phase 3/3a/3b: Compute terminal match/reject signals (cleans up stateSig). - SignalSet sigs = computeSignals(c, o.failAttemptSrcsp, o.matchAttemptSrcsp, - trackStrongResolved ? &ambiguousResolvedDepth : nullptr, - o.perMidSrcsp, o.perMidCountsp); + const SignalSet sigs = computeSignals( + c, needThreadFailCount, needThroughoutThreadFailCount, coverp && coverp->isCoverSeq()); - pruneSingleFailSource(req, o, sigs, res); - - AstNodeExpr* abortPassCountp = nullptr; - AstNodeExpr* abortFailCountp = nullptr; - AstNodeExpr* resultp - = applyAbortToResult(c, activeAttemptCountp, req.isCover, req.negated, req.matchCondp, - sigs, abortPassCountp, abortFailCountp); - - AstNode* captureBodyp = nullptr; - resultp = materializeObserved(c, baseName + "__result", resultp, captureBodyp); - materializeLoweringOutputs(c, baseName, sigs, o, abortPassCountp, abortFailCountp, - captureBodyp); - - // Strong EOS pending count; ambiguous resolved-match depths fall back gracefully. - finalizeStrongPending(c, trackStrongResolved, ambiguousResolvedDepth, - o.strongPendingCountpp); - - AstNode* observedBodyp = c.snapshotBodyp; - appendStmt(observedBodyp, captureBodyp); - appendStmt(observedBodyp, c.updateBodyp); - AstNodeExpr* const notFinishp - = new AstLogNot{flp, new AstCExpr{flp, - "(vlSymsp->_vm_contextp__->finishPending()" - " || vlSymsp->_vm_contextp__->gotFinish())", - 1}}; - m_modp->addStmtsp(new AstAlwaysObserved{flp, req.senTreep->cloneTree(false), - new AstIf{flp, notFinishp, observedBodyp}}); + // Strong s_always[m:n] end-of-simulation liveness: if any in-window state + // is still set at $finish, the universal-quantifier window never completed + // (IEEE 1800-2023 16.12.11 strong semantics). Fire the assertion failure + // from a final block; V3Assert turns the DT_ERROR display into the standard + // "Assertion failed in %m" message. + // pending = |{strong state registers, strong delay live counts != 0}; + AstNodeExpr* pendingp = nullptr; + for (int i = 0; i < N; ++i) { + if (!vtx[i]->m_strongPending) continue; + AstNodeExpr* pendingExprp = nullptr; + if (vtx[i]->datap()->stateVarp) { + pendingExprp = new AstVarRef{flp, vtx[i]->datap()->stateVarp, VAccess::READ}; + } else if (vtx[i]->m_strongAlwaysRing) { + pendingExprp = delayRingHasLiveBitsp(flp, vtx[i]->datap()->delayRingLiveCountVarp); + } else { + continue; + } + if (!pendingp) { + pendingp = pendingExprp; + } else { + pendingp = new AstLogOr{flp, pendingp, pendingExprp}; + } + } + if (pendingp) { + AstCExpr* const assertOnp + = new AstCExpr{flp, AstCExpr::Pure{}, "vlSymsp->_vm_contextp__->assertOn()", 1}; + AstNodeExpr* const condp = new AstLogAnd{flp, assertOnp, pendingp}; + AstDisplay* const dispp + = new AstDisplay{flp, VDisplayType::DT_ERROR, "", nullptr, nullptr}; + dispp->fmtp()->timeunit(m_modp->timeunit()); + AstNodeStmt* const firep = dispp; + if (v3Global.opt.stopFail()) firep->addNext(new AstStop{flp, false}); + m_modp->addStmtsp(new AstFinal{flp, new AstIf{flp, condp, firep}}); + } // Clear userp on every vertex before vertexData unique_ptrs are destroyed. for (int i = 0; i < N; ++i) vtx[i]->userp(nullptr); - res.outputExprp = resultp; - return res; + return sigs; } }; -// Out-of-line definitions, required under C++14 as these are odr-used -// (bound to a const reference by std::vector's fill constructor) -constexpr int SvaNfaLowering::DEPTH_UNREACHABLE; -constexpr int SvaNfaLowering::DEPTH_AMBIGUOUS; - } // namespace //###################################################################### @@ -3679,10 +2685,8 @@ class AssertNfaVisitor final : public VNVisitor { AstSenTree* m_sampledValueClockp = nullptr; // Inherited clock during scoped attachment V3UniqueNames m_propVarNames{"__Vpropvar"}; // Property-local variable names V3UniqueNames m_disableCntNames{"__VnfaDis"}; // Disable-iff counter names - V3UniqueNames m_disableSampleNames{"__VnfaDisSample"}; V3UniqueNames m_propTempNames{"__VnfaSampled"}; // Hoisted $sampled(propp) temps - V3UniqueNames m_actionCountNames{"__VnfaActionCount"}; - std::unordered_set m_inliningProps; // Recursion guard + std::set m_inliningProps; // Recursion guard for inlineNamedProperty template void visitSampledValue(T_Node* const nodep) { @@ -3740,7 +2744,7 @@ class AssertNfaVisitor final : public VNVisitor { } m_inliningProps.insert(propyp); struct Guard final { - std::unordered_set& setr; + std::set& setr; const AstProperty* keyp; ~Guard() { setr.erase(keyp); } } guard{m_inliningProps, propyp}; @@ -3876,7 +2880,11 @@ class AssertNfaVisitor final : public VNVisitor { while (AstLogNot* const notp = VN_CAST(p, LogNot)) p = notp->lhsp(); AstUntil* const untilp = VN_CAST(p, Until); if (!untilp) return false; - return !containsMultiCycleSva(untilp->lhsp()) && !containsMultiCycleSva(untilp->rhsp()); + const auto hasSeq = [](const AstNodeExpr* ep) { + return ep->exists([](const AstNodeExpr* np) { return np->isMultiCycleSva(); }); + }; + if (hasSeq(untilp->lhsp()) || hasSeq(untilp->rhsp())) return false; + return true; } struct PropertyParts final { @@ -3903,35 +2911,6 @@ class AssertNfaVisitor final : public VNVisitor { return parts; } - static std::vector peelAbortPrefix(AstNodeExpr*& exprpr) { - std::vector result; - while (AstAbortOn* const abortp = VN_CAST(exprpr, AbortOn)) { - result.push_back({abortp->kind(), abortp->condp(), abortp}); - exprpr = abortp->propp(); - } - return result; - } - - static bool isLinearAbortBody(AstNodeExpr* nodep) { - if (AstImplication* const implp = VN_CAST(nodep, Implication)) { - return !hasMultiCycleExpr(implp->lhsp()) && isLinearAbortBody(implp->rhsp()); - } - if (AstSExpr* const sexprp = VN_CAST(nodep, SExpr)) { - AstDelay* const delayp = VN_AS(sexprp->delayp(), Delay); - if (delayp->isUnbounded()) return false; - if (delayp->isRangeDelay() && sexprp->exprp()->isMultiCycleSva()) return false; - return (!sexprp->preExprp() || isLinearAbortBody(sexprp->preExprp())) - && isLinearAbortBody(sexprp->exprp()); - } - if (AstPropAlways* const alwaysp = VN_CAST(nodep, PropAlways)) { - return !VN_IS(alwaysp->hiBoundp(), Unbounded); - } - if (AstLogNot* const notp = VN_CAST(nodep, LogNot)) { - return isLinearAbortBody(notp->lhsp()); - } - return !nodep->isMultiCycleSva(); - } - static bool canSplitImplicationPassActions(const PropertyParts& parts) { UASSERT(parts.hasImplication, "Implication pass action split requested without implication"); @@ -3941,98 +2920,40 @@ class AssertNfaVisitor final : public VNVisitor { return !hasMultiCycleExpr(parts.triggerExprp); } - static void addNfaHandler(AstAssert* assertp, AstAssert* handlerp) { - UASSERT_OBJ(!assertp->sentreep(), assertp, "Assertion clock resolves in V3AssertPre"); - handlerp->senFromAlways(assertp->senFromAlways()); - handlerp->nfaLowered(true); - assertp->addNextHere(handlerp); - } - - // Split an implication's pass action into vacuous and per-attempt branches. - void splitImplicationPassActions(AstAssert* assertp, AstPropSpec* propSpecp, - const PropertyParts& parts, AstNodeExpr* nonvacuousCountp, - AstNodeExpr* abortAnyp = nullptr) { + static void splitImplicationPassActions(AstAssert* assertp, const PropertyParts& parts, + AstNodeExpr* nonvacuousMatchp) { FileLine* const flp = assertp->fileline(); AstNode* const passsp = assertp->passsp()->unlinkFrBackWithNext(); AstNode* splitsp = nullptr; if (!parts.isFollowedBy) { - AstNodeExpr* vacuousp + AstNodeExpr* const vacuousp = new AstLogNot{flp, sampled(parts.triggerExprp->cloneTreePure(false))}; - // IEEE 1800-2023 16.12.14 gives abort priority over same-step completion. - if (abortAnyp) { - vacuousp = new AstLogAnd{flp, vacuousp, - new AstLogNot{flp, abortAnyp->cloneTreePure(false)}}; - } - AstNode* const vacuousBodyp = passsp->cloneTree(true); + AstNode* const vacuousBodyp = passsp->cloneTree(false); splitsp = newPassOnIf(flp, vacuousp, vacuousBodyp, assertp->userType(), assertp->directive(), /*vacuous=*/true); } - AstNodeExpr* const nonvacuousCondp - = new AstNeq{flp, nonvacuousCountp->cloneTreePure(false), - new AstConst{flp, AstConst::WidthedValue{}, 32, 0}}; - AstIf* const nonvacuousIfp - = newPassOnIf(flp, nonvacuousCondp, repeatAction(flp, nonvacuousCountp, passsp), - assertp->userType(), assertp->directive(), /*vacuous=*/false); - splitsp = splitsp ? AstNode::addNext(splitsp, nonvacuousIfp) - : static_cast(nonvacuousIfp); - if (!assertp->failsp()) assertp->addFailsp(new AstComment{flp, ""}); - AstAssert* const handlerp = new AstAssert{ - flp, - clonePropSpecWithBody(propSpecp, new AstConst{flp, AstConst::BitTrue{}}), - splitsp, - nullptr, - assertp->userType(), - assertp->directive(), - assertp->name()}; - addNfaHandler(assertp, handlerp); + AstIf* const nonvacuousp + = newPassOnIf(flp, nonvacuousMatchp, passsp, assertp->userType(), assertp->directive(), + /*vacuous=*/false); + splitsp = splitsp ? AstNode::addNext(splitsp, nonvacuousp) + : static_cast(nonvacuousp); + assertp->addPasssp(splitsp); } - // Allocate disable-iff counter + snapshot vars. Returns {cntp, snapp} or + // Allocate disable-iff counter + snapshot vars and unlink the original + // disable expression from the PropSpec. Returns {cntp, snapp} or // {nullptr, nullptr} if no counter is needed. struct DisableVars final { AstVar* cntp = nullptr; AstVar* snapp = nullptr; }; - - AstNodeExpr* normalizeDisableExpr(AstNodeExpr* disableExprp, AstSenTree* senTreep) { - FileLine* const flp = disableExprp->fileline(); - AstNodeExpr* const normalizedp - = new AstLogNot{flp, new AstLogNot{flp, disableExprp->cloneTreePure(false)}}; - std::vector sampleps; - normalizedp->foreach([&sampleps](AstSampled* const nodep) { sampleps.push_back(nodep); }); - std::unordered_set nestedps; - for (AstSampled* const samplep : sampleps) { - samplep->exprp()->foreach( - [&nestedps](AstSampled* const nodep) { nestedps.insert(nodep); }); - } - AstNode* sampleBodyp = nullptr; - for (AstSampled* const samplep : sampleps) { - // Nested $sampled moves with the outer clone; extract outermost only - if (nestedps.count(samplep)) continue; - FileLine* const sampleFlp = samplep->fileline(); - AstVar* const varp = new AstVar{sampleFlp, VVarType::MODULETEMP, - m_disableSampleNames.get(""), samplep->dtypep()}; - varp->lifetime(VLifetime::STATIC_EXPLICIT); - m_modp->addStmtsp(varp); - AstNodeExpr* const sampledValuep = samplep->cloneTreePure(false); - samplep->replaceWith(new AstVarRef{sampleFlp, varp, VAccess::READ}); - VL_DO_DANGLING(samplep->deleteTree(), samplep); - AstAssign* const assignp = new AstAssign{ - sampleFlp, new AstVarRef{sampleFlp, varp, VAccess::WRITE}, sampledValuep}; - sampleBodyp = AstNode::addNext(sampleBodyp, assignp); - } - if (sampleBodyp) { - m_modp->addStmtsp( - new AstAlways{flp, VAlwaysKwd::ALWAYS, senTreep->cloneTree(false), sampleBodyp}); - } - return normalizedp; - } - - DisableVars createDisableCounterMechanism(FileLine* flp, AstNodeExpr* disableExprp) { - if (!disableExprp) return {}; + DisableVars createDisableCounterMechanism(FileLine* flp, AstNodeExpr* disableExprp, + bool hasImplication, AstPropSpec* propSpecp) { + if (!disableExprp || hasImplication || VN_IS(disableExprp, Const)) return {}; + if (disableExprp->exists([](const AstSampled*) { return true; })) return {}; AstNodeDType* const u32DTypep = m_modp->findBasicDType(VBasicDTypeKwd::UINT32); const std::string cntName = m_disableCntNames.get(""); @@ -4054,6 +2975,7 @@ class AssertNfaVisitor final : public VNVisitor { snapp->lifetime(VLifetime::STATIC_EXPLICIT); m_modp->addStmtsp(snapp); + if (propSpecp && propSpecp->disablep()) propSpecp->disablep()->unlinkFrBack(); return {cntp, snapp}; } @@ -4117,193 +3039,60 @@ class AssertNfaVisitor final : public VNVisitor { parts.triggerExprp, flp); } - AstPropSpec* clonePropSpecWithBody(AstPropSpec* propSpecp, AstNodeExpr* bodyp) { - // Build a fresh PropSpec; a temporal body is not cloneTreePure-able. - AstPropSpec* const clonep = new AstPropSpec{ - propSpecp->fileline(), propSpecp->sensesp()->cloneTree(true), - propSpecp->disablep() ? propSpecp->disablep()->cloneTreePure(false) : nullptr, bodyp}; - clonep->dtypeFrom(propSpecp); - return clonep; - } - - AstNodeExpr* outcomeCount(std::vector& srcs, - AstNodeExpr* additionalCountp = nullptr) { - AstNodeDType* const u32p = m_modp->findBasicDType(VBasicDTypeKwd::UINT32); - AstNodeExpr* countp = additionalCountp; - for (AstNodeExpr* const srcp : srcs) { - AstCond* const oneIfp - = new AstCond{srcp->fileline(), srcp, - new AstConst{srcp->fileline(), AstConst::WidthedValue{}, 32, 1}, - new AstConst{srcp->fileline(), AstConst::WidthedValue{}, 32, 0}}; - oneIfp->dtypeFrom(u32p); - if (countp) { - AstAdd* const addp = new AstAdd{srcp->fileline(), countp, oneIfp}; - addp->dtypeFrom(u32p); - countp = addp; - } else { - countp = oneIfp; + // Install pass-action gating and replay simultaneous per-thread failures. + void attachActionHandlers(AstAssert* const assertp, AstNodeExpr* matchExprp, + AstSenTree* const threadFailReplaySenTreep, + AstNodeExpr* const threadFailCountp) { + // Gate pass handler on match to prevent vacuous-pass firings. + if (matchExprp) { + // needMatch implies passsp() was non-null when evaluated above; + // lowering does not mutate the assert's pass-action between the + // two reads, so passsp() is still non-null here. + AstNode* passsp = assertp->passsp(); + UASSERT_OBJ(passsp, assertp, "needMatch set but passsp is null"); + passsp->unlinkFrBackWithNext(); + FileLine* const flp = assertp->fileline(); + AstIf* const ifp = new AstIf{flp, matchExprp, passsp, nullptr}; + assertp->addPasssp(ifp); + // Fail-handler prefix for overlapping instances (IEEE 16.12): + // fires when reject=1 && match=1 in the same cycle. + if (AstNode* const failsp = assertp->failsp()) { + failsp->addHereThisAsNext(ifp->cloneTree(false)); } } - srcs.clear(); - return countp; - } - AstNode* newDefaultFailAction(FileLine* flp) { - AstDisplay* const dispp - = new AstDisplay{flp, VDisplayType::DT_ERROR, "'assert' failed.", nullptr, nullptr}; - dispp->fmtp()->timeunit(m_modp->timeunit()); - AstNode* resultp = dispp; - if (v3Global.opt.stopFail()) resultp->addNext(new AstStop{flp, false}); - return resultp; - } - - // Module-level counter keeps the action out of a named block, for %m - AstNode* repeatAction(FileLine* flp, AstNodeExpr* countp, AstNode* actionp) { - AstNodeDType* const u32p = m_modp->findBasicDType(VBasicDTypeKwd::UINT32); - AstVar* const counterp - = new AstVar{flp, VVarType::MODULETEMP, m_actionCountNames.get(""), u32p}; - counterp->lifetime(VLifetime::STATIC_EXPLICIT); - m_modp->addStmtsp(counterp); - return V3AssertCommon::repeatLoop(flp, counterp, countp, actionp); - } - - void addStrongPendingHandler(AstAssert* assertp, AstNodeExpr* countp, AstSenTree* senTreep, - bool defaultSynthesized) { - if (!countp) return; - FileLine* const flp = assertp->fileline(); - AstNode* actionp = nullptr; - if (assertp->failsp() && !defaultSynthesized) { - actionp = assertp->failsp()->cloneTree(true); - actionp->foreachAndNext( - [senTreep](AstPast* const pastp) { pastp->sentreep(senTreep->cloneTree(false)); }); - } else if (!assertp->passsp()) { - AstDisplay* const dispp - = new AstDisplay{flp, VDisplayType::DT_ERROR, "", nullptr, nullptr}; - dispp->fmtp()->timeunit(m_modp->timeunit()); - actionp = dispp; - if (v3Global.opt.stopFail()) actionp->addNext(new AstStop{flp, false}); + if (threadFailCountp) { + UASSERT_OBJ(threadFailReplaySenTreep, assertp, + "Thread fail count missing sensitivity tree"); + AstNode* const failsp = assertp->failsp(); + FileLine* const flp = assertp->fileline(); + // IEEE 1800-2023 16.12 requires one action-block evaluation per failed + // thread. AstAssert handles the first, so replay the rest here. + AstVar* const remainingFailCountVarp + = new AstVar{flp, VVarType::BLOCKTEMP, "__VnfaRemainingFailCount", + m_modp->findBasicDType(VBasicDTypeKwd::UINT32)}; + remainingFailCountVarp->lifetime(VLifetime::AUTOMATIC_EXPLICIT); + AstBegin* const replayBlockp = new AstBegin{flp, "", remainingFailCountVarp, true}; + replayBlockp->addStmtsp( + new AstAssign{flp, new AstVarRef{flp, remainingFailCountVarp, VAccess::WRITE}, + threadFailCountp}); + AstLoop* const replayLoopp = new AstLoop{flp}; + replayLoopp->addStmtsp(new AstLoopTest{ + flp, replayLoopp, + new AstGt{flp, new AstVarRef{flp, remainingFailCountVarp, VAccess::READ}, + newTypedConstp(flp, remainingFailCountVarp->dtypep(), 1)}}); + replayLoopp->addStmtsp(newIfAssertFailOn(failsp->cloneTree(true), assertp->directive(), + assertp->userType())); + AstSub* const decrementedFailCountp + = new AstSub{flp, new AstVarRef{flp, remainingFailCountVarp, VAccess::READ}, + newTypedConstp(flp, remainingFailCountVarp->dtypep(), 1)}; + replayLoopp->addStmtsp( + new AstAssign{flp, new AstVarRef{flp, remainingFailCountVarp, VAccess::WRITE}, + decrementedFailCountp}); + replayBlockp->addStmtsp(replayLoopp); + m_modp->addStmtsp( + new AstAlways{flp, VAlwaysKwd::ALWAYS, threadFailReplaySenTreep, replayBlockp}); } - if (!actionp) { - VL_DO_DANGLING(pushDeletep(countp), countp); - return; - } - - AstIf* const failOnp - = new AstIf{flp, assertFailOnCond(flp, assertp->userType(), assertp->directive()), - repeatAction(flp, countp, actionp)}; - failOnp->isBoundsCheck(true); - failOnp->user1(true); - failOnp->user2(true); - AstIf* const assertOnp = new AstIf{ - flp, assertOnCond(flp, assertp->userType(), assertp->directive()), failOnp}; - assertOnp->isBoundsCheck(true); - assertOnp->user2(true); - m_modp->addStmtsp(new AstFinal{flp, assertOnp}); - } - - void addCountPassHandler(AstAssert* assertp, AstPropSpec* propSpecp, AstNodeExpr* countp) { - UASSERT_OBJ(assertp->passsp() && countp, assertp, "Missing counted pass action"); - FileLine* const flp = assertp->fileline(); - AstNode* const actionp = assertp->passsp()->unlinkFrBackWithNext(); - if (!assertp->failsp()) assertp->addFailsp(new AstComment{flp, ""}); - AstAssert* const handlerp = new AstAssert{ - flp, - clonePropSpecWithBody(propSpecp, - new AstNeq{flp, countp->cloneTreePure(false), - new AstConst{flp, AstConst::WidthedValue{}, 32, 0}}), - repeatAction(flp, countp->cloneTreePure(false), actionp), - nullptr, - assertp->userType(), - assertp->directive(), - assertp->name()}; - addNfaHandler(assertp, handlerp); - VL_DO_DANGLING(pushDeletep(countp), countp); - } - - void addCountVacuousPassHandler(AstAssert* assertp, AstPropSpec* propSpecp, - AstNodeExpr* countp) { - UASSERT_OBJ(assertp->passsp() && countp, assertp, "Missing counted vacuous pass action"); - FileLine* const flp = assertp->fileline(); - AstNode* const actionp = assertp->passsp()->cloneTree(true); - AstNodeExpr* const firep = new AstNeq{flp, countp->cloneTreePure(false), - new AstConst{flp, AstConst::WidthedValue{}, 32, 0}}; - AstIf* const passp - = newPassOnIf(flp, firep, repeatAction(flp, countp, actionp), assertp->userType(), - assertp->directive(), /*vacuous=*/true); - AstAssert* const handlerp = new AstAssert{ - flp, - clonePropSpecWithBody(propSpecp, new AstConst{flp, AstConst::BitTrue{}}), - passp, - nullptr, - assertp->userType(), - assertp->directive(), - assertp->name()}; - addNfaHandler(assertp, handlerp); - } - - void addCountFailHandler(AstAssert* assertp, AstPropSpec* propSpecp, AstNodeExpr* countp) { - UASSERT_OBJ(assertp->failsp() && countp, assertp, "Missing counted failure action"); - FileLine* const flp = assertp->fileline(); - AstNode* const actionp = assertp->failsp()->unlinkFrBackWithNext(); - assertp->addFailsp(new AstComment{flp, ""}); - AstAssert* const handlerp = new AstAssert{ - flp, - clonePropSpecWithBody(propSpecp, - new AstEq{flp, countp->cloneTreePure(false), - new AstConst{flp, AstConst::WidthedValue{}, 32, 0}}), - nullptr, - repeatAction(flp, countp->cloneTreePure(false), actionp), - assertp->userType(), - assertp->directive(), - assertp->name()}; - addNfaHandler(assertp, handlerp); - VL_DO_DANGLING(pushDeletep(countp), countp); - } - - void setCoverCount(AstCover* coverp, AstPropSpec* propSpecp, AstNodeExpr* outputExprp, - AstNodeExpr* countp) { - UASSERT_OBJ(countp, coverp, "Missing cover match count"); - FileLine* const flp = coverp->fileline(); - AstNode* const innerp = propSpecp->propp(); - innerp->replaceWith(new AstNeq{flp, countp->cloneTreePure(false), - new AstConst{flp, AstConst::WidthedValue{}, 32, 0}}); - VL_DO_DANGLING(pushDeletep(innerp), innerp); - if (AstCoverInc* const incp = VN_CAST(coverp->coverincsp(), CoverInc)) { - incp->multiplicityp(countp->cloneTreePure(false)); - } - VL_DO_DANGLING(outputExprp->deleteTree(), outputExprp); - VL_DO_DANGLING(pushDeletep(countp), countp); - } - - void splitCoverOutcomes(AstCover* coverp, AstNodeExpr* outputExprp, - std::vector& outcomeSrcs, - std::vector& outcomeCounts) { - UASSERT_OBJ(!outcomeSrcs.empty(), coverp, "Cover split without outcome source"); - UASSERT_OBJ(outcomeCounts.size() == outcomeSrcs.size(), coverp, - "Cover split count channel out of step"); - std::vector coverList; - coverList.push_back(coverp); - for (size_t i = 1; i < outcomeSrcs.size(); ++i) { - AstCover* const clonep = coverp->cloneTree(false); - coverp->addNextHere(clonep); - coverList.push_back(clonep); - } - for (size_t i = 0; i < outcomeSrcs.size(); ++i) { - AstPropSpec* const clonePropSpecp = VN_CAST(coverList[i]->propp(), PropSpec); - AstNode* const innerp = clonePropSpecp->propp(); - innerp->replaceWith(outcomeSrcs[i]); - VL_DO_DANGLING(pushDeletep(innerp), innerp); - if (AstNodeExpr* const countp = outcomeCounts[i]) { - if (AstCoverInc* const incp = VN_CAST(coverList[i]->coverincsp(), CoverInc)) { - incp->multiplicityp(countp); - } else { - VL_DO_DANGLING(countp->deleteTree(), outcomeCounts[i]); - } - } - } - outcomeSrcs.clear(); - outcomeCounts.clear(); - VL_DO_DANGLING(outputExprp->deleteTree(), outputExprp); } // Replace one VarRef to a captured local var with $past(rhs, K) @@ -4408,62 +3197,6 @@ class AssertNfaVisitor final : public VNVisitor { return false; } - struct ProcState final { - AstNodeCoverOrAssert* assertp = nullptr; // Assertion being lowered - AstPropSpec* propSpecp = nullptr; // Its property spec - FileLine* flp = nullptr; // Assertion file line - bool isCover = false; // cover directive - bool isCoverSeq = false; // cover sequence directive - bool isSeqEvent = false; // Sequence used as an event control - AstCover* coverp = nullptr; // Cover directive, else nullptr - PropertyParts parts; // Antecedent/consequent split - AstNodeExpr* seqBodyp = nullptr; // Body under any leading not - bool negated = false; // Odd number of leading not - const char* propertyControlp = nullptr; // Unsupported if/case context, else nullptr - std::vector abortSpecs; // Peeled top-level aborts - AstSenTree* senTreep = nullptr; // Owned clock sensitivity tree - AstNodeExpr* disableExprp = nullptr; // disable iff expression - AstNodeExpr* outputExprp = nullptr; // Materialized verdict - bool countNegatedOutcomes = false; // Swap pass/fail counts under not - bool countNegatedPasssp = false; // Negated assert with a pass action - bool countNegatedFailsp = false; // Negated assert with a fail action - bool countNegatedCover = false; // Negated cover property - bool splitImplicationPasssp = false; // Vacuous/nonvacuous pass split - bool perAttemptPasssp = false; // Pass action counted per attempt - bool defaultFailSynthesized = false; // Default fail action added here - bool needPerSrcFail = false; // Per-depth failure sources requested - bool needPerSrcMatch = false; // Per-depth match sources requested - bool needAbortPassCount = false; // Forced-accept count requested - bool needAbortFailCount = false; // Forced-reject count requested - std::vector failAttemptSrcs; // Per-depth failure outcomes - std::vector matchAttemptSrcs; // Per-depth match outcomes - std::vector perMidSrcs; // Per-end cover sequence signals - std::vector perMidCounts; // Ring occupants per perMidSrcs entry, or null - AstNodeExpr* additionalFailCountp = nullptr; // Extra dynamically counted failures - AstNodeExpr* matchCountp = nullptr; // Extra range-ring match multiplicity - AstNodeExpr* abortPassCountp = nullptr; // Forced-accept attempt count - AstNodeExpr* abortFailCountp = nullptr; // Forced-reject attempt count - AstNodeExpr* abortAnyp = nullptr; // Any abort fired this evaluation - AstNodeExpr* strongPendingCountp = nullptr; // End-of-sim pending attempts - AstNodeExpr* passCountp = nullptr; // Final pass action multiplicity - AstNodeExpr* failCountp = nullptr; // Final fail action multiplicity - }; - - // Aborts peeled here are counted exactly; other shapes stay in the builder. - static bool canPeelAborts(const AstNodeCoverOrAssert* assertp, const AstNodeExpr* bodyp, - const std::vector& abortSpecs) { - if (abortSpecs.empty()) return true; - if (hasMultiCycleExpr(bodyp)) { - if (VN_IS(assertp, Cover)) return false; - if (bodyp->exists([](const AstAbortOn*) { return true; })) return false; - if (std::any_of(abortSpecs.begin(), abortSpecs.end(), - [](const AbortSpec& spec) { return spec.kind.isAsync(); })) { - return false; - } - } - return isLinearAbortBody(const_cast(bodyp)); - } - // 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, @@ -4472,379 +3205,25 @@ class AssertNfaVisitor final : public VNVisitor { if (negated) return "negation"; if (VN_IS(assertp, Cover)) return "cover"; if (VN_AS(assertp, Assert)->passsp()) return "a pass action"; - if (seqBodyp->exists([](const AstAbortOn*) { return true; })) return "an abort operator"; - if (seqBodyp->exists([](const AstPropAlways* alwaysp) { return alwaysp->isStrong(); })) { - return "a strong end-of-trace obligation"; - } return nullptr; } - bool prepareConcurrentAssertion(ProcState& s) { - AstNodeCoverOrAssert* const assertp = s.assertp; - s.coverp = VN_CAST(assertp, Cover); - s.isCover = s.coverp != nullptr; - s.isCoverSeq = s.coverp && s.coverp->isCoverSeq(); - s.isSeqEvent = s.coverp && s.coverp->isSeqEvent(); - AstNode* const propp = assertp->propp(); - AstPropSpec* const propSpecp = s.propSpecp = VN_CAST(assertp->propp(), PropSpec); - UASSERT_OBJ(propSpecp, assertp, "Concurrent assertion must have PropSpec"); - AstNodeExpr* decompositionRootp = VN_AS(propSpecp->propp(), NodeExpr); - s.abortSpecs = peelAbortPrefix(decompositionRootp); - if (!canPeelAborts(assertp, decompositionRootp, s.abortSpecs)) { - s.abortSpecs.clear(); - decompositionRootp = VN_AS(propSpecp->propp(), NodeExpr); - } - s.parts = decomposeProperty(decompositionRootp); - PropertyParts& parts = s.parts; - UASSERT_OBJ(parts.seqExprp, propp, "Property body must be an expression"); - - AstNodeExpr*& seqBodyp = s.seqBodyp; - seqBodyp = parts.seqExprp; - bool& negated = s.negated; - negated = false; - while (AstLogNot* const notp = VN_CAST(seqBodyp, LogNot)) { - if (!hasMultiCycleExpr(notp->lhsp())) break; - negated = !negated; - seqBodyp = notp->lhsp(); - } - if (negated && parts.hasImplication && !canSplitImplicationPassActions(parts)) { - if (s.isCover || VN_AS(s.assertp, Assert)->passsp()) { - seqBodyp->v3warn( - E_UNSUPPORTED, - "Unsupported: temporal implication antecedent with a negated consequent " - "and a pass or cover action cannot preserve attempt identity"); - replaceBodyOnBuildError(s.assertp->fileline(), propSpecp, - /*errorEmitted=*/true); - return false; - } - } - s.propertyControlp = unsupportedPropertyControl(assertp, seqBodyp, negated); - if (liftMatchItemSubstitutions(parts, seqBodyp)) { - replaceBodyOnBuildError(assertp->fileline(), propSpecp, /*errorEmitted=*/true); - return false; - } - - if (!propSpecp->sensesp() && m_defaultClockingp) { - propSpecp->sensesp(m_defaultClockingp->sensesp()->cloneTree(true)); - } - if (!propSpecp->disablep() && m_defaultDisablep && !s.isSeqEvent) { - propSpecp->disablep(m_defaultDisablep->condp()->cloneTreePure(true)); - } - if (!propSpecp->sensesp()) return false; - AstSenTree*& senTreep = s.senTreep; - senTreep = new AstSenTree{propSpecp->fileline(), propSpecp->sensesp()->cloneTree(true)}; - s.disableExprp = propSpecp->disablep(); - - // NFA lowering clones repeated operands and may hoist them into an - // always_comb block. Resolve implicit sampled-value clocks first, while - // the enclosing assertion clock is still available. - { - VL_RESTORER(m_sampledValueClockp); - m_sampledValueClockp = senTreep; - iterate(propSpecp->propp()); - } - - s.flp = assertp->fileline(); - return true; - } - - void planOutcomeChannels(ProcState& s) { - AstNodeCoverOrAssert* const assertp = s.assertp; - const PropertyParts& parts = s.parts; - const bool negated = s.negated; - const bool isCover = s.isCover; - const bool isCoverSeq = s.isCoverSeq; - const std::vector& abortSpecs = s.abortSpecs; - const AstAssert* const assertAssertp = VN_CAST(assertp, Assert); - const bool hasPass = assertAssertp && assertAssertp->passsp(); - const bool hasFail = assertAssertp && assertAssertp->failsp(); - // Negated-consequent failure multiplicity is independent of vacuous-pass splitting. - s.countNegatedOutcomes = negated; - s.countNegatedPasssp = negated && hasPass; - s.countNegatedFailsp = negated && hasFail; - s.countNegatedCover = negated && isCover; - s.splitImplicationPasssp - = hasPass && parts.hasImplication && !negated && canSplitImplicationPassActions(parts); - s.perAttemptPasssp = hasPass && !parts.hasImplication && !negated; - s.needPerSrcFail = (!negated && hasFail) || s.countNegatedPasssp || s.countNegatedCover; - s.needPerSrcMatch = s.perAttemptPasssp || s.splitImplicationPasssp - || (isCover && !isCoverSeq && !negated) || s.countNegatedFailsp; - s.needAbortPassCount = !abortSpecs.empty() && (hasPass || isCover); - s.needAbortFailCount = !abortSpecs.empty() && hasFail; - } - - void replaceObservedDisable(AstPropSpec* propSpecp, AstNodeExpr* disableObservedp) { - if (!disableObservedp) return; - AstNodeExpr* const oldDisablep = propSpecp->disablep(); - UASSERT_OBJ(oldDisablep, propSpecp, "Observed disable without PropSpec disable"); - oldDisablep->replaceWith(disableObservedp); - VL_DO_DANGLING(pushDeletep(oldDisablep), oldDisablep); - } - - static void bindLowerResult(ProcState& s, SvaNfaLowering::LowerResult& res) { - s.outputExprp = res.outputExprp; - s.abortAnyp = res.abortAnyp; - s.additionalFailCountp = res.failCountp; - s.matchCountp = res.matchCountp; - s.abortPassCountp = res.abortPassCountp; - s.abortFailCountp = res.abortFailCountp; - s.strongPendingCountp = res.strongPendingCountp; - s.failAttemptSrcs = std::move(res.failAttemptSrcs); - s.matchAttemptSrcs = std::move(res.matchAttemptSrcs); - s.perMidSrcs = std::move(res.perMidSrcs); - s.perMidCounts = std::move(res.perMidCounts); - } - - bool lowerConcurrentAssertion(ProcState& s) { - AstNodeCoverOrAssert* const assertp = s.assertp; - AstPropSpec* const propSpecp = s.propSpecp; - FileLine* const flp = s.flp; - const bool isCover = s.isCover; - const bool isCoverSeq = s.isCoverSeq; - const bool isSeqEvent = s.isSeqEvent; - const bool negated = s.negated; - AstNodeExpr* const seqBodyp = s.seqBodyp; - PropertyParts& parts = s.parts; - AstSenTree*& senTreep = s.senTreep; - AstNodeExpr*& disableExprp = s.disableExprp; - const std::vector& abortSpecs = s.abortSpecs; - - SvaGraph graph; - SvaNfaBuilder builder{graph, m_modp, m_propTempNames, isCoverSeq, !isCover || negated, - isSeqEvent, isCover}; - - const BuildResult result = buildAssertionGraph(builder, graph, seqBodyp, parts, flp); - if (result.valid()) wireMatchAndMidSources(graph, result, flp); - if (!result.valid()) { - replaceBodyOnBuildError(flp, propSpecp, result.errorEmitted); - VL_DO_DANGLING(pushDeletep(senTreep), senTreep); - return false; - } - // After the build, so a construct the builder rejects reports itself. - if (s.propertyControlp) { - seqBodyp->v3warn(E_UNSUPPORTED, - "Unsupported: temporal property if/case with " << s.propertyControlp); - replaceBodyOnBuildError(flp, propSpecp, /*errorEmitted=*/true); - VL_DO_DANGLING(pushDeletep(senTreep), senTreep); - if (result.finalCondp && !result.finalCondp->backp()) pushDeletep(result.finalCondp); - return false; - } - - AstNodeExpr* const normalizedDisablep - = disableExprp ? normalizeDisableExpr(disableExprp, senTreep) : nullptr; - const DisableVars disableVars = createDisableCounterMechanism(flp, normalizedDisablep); - AstVar* const disableCntVarp = disableVars.cntp; - AstVar* const snapshotVarp = disableVars.snapp; - - AstAssert* const assertWithFailp = VN_CAST(assertp, Assert); - // Synthesize the default fail action before planning counts, like an explicit else. - if (assertWithFailp && !assertWithFailp->passsp() && !assertWithFailp->failsp()) { - assertWithFailp->addFailsp(newDefaultFailAction(flp)); - s.defaultFailSynthesized = true; - } - planOutcomeChannels(s); - - AstNodeExpr* const alwaysTriggerp - = isSeqEvent ? new AstConst{flp, AstConst::BitTrue{}} - : assertOnCond(flp, assertp->userType(), assertp->directive()); - SvaNfaLowering::LowerRequest req; - req.triggerExprp = alwaysTriggerp; - req.senTreep = senTreep; - req.matchCondp = result.finalCondp; - req.disableExprp = normalizedDisablep ? normalizedDisablep->cloneTreePure(false) : nullptr; - req.abortSpecsp = abortSpecs.empty() ? nullptr : &abortSpecs; - req.disableCntVarp = disableCntVarp; - req.snapshotVarp = snapshotVarp; - req.isCover = isCover; - req.negated = negated; - req.assertType = isSeqEvent ? VAssertType{VAssertType::INTERNAL} : assertp->userType(); - req.directiveType = isSeqEvent ? VAssertDirectiveType{VAssertDirectiveType::INTERNAL} - : assertp->directive(); - req.wantPerSrcFail = s.needPerSrcFail; - req.pruneSingleFailSource = s.defaultFailSynthesized && !negated && abortSpecs.empty(); - req.wantPerSrcMatch = s.needPerSrcMatch; - req.wantAbortPassCount = s.needAbortPassCount; - req.wantAbortFailCount = s.needAbortFailCount; - req.wantStrongPending = assertWithFailp != nullptr; - req.wantPerMid = isCoverSeq; - SvaNfaLowering::LowerResult res = m_loweringp->lower(flp, graph, req); - bindLowerResult(s, res); - AstNodeExpr* const disableObservedp = res.disableRefp; - - if (assertWithFailp) { - addStrongPendingHandler(assertWithFailp, s.strongPendingCountp, senTreep, - s.defaultFailSynthesized); - } - - VL_DO_DANGLING(pushDeletep(alwaysTriggerp), alwaysTriggerp); - if (normalizedDisablep) { - VL_DO_DANGLING(normalizedDisablep->deleteTree(), normalizedDisablep); - } - VL_DO_DANGLING(pushDeletep(senTreep), senTreep); - replaceObservedDisable(propSpecp, disableObservedp); - if (result.finalCondp && !result.finalCondp->backp()) pushDeletep(result.finalCondp); - if (dumpGraphLevel() >= 6) graph.m_graph.dumpDotFilePrefixed("assert-nfa"); - assertp->nfaLowered(true); - return true; - } - - void finalizeOutcomeCounts(ProcState& s) { - FileLine* const flp = s.flp; - const bool needPerSrcMatch = s.needPerSrcMatch; - const bool needPerSrcFail = s.needPerSrcFail; - const bool countNegatedOutcomes = s.countNegatedOutcomes; - const bool countNegatedPasssp = s.countNegatedPasssp; - const bool countNegatedCover = s.countNegatedCover; - std::vector& matchAttemptSrcs = s.matchAttemptSrcs; - std::vector& failAttemptSrcs = s.failAttemptSrcs; - AstNodeExpr*& matchCountp = s.matchCountp; - AstNodeExpr*& additionalFailCountp = s.additionalFailCountp; - AstNodeExpr*& abortPassCountp = s.abortPassCountp; - AstNodeExpr*& abortFailCountp = s.abortFailCountp; - AstNodeExpr*& passCountp = s.passCountp; - AstNodeExpr*& failCountp = s.failCountp; - - if (needPerSrcMatch) { - passCountp = outcomeCount(matchAttemptSrcs, matchCountp); - matchCountp = nullptr; - } - if (needPerSrcFail) failCountp = outcomeCount(failAttemptSrcs, additionalFailCountp); - additionalFailCountp = nullptr; - - if (countNegatedOutcomes) std::swap(passCountp, failCountp); - - failCountp = addOutcomeCounts(flp, failCountp, abortFailCountp); - abortFailCountp = nullptr; - if (s.isCover) { - passCountp = addOutcomeCounts(flp, passCountp, abortPassCountp); - abortPassCountp = nullptr; - } - - if (countNegatedOutcomes && (countNegatedPasssp || countNegatedCover) && !passCountp) { - passCountp = new AstConst{flp, AstConst::WidthedValue{}, 32, 0}; - } - } - - AstNodeExpr* addOutcomeCounts(FileLine* flp, AstNodeExpr* lhsp, AstNodeExpr* rhsp) { - if (!lhsp) return rhsp; - if (!rhsp) return lhsp; - AstAdd* const addp = new AstAdd{flp, lhsp, rhsp}; - addp->dtypeFrom(m_modp->findBasicDType(VBasicDTypeKwd::UINT32)); - return addp; - } - - void installActionHandlers(ProcState& s) { - AstNodeCoverOrAssert* const assertp = s.assertp; - AstPropSpec* const propSpecp = s.propSpecp; - PropertyParts& parts = s.parts; - const bool countNegatedOutcomes = s.countNegatedOutcomes; - const bool countNegatedPasssp = s.countNegatedPasssp; - const bool countNegatedFailsp = s.countNegatedFailsp; - const bool perAttemptPasssp = s.perAttemptPasssp; - const bool splitImplicationPasssp = s.splitImplicationPasssp; - AstNodeExpr*& passCountp = s.passCountp; - AstNodeExpr*& failCountp = s.failCountp; - AstNodeExpr*& abortAnyp = s.abortAnyp; - AstAssert* const assertAssertp = VN_CAST(assertp, Assert); - AstAssert* const assertWithFailp = VN_CAST(assertp, Assert); - - if (countNegatedOutcomes && assertAssertp) { - if (countNegatedPasssp) { - UASSERT_OBJ(passCountp, assertAssertp, - "Negated pass action requested without a reject count"); - if (parts.hasImplication) { - splitImplicationPassActions(assertAssertp, propSpecp, parts, passCountp, - abortAnyp); - } else { - addCountPassHandler(assertAssertp, propSpecp, passCountp); - } - passCountp = nullptr; - } - if (countNegatedFailsp) { - UASSERT_OBJ(failCountp, assertWithFailp, - "Negated failure action requested without a match count"); - addCountFailHandler(assertWithFailp, propSpecp, failCountp); - failCountp = nullptr; - } - } else if (perAttemptPasssp) { - UASSERT_OBJ(passCountp, assertAssertp, "Pass action requested without a match count"); - addCountPassHandler(assertAssertp, propSpecp, passCountp); - passCountp = nullptr; - if (failCountp) { - addCountFailHandler(assertWithFailp, propSpecp, failCountp); - failCountp = nullptr; - } - } else if (splitImplicationPasssp) { - UASSERT_OBJ(passCountp, assertAssertp, - "Implication pass action requested without a match count"); - splitImplicationPassActions(assertAssertp, propSpecp, parts, passCountp, abortAnyp); - passCountp = nullptr; - if (failCountp) { - addCountFailHandler(assertWithFailp, propSpecp, failCountp); - failCountp = nullptr; - } - } else if (failCountp) { - addCountFailHandler(assertWithFailp, propSpecp, failCountp); - failCountp = nullptr; - } - } - - void installOutcomeHandlers(ProcState& s) { - AstNodeCoverOrAssert* const assertp = s.assertp; - AstPropSpec* const propSpecp = s.propSpecp; - const bool isCover = s.isCover; - const bool isCoverSeq = s.isCoverSeq; - AstCover* const coverp = s.coverp; - AstNodeExpr* const outputExprp = s.outputExprp; - std::vector& perMidSrcs = s.perMidSrcs; - AstNodeExpr*& passCountp = s.passCountp; - AstNodeExpr*& failCountp = s.failCountp; - AstNodeExpr*& matchCountp = s.matchCountp; - AstNodeExpr*& additionalFailCountp = s.additionalFailCountp; - AstNodeExpr*& abortPassCountp = s.abortPassCountp; - AstNodeExpr*& abortFailCountp = s.abortFailCountp; - AstNodeExpr*& abortAnyp = s.abortAnyp; - AstAssert* const assertAssertp = VN_CAST(assertp, Assert); - - if (isCoverSeq) { - splitCoverOutcomes(coverp, outputExprp, perMidSrcs, s.perMidCounts); - } else if (isCover) { - UASSERT_OBJ(passCountp, coverp, "Cover requested without a match count"); - setCoverCount(coverp, propSpecp, outputExprp, passCountp); - passCountp = nullptr; - } else { - AstNode* const innerPropp = propSpecp->propp(); - innerPropp->replaceWith(outputExprp); - VL_DO_DANGLING(pushDeletep(innerPropp), innerPropp); - } - - if (abortPassCountp) { - UASSERT_OBJ(assertAssertp, assertp, "Forced-accept pass count without an assert"); - addCountVacuousPassHandler(assertAssertp, propSpecp, abortPassCountp); - abortPassCountp = nullptr; - } - - installActionHandlers(s); - - UASSERT_OBJ(!passCountp && !failCountp && !matchCountp && !additionalFailCountp - && !abortPassCountp && !abortFailCountp, - assertp, "Outcome counts not consumed by the action handlers"); - if (abortAnyp) VL_DO_DANGLING(pushDeletep(abortAnyp), abortAnyp); - } - // Inline property/sequence refs and reject unsupported shapes. // Returns the PropSpec to lower, or nullptr when fully handled here. AstPropSpec* prepareAssertionProp(AstNodeCoverOrAssert* assertp) { - AstPropSpec* const specp = VN_AS(assertp->propp(), PropSpec); - if (AstFuncRef* const funcrefp = VN_CAST(specp->propp(), FuncRef)) { - if (const AstProperty* const propyp = VN_CAST(funcrefp->taskp(), Property)) { - inlineNamedProperty(specp, funcrefp, propyp); + 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)) { + inlineNamedProperty(specp, funcrefp, propyp); + } } } inlineAllSequenceRefs(assertp->propp()); - if (hoistClockedSeq(VN_AS(assertp->propp(), PropSpec))) return nullptr; + if (AstPropSpec* const specp = VN_CAST(assertp->propp(), PropSpec)) { + if (hoistClockedSeq(specp)) return nullptr; + } AstPropSpec* const propp = VN_AS(assertp->propp(), PropSpec); if (!VN_IS(assertp, Cover) @@ -4868,18 +3247,139 @@ class AssertNfaVisitor final : public VNVisitor { return propp; } - // Entry point: prepare, build the NFA, lower, and install the action handlers. void processAssertion(AstNodeCoverOrAssert* assertp) { if (assertp->immediate()) return; - if (!prepareAssertionProp(assertp)) return; - ProcState s; - s.assertp = assertp; - if (!prepareConcurrentAssertion(s)) return; - if (!lowerConcurrentAssertion(s)) return; - finalizeOutcomeCounts(s); - installOutcomeHandlers(s); - UINFO(4, "NFA converted assertion at " << s.flp << endl); + AstPropSpec* const propp = prepareAssertionProp(assertp); + if (!propp) return; + + PropertyParts parts = decomposeProperty(propp); + UASSERT_OBJ(parts.seqExprp, propp, "Property body must be an expression"); + + // Unwrap `not` (IEEE 1800-2023 16.12.1); odd count -> negated semantics. + AstNodeExpr* seqBodyp = parts.seqExprp; + bool negated = false; + while (AstLogNot* const notp = VN_CAST(seqBodyp, LogNot)) { + negated = !negated; + 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)) { + AstPropSpec* const psp = VN_CAST(assertp->propp(), PropSpec); + UASSERT_OBJ(psp, assertp, "Concurrent assertion must have PropSpec"); + replaceBodyOnBuildError(assertp->fileline(), psp, /*errorEmitted=*/true); + return; + } + + AstCover* const coverp = VN_CAST(assertp, Cover); + const bool isCoverSeq = coverp && coverp->isCoverSeq(); + // A sequence event control is not an assertion directive; no default + // disable iff, no assertion control + const bool isSeqEvent = coverp && coverp->isSeqEvent(); + // Inherit module defaults (IEEE 14.12, 16.15) when assertion has none. + if (!propp->sensesp() && m_defaultClockingp) { + propp->sensesp(m_defaultClockingp->sensesp()->cloneTree(true)); + } + if (!propp->disablep() && m_defaultDisablep && !isSeqEvent) { + propp->disablep(m_defaultDisablep->condp()->cloneTreePure(true)); + } + if (!propp->sensesp()) return; + AstSenTree* senTreep + = new AstSenTree{propp->fileline(), propp->sensesp()->cloneTree(true)}; + AstNodeExpr* disableExprp = propp->disablep(); + + // NFA lowering clones repeated operands and may hoist them into an + // always_comb block. Resolve implicit sampled-value clocks first, while + // the enclosing assertion clock is still available. + { + VL_RESTORER(m_sampledValueClockp); + m_sampledValueClockp = senTreep; + iterate(propp->propp()); + } + + FileLine* const flp = assertp->fileline(); + + SvaGraph graph; + SvaNfaBuilder builder{graph, m_modp, m_propTempNames, + isCoverSeq, isSeqEvent, coverp != nullptr}; + + const BuildResult result = buildAssertionGraph(builder, graph, seqBodyp, parts, flp); + if (result.valid()) wireMatchAndMidSources(graph, result, flp); + if (!result.valid()) { + // Fall through to V3AssertPre for unsupported constructs; only + // replace the body on real semantic errors. Any hoisted temps + // from this attempt become orphan MODULETEMPs; V3Dead removes + // them along with the dead always_comb driver. + replaceBodyOnBuildError(flp, propp, result.errorEmitted); + 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; + } + + // Build succeeded. Now create snapshot mechanism for disable iff if needed. + // Done here (not before build) so failed builds don't pollute the AST. + const DisableVars disableVars + = createDisableCounterMechanism(flp, disableExprp, parts.hasImplication, propp); + AstVar* const disableCntVarp = disableVars.cntp; + AstVar* const snapshotVarp = disableVars.snapp; + const bool disableExprUnlinked = disableCntVarp && disableExprp; + + AstAssert* const assertAssertp = VN_CAST(assertp, Assert); + const bool splitImplicationPasssp = assertAssertp && assertAssertp->passsp() + && parts.hasImplication + && canSplitImplicationPassActions(parts); + const bool needMatch = assertAssertp && assertAssertp->passsp() + && (!parts.hasImplication || splitImplicationPasssp); + + const bool needsThreadFailReplay + = assertAssertp && assertAssertp->failsp() && !parts.hasImplication; + const auto signals = m_loweringp->lower( + assertp, graph, senTreep, result.finalCondp, disableExprp, disableCntVarp, + snapshotVarp, needsThreadFailReplay, needsThreadFailReplay && !negated); + AstNodeExpr* matchExprp = nullptr; + AstNodeExpr* outputExprp = m_loweringp->assembleResult( + assertp, negated, result.finalCondp, signals, needMatch ? &matchExprp : nullptr); + if (isCoverSeq) { + UASSERT_OBJ(signals.matchCountp, coverp, "Cover sequence missing match count"); + VL_DO_DANGLING(outputExprp->deleteTree(), outputExprp); + propp->matchCountp(signals.matchCountp); + outputExprp = new AstNeq{flp, signals.matchCountp->cloneTreePure(false), + newTypedConstp(flp, signals.matchCountp->dtypep(), 0)}; + } + + AstSenTree* const threadFailReplaySenTreep + = signals.threadFailCountp ? senTreep->cloneTree(false) : nullptr; + + VL_DO_DANGLING(pushDeletep(senTreep), senTreep); + if (disableExprUnlinked) VL_DO_DANGLING(pushDeletep(disableExprp), disableExprp); + if (result.finalCondp && !result.finalCondp->backp()) pushDeletep(result.finalCondp); + + if (splitImplicationPasssp) { + splitImplicationPassActions(assertAssertp, parts, matchExprp); + } else { + attachActionHandlers(assertAssertp, matchExprp, threadFailReplaySenTreep, + signals.threadFailCountp); + } + + AstNode* const innerPropp = propp->propp(); + innerPropp->replaceWith(outputExprp); + VL_DO_DANGLING(pushDeletep(innerPropp), innerPropp); + + UINFO(4, "NFA converted assertion at " << flp << endl); + + if (dumpGraphLevel() >= 6) graph.m_graph.dumpDotFilePrefixed("assert-nfa"); } // VISITORS diff --git a/src/V3AssertPre.cpp b/src/V3AssertPre.cpp index ec6b0f35c..a7a2ca464 100644 --- a/src/V3AssertPre.cpp +++ b/src/V3AssertPre.cpp @@ -675,12 +675,6 @@ private: iterateAndNextNull(nodep->stmtsp()); m_seniAlwaysp = nullptr; } - void visit(AstAlwaysObserved* nodep) override { - iterateAndNextNull(nodep->sentreep()); - VL_RESTORER(m_seniAlwaysp); - m_seniAlwaysp = nodep->sentreep()->sensesp(); - iterateAndNextNull(nodep->stmtsp()); - } void visit(AstNodeCoverOrAssert* nodep) override { if (nodep->sentreep()) return; // Already processed diff --git a/src/V3AstAttr.h b/src/V3AstAttr.h index 3855008d0..bd9d732d9 100644 --- a/src/V3AstAttr.h +++ b/src/V3AstAttr.h @@ -58,8 +58,6 @@ public: return names[m_e]; } bool isAccept() const { return m_e == ACCEPT_ON || m_e == SYNC_ACCEPT_ON; } - bool isSync() const { return m_e == SYNC_ACCEPT_ON || m_e == SYNC_REJECT_ON; } - bool isAsync() const { return !isSync(); } }; //###################################################################### diff --git a/src/V3AstNodeStmt.h b/src/V3AstNodeStmt.h index 4bbc98da2..a6ca93d3d 100644 --- a/src/V3AstNodeStmt.h +++ b/src/V3AstNodeStmt.h @@ -102,7 +102,6 @@ class AstNodeCoverOrAssert VL_NOT_FINAL : public AstNodeStmt { const VAssertDirectiveType m_directive; // Assertion directive type bool m_senFromAlways = false; // Sensitivity list copied from upper always bool m_immediate = false; // Immediate assert (may differ from userType being immediate) - bool m_nfaLowered = false; // Property was lowered by V3AssertNfa public: AstNodeCoverOrAssert(VNType t, FileLine* fl, AstNode* propp, AstNode* passsp, @@ -132,8 +131,6 @@ public: void immediate(bool flag) { m_immediate = flag; } bool senFromAlways() const VL_MT_STABLE { return m_senFromAlways; } void senFromAlways(bool flag) { m_senFromAlways = flag; } - bool nfaLowered() const { return m_nfaLowered; } - void nfaLowered(bool flag) { m_nfaLowered = flag; } }; class AstNodeForeach VL_NOT_FINAL : public AstNodeStmt { // @astgen op1 := headerp : AstForeachHeader @@ -526,7 +523,6 @@ class AstCoverInc final : public AstNodeStmt { // @astgen op1 := toggleExprp : Optional[AstNodeExpr] // [After V3Clock] // @astgen op2 := toggleCovExprp : Optional[AstNodeExpr] // [After V3Clock] // These are expressions to which the node corresponds. Used only in toggle coverage - // @astgen op3 := multiplicityp : Optional[AstNodeExpr] // NFA action count // // @astgen ptr := m_declp : AstNodeCoverDecl // [After V3CoverageJoin] Declaration public: diff --git a/src/V3AstNodes.cpp b/src/V3AstNodes.cpp index 706fd07bd..0a53d7aa6 100644 --- a/src/V3AstNodes.cpp +++ b/src/V3AstNodes.cpp @@ -2308,14 +2308,12 @@ void AstNodeCoverOrAssert::dump(std::ostream& str) const { str << " ["s + this->userType().ascii() + "]"; if (immediate()) str << " [IMMEDIATE]"; if (senFromAlways()) str << " [SENALW]"; - if (nfaLowered()) str << " [NFA]"; } void AstNodeCoverOrAssert::dumpJson(std::ostream& str) const { dumpJsonStr(str, "type", "["s + this->userType().ascii() + "]"); dumpJsonGen(str); dumpJsonBoolFuncIf(str, immediate); dumpJsonBoolFuncIf(str, senFromAlways); - dumpJsonBoolFuncIf(str, nfaLowered); } void AstCover::dump(std::ostream& str) const { this->AstNodeCoverOrAssert::dump(str); diff --git a/src/V3Delayed.cpp b/src/V3Delayed.cpp index 30a00960f..f2ed71ebc 100644 --- a/src/V3Delayed.cpp +++ b/src/V3Delayed.cpp @@ -154,7 +154,6 @@ class DelayedVisitor final : public VNVisitor { bool m_partial = false; // Used on LHS of NBA under a Sel bool m_inLoop = false; // Used on LHS of NBA in a loop bool m_inSuspOrFork = false; // Used on LHS of NBA in suspendable process or fork - bool m_hasReactiveNba = false; // Updated by an NBA from the Reactive region Scheme m_scheme = Scheme::Undecided; // Conversion scheme to use for this variable uint32_t m_nTmp = 0; // Temporary number for unique names @@ -166,7 +165,6 @@ class DelayedVisitor final : public VNVisitor { union { struct { // Stuff needed for Scheme::ShadowVar AstVarScope* vscp; // The shadow variable - AstVarScope* pendingp; // Reactive capture awaiting Re-NBA commit } m_shadowVariableKit; struct { // Stuff needed for Scheme::ShadowVarMasked AstVarScope* vscp; // The shadow variable @@ -246,7 +244,6 @@ class DelayedVisitor final : public VNVisitor { struct NBA final { AstAssignDly* nodep = nullptr; // The NBA this record refers to AstVarScope* vscp = nullptr; // The target variable the NBA is updating - bool fromReactive = false; // The NBA executes in the Reactive region }; // NODE STATE @@ -446,15 +443,10 @@ class DelayedVisitor final : public VNVisitor { return Scheme::ShadowVar; } - const bool isIntegralOrPacked = dtypep->isIntegralOrPacked(); - // Repeated Reactive partial NBAs need an accumulating mask, not one unique LHS. - if (vscpInfo.m_inSuspOrFork) { - if (vscpInfo.m_hasReactiveNba && vscpInfo.m_partial && isIntegralOrPacked) { - return Scheme::ShadowVarMasked; - } - return Scheme::FlagUnique; - } + // In a suspendable of fork, we must use the unique flag scheme, TODO: why? + if (vscpInfo.m_inSuspOrFork) return Scheme::FlagUnique; + const bool isIntegralOrPacked = dtypep->isIntegralOrPacked(); // Check for mixed usage (this also warns if not OK) if (checkMixedUsage(vscp, isIntegralOrPacked)) { // If it's a variable updated by both blocking and non-blocking @@ -503,35 +495,6 @@ class DelayedVisitor final : public VNVisitor { return createTemp(flp, scopep, name, dtypep); } - // Add the global NBA event to a Reactive NBA target's commit domain. - void prepareReactiveNbaEvent(AstNetlist* netlistp, AstVarScope* vscp, VarScopeInfo& vscpInfo) { - UASSERT_OBJ(vscpInfo.m_hasReactiveNba, vscp, "Variable has no Reactive NBA"); - FileLine* const flp = vscp->fileline(); - AstScope* const topScopep = netlistp->topScopep()->scopep(); - if (!netlistp->nbaEventp()) { - AstBasicDType* const dtypep - = new AstBasicDType{flp, VBasicDTypeKwd::EVENT, VSigning::UNSIGNED}; - netlistp->typeTablep()->addTypesp(dtypep); - netlistp->nbaEventp(topScopep->createTemp("__VnbaEvent", dtypep)); - } - if (!netlistp->nbaEventTriggerp()) { - netlistp->nbaEventTriggerp(topScopep->createTemp("__VnbaEventTrigger", 1)); - } - v3Global.setHasEvents(); - AstSenItem* const senItemp = new AstSenItem{ - flp, VEdgeType::ET_EVENT, new AstVarRef{flp, netlistp->nbaEventp(), VAccess::READ}}; - vscpInfo.addSensitivity(senItemp); - VL_DO_DANGLING(senItemp->deleteTree(), senItemp); - } - - // Request another NBA iteration after capturing an NBA in the Reactive region. - AstNodeStmt* newReactiveNbaTrigger(FileLine* flp) { - AstVarScope* const triggerp = v3Global.rootp()->nbaEventTriggerp(); - UASSERT(triggerp, "Reactive NBA event not prepared"); - return new AstAssign{flp, new AstVarRef{flp, triggerp, VAccess::WRITE}, - new AstConst{flp, AstConst::BitTrue{}}}; - } - // Given an expression 'exprp', return a new expression that always evaluates to the // value of the given expression at this point in the program. That is: // - If given a non-constant expression, create a new temporary AstVarScope under the given @@ -631,12 +594,6 @@ class DelayedVisitor final : public VNVisitor { const std::string name = "__Vdly__" + vscp->varp()->shortName(); AstVarScope* const shadowVscp = createTemp(flp, scopep, name, vscp->dtypep()); vscpInfo.shadowVariableKit().vscp = shadowVscp; - AstVarScope* pendingVscp = nullptr; - if (vscpInfo.m_hasReactiveNba) { - pendingVscp = createTemp(flp, scopep, name + "__Vpending", 1); - pendingVscp->varp()->setIgnorePostWrite(); - } - vscpInfo.shadowVariableKit().pendingp = pendingVscp; // Mark both for V3LifePsot vscp->optimizeLifePost(true); shadowVscp->optimizeLifePost(true); @@ -647,27 +604,15 @@ class DelayedVisitor final : public VNVisitor { // Add 'Pre' scheduled 'shadowVariable = originalVariable' assignment AstAlwaysPre* const prep = new AstAlwaysPre{flp}; activep->addStmtsp(prep); - AstNodeStmt* const initp - = new AstAssign{flp, new AstVarRef{flp, shadowVscp, VAccess::WRITE}, - new AstVarRef{flp, vscp, VAccess::READ}}; - prep->addStmtsp( - pendingVscp - ? static_cast(new AstIf{ - flp, new AstLogNot{flp, new AstVarRef{flp, pendingVscp, VAccess::READ}}, - initp}) - : initp); + prep->addStmtsp(new AstAssign{flp, new AstVarRef{flp, shadowVscp, VAccess::WRITE}, + new AstVarRef{flp, vscp, VAccess::READ}}); // Add 'Post' scheduled 'originalVariable = shadowVariable' assignment AstAlwaysPost* const postp = new AstAlwaysPost{flp}; activep->addStmtsp(postp); postp->addStmtsp(new AstAssign{flp, new AstVarRef{flp, vscp, VAccess::WRITE}, new AstVarRef{flp, shadowVscp, VAccess::READ}}); - if (pendingVscp) { - postp->addStmtsp(new AstAssign{flp, new AstVarRef{flp, pendingVscp, VAccess::WRITE}, - new AstConst{flp, AstConst::BitFalse{}}}); - } } - void convertSchemeShadowVar(AstAssignDly* nodep, AstVarScope* vscp, VarScopeInfo& vscpInfo, - bool fromReactive) { + void convertSchemeShadowVar(AstAssignDly* nodep, AstVarScope* vscp, VarScopeInfo& vscpInfo) { UASSERT_OBJ(vscpInfo.m_scheme == Scheme::ShadowVar, vscp, "Inconsistent NBA scheme"); AstVarScope* const shadowVscp = vscpInfo.shadowVariableKit().vscp; @@ -678,16 +623,6 @@ class DelayedVisitor final : public VNVisitor { refp->varScopep(shadowVscp); refp->varp(shadowVscp->varp()); }); - if (fromReactive) { - AstVarScope* const pendingp = vscpInfo.shadowVariableKit().pendingp; - UASSERT_OBJ(pendingp, nodep, "Reactive shadow NBA missing pending flag"); - AstAssign* const pendingSetp = new AstAssign{ - nodep->fileline(), new AstVarRef{nodep->fileline(), pendingp, VAccess::WRITE}, - new AstConst{nodep->fileline(), AstConst::BitTrue{}}}; - AstNode::addNext(pendingSetp, - newReactiveNbaTrigger(nodep->fileline())); - nodep->addNextHere(pendingSetp); - } } // Scheme::ShadowVarMasked @@ -727,26 +662,12 @@ class DelayedVisitor final : public VNVisitor { new AstConst{flp, AstConst::WidthedValue{}, maskVscp->width(), 0}}); } void convertSchemeShadowVarMasked(AstAssignDly* nodep, AstVarScope* vscp, - VarScopeInfo& vscpInfo, bool fromReactive) { + VarScopeInfo& vscpInfo) { UASSERT_OBJ(vscpInfo.m_scheme == Scheme::ShadowVarMasked, vscp, "Inconsistent NBA scheme"); AstVarScope* const shadowVscp = vscpInfo.shadowVarMaskedKit().vscp; AstVarScope* const maskVscp = vscpInfo.shadowVarMaskedKit().maskp; - AstNodeExpr* lhsClonep = nullptr; - if (fromReactive) { - AstScope* const scopep = VN_AS(nodep->user2p(), Scope); - const std::string baseName = uniqueTmpName(scopep, vscp, vscpInfo); - // A Reactive action loop re-executes the NBA; capture RHS then LHS once. - AstNodeExpr* const capturedRhsp - = captureVal(scopep, nodep, nodep->rhsp()->unlinkFrBack(), "__VdlyVal" + baseName); - nodep->rhsp(capturedRhsp); - AstNodeExpr* const capturedLhsp - = captureLhs(scopep, nodep, nodep->lhsp()->unlinkFrBack(), baseName); - lhsClonep = capturedLhsp->cloneTree(false); - nodep->lhsp(capturedLhsp); - } else { - lhsClonep = nodep->lhsp()->cloneTree(false); - } + AstNodeExpr* lhsClonep = nodep->lhsp()->cloneTree(false); // Replace the write ref on the LHS with the shadow variable nodep->lhsp()->foreach([&](AstVarRef* const refp) { @@ -765,7 +686,6 @@ class DelayedVisitor final : public VNVisitor { FileLine* const flp = nodep->fileline(); AstConst* const onesp = new AstConst{flp, AstConst::DTyped{}, lhsClonep->dtypep()}; onesp->num().setAllBits1(); - if (fromReactive) nodep->addNextHere(newReactiveNbaTrigger(flp)); nodep->addNextHere(new AstAssign{flp, lhsClonep, onesp}); } @@ -881,8 +801,7 @@ class DelayedVisitor final : public VNVisitor { activep->addStmtsp(postp); vscpInfo.flagUniqueKit().postp = postp; } - void convertSchemeFlagUnique(AstAssignDly* nodep, AstVarScope* vscp, VarScopeInfo& vscpInfo, - bool fromReactive) { + void convertSchemeFlagUnique(AstAssignDly* nodep, AstVarScope* vscp, VarScopeInfo& vscpInfo) { UASSERT_OBJ(vscpInfo.m_scheme == Scheme::FlagUnique, vscp, "Inconsistent NBA scheme"); FileLine* const flp = vscp->fileline(); AstScope* const scopep = VN_AS(nodep->user2p(), Scope); @@ -914,8 +833,6 @@ class DelayedVisitor final : public VNVisitor { // Commit the value ifp->addThensp(new AstAssign{flp, capturedLhsp, capturedRhsp}); - if (fromReactive) nodep->addHereThisAsNext(newReactiveNbaTrigger(flp)); - // Delete original NBA pushDeletep(nodep->unlinkFrBack()); } @@ -955,7 +872,7 @@ class DelayedVisitor final : public VNVisitor { } void convertSchemeValueQueue(AstAssignDly* nodep, AstVarScope* vscp, VarScopeInfo& vscpInfo, - bool partial, bool fromReactive) { + bool partial) { UASSERT_OBJ(partial ? vscpInfo.m_scheme == Scheme::ValueQueuePartial : vscpInfo.m_scheme == Scheme::ValueQueueWhole, vscp, "Inconsistent NBA scheme"); @@ -1064,7 +981,6 @@ class DelayedVisitor final : public VNVisitor { if (partial) callp->addPinsp(maskp); for (AstNodeExpr* const indexp : idxps) callp->addPinsp(indexp); nodep->addHereThisAsNext(callp->makeStmt()); - if (fromReactive) nodep->addHereThisAsNext(newReactiveNbaTrigger(flp)); // Delete original NBA pushDeletep(nodep->unlinkFrBack()); @@ -1098,7 +1014,6 @@ class DelayedVisitor final : public VNVisitor { for (AstVarScope* const vscp : m_vscps) { VarScopeInfo& vscpInfo = m_vscpInfo(vscp); vscpInfo.m_scheme = chooseScheme(vscp, vscpInfo); - if (vscpInfo.m_hasReactiveNba) prepareReactiveNbaEvent(nodep, vscp, vscpInfo); // Run 'prepare' step switch (vscpInfo.m_scheme) { case Scheme::Undecided: // LCOV_EXCL_START @@ -1158,11 +1073,11 @@ class DelayedVisitor final : public VNVisitor { break; } case Scheme::ShadowVar: { - convertSchemeShadowVar(nbap, vscp, vscpInfo, nba.fromReactive); + convertSchemeShadowVar(nbap, vscp, vscpInfo); break; } case Scheme::ShadowVarMasked: { - convertSchemeShadowVarMasked(nbap, vscp, vscpInfo, nba.fromReactive); + convertSchemeShadowVarMasked(nbap, vscp, vscpInfo); break; } case Scheme::FlagShared: { @@ -1170,17 +1085,15 @@ class DelayedVisitor final : public VNVisitor { break; } case Scheme::FlagUnique: { - convertSchemeFlagUnique(nbap, vscp, vscpInfo, nba.fromReactive); + convertSchemeFlagUnique(nbap, vscp, vscpInfo); break; } case Scheme::ValueQueueWhole: { - convertSchemeValueQueue(nbap, vscp, vscpInfo, /* partial: */ false, - nba.fromReactive); + convertSchemeValueQueue(nbap, vscp, vscpInfo, /* partial: */ false); break; } case Scheme::ValueQueuePartial: - convertSchemeValueQueue(nbap, vscp, vscpInfo, /* partial: */ true, - nba.fromReactive); + convertSchemeValueQueue(nbap, vscp, vscpInfo, /* partial: */ true); break; } } @@ -1379,7 +1292,6 @@ class DelayedVisitor final : public VNVisitor { vscpInfo.m_partial |= VN_IS(nodep->lhsp(), Sel); vscpInfo.m_inLoop |= m_inLoop; vscpInfo.m_inSuspOrFork |= m_inSuspendableOrFork; - vscpInfo.m_hasReactiveNba |= VN_IS(m_procp, AlwaysReactive); // Sensitivity might be non-clocked, in a suspendable process, which are handled elsewhere if (m_activep->sentreep()->hasClocked()) { if (vscpInfo.m_fistActivep != m_activep) { @@ -1406,7 +1318,6 @@ class DelayedVisitor final : public VNVisitor { NBA& nba = m_nbas.back(); nba.nodep = nodep; nba.vscp = vscp; - nba.fromReactive = VN_IS(m_procp, AlwaysReactive); // Record write reference recordWriteRef(m_currNbaLhsRefp, true); diff --git a/test_regress/t/t_assert_always_unbounded.v b/test_regress/t/t_assert_always_unbounded.v index 12c2f374d..1602bd74b 100644 --- a/test_regress/t/t_assert_always_unbounded.v +++ b/test_regress/t/t_assert_always_unbounded.v @@ -46,10 +46,13 @@ module t ( cyc <= cyc + 1; if (cyc >= 4) a_drop <= 1'b0; if (cyc == 19) begin + // Counts pinned to Verilator (NFA per-cycle reject). For all-fail windows + // others are one lower (it does not fire the end-of-sim tick); see the sva + // lessons "multi-cycle end-of-simulation offset" note. `checkd(high_fail_q.size(), 0); - `checkd(low0_fail_q.size(), 19); - `checkd(low2_fail_q.size(), 17); - `checkd(drop_fail_q[0], 6); + `checkd(low0_fail_q.size(), 20); // All others: 19 + `checkd(low2_fail_q.size(), 18); // All others: 17 + `checkd(drop_fail_q[0], 5); // All others: 6; first fail tick: a_drop sampled low from cyc 5 $write("*-* All Finished *-*\n"); $finish; end diff --git a/test_regress/t/t_assert_consec_rep_large.v b/test_regress/t/t_assert_consec_rep_large.v index a8e99cdbc..dbf6b7c63 100644 --- a/test_regress/t/t_assert_consec_rep_large.v +++ b/test_regress/t/t_assert_consec_rep_large.v @@ -41,7 +41,7 @@ module t ( // A blocking action counts all live threads rejected when the long run ends. assert property (@(posedge clk) cons_a [* 513]) - else if (cyc == 1101) count_fail_threads_513++; + else if (cyc == 1100) count_fail_threads_513++; // One triggered attempt makes an off-by-one ring exit observable. Consecutive // repetition sees a long run; goto repetition advances across regular gaps. diff --git a/test_regress/t/t_assert_ctl_lock.v b/test_regress/t/t_assert_ctl_lock.v index f891db64e..c47e3b341 100644 --- a/test_regress/t/t_assert_ctl_lock.v +++ b/test_regress/t/t_assert_ctl_lock.v @@ -61,7 +61,7 @@ module t; final begin `checkd(imm_fails, 5); - `checkd(conc_fails, 7); // One other sim: 1 + `checkd(conc_fails, 7); // Other sims: 7 or 1 $write("*-* All Finished *-*\n"); end endmodule diff --git a/test_regress/t/t_assert_seq_event_unsup.out b/test_regress/t/t_assert_seq_event_unsup.out index c175d6ebb..61d38b9f7 100644 --- a/test_regress/t/t_assert_seq_event_unsup.out +++ b/test_regress/t/t_assert_seq_event_unsup.out @@ -6,15 +6,6 @@ %Error-UNSUPPORTED: t/t_assert_seq_event_unsup.v:28:30: Unsupported: sequence used as an event control with a sequence operand of 'or' 28 | @(posedge clk) (a ##1 b) or (a ##2 b); | ^~ -%Error-UNSUPPORTED: t/t_assert_seq_event_unsup.v:32:34: Unsupported: sequence used as an event control with a variable/unbounded temporal 'and' - 32 | @(posedge clk) (a ##[1:2] b) and (a ##2 b); - | ^~~ -%Error-UNSUPPORTED: t/t_assert_seq_event_unsup.v:36:30: Unsupported: sequence used as an event control with a sequence operand of 'or' - 36 | @(posedge clk) (a ##1 b) or a; - | ^~ -%Error-UNSUPPORTED: t/t_assert_seq_event_unsup.v:40:22: Unsupported: sequence used as an event control with a sequence operand of 'or' - 40 | @(posedge clk) a or (a ##1 b); - | ^~ %Error-UNSUPPORTED: t/t_assert_seq_event_unsup.v:21:12: Unsupported: sequence referenced outside assertion property : ... note: In instance 't' 21 | sequence s_ref; diff --git a/test_regress/t/t_assert_seq_event_unsup.v b/test_regress/t/t_assert_seq_event_unsup.v index 2de1408e7..eda3b0772 100644 --- a/test_regress/t/t_assert_seq_event_unsup.v +++ b/test_regress/t/t_assert_seq_event_unsup.v @@ -27,22 +27,6 @@ module t ( sequence s_or; @(posedge clk) (a ##1 b) or (a ##2 b); endsequence - - sequence s_and_range; - @(posedge clk) (a ##[1:2] b) and (a ##2 b); - endsequence - - sequence s_or_boolr; - @(posedge clk) (a ##1 b) or a; - endsequence - - sequence s_or_booll; - @(posedge clk) a or (a ##1 b); - endsequence - - sequence s_or_bool; - @(posedge clk) a or b; - endsequence // verilog_format: on // Legal: p is never asserted, so s_ref stays referenced outside any @@ -54,9 +38,5 @@ module t ( initial begin @s_nonedge; @s_or; - @s_and_range; - @s_or_boolr; - @s_or_booll; - @s_or_bool; end endmodule diff --git a/test_regress/t/t_cover_property.v b/test_regress/t/t_cover_property.v index d48cef9eb..97174b434 100644 --- a/test_regress/t/t_cover_property.v +++ b/test_regress/t/t_cover_property.v @@ -74,7 +74,7 @@ module t ( `checkd(n_seq, 28); // Other sims: 45, 27 `checkd(n_seq0, 27); `checkd(n_strong, 28); - `checkd(n_bool, 55); // Other sims: pass, 25, 54 + `checkd(n_bool, 55); // Other sims: pass, 25 `checkd(n_named, 28); // Other sims: 73, 54, 54 end diff --git a/test_regress/t/t_cover_sequence.v b/test_regress/t/t_cover_sequence.v index b634ca19d..553029982 100644 --- a/test_regress/t/t_cover_sequence.v +++ b/test_regress/t/t_cover_sequence.v @@ -106,12 +106,12 @@ module t ( `checkd(hit_clocked, 149); `checkd(hit_clocked_disable, 27); `checkd(hit_default_disable, 30); - `checkd(hit_consrep_2, 29); - `checkd(hit_consrep_3, 13); // a[*1:1] == a[*1] (IEEE 1800-2023 16.9.2) - `checkd(hit_consrep_1, 54); + `checkd(hit_consrep_1, 55); `checkd(hit_consrep_equal_range, hit_consrep_1); - `checkd(hit_consrep_range_0, 152); // Counts both empty and nonempty matches; one other sim: 54 + `checkd(hit_consrep_range_0, 154); // Counts both empty and nonempty matches + `checkd(hit_consrep_2, 30); // Other sims: 29 + `checkd(hit_consrep_3, 14); // Other sims: 13 // a[*2:3] == a[*2] or a[*3] (IEEE 1800-2023 16.9.2) `checkd(hit_consrep_range, hit_consrep_2 + hit_consrep_3); `checkd(hit_consrep_unbounded_0, hit_consrep_star); diff --git a/test_regress/t/t_cover_sequence_unsup.out b/test_regress/t/t_cover_sequence_unsup.out index 7c75adf83..7850b7d99 100644 --- a/test_regress/t/t_cover_sequence_unsup.out +++ b/test_regress/t/t_cover_sequence_unsup.out @@ -1,15 +1,21 @@ -%Warning-COVERIGN: t/t_cover_sequence_unsup.v:21:21: Ignoring unsupported: cover sequence with this ranged cycle delay - 21 | cover sequence (a ##[1:2] (b ##1 c)); - | ^~ +%Warning-COVERIGN: t/t_cover_sequence_unsup.v:21:33: Ignoring unsupported: cover sequence with a sequence operand of 'or' + 21 | cover sequence ((a ##[1:3] b) or 1'b0); + | ^~ ... For warning description see https://verilator.org/warn/COVERIGN?v=latest ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. -%Warning-COVERIGN: t/t_cover_sequence_unsup.v:24:21: Ignoring unsupported: cover sequence with this ranged cycle delay - 24 | cover sequence (a ##[1:300] b); +%Warning-COVERIGN: t/t_cover_sequence_unsup.v:24:32: Ignoring unsupported: cover sequence with a sequence operand of 'or' + 24 | cover sequence ((a [* 1: 3]) or 1'b0); + | ^~ +%Warning-COVERIGN: t/t_cover_sequence_unsup.v:27:21: Ignoring unsupported: cover sequence with this ranged cycle delay + 27 | cover sequence (a ##[1:2] (b ##1 c)); | ^~ -%Warning-COVERIGN: t/t_cover_sequence_unsup.v:27:21: Ignoring unsupported: cover sequence with a goto repetition - 27 | cover sequence (a [-> 2]); +%Warning-COVERIGN: t/t_cover_sequence_unsup.v:30:21: Ignoring unsupported: cover sequence with this ranged cycle delay + 30 | cover sequence (a ##[1:300] b); + | ^~ +%Warning-COVERIGN: t/t_cover_sequence_unsup.v:33:21: Ignoring unsupported: cover sequence with a goto repetition + 33 | cover sequence (a [-> 2]); | ^~~ -%Warning-COVERIGN: t/t_cover_sequence_unsup.v:28:21: Ignoring unsupported: cover sequence with a goto repetition - 28 | cover sequence (a [-> 2: 3]); +%Warning-COVERIGN: t/t_cover_sequence_unsup.v:34:21: Ignoring unsupported: cover sequence with a goto repetition + 34 | cover sequence (a [-> 2: 3]); | ^~~ %Error: Exiting due to diff --git a/test_regress/t/t_cover_sequence_unsup.v b/test_regress/t/t_cover_sequence_unsup.v index bab9f5d87..660d0409b 100644 --- a/test_regress/t/t_cover_sequence_unsup.v +++ b/test_regress/t/t_cover_sequence_unsup.v @@ -14,13 +14,19 @@ module t ( endclocking // cover sequence (IEEE 1800-2023 16.14.3) counts every end-of-match. The - // following forms lack an exact per-end representation, so they are - // ignored (COVERIGN) rather than under-counted. + // following forms put a sub-sequence where only its final end is forwarded, + // so they are ignored (COVERIGN) rather than under-counted. + + // Sequence operand of 'or' (ranged cycle delay). + cover sequence ((a ##[1:3] b) or 1'b0); + + // Sequence operand of 'or' (consecutive repetition). + cover sequence ((a [* 1: 3]) or 1'b0); // Ranged cycle delay before a multi-cycle sequence. cover sequence (a ##[1:2] (b ##1 c)); - // Ranged cycle delay wider than the unroll limit. + // Ranged cycle delay wide enough to use the counter FSM. cover sequence (a ##[1:300] b); // Goto repetition coalesces multiple live attempts into one NFA state. diff --git a/test_regress/t/t_prop_always.v b/test_regress/t/t_prop_always.v index ca026e092..69ccda1c9 100644 --- a/test_regress/t/t_prop_always.v +++ b/test_regress/t/t_prop_always.v @@ -73,22 +73,22 @@ module t ( cyc <= cyc + 1; crc <= {crc[62:0], crc[63] ^ crc[2] ^ crc[0]}; if (cyc == 19) begin - // Constant-true window [0:3]: the finish-edge action does not execute. - `checkd(high_bounded_pass_q.size(), 16); - `checkd(high_bounded_pass_q[0], 4); + // Constant-true window [0:3]: K=0..16 succeed at cyc K+3 = 3..19. + `checkd(high_bounded_pass_q.size(), 17); // Other sims: 16 + `checkd(high_bounded_pass_q[0], 3); // Other sims: 4 `checkd(high_bounded_pass_q[$], 19); - // Reactive actions observe the post-NBA value of cyc. - `checkd(high_degenerate_pass_q.size(), 19); - `checkd(high_degenerate_pass_q[0], 1); // One other sim: 0 + // Degenerate [0:0]: K=0..19 succeed at cyc K = 0..19. + `checkd(high_degenerate_pass_q.size(), 20); // Other sims: 19 + `checkd(high_degenerate_pass_q[0], 0); // Other sims: 0, 1 `checkd(high_degenerate_pass_q[$], 19); // Constant-false: every attempt fails immediately. - `checkd(low_bounded_fail_q.size(), 19); - `checkd(low_degenerate_fail_q.size(), 19); + `checkd(low_bounded_fail_q.size(), 20); // Other sims: 19 + `checkd(low_degenerate_fail_q.size(), 20); // Other sims: 19 // CRC + disable streams `checkd(rand_bounded_pass_q.size(), 0); - `checkd(rand_bounded_fail_q.size(), 19); // One other sim: 11 + `checkd(rand_bounded_fail_q.size(), 20); // Other sims: 19, 11 `checkd(disable_bounded_pass_q.size(), 0); - `checkd(disable_bounded_fail_q.size(), 6); // One other sim: 5 + `checkd(disable_bounded_fail_q.size(), 8); // Other sims: 5, 6 $write("*-* All Finished *-*\n"); $finish; end diff --git a/test_regress/t/t_prop_always_wide.v b/test_regress/t/t_prop_always_wide.v index 9c0c8d5c1..13479dbe4 100644 --- a/test_regress/t/t_prop_always_wide.v +++ b/test_regress/t/t_prop_always_wide.v @@ -51,21 +51,22 @@ module t ( always @(posedge clk) begin cyc <= cyc + 1; if (cyc == 49) begin - `checkd(wide_pass_q.size(), 16); - `checkd(wide_pass_q[0], 34); + // Constant-true [1:33]: K=0..16 succeed at cyc K+33 = 33..49. + `checkd(wide_pass_q.size(), 17); + `checkd(wide_pass_q[0], 33); `checkd(wide_pass_q[$], 49); end if (cyc == 1041) begin - // Constant-true [1:1025]: K=0..15 succeed at cyc K+1025 and push the updated cyc. - `checkd(wide_ring_pass_q.size(), 16); - `checkd(wide_ring_pass_q[0], 1026); + // Constant-true [1:1025]: K=0..16 succeed at cyc K+1025 = 1025..1041. + `checkd(wide_ring_pass_q.size(), 17); + `checkd(wide_ring_pass_q[0], 1025); `checkd(wide_ring_pass_q[$], 1041); `checkd(wide_fail_q.size(), 1025); - `checkd(wide_fail_q[0], 1026); - `checkd(wide_fail_q[$], 1026); + `checkd(wide_fail_q[0], 1025); + `checkd(wide_fail_q[$], 1025); `checkd(narrow_fail_q.size(), 2); - `checkd(narrow_fail_q[0], 1026); - `checkd(narrow_fail_q[$], 1026); + `checkd(narrow_fail_q[0], 1025); + `checkd(narrow_fail_q[$], 1025); `checkd(nested_or_fail_q.size(), 0); `checkd(negated_fail_q.size(), 0); $write("*-* All Finished *-*\n"); diff --git a/test_regress/t/t_prop_followed_by.v b/test_regress/t/t_prop_followed_by.v index 783af2dbb..6cc11045a 100644 --- a/test_regress/t/t_prop_followed_by.v +++ b/test_regress/t/t_prop_followed_by.v @@ -53,10 +53,11 @@ module t ( cyc <= cyc + 1; crc <= {crc[62:0], crc[63] ^ crc[2] ^ crc[0]}; if (cyc == 32) begin + // ovl/novl deltas vs Verilator are 1-cycle preponed-sampling differences. $display("ovl=%0d novl=%0d impl=%0d nimp=%0d wide=%0d", ovl_f, novl_f, impl_f, nimp_f, wide_f); - `checkd(ovl_f, 28); // One other sim: 5 - `checkd(novl_f, 19); + `checkd(ovl_f, 29); // Other sims: 28, one other sim: 5 + `checkd(novl_f, 20); // Other sims: 19 `checkd(impl_f, 9); `checkd(nimp_f, 0); `checkd(wide_f, 0); diff --git a/test_regress/t/t_prop_s_always_eos_count.out b/test_regress/t/t_prop_s_always_eos_count.out index 2417b5f04..64edf2348 100644 --- a/test_regress/t/t_prop_s_always_eos_count.out +++ b/test_regress/t/t_prop_s_always_eos_count.out @@ -1,33 +1,8 @@ *-* All Finished *-* -EOS_FAIL -EOS_FAIL -EOS_FAIL -EOS_RING -EOS_RING -EOS_PAST=0 -EOS_BRANCH -EOS_BRANCH -EOS_BRANCH -EOS_LARGE_RING -EOS_LARGE_RING -EOS_LARGE_RING -EOS_LARGE_RING -EOS_LARGE_RING -EOS_LARGE_RING -EOS_LARGE_RING -EOS_LARGE_RING -EOS_LARGE_RING -EOS_LARGE_RING -EOS_DELAY_RING -EOS_DELAY_RING -EOS_DELAY_RING -EOS_DELAY_RING -EOS_DELAY_RING -EOS_DELAY_RING -EOS_DELAY_RING -EOS_DELAY_RING -EOS_DELAY_RING -EOS_DELAY_RING -[115] %Error: t_prop_s_always_eos_count.v:73: Assertion failed in top.t -[115] %Error: t_prop_s_always_eos_count.v:73: Assertion failed in top.t +[115] %Error: t_prop_s_always_eos_count.v:33: Assertion failed in top.t +[115] %Error: t_prop_s_always_eos_count.v:40: Assertion failed in top.t +[115] %Error: t_prop_s_always_eos_count.v:44: Assertion failed in top.t +[115] %Error: t_prop_s_always_eos_count.v:48: Assertion failed in top.t +[115] %Error: t_prop_s_always_eos_count.v:56: Assertion failed in top.t +[115] %Error: t_prop_s_always_eos_count.v:70: Assertion failed in top.t [115] %Error: t_prop_s_always_eos_count.v:73: Assertion failed in top.t diff --git a/test_regress/t/t_prop_s_always_eos_count.py b/test_regress/t/t_prop_s_always_eos_count.py index 81d02ddc2..bf66c17d8 100755 --- a/test_regress/t/t_prop_s_always_eos_count.py +++ b/test_regress/t/t_prop_s_always_eos_count.py @@ -9,7 +9,7 @@ import vltest_bootstrap -test.scenarios('vlt_all') +test.scenarios('vlt') # UNOPTTHREADS in vltmt due to many small assertion states # Keep running after an assertion failure so all unfinished strong-property # attempts can execute their action block at end of simulation. diff --git a/test_regress/t/t_prop_s_always_eos_count.v b/test_regress/t/t_prop_s_always_eos_count.v index ffde6603f..d3e220aec 100644 --- a/test_regress/t/t_prop_s_always_eos_count.v +++ b/test_regress/t/t_prop_s_always_eos_count.v @@ -45,7 +45,7 @@ module t ( else $display("EOS_BRANCH"); // Exercise the bit-vector pending ring and resolved-attempt depth tracking. - assert property (@(posedge clk) (s_always[1: 300] 1'b1) or(s_always[1: 300] 1'b1)) + assert property (@(posedge clk) (s_always[1: 40] 1'b1) or(s_always[1: 40] 1'b1)) else $display("EOS_LARGE_RING"); // Large lower bounds use bit-vector pending rings and age-indexed resolution. diff --git a/test_regress/t/t_prop_s_always_liveness.out b/test_regress/t/t_prop_s_always_liveness.out index 7ecfc8ec5..8a1f69c16 100644 --- a/test_regress/t/t_prop_s_always_liveness.out +++ b/test_regress/t/t_prop_s_always_liveness.out @@ -1,4 +1,4 @@ *-* All Finished *-* -[120] %Error: t_prop_s_always_liveness.v:26: Assertion failed in top.t -%Error: t/t_prop_s_always_liveness.v:26: Verilog $stop +[115] %Error: t_prop_s_always_liveness.v:25: Assertion failed in top.t +%Error: t/t_prop_s_always_liveness.v:25: Verilog $stop Aborting... diff --git a/test_regress/t/t_prop_s_always_liveness.v b/test_regress/t/t_prop_s_always_liveness.v index 5d662ef5a..c7158d79a 100644 --- a/test_regress/t/t_prop_s_always_liveness.v +++ b/test_regress/t/t_prop_s_always_liveness.v @@ -19,7 +19,6 @@ module t ( int low_s_fail_q[$]; int low_w_fail_q[$]; - bit finish_pending = 0; // The youngest [2:5] windows are still open at $finish, so strong s_always // reports a liveness failure even with a_high always 1; weak always does not. @@ -35,12 +34,7 @@ module t ( always @(posedge clk) begin cyc <= cyc + 1; - if (cyc == 10) finish_pending <= 1; - end - - // Check at negedge so the final posedge's Reactive action is visible. - always @(negedge clk) begin - if (finish_pending) begin + if (cyc == 10) begin `checkd(low_s_fail_q.size(), low_w_fail_q.size()); `checkd(low_w_fail_q.size(), 9); $write("*-* All Finished *-*\n"); diff --git a/test_regress/t/t_property_abort_implication.v b/test_regress/t/t_property_abort_implication.v index bb18a95fe..9a8157bf9 100644 --- a/test_regress/t/t_property_abort_implication.v +++ b/test_regress/t/t_property_abort_implication.v @@ -173,14 +173,14 @@ module t; `checkd(fail_rep_ring, fail_rep_ref); `checkd(fail_rep_ref_true, 5); `checkd(fail_rep_ring_true, fail_rep_ref_true); - `checkd(fail_goto, 5); // One other sim: 9 - `checkd(fail_goto2, 6); // One other sim: 11 - `checkd(fail_goto_range, 5); - `checkd(fail_goto_range2, 6); // One other sim: 4 + `checkd(fail_goto, 9); + `checkd(fail_goto2, 6); + `checkd(fail_goto_range, 10); + `checkd(fail_goto_range2, 6); `checkd(fail_delay, 12); `checkd(fail_fby, 16); `checkd(fail_and, 16); - `checkd(fail_unb, 2); // One other sim: 19 + `checkd(fail_unb, 3); // One other sim: 19 $write("*-* All Finished *-*\n"); $finish; end diff --git a/test_regress/t/t_property_accept_reject_on.v b/test_regress/t/t_property_accept_reject_on.v index 6971c3c82..5dcef2871 100644 --- a/test_regress/t/t_property_accept_reject_on.v +++ b/test_regress/t/t_property_accept_reject_on.v @@ -92,7 +92,7 @@ module t ( end else if (cyc == 99) begin `checkh(crc, 64'hc77bb9b3784ea091); - `checkd(count_fail1, 14); // One other sim: 15 + `checkd(count_fail1, 14); `checkd(count_fail2, 64); // One other sim: 66 `checkd(count_fail3, 14); `checkd(count_fail4, 64); diff --git a/test_regress/t/t_property_if_else.v b/test_regress/t/t_property_if_else.v index a2f59ce49..9368c24b1 100644 --- a/test_regress/t/t_property_if_else.v +++ b/test_regress/t/t_property_if_else.v @@ -26,15 +26,12 @@ module t ( always @(posedge clk) begin cyc <= cyc + 1; if (cyc == 10) begin + `checkd(vacuous_passes, 10); $write("*-* All Finished *-*\n"); $finish; end end - // Read the action counter in final to avoid a same-slot threaded race. - // IEEE 1800-2023 20.11: by default the pass action executes on vacuous success. - final `checkd(vacuous_passes, 10); // One other sim: 2 - property p_named; if (sel) a else c; endproperty @@ -60,10 +57,6 @@ module t ( assert property (@(posedge clk) if (sel) a |-> ##1 b else c) else $stop; - // A selected ranged branch contributes each endpoint without evaluating its sibling. - assert property (@(posedge clk) if (sel) 1'b1 ##[1:2] b else c) - else $stop; - // Dangling else binds to the inner property if. If it bound to the outer if, // the false outer condition would select the failing 1'b0 branch. assert property (@(posedge clk) if (nested_inner) if (nested_inner) 1'b1 else 1'b0) diff --git a/test_regress/t/t_property_nfa_abort_counts.py b/test_regress/t/t_property_nfa_abort_counts.py deleted file mode 100755 index 926563ace..000000000 --- a/test_regress/t/t_property_nfa_abort_counts.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# DESCRIPTION: Verilator: Verilog Test driver/expect definition -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of either the GNU Lesser General Public License Version 3 -# or the Perl Artistic License Version 2.0. -# SPDX-FileCopyrightText: 2026 Wilson Snyder -# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 - -import vltest_bootstrap - -test.scenarios('vlt_all') - -test.compile(timing_loop=True, verilator_flags2=['--assert', '--timing']) - -test.execute() - -test.passes() diff --git a/test_regress/t/t_property_nfa_abort_counts.v b/test_regress/t/t_property_nfa_abort_counts.v deleted file mode 100644 index fbaad3e12..000000000 --- a/test_regress/t/t_property_nfa_abort_counts.v +++ /dev/null @@ -1,108 +0,0 @@ -// DESCRIPTION: Verilator: Verilog Test module -// -// This file ONLY is placed under the Creative Commons Public Domain. -// SPDX-FileCopyrightText: 2026 PlanV GmbH -// SPDX-License-Identifier: CC0-1.0 - -// Per-attempt abort outcomes: windowed implication, negated body, vacuous passes. - -// verilog_format: off -`define stop $stop -`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); -// verilog_format: on - -module t ( - input clk -); - - bit large_a = 0, large_b = 0, large_abort = 0, large_disable = 0; - int large_accept_pass = 0, large_accept_fail = 0; - int large_reject_pass = 0, large_reject_fail = 0; - int large_disable_accept_pass = 0, large_disable_accept_fail = 0; - - bit b = 0, abort_cond = 0, abort_a = 0, never_abort = 0; - int accept_pass_at_abort = 0, accept_fail_at_abort = 0; - int reject_pass_at_abort = 0, reject_fail_at_abort = 0; - int plain_vacuous_pass = 0, wrapped_vacuous_pass = 0; - int negated_plain_vacuous_pass = 0, negated_wrapped_vacuous_pass = 0; - - initial $assertpasson; - - assert property (@(posedge clk) sync_accept_on (large_abort) (large_a |-> ##[1:300] large_b)) begin - if (large_abort) large_accept_pass++; - end - else if (large_abort) large_accept_fail++; - assert property (@(posedge clk) sync_reject_on (large_abort) (large_a |-> ##[1:300] large_b)) begin - if (large_abort) large_reject_pass++; - end - else if (large_abort) large_reject_fail++; - assert property (@(posedge clk) disable iff (large_disable) - sync_accept_on (large_abort) (large_a |-> ##[1:300] large_b)) begin - if (large_abort) large_disable_accept_pass++; - end - else if (large_abort) large_disable_accept_fail++; - - // Abort outcome is outside `not`: accept stays a pass, reject stays a failure. - assert property (@(posedge clk) sync_accept_on (abort_cond) (abort_a |-> not (##[1:300] b))) begin - if (abort_cond) accept_pass_at_abort++; - end - else begin - if (abort_cond) accept_fail_at_abort++; - end - assert property (@(posedge clk) sync_reject_on (abort_cond) (abort_a |-> not (##[1:300] b))) begin - if (abort_cond) reject_pass_at_abort++; - end - else begin - if (abort_cond) reject_fail_at_abort++; - end - - // A false outer abort must not suppress the vacuous pass action. - assert property (@(posedge clk) 0 |-> ##1 1) plain_vacuous_pass++; - assert property (@(posedge clk) sync_accept_on (never_abort) (0 |-> ##1 1)) - wrapped_vacuous_pass++; - assert property (@(posedge clk) 0 |-> not (##1 1)) negated_plain_vacuous_pass++; - assert property (@(posedge clk) sync_accept_on (never_abort) (0 |-> not (##1 1))) - negated_wrapped_vacuous_pass++; - - initial begin - @(negedge clk) begin - large_a = 1; - abort_a = 1; - end - @(negedge clk) begin - large_a = 1; - abort_a = 1; - end - @(negedge clk) begin - large_a = 0; - large_b = 1; - large_abort = 1; - b = 1; - abort_cond = 1; - abort_a = 1; - end - @(negedge clk) begin - large_b = 0; - large_abort = 0; - b = 0; - abort_cond = 0; - `checkd(large_accept_pass, 3); - `checkd(large_accept_fail, 0); // zero-ok: an accept never fails - `checkd(large_reject_pass, 0); // Other simulator: 1; zero-ok: a reject never passes - `checkd(large_reject_fail, 3); // Other simulator: 2 - `checkd(large_disable_accept_pass, 3); - `checkd(large_disable_accept_fail, 0); // zero-ok: an accept never fails - `checkd(accept_pass_at_abort, 3); - `checkd(accept_fail_at_abort, 0); // zero-ok: an accept never fails - `checkd(reject_pass_at_abort, 0); // zero-ok: a reject never passes - `checkd(reject_fail_at_abort, 3); - `checkd(plain_vacuous_pass, 4); - `checkd(wrapped_vacuous_pass, 4); - `checkd(negated_plain_vacuous_pass, 4); - `checkd(negated_wrapped_vacuous_pass, 4); - $write("*-* All Finished *-*\n"); - $finish; - end - end - -endmodule diff --git a/test_regress/t/t_property_nfa_bbox_unsup.py b/test_regress/t/t_property_nfa_bbox_unsup.py deleted file mode 100755 index 1fc34411f..000000000 --- a/test_regress/t/t_property_nfa_bbox_unsup.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -# DESCRIPTION: Verilator: Verilog Test driver/expect definition -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of either the GNU Lesser General Public License Version 3 -# or the Perl Artistic License Version 2.0. -# SPDX-FileCopyrightText: 2026 Wilson Snyder -# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 - -import vltest_bootstrap - -test.scenarios('vlt') - -test.compile(verilator_flags2=['--assert', '--timing', '--bbox-unsup']) - -test.passes() diff --git a/test_regress/t/t_property_nfa_bbox_unsup.v b/test_regress/t/t_property_nfa_bbox_unsup.v deleted file mode 100644 index 5f58210ec..000000000 --- a/test_regress/t/t_property_nfa_bbox_unsup.v +++ /dev/null @@ -1,32 +0,0 @@ -// DESCRIPTION: Verilator: Verilog Test module -// -// This file ONLY is placed under the Creative Commons Public Domain. -// SPDX-FileCopyrightText: 2026 PlanV GmbH -// SPDX-License-Identifier: CC0-1.0 - -// Zero-repetition and folded-operand composites reach the NFA under --bbox-unsup - -module t ( - input clk -); - - logic a, b, c, d, x; - - // verilog_format: off - assert property (@(posedge clk) (a[*0]) and (b ##1 c)); - - assert property (@(posedge clk) (a[*0]) and (b ##[1:2] c)); - - assert property (@(posedge clk) ((a[*0]) ##1 b ##1 c) intersect (d ##2 c)); - - assert property (@(posedge clk) (a ##1 b) or (accept_on(x) (c ##1 d))); - - assert property (@(posedge clk) (accept_on(x) (a ##1 b)) and (c ##2 d)); - // verilog_format: on - - always @(posedge clk) begin - $write("*-* All Finished *-*\n"); - $finish; - end - -endmodule diff --git a/test_regress/t/t_property_nfa_composites.v b/test_regress/t/t_property_nfa_composites.v index 30e5464a3..96858047b 100644 --- a/test_regress/t/t_property_nfa_composites.v +++ b/test_regress/t/t_property_nfa_composites.v @@ -123,7 +123,7 @@ module t ( repeat (3) @(negedge clk); `checkd(or_fail_pass, 0); - `checkd(or_fail_count, 1); + `checkd(or_fail_count, 0); `checkd(or_both_pass, 1); `checkd(or_both_fail, 0); `checkd(cross_hits, 0); @@ -132,7 +132,7 @@ module t ( `checkd(isect_pass_one, 17); `checkd(isect_pass_both, 17); `checkd(isect_pass_zero, 18); - `checkd(isect_fail_left, 17); + `checkd(isect_fail_left, 0); `checkd(isect_fail_right, 17); `checkd(isect_fail_zero, 18); `checkd(isect_unexp_one, 0); diff --git a/test_regress/t/t_property_nfa_counts.v b/test_regress/t/t_property_nfa_counts.v index eb1221061..bea0fc27d 100644 --- a/test_regress/t/t_property_nfa_counts.v +++ b/test_regress/t/t_property_nfa_counts.v @@ -152,22 +152,22 @@ module t ( always @(negedge clk) begin if (cyc == 12) begin - `checkd(imp_pass, 11); + `checkd(imp_pass, 10); `checkd(imp_fail, 1); - `checkd(chain_pass, 1); + `checkd(chain_pass, 2); `checkd(chain_fail, 11); - `checkd(range_pass, 2); + `checkd(range_pass, 1); `checkd(range_fail, 10); - `checkd(range_cover, 2); - `checkd(large_pass, 2); + `checkd(range_cover, 1); + `checkd(large_pass, 1); `checkd(large_fail, 10); - `checkd(large_cover, 2); - `checkd(large_imp_at_b, 3); - `checkd(neg_fail_at_b, 2); - `checkd(neg_large_fail_at_b, 2); + `checkd(large_cover, 1); + `checkd(large_imp_at_b, 2); + `checkd(neg_fail_at_b, 1); + `checkd(neg_large_fail_at_b, 1); `checkd(neg_chain_pass, 2); - `checkd(neg_chain_fail, 1); - `checkd(neg_chain_cover, 2); + `checkd(neg_chain_fail, 2); + `checkd(neg_chain_cover, 1); `checkd(neg_imp_pass, 2); `checkd(vacuous_pass, 12); `checkd(negated_vacuous_pass, 12); diff --git a/test_regress/t/t_property_nfa_edge_shapes.out b/test_regress/t/t_property_nfa_edge_shapes.out new file mode 100644 index 000000000..b1fd54a1d --- /dev/null +++ b/test_regress/t/t_property_nfa_edge_shapes.out @@ -0,0 +1,4 @@ +*-* All Finished *-* +[215] %Error: t_property_nfa_edge_shapes.v:27: Assertion failed in top.t +%Error: t/t_property_nfa_edge_shapes.v:27: Verilog $stop +Aborting... diff --git a/test_regress/t/t_property_nfa_edge_shapes.py b/test_regress/t/t_property_nfa_edge_shapes.py index e3b4fc447..27c736e24 100755 --- a/test_regress/t/t_property_nfa_edge_shapes.py +++ b/test_regress/t/t_property_nfa_edge_shapes.py @@ -13,6 +13,6 @@ test.scenarios('vlt_all') test.compile(verilator_flags2=['--assert', '--timing', '--coverage-user']) -test.execute() +test.execute(fails=True, expect_filename=test.golden_filename) test.passes() diff --git a/test_regress/t/t_property_nfa_edge_shapes.v b/test_regress/t/t_property_nfa_edge_shapes.v index 4ba21f5b4..720e71744 100644 --- a/test_regress/t/t_property_nfa_edge_shapes.v +++ b/test_regress/t/t_property_nfa_edge_shapes.v @@ -17,9 +17,7 @@ module t ( logic a = 0, b = 0, c = 0, x = 0; int cyc = 0; - int np1 = 0, nf1 = 0, nc2 = 0, nc3 = 0, nf4 = 0, nc5 = 0, np6 = 0, nf6 = 0; - int nc9 = 0, nf10 = 0, np11 = 0, nf12 = 0, nf13 = 0, nf14 = 0; - int np15 = 0, nf15 = 0, nf16 = 0, nc17 = 0, nf18 = 0; + int np1 = 0, nf1 = 0, nc2 = 0, nc3 = 0, nf4 = 0, nc5 = 0; // verilog_format: off assert property (@(posedge clk) not (##[1:$] b)) np1 = np1 + 1; else nf1 = nf1 + 1; @@ -31,28 +29,6 @@ module t ( assert property (@(posedge clk) ((a ##2 b) or (c ##2 b)) |-> s_always [1:2] a) else nf4 = nf4 + 1; cover property (@(posedge clk) sync_accept_on (x) c) nc5 = nc5 + 1; - - assert property (@(posedge clk) disable iff ($sampled(a) || $sampled(c)) not (b ##1 !b)) np6 = np6 + 1; else nf6 = nf6 + 1; - - cover property (@(posedge clk) a |-> (!c until b)) nc9 = nc9 + 1; - - assert property (@(posedge clk) sync_accept_on (x) (a |-> (b ##[1:2] (c ##1 a)))) else nf10 = nf10 + 1; - - assert property (@(posedge clk) sync_accept_on (x) (a ##[1:$] b)) np11 = np11 + 1; - - assert property (@(posedge clk) sync_accept_on (x) ((a ##[1:2] (b ##1 c)) ##1 a)) else nf12 = nf12 + 1; - - assert property (@(posedge clk) sync_accept_on (x) (a ##1 (b ##[1:2] (c ##1 a)))) else nf13 = nf13 + 1; - - assert property (@(posedge clk) sync_accept_on (x) (always [0:$] b)) else nf14 = nf14 + 1; - - assert property (@(posedge clk) disable iff ($sampled(a && $sampled(c))) not (b ##1 !b)) np15 = np15 + 1; else nf15 = nf15 + 1; - - assert property (@(posedge clk) accept_on (x) (a ##1 b)) else nf16 = nf16 + 1; - - cover property (@(posedge clk) sync_accept_on (x) (a ##1 b)) nc17 = nc17 + 1; - - assert property (@(posedge clk) (a ##[1:$] b) ##1 c) else nf18 = nf18 + 1; // verilog_format: on always @(posedge clk) begin @@ -65,25 +41,12 @@ module t ( end final begin - `checkd(np1, 0); // zero-ok: unbounded not() never resolves to a pass - `checkd(nf1, 2); // One other sim: 11 - `checkd(nc2, 1); // One other sim: 15 + `checkd(np1, 19); + `checkd(nf1, 2); + `checkd(nc2, 1); `checkd(nc3, 5); `checkd(nf4, 0); - `checkd(nc5, 1); // One other sim: 15 - `checkd(np6, 12); - `checkd(nf6, 1); - `checkd(nc9, 1); // One other sim: 3 - `checkd(nf10, 6); - `checkd(np11, 2); // One other sim: 3 - `checkd(nf12, 19); - `checkd(nf13, 19); - `checkd(nf14, 17); // One other sim: 19 - `checkd(np15, 18); - `checkd(nf15, 2); - `checkd(nf16, 18); - `checkd(nc17, 2); // One other sim: 6 - `checkd(nf18, 14); + `checkd(nc5, 1); $write("*-* All Finished *-*\n"); end endmodule diff --git a/test_regress/t/t_property_nfa_error_limit.out b/test_regress/t/t_property_nfa_error_limit.out index 398d09514..45412db25 100644 --- a/test_regress/t/t_property_nfa_error_limit.out +++ b/test_regress/t/t_property_nfa_error_limit.out @@ -1,15 +1,7 @@ -[5] %Error: t_property_nfa_error_limit.v:51: Assertion failed in top.t: 'assert' failed. --Info: t/t_property_nfa_error_limit.v:51: Verilog $stop, ignored due to +verilator+error+limit -[7] %Error: t_property_nfa_error_limit.v:52: Assertion failed in top.t: 'assert' failed. -[7] %Error: t_property_nfa_error_limit.v:52: Assertion failed in top.t: 'assert' failed. -[9] %Error: t_property_nfa_error_limit.v:32: Assertion failed in top.t: 'assert' failed. +[5] %Error: t_property_nfa_error_limit.v:48: Assertion failed in top.t: 'assert' failed. +-Info: t/t_property_nfa_error_limit.v:48: Verilog $stop, ignored due to +verilator+error+limit +[7] %Error: t_property_nfa_error_limit.v:49: Assertion failed in top.t: 'assert' failed. [9] %Error: t_property_nfa_error_limit.v:32: Assertion failed in top.t: 'assert' failed. [11] %Error: t_property_nfa_error_limit.v:35: Assertion failed in top.t: 'assert' failed. -[11] %Error: t_property_nfa_error_limit.v:35: Assertion failed in top.t: 'assert' failed. -[11] %Error: t_property_nfa_error_limit.v:40: Assertion failed in top.t: 'assert' failed. -[11] %Error: t_property_nfa_error_limit.v:40: Assertion failed in top.t: 'assert' failed. -[11] %Error: t_property_nfa_error_limit.v:40: Assertion failed in top.t: 'assert' failed. -[11] %Error: t_property_nfa_error_limit.v:40: Assertion failed in top.t: 'assert' failed. -[13] %Error: t_property_nfa_error_limit.v:37: Assertion failed in top.t: 'assert' failed. [13] %Error: t_property_nfa_error_limit.v:37: Assertion failed in top.t: 'assert' failed. *-* All Finished *-* diff --git a/test_regress/t/t_property_nfa_error_limit.py b/test_regress/t/t_property_nfa_error_limit.py index 42098a903..f41698efd 100755 --- a/test_regress/t/t_property_nfa_error_limit.py +++ b/test_regress/t/t_property_nfa_error_limit.py @@ -13,6 +13,6 @@ test.scenarios('vlt_all') test.compile(timing_loop=True, verilator_flags2=['--assert', '--timing']) -test.execute(all_run_flags=['+verilator+error+limit+100'], expect_filename=test.golden_filename) +test.execute(all_run_flags=['+verilator+error+limit+10'], expect_filename=test.golden_filename) test.passes() diff --git a/test_regress/t/t_property_nfa_error_limit.v b/test_regress/t/t_property_nfa_error_limit.v index 3dcc7625b..587c56b27 100644 --- a/test_regress/t/t_property_nfa_error_limit.v +++ b/test_regress/t/t_property_nfa_error_limit.v @@ -36,9 +36,6 @@ module t ( assert property (@(posedge clk) sync_reject_on (abrt) always[0: 1] (!zz)); - // A guard drop fails every attempt in the delay ring with the default action - assert property (@(posedge clk) (!nb) throughout (1'b1 ##3 1'b1)); - // Simultaneous negated-consequent failures behind a temporal antecedent bit ant = 0; bit early_b = 0; @@ -86,8 +83,8 @@ module t ( always @(negedge clk) begin if (cyc == 8) begin `checkd(temporal_small_fail, 1); - `checkd(temporal_ring_fail, 2); - `checkd(boolean_ant_fail, 2); + `checkd(temporal_ring_fail, 1); + `checkd(boolean_ant_fail, 1); `checkd(impossible_pass, 0); `checkd(impossible_fail, 0); $write("*-* All Finished *-*\n"); diff --git a/test_regress/t/t_property_nfa_msgs_unsup.out b/test_regress/t/t_property_nfa_msgs_unsup.out index a28ee37b5..5cdbf6675 100644 --- a/test_regress/t/t_property_nfa_msgs_unsup.out +++ b/test_regress/t/t_property_nfa_msgs_unsup.out @@ -1,245 +1,52 @@ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:33:50: Unsupported: impure expression in a temporal 'or' composite - 33 | assert property (@(posedge clk) (fimp() ##1 a) or(b ##1 c)); - | ^~ +%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:26:63: Unsupported: temporal property if/case with a pass action + : ... note: In instance 't' + 26 | assert property (@(posedge clk) if (a) 1'b1 ##1 b else 1'b1 ##2 c) $display("pass"); + | ^~ ... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:36:54: Unsupported: temporal 'or' endpoint deadline after an operand that can reject earlier - 36 | assert property (@(posedge clk) (s_always[1: 2] a) or(s_always[1: 2] b)); - | ^~ -%Warning-COVERIGN: t/t_property_nfa_msgs_unsup.v:39:39: Ignoring unsupported: cover sequence with a sequence operand of 'or' - 39 | cover sequence (@(posedge clk) 1'b1 or(a ##1 b)); - | ^~ +%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:27:62: Unsupported: temporal property if/case with cover + : ... note: In instance 't' + 27 | cover property (@(posedge clk) if (a) 1'b1 ##1 b else 1'b1 ##2 c); + | ^~ +%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:28:68: Unsupported: temporal property if/case with negation + : ... note: In instance 't' + 28 | assert property (@(posedge clk) not (if (a) 1'b1 ##1 b else 1'b1 ##2 c)); + | ^~ +%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:30:35: Unsupported: temporal property if/case with a pass action + : ... note: In instance 't' + 30 | assert property (@(posedge clk) case (a) 1'b0: 1'b1 ##1 b; 1'b1: 1'b1 ##2 c; default: 1'b1 ##1 d; + | ^~~~ +%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:35:65: Unsupported: multi-cycle sequence expression inside consecutive repetition (IEEE 1800-2023 16.9.2) + : ... note: In instance 't' + 35 | assert property (@(posedge clk) sync_accept_on (a) ((b ##1 c) [* 2])); + | ^~ +%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:38:52: Unsupported: multi-cycle sequence expression inside consecutive repetition (IEEE 1800-2023 16.9.2) + : ... note: In instance 't' + 38 | assert property (@(posedge clk) if (a) ((b ##1 c)[*2]) else d) $display("pass"); + | ^~ +%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:41:3: Unsupported: property instance inside a multi-cycle property expression + : ... note: In instance 't' + 41 | assert property (@(posedge clk) p_nested or e); + | ^~~~~~ +%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:59:52: Unsupported: ranged cycle delay in an operand of property 'and' + 59 | assert property (@(posedge clk) (1'b1 ##[1:2] b) and c); + | ^~~ +%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:60:37: Unsupported: ranged cycle delay in an operand of property 'and' + 60 | assert property (@(posedge clk) c and (1'b1 ##[1:2] b)); + | ^~~ +%Warning-COVERIGN: t/t_property_nfa_msgs_unsup.v:63:45: Ignoring unsupported: cover sequence with a sequence operand of 'or' + 63 | cover sequence (@(posedge clk) ((a and b) or(c ##1 d))); + | ^~ ... For warning description see https://verilator.org/warn/COVERIGN?v=latest ... Use "/* verilator lint_off COVERIGN */" and lint_on around source to disable this message. -%Warning-COVERIGN: t/t_property_nfa_msgs_unsup.v:40:44: Ignoring unsupported: cover sequence with a sequence operand of 'or' - 40 | cover sequence (@(posedge clk) (a ##1 b) or 1'b1); - | ^~ -%Warning-COVERIGN: t/t_property_nfa_msgs_unsup.v:41:44: Ignoring unsupported: cover sequence with a sequence operand of 'or' - 41 | cover sequence (@(posedge clk) (a ##1 b) or(c ##1 d)); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:44:68: Unsupported: intersect/within endpoint deadline after an operand that can reject earlier - 44 | assert property (@(posedge clk) (((a ##1 b) or (c ##1 d)) ##1 e) intersect (a throughout (b ##2 c))); - | ^~~~~~~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:47:43: Unsupported: impure guard in a flattened throughout composite - : ... note: In instance 't' - 47 | assert property (@(posedge clk) (fimp() throughout (a ##1 b)) and(c ##1 d)); - | ^~~~~~~~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:50:36: Unsupported: impure expression in a flattened temporal composite - : ... note: In instance 't' - 50 | assert property (@(posedge clk) (fimp() ##1 a) and(b ##1 c)); - | ^~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:53:38: Unsupported: concurrent assertion fixed-trace expansion exceeds 1024 sites - : ... note: In instance 't' - 53 | assert property (@(posedge clk) (a throughout (b ##1024 c)) and(d ##1024 e)); - | ^~~~~~~~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:56:53: Unsupported: property if/case inside a variable-end temporal window - : ... note: In instance 't' - 56 | assert property (@(posedge clk) (a ##[1:$] b) or (if (c) d else e)); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:59:59: Unsupported: bounded temporal 'and' operand cannot be represented as a single fixed match trace - : ... note: In instance 't' - 59 | assert property (@(posedge clk) ((a ##1 b) or(c ##1 d)) and(e ##1 a)); - | ^~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:62:3: Unsupported: strong s_always in a temporal AND/intersect composite cannot preserve resolved attempt identity - 62 | assert property (@(posedge clk) (((a ##1 b) or (c ##1 d)) intersect (e ##1 a)) |-> s_always [1:2] b); - | ^~~~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:65:3: Unsupported: end-of-simulation attempt counting for multiple strong operators with an ambiguous temporal depth - 65 | assert property (@(posedge clk) ((a ##1 b) or(c ##1 d)) ##1 (e [-> 1]) |-> s_always[1: 2] a); - | ^~~~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:68:3: Unsupported: end-of-simulation attempt counting for multiple strong operators requires expanding 1030 ring slots (limit 1024) - 68 | assert property (@(posedge clk) ((a ##1 b) or(c ##1 d)) |-> s_always[1030: 1030] e) cnt++; - | ^~~~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:71:3: Unsupported: strong s_always pending state has a non-positive temporal depth - 71 | assert property (@(posedge clk) ((a ##1 b) or(c ##1 d)) |-> s_always[0: 0] e) cnt++; - | ^~~~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:74:3: Unsupported: pass-action multiplicity for strong s_always in a temporal OR composite cannot preserve resolved attempts - 74 | assert property (@(posedge clk) a |-> (((a ##1 b) or(c ##1 d)) |-> s_always[1: 2] e)) cnt++; - | ^~~~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:78:45: Unsupported: unequal/variable-end temporal 'or' cannot preserve overlapping assertion attempt identity - 78 | assert property (@(posedge clk) (a ##1 b) or (c ##2 d)); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:80:48: Unsupported: variable/unbounded temporal 'and' cannot preserve overlapping assertion attempt identity - : ... note: In instance 't' - 80 | cover property (@(posedge clk) (a ##[1:$] b) and (c ##1 d)); - | ^~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:83:49: Unsupported: ranged cycle delay in an operand of property 'and' - 83 | assert property (@(posedge clk) (a ##[1:2] b) and (c ##2 d)); - | ^~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:85:56: Unsupported: temporal implication antecedent with a negated consequent and a pass or cover action cannot preserve attempt identity - : ... note: In instance 't' - 85 | assert property (@(posedge clk) (a ##1 b) |-> not (c ##[1:2] d)) $display("pass"); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:87:58: Unsupported: impure expression in a flattened consecutive repetition - : ... note: In instance 't' - 87 | assert property (@(posedge clk) ((|($random | $random))[*2]) and (1'b1 ##1 1'b1)); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:88:60: Unsupported: impure expression in a flattened consecutive repetition - : ... note: In instance 't' - 88 | assert property (@(posedge clk) ((|($urandom | $urandom))[*2]) and (1'b1 ##1 1'b1)); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:90:37: Unsupported: concurrent assertion fixed-trace expansion exceeds 1024 sites - : ... note: In instance 't' - 90 | assert property (@(posedge clk) (a[*2000]) and (1'b1 ##1 1'b1)); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:92:3: Unsupported: end-of-simulation attempt counting for multiple strong operators with an ambiguous temporal depth - 92 | assert property (@(posedge clk) - | ^~~~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:95:46: Unsupported: 'until' inside a variable-length property window - : ... note: In instance 't' - 95 | assert property (@(posedge clk) ##[1:$] (a until b)); - | ^~~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:99:63: Unsupported: temporal property if/case with a pass action - : ... note: In instance 't' - 99 | assert property (@(posedge clk) if (a) 1'b1 ##1 b else 1'b1 ##2 c) $display("pass"); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:100:62: Unsupported: temporal property if/case with cover - : ... note: In instance 't' - 100 | cover property (@(posedge clk) if (a) 1'b1 ##1 b else 1'b1 ##2 c); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:101:68: Unsupported: temporal property if/case with negation - : ... note: In instance 't' - 101 | assert property (@(posedge clk) not (if (a) 1'b1 ##1 b else 1'b1 ##2 c)); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:103:35: Unsupported: temporal property if/case with a pass action - : ... note: In instance 't' - 103 | assert property (@(posedge clk) case (a) 1'b0: 1'b1 ##1 b; 1'b1: 1'b1 ##2 c; default: 1'b1 ##1 d; - | ^~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:107:68: Unsupported: temporal property if/case with a strong end-of-trace obligation - : ... note: In instance 't' - 107 | assert property (@(posedge clk) if (a) s_always[1:2] b else 1'b1 ##1 c); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:110:32: Unsupported: impure property if/case selector cannot be sampled once - : ... note: In instance 't' - 110 | if ($random == 0) 1'b1 ##1 b - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:114:65: Unsupported: multi-cycle sequence expression inside consecutive repetition (IEEE 1800-2023 16.9.2) - : ... note: In instance 't' - 114 | assert property (@(posedge clk) sync_accept_on (a) ((b ##1 c) [* 2])); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:117:52: Unsupported: multi-cycle sequence expression inside consecutive repetition (IEEE 1800-2023 16.9.2) - : ... note: In instance 't' - 117 | assert property (@(posedge clk) if (a) ((b ##1 c)[*2]) else d) $display("pass"); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:121:46: Unsupported: multi-cycle sequence expression inside consecutive repetition (IEEE 1800-2023 16.9.2) - : ... note: In instance 't' - 121 | assert property (@(posedge clk) ((b ##1 c) [* 2]) and(d ##2 e)); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:122:50: Unsupported: ranged cycle delay in an operand of property 'and' - 122 | assert property (@(posedge clk) ((a ##[1:2] b) and(c ##1 d)) and(e ##1 a)); - | ^~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:123:74: Unsupported: bounded temporal 'and' operand cannot be represented as a single fixed match trace - : ... note: In instance 't' - 123 | assert property (@(posedge clk) (a throughout ((b ##1 c) or(d ##1 e))) and(c ##2 a)); - | ^~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:126:45: Unsupported: variable/unbounded temporal 'and' cannot preserve overlapping assertion attempt identity - : ... note: In instance 't' - 126 | cover property (@(posedge clk) (a [-> 1]) and(b [* 2])); - | ^~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:129:45: Unsupported: impure expression in a temporal 'or' composite - 129 | assert property (@(posedge clk) (a ##1 b) or(fimp() ##1 c)); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:130:66: Unsupported: temporal 'or' endpoint deadline after an operand that can reject earlier - 130 | assert property (@(posedge clk) (b throughout (1'b1 ##2 1'b1)) or(c ##2 d)); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:131:45: Unsupported: temporal 'or' endpoint deadline after an operand that can reject earlier - 131 | assert property (@(posedge clk) (c ##2 d) or(b throughout (1'b1 ##2 1'b1))); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:134:74: Unsupported: bounded temporal 'and' operand cannot be represented as a single fixed match trace - : ... note: In instance 't' - 134 | assert property (@(posedge clk) (((a ##1 b) or(c ##1 d)) and(e ##1 a)) and(b ##1 c)); - | ^~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:135:37: Unsupported: impure expression in a flattened temporal composite - : ... note: In instance 't' - 135 | assert property (@(posedge clk) ((fimp() ##1 a) and(b ##1 c)) and(d ##1 e)); - | ^~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:136:49: Unsupported: impure expression in a flattened temporal composite - : ... note: In instance 't' - 136 | assert property (@(posedge clk) (a ##1 b) and(fimp() ##1 c)); - | ^~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:137:40: Unsupported: impure expression in a flattened temporal composite - : ... note: In instance 't' - 137 | assert property (@(posedge clk) (##1 fimp()) and(b ##1 c)); - | ^~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:138:60: Unsupported: bounded temporal 'and' operand cannot be represented as a single fixed match trace - : ... note: In instance 't' - 138 | assert property (@(posedge clk) (((a ##1 b) or(c ##1 d)) and(e ##1 a)) or(b ##1 c)); - | ^~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:139:72: Unsupported: bounded temporal 'and' operand cannot be represented as a single fixed match trace - : ... note: In instance 't' - 139 | assert property (@(posedge clk) (b ##1 c) or(((a ##1 b) or(c ##1 d)) and(e ##1 a))); - | ^~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:140:71: Unsupported: impure expression in a temporal 'or' composite - 140 | assert property (@(posedge clk) (a ##2 b) intersect ((fimp() ##2 c) or(d ##2 e))); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:141:68: Unsupported: impure expression in a temporal 'or' composite - 141 | assert property (@(posedge clk) (a ##[1:2] b) and((fimp() ##1 c) or(d ##1 e))); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:142:51: Unsupported: impure expression in a temporal 'or' composite - 142 | assert property (@(posedge clk) ((fimp() ##1 c) or(d ##1 e)) and(a ##[1:2] b)); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:145:63: Unsupported: unequal/variable-end temporal 'or' cannot preserve overlapping assertion attempt identity - 145 | assert property (@(posedge clk) ((a [* 1: 2]) and(b ##1 c)) or(d ##1 e)); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:146:63: Unsupported: unequal/variable-end temporal 'or' cannot preserve overlapping assertion attempt identity - 146 | assert property (@(posedge clk) ((a ##1 b) and(c [* 1: 2])) or(d ##1 e)); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:149:45: Unsupported: 'until' in complex property expression - : ... note: In instance 't' - 149 | assert property (@(posedge clk) (a ##1 b) until c); - | ^~~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:150:59: Unsupported: multi-cycle sequence expression inside consecutive repetition (IEEE 1800-2023 16.9.2) - : ... note: In instance 't' - 150 | assert property (@(posedge clk) a throughout ((b ##1 c) [* 2])); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:151:35: Unsupported: temporal property if/case with an abort operator - : ... note: In instance 't' - 151 | assert property (@(posedge clk) sync_accept_on (a) (if (b) 1'b1 ##1 c else 1'b1 ##2 d)); - | ^~~~~~~~~~~~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:152:55: Unsupported: temporal implication antecedent with a negated consequent and a pass or cover action cannot preserve attempt identity - : ... note: In instance 't' - 152 | cover property (@(posedge clk) (a ##1 b) |-> not (c ##[1:2] d)); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:155:3: Unsupported: property instance inside a multi-cycle property expression - : ... note: In instance 't' - 155 | assert property (@(posedge clk) p_nested or e); - | ^~~~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:173:52: Unsupported: ranged cycle delay in an operand of property 'and' - 173 | assert property (@(posedge clk) (1'b1 ##[1:2] b) and c); - | ^~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:174:37: Unsupported: ranged cycle delay in an operand of property 'and' - 174 | assert property (@(posedge clk) c and(1'b1 ##[1:2] b)); - | ^~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:177:45: Unsupported: unequal/variable-end temporal 'or' cannot preserve overlapping assertion attempt identity - 177 | cover sequence (@(posedge clk) ((a and b) or(c ##1 d))); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:190:51: Unsupported: unequal/variable-end temporal 'or' cannot preserve overlapping assertion attempt identity - 190 | assert property (@(posedge clk) (a [= 1] ##1 b) or(c ##2 d)); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:191:45: Unsupported: unequal/variable-end temporal 'or' cannot preserve overlapping assertion attempt identity - 191 | assert property (@(posedge clk) (c ##2 d) or(a [= 1] ##1 b)); - | ^~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:200:74: Unsupported: bounded temporal 'and' operand cannot be represented as a single fixed match trace - : ... note: In instance 't' - 200 | assert property (@(posedge clk) ((a ##1 b) and((c ##1 d) or(e ##1 a))) and(b ##1 c)); - | ^~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:201:50: Unsupported: impure expression in a flattened temporal composite - : ... note: In instance 't' - 201 | assert property (@(posedge clk) ((a ##1 b) and(fimp() ##1 c)) and(d ##1 e)); - | ^~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:218:74: Unsupported: intersect/within endpoint deadline after an operand that can reject earlier - 218 | assert property (@(posedge clk) (a throughout ((b ##2 c) or(d ##2 e))) intersect (c ##2 a)); - | ^~~~~~~~~ -%Error-UNSUPPORTED: t/t_property_nfa_msgs_unsup.v:219:51: Unsupported: impure expression in a temporal 'or' composite - 219 | assert property (@(posedge clk) ((fimp() ##2 c) or(d ##2 e)) intersect (a ##2 b)); - | ^~ -%Error: t/t_property_nfa_msgs_unsup.v:170:22: Concurrent assertion has no clock (IEEE 1800-2023 16.16) - : ... note: In instance 't' - : ... Suggest provide a clocking event, a default clocking, or a clocked procedural context - 170 | assert property (a [* 2]); - | ^~ - ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. -%Error: t/t_property_nfa_msgs_unsup.v:170:3: Concurrent assertion has no clock (IEEE 1800-2023 16.16) +%Error: t/t_property_nfa_msgs_unsup.v:56:22: Concurrent assertion has no clock (IEEE 1800-2023 16.16) : ... note: In instance 't' : ... Suggest provide a clocking event, a default clocking, or a clocked procedural context - 170 | assert property (a [* 2]); + 56 | assert property (a [* 2]); + | ^~ + ... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance. +%Error: t/t_property_nfa_msgs_unsup.v:56:3: Concurrent assertion has no clock (IEEE 1800-2023 16.16) + : ... note: In instance 't' + : ... Suggest provide a clocking event, a default clocking, or a clocked procedural context + 56 | assert property (a [* 2]); | ^~~~~~ %Error: Exiting due to diff --git a/test_regress/t/t_property_nfa_msgs_unsup.py b/test_regress/t/t_property_nfa_msgs_unsup.py index ca7d48e9d..d043caa09 100755 --- a/test_regress/t/t_property_nfa_msgs_unsup.py +++ b/test_regress/t/t_property_nfa_msgs_unsup.py @@ -22,7 +22,6 @@ test.lint(expect_filename=test.golden_filename, test.file_grep_any(glob.glob(test.obj_dir + "/V" + test.name + "_*.tree"), r'SAND.*\[PROPERTY_CONTROL\]') -test.file_grep_any(glob.glob(test.obj_dir + "/V" + test.name + "_*.tree"), r'ASSERT.*\[NFA\]') jsons = glob.glob(test.obj_dir + "/V" + test.name + "_*.tree.json") if not jsons: diff --git a/test_regress/t/t_property_nfa_msgs_unsup.v b/test_regress/t/t_property_nfa_msgs_unsup.v index 2e25c36e6..e8d7ad73a 100644 --- a/test_regress/t/t_property_nfa_msgs_unsup.v +++ b/test_regress/t/t_property_nfa_msgs_unsup.v @@ -11,8 +11,6 @@ module t ( ); bit a = 0, b = 0, c = 0, d = 0, e = 0; - int cnt = 0; - int impure_count = 0; property p_nested; a ##1 b; @@ -24,77 +22,6 @@ module t ( return a; endfunction - function automatic bit fimp(); - impure_count++; - return impure_count[0]; - endfunction - - // Unsupported: impure expression in a temporal 'or' composite - assert property (@(posedge clk) (fimp() ##1 a) or(b ##1 c)); - - // Unsupported: temporal 'or' endpoint deadline after an operand that can reject earlier - assert property (@(posedge clk) (s_always[1: 2] a) or(s_always[1: 2] b)); - - // Ignoring unsupported: cover sequence with a sequence operand of 'or' - cover sequence (@(posedge clk) 1'b1 or(a ##1 b)); - cover sequence (@(posedge clk) (a ##1 b) or 1'b1); - cover sequence (@(posedge clk) (a ##1 b) or(c ##1 d)); - - // Unsupported: intersect/within endpoint deadline after an operand that can reject earlier - assert property (@(posedge clk) (((a ##1 b) or (c ##1 d)) ##1 e) intersect (a throughout (b ##2 c))); - - // Unsupported: impure guard in a flattened throughout composite - assert property (@(posedge clk) (fimp() throughout (a ##1 b)) and(c ##1 d)); - - // Unsupported: impure expression in a flattened temporal composite - assert property (@(posedge clk) (fimp() ##1 a) and(b ##1 c)); - - // Fixed-trace expansion diagnostic from the throughout path - assert property (@(posedge clk) (a throughout (b ##1024 c)) and(d ##1024 e)); - - // Unsupported: property if/case inside a variable-end temporal window - assert property (@(posedge clk) (a ##[1:$] b) or (if (c) d else e)); - - // Unsupported: bounded temporal 'and' operand cannot be represented as a fixed trace - assert property (@(posedge clk) ((a ##1 b) or(c ##1 d)) and(e ##1 a)); - - // Unsupported: strong s_always in a temporal AND/intersect composite - assert property (@(posedge clk) (((a ##1 b) or (c ##1 d)) intersect (e ##1 a)) |-> s_always [1:2] b); - - // Unsupported multiple strong operators with ambiguous EOS attempt depth - assert property (@(posedge clk) ((a ##1 b) or(c ##1 d)) ##1 (e [-> 1]) |-> s_always[1: 2] a); - - // Multiple strong operators require too many EOS ring slots - assert property (@(posedge clk) ((a ##1 b) or(c ##1 d)) |-> s_always[1030: 1030] e) cnt++; - - // Unsupported: strong s_always pending state has a non-positive temporal depth - assert property (@(posedge clk) ((a ##1 b) or(c ##1 d)) |-> s_always[0: 0] e) cnt++; - - // Unsupported strong pass multiplicity when temporal OR loses resolved attempts - assert property (@(posedge clk) a |-> (((a ##1 b) or(c ##1 d)) |-> s_always[1: 2] e)) cnt++; - - // Composite sequence operators the count engine cannot lower - // verilog_format: off - assert property (@(posedge clk) (a ##1 b) or (c ##2 d)); - - cover property (@(posedge clk) (a ##[1:$] b) and (c ##1 d)); - - // One done latch cannot identify ranged endpoints for overlapping attempts. - assert property (@(posedge clk) (a ##[1:2] b) and (c ##2 d)); - - assert property (@(posedge clk) (a ##1 b) |-> not (c ##[1:2] d)) $display("pass"); - - assert property (@(posedge clk) ((|($random | $random))[*2]) and (1'b1 ##1 1'b1)); - assert property (@(posedge clk) ((|($urandom | $urandom))[*2]) and (1'b1 ##1 1'b1)); - - assert property (@(posedge clk) (a[*2000]) and (1'b1 ##1 1'b1)); - - assert property (@(posedge clk) - (s_always [1:2] b) and (((a ##2 b) or (c ##2 d)) ##[1:300] a)) else $display("f"); - - assert property (@(posedge clk) ##[1:$] (a until b)); - // verilog_format: on - // Property if/else control the fail-only count engine cannot lower assert property (@(posedge clk) if (a) 1'b1 ##1 b else 1'b1 ##2 c) $display("pass"); cover property (@(posedge clk) if (a) 1'b1 ##1 b else 1'b1 ##2 c); @@ -104,58 +31,17 @@ module t ( endcase) $display("pass"); - assert property (@(posedge clk) if (a) s_always[1:2] b else 1'b1 ##1 c); - - assert property (@(posedge clk) - if ($random == 0) 1'b1 ##1 b - else 1'b1 ##1 c); - // An unsupported body under an abort reports itself, not an internal error assert property (@(posedge clk) sync_accept_on (a) ((b ##1 c) [* 2])); // A body the builder rejects wins over the property if/case message assert property (@(posedge clk) if (a) ((b ##1 c)[*2]) else d) $display("pass"); - // Fixed-trace conjunction rejects each non-flattenable operand form - assert property (@(posedge clk) (a [* 1: $]) and(b ##1 c)); - assert property (@(posedge clk) ((b ##1 c) [* 2]) and(d ##2 e)); - assert property (@(posedge clk) ((a ##[1:2] b) and(c ##1 d)) and(e ##1 a)); - assert property (@(posedge clk) (a throughout ((b ##1 c) or(d ##1 e))) and(c ##2 a)); - - // Variable/unbounded 'and' rejection in a cover context - cover property (@(posedge clk) (a [-> 1]) and(b [* 2])); - - // Per-operand rejection inside temporal 'or' - assert property (@(posedge clk) (a ##1 b) or(fimp() ##1 c)); - assert property (@(posedge clk) (b throughout (1'b1 ##2 1'b1)) or(c ##2 d)); - assert property (@(posedge clk) (c ##2 d) or(b throughout (1'b1 ##2 1'b1))); - - // Nested fixed-trace conjunctions reject through the enclosing operator - assert property (@(posedge clk) (((a ##1 b) or(c ##1 d)) and(e ##1 a)) and(b ##1 c)); - assert property (@(posedge clk) ((fimp() ##1 a) and(b ##1 c)) and(d ##1 e)); - assert property (@(posedge clk) (a ##1 b) and(fimp() ##1 c)); - assert property (@(posedge clk) (##1 fimp()) and(b ##1 c)); - assert property (@(posedge clk) (((a ##1 b) or(c ##1 d)) and(e ##1 a)) or(b ##1 c)); - assert property (@(posedge clk) (b ##1 c) or(((a ##1 b) or(c ##1 d)) and(e ##1 a))); - assert property (@(posedge clk) (a ##2 b) intersect ((fimp() ##2 c) or(d ##2 e))); - assert property (@(posedge clk) (a ##[1:2] b) and((fimp() ##1 c) or(d ##1 e))); - assert property (@(posedge clk) ((fimp() ##1 c) or(d ##1 e)) and(a ##[1:2] b)); - - // Variable-length repetition operands of a temporal 'or' - assert property (@(posedge clk) ((a [* 1: 2]) and(b ##1 c)) or(d ##1 e)); - assert property (@(posedge clk) ((a ##1 b) and(c [* 1: 2])) or(d ##1 e)); - - // A sequence operand of 'until', a throughout body the builder rejects, if/case under an abort - assert property (@(posedge clk) (a ##1 b) until c); - assert property (@(posedge clk) a throughout ((b ##1 c) [* 2])); - assert property (@(posedge clk) sync_accept_on (a) (if (b) 1'b1 ##1 c else 1'b1 ##2 d)); - cover property (@(posedge clk) (a ##1 b) |-> not (c ##[1:2] d)); - // A named property instance nested in a composite is rejected, not dropped assert property (@(posedge clk) p_nested or e); // A named sequence instance is inlined, not rejected - assert property (@(posedge clk) s_nested or(c ##1 d)); + assert property (@(posedge clk) s_nested or e); // A function call is not a property instance assert property (@(posedge clk) fbool() ##1 b); @@ -171,51 +57,9 @@ module t ( // An 'and' operand carrying mid-window sources defers to later passes assert property (@(posedge clk) (1'b1 ##[1:2] b) and c); - assert property (@(posedge clk) c and(1'b1 ##[1:2] b)); + assert property (@(posedge clk) c and (1'b1 ##[1:2] b)); // A boolean 'and' operand of a rejected cover-sequence 'or' is freed cover sequence (@(posedge clk) ((a and b) or(c ##1 d))); - // A constant strong 'or' operand is not folded - assert property (@(posedge clk) (s_always [1:2] 1'b1) or(a ##2 b)); - - // A ranged repetition in a cover sequence counts ring occupants per end - cover sequence (@(posedge clk) a [* 1: 3]); - - // A constant-true 'or' operand of a cover sequence folds away - cover sequence (@(posedge clk) (1'b1 or c) ##1 d); - cover sequence (@(posedge clk) (c or 1'b1) ##1 d); - - // A nonconsecutive repetition operand of 'or' is diagnosed at the 'or' - assert property (@(posedge clk) (a [= 1] ##1 b) or(c ##2 d)); - assert property (@(posedge clk) (c ##2 d) or(a [= 1] ##1 b)); - - // An 'or' under intersect in a cover property is lowered - cover property (@(posedge clk) (a ##2 b) intersect ((c ##2 d) or(e ##2 a))); - - // An unbounded 'and' operand is lowered through the combiner - assert property (@(posedge clk) (a ##[1:$] b) and(c ##1 d)); - - // A nested 'and' whose right operand cannot be flattened - assert property (@(posedge clk) ((a ##1 b) and((c ##1 d) or(e ##1 a))) and(b ##1 c)); - assert property (@(posedge clk) ((a ##1 b) and(fimp() ##1 c)) and(d ##1 e)); - - // Property if/else with a boolean else branch is lowered - assert property (@(posedge clk) if (a) 1'b1 ##1 b else c); - - // A fixed-count repetition range and a four-state constant operand are lowered - assert property (@(posedge clk) (a [* 2: 2]) and(b ##1 c)); - assert property (@(posedge clk) (always [1:2] 1'bx) or(a ##2 b)); - - // A boolean operand of a bounded variable-length 'and' is lowered - assert property (@(posedge clk) a and(b [* 1: 2])); - assert property (@(posedge clk) (b [* 1: 2]) and a); - - // A property if/else branch ending in throughout is lowered - assert property (@(posedge clk) if (a) (b throughout (1'b1 ##1 c)) else d); - - // Left-operand mirrors of the intersect rejections - assert property (@(posedge clk) (a throughout ((b ##2 c) or(d ##2 e))) intersect (c ##2 a)); - assert property (@(posedge clk) ((fimp() ##2 c) or(d ##2 e)) intersect (a ##2 b)); - endmodule diff --git a/test_regress/t/t_property_nfa_reactive_nba.py b/test_regress/t/t_property_nfa_reactive_nba.py deleted file mode 100755 index bedaf0367..000000000 --- a/test_regress/t/t_property_nfa_reactive_nba.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -# DESCRIPTION: Verilator: Verilog Test driver/expect definition -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of either the GNU Lesser General Public License Version 3 -# or the Perl Artistic License Version 2.0. -# SPDX-FileCopyrightText: 2026 Wilson Snyder -# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 - -import vltest_bootstrap - -test.scenarios('vlt_all') - -test.compile(timing_loop=True, verilator_flags2=['--assert', '--timing', '--stats']) - -test.execute() - -test.file_grep(test.stats, r'NBA, variables using ShadowVarMasked scheme\s+(\d+)', 2) -test.file_grep(test.stats, r'NBA, variables using FlagShared scheme\s+(\d+)', 0) -test.file_grep(test.stats, r'NBA, variables using FlagUnique scheme\s+(\d+)', 4) -test.file_grep(test.stats, r'NBA, variables using ValueQueuePartial scheme\s+(\d+)', 1) -test.file_grep(test.stats, r'NBA, variables using ValueQueueWhole scheme\s+(\d+)', 1) - -test.passes() diff --git a/test_regress/t/t_property_nfa_reactive_nba.v b/test_regress/t/t_property_nfa_reactive_nba.v deleted file mode 100644 index 049df4360..000000000 --- a/test_regress/t/t_property_nfa_reactive_nba.v +++ /dev/null @@ -1,204 +0,0 @@ -// DESCRIPTION: Verilator: Verilog Test module -// -// This file ONLY is placed under the Creative Commons Public Domain. -// SPDX-FileCopyrightText: 2026 PlanV GmbH -// SPDX-License-Identifier: CC0-1.0 - -// NBAs issued from a Reactive assertion action commit in the same time slot. - -// verilog_format: off -`define stop $stop -`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0h exp=%0h\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); -// verilog_format: on - -module t ( - input clk -); - - int cyc = 0; - bit a = 1; - bit b = 0; - - int action_ordinal = 0; - int lhs_index_calls = 0; - - function automatic int next_lhs_index(ref int calls); - /* verilator no_inline_task */ - next_lhs_index = calls; - calls++; - endfunction - - typedef logic [7:0] unpacked_pair_t[0:1]; - logic [7:0] scalar = 8'h80; - logic [15:0] packed_parts = 16'h0000; - unpacked_pair_t unpacked_whole = '{8'ha0, 8'hb0}; - unpacked_pair_t whole_first = '{8'h11, 8'h19}; - unpacked_pair_t whole_last = '{8'h22, 8'h2a}; - logic [7:0] unpacked_elements[0:3] = '{default: 8'h00}; - - // Targets for Reactive-NBA lowering schemes - logic [15:0] masked_parts = 16'h0000; - logic [7:0] unique_flag = 8'h00; - typedef struct { - logic [7:0] lo; - logic [7:0] hi; - } pair_t; - pair_t unique_pair = '{lo: 8'h0a, hi: 8'h90}; - /* verilator lint_off MULTIDRIVEN */ - logic [7:0] mixed_nba = 8'h90; - logic [15:0] queued_parts[0:1] = '{default: 16'hf000}; - /* verilator lint_on MULTIDRIVEN */ - logic [7:0] second_wave = 8'h00; - - time match_slot_time = 0; - bit renba_event_seen = 0; - bit masked_event_seen = 0; - bit unique_event_seen = 0; - bit queue_event_seen = 0; - - assert property (@(posedge clk) a ##[1:2] b) begin - action_ordinal++; - - scalar <= action_ordinal[7:0]; - - packed_parts[next_lhs_index(lhs_index_calls)*4+:4] <= action_ordinal[3:0]; - packed_parts[15:12] <= 4'h8 + action_ordinal[3:0]; - - if (action_ordinal == 1) unpacked_whole <= whole_first; - else unpacked_whole <= whole_last; - - unpacked_elements[action_ordinal-1] <= 8'h30 + action_ordinal[7:0]; - unpacked_elements[3] <= 8'h40 + action_ordinal[7:0]; - - // Mixed blocking/nonblocking updates to disjoint bits - masked_parts[7:0] = action_ordinal[7:0]; - masked_parts[15:8] <= 8'h60 + action_ordinal[7:0]; - - queued_parts[(action_ordinal-1)%2][7:0] <= 8'h70 + action_ordinal[7:0]; - queued_parts[1][15:12] <= action_ordinal[3:0]; - - mixed_nba <= 8'ha0 + action_ordinal[7:0]; - - fork - unique_flag <= 8'h50 + action_ordinal[7:0]; - unique_pair.hi[3:0] <= action_ordinal[3:0]; - join - end - - always @(posedge clk) begin - cyc <= cyc + 1; - if (cyc == 0) begin - a <= 1; - b <= 0; - end - else if (cyc == 1) begin - a <= 0; - b <= 1; - end - else if (cyc == 3) begin - a <= 1; - b <= 0; - end - else if (cyc == 4) begin - a <= 0; - b <= 1; - end - - if (cyc == 2) begin - mixed_nba <= 8'h55; - match_slot_time = $time; - $strobe("RE-NBA scalar=%02x packed=%04x whole=%02x,%02x elems=%02x,%02x,%02x,%02x", scalar, - packed_parts, unpacked_whole[0], unpacked_whole[1], unpacked_elements[0], - unpacked_elements[1], unpacked_elements[2], unpacked_elements[3]); - end - else if (cyc == 3) begin - mixed_nba <= 8'h66; - end - else if (cyc == 5) begin - mixed_nba <= 8'h77; - end - end - - // Same-slot observation via a process sensitive to the Re-NBA commit - always @(scalar) begin - if (scalar == 8'h02) begin - `checkd($time, match_slot_time); - `checkd(action_ordinal, 2); - renba_event_seen = 1; - second_wave <= scalar + 8'h10; - end - end - - always @(second_wave) begin - if (second_wave == 8'h12) `checkd($time, match_slot_time); - end - - always @(masked_parts) begin - if (masked_parts == 16'h6202) begin - `checkd($time, match_slot_time); - masked_event_seen = 1; - end - end - - always @(unique_flag) begin - if (unique_flag == 8'h52) begin - `checkd($time, match_slot_time); - unique_event_seen = 1; - end - end - - always @(queued_parts[0] or queued_parts[1]) begin - if (queued_parts[0] == 16'hf071 && queued_parts[1] == 16'h2072) begin - `checkd($time, match_slot_time); - queue_event_seen = 1; - end - end - - initial begin - repeat (3) @(negedge clk); - `checkd(renba_event_seen, 1); - `checkd(action_ordinal, 2); - `checkd(lhs_index_calls, 2); - `checkd(scalar, 8'h02); - `checkd(second_wave, 8'h12); - `checkd(packed_parts, 16'ha021); - `checkd(unpacked_whole[0], 8'h22); - `checkd(unpacked_whole[1], 8'h2a); - `checkd(unpacked_elements[0], 8'h31); - `checkd(unpacked_elements[1], 8'h32); - `checkd(unpacked_elements[2], 8'h00); // zero-ok: untouched element - `checkd(unpacked_elements[3], 8'h42); - `checkd(masked_event_seen, 1); - `checkd(masked_parts, 16'h6202); - `checkd(unique_event_seen, 1); - `checkd(unique_flag, 8'h52); - `checkd(unique_pair.lo, 8'h0a); - `checkd(unique_pair.hi, 8'h92); - `checkd(mixed_nba, 8'ha2); - `checkd(queue_event_seen, 1); - `checkd(queued_parts[0], 16'hf071); - `checkd(queued_parts[1], 16'h2072); - - @(negedge clk); - `checkd(mixed_nba, 8'h66); - - repeat (2) @(negedge clk); - `checkd(action_ordinal, 3); - `checkd(lhs_index_calls, 3); - `checkd(scalar, 8'h03); - `checkd(packed_parts, 16'hb321); - `checkd(unpacked_whole[0], 8'h22); - `checkd(unpacked_whole[1], 8'h2a); - `checkd(unpacked_elements[2], 8'h33); - `checkd(unpacked_elements[3], 8'h43); - `checkd(masked_parts, 16'h6303); - `checkd(unique_flag, 8'h53); - `checkd(unique_pair.hi, 8'h93); - `checkd(mixed_nba, 8'ha3); - `checkd(queued_parts[0], 16'hf073); - `checkd(queued_parts[1], 16'h3072); - $write("*-* All Finished *-*\n"); - $finish; - end - -endmodule diff --git a/test_regress/t/t_property_nfa_stop_error_limit_crossing.out b/test_regress/t/t_property_nfa_stop_error_limit_crossing.out index 6f68a6d7f..7d42f0a44 100644 --- a/test_regress/t/t_property_nfa_stop_error_limit_crossing.out +++ b/test_regress/t/t_property_nfa_stop_error_limit_crossing.out @@ -1,2 +1,3 @@ +ACTION_RAN %Error: t/t_property_nfa_stop_error_limit_crossing.v:23: Verilog $stop Aborting... diff --git a/test_regress/t/t_property_nfa_sync_abort_counts.py b/test_regress/t/t_property_nfa_sync_abort_counts.py deleted file mode 100755 index 926563ace..000000000 --- a/test_regress/t/t_property_nfa_sync_abort_counts.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# DESCRIPTION: Verilator: Verilog Test driver/expect definition -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of either the GNU Lesser General Public License Version 3 -# or the Perl Artistic License Version 2.0. -# SPDX-FileCopyrightText: 2026 Wilson Snyder -# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 - -import vltest_bootstrap - -test.scenarios('vlt_all') - -test.compile(timing_loop=True, verilator_flags2=['--assert', '--timing']) - -test.execute() - -test.passes() diff --git a/test_regress/t/t_property_nfa_sync_abort_counts.v b/test_regress/t/t_property_nfa_sync_abort_counts.v deleted file mode 100644 index 6ec561f63..000000000 --- a/test_regress/t/t_property_nfa_sync_abort_counts.v +++ /dev/null @@ -1,75 +0,0 @@ -// DESCRIPTION: Verilator: Verilog Test module -// -// This file ONLY is placed under the Creative Commons Public Domain. -// SPDX-FileCopyrightText: 2026 PlanV GmbH -// SPDX-License-Identifier: CC0-1.0 - -// verilog_format: off -`define stop $stop -`define checkh(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0x exp=%0x (%s !== %s)\n", `__FILE__,`__LINE__, (gotv), (expv), `"gotv`", `"expv`"); `stop; end while(0); -`define checkd(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0); -// verilog_format: on - -module t ( - input clk -); - - // These multi-cycle counts are a nonvacuous-attempt oracle. - initial $assertvacuousoff; - - int cyc; - reg [63:0] crc; - - // Non-adjacent CRC bits to avoid LFSR shift correlation - wire a = crc[0]; - wire b = crc[4]; - wire cnd_a = crc[8]; - wire cnd_r = crc[12]; - - int multi_pass_pair1 = 0; - int multi_fail_pair1 = 0; - int multi_pass_pair2 = 0; - int multi_fail_pair2 = 0; - int multi_pass_nested = 0; - int multi_fail_nested = 0; - - // Multi-cycle sync abort forms; accept-forced passes suppressed by $assertvacuousoff. - assert property (@(posedge clk) sync_accept_on (cnd_a) (a |-> ##1 b)) multi_pass_pair1++; - else multi_fail_pair1++; - - assert property (@(posedge clk) sync_reject_on (cnd_r) (a |-> b)) - else multi_fail_pair1++; - - assert property (@(posedge clk) sync_accept_on (cnd_a) b) multi_pass_pair2++; - else multi_fail_pair2++; - - assert property (@(posedge clk) sync_reject_on (cnd_r) (a |-> ##2 b)) multi_pass_pair2++; - else multi_fail_pair2++; - - assert property (@(posedge clk) sync_accept_on (cnd_a) sync_reject_on (cnd_r) (a |-> b)) - multi_pass_nested++; - else multi_fail_nested++; - - always @(posedge clk) begin -`ifdef TEST_VERBOSE - $write("[%0t] cyc==%0d crc=%x a=%b b=%b cnd_a=%b cnd_r=%b\n", $time, cyc, crc, a, b, cnd_a, - cnd_r); -`endif - cyc <= cyc + 1; - crc <= {crc[62:0], crc[63] ^ crc[2] ^ crc[0]}; - if (cyc == 0) begin - crc <= 64'h5aef0c8d_d70a4497; - end - else if (cyc == 99) begin - `checkh(crc, 64'hc77bb9b3784ea091); - `checkd(multi_pass_pair1, 5); // Other simulator: 42 - `checkd(multi_fail_pair1, 65); // Other simulator: 39 - `checkd(multi_pass_pair2, 24); // Other simulator: 74 - `checkd(multi_fail_pair2, 99); // Other simulator: 73 - `checkd(multi_pass_nested, 7); // Other simulator: 30 - `checkd(multi_fail_nested, 27); // Other simulator: 18 - $write("*-* All Finished *-*\n"); - $finish; - end - end -endmodule diff --git a/test_regress/t/t_property_nfa_throughout_count.v b/test_regress/t/t_property_nfa_throughout_count.v index 7339076d5..32bcbf47c 100644 --- a/test_regress/t/t_property_nfa_throughout_count.v +++ b/test_regress/t/t_property_nfa_throughout_count.v @@ -65,10 +65,10 @@ module t ( end #1 disable_now = 0; @(negedge clk) begin - `checkd(fixed_fail, 3); - `checkd(range_fail, 3); - `checkd(negated_pass, 4); - `checkd(negated_cover, 4); + `checkd(fixed_fail, 1); + `checkd(range_fail, 1); + `checkd(negated_pass, 1); + `checkd(negated_cover, 1); `checkd(disabled_fail, 0); drop_tag = 0; kill_start = 1; diff --git a/test_regress/t/t_property_s_until_nfa.v b/test_regress/t/t_property_s_until_nfa.v index 21c25c05d..d187cd9c9 100644 --- a/test_regress/t/t_property_s_until_nfa.v +++ b/test_regress/t/t_property_s_until_nfa.v @@ -45,12 +45,12 @@ module t ( assert property (@(posedge clk) (cyc == 1) |=> 1'b1 s_until 1'b0); final begin - `checkd(pass_count[0], 5); - `checkd(pass_count[1], 5); - `checkd(pass_count[2], 5); - `checkd(pass_count[3], 5); - `checkd(pass_count[4], 4); - `checkd(pass_count[5], 4); + `checkd(pass_count[0], 6); + `checkd(pass_count[1], 6); + `checkd(pass_count[2], 6); + `checkd(pass_count[3], 6); + `checkd(pass_count[4], 5); + `checkd(pass_count[5], 5); end always @(posedge clk) begin diff --git a/test_regress/t/t_property_sexpr.out b/test_regress/t/t_property_sexpr.out index 7d17cae3c..0c9ae3f6a 100644 --- a/test_regress/t/t_property_sexpr.out +++ b/test_regress/t/t_property_sexpr.out @@ -1,60 +1,134 @@ +[2] triggered e1 +[4] triggered e1 [4] single delay with const stmt, fileline:99 +[6] triggered e1 [6] single delay with const stmt, fileline:99 +[8] triggered e2 +[10] triggered e2 [10] single delay with var stmt, fileline:102 +[12] triggered e2 [12] single delay with var else, fileline:103 +[14] triggered e2 [14] single delay with var stmt, fileline:102 +[16] triggered e2 [16] single delay with var else, fileline:103 +[18] triggered e2 [18] single delay with var stmt, fileline:102 +[20] triggered e3 +[22] triggered e3 [22] stmt1, fileline:106 [22] stmt2, fileline:107 +[24] triggered e3 [24] else1, fileline:110 [24] else2, fileline:111 +[26] triggered e3 [26] stmt1, fileline:106 [26] stmt2, fileline:107 +[28] triggered e3 [28] else1, fileline:110 [28] else2, fileline:111 +[30] triggered e4 +[32] triggered e4 +[34] triggered e4 [34] single multi-cycle delay with var stmt, fileline:115 +[36] triggered e4 [36] single multi-cycle delay with var else, fileline:116 +[38] triggered e4 [38] single multi-cycle delay with var stmt, fileline:115 +[40] triggered e5 +[42] triggered e5 [42] single delay with var brackets 1 stmt, fileline:119 +[44] triggered e5 [44] single delay with var brackets 1 else, fileline:120 +[46] triggered e5 [46] single delay with var brackets 1 stmt, fileline:119 +[48] triggered e5 [48] single delay with var brackets 1 else, fileline:120 +[50] triggered e6 +[52] triggered e6 [52] single delay with var brackets 2 else, fileline:124 +[54] triggered e6 [54] single delay with var brackets 2 stmt, fileline:123 +[56] triggered e6 [56] single delay with var brackets 2 else, fileline:124 +[58] triggered e6 [58] single delay with var brackets 2 stmt, fileline:123 +[60] triggered e7 +[62] triggered e7 [62] single delay with and var stmt, fileline:127 +[64] triggered e7 [64] single delay with and var else, fileline:128 +[66] triggered e7 [66] single delay with and var else, fileline:128 +[68] triggered e7 [68] single delay with and var else, fileline:128 +[70] triggered e8 +[72] triggered e8 [72] single delay with negated var stmt, fileline:131 +[74] triggered e8 [74] single delay with negated var else, fileline:132 +[76] triggered e8 [76] single delay with negated var stmt, fileline:131 +[78] triggered e8 [78] single delay with negated var else, fileline:132 +[80] triggered e9 +[82] triggered e9 [82] single delay with negated var brackets else, fileline:137 +[84] triggered e9 [84] single delay with negated var brackets stmt, fileline:135 +[86] triggered e9 [86] single delay with negated var brackets else, fileline:137 +[88] triggered e9 [88] single delay with negated var brackets stmt, fileline:135 +[90] triggered e10 +[92] triggered e10 +[94] triggered e10 [94] single delay with negated var brackets else, fileline:141 +[96] triggered e10 +[98] triggered e10 [98] single delay with negated var brackets else, fileline:141 +[100] triggered e11 +[102] triggered e11 [102] single delay with nested not stmt, fileline:144 +[104] triggered e11 [104] single delay with nested not else, fileline:145 +[106] triggered e11 [106] single delay with nested not stmt, fileline:144 +[108] triggered e11 [108] single delay with nested not else, fileline:145 +[110] triggered e12 +[112] triggered e12 +[114] triggered e12 [114] stmt, fileline: 148 +[116] triggered e12 [116] else, fileline: 150 +[118] triggered e12 [118] stmt, fileline: 148 +[120] triggered e13 +[122] triggered e13 [122] property, fileline:161 +[124] triggered e13 [124] property, fileline:160 +[126] triggered e13 [126] property, fileline:161 +[128] triggered e13 [128] property, fileline:160 +[130] triggered e14 +[132] triggered e14 [132] else, fileline: 166 +[134] triggered e14 [134] stmt, fileline: 164 +[136] triggered e14 [136] else, fileline: 166 +[138] triggered e14 [138] else, fileline: 166 +[140] triggered e15 [140] else, fileline: 171 +[142] triggered e15 +[144] triggered e15 [144] else, fileline: 171 [144] else, fileline: 171 +[146] triggered e15 +[148] triggered e15 [148] else, fileline: 171 *-* All Finished *-* diff --git a/test_regress/t/t_property_sexpr.v b/test_regress/t/t_property_sexpr.v index fdb0dc0cb..a48d6631a 100644 --- a/test_regress/t/t_property_sexpr.v +++ b/test_regress/t/t_property_sexpr.v @@ -4,8 +4,8 @@ // SPDX-FileCopyrightText: 2025 Antmicro // SPDX-License-Identifier: CC0-1.0 -// No display here: it would race the reactive action prints under --threads -`define TRIGGER(e) ->e +`define STRINGIFY(x) `"x`" +`define TRIGGER(e) ->e; $display("[%0t] triggered %s", $time, `STRINGIFY(e)) module t ( /*AUTOARG*/ // Inputs diff --git a/test_regress/t/t_sequence_bool_ops.v b/test_regress/t/t_sequence_bool_ops.v index 519863629..1bab5c01d 100644 --- a/test_regress/t/t_sequence_bool_ops.v +++ b/test_regress/t/t_sequence_bool_ops.v @@ -43,7 +43,6 @@ module t ( else if (cyc == 99) begin `checkh(crc, 64'hc77bb9b3784ea091); `checkh(sum, 64'hdb7bc8bfe61f987e); - `checkh(dc_pass, 'h62); $write("*-* All Finished *-*\n"); $finish; end @@ -99,65 +98,34 @@ module t ( assert property (@(posedge clk) (a | b) |-> (a ##1 1'b1) or (b ##1 1'b1)); + // Sequence 'or': different lengths, CRC-driven + assert property (@(posedge clk) + (a | b) |-> (a ##1 1'b1) or (b ##2 1'b1)); + + // Sequence 'or': standalone (constant, always passes) + assert property (@(posedge clk) + (1'b1 ##1 1'b1) or (1'b1 ##2 1'b1)); + // Sequence 'or' where both branches end in registered state (no finalCondp). - // Exercises guardedLink(termVertexp,...) in buildOrMerge for both sides. + // Exercises guardedLink(termVertexp,...) in buildOrCombiner for both sides. cover property (@(posedge clk) (a[*2]) or (b[*2])); - // Unequal-end 'or' and unbounded temporal 'and' diagnostics live in - // t_property_nfa_msgs_unsup. + // Sequence 'and' with unbounded range on one side -- marks combiner unbounded. + cover property (@(posedge clk) (a ##[1:$] b) and (c ##1 d)); // ========================================================================= // SAnd edge cases (NFA builder coverage) // ========================================================================= - // Fixed-end SAnd forms are flattened into a single per-cycle trace. + // SAnd where only one side has finalCondp: a[*2] has no finalCond (consumed + // by ConsRep); b ##1 c has finalCond=c. Exercises single-cond path in + // buildAndCombiner. cover property (@(posedge clk) (a[*2]) and (b ##1 c)); + // SAnd where both sides lack finalCondp (both end with registered state). + // Exercises buildMatchNow(!condp) path. cover property (@(posedge clk) (a[*2]) and (b[*2])); - // An unbounded single-end operand uses the persistent endpoint combiner. - assert property (@(posedge clk) (1'b1 ##1 1'b1) and (1'b1 [-> 1])); - assert property (@(posedge clk) (1'b1 [-> 1]) and (1'b1 ##1 1'b1)); - assert property (@(posedge clk) disable iff (cyc < 0) (1'b1 ##1 1'b1) and (1'b1 [-> 1])); - - // Nested fixed-end conjunctions exercise recursive trace flattening. - assert property (@(posedge clk) ((1'b1 ##1 1'b1) and (1'b1 ##1 1'b1)) and (1'b1 ##1 1'b1)); - - // A boolean intersect is folded before the following temporal step. - assert property (@(posedge clk) (1'b1 intersect 1'b1) ##1 1'b1); - - // A constant-true always operand cannot reject early. - assert property (@(posedge clk) (always [1:2] 1'b1) or (c ##2 d)); - - // Non-overlapped implication and mixed branch forms in property control. - assert property (@(posedge clk) case (a) 1'b0: 1'b0 |=> b; default: 1'b1; endcase); - assert property (@(posedge clk) if (a) 1'b1 else (1'b1 ##1 1'b1)); - - // Same-end intersect nested in an unbounded window. - assert property (@(posedge clk) 1'b0 |-> (a ##[1:$] - (((1'b1 ##1 1'b1) or (1'b1 ##1 1'b1)) intersect (1'b1 ##1 1'b1)))); - - // Constant 'or' operands fold to the constant or the remaining sequence. - assert property (@(posedge clk) (a ##1 1'b1) or 1'b1); - assert property (@(posedge clk) 1'b0 or (1'b1 ##1 1'b1)); - - // Ranged consecutive repetition under 'and' uses the variable-length combiner. - assert property (@(posedge clk) 1'b0 |-> ((a [* 1:3]) and (b ##2 c))); - - // A wide constant with a small value is a legal repetition count. - assert property (@(posedge clk) 1'b1 [* 66'd2]); - - // Default-clocking action handlers (clockless assertion forms). - int dc_pass = 0, dc_vac = 0, dc_cover = 0; - - default clocking cb @(posedge clk); - endclocking - - assert property (1'b1 ##1 1'b1) dc_pass++; - else $display("dc fail"); - assert property (b |-> ##1 1'b1) dc_vac++; - cover property (1'b1 ##1 1'b1) dc_cover++; - // ========================================================================= // Negated cover property (NFA assembleResult negated+cover branches) // Different shapes to cover the matchCondp/rejectBasep/throughoutRejectp diff --git a/test_regress/t/t_sequence_sexpr_throughout.v b/test_regress/t/t_sequence_sexpr_throughout.v index 43caa6e1a..8b9fe3cfa 100644 --- a/test_regress/t/t_sequence_sexpr_throughout.v +++ b/test_regress/t/t_sequence_sexpr_throughout.v @@ -89,7 +89,10 @@ module t ( `checkd(count_fail2, 33); `checkd(count_fail3, 31); `checkd(count_fail4, 35); - `checkd(count_fail5, 36); + // count_fail5: NFA undercounts by 12; throughout+temporal-and first-step + // rejection is a known limitation of the SAnd combiner architecture + // (propagating isTopLevelStep causes double-counting; fix is future work). + `checkd(count_fail5, 25); // All other sims: 36 `checkd(count_fail6, 33); $write("*-* All Finished *-*\n"); $finish; diff --git a/test_regress/t/t_sequence_within.v b/test_regress/t/t_sequence_within.v index 34e2a43f7..a7cc3c5ed 100644 --- a/test_regress/t/t_sequence_within.v +++ b/test_regress/t/t_sequence_within.v @@ -102,16 +102,16 @@ module t ( // p1/p2/p5 use |->; the NFA currently fires the pass action on // vacuous passes too, so counts are inflated vs. others. Pre-existing // engine-wide behavior, not within-specific. - `checkd(count_p1, 23); // One other sim: 16 - `checkd(count_p2, 44); // One other sim: 21 - `checkd(count_p3, 20); - `checkd(count_p4, 22); + `checkd(count_p1, 23); // Other sims: 23, or 16 + `checkd(count_p2, 44); // Other sims: 44, or 21 + `checkd(count_p3, 24); // Other sims: 20 + `checkd(count_p4, 23); // Other sims: 22 `checkd(count_p5, 26); - `checkd(count_p6, 16); - `checkd(count_p7, 9); - `checkd(count_p8, 4); - `checkd(count_p9, 10); - `checkd(count_p10, 15); + `checkd(count_p6, 21); // Other sims: 16 + `checkd(count_p7, 15); // Other sims: 9 + `checkd(count_p8, 15); // Other sims: 4 + `checkd(count_p9, 15); // Other sims: 10 + `checkd(count_p10, 21); // Other sims: 15 $write("*-* All Finished *-*\n"); $finish; end