// -*- mode: C++; c-file-style: "cc-mode" -*- //************************************************************************* // DESCRIPTION: Verilator: NFA-based multi-cycle SVA assertion evaluation // // Code available from: https://verilator.org // //************************************************************************* // // 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: 2005-2026 Wilson Snyder // SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 // //************************************************************************* // 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. // // Members marked OWNED hold an AST tree this pass allocated and must delete; // they are not linked into the netlist. // //************************************************************************* #include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT #include "V3AssertNfa.h" #include "V3Assert.h" #include "V3Const.h" #include "V3Graph.h" #include "V3Stats.h" #include "V3Task.h" #include "V3UniqueNames.h" #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) namespace { 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* doneLVarp = nullptr; // SAnd LHS done-latch AstVar* doneRVarp = nullptr; // SAnd RHS done-latch AstNodeExpr* stateSigp = nullptr; // Combinational state signal; OWNED during lowering bool needsReg = false; // True if vertex has incoming clocked edge }; // NFA state vertex -- one per NFA position in the sequence evaluation class SvaStateVertex final : public V3GraphVertex { VL_RTTI_IMPL(SvaStateVertex, V3GraphVertex) public: // True if this is the sequence-match terminal vertex bool m_isMatch = false; // OWNED throughout-guard condition clones; IEEE 1800-2023 16.9.9 std::vector m_throughoutConds; // Nonzero for a bitset ring-buffer vertex for ## delays. bool m_isFixedDelayRing = false; unsigned m_delayRingSize = 0; // Number of ring slots. Range: max-min+1. AstNodeExpr* m_delayRingClearCondp = nullptr; // local RHS for pure-boolean range AstNodeExpr* m_delayRingAdvanceCondp = nullptr; // Advance only when this condition holds SvaStateVertex* m_matchCountRingp = nullptr; // Ring supplying this checked match's count bool m_replayAbortReject = false; // Compressed repetition needs per-thread abort replay // OWNED; enclosing-abort fire condition clearing state or suppressing guard rejection AstNodeExpr* m_abortClearp = nullptr; // OWNED; reject-abort fire condition rejecting all represented live threads 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 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) AstNodeExpr* m_andRhsCondp = nullptr; // OWNED; RHS final condition (may be nullptr) // Reject sink for SAnd rejectOnFail wiring; not a state-signal source bool m_isRejectSink = false; // In-window vertex of a strong s_always[m:n]: if its state is still set at // 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 // CONSTRUCTORS explicit SvaStateVertex(V3Graph* graphp) : V3GraphVertex{graphp} {} ~SvaStateVertex() override { for (AstNodeExpr* cp : m_throughoutConds) VL_DO_DANGLING(cp->deleteTree(), cp); if (m_delayRingClearCondp) VL_DO_DANGLING(m_delayRingClearCondp->deleteTree(), m_delayRingClearCondp); if (m_delayRingAdvanceCondp) VL_DO_DANGLING(m_delayRingAdvanceCondp->deleteTree(), m_delayRingAdvanceCondp); if (m_abortClearp) VL_DO_DANGLING(m_abortClearp->deleteTree(), m_abortClearp); if (m_abortRejectp) VL_DO_DANGLING(m_abortRejectp->deleteTree(), m_abortRejectp); if (m_andLhsCondp) VL_DO_DANGLING(m_andLhsCondp->deleteTree(), m_andLhsCondp); if (m_andRhsCondp) VL_DO_DANGLING(m_andRhsCondp->deleteTree(), m_andRhsCondp); } // METHODS // LCOV_EXCL_START -- Graphviz dump only string name() const override { string name = "s" + cvtToStr(color()); if (m_delayRingSize) { name += "\\n"; name += m_isFixedDelayRing ? "fixed chain " : "range chain "; name += cvtToStr(m_delayRingSize) + " bits"; } return name; } string dotColor() const override { if (m_isMatch) return "red"; if (m_delayRingSize) return "blue"; if (m_isAndCombiner) return "purple"; return "black"; } // LCOV_EXCL_STOP // Access per-vertex algorithm data (valid only during lowering phase) SvaVertexData* datap() const { return static_cast(userp()); } }; // NFA transition edge -- clocked (##1) or combinational link (##0) class SvaTransEdge final : public V3GraphEdge { VL_RTTI_IMPL(SvaTransEdge, V3GraphEdge) public: AstNodeExpr* m_condp; // Transition condition; nullptr = unconditional; OWNED bool m_consumesCycle; // true = clocked edge (##1), false = link (##0/boolean) // Reject when source is active and condp is false; set only on // outermost required-step Link bool m_rejectOnFail = false; // Optional dynamic condition vertex for m_rejectOnFail. Used when the // success condition is another NFA state rather than a static expression. SvaStateVertex* m_condVtxp = nullptr; // CONSTRUCTORS SvaTransEdge(V3Graph* graphp, V3GraphVertex* fromp, V3GraphVertex* top, AstNodeExpr* condp, bool consumesCycle) : V3GraphEdge{graphp, fromp, top, /*weight=*/1} , m_condp{condp} , m_consumesCycle{consumesCycle} {} ~SvaTransEdge() override { if (m_condp) VL_DO_DANGLING(m_condp->deleteTree(), m_condp); } // METHODS // LCOV_EXCL_START -- Graphviz dump only string dotLabel() const override { return m_consumesCycle ? "##1" : "link"; } string dotStyle() const override { return m_consumesCycle ? "" : "dashed"; } // LCOV_EXCL_STOP // Typed accessors for NFA vertices SvaStateVertex* fromVtxp() const { return static_cast(fromp()); } SvaStateVertex* toVtxp() const { return static_cast(top()); } }; // NFA graph container class SvaGraph final { 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}; } // Create the match terminal vertex SvaStateVertex* createMatchVertex() { SvaStateVertex* const vtxp = createStateVertex(); vtxp->m_isMatch = true; m_matchVertexp = vtxp; return vtxp; } // Add a clocked transition edge (##1) SvaTransEdge* addClockedEdge(SvaStateVertex* fromp, SvaStateVertex* top, AstNodeExpr* condp = nullptr) { return new SvaTransEdge{&m_graph, fromp, top, condp, /*consumesCycle=*/true}; } // Add a combinational link (##0 / boolean condition) SvaTransEdge* addLink(SvaStateVertex* fromp, SvaStateVertex* top, AstNodeExpr* condp = nullptr) { return new SvaTransEdge{&m_graph, fromp, top, condp, /*consumesCycle=*/false}; } // Collect all edges into a flat vector for iteration. // Used by the lowering phase which needs global edge scans. std::vector allEdges() const { std::vector result; for (const V3GraphVertex& vtxr : m_graph.vertices()) { for (const V3GraphEdge& edger : vtxr.outEdges()) { result.push_back(static_cast(&edger)); } } return result; } }; //###################################################################### // Builder result: terminal vertex + optional final condition (match Link condition). struct BuildResult final { SvaStateVertex* termVertexp; // Primary terminal; contributes to both match and reject AstNodeExpr* finalCondp; // nullptr = unconditional // Mid-window sources for range delays (pure boolean RHS): match-only (isUnbounded) std::vector midSources; bool errorEmitted = false; // Builder already emitted specific error; skip generic // For cover_sequence: when true, midSources already enumerate every // end-of-match, so wireMatchAndMidSources must NOT add the main // termVtxp -> matchVertex Link (would double-count via the merge vertex). bool termIsMidMerge = false; bool valid() const { return termVertexp != nullptr; } static BuildResult fail(bool errored = false) { return {nullptr, nullptr, {}, errored}; } static BuildResult failWithError() { return {nullptr, nullptr, {}, true}; } }; // Parser-marked SAnd of overlapped implications: a property if/else/case. static bool hasPropertyControlConjunction(const AstNodeExpr* nodep) { return nodep->exists([](const AstSAnd* andp) { return andp->propertyControl(); }); } // 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 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 + std::to_string(type) + ", "s + std::to_string(directiveType) + ")"s; } static const char* assertPassOnQuery(bool vacuous) { static constexpr const char* queries[2] = {"ASSERT_CTL_PASS_ON_NONVACUOUS", "ASSERT_CTL_PASS_ON_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}; } static AstNodeExpr* assertKillGet(FileLine* flp, VAssertType type, VAssertDirectiveType directiveType) { return new AstCExpr{flp, assertCtlGetCall("ASSERT_CTL_KILL", type, directiveType), 32}; } static string assertActionControlPrefix(VAssertDirectiveType directiveType) { const int controlled = !!(static_cast(directiveType) & (static_cast(VAssertDirectiveType::ASSERT) | static_cast(VAssertDirectiveType::COVER) | static_cast(VAssertDirectiveType::ASSUME))); const int checkRuntime = controlled & static_cast(v3Global.opt.assertOn()); return "("s + std::to_string(controlled ^ 1) + " || ("s + std::to_string(checkRuntime) + " && "s; } static AstNodeExpr* assertPassOnCond(FileLine* flp, VAssertType type, VAssertDirectiveType directiveType, bool vacuous) { return new AstCExpr{flp, assertActionControlPrefix(directiveType) + assertCtlGetCall(assertPassOnQuery(vacuous), type, directiveType) + "))"s, 1}; } static AstNodeExpr* assertFailOnCond(FileLine* flp, VAssertType type, VAssertDirectiveType directiveType) { return new AstCExpr{flp, assertActionControlPrefix(directiveType) + assertCtlGetCall("ASSERT_CTL_FAIL_ON", type, directiveType) + "))"s, 1}; } static AstIf* newPassOnIf(FileLine* flp, AstNodeExpr* firep, AstNode* bodyp, VAssertType type, VAssertDirectiveType directiveType, bool vacuous) { AstNodeExpr* const condp = new AstLogAnd{flp, firep, assertPassOnCond(flp, type, directiveType, vacuous)}; AstIf* const ifp = new AstIf{flp, condp, bodyp}; ifp->isBoundsCheck(true); ifp->user1(true); return ifp; } //###################################################################### // NFA Builder class SvaNfaBuilder final { SvaGraph& m_graph; // NFA graph being built AstNodeModule* const m_modp; // Module to receive hoisted sampled-prop temps V3UniqueNames& m_propTempNames; // Module-shared temp-var name source std::vector m_temporalGuardStack; // Guards active across nested temporal states // Outer abort conditions, AND-ed as !cond into inner abort edges // (IEEE 1800-2023 16.12.14 outer-wraps-inner). 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; struct RangeDelayRejectInfo final { SvaStateVertex* startp = nullptr; unsigned range = 0; int rhsLen = 0; }; void warnEndpointUnsupported(FileLine* flp, const string& what) const { if (m_isSeqEvent) { flp->v3warn(E_UNSUPPORTED, "Unsupported: sequence used as an event control with " << what); } else { flp->v3warn(COVERIGN, "Ignoring unsupported: cover sequence with " << what); } } AstNodeExpr* throughoutCond(AstNodeExpr* baseCondp, FileLine* flp) { if (m_temporalGuardStack.empty()) return baseCondp; // AND all active temporal guards (supports nesting) // Each must use $sampled values. AstNodeExpr* guardp = nullptr; for (AstNodeExpr* const condp : m_temporalGuardStack) { AstNodeExpr* const clonep = sampled(condp->cloneTreePure(false)); if (!guardp) { guardp = clonep; } else { guardp = new AstLogAnd{flp, guardp, clonep}; } } if (baseCondp) { guardp = new AstLogAnd{flp, baseCondp, guardp}; } return guardp; } static unsigned getConstUInt(AstNodeExpr* exprp) { AstNodeExpr* const constp = V3Const::constifyEdit(exprp->cloneTreePure(false)); const AstConst* const cp = VN_CAST(constp, Const); const unsigned val = cp ? cp->toUInt() : 0; VL_DO_DANGLING(constp->deleteTree(), constp); return val; } // Return a fixed clock-tick length, or -1 for variable/unbounded operands. static int fixedLength(AstNodeExpr* nodep) { if (AstSExpr* const sexprp = VN_CAST(nodep, SExpr)) { AstDelay* const delayp = VN_CAST(sexprp->delayp(), Delay); if (!delayp || !delayp->isCycleDelay()) return -1; unsigned delayCycles; if (delayp->isRangeDelay()) { if (delayp->isUnbounded()) return -1; // LCOV_EXCL_LINE const unsigned minD = getConstUInt(delayp->lhsp()); const unsigned maxD = getConstUInt(delayp->rhsp()); if (minD != maxD) return -1; delayCycles = minD; } else { delayCycles = getConstUInt(delayp->lhsp()); } int preLen = 0; if (AstNodeExpr* const prep = sexprp->preExprp()) { preLen = fixedLength(prep); if (preLen < 0) return -1; // LCOV_EXCL_LINE } const int bodyLen = fixedLength(sexprp->exprp()); if (bodyLen < 0) return -1; // LCOV_EXCL_LINE return preLen + delayCycles + bodyLen; } 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. const int lhsLen = fixedLength(orp->lhsp()); const int rhsLen = fixedLength(orp->rhsp()); if (lhsLen < 0 || rhsLen < 0 || lhsLen != rhsLen) return -1; return lhsLen; } if (AstSWithin* const withinp = VN_CAST(nodep, SWithin)) { // `seq1 within seq2` ends at seq2's end cycle (IEEE 16.9.10). const int lhsLen = fixedLength(withinp->lhsp()); const int rhsLen = fixedLength(withinp->rhsp()); if (lhsLen < 0 || rhsLen < 0 || lhsLen > rhsLen) return -1; return rhsLen; } // LCOV_EXCL_START -- defensive: V3AssertPre rejects composite SVA ops // 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; // 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). static std::pair lengthRange(AstNodeExpr* nodep) { if (AstSExpr* const sexprp = VN_CAST(nodep, SExpr)) { AstDelay* const delayp = VN_CAST(sexprp->delayp(), Delay); if (!delayp || !delayp->isCycleDelay()) return {-1, -1}; std::pair delayRange; if (delayp->isRangeDelay()) { if (delayp->isUnbounded()) return {-1, -1}; const unsigned minD = getConstUInt(delayp->lhsp()); const unsigned maxD = getConstUInt(delayp->rhsp()); delayRange = {minD, maxD}; } else { const unsigned d = getConstUInt(delayp->lhsp()); delayRange = {d, d}; } std::pair preRange{0, 0}; if (AstNodeExpr* const prep = sexprp->preExprp()) { preRange = lengthRange(prep); if (preRange.first < 0) return {-1, -1}; } const std::pair bodyRange = lengthRange(sexprp->exprp()); if (bodyRange.first < 0) return {-1, -1}; const int variableParts = (preRange.first != preRange.second) + (delayRange.first != delayRange.second) + (bodyRange.first != bodyRange.second); if (variableParts > 1) return {-1, -1}; return {preRange.first + delayRange.first + bodyRange.first, preRange.second + delayRange.second + bodyRange.second}; } if (AstSThroughout* const throughp = VN_CAST(nodep, SThroughout)) { return lengthRange(throughp->rhsp()); } if (nodep->isMultiCycleSva()) return {-1, -1}; return {0, 0}; // plain boolean -- 0 cycles } // Clone `operand` with its sole variable ranged cycle delay pinned so the // total match length is exactly `len`. `lo` is the operand's minimum length // (lengthRange().first). A fixed operand has no such delay and is returned // as a plain clone (callers only request its single achievable length). static AstNodeExpr* realizeAtLength(AstNodeExpr* operand, int len, int lo) { AstNodeExpr* const clonep = operand->cloneTreePure(false); AstDelay* rangeDelayp = nullptr; clonep->foreach([&](AstDelay* dp) { if (!rangeDelayp && dp->isRangeDelay() && !dp->isUnbounded() && getConstUInt(dp->lhsp()) != getConstUInt(dp->rhsp())) { rangeDelayp = dp; } }); if (rangeDelayp) { FileLine* const flp = rangeDelayp->fileline(); const unsigned pinned = getConstUInt(rangeDelayp->lhsp()) + (len - lo); AstNodeExpr* const oldMinp = rangeDelayp->lhsp(); oldMinp->replaceWith(new AstConst{flp, pinned}); VL_DO_DANGLING(oldMinp->deleteTree(), oldMinp); // Drop the max bound so it lowers as a fixed `##d`, not `##[d:d]`. AstNode* const oldMaxp = rangeDelayp->rhsp()->unlinkFrBack(); VL_DO_DANGLING(oldMaxp->deleteTree(), oldMaxp); } return clonep; } // Cuts AST size from O(N * sizeof(exprp)) to O(N) + O(sizeof(exprp)) by // sharing a single `VarRef` across N check edges. Hoist also matches the // IEEE 1800-2023 16.9.9 "single preponed-region snapshot" semantic for // any exprp -- even an impure one would now evaluate exactly once per // clock instead of N times. Orphan temps from failed builds are unused // MODULETEMPs and are removed by V3Dead. AstVar* tryHoistSampled(AstNodeExpr* exprp, FileLine* flp, unsigned cloneCount) { constexpr unsigned kHoistThreshold = 2; if (cloneCount < kHoistThreshold) return nullptr; AstVar* const tempVarp = new AstVar{flp, VVarType::MODULETEMP, m_propTempNames.get(exprp), exprp->dtypep()}; m_modp->addStmtsp(tempVarp); AstAssign* const assignp = new AstAssign{flp, new AstVarRef{flp, tempVarp, VAccess::WRITE}, sampled(exprp->cloneTreePure(false))}; m_modp->addStmtsp(new AstAlways{flp, VAlwaysKwd::ALWAYS_COMB, nullptr, assignp}); return tempVarp; } static AstNodeExpr* sampledRefOrClone(AstVar* hoistVarp, AstNodeExpr* exprp, FileLine* flp) { if (hoistVarp) return new AstVarRef{flp, hoistVarp, VAccess::READ}; return sampled(exprp->cloneTreePure(false)); } // Create vertex and inherit temporal guards from the current scope. SvaStateVertex* scopedCreateVertex() { SvaStateVertex* const vtxp = m_graph.createStateVertex(); for (AstNodeExpr* const cp : m_temporalGuardStack) { 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; } return vtxp; } // AND current temporal guards into every edge/link. SvaTransEdge* guardedLink(SvaStateVertex* fromp, SvaStateVertex* top, AstNodeExpr* condp, FileLine* flp) { return m_graph.addLink(fromp, top, throughoutCond(condp, flp)); } SvaTransEdge* guardedLink(SvaStateVertex* fromp, SvaStateVertex* top, FileLine* flp) { return m_graph.addLink(fromp, top, throughoutCond(nullptr, flp)); } SvaTransEdge* guardedEdge(SvaStateVertex* fromp, SvaStateVertex* top, AstNodeExpr* condp, FileLine* flp) { return m_graph.addClockedEdge(fromp, top, throughoutCond(condp, flp)); } SvaTransEdge* guardedEdge(SvaStateVertex* fromp, SvaStateVertex* top, FileLine* flp) { return m_graph.addClockedEdge(fromp, top, throughoutCond(nullptr, flp)); } SvaStateVertex* addDelayChain(SvaStateVertex* startp, unsigned size, FileLine* flp, bool isFixed = true, AstNodeExpr* clearCondp = nullptr, AstNodeExpr* advanceCondp = nullptr) { if (isFixed && size == 0) return startp; UASSERT_OBJ(size > 0, startp, "Delay chain needs at least one slot"); if (isFixed && size == 1 && !advanceCondp) { SvaStateVertex* const nextp = scopedCreateVertex(); guardedEdge(startp, nextp, flp); return nextp; } SvaStateVertex* const ringVtxp = scopedCreateVertex(); ringVtxp->m_isFixedDelayRing = isFixed; ringVtxp->m_delayRingSize = size; if (clearCondp) { UASSERT_OBJ(!isFixed, startp, "Fixed delay cannot have a clear condition"); ringVtxp->m_delayRingClearCondp = clearCondp->cloneTreePure(false); } ringVtxp->m_delayRingAdvanceCondp = advanceCondp; if (isFixed) { guardedEdge(startp, ringVtxp, flp); } else { guardedLink(startp, ringVtxp, flp); } return ringVtxp; } // Build NFA for an SExpr. finalCond = RHS (not yet added as a vertex). // isTopLevelStep: marks outermost required boolean check as rejectOnFail. // Apply a range delay `##[M:N]` to currentp. Returns true on success. On // failure, sets outErrorEmitted per semantic-error policy and returns false. bool applyRangeDelay(AstDelay* delayp, AstNodeExpr* rhsExprp, SvaStateVertex*& currentp, std::vector& midSources, FileLine* flp, bool& outErrorEmitted, RangeDelayRejectInfo* rangeRejectInfop = nullptr) { const unsigned minDelay = getConstUInt(delayp->lhsp()); if (delayp->isUnbounded()) { // `##[M:$]`: wait M cycles, then self-loop waiting for the match // condition. Unbounded = liveness, so no reject. currentp = addDelayChain(currentp, minDelay, flp); guardedEdge(currentp, currentp, flp); currentp->m_isUnbounded = true; m_inUnboundedScope = true; return true; } const unsigned maxDelay = getConstUInt(delayp->rhsp()); if (minDelay == maxDelay) { currentp = addDelayChain(currentp, minDelay, flp); return true; } const unsigned range = maxDelay - minDelay; currentp = addDelayChain(currentp, minDelay, flp); // kChainLimit bounds per-attempt unrolled vertices. Above this, a // ring buffer (constant-size state) is used instead, so the vertex // count is O(1) in range regardless of user input; no adversarial N // blowup is possible. constexpr unsigned kChainLimit = 256; // IEEE 1800-2023 16.14.3: only a small bounded range before a plain // boolean enumerates every end-of-match below. The counter FSM drops // overlapping ends and the nested-sequence merge collapses them, so // reject those for a cover sequence rather than under-count. if (m_isCoverSeq && (range > kChainLimit || VN_IS(rhsExprp, SExpr))) { warnEndpointUnsupported(flp, "this ranged cycle delay"); outErrorEmitted = true; return false; } if (range > kChainLimit) { currentp = addDelayChain(currentp, range + 1U, flp, false, rhsExprp->isMultiCycleSva() ? nullptr : rhsExprp); } else if (VN_IS(rhsExprp, SExpr)) { // Nested-SExpr RHS: merge all [M,N] positions. Candidate-local misses // are not assertion rejects while a later position can still match. if (rangeRejectInfop) { const int rhsLen = fixedLength(rhsExprp); if (rhsLen >= 0) *rangeRejectInfop = {currentp, range, rhsLen}; } SvaStateVertex* const mergeVtxp = scopedCreateVertex(); mergeVtxp->m_isUnbounded = true; guardedLink(currentp, mergeVtxp, flp); for (unsigned i = 0; i < range; ++i) { SvaStateVertex* const nextVtxp = scopedCreateVertex(); guardedEdge(currentp, nextVtxp, flp); guardedLink(nextVtxp, mergeVtxp, flp); currentp = nextVtxp; } currentp = mergeVtxp; m_inUnboundedScope = true; } else { // Pure boolean RHS: register chain. Each mid-position links to // match (match-only); last position is the reject source. // For cover_sequence (IEEE 1800-2023 16.14.3) the advance edge is // unconditional so every (start, end) pair fires independently -- // dropping NOT(b) turns "first-match-wins" into "every end fires". AstVar* const hoistVarp = m_isCoverSeq ? nullptr : tryHoistSampled(rhsExprp, flp, range); midSources.push_back(currentp); for (unsigned i = 0; i < range; ++i) { SvaStateVertex* const nextVtxp = scopedCreateVertex(); if (m_isCoverSeq) { guardedEdge(currentp, nextVtxp, flp); } else { AstNodeExpr* const notExprp = new AstLogNot{flp, sampledRefOrClone(hoistVarp, rhsExprp, flp)}; guardedEdge(currentp, nextVtxp, notExprp, flp); } if (i < range - 1) midSources.push_back(nextVtxp); currentp = nextVtxp; } } return true; } void addFiniteRangeReject(const RangeDelayRejectInfo& info, const BuildResult& result, FileLine* flp) { if (!info.startp) return; SvaStateVertex* const expiryVtxp = addDelayChain(info.startp, info.range + info.rhsLen, flp); SvaStateVertex* const expiryMatchp = scopedCreateVertex(); std::vector sources = result.midSources; sources.push_back(result.termVertexp); for (SvaStateVertex* const srcp : sources) { AstNodeExpr* const condp = result.finalCondp ? sampled(result.finalCondp->cloneTreePure(false)) : nullptr; SvaStateVertex* const successNowp = scopedCreateVertex(); guardedLink(srcp, successNowp, condp, flp); SvaStateVertex* stagep = successNowp; guardedLink(stagep, expiryMatchp, flp); for (unsigned i = 0; i < info.range; ++i) { SvaStateVertex* const nextp = scopedCreateVertex(); guardedEdge(stagep, nextp, flp); stagep = nextp; guardedLink(stagep, expiryMatchp, flp); } } SvaStateVertex* const sinkVtxp = m_graph.createStateVertex(); sinkVtxp->m_isRejectSink = true; SvaTransEdge* const rejectp = m_graph.addLink(expiryVtxp, sinkVtxp); rejectp->m_rejectOnFail = true; rejectp->m_condVtxp = expiryMatchp; } BuildResult buildSExpr(AstSExpr* sexprp, SvaStateVertex* entryVtxp, bool isTopLevelStep = false) { AstDelay* const delayp = VN_CAST(sexprp->delayp(), Delay); if (!delayp || !delayp->isCycleDelay()) return BuildResult::fail(); FileLine* const flp = sexprp->fileline(); AstNodeExpr* const exprp = sexprp->exprp(); // Handle LHS (preExpr) SvaStateVertex* currentp = entryVtxp; if (AstNodeExpr* const preExprp = sexprp->preExprp()) { const BuildResult pre = buildExpr(preExprp, currentp, isTopLevelStep); if (!pre.valid()) return BuildResult::fail(pre.errorEmitted); // LCOV_EXCL_LINE if (pre.finalCondp) { 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"); // Do not mark liveness sources: first boolean check is deferred. edgep->m_rejectOnFail = true; } cleanupProbeResult(pre); currentp = condVtxp; } else { currentp = pre.termVertexp; } } // Handle delay std::vector rangeMidSources; RangeDelayRejectInfo rangeRejectInfo; const bool addRangeReject = mayEmitLocalReject(isTopLevelStep); if (delayp->isRangeDelay()) { bool errorEmitted = false; if (!applyRangeDelay(delayp, sexprp->exprp(), currentp, rangeMidSources, flp, errorEmitted, addRangeReject ? &rangeRejectInfo : nullptr)) { return BuildResult::fail(errorEmitted); } } else { const unsigned delayCycles = getConstUInt(delayp->lhsp()); currentp = addDelayChain(currentp, delayCycles, flp); } // Multi-cycle RHS: recurse (only plain boolean is returned as finalCondp). if (exprp->isMultiCycleSva()) { const BuildResult result = buildExpr(exprp, currentp, isTopLevelStep); if (result.valid()) addFiniteRangeReject(rangeRejectInfo, result, flp); return result; } return {currentp, exprp, std::move(rangeMidSources)}; } BuildResult buildConsRep(AstSConsRep* repp, SvaStateVertex* entryVtxp, bool isTopLevelStep = false) { FileLine* const flp = repp->fileline(); AstNodeExpr* const exprp = repp->exprp(); // Multi-cycle expr in ConsRep not yet supported; bail to avoid invalid AST. if (exprp->isMultiCycleSva()) { repp->v3warn(E_UNSUPPORTED, "Unsupported: multi-cycle sequence expression inside" " consecutive repetition (IEEE 1800-2023 16.9.2)"); return BuildResult::failWithError(); } const unsigned minN = getConstUInt(repp->countp()); // Sum sites across prefix + unbounded/range tail so one hoist covers // every check edge of this repetition. unsigned totalSites = minN; if (repp->unbounded()) { totalSites += 1; } else if (repp->maxCountp()) { totalSites += getConstUInt(repp->maxCountp()) - minN; } AstVar* const hoistVarp = tryHoistSampled(exprp, flp, totalSites); // Cover-sequence (IEEE 1800-2023 16.14.3): collect each end-of-match // position so they all fire the action, not just the merged terminal. std::vector consMidSources; SvaStateVertex* currentp = entryVtxp; for (unsigned i = 0; i < minN; ++i) { // Keep the first repetition explicit, collapse all remaining checks into the ring. if (i == 1) { currentp = addDelayChain(currentp, minN - 1, flp); currentp->m_delayRingClearCondp = new AstLogNot{flp, sampledRefOrClone(hoistVarp, exprp, flp)}; currentp->m_replayAbortReject = true; if (isTopLevelStep) { currentp->m_throughoutConds.push_back( sampledRefOrClone(hoistVarp, exprp, flp)); } i = minN - 1; } // Every repetition in the minimum prefix is required. SvaStateVertex* const condVtxp = scopedCreateVertex(); SvaTransEdge* const linkp = guardedLink(currentp, condVtxp, sampledRefOrClone(hoistVarp, exprp, flp), flp); if (mayEmitLocalReject(isTopLevelStep) && i == 0) linkp->m_rejectOnFail = true; currentp = condVtxp; } // After minN: currentp is the first valid end-of-match position for [*m:n]. if (m_isCoverSeq && (repp->unbounded() || repp->maxCountp())) { consMidSources.push_back(currentp); } if (repp->unbounded()) { if (minN == 0) { SvaStateVertex* const waitVtxp = scopedCreateVertex(); guardedEdge(currentp, waitVtxp, flp); SvaStateVertex* const checkVtxp = scopedCreateVertex(); guardedLink(waitVtxp, checkVtxp, sampledRefOrClone(hoistVarp, exprp, flp), flp); guardedEdge(checkVtxp, waitVtxp, flp); guardedLink(currentp, checkVtxp, flp); currentp = checkVtxp; } else { SvaStateVertex* const loopBackVtxp = scopedCreateVertex(); guardedEdge(currentp, loopBackVtxp, flp); SvaStateVertex* const reCheckVtxp = scopedCreateVertex(); guardedLink(loopBackVtxp, reCheckVtxp, sampledRefOrClone(hoistVarp, exprp, flp), flp); guardedEdge(reCheckVtxp, loopBackVtxp, flp); guardedLink(reCheckVtxp, currentp, flp); } currentp->m_isUnbounded = true; m_inUnboundedScope = true; } else if (repp->maxCountp()) { const unsigned maxN = getConstUInt(repp->maxCountp()); SvaStateVertex* const mergeVtxp = scopedCreateVertex(); guardedLink(currentp, mergeVtxp, flp); unsigned tailMinN = minN; SvaStateVertex* tailStartp = currentp; if (minN == 0) { // Build the first optional iteration explicitly. Feeding the empty endpoint // directly into a range ring would incorrectly keep that match alive. SvaStateVertex* const nextVtxp = scopedCreateVertex(); guardedEdge(currentp, nextVtxp, flp); SvaStateVertex* const checkVtxp = scopedCreateVertex(); guardedLink(nextVtxp, checkVtxp, sampledRefOrClone(hoistVarp, exprp, flp), flp); guardedLink(checkVtxp, mergeVtxp, flp); if (m_isCoverSeq) consMidSources.push_back(checkVtxp); tailStartp = checkVtxp; tailMinN = 1; } if (maxN > tailMinN) { // Add tail-ring only if the tail is non-empty. SvaStateVertex* const nextVtxp = addDelayChain(tailStartp, maxN - tailMinN + 1, flp, false); nextVtxp->m_delayRingClearCondp = new AstLogNot{flp, sampledRefOrClone(hoistVarp, exprp, flp)}; nextVtxp->m_replayAbortReject = true; SvaStateVertex* const checkVtxp = scopedCreateVertex(); guardedLink(nextVtxp, checkVtxp, sampledRefOrClone(hoistVarp, exprp, flp), flp); checkVtxp->m_matchCountRingp = nextVtxp; guardedLink(checkVtxp, mergeVtxp, flp); if (m_isCoverSeq) consMidSources.push_back(checkVtxp); } currentp = mergeVtxp; } // finalCond = nullptr (already checked via Links) BuildResult res; res.termVertexp = currentp; res.finalCondp = nullptr; res.midSources = std::move(consMidSources); // mergeVtxp is the OR of all the end-positions we already pushed to // midSources, so the main termVtxp -> matchVertex Link would duplicate. res.termIsMidMerge = m_isCoverSeq && !res.midSources.empty(); return res; } // always[lo:hi] / s_always[lo:hi] (IEEE 1800-2023 16.12.11). BuildResult buildPropAlways(AstPropAlways* nodep, SvaStateVertex* entryVtxp, bool isTopLevelStep = false) { FileLine* const flp = nodep->fileline(); AstNodeExpr* const propp = nodep->propp(); const unsigned lo = getConstUInt(nodep->loBoundp()); if (VN_IS(nodep->hiBoundp(), Unbounded)) { // Weak always [lo:$]: unbounded upper bound (IEEE 1800-2023 16.12.11). // p must hold at every clock tick at least lo cycles after the attempt // start; those ticks are not required to exist, so there is no // end-of-trace obligation (weak). The self-loop keeps the attempt live // every cycle; each observed cycle is a safety obligation, so a false p // rejects immediately. UASSERT_OBJ(!nodep->isStrong(), nodep, "Unbounded always must be weak (V3Width)"); SvaStateVertex* const livep = addDelayChain(entryVtxp, lo, flp); livep->m_isUnbounded = true; guardedEdge(livep, livep, flp); // stay active every subsequent cycle SvaStateVertex* const sinkp = m_graph.createStateVertex(); sinkp->m_isRejectSink = true; SvaTransEdge* const rejEdgep = guardedLink(livep, sinkp, sampled(propp->cloneTreePure(false)), flp); if (mayEmitLocalReject(isTopLevelStep)) rejEdgep->m_rejectOnFail = true; return {livep, nullptr, {}}; } const unsigned hi = getConstUInt(nodep->hiBoundp()); // Strong s_always[m:n]: mark every in-window registered vertex so an // attempt still mid-window at end-of-simulation is reported as a liveness // failure (IEEE strong: the n+1 ticks must exist). An attempt that has // completed earlier in the trace has already cleared its state, so it is // 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; currentp = checkp; m_temporalGuardStack.push_back(propp); currentp = addDelayChain(currentp, hi - lo, flp); m_temporalGuardStack.pop_back(); return {currentp, propp, {}}; } BuildResult buildGotoRep(AstSGotoRep* repp, SvaStateVertex* entryVtxp) { FileLine* const flp = repp->fileline(); AstNodeExpr* const exprp = repp->exprp(); const unsigned minN = getConstUInt(repp->countp()); if (minN == 0) return BuildResult::fail(); const bool hasMax = repp->maxCountp() != nullptr; const unsigned maxN = hasMax ? getConstUInt(repp->maxCountp()) : minN; if (m_isCoverSeq) { // Several matches may wait across false cycles, but the ring stores only one bit for // them, so a cover sequence action block could run too few times. warnEndpointUnsupported(flp, "a goto repetition"); return BuildResult::failWithError(); } AstVar* const hoistVarp = tryHoistSampled(exprp, flp, 2); // The first guardedEdge is the ##1 before waiting for a match. In the wait state, false // takes the clocked self-loop, while true takes the zero-delay guardedLink on that tick. SvaStateVertex* const waitVtxp = scopedCreateVertex(); guardedEdge(entryVtxp, waitVtxp, flp); guardedEdge(waitVtxp, waitVtxp, new AstLogNot{flp, sampledRefOrClone(hoistVarp, exprp, flp)}, flp); SvaStateVertex* currentp = scopedCreateVertex(); guardedLink(waitVtxp, currentp, sampledRefOrClone(hoistVarp, exprp, flp), flp); if (minN > 1) { SvaStateVertex* const ringVtxp = addDelayChain( currentp, minN - 1, flp, true, nullptr, sampledRefOrClone(hoistVarp, exprp, flp)); ringVtxp->m_replayAbortReject = true; SvaStateVertex* const checkVtxp = scopedCreateVertex(); guardedLink(ringVtxp, checkVtxp, sampledRefOrClone(hoistVarp, exprp, flp), flp); currentp = checkVtxp; } if (!hasMax) { currentp->m_isUnbounded = true; // [->N] waits unboundedly m_inUnboundedScope = true; return {currentp, nullptr, {}}; } // [->M:N]: the range ring holds matches from M through N and advances // only on expr, preserving arbitrarily long gaps between occurrences. SvaStateVertex* const mergeVtxp = scopedCreateVertex(); guardedLink(currentp, mergeVtxp, flp); // accept at match_M if (maxN > minN) { SvaStateVertex* const ringVtxp = addDelayChain(currentp, maxN - minN + 1, flp, false, nullptr, sampledRefOrClone(hoistVarp, exprp, flp)); ringVtxp->m_replayAbortReject = true; SvaStateVertex* const checkVtxp = scopedCreateVertex(); guardedLink(ringVtxp, checkVtxp, sampledRefOrClone(hoistVarp, exprp, flp), flp); guardedLink(checkVtxp, mergeVtxp, flp); } mergeVtxp->m_isUnbounded = true; // [->M:N] still has unbounded waits between matches m_inUnboundedScope = true; 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; } // 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; const BuildResult lhs = buildExpr(lhsp, entryVtxp); const BuildResult rhs = buildExpr(rhsp, entryVtxp); if (!lhs.valid() || !rhs.valid()) { cleanupProbeResult(lhs); cleanupProbeResult(rhs); return BuildResult::fail(lhs.errorEmitted || rhs.errorEmitted); } // Reject cover-seq 'or' operands whose earlier endpoints bypass this merge. if (m_isCoverSeq && (lhs.termVertexp != entryVtxp || rhs.termVertexp != entryVtxp)) { cleanupProbeResult(lhs); cleanupProbeResult(rhs); warnEndpointUnsupported(flp, "a sequence operand of 'or'"); 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; } return {mergeVtxp, nullptr, {}}; } // A done latch retains the first temporal-AND endpoint until its sibling arrives. BuildResult buildAndCombiner(AstNodeExpr* lhsExprp, AstNodeExpr* rhsExprp, SvaStateVertex* entryVtxp, FileLine* flp) { const bool savedScope = m_inUnboundedScope; const BuildResult lhs = buildExpr(lhsExprp, entryVtxp); const bool lhsScope = m_inUnboundedScope; m_inUnboundedScope = savedScope; 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); return BuildResult::fail(lhs.errorEmitted || rhs.errorEmitted); } // Single-cycle operands: use boolean AND (done-latch would fire across cycles). // If both operands stayed at entry, they must be boolean leaves which // buildExpr returns with finalCondp=nodep (non-null). if (lhs.termVertexp == entryVtxp && rhs.termVertexp == entryVtxp) { UASSERT_OBJ(lhs.finalCondp && rhs.finalCondp, lhsExprp, "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); 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'"); 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); if (rhs.finalCondp) combVtxp->m_andRhsCondp = rhs.finalCondp->cloneTreePure(false); if (lhs.termVertexp->m_isUnbounded || rhs.termVertexp->m_isUnbounded) { combVtxp->m_isUnbounded = true; } 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); if (needSink) { SvaStateVertex* const sinkVtxp = m_graph.createStateVertex(); sinkVtxp->m_isRejectSink = true; if (lhs.finalCondp && lhsMultiCycle && !lhs.termVertexp->m_isUnbounded) { SvaTransEdge* const ep = m_graph.addLink( lhs.termVertexp, sinkVtxp, sampled(lhs.finalCondp->cloneTreePure(false))); ep->m_rejectOnFail = true; } if (rhs.finalCondp && rhsMultiCycle && !rhs.termVertexp->m_isUnbounded) { SvaTransEdge* const ep = m_graph.addLink( rhs.termVertexp, sinkVtxp, sampled(rhs.finalCondp->cloneTreePure(false))); ep->m_rejectOnFail = true; } } } cleanupProbeResult(lhs); cleanupProbeResult(rhs); 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. BuildResult buildSWithin(AstSWithin* nodep, SvaStateVertex* entryVtxp, bool isTopLevelStep = false) { const int innerLen = fixedLength(nodep->lhsp()); const int outerLen = fixedLength(nodep->rhsp()); if (innerLen < 0 || outerLen < 0) { nodep->v3warn(E_UNSUPPORTED, "Unsupported: within with ranged cycle-delay operand"); return BuildResult::failWithError(); } if (innerLen > outerLen) { return buildNeverMatchIntersect( nodep, entryVtxp, isTopLevelStep, "the inner sequence is longer than the outer sequence"); } FileLine* const flp = nodep->fileline(); const int slack = outerLen - innerLen; AstNodeExpr* innerOrp = nullptr; for (int i = 0; i <= slack; ++i) { const int postPad = slack - i; AstNodeExpr* branchp = nodep->lhsp()->cloneTreePure(false); if (i > 0) { AstConst* const prePadp = new AstConst{flp, AstConst::BitTrue{}}; AstDelay* const delayp = new AstDelay{flp, new AstConst{flp, static_cast(i)}, true}; AstSExpr* const wrapped = new AstSExpr{flp, prePadp, delayp, branchp}; wrapped->dtypeSetBit(); branchp = wrapped; } if (postPad > 0) { AstConst* const postTruep = new AstConst{flp, AstConst::BitTrue{}}; AstDelay* const delayp = new AstDelay{flp, new AstConst{flp, static_cast(postPad)}, true}; AstSExpr* const wrapped = new AstSExpr{flp, branchp, delayp, postTruep}; wrapped->dtypeSetBit(); branchp = wrapped; } innerOrp = innerOrp ? static_cast(new AstSOr{flp, innerOrp, branchp}) : branchp; } AstNodeExpr* const outerClonep = nodep->rhsp()->cloneTreePure(false); 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. if (result.valid() && result.finalCondp && !result.finalCondp->backp()) { SvaStateVertex* const wrapVtxp = scopedCreateVertex(); guardedLink(result.termVertexp, wrapVtxp, sampled(result.finalCondp), flp); result = {wrapVtxp, nullptr, result.midSources}; } 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. static bool flattenFixedSeq(AstNodeExpr* nodep, int baseOffset, std::map>& out) { if (AstSExpr* const sexprp = VN_CAST(nodep, SExpr)) { AstDelay* const delayp = VN_CAST(sexprp->delayp(), Delay); if (!delayp || !delayp->isCycleDelay() || delayp->isUnbounded()) return false; const unsigned delayCycles = getConstUInt(delayp->lhsp()); if (delayp->isRangeDelay() && getConstUInt(delayp->rhsp()) != delayCycles) return false; int preLen = 0; if (AstNodeExpr* const prep = sexprp->preExprp()) { if (!flattenFixedSeq(prep, baseOffset, out)) return false; preLen = fixedLength(prep); if (preLen < 0) return false; } 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; out[baseOffset].push_back(nodep); return true; } // Conjoin two fixed sequences into one, AND-ing leaf checks at each offset. static AstNodeExpr* conjoinFixedSeqs(AstNodeExpr* lhsp, AstNodeExpr* rhsp, FileLine* flp) { std::map> checks; if (!flattenFixedSeq(lhsp, 0, checks) || !flattenFixedSeq(rhsp, 0, checks)) return nullptr; if (checks.empty()) return nullptr; AstNodeExpr* resultp = nullptr; int prevOffset = 0; for (const auto& offsetChecks : checks) { const int offset = offsetChecks.first; AstNodeExpr* condp = nullptr; for (AstNodeExpr* const leafp : offsetChecks.second) { AstNodeExpr* const clonep = leafp->cloneTreePure(false); if (!condp) { condp = clonep; } else { condp = new AstLogAnd{flp, condp, clonep}; condp->dtypeSetBit(); } } if (!resultp) { if (offset > 0) { AstDelay* const delayp = new AstDelay{ flp, new AstConst{flp, static_cast(offset)}, /*isCycle=*/true}; resultp = new AstSExpr{flp, new AstConst{flp, AstConst::BitTrue{}}, delayp, condp}; resultp->dtypeSetBit(); } else { resultp = condp; } } else { AstDelay* const delayp = new AstDelay{ flp, new AstConst{flp, static_cast(offset - prevOffset)}, /*isCycle=*/true}; resultp = new AstSExpr{flp, resultp, delayp, condp}; resultp->dtypeSetBit(); } prevOffset = offset; } 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). struct SimpleRanged final { bool ok = false; AstNodeExpr* startp = nullptr; // may be null (absent start) AstNodeExpr* endp = nullptr; }; static SimpleRanged asSimpleRanged(AstNodeExpr* nodep) { AstSExpr* const sexprp = VN_CAST(nodep, SExpr); if (!sexprp) return {}; AstDelay* const delayp = VN_CAST(sexprp->delayp(), Delay); if (!delayp || !delayp->isCycleDelay() || !delayp->isRangeDelay() || delayp->isUnbounded()) return {}; if (getConstUInt(delayp->lhsp()) == getConstUInt(delayp->rhsp())) return {}; AstNodeExpr* const prep = sexprp->preExprp(); if (prep && fixedLength(prep) != 0) return {}; if (fixedLength(sexprp->exprp()) != 0) return {}; return {true, prep, sexprp->exprp()}; } // Build the NFA for a synthesized lowering tree, cloning finalCondp before freeing it. BuildResult buildFromLoweringTree(AstNodeExpr* treep, SvaStateVertex* entryVtxp, bool isTopLevelStep) { BuildResult result = buildExpr(treep, entryVtxp, isTopLevelStep); if (result.valid() && result.finalCondp) { result.finalCondp = result.finalCondp->cloneTreePure(false); } VL_DO_DANGLING(treep->deleteTree(), treep); return result; } // No common length: the intersect never matches (legal, 16.9.6) -> constant false. BuildResult buildNeverMatchIntersect(AstNodeExpr* nodep, SvaStateVertex* entryVtxp, bool isTopLevelStep, const char* reason) { nodep->v3warn(NEVERMATCH, "Sequence can never match because " << reason << "."); AstNodeExpr* const falsep = new AstConst{nodep->fileline(), AstConst::BitFalse{}}; return buildFromLoweringTree(falsep, entryVtxp, isTopLevelStep); } // Lower supported variable-length intersect forms under IEEE 1800-2023 16.9.6. BuildResult buildVarLenIntersect(AstSIntersect* nodep, SvaStateVertex* entryVtxp, bool isTopLevelStep) { const std::pair lhsRange = lengthRange(nodep->lhsp()); const std::pair rhsRange = lengthRange(nodep->rhsp()); if (lhsRange.first < 0 || rhsRange.first < 0) { nodep->v3warn(E_UNSUPPORTED, "Unsupported: intersect with this variable-length operand"); return BuildResult::failWithError(); } const int lo = std::max(lhsRange.first, rhsRange.first); const int hi = std::min(lhsRange.second, rhsRange.second); if (lo > hi) { // Disjoint length ranges share no common length -> never matches. return buildNeverMatchIntersect(nodep, entryVtxp, isTopLevelStep, "intersect operands have no common length"); } 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); VL_DO_DANGLING(lp->deleteTree(), lp); VL_DO_DANGLING(rp->deleteTree(), rp); if (!conjp) { nodep->v3warn(E_UNSUPPORTED, "Unsupported: intersect operand is not a plain boolean sequence"); return BuildResult::failWithError(); } return buildFromLoweringTree(conjp, entryVtxp, isTopLevelStep); } const SimpleRanged sl = asSimpleRanged(nodep->lhsp()); const SimpleRanged sr = asSimpleRanged(nodep->rhsp()); if (!sl.ok || !sr.ok) { nodep->v3warn(E_UNSUPPORTED, "Unsupported: intersect of two sequences that each vary in length over a" " range with internal structure"); return BuildResult::failWithError(); } const auto andBool = [&](AstNodeExpr* ap, AstNodeExpr* bp) -> AstNodeExpr* { AstNodeExpr* const aClonep = ap ? ap->cloneTreePure(false) : new AstConst{flp, AstConst::BitTrue{}}; AstNodeExpr* const bClonep = bp ? bp->cloneTreePure(false) : new AstConst{flp, AstConst::BitTrue{}}; AstLogAnd* const andp = new AstLogAnd{flp, aClonep, bClonep}; andp->dtypeSetBit(); return andp; }; AstDelay* const delayp = new AstDelay{flp, new AstConst{flp, static_cast(lo)}, /*isCycle=*/true}; delayp->rhsp(new AstConst{flp, static_cast(hi)}); AstSExpr* const reducedp = new AstSExpr{flp, andBool(sl.startp, sr.startp), delayp, andBool(sl.endp, sr.endp)}; reducedp->dtypeSetBit(); return buildFromLoweringTree(reducedp, entryVtxp, isTopLevelStep); } BuildResult buildThroughout(AstSThroughout* nodep, SvaStateVertex* entryVtxp, bool isTopLevelStep = false) { // 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()); } m_temporalGuardStack.pop_back(); return result; } // until / until_with per IEEE 1800-2023 16.12.12. // Topology: combinational wait vertex with self-feeding state register. // entry --link[T]--> waitC // waitR --link[T]--> waitC (back-loop) // waitC --edge[##1, sampled(p) && !sampled(q)]--> waitR (continue) // waitC --link[REQUIRE, rejectOnFail]--> sink (per-cycle fail) // waitC --link[T]--> match (added by wireMatchAndMidSources; // accept condition rides via finalCondp) // waitC is m_isUnbounded so the terminal-match link contributes only to // terminalActive, not to rejectBase (which would otherwise spuriously fire // every cycle q is false). Per-cycle reject comes from the explicit // rejectOnFail link to the sink vertex. // // Non-overlapping (p until q): // REQUIRE = sampled(p) || sampled(q) accept = sampled(q) // Overlapping (p until_with q): // REQUIRE = sampled(p) accept = sampled(p) && sampled(q) // Strong forms use the same checks and mark the registered wait state as // an end-of-simulation liveness obligation. BuildResult buildUntil(AstUntil* nodep, SvaStateVertex* entryVtxp, bool isTopLevelStep) { FileLine* const flp = nodep->fileline(); if (!isTopLevelStep) { nodep->v3warn(E_UNSUPPORTED, "Unsupported: '" << nodep->verilogKwd() << "' in complex property expression"); return BuildResult::failWithError(); } AstNodeExpr* const lhsBitp = nodep->lhsp(); AstNodeExpr* const rhsBitp = nodep->rhsp(); if (containsMultiCycleSva(lhsBitp) || containsMultiCycleSva(rhsBitp)) { 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(); // 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). AstVar* const qHoistp = ov ? nullptr : tryHoistSampled(rhsBitp, flp, 2); 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++; } // Entry and back-loop Links carry no condition; throughout-folding still applies. guardedLink(entryVtxp, waitCp, flp); guardedLink(waitRp, waitCp, flp); // Continue clocked edge: p && !q advances to next-cycle wait. AstNodeExpr* const contCondp = new AstLogAnd{flp, sampledRefOrClone(pHoistp, lhsBitp, flp), new AstLogNot{flp, sampledRefOrClone(qHoistp, rhsBitp, flp)}}; guardedEdge(waitCp, waitRp, contCondp, flp); // Reject sink: fires when require-condition is false. SvaStateVertex* const sinkVtxp = m_graph.createStateVertex(); sinkVtxp->m_isRejectSink = true; AstNodeExpr* requireCondp; if (ov) { requireCondp = sampledRefOrClone(pHoistp, lhsBitp, flp); } else { requireCondp = new AstLogOr{flp, sampledRefOrClone(pHoistp, lhsBitp, flp), sampledRefOrClone(qHoistp, rhsBitp, flp)}; } SvaTransEdge* const rejEdgep = m_graph.addLink(waitCp, sinkVtxp, requireCondp); if (mayEmitLocalReject(isTopLevelStep)) rejEdgep->m_rejectOnFail = true; // Accept condition rides via finalCondp; assembleResult $sampled-wraps it. AstNodeExpr* acceptCondp; if (ov) { acceptCondp = new AstLogAnd{flp, lhsBitp->cloneTreePure(false), rhsBitp->cloneTreePure(false)}; } else { acceptCondp = rhsBitp->cloneTreePure(false); } return {waitCp, acceptCondp, {}}; } // IEEE 1800-2023 16.12.14 property abort operators. Sync and async share // the same NFA encoding: AstSampled already gives matured values at every // maturing clocking event, and async firing "between clocks" is not // observable in a cycle-based model. VAbortKind selects accept vs reject // verdict (sync vs async only changes the user-visible spelling). // Build `condp && !outer_1 && !outer_2 ...` (unsampled). AstNodeExpr* abortFireExpr(AstNodeExpr* condp, FileLine* flp) { AstNodeExpr* resultp = condp->cloneTreePure(false); for (AstNodeExpr* const op : m_outerAbortStack) resultp = new AstLogAnd{flp, resultp, new AstLogNot{flp, op->cloneTreePure(false)}}; return resultp; } // True when unguarded ring-wide rejection or a same-tick Link chain already accounts the // attempt: a required-step Link covers both outcomes; followed-by pairs both edges. static bool chainAccountsSource(const SvaStateVertex* srcp, const std::unordered_set& preEdges) { if (srcp->m_delayRingSize && srcp->m_throughoutConds.empty()) return true; for (const V3GraphEdge& edger : srcp->inEdges()) { if (preEdges.count(&edger)) continue; const SvaTransEdge& tedger = static_cast(edger); if (tedger.m_consumesCycle) continue; const auto* const fromp = static_cast(tedger.fromVtxp()); if (fromp->m_abortRejectp) return true; } bool plainNonSink = false; bool markedSink = false; for (const V3GraphEdge& edger : srcp->outEdges()) { if (preEdges.count(&edger)) continue; const SvaTransEdge& tedger = static_cast(edger); if (tedger.m_consumesCycle) continue; const bool sink = static_cast(tedger.toVtxp())->m_isRejectSink; if (tedger.m_rejectOnFail) { if (!sink) return true; markedSink = true; } else if (!sink) { if (srcp->m_abortRejectp) return true; plainNonSink = true; } } return plainNonSink && markedSink; } // Reject edge: fires when the source is live and the abort samples true. void addAbortRejectEdge(SvaStateVertex* srcp, SvaStateVertex* sinkp, AstNodeExpr* condp, FileLine* flp) { AstNodeExpr* const notFirep = new AstLogNot{flp, sampled(abortFireExpr(condp, flp))}; m_graph.addLink(srcp, sinkp, notFirep)->m_rejectOnFail = true; return; } // On the fire tick: kill body threads; accept kinds also forgive step misses. void gateBodyEdgesOnAbort(const std::unordered_set& preEdges, AstNodeExpr* condp, VAbortKind kind, FileLine* flp) { for (V3GraphVertex& vtxr : m_graph.m_graph.vertices()) { for (V3GraphEdge& edger : vtxr.outEdges()) { if (preEdges.count(&edger)) continue; SvaTransEdge* const tedgep = static_cast(&edger); if (tedgep->m_rejectOnFail) { if (!kind.isAccept()) continue; AstNodeExpr* const firep = sampled(abortFireExpr(condp, flp)); tedgep->m_condp = tedgep->m_condp ? new AstLogOr{flp, tedgep->m_condp, firep} : firep; } else if (tedgep->m_consumesCycle) { AstNodeExpr* const notFirep = new AstLogNot{flp, sampled(abortFireExpr(condp, flp))}; tedgep->m_condp = tedgep->m_condp ? new AstLogAnd{flp, tedgep->m_condp, notFirep} : notFirep; } } } } BuildResult buildAbortOn(AstNodeExpr* condp, AstNodeExpr* bodyp, SvaStateVertex* entryVtxp, VAbortKind kind, FileLine* flp, bool isTopLevelStep) { // Snapshot pre-body vertices/edges so post-build diff yields the body's sub-NFA. std::unordered_set preExisting; std::unordered_set preEdges; for (V3GraphVertex& vtxr : m_graph.m_graph.vertices()) { preExisting.insert(&vtxr); for (V3GraphEdge& edger : vtxr.outEdges()) preEdges.insert(&edger); } m_outerAbortStack.push_back(condp); const BuildResult bodyResult = buildExpr(bodyp, entryVtxp, isTopLevelStep); m_outerAbortStack.pop_back(); if (!bodyResult.valid()) return bodyResult; gateBodyEdgesOnAbort(preEdges, condp, kind, flp); // Live-thread sources for the abort edge: entry + new body vertices, // minus reject sinks (they carry reject fuel, not live-thread fuel). std::vector abortSources; abortSources.push_back(entryVtxp); for (V3GraphVertex& vtxr : m_graph.m_graph.vertices()) { if (preExisting.count(&vtxr)) continue; auto* const sp = static_cast(&vtxr); if (sp->m_delayRingSize || !sp->m_throughoutConds.empty()) { AstNodeExpr* const firep = abortFireExpr(condp, flp); sp->m_abortClearp = sp->m_abortClearp ? new AstLogOr{flp, sp->m_abortClearp, firep} : firep; } if (!kind.isAccept() && ((sp->m_delayRingSize && sp->m_throughoutConds.empty()) || sp->m_replayAbortReject)) { AstNodeExpr* const firep = abortFireExpr(condp, flp); sp->m_abortRejectp = sp->m_abortRejectp ? new AstLogOr{flp, sp->m_abortRejectp, firep} : firep; } if (sp->m_isRejectSink) continue; abortSources.push_back(sp); } auto sampledAbortFire = [&]() -> AstNodeExpr* { AstNodeExpr* const expr = abortFireExpr(condp, flp); return sampled(expr); }; if (kind.isAccept()) { // Match-only sink fed by $sampled(abort-fire) from every live source; // registered as midSource so it never contributes a reject. SvaStateVertex* const acceptSinkp = scopedCreateVertex(); for (SvaStateVertex* const srcp : abortSources) guardedLink(srcp, acceptSinkp, sampledAbortFire(), flp); std::vector midSources = bodyResult.midSources; midSources.push_back(acceptSinkp); AstNodeExpr* finalCondp = bodyResult.finalCondp; if (finalCondp) { if (finalCondp->backp()) finalCondp = finalCondp->cloneTreePure(false); finalCondp = new AstLogOr{flp, finalCondp, abortFireExpr(condp, flp)}; } return {bodyResult.termVertexp, finalCondp, std::move(midSources)}; } // rejectOnFail treats m_condp as the success condition and fires on // !condp, so the edge carries !sampledAbortFire(). SvaStateVertex* const rejectSinkp = m_graph.createStateVertex(); rejectSinkp->m_isRejectSink = true; for (SvaStateVertex* const srcp : abortSources) if (!chainAccountsSource(srcp, preEdges)) addAbortRejectEdge(srcp, rejectSinkp, condp, flp); AstNodeExpr* finalCondp = bodyResult.finalCondp; if (finalCondp) { if (finalCondp->backp()) finalCondp = finalCondp->cloneTreePure(false); finalCondp = new AstLogAnd{flp, finalCondp, new AstLogNot{flp, abortFireExpr(condp, flp)}}; } return {bodyResult.termVertexp, finalCondp, bodyResult.midSources}; } public: SvaNfaBuilder(SvaGraph& graph, AstNodeModule* modp, V3UniqueNames& propTempNames, bool isCoverSeq = false, bool needsRejectVerdict = true, 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. // m_outerAbortStack survives: an abort wrapping the implication covers the // consequent too (IEEE 1800-2023 16.12.14). void resetScope() { m_inUnboundedScope = false; m_temporalGuardStack.clear(); } // 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)) { return buildSExpr(sexprp, entryVtxp, isTopLevelStep); } if (AstSConsRep* const repp = VN_CAST(nodep, SConsRep)) { return buildConsRep(repp, entryVtxp, isTopLevelStep); } if (AstPropAlways* const alwaysp = VN_CAST(nodep, PropAlways)) { return buildPropAlways(alwaysp, entryVtxp, isTopLevelStep); } if (AstSGotoRep* const repp = VN_CAST(nodep, SGotoRep)) { return buildGotoRep(repp, entryVtxp); } if (AstSThroughout* const throughoutp = VN_CAST(nodep, SThroughout)) { return buildThroughout(throughoutp, entryVtxp, isTopLevelStep); } if (AstSOr* const orp = VN_CAST(nodep, SOr)) { return buildOrMerge(orp->lhsp(), orp->rhsp(), entryVtxp, orp->fileline(), isTopLevelStep); } 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, {}}; } if (AstSAnd* const andp = VN_CAST(nodep, SAnd)) { return buildSAnd(andp, entryVtxp, isTopLevelStep); } if (AstSIntersect* const intp = VN_CAST(nodep, SIntersect)) { // Conjoin equal-length intersect checks, retaining a non-flattened fallback. const int lhsLen = fixedLength(intp->lhsp()); const int rhsLen = fixedLength(intp->rhsp()); if (lhsLen >= 0 && rhsLen >= 0) { if (lhsLen != rhsLen) { // Unequal fixed lengths share no common length -> never matches. return buildNeverMatchIntersect(intp, entryVtxp, isTopLevelStep, "intersect operands have no common length"); } if (AstNodeExpr* const conjp = conjoinFixedSeqs(intp->lhsp(), intp->rhsp(), intp->fileline())) { return buildFromLoweringTree(conjp, entryVtxp, isTopLevelStep); } return buildSameEndIntersectCombiner(intp->lhsp(), intp->rhsp(), entryVtxp, intp->fileline(), lhsLen, isTopLevelStep); } return buildVarLenIntersect(intp, entryVtxp, isTopLevelStep); } if (AstSWithin* const withinp = VN_CAST(nodep, SWithin)) { return buildSWithin(withinp, entryVtxp, isTopLevelStep); } if (AstAbortOn* const ap = VN_CAST(nodep, AbortOn)) { return buildAbortOn(ap->condp(), ap->propp(), entryVtxp, ap->kind(), ap->fileline(), isTopLevelStep); } if (VN_IS(nodep, SNonConsRep)) return BuildResult::fail(); if (AstImplication* const implp = VN_CAST(nodep, Implication)) { return buildImplicationEdges(implp->lhsp(), implp->rhsp(), entryVtxp, implp->isOverlapped(), implp->isFollowedBy(), implp->lhsp(), implp->fileline()); } 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. 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. if (isFollowedBy && antResult.termVertexp != entryVtxp) { 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, "followed-by antecedent terminal at entry must carry finalCondp"); // Use raw createStateVertex() so trigVtxp starts without liveness -- // reaching the antecedent terminal is a definitive event. SvaStateVertex* const trigVtxp = m_graph.createStateVertex(); if (antResult.finalCondp) { m_graph.addLink(antResult.termVertexp, trigVtxp, sampled(antResult.finalCondp->cloneTreePure(false))); // Followed-by non-vacuous fail: rejectOnFail fires when the attempt // is live (termVtx reachable) and sampled(antecedent) is false. if (isFollowedBy) { SvaStateVertex* const sinkVtxp = m_graph.createStateVertex(); sinkVtxp->m_isRejectSink = true; SvaTransEdge* const ep = m_graph.addLink(antResult.termVertexp, sinkVtxp, sampled(antResult.finalCondp->cloneTreePure(false))); ep->m_rejectOnFail = true; } // finalCondp is cloned into the Sampled nodes; if the original is // not parented anywhere in the AST anymore it must be freed here // or ASan flags it as a leak (e.g. t_sequence_bool_ops). if (!antResult.finalCondp->backp()) { VL_DO_DANGLING(antResult.finalCondp->deleteTree(), antResult.finalCondp); } } else { m_graph.addLink(antResult.termVertexp, trigVtxp); } resetScope(); SvaStateVertex* bodyEntryp = trigVtxp; if (!isOverlapped) { SvaStateVertex* const delayVtxp = m_graph.createStateVertex(); m_graph.addClockedEdge(trigVtxp, delayVtxp); bodyEntryp = delayVtxp; } return buildExpr(bodyExprp, bodyEntryp, /*isTopLevelStep=*/true); } BuildResult build(AstNodeExpr* exprp) { m_graph.m_startVertexp = scopedCreateVertex(); return buildExpr(exprp, m_graph.m_startVertexp, /*isTopLevelStep=*/true); } }; //###################################################################### // NFA Lowering (Observed evaluation/state update, Reactive action dispatch) 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 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 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} {} }; 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); if (!condp) return statep; return new AstLogAnd{flp, statep, sampled(condp->cloneTreePure(false))}; } static AstNodeExpr* andCond(FileLine* flp, AstNodeExpr* exprp, AstNodeExpr* condp) { if (!condp) return exprp; return new AstLogAnd{flp, exprp, condp->cloneTreePure(false)}; } // bp is always non-null; only ap can be null (serving as accumulator). static AstNodeExpr* orExprs(FileLine* flp, AstNodeExpr* ap, AstNodeExpr* bp) { if (!ap) return bp; return new AstLogOr{flp, ap, bp}; } 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}}; } 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}; }; UASSERT_OBJ(size > 0, idxp, "Ring size must be positive"); if (size == 1) return u32Const(0); // idx == size - 1 ? 0 : idx + 1 AstAdd* const addp = new AstAdd{flp, new AstVarRef{flp, idxp, VAccess::READ}, u32Const(1)}; addp->dtypeFrom(idxp); AstCond* const condp = new AstCond{ flp, new AstEq{flp, new AstVarRef{flp, idxp, VAccess::READ}, u32Const(size - 1)}, u32Const(0), addp}; condp->dtypeFrom(idxp); return condp; } static AstNodeExpr* delayRingBit(FileLine* flp, AstVar* ringp, AstNodeExpr* idxExprp, VAccess access = VAccess::READ) { // 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* 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); } } 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; } // 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* 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 }; // 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) { AstNode* bodyp = nullptr; for (int i = 0; i < c.N; ++i) { if (!c.vtx[i]->datap()->stateVarp) continue; AstNodeExpr* nextStatep = nullptr; for (const V3GraphEdge& edger : c.vtx[i]->inEdges()) { const SvaTransEdge& tedger = static_cast(edger); if (!tedger.m_consumesCycle) continue; const int fromIdx = tedger.fromVtxp()->color(); UASSERT_OBJ(c.vtx[fromIdx]->datap()->stateSigp, tedger.fromVtxp(), "Clocked-edge source missing stateSig"); AstNodeExpr* srcSigp = c.vtx[fromIdx]->datap()->stateSigp->cloneTreePure(false); srcSigp = andCond(c.flp, srcSigp, tedger.m_condp); 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{ c.flp, new AstVarRef{c.flp, c.vtx[i]->datap()->stateVarp, VAccess::WRITE}, nextStatep}; appendStmt(bodyp, assignp); } if (bodyp) appendStmt(c.updateBodyp, bodyp); } // Phase 2b: Bitset ring-buffer delay update. void emitDelayRingUpdate(LowerCtx& c) { for (int ri = 0; ri < c.N; ++ri) { SvaStateVertex* const vtxp = c.vtx[ri]; 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; const uint32_t size = static_cast(vtxp->m_delayRingSize); AstNodeExpr* incomingp = nullptr; for (const V3GraphEdge& edger : vtxp->inEdges()) { ++m_statDelayRingEdgeVisits; const SvaTransEdge& tedger = static_cast(edger); UASSERT_OBJ(tedger.m_consumesCycle == vtxp->m_isFixedDelayRing, vtxp, "Delay-ring incoming edge kind mismatch"); const int fi = tedger.fromVtxp()->color(); UASSERT_OBJ(c.vtx[fi]->datap()->stateSigp, c.vtx[fi], "Delay-ring incoming source missing stateSig"); AstNodeExpr* contribp = c.vtx[fi]->datap()->stateSigp->cloneTreePure(false); contribp = andCond(c.flp, contribp, tedger.m_condp); 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; } AstNodeExpr* clearp = killActive(c); if (c.abortAnyVarp) clearp = orExprs(c.flp, clearp, abortActive(c)); if (vtxp->m_delayRingClearCondp) { clearp = orExprs(c.flp, clearp, sampled(vtxp->m_delayRingClearCondp->cloneTreePure(false))); } if (vtxp->m_abortClearp) { clearp = orExprs(c.flp, clearp, sampled(vtxp->m_abortClearp->cloneTreePure(false))); } if (AstNodeExpr* const alivep = oldAttemptAlive(c)) { clearp = orExprs(c.flp, clearp, new AstLogNot{c.flp, alivep}); } AstNodeExpr* guardp = nullptr; for (AstNodeExpr* const cp : vtxp->m_throughoutConds) { AstNodeExpr* const sampledp = sampled(cp->cloneTreePure(false)); 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); UASSERT_OBJ(evalRingp, vtxp, "Delay ring missing evaluation snapshot"); } } // Done latches retain early AND endpoints after outcome expressions are captured. void emitAndCombinerDoneUpdate(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(); UASSERT_OBJ(c.vtx[l]->datap()->stateSigp && c.vtx[r]->datap()->stateSigp && vtxp->datap()->stateSigp, vtxp, "And-combiner signals unresolved"); 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}, 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{}}}); 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}); } appendStmt(c.updateBodyp, new AstIf{c.flp, clearp, clearLp, setLIfp}); } } 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); } // 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) { 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 (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))}; } 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)); } if (isRangeRing) { 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); 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); } } // wireMatchAndMidSources always adds a Link from result.termVertexp // to m_matchVertexp, so the loop above always sets terminalActivep. UASSERT_OBJ(sigs.terminalActivep, c.graph.m_matchVertexp, "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) { 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}}); } else { stateExprp = c.vtx[i]->datap()->stateSigp->cloneTreePure(false); } AstNodeExpr* guardp = nullptr; for (AstNodeExpr* const cp : conds) { 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))}; 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))}; } } 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)); } sigs.throughoutRejectp = orExprs(c.flp, sigs.throughoutRejectp, failp); } } SignalSet computeSignals(LowerCtx& c, std::vector* outFailAttemptSrcsp, std::vector* outMatchAttemptSrcsp, bool* outAmbiguousResolvedDepthp = nullptr, std::vector* outPerMidSrcsp = nullptr, std::vector* outPerMidCountsp = nullptr) { 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); // Phase 3a: required-step rejection. // Builder only sets m_rejectOnFail on non-clocked Links with m_condp // or m_condVtxp, and the source always has a resolved stateSig. for (const SvaTransEdge* const tedgep : c.edges) { if (!tedgep->m_rejectOnFail) continue; const int fi = tedgep->fromVtxp()->color(); UASSERT_OBJ(c.vtx[fi]->datap()->stateSigp && (tedgep->m_condp || tedgep->m_condVtxp), tedgep->fromVtxp(), "rejectOnFail Link must have condp/condVtxp and source stateSig"); AstNodeExpr* const srcSigp = c.vtx[fi]->datap()->stateSigp->cloneTreePure(false); AstNodeExpr* condp = nullptr; if (tedgep->m_condVtxp) { const int ci = tedgep->m_condVtxp->color(); UASSERT_OBJ(c.vtx[ci]->datap()->stateSigp, tedgep->m_condVtxp, "rejectOnFail condVtxp missing stateSig"); condp = c.vtx[ci]->datap()->stateSigp->cloneTreePure(false); if (tedgep->m_condp) { condp = new AstLogOr{c.flp, condp, tedgep->m_condp->cloneTreePure(false)}; } } else { condp = tedgep->m_condp->cloneTreePure(false); } 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); sigs.requiredStepRejectp = orExprs(c.flp, sigs.requiredStepRejectp, failp); } computeThroughoutReject(c, sigs, failBucketsp, depths); 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. if (c.disableExprp) { // terminalActivep is always set, so gate it unconditionally. AstNodeExpr* const notTermp = new AstLogNot{c.flp, c.disableExprp->cloneTreePure(false)}; sigs.terminalActivep = new AstLogAnd{c.flp, sigs.terminalActivep, notTermp}; if (sigs.rejectBasep) { AstNodeExpr* const notDisp = new AstLogNot{c.flp, c.disableExprp->cloneTreePure(false)}; sigs.rejectBasep = new AstLogAnd{c.flp, sigs.rejectBasep, notDisp}; } if (sigs.throughoutRejectp) { AstNodeExpr* const notDisp = 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; } } finishAttemptOutcomes(c, failBuckets, outFailAttemptSrcsp); finishAttemptOutcomes(c, matchBuckets, outMatchAttemptSrcsp); return sigs; } // Phase 1 seeds: start trigger, registered state reads, delay-ring reads. void seedLinkBaseSignals(LowerCtx& c, AstVar* triggerVarp) { // 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; 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}); } 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}); } else { ringStatep = new AstRedOr{c.flp, new AstVarRef{c.flp, ringp, VAccess::READ}}; } 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"); { AstNodeExpr* const matchLp = buildMatchNow(c.flp, c.vtx[l]->datap()->stateSigp, vtxp->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}; } sigp = orExprs(c.flp, sigp, matchp); } } 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); } } 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"); } } // 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) { // Property negation (IEEE 1800-2023 16.12.1 `not`): invert match/reject. if (negated) { if (isCover) { if (terminalActivep) VL_DO_DANGLING(terminalActivep->deleteTree(), terminalActivep); AstNodeExpr* negRejectp = nullptr; if (matchCondp && rejectBasep) { AstNodeExpr* const sampledCondp = sampled(matchCondp->cloneTreePure(false)); AstNodeExpr* const notCondp = new AstLogNot{flp, sampledCondp}; negRejectp = new AstLogAnd{flp, rejectBasep, notCondp}; } else if (rejectBasep) { VL_DO_DANGLING(rejectBasep->deleteTree(), rejectBasep); } if (throughoutRejectp) negRejectp = orExprs(flp, negRejectp, throughoutRejectp); if (requiredStepRejectp) negRejectp = orExprs(flp, negRejectp, requiredStepRejectp); return negRejectp ? negRejectp : new AstConst{flp, AstConst::BitFalse{}}; } // Negated assert/assume: output = !match. AstNodeExpr* matchp = terminalActivep; if (matchCondp) { AstNodeExpr* const sampledCondp = sampled(matchCondp->cloneTreePure(false)); matchp = new AstLogAnd{flp, matchp, sampledCondp}; } if (throughoutRejectp) VL_DO_DANGLING(throughoutRejectp->deleteTree(), throughoutRejectp); if (rejectBasep) VL_DO_DANGLING(rejectBasep->deleteTree(), rejectBasep); if (requiredStepRejectp) VL_DO_DANGLING(requiredStepRejectp->deleteTree(), requiredStepRejectp); AstNodeExpr* const resultExprp = new AstLogNot{flp, matchp}; return resultExprp; } if (isCover) { if (throughoutRejectp) VL_DO_DANGLING(throughoutRejectp->deleteTree(), throughoutRejectp); if (rejectBasep) VL_DO_DANGLING(rejectBasep->deleteTree(), rejectBasep); if (requiredStepRejectp) VL_DO_DANGLING(requiredStepRejectp->deleteTree(), requiredStepRejectp); if (matchCondp) { AstNodeExpr* const sampledCondp = sampled(matchCondp->cloneTreePure(false)); return new AstLogAnd{flp, terminalActivep, sampledCondp}; } return terminalActivep; } // Assert/assume: output = !reject AstNodeExpr* rejectp = nullptr; if (matchCondp && rejectBasep) { AstNodeExpr* const sampledCondp = sampled(matchCondp->cloneTreePure(false)); rejectp = new AstLogAnd{flp, rejectBasep, new AstLogNot{flp, sampledCondp}}; } else if (rejectBasep) { VL_DO_DANGLING(rejectBasep->deleteTree(), rejectBasep); } if (terminalActivep) VL_DO_DANGLING(terminalActivep->deleteTree(), terminalActivep); if (throughoutRejectp) rejectp = orExprs(flp, rejectp, throughoutRejectp); if (requiredStepRejectp) rejectp = orExprs(flp, rejectp, requiredStepRejectp); if (!rejectp) return new AstConst{flp, AstConst::BitTrue{}}; AstNodeExpr* const resultExprp = new AstLogNot{flp, rejectp}; 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; } 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}; } // 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; } return false; } 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); } 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); } } 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) { const std::string base = baseName + "__a" + std::to_string(i); AstVar* const lp = new AstVar{flp, VVarType::MODULETEMP, base + "_doneL", m_modp->findBitDType()}; lp->lifetime(VLifetime::STATIC_EXPLICIT); m_modp->addStmtsp(lp); vtx[i]->datap()->doneLVarp = lp; AstVar* const rp = new AstVar{flp, VVarType::MODULETEMP, base + "_doneR", m_modp->findBitDType()}; rp->lifetime(VLifetime::STATIC_EXPLICIT); m_modp->addStmtsp(rp); vtx[i]->datap()->doneRVarp = rp; continue; } if (vtx[i]->m_delayRingSize) { const std::string base = baseName + "__d" + std::to_string(i); AstNodeDType* const ringDTypep = m_modp->findLogicDType( 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; 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; continue; } if (!vtx[i]->datap()->needsReg) continue; if (i == lv.startIdx) 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); // Phase 1: Resolve combinational Links via fixed-point propagation. resolveLinks(c, ctlOnVarp); 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 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); 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}}); // 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; } }; // 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 //###################################################################### // Top-level visitor class AssertNfaVisitor final : public VNVisitor { // STATE AstNodeModule* m_modp = nullptr; // Current module being processed AstClocking* m_defaultClockingp = nullptr; // Default clocking AstDefaultDisable* m_defaultDisablep = nullptr; // Default disable iff SvaNfaLowering* m_loweringp = nullptr; // NFA-to-hardware lowering engine 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 template void visitSampledValue(T_Node* const nodep) { if (m_sampledValueClockp && !nodep->sentreep()) { nodep->sentreep(m_sampledValueClockp->cloneTree(true)); } iterateChildren(nodep); } // Wire match vertex and mid-window sources for a successful NFA build. static void wireMatchAndMidSources(SvaGraph& graph, const BuildResult& result, FileLine* flp) { graph.createMatchVertex(); // Skip the main term Link when midSources already cover every // end-of-match (cover_sequence path); otherwise the per-mid extraction // double-counts via the merge vertex. if (!result.termIsMidMerge) { graph.addLink(result.termVertexp, graph.m_matchVertexp); } for (SvaStateVertex* srcVtxp : result.midSources) { AstNodeExpr* condp = nullptr; for (AstNodeExpr* const tc : srcVtxp->m_throughoutConds) { AstNodeExpr* const tcClone = tc->cloneTreePure(false); condp = condp ? new AstLogAnd{flp, condp, tcClone} : tcClone; } graph.addLink(srcVtxp, graph.m_matchVertexp, condp); srcVtxp->m_isUnbounded = true; } } static AstNodeExpr* getSequenceBodyExprp(const AstSequence* seqp) { AstNode* bodyp = seqp->stmtsp(); while (bodyp && VN_IS(bodyp, Var)) bodyp = bodyp->nextp(); return VN_CAST(bodyp, NodeExpr); } static AstPropSpec* getPropertySpecp(const AstProperty* propp) { AstNode* stmtp = propp->stmtsp(); // V3LinkParse emits InitialStaticStmt for property-local variable // initialisers; the InitialAutomaticStmt variant only appears for // task/function-scope automatic lifetime, not properties. while (stmtp && (VN_IS(stmtp, Var) || VN_IS(stmtp, InitialStaticStmt) || VN_IS(stmtp, InitialAutomaticStmt))) { // LCOV_EXCL_LINE stmtp = stmtp->nextp(); } return VN_CAST(stmtp, PropSpec); } void inlineNamedProperty(AstPropSpec* outerSpecp, AstFuncRef* funcrefp, const AstProperty* propyp) { // Recursion guard: IEEE 1800-2023 16.12.1 forbids recursive properties. // V3Width emits "Recursive property call" for direct recursion before this // pass runs; this catches any nested-inlining cycle that slips past. if (m_inliningProps.count(propyp)) { funcrefp->v3error("Illegal recursive property reference"); // LCOV_EXCL_LINE return; // LCOV_EXCL_LINE } m_inliningProps.insert(propyp); struct Guard final { std::unordered_set& setr; const AstProperty* keyp; ~Guard() { setr.erase(keyp); } } guard{m_inliningProps, propyp}; AstPropSpec* propSpecp = getPropertySpecp(propyp); UASSERT_OBJ(propSpecp, funcrefp, "Property has no body PropSpec"); propSpecp = propSpecp->cloneTree(false); const V3TaskConnects tconnects = V3Task::taskConnects(funcrefp, propyp->stmtsp()); std::unordered_map portMap; for (const auto& tconnect : tconnects) { portMap[tconnect.first] = tconnect.second->exprp(); } // Promote property-local variables to module-level temps (IEEE 16.10). std::unordered_map localVarMap; for (AstNode* stmtp = propyp->stmtsp(); stmtp; stmtp = stmtp->nextp()) { if (AstVar* const varp = VN_CAST(stmtp, Var)) { if (!varp->isIO()) { const string newName = m_propVarNames.get(varp); AstVar* const newVarp = new AstVar{varp->fileline(), VVarType::MODULETEMP, newName, varp->dtypep()}; newVarp->lifetime(VLifetime::STATIC_EXPLICIT); m_modp->addStmtsp(newVarp); localVarMap[varp] = newVarp; } } } propSpecp->foreach([&](AstVarRef* refp) { const auto portIt = portMap.find(refp->varp()); if (portIt != portMap.end()) { refp->replaceWith(portIt->second->cloneTree(false)); VL_DO_DANGLING(pushDeletep(refp), refp); return; } const auto localIt = localVarMap.find(refp->varp()); if (localIt != localVarMap.end()) refp->varp(localIt->second); }); // LCOV_EXCL_LINE -- gcov attributes lambda's implicit return to `})` for (const auto& tconnect : tconnects) { pushDeletep(tconnect.second->exprp()->unlinkFrBack()); } // Merge disable iff (IEEE 1800-2023 16.12.1) if (outerSpecp->disablep() && propSpecp->disablep()) { outerSpecp->v3error("disable iff expression before property call " "and in its body is not legal"); pushDeletep(propSpecp->disablep()->unlinkFrBack()); } if (outerSpecp->disablep()) { propSpecp->disablep(outerSpecp->disablep()->unlinkFrBack()); } if (outerSpecp->sensesp() && propSpecp->sensesp()) { outerSpecp->v3warn(E_UNSUPPORTED, "Unsupported: Clock event before property call and in its body"); pushDeletep(propSpecp->sensesp()->unlinkFrBack()); } if (outerSpecp->sensesp()) { AstSenItem* const sensesp = outerSpecp->sensesp(); sensesp->unlinkFrBack(); propSpecp->sensesp(sensesp); } outerSpecp->replaceWith(propSpecp); VL_DO_DANGLING(pushDeletep(outerSpecp), outerSpecp); } void inlineSequenceRef(AstFuncRef* funcrefp, AstSequence* seqp) { AstNodeExpr* const bodyExprp = getSequenceBodyExprp(seqp); UASSERT_OBJ(bodyExprp, funcrefp, "Sequence has no body expression"); AstNodeExpr* const clonedp = bodyExprp->cloneTree(false); const V3TaskConnects tconnects = V3Task::taskConnects(funcrefp, seqp->stmtsp()); std::unordered_map portMap; for (const auto& tconnect : tconnects) { portMap[tconnect.first] = tconnect.second->exprp(); } clonedp->foreach([&](AstVarRef* refp) { const auto it = portMap.find(refp->varp()); if (it != portMap.end()) { refp->replaceWith(it->second->cloneTree(false)); VL_DO_DANGLING(pushDeletep(refp), refp); } }); for (const auto& tconnect : tconnects) { pushDeletep(tconnect.second->exprp()->unlinkFrBack()); } funcrefp->replaceWith(clonedp); VL_DO_DANGLING(pushDeletep(funcrefp), funcrefp); // Clear referenced flag so V3AssertPre cleanup does not emit // spurious UNSUPPORTED for sequences that were already inlined here. seqp->isReferenced(false); } // Must run before hasMultiCycleExpr() so NFA sees sequence bodies. void inlineAllSequenceRefs(AstNode* rootp) { bool changed = true; while (changed) { changed = false; rootp->foreach([&](AstFuncRef* funcrefp) { if (changed) return; if (AstSequence* const seqp = VN_CAST(funcrefp->taskp(), Sequence)) { inlineSequenceRef(funcrefp, seqp); changed = true; } }); } } static bool hasMultiCycleExpr(const AstNode* nodep) { return nodep->exists([](const AstNode* np) { if (const auto* const ep = VN_CAST(np, NodeExpr)) return ep->isMultiCycleSva(); return false; }); } static VPropStrength effectiveAssertPropStrength(const AstPropSpec* const propSpecp) { if (propSpecp->propStrength() != VPropStrength::DEFAULT) return propSpecp->propStrength(); return propSpecp->fileline()->language() <= V3LangCode::L1800_2005 ? VPropStrength::STRONG : VPropStrength::WEAK; } // Bare `assert property (p until q)` with boolean operands stays on // V3AssertPre's AstLoop lowering, which preserves per-attempt action-block // firings that this NFA's single-bit aggregated state cannot. Strong bare // forms are also lowered there. NFA still owns sequence operands and any // embedding inside a multi-cycle context (implication consequent, or/and // operands, etc.). static bool isBareTopLevelUntil(AstNode* propp) { AstNode* p = propp; if (AstPropSpec* const specp = VN_CAST(p, PropSpec)) p = specp->propp(); 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()); } struct PropertyParts final { AstNodeExpr* triggerExprp = nullptr; AstNodeExpr* seqExprp = nullptr; bool isOverlapped = true; bool hasImplication = false; bool isFollowedBy = false; // True for #-# / #=# (non-vacuous-fail on antecedent miss) }; static PropertyParts decomposeProperty(AstNode* propp) { PropertyParts parts; if (AstPropSpec* const specp = VN_CAST(propp, PropSpec)) { propp = specp->propp(); } if (AstImplication* const implp = VN_CAST(propp, Implication)) { parts.hasImplication = true; parts.isOverlapped = implp->isOverlapped(); parts.isFollowedBy = implp->isFollowedBy(); parts.triggerExprp = implp->lhsp(); parts.seqExprp = implp->rhsp(); } else if (AstNodeExpr* const exprp = VN_CAST(propp, NodeExpr)) { parts.triggerExprp = nullptr; parts.seqExprp = exprp; } 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"); UASSERT(parts.triggerExprp, "Implication pass action split requested without trigger"); // Direct vacuous/nonvacuous classification uses the antecedent value in the current // assertion attempt. Leave delayed antecedents on the existing NFA pass path. 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) { FileLine* const flp = assertp->fileline(); AstNode* const passsp = assertp->passsp()->unlinkFrBackWithNext(); AstNode* splitsp = nullptr; if (!parts.isFollowedBy) { AstNodeExpr* 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); 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); } // Allocate disable-iff counter + snapshot vars. 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 {}; AstNodeDType* const u32DTypep = m_modp->findBasicDType(VBasicDTypeKwd::UINT32); const std::string cntName = m_disableCntNames.get(""); AstVar* const cntp = new AstVar{flp, VVarType::MODULETEMP, cntName, u32DTypep}; cntp->lifetime(VLifetime::STATIC_EXPLICIT); m_modp->addStmtsp(cntp); AstNodeExpr* const incrExprp = new AstAdd{flp, new AstVarRef{flp, cntp, VAccess::READ}, new AstConst{flp, AstConst::WidthedValue{}, 32, 1u}}; incrExprp->dtypeFrom(cntp); m_modp->addStmtsp(new AstAlways{ flp, VAlwaysKwd::ALWAYS, new AstSenTree{flp, new AstSenItem{flp, VEdgeType::ET_POSEDGE, disableExprp->cloneTreePure(false)}}, new AstAssign{flp, new AstVarRef{flp, cntp, VAccess::WRITE}, incrExprp}}); AstVar* const snapp = new AstVar{flp, VVarType::MODULETEMP, cntName + "__snap", u32DTypep}; snapp->lifetime(VLifetime::STATIC_EXPLICIT); m_modp->addStmtsp(snapp); return {cntp, snapp}; } // On a PropSpec-wrapped assertion whose NFA build failed with a semantic // error (errorEmitted), replace the body with a BitFalse const so later // passes see a well-formed AST. Returns true if replaced. void replaceBodyOnBuildError(FileLine* flp, AstPropSpec* propSpecp, bool errorEmitted) { if (!errorEmitted) return; AstNode* const innerPropp = propSpecp->propp(); innerPropp->replaceWith(new AstConst{flp, AstConst::BitFalse{}}); VL_DO_DANGLING(pushDeletep(innerPropp), innerPropp); } // Hoist a leading clocking event (IEEE 1800-2023 16.7): bool hoistClockedSeq(AstPropSpec* specp) { while (AstSClocked* const clockedp = VN_CAST(specp->propp(), SClocked)) { if (specp->sensesp()) { clockedp->v3warn(E_UNSUPPORTED, "Unsupported: multiclocked sequence or property"); replaceBodyOnBuildError(specp->fileline(), specp, true); return true; } for (const AstSenItem* sp = clockedp->sensesp(); sp; sp = VN_CAST(sp->nextp(), SenItem)) { if (!sp->edgeType().anEdge()) { clockedp->v3warn(E_UNSUPPORTED, "Unsupported: non-edge clocking event on a sequence; " "use an edge such as @(posedge clk)"); replaceBodyOnBuildError(specp->fileline(), specp, true); return true; } } specp->sensesp(clockedp->sensesp()->unlinkFrBackWithNext()); AstNodeExpr* const bodyp = clockedp->exprp()->unlinkFrBack(); clockedp->replaceWith(bodyp); VL_DO_DANGLING(pushDeletep(clockedp), clockedp); } // A clocking event anywhere else in the sequence is not supported. const AstSClocked* nestedp = nullptr; specp->propp()->foreach([&](const AstSClocked* p) { if (!nestedp) nestedp = p; }); if (nestedp) { nestedp->v3warn(E_UNSUPPORTED, "Unsupported: clocking event inside sequence expression"); replaceBodyOnBuildError(specp->fileline(), specp, true); return true; } return false; } // Build the NFA graph for a property body, handling both the antecedent // |-> consequent and simple sequence cases. Returns the consequent/body // BuildResult (invalid on parse/build failure). BuildResult buildAssertionGraph(SvaNfaBuilder& builder, SvaGraph& graph, AstNodeExpr* seqBodyp, const PropertyParts& parts, FileLine* flp) { if (!parts.hasImplication) return builder.build(seqBodyp); graph.m_startVertexp = graph.createStateVertex(); return builder.buildImplicationEdges(parts.triggerExprp, seqBodyp, graph.m_startVertexp, parts.isOverlapped, parts.isFollowedBy, 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; } } 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 (!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) // (or rhs inline when K == 0). No-op if refp is not in matchMap. void substituteMatchItemRef(AstVarRef* refp, unsigned K, const std::unordered_map& matchMap) { const auto it = matchMap.find(refp->varp()); if (it == matchMap.end()) return; AstNodeExpr* newp = it->second->cloneTreePure(false); if (K > 0) { AstConst* const ticksp = new AstConst{refp->fileline(), AstConst::WidthedValue{}, 32, static_cast(K)}; AstPast* const pastp = new AstPast{refp->fileline(), newp, ticksp, nullptr, /* propertyTiming */ true}; pastp->dtypeFrom(newp); newp = pastp; } refp->replaceWith(newp); VL_DO_DANGLING(pushDeletep(refp), refp); return; } // Recursively walk a consequent. Returns cycle length consumed and // substitutes each VarRef to a captured local var with $past(rhs, K) // (or rhs inline when K == 0). Reports E_UNSUPPORTED on non-constant // delays or composite sequence operators. int walkSubstituteMatchItems(AstNodeExpr* nodep, unsigned K, const std::unordered_map& matchItems, bool& errorEmitted) { if (AstSExpr* const sexprp = VN_CAST(nodep, SExpr)) { // IEEE 1800-2023 16.9.2: cycle_delay's lhsp is a constant_expression // and the delay form in a sequence is always `##N`, folded by // V3Const + V3Param before V3AssertNfa. Range form `##[m:n]` is the // only user-visible reject here. AstDelay* const delayp = VN_AS(sexprp->delayp(), Delay); UASSERT_OBJ(delayp->isCycleDelay() && VN_IS(delayp->lhsp(), Const), sexprp, "SVA cycle delay must have a constant lhsp"); if (delayp->isRangeDelay()) { sexprp->v3warn(E_UNSUPPORTED, "Unsupported: property local variable used across " "non-constant cycle delay in consequent" " (IEEE 1800-2023 16.10)"); errorEmitted = true; return -1; } const unsigned delayCycles = VN_AS(delayp->lhsp(), Const)->toUInt(); int preLen = 0; if (AstNodeExpr* const prep = sexprp->preExprp()) { preLen = walkSubstituteMatchItems(prep, K, matchItems, errorEmitted); if (errorEmitted) return -1; } const int bodyLen = walkSubstituteMatchItems(sexprp->exprp(), K + preLen + delayCycles, matchItems, errorEmitted); if (errorEmitted) return -1; return preLen + delayCycles + bodyLen; } if (nodep->isMultiCycleSva()) { nodep->v3warn(E_UNSUPPORTED, "Unsupported: property local variable used across " "composite sequence operator in consequent" " (IEEE 1800-2023 16.10)"); errorEmitted = true; return -1; } std::vector refs; nodep->foreach([&refs](AstVarRef* p) { refs.push_back(p); }); for (AstVarRef* const refp : refs) substituteMatchItemRef(refp, K, matchItems); return 0; } // Lower property-local match-item assignments before NFA construction. // Without this, the antecedent's AstExprStmt(, antBool) // survives into every NFA edge as a continuous-alias side-effect, so the // local-var temp tracks the current cycle's rhs_expr rather than the // antecedent-match cycle's value -- wrong for `|-> ##N` and `|=> ##N` // with N > 0 (issue #7587). Each consequent reference to the local var // is replaced with `$past(rhs_expr, K)` where K = (overlapped ? 0 : 1) // plus any accumulated `##N` delay. Returns true if E_UNSUPPORTED was // emitted; caller must replace the body with BitFalse and bail. bool liftMatchItemSubstitutions(PropertyParts& parts, AstNodeExpr* seqBodyp) { if (!parts.hasImplication) return false; AstExprStmt* const exprStmtp = VN_CAST(parts.triggerExprp, ExprStmt); if (!exprStmtp) return false; // IEEE 1800-2023 16.10 BNF requires `(expr, match_item {, match_item})` // with at least one match item; V3LinkParse only emits ExprStmt for // this form and only emits AstAssign with VarRef LHS for each item. std::unordered_map matchItems; for (AstNode* stmtp = exprStmtp->stmtsp(); stmtp; stmtp = stmtp->nextp()) { AstAssign* const assignp = VN_AS(stmtp, Assign); AstVarRef* const lhsRefp = VN_AS(assignp->lhsp(), VarRef); matchItems[lhsRefp->varp()] = assignp->rhsp(); } const unsigned startK = parts.isOverlapped ? 0 : 1; bool errorEmitted = false; walkSubstituteMatchItems(seqBodyp, startK, matchItems, errorEmitted); // Match-item substitution / strip mutates ancestor purity. Release // builds don't auto-clear caches on edits, so refresh here. VIsCached::clearCacheTree(); if (errorEmitted) return true; AstNodeExpr* const antBoolp = exprStmtp->resultp()->unlinkFrBack(); exprStmtp->replaceWith(antBoolp); VL_DO_DANGLING(pushDeletep(exprStmtp), exprStmtp); parts.triggerExprp = antBoolp; 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, const AstNodeExpr* seqBodyp, bool negated) { if (!hasPropertyControlConjunction(seqBodyp)) return nullptr; if (negated) return "negation"; if (VN_IS(assertp, Cover)) return "cover"; if (VN_AS(assertp, Assert)->passsp()) return "a pass action"; 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); } } inlineAllSequenceRefs(assertp->propp()); if (hoistClockedSeq(VN_AS(assertp->propp(), PropSpec))) return nullptr; AstPropSpec* const propp = VN_AS(assertp->propp(), PropSpec); if (!VN_IS(assertp, Cover) && effectiveAssertPropStrength(propp) == VPropStrength::STRONG) { propp->v3warn(E_UNSUPPORTED, "Unsupported: strong property in " + assertp->verilogKwd() + "."); replaceBodyOnBuildError(assertp->fileline(), propp, /*errorEmitted=*/true); return nullptr; } if (!hasMultiCycleExpr(propp)) return nullptr; // A nested property instance keeps its body behind the call; lowering would drop it. if (propp->exists([](const AstFuncRef* refp) { return VN_IS(refp->taskp(), Property); })) { assertp->v3warn(E_UNSUPPORTED, "Unsupported: property instance inside a multi-cycle property " "expression"); VL_DO_DANGLING(pushDeletep(assertp->unlinkFrBack()), assertp); return nullptr; } if (isBareTopLevelUntil(propp)) return nullptr; return propp; } // 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); } // VISITORS void visit(AstNodeModule* nodep) override { VL_RESTORER(m_modp); VL_RESTORER(m_loweringp); VL_RESTORER(m_defaultClockingp); VL_RESTORER(m_defaultDisablep); m_modp = nodep; m_defaultClockingp = nullptr; m_defaultDisablep = nodep->defaultDisablep(); SvaNfaLowering lowering{nodep}; m_loweringp = &lowering; iterateChildren(nodep); } void visit(AstClocking* nodep) override { if (nodep->isDefault() && !m_defaultClockingp) m_defaultClockingp = nodep; iterateChildren(nodep); } void visit(AstGenBlock* nodep) override { VL_RESTORER(m_defaultDisablep); m_defaultDisablep = nodep->defaultDisablep(); iterateChildren(nodep); } void visit(AstDefaultDisable* nodep) override {} void visit(AstFell* nodep) override { visitSampledValue(nodep); } void visit(AstPast* nodep) override { visitSampledValue(nodep); } void visit(AstRose* nodep) override { visitSampledValue(nodep); } void visit(AstStable* nodep) override { visitSampledValue(nodep); } void visit(AstAssert* nodep) override { processAssertion(nodep); } void visit(AstCover* nodep) override { processAssertion(nodep); } void visit(AstRestrict* nodep) override { // Restrict property is ignored by simulators (IEEE 1800-2023 16.12.2). // Remove here so temporal SExpr don't leak to V3AssertPre. VL_DO_DANGLING(pushDeletep(nodep->unlinkFrBack()), nodep); } void visit(AstAssertIntrinsic* nodep) override {} void visit(AstNode* nodep) override { iterateChildren(nodep); } public: explicit AssertNfaVisitor(AstNetlist* nodep) { iterate(nodep); } }; //###################################################################### // Top entry point void V3AssertNfa::assertNfaAll(AstNetlist* nodep) { UINFO(2, __FUNCTION__ << ":" << endl); { AssertNfaVisitor{nodep}; } V3Global::dumpCheckGlobalTree("assertnfa", 0, dumpTreeEitherLevel() >= 3); }