Support a sequence used as an event control (#7797) (#7846)

This commit is contained in:
Yilou Wang
2026-07-06 13:12:01 -04:00
committed by GitHub
parent 39b646349f
commit 8ed0acf1ec
23 changed files with 465 additions and 28 deletions
+69 -6
View File
@@ -90,6 +90,58 @@ public:
explicit DefaultDisablePropagateVisitor(AstNetlist* nodep) { iterate(nodep); }
};
// Lower a sequence used as an event control ('@seq', IEEE 1800-2023 9.4.2.4) into a
// synthesized event fired by an internal 'cover sequence' on each end-of-match
class SeqEventLowerVisitor final : public VNVisitor {
// STATE
AstNodeModule* m_modp = nullptr; // Current module
V3UniqueNames m_names{"__VseqEvent"}; // Synthesized event names
// VISITORS
void visit(AstNodeModule* nodep) override {
VL_RESTORER(m_modp);
m_modp = nodep;
iterateChildren(nodep);
}
void visit(AstSenItem* nodep) override {
AstFuncRef* const funcrefp = VN_CAST(nodep->sensp(), FuncRef);
if (funcrefp && VN_IS(funcrefp->taskp(), Sequence)) {
FileLine* const flp = nodep->fileline();
AstVar* const eventp = new AstVar{flp, VVarType::MODULETEMP, m_names.get(nodep),
m_modp->findBasicDType(VBasicDTypeKwd::EVENT)};
eventp->lifetime(VLifetime::STATIC_EXPLICIT);
m_modp->addStmtsp(eventp);
v3Global.setHasEvents();
funcrefp->unlinkFrBack();
nodep->sensp(new AstVarRef{flp, eventp, VAccess::READ});
const bool automaticActual = funcrefp->exists([](const AstNodeVarRef* refp) {
return refp->varp() && refp->varp()->lifetime().isAutomatic();
});
if (automaticActual) {
nodep->v3error("Arguments to a sequence used as an event control must be"
" static (IEEE 1800-2023 9.4.2.4)");
VN_AS(funcrefp->taskp(), Sequence)->isReferenced(false);
VL_DO_DANGLING(pushDeletep(funcrefp), funcrefp);
return;
}
AstFireEvent* const firep
= new AstFireEvent{flp, new AstVarRef{flp, eventp, VAccess::WRITE}, false};
AstCover* const coverp
= new AstCover{flp, new AstPropSpec{flp, nullptr, nullptr, funcrefp}, firep,
VAssertType::CONCURRENT};
coverp->isCoverSeq(true);
coverp->isSeqEvent(true);
m_modp->addStmtsp(coverp);
return;
}
iterateChildren(nodep);
}
void visit(AstNode* nodep) override { iterateChildren(nodep); }
public:
explicit SeqEventLowerVisitor(AstNetlist* nodep) { iterate(nodep); }
};
} // namespace
void V3AssertCommon::collectDefaultDisable(AstNetlist* nodep) {
@@ -97,6 +149,11 @@ void V3AssertCommon::collectDefaultDisable(AstNetlist* nodep) {
{ DefaultDisablePropagateVisitor{nodep}; }
}
void V3AssertCommon::lowerSequenceEvents(AstNetlist* nodep) {
{ SeqEventLowerVisitor{nodep}; }
V3Global::dumpCheckGlobalTree("assertseqevent", 0, dumpTreeEitherLevel() >= 3);
}
//######################################################################
// AssertDeFutureVisitor
// If any AstFuture, then move all non-future varrefs to be one cycle behind,
@@ -546,14 +603,19 @@ class AssertVisitor final : public VNVisitor {
bool selfDestruct = false;
bool passspGated = false;
if (const AstCover* const snodep = VN_CAST(nodep, Cover)) {
const AstCover* const coverp = VN_CAST(nodep, Cover);
// A sequence event control is not an assertion directive; no assertion control
const bool seqEvent = coverp && coverp->isSeqEvent();
if (coverp) {
++m_statCover;
if (!v3Global.opt.coverageUser()) {
if (seqEvent) {
// Keep the event-fire action, with no coverage bucket
} else if (!v3Global.opt.coverageUser()) {
selfDestruct = true;
} else {
// V3Coverage assigned us a bucket to increment.
AstCoverInc* const covincp = VN_AS(snodep->coverincsp(), CoverInc);
UASSERT_OBJ(covincp, snodep, "Missing AstCoverInc under assertion");
AstCoverInc* const covincp = VN_AS(coverp->coverincsp(), CoverInc);
UASSERT_OBJ(covincp, coverp, "Missing AstCoverInc under assertion");
covincp->unlinkFrBackWithNext(); // next() might have AstAssign for trace
if (message != "") covincp->declp()->comment(message);
if (passsp) {
@@ -597,7 +659,8 @@ class AssertVisitor final : public VNVisitor {
FileLine* const flp = nodep->fileline();
bool passspAlreadyGated = false;
if (passsp && VN_IS(passsp, If)) passspAlreadyGated = VN_AS(passsp, If)->user1();
if (passsp && !passspGated && !passspAlreadyGated && !VN_IS(propExprp, PExpr)) {
if (passsp && !passspGated && !passspAlreadyGated && !VN_IS(propExprp, PExpr)
&& !seqEvent) {
passsp = newIfAssertPassOn(passsp, nodep->directive(), nodep->userType(),
/*vacuous=*/false);
}
@@ -607,7 +670,7 @@ class AssertVisitor final : public VNVisitor {
AstNode* bodysp = assertBody(nodep, propExprp, passsp, failsp);
if (disablep) bodysp = new AstIf{flp, new AstLogNot{flp, disablep}, bodysp};
// Add assertOn check last, for better combining
bodysp = newIfAssertOn(bodysp, nodep->directive(), nodep->userType());
if (!seqEvent) bodysp = newIfAssertOn(bodysp, nodep->directive(), nodep->userType());
if (sentreep) bodysp = new AstAlways{flp, VAlwaysKwd::ALWAYS, sentreep, bodysp};
if (passsp && !passsp->backp()) VL_DO_DANGLING(pushDeletep(passsp), passsp);
+1
View File
@@ -27,6 +27,7 @@
class V3AssertCommon final {
public:
static void collectDefaultDisable(AstNetlist* nodep) VL_MT_DISABLED;
static void lowerSequenceEvents(AstNetlist* nodep) VL_MT_DISABLED;
};
class V3Assert final {
+30 -15
View File
@@ -287,6 +287,8 @@ class SvaNfaBuilder final {
// 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;
// Unsupported endpoint topology must reject, not ignore, or the wait hangs
bool m_isSeqEvent = false;
struct RangeDelayRejectInfo final {
SvaStateVertex* startp = nullptr;
@@ -294,6 +296,15 @@ class SvaNfaBuilder final {
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_throughoutStack.empty()) return baseCondp;
// AND all throughout conditions (supports nesting)
@@ -574,8 +585,7 @@ class SvaNfaBuilder final {
// 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))) {
flp->v3warn(COVERIGN,
"Ignoring unsupported: cover sequence with this ranged cycle delay");
warnEndpointUnsupported(flp, "this ranged cycle delay");
outErrorEmitted = true;
return false;
}
@@ -881,8 +891,7 @@ class SvaNfaBuilder final {
// terminal, so a cover sequence would under-count. Reject the ranged form
// (the single-count b[->N] has one end and is enumerated correctly).
if (m_isCoverSeq && hasMax && maxN > minN) {
flp->v3warn(COVERIGN,
"Ignoring unsupported: cover sequence with a ranged goto repetition");
warnEndpointUnsupported(flp, "a ranged goto repetition");
return BuildResult::failWithError();
}
@@ -946,8 +955,7 @@ class SvaNfaBuilder final {
// than under-count. Plain boolean disjunction has one end per cycle and
// is handled by the OR-fold.
if (m_isCoverSeq && (lhs.termVertexp != entryVtxp || rhs.termVertexp != entryVtxp)) {
flp->v3warn(COVERIGN,
"Ignoring unsupported: cover sequence with a sequence operand of 'or'");
warnEndpointUnsupported(flp, "a sequence operand of 'or'");
return BuildResult::failWithError();
}
SvaStateVertex* const mergeVtxp = scopedCreateVertex();
@@ -1442,11 +1450,12 @@ class SvaNfaBuilder final {
public:
SvaNfaBuilder(SvaGraph& graph, AstNodeModule* modp, V3UniqueNames& propTempNames,
bool isCoverSeq = false)
bool isCoverSeq = false, bool isSeqEvent = false)
: m_graph{graph}
, m_modp{modp}
, m_propTempNames{propTempNames}
, m_isCoverSeq{isCoverSeq} {}
, m_isCoverSeq{isCoverSeq}
, m_isSeqEvent{isSeqEvent} {}
// Reset scope between antecedent and consequent: liveness must not leak.
void resetScope() {
@@ -2876,11 +2885,17 @@ class AssertNfaVisitor final : public VNVisitor {
bool senTreeOwned = false; // True if we created senTreep locally
AstPropSpec* const propSpecp = VN_CAST(assertp->propp(), PropSpec);
UASSERT_OBJ(propSpecp, assertp, "Concurrent assertion must have PropSpec");
AstCover* const coverp = VN_CAST(assertp, Cover);
const bool isCover = coverp != nullptr;
const bool isCoverSeq = coverp && coverp->isCoverSeq();
// A sequence event control is not an assertion directive; no default
// disable iff, no assertion control
const bool isSeqEvent = coverp && coverp->isSeqEvent();
// Inherit module defaults (IEEE 14.12, 16.15) when assertion has none.
if (!propSpecp->sensesp() && m_defaultClockingp) {
propSpecp->sensesp(m_defaultClockingp->sensesp()->cloneTree(true));
}
if (!propSpecp->disablep() && m_defaultDisablep) {
if (!propSpecp->disablep() && m_defaultDisablep && !isSeqEvent) {
propSpecp->disablep(m_defaultDisablep->condp()->cloneTreePure(true));
}
if (!senTreep && propSpecp->sensesp()) {
@@ -2892,12 +2907,9 @@ class AssertNfaVisitor final : public VNVisitor {
if (!senTreep) return;
FileLine* const flp = assertp->fileline();
const bool isCover = VN_IS(assertp, Cover);
AstCover* const coverp = VN_CAST(assertp, Cover);
const bool isCoverSeq = coverp && coverp->isCoverSeq();
SvaGraph graph;
SvaNfaBuilder builder{graph, m_modp, m_propTempNames, isCoverSeq};
SvaNfaBuilder builder{graph, m_modp, m_propTempNames, isCoverSeq, isSeqEvent};
const BuildResult result = buildAssertionGraph(builder, graph, seqBodyp, parts, flp);
if (result.valid()) wireMatchAndMidSources(graph, result, flp);
@@ -2939,13 +2951,16 @@ class AssertNfaVisitor final : public VNVisitor {
std::vector<AstNodeExpr*> perMidSrcs;
AstNodeExpr* const alwaysTriggerp
= assertOnCond(flp, assertp->userType(), assertp->directive());
= isSeqEvent ? new AstConst{flp, AstConst::BitTrue{}}
: assertOnCond(flp, assertp->userType(), assertp->directive());
AstNodeExpr* const outputExprp = m_loweringp->lower(
flp, graph, alwaysTriggerp, senTreep, result.finalCondp, isCover,
disableExprp ? disableExprp->cloneTreePure(false) : nullptr, negated,
needMatch ? &matchExprp : nullptr, disableCntVarp, snapshotVarp,
needPerSrcFail ? &requiredStepSrcs : nullptr, isCoverSeq ? &perMidSrcs : nullptr,
assertp->userType(), assertp->directive());
isSeqEvent ? VAssertType{VAssertType::INTERNAL} : assertp->userType(),
isSeqEvent ? VAssertDirectiveType{VAssertDirectiveType::INTERNAL}
: assertp->directive());
AstSenTree* const perSrcSenTreep
= (requiredStepSrcs.size() >= 2) ? senTreep->cloneTree(false) : nullptr;
+3 -1
View File
@@ -1466,7 +1466,9 @@ private:
iterateAndNextNull(nodep->sensesp());
if (m_senip && m_senip != nodep->sensesp())
nodep->v3warn(E_UNSUPPORTED, "Unsupported: Only one PSL clock allowed per assertion");
if (!nodep->disablep() && m_defaultDisablep) {
const AstCover* const coverp = VN_CAST(nodep->backp(), Cover);
const bool seqEvent = coverp && coverp->isSeqEvent();
if (!nodep->disablep() && m_defaultDisablep && !seqEvent) {
nodep->disablep(m_defaultDisablep->condp()->cloneTreePure(true));
}
m_disablep = nodep->disablep();
+4
View File
@@ -1612,6 +1612,8 @@ class AstCover final : public AstNodeCoverOrAssert {
// @astgen op3 := coverincsp: List[AstNode] // Coverage node
bool m_isCoverSeq = false; // 'cover sequence' (IEEE 1800-2023 16.14.3): fires per
// end-of-match, not per property success
bool m_isSeqEvent = false; // Synthesized for a sequence used as an event control
// (IEEE 1800-2023 9.4.2.4)
public:
ASTGEN_MEMBERS_AstCover;
AstCover(FileLine* fl, AstNode* propp, AstNode* stmtsp, VAssertType type,
@@ -1622,6 +1624,8 @@ public:
void dumpJson(std::ostream& str) const override;
bool isCoverSeq() const { return m_isCoverSeq; }
void isCoverSeq(bool flag) { m_isCoverSeq = flag; }
bool isSeqEvent() const { return m_isSeqEvent; }
void isSeqEvent(bool flag) { m_isSeqEvent = flag; }
};
class AstRestrict final : public AstNodeCoverOrAssert {
public:
+2
View File
@@ -2241,9 +2241,11 @@ void AstNodeCoverOrAssert::dumpJson(std::ostream& str) const {
void AstCover::dump(std::ostream& str) const {
this->AstNodeCoverOrAssert::dump(str);
if (isCoverSeq()) str << " [COVERSEQ]";
if (isSeqEvent()) str << " [SEQEVENT]";
}
void AstCover::dumpJson(std::ostream& str) const {
dumpJsonBoolFuncIf(str, isCoverSeq);
dumpJsonBoolFuncIf(str, isSeqEvent);
this->AstNodeCoverOrAssert::dumpJson(str);
}
void AstClocking::dump(std::ostream& str) const {
+1
View File
@@ -261,6 +261,7 @@ static void process() {
// Assertion insertion
// After we've added block coverage, but before other nasty transforms
V3AssertCommon::collectDefaultDisable(v3Global.rootp());
V3AssertCommon::lowerSequenceEvents(v3Global.rootp());
V3AssertNfa::assertNfaAll(v3Global.rootp());
// V3AssertProp removed: NFA subsumes multi-cycle property lowering.
// Unsupported constructs fall through to V3AssertPre.