From c8180754cc6ce4d0a3e91366b171d132010bb93a Mon Sep 17 00:00:00 2001 From: Ethan Sifferman Date: Fri, 13 Feb 2026 11:20:04 -0800 Subject: [PATCH] use ValueQueue for suspendable block --- src/V3AstNodeOther.h | 6 + src/V3Delayed.cpp | 191 +++++++++++--------- src/V3Sched.h | 4 + src/V3SchedTiming.cpp | 115 ++++++++---- test_regress/t/t_timing_nba_whole_partial.v | 176 ++++++++++++++++-- 5 files changed, 355 insertions(+), 137 deletions(-) diff --git a/src/V3AstNodeOther.h b/src/V3AstNodeOther.h index e6f9767ef..b47392c28 100644 --- a/src/V3AstNodeOther.h +++ b/src/V3AstNodeOther.h @@ -1248,6 +1248,9 @@ class AstNetlist final : public AstNode { // @astgen ptr := m_nbaEventTriggerp : Optional[AstVarScope] // NBA event trigger // @astgen ptr := m_topScopep : Optional[AstTopScope] // Singleton AstTopScope // @astgen ptr := m_stlFirstIterationp: Optional[AstVarScope] // Settle first iteration flag + // NBA commit queue pairs: (queue VarScope, target VarScope) + // Used to commit queues before resuming coroutines from delays + std::vector> m_nbaQueuePairs; VTimescale m_timeunit; // Global time unit VTimescale m_timeprecision; // Global time precision bool m_timescaleSpecified = false; // Input HDL specified timescale @@ -1294,6 +1297,9 @@ public: bool timescaleSpecified() const { return m_timescaleSpecified; } AstVarScope* stlFirstIterationp(); void clearStlFirstIterationp() { m_stlFirstIterationp = nullptr; } + // NBA queue pairs for pre-resume commits + auto& nbaQueuePairs() { return m_nbaQueuePairs; } + const auto& nbaQueuePairs() const { return m_nbaQueuePairs; } }; class AstPackageExport final : public AstNode { // A package export declaration diff --git a/src/V3Delayed.cpp b/src/V3Delayed.cpp index b4a47179b..89daa6fec 100644 --- a/src/V3Delayed.cpp +++ b/src/V3Delayed.cpp @@ -151,6 +151,7 @@ class DelayedVisitor final : public VNVisitor { // Active block 'm_firstNbaRefp' is under const AstActive* m_fistActivep = nullptr; bool m_partial = false; // Used on LHS of NBA under a Sel + bool m_whole = false; // Used on LHS of NBA as whole variable (no Sel) bool m_inLoop = false; // Used on LHS of NBA in a loop bool m_inSuspOrFork = false; // Used on LHS of NBA in suspendable process or fork Scheme m_scheme = Scheme::Undecided; // Conversion scheme to use for this variable @@ -430,8 +431,17 @@ class DelayedVisitor final : public VNVisitor { if (vscpInfo.m_partial) return Scheme::ValueQueuePartial; return Scheme::ValueQueueWhole; } - // In a suspendable of fork, we must use the unique flag scheme, TODO: why? - if (vscpInfo.m_inSuspOrFork) return Scheme::FlagUnique; + // In a suspendable or fork, use the value queue scheme for arrays + if (vscpInfo.m_inSuspOrFork) { + // Arrays with compound element types are not supported + const bool isSupportedBasicType = basicp + && (basicp->isIntegralOrPacked() + || basicp->isDouble() || basicp->isString()); + + if (!isSupportedBasicType) { return Scheme::UnsupportedCompoundArrayInLoop; } + + return vscpInfo.m_partial ? Scheme::ValueQueuePartial : Scheme::ValueQueueWhole; + } // Otherwise if an array of packed/basic elements, use the shared flag scheme if (basicp) return Scheme::FlagShared; // Finally fall back on the shadow variable scheme, e.g. for @@ -440,10 +450,18 @@ class DelayedVisitor final : public VNVisitor { return Scheme::ShadowVar; } - // In a suspendable of fork, we must use the unique flag scheme, TODO: why? - if (vscpInfo.m_inSuspOrFork) return Scheme::FlagUnique; - const bool isIntegralOrPacked = dtypep->isIntegralOrPacked(); + + // In a suspendable or fork, we need special handling + if (vscpInfo.m_inSuspOrFork) { + // If there are any partial updates to a packed type, use the value queue + // scheme which correctly implements "last NBA wins" semantics by applying + // updates in order at commit time. ShadowVarMasked doesn't work here + // because suspend points between NBAs cause intermediate commits. + if (vscpInfo.m_partial && isIntegralOrPacked) return Scheme::ValueQueuePartial; + // For whole-only updates or non-packed types, use the unique flag scheme + return Scheme::FlagUnique; + } // Check for mixed usage (this also warns if not OK) if (checkMixedUsage(vscp, isIntegralOrPacked)) { // If it's a variable updated by both blocking and non-blocking @@ -785,11 +803,13 @@ class DelayedVisitor final : public VNVisitor { } // Scheme::FlagUnique + // Used for whole-variable updates in suspendable processes where all NBAs to the + // same variable share a flag/value, so the last NBA wins. void prepareSchemeFlagUnique(AstVarScope* vscp, VarScopeInfo& vscpInfo) { UASSERT_OBJ(vscpInfo.m_scheme == Scheme::FlagUnique, vscp, "Inconsistent NBA scheme"); FileLine* const flp = vscp->fileline(); AstScope* const scopep = vscp->scopep(); - // Create the AstActive for the Pre/Post logic + // Create the AstActive for the Post logic AstActive* const activep = new AstActive{flp, "nba-flag-unique", vscpInfo.senTreep()}; activep->senTreeStorep(vscpInfo.senTreep()); scopep->addBlocksp(activep); @@ -798,18 +818,19 @@ class DelayedVisitor final : public VNVisitor { activep->addStmtsp(postp); vscpInfo.flagUniqueKit().postp = postp; - // Create a flag variable to track whether NBA occurred + // Create a shared flag variable to track whether any NBA occurred const std::string flagName = "__VdlySet__" + vscp->varp()->shortName(); AstVarScope* const commitFlagp = createTemp(flp, scopep, flagName, 1); commitFlagp->varp()->setIgnorePostWrite(); vscpInfo.flagUniqueKit().commitFlagp = commitFlagp; - // Create a value variable to track the final value after a NBA + // Create a shared value variable to hold the final value const std::string valName = "__VdlyVal__" + vscp->varp()->shortName(); AstVarScope* const commitValp = createTemp(flp, scopep, valName, vscp->dtypep()); vscpInfo.flagUniqueKit().commitValp = commitValp; // NBA 'Post' block: if (__VdlySet) { __VdlySet = 0; var = __VdlyVal; } - // This runs after all NBAs in the time step, applying the last captured value + // Multiple NBAs to the same variable overwrite the shared value, + // so the last NBA wins when the post block commits. AstIf* const ifp = new AstIf{flp, new AstVarRef{flp, commitFlagp, VAccess::READ}}; postp->addStmtsp(ifp); ifp->addThensp(new AstAssign{flp, new AstVarRef{flp, commitFlagp, VAccess::WRITE}, @@ -824,39 +845,14 @@ class DelayedVisitor final : public VNVisitor { AstVarScope* const commitFlagp = vscpInfo.flagUniqueKit().commitFlagp; AstVarScope* const commitValp = vscpInfo.flagUniqueKit().commitValp; - // Whole-variable update: use shared flag/value so last NBA wins - if (VN_IS(nodep->lhsp(), VarRef)) { - // Capture the RHS value to be applied at 'Post' time - nodep->addHereThisAsNext(new AstAssign{flp, - new AstVarRef{flp, commitValp, VAccess::WRITE}, - nodep->rhsp()->unlinkFrBack()}); - // Set the flag to indicate this NBA needs to be applied - nodep->addHereThisAsNext(new AstAssign{flp, - new AstVarRef{flp, commitFlagp, VAccess::WRITE}, - new AstConst{flp, AstConst::BitTrue{}}}); - } else { - // Partial update: different bits/indices may be written, so each needs its own flag - AstScope* const scopep = VN_AS(nodep->user2p(), Scope); - const std::string baseName = uniqueTmpName(scopep, vscp, vscpInfo); - AstNodeExpr* const capturedRhsp - = captureVal(scopep, nodep, nodep->rhsp()->unlinkFrBack(), "__VdlyVal" + baseName); - AstNodeExpr* const capturedLhsp - = captureLhs(scopep, nodep, nodep->lhsp()->unlinkFrBack(), baseName); - AstVarScope* const uniqueFlagVscp = createTemp(flp, scopep, "__VdlySet" + baseName, 1); - uniqueFlagVscp->varp()->setIgnorePostWrite(); - - // Set the flag to indicate this partial NBA needs to be applied - nodep->addHereThisAsNext( - new AstAssign{flp, new AstVarRef{flp, uniqueFlagVscp, VAccess::WRITE}, - new AstConst{flp, AstConst::BitTrue{}}}); - - // NBA 'Post' block: if (__VdlySet) { __VdlySet = 0; var[idx] = __VdlyVal; } - AstIf* const ifp = new AstIf{flp, new AstVarRef{flp, uniqueFlagVscp, VAccess::READ}}; - vscpInfo.flagUniqueKit().postp->addStmtsp(ifp); - ifp->addThensp(new AstAssign{flp, new AstVarRef{flp, uniqueFlagVscp, VAccess::WRITE}, - new AstConst{flp, AstConst::BitFalse{}}}); - ifp->addThensp(new AstAssign{flp, capturedLhsp, capturedRhsp}); - } + // Capture the RHS value to be applied at 'Post' time + // Each NBA overwrites the shared value, so the last one wins + nodep->addHereThisAsNext(new AstAssign{flp, new AstVarRef{flp, commitValp, VAccess::WRITE}, + nodep->rhsp()->unlinkFrBack()}); + // Set the flag to indicate this NBA needs to be applied + nodep->addHereThisAsNext(new AstAssign{flp, + new AstVarRef{flp, commitFlagp, VAccess::WRITE}, + new AstConst{flp, AstConst::BitTrue{}}}); // Delete original NBA pushDeletep(nodep->unlinkFrBack()); @@ -871,7 +867,6 @@ class DelayedVisitor final : public VNVisitor { FileLine* const flp = vscp->fileline(); AstScope* const scopep = vscp->scopep(); - // Create the commit queue variable auto* const cqDTypep = new AstNBACommitQueueDType{flp, vscp->dtypep()->skipRefp(), N_Partial}; v3Global.rootp()->typeTablep()->addTypesp(cqDTypep); @@ -880,15 +875,21 @@ class DelayedVisitor final : public VNVisitor { queueVscp->varp()->noReset(true); queueVscp->varp()->setIgnorePostWrite(); vscpInfo.valueQueueKit().vscp = queueVscp; - // Create the AstActive for the Post logic + + // Register queues in suspendable processes for pre-resume commits. + // This ensures coroutines see committed values when they resume from delays. + if (vscpInfo.m_inSuspOrFork) { + v3Global.rootp()->nbaQueuePairs().emplace_back(queueVscp, vscp); + } + AstActive* const activep - = new AstActive{flp, "nba-value-queue-whole", vscpInfo.senTreep()}; + = new AstActive{flp, "nba-value-queue", vscpInfo.senTreep()}; activep->senTreeStorep(vscpInfo.senTreep()); scopep->addBlocksp(activep); - // Add 'Post' scheduled process for the commit + AstAlwaysPost* const postp = new AstAlwaysPost{flp}; activep->addStmtsp(postp); - // Add the commit + AstCMethodHard* const callp = new AstCMethodHard{ flp, new AstVarRef{flp, queueVscp, VAccess::READWRITE}, VCMethod::SCHED_COMMIT}; callp->dtypeSetVoid(); @@ -935,45 +936,14 @@ class DelayedVisitor final : public VNVisitor { }(); if (const AstSel* const lSelp = VN_CAST(lhsNodep, Sel)) { - // This is a partial assignment. - // Need to create a mask and widen the value to element size. + // This is a partial assignment - need to create a mask and widen the value lhsNodep = lSelp->fromp(); AstNodeExpr* const sLsbp = lSelp->lsbp(); const int sWidth = lSelp->widthConst(); - // Create mask value - maskp = [&]() -> AstNodeExpr* { - // Constant mask we can compute here - if (const AstConst* const cLsbp = VN_CAST(sLsbp, Const)) { - AstConst* const cp = new AstConst{flp, AstConst::DTyped{}, eDTypep}; - cp->num().setMask(sWidth, cLsbp->toSInt()); - return cp; - } - - // A non-constant mask we must compute at run-time. - AstConst* const onesp = new AstConst{flp, AstConst::WidthedValue{}, sWidth, 0}; - onesp->num().setAllBits1(); - return createWidened(flp, scopep, eDTypep, sLsbp, sWidth, - "__VdlyMask" + baseName, onesp, nodep); - }(); - - // Adjust value to element size - valuep = [&]() -> AstNodeExpr* { - // Constant value with constant select we can compute here - if (AstConst* const cValuep = VN_CAST(valuep, Const)) { - if (const AstConst* const cLsbp = VN_CAST(sLsbp, Const)) { - AstConst* const cp = new AstConst{flp, AstConst::DTyped{}, eDTypep}; - cp->num().setAllBits0(); - cp->num().opSelInto(cValuep->num(), cLsbp->toSInt(), sWidth); - VL_DO_DANGLING(valuep->deleteTree(), valuep); - return cp; - } - } - - // A non-constant value we must adjust. - return createWidened(flp, scopep, eDTypep, sLsbp, sWidth, // - "__VdlyElem" + baseName, valuep, nodep); - }(); + // Use helper to create mask and widened value + std::tie(maskp, valuep) = createPartialUpdateMaskAndValue( + flp, scopep, eDTypep, sLsbp, sWidth, baseName, valuep, nodep); } else { // If this assignment is not partial, set mask to ones and we are done AstConst* const ones = new AstConst{flp, AstConst::DTyped{}, eDTypep}; @@ -982,17 +952,16 @@ class DelayedVisitor final : public VNVisitor { } } - // Extract array indices + // Extract array indices (if any — scalars have none) std::vector idxps; - { - UASSERT_OBJ(VN_IS(lhsNodep, ArraySel), lhsNodep, "Unexpected LHS form"); + if (VN_IS(lhsNodep, ArraySel)) { while (AstArraySel* const aSelp = VN_CAST(lhsNodep, ArraySel)) { idxps.emplace_back(aSelp->bitp()->unlinkFrBack()); lhsNodep = aSelp->fromp(); } - UASSERT_OBJ(VN_IS(lhsNodep, VarRef), lhsNodep, "Unexpected LHS form"); std::reverse(idxps.begin(), idxps.end()); } + UASSERT_OBJ(VN_IS(lhsNodep, VarRef), lhsNodep, "Unexpected LHS form"); // Done with the LHS at this point VL_DO_DANGLING(pushDeletep(capturedLhsp), capturedLhsp); @@ -1011,6 +980,52 @@ class DelayedVisitor final : public VNVisitor { pushDeletep(nodep->unlinkFrBack()); } + // Helper function to create mask and widened value for partial bit-select updates + // Returns pair of (mask, widened_value) + // If the selection is constant, optimizes by computing at compile time + std::pair + createPartialUpdateMaskAndValue(FileLine* flp, AstScope* scopep, AstNodeDType* eDTypep, + AstNodeExpr* lsbExpr, int width, const std::string& baseName, + AstNodeExpr* value, AstNode* insertBefore) { + + AstNodeExpr* maskp = nullptr; + AstNodeExpr* valuep = value; + + // Create the mask indicating which bits will be updated + maskp = [&]() -> AstNodeExpr* { + // If LSB is constant, compute mask at compile time + if (const AstConst* const cLsbp = VN_CAST(lsbExpr, Const)) { + AstConst* const cp = new AstConst{flp, AstConst::DTyped{}, eDTypep}; + cp->num().setMask(width, cLsbp->toSInt()); + return cp; + } + // LSB is dynamic, must compute mask at runtime + AstConst* const onesp = new AstConst{flp, AstConst::WidthedValue{}, width, 0}; + onesp->num().setAllBits1(); + return createWidened(flp, scopep, eDTypep, lsbExpr, width, "__VdlyMask" + baseName, + onesp, insertBefore); + }(); + + // Widen the value to element size (zero-extended with selected bits in position) + valuep = [&]() -> AstNodeExpr* { + // If both value and LSB are constant, compute at compile time + if (AstConst* const cValuep = VN_CAST(valuep, Const)) { + if (const AstConst* const cLsbp = VN_CAST(lsbExpr, Const)) { + AstConst* const cp = new AstConst{flp, AstConst::DTyped{}, eDTypep}; + cp->num().setAllBits0(); + cp->num().opSelInto(cValuep->num(), cLsbp->toSInt(), width); + VL_DO_DANGLING(valuep->deleteTree(), valuep); + return cp; + } + } + // Dynamic value or LSB, must widen at runtime + return createWidened(flp, scopep, eDTypep, lsbExpr, width, "__VdlyElem" + baseName, + valuep, insertBefore); + }(); + + return {maskp, valuep}; + } + // Record where a variable is assigned void recordWriteRef(AstVarRef* nodep, bool nonBlocking) { // Ignore references in certain contexts @@ -1285,7 +1300,9 @@ class DelayedVisitor final : public VNVisitor { m_vscps.emplace_back(vscp); } // Note usage context - vscpInfo.m_partial |= VN_IS(nodep->lhsp(), Sel); + const bool isPartial = VN_IS(nodep->lhsp(), Sel); + vscpInfo.m_partial |= isPartial; + vscpInfo.m_whole |= !isPartial; vscpInfo.m_inLoop |= m_inLoop; vscpInfo.m_inSuspOrFork |= m_inSuspendableOrFork; // Sensitivity might be non-clocked, in a suspendable process, which are handled elsewhere diff --git a/src/V3Sched.h b/src/V3Sched.h index 90e25ab59..72612e716 100644 --- a/src/V3Sched.h +++ b/src/V3Sched.h @@ -301,6 +301,10 @@ class TimingKit final { // Additional var sensitivities for V3Order std::map> m_externalDomains; + // Helper functions for createResume + void addNbaCommitsBeforeResume(AstIf* dlyShedIfp, AstNetlist* netlistp) VL_MT_DISABLED; + AstIf* processTimingActives() VL_MT_DISABLED; + public: LogicByScope m_lbs; // Actives that resume timing schedulers AstNodeStmt* m_postUpdates = nullptr; // Post updates for the trigger eval function diff --git a/src/V3SchedTiming.cpp b/src/V3SchedTiming.cpp index 7b32a0713..67b3645a3 100644 --- a/src/V3SchedTiming.cpp +++ b/src/V3SchedTiming.cpp @@ -53,44 +53,93 @@ TimingKit::remapDomains(const std::unordered_map return remappedDomainMap; } +//============================================================================ +// Helper: Add NBA queue commits before delay scheduler resume + +void TimingKit::addNbaCommitsBeforeResume(AstIf* dlyShedIfp, AstNetlist* netlistp) { + const auto& pairs = netlistp->nbaQueuePairs(); + if (pairs.empty()) return; + + // Unlink the existing statements (the resume call) + AstNode* const resumeStmtsp = dlyShedIfp->thensp()->unlinkFrBackWithNext(); + + // Add NBA scalar queue commits BEFORE delay scheduler resume. + // This ensures coroutines see committed values when they resume from delays. + for (const auto& pair : pairs) { + AstVarScope* const queueVscp = pair.first; + AstVarScope* const targetVscp = pair.second; + FileLine* const flp = queueVscp->fileline(); + AstCMethodHard* const commitp = new AstCMethodHard{ + flp, new AstVarRef{flp, queueVscp, VAccess::READWRITE}, VCMethod::SCHED_COMMIT}; + commitp->dtypeSetVoid(); + commitp->addPinsp(new AstVarRef{flp, targetVscp, VAccess::WRITE}); + dlyShedIfp->addThensp(commitp->makeStmt()); + } + + // Re-add the resume call after the commits + dlyShedIfp->addThensp(resumeStmtsp); +} + +//============================================================================ +// Helper: Process timing actives and return delay scheduler if found + +AstIf* TimingKit::processTimingActives() { + AstIf* dlyShedIfp = nullptr; + for (auto& p : m_lbs) { + AstActive* const activep = p.second; + // Hack to ensure that #0 delays will be executed after any other `act` events. + // Just handle delayed coroutines last. + AstVarRef* const schedrefp = VN_AS( + VN_AS(VN_AS(activep->stmtsp(), StmtExpr)->exprp(), CMethodHard)->fromp(), VarRef); + + AstIf* const ifp = V3Sched::util::createIfFromSenTree(activep->sentreep()); + ifp->addThensp(activep->stmtsp()->unlinkFrBackWithNext()); + + if (schedrefp->varScopep()->dtypep()->basicp()->isDelayScheduler()) { + dlyShedIfp = ifp; + } else { + m_resumeFuncp->addStmtsp(ifp); + } + } + return dlyShedIfp; +} + //============================================================================ // Creates a timing resume call (if needed, else returns null) AstCCall* TimingKit::createResume(AstNetlist* const netlistp) { - if (!m_resumeFuncp) { - if (m_lbs.empty()) return nullptr; - // Create global resume function - AstScope* const scopeTopp = netlistp->topScopep()->scopep(); - m_resumeFuncp = new AstCFunc{netlistp->fileline(), "_timing_resume", scopeTopp, ""}; - m_resumeFuncp->dontCombine(true); - m_resumeFuncp->isLoose(true); - m_resumeFuncp->isConst(false); - m_resumeFuncp->declPrivate(true); - scopeTopp->addBlocksp(m_resumeFuncp); - - // Put all the timing actives in the resume function - AstIf* dlyShedIfp = nullptr; - for (auto& p : m_lbs) { - AstActive* const activep = p.second; - // Hack to ensure that #0 delays will be executed after any other `act` events. - // Just handle delayed coroutines last. - AstVarRef* const schedrefp = VN_AS( - VN_AS(VN_AS(activep->stmtsp(), StmtExpr)->exprp(), CMethodHard)->fromp(), VarRef); - - AstIf* const ifp = V3Sched::util::createIfFromSenTree(activep->sentreep()); - ifp->addThensp(activep->stmtsp()->unlinkFrBackWithNext()); - - if (schedrefp->varScopep()->dtypep()->basicp()->isDelayScheduler()) { - dlyShedIfp = ifp; - } else { - m_resumeFuncp->addStmtsp(ifp); - } - } - if (dlyShedIfp) m_resumeFuncp->addStmtsp(dlyShedIfp); - - // These are now spent, oispose of now empty AstActive instances - m_lbs.deleteActives(); + // Return existing call if already created + if (m_resumeFuncp) { + AstCCall* const callp = new AstCCall{m_resumeFuncp->fileline(), m_resumeFuncp}; + callp->dtypeSetVoid(); + return callp; } + + // No timing events to resume + if (m_lbs.empty()) return nullptr; + + // Create global resume function + AstScope* const scopeTopp = netlistp->topScopep()->scopep(); + m_resumeFuncp = new AstCFunc{netlistp->fileline(), "_timing_resume", scopeTopp, ""}; + m_resumeFuncp->dontCombine(true); + m_resumeFuncp->isLoose(true); + m_resumeFuncp->isConst(false); + m_resumeFuncp->declPrivate(true); + scopeTopp->addBlocksp(m_resumeFuncp); + + // Process all timing actives and get delay scheduler if present + AstIf* const dlyShedIfp = processTimingActives(); + + // Add delay scheduler with NBA commits if needed + if (dlyShedIfp) { + addNbaCommitsBeforeResume(dlyShedIfp, netlistp); + m_resumeFuncp->addStmtsp(dlyShedIfp); + } + + // Dispose of now-empty AstActive instances + m_lbs.deleteActives(); + + // Create and return the call AstCCall* const callp = new AstCCall{m_resumeFuncp->fileline(), m_resumeFuncp}; callp->dtypeSetVoid(); return callp; diff --git a/test_regress/t/t_timing_nba_whole_partial.v b/test_regress/t/t_timing_nba_whole_partial.v index 4257f5807..2d216d2de 100644 --- a/test_regress/t/t_timing_nba_whole_partial.v +++ b/test_regress/t/t_timing_nba_whole_partial.v @@ -4,11 +4,8 @@ // SPDX-FileCopyrightText: 2026 Ethan Sifferman // SPDX-License-Identifier: CC0-1.0 -// Test that mixing whole-variable and partial NBAs with suspend points between -// them produces E_UNSUPPORTED error (not yet correctly supported) -// -// The "last NBA wins" semantics are not correctly implemented when there are -// suspend points between NBAs to the same variable with mixed whole/partial updates. +// Test that mixing whole-variable and partial NBAs with suspend points +// correctly implements "last NBA wins" semantics for both scalars and arrays. module t; @@ -17,26 +14,171 @@ module t; if (delay > 0) #(delay); endtask - // Test: whole then partial with suspend between - logic [7:0] a = 0; + // ---- Scalar tests ---- + + // Test S1: whole then partial with suspend between + logic [7:0] s1 = 0; always begin - a <= 8'hff; + s1 <= 8'hff; do_delay(); - a[3:0] <= 4'h0; // Last NBA should win + s1[3:0] <= 4'h0; #1; - // Expected: a = 8'hf0, Receives a = 8'h00 - assert (a == 8'hf0) else $fatal(0, "received %0h", a); + assert (s1 == 8'hf0) else $fatal(0, "S1 whole->partial: expected f0, got %0h", s1); end - // Test: partial then whole with suspend between - logic [7:0] b = 0; + // Test S2: partial then whole with suspend between + logic [7:0] s2 = 0; always begin - b[3:0] <= 4'h0; + s2[3:0] <= 4'h0; do_delay(); - b <= 8'hff; // Last NBA should win + s2 <= 8'hff; #1; - // Expected: b = 8'hff, Receives b = 8'h00 - assert (b == 8'hff) else $fatal(0, "received %0h", b); + assert (s2 == 8'hff) else $fatal(0, "S2 partial->whole: expected ff, got %0h", s2); + end + + // Test S3: two partials to non-overlapping ranges + logic [7:0] s3 = 0; + always begin + s3[7:4] <= 4'ha; + do_delay(); + s3[3:0] <= 4'h5; + #1; + assert (s3 == 8'ha5) else $fatal(0, "S3 partial+partial: expected a5, got %0h", s3); + end + + // Test S4: two partials to same range (last wins) + logic [7:0] s4 = 0; + always begin + s4[3:0] <= 4'ha; + do_delay(); + s4[3:0] <= 4'h5; + #1; + assert (s4 == 8'h05) else $fatal(0, "S4 partial overwrite: expected 05, got %0h", s4); + end + + // Test S5: whole, partial, whole (last wins) + logic [7:0] s5 = 0; + always begin + s5 <= 8'haa; + do_delay(); + s5[3:0] <= 4'hb; + do_delay(); + s5 <= 8'hcc; + #1; + assert (s5 == 8'hcc) else $fatal(0, "S5 whole-partial-whole: expected cc, got %0h", s5); + end + + // ---- Unpacked array element tests ---- + + // Test A1: whole element then partial element with suspend between + logic [7:0] a1 [0:3]; + initial for (int i = 0; i < 4; i++) a1[i] = 0; + always begin + a1[1] <= 8'hff; + do_delay(); + a1[1][3:0] <= 4'h0; + #1; + assert (a1[1] == 8'hf0) else $fatal(0, "A1 elem->partial: expected f0, got %0h", a1[1]); + end + + // Test A2: partial element then whole element with suspend between + logic [7:0] a2 [0:3]; + initial for (int i = 0; i < 4; i++) a2[i] = 0; + always begin + a2[2][7:4] <= 4'hf; + do_delay(); + a2[2] <= 8'h00; + #1; + assert (a2[2] == 8'h00) else $fatal(0, "A2 partial->elem: expected 00, got %0h", a2[2]); + end + + // Test A3: writes to different indices (both should apply) + logic [7:0] a3 [0:3]; + initial for (int i = 0; i < 4; i++) a3[i] = 0; + always begin + a3[0] <= 8'haa; + do_delay(); + a3[1] <= 8'hbb; + #1; + assert (a3[0] == 8'haa) else $fatal(0, "A3 idx0: expected aa, got %0h", a3[0]); + assert (a3[1] == 8'hbb) else $fatal(0, "A3 idx1: expected bb, got %0h", a3[1]); + end + + // Test A4: same index overwrite (last wins) + logic [7:0] a4 [0:3]; + initial for (int i = 0; i < 4; i++) a4[i] = 0; + always begin + a4[0] <= 8'haa; + do_delay(); + a4[0] <= 8'hbb; + #1; + assert (a4[0] == 8'hbb) else $fatal(0, "A4 idx overwrite: expected bb, got %0h", a4[0]); + end + + // ---- Full array assignment tests ---- + + // Test F1: full array then element (element write wins for that index) + logic [7:0] f1 [0:3]; + initial for (int i = 0; i < 4; i++) f1[i] = 0; + logic [7:0] f1_src [0:3]; + initial begin f1_src[0]=8'h11; f1_src[1]=8'h22; f1_src[2]=8'h33; f1_src[3]=8'h44; end + always begin + f1 <= f1_src; + do_delay(); + f1[1] <= 8'hff; + #1; + assert (f1[0] == 8'h11) else $fatal(0, "F1 [0]: expected 11, got %0h", f1[0]); + assert (f1[1] == 8'hff) else $fatal(0, "F1 [1]: expected ff, got %0h", f1[1]); + assert (f1[2] == 8'h33) else $fatal(0, "F1 [2]: expected 33, got %0h", f1[2]); + assert (f1[3] == 8'h44) else $fatal(0, "F1 [3]: expected 44, got %0h", f1[3]); + end + + // Test F2: element then full array (full array wins) + logic [7:0] f2 [0:3]; + initial for (int i = 0; i < 4; i++) f2[i] = 0; + logic [7:0] f2_src [0:3]; + initial begin f2_src[0]=8'h11; f2_src[1]=8'h22; f2_src[2]=8'h33; f2_src[3]=8'h44; end + always begin + f2[1] <= 8'hff; + do_delay(); + f2 <= f2_src; + #1; + assert (f2[0] == 8'h11) else $fatal(0, "F2 [0]: expected 11, got %0h", f2[0]); + assert (f2[1] == 8'h22) else $fatal(0, "F2 [1]: expected 22, got %0h", f2[1]); + assert (f2[2] == 8'h33) else $fatal(0, "F2 [2]: expected 33, got %0h", f2[2]); + assert (f2[3] == 8'h44) else $fatal(0, "F2 [3]: expected 44, got %0h", f2[3]); + end + + // Test F3: full array then partial element (partial wins for that element's bits) + logic [7:0] f3 [0:3]; + initial for (int i = 0; i < 4; i++) f3[i] = 0; + logic [7:0] f3_src [0:3]; + initial begin f3_src[0]=8'haa; f3_src[1]=8'hbb; f3_src[2]=8'hcc; f3_src[3]=8'hdd; end + always begin + f3 <= f3_src; + do_delay(); + f3[2][3:0] <= 4'h0; + #1; + assert (f3[0] == 8'haa) else $fatal(0, "F3 [0]: expected aa, got %0h", f3[0]); + assert (f3[1] == 8'hbb) else $fatal(0, "F3 [1]: expected bb, got %0h", f3[1]); + assert (f3[2] == 8'hc0) else $fatal(0, "F3 [2]: expected c0, got %0h", f3[2]); + assert (f3[3] == 8'hdd) else $fatal(0, "F3 [3]: expected dd, got %0h", f3[3]); + end + + // Test F4: partial element then full array (full array wins) + logic [7:0] f4 [0:3]; + initial for (int i = 0; i < 4; i++) f4[i] = 0; + logic [7:0] f4_src [0:3]; + initial begin f4_src[0]=8'haa; f4_src[1]=8'hbb; f4_src[2]=8'hcc; f4_src[3]=8'hdd; end + always begin + f4[2][3:0] <= 4'hf; + do_delay(); + f4 <= f4_src; + #1; + assert (f4[0] == 8'haa) else $fatal(0, "F4 [0]: expected aa, got %0h", f4[0]); + assert (f4[1] == 8'hbb) else $fatal(0, "F4 [1]: expected bb, got %0h", f4[1]); + assert (f4[2] == 8'hcc) else $fatal(0, "F4 [2]: expected cc, got %0h", f4[2]); + assert (f4[3] == 8'hdd) else $fatal(0, "F4 [3]: expected dd, got %0h", f4[3]); end initial begin