use ValueQueue for suspendable block

This commit is contained in:
Ethan Sifferman 2026-02-13 11:20:04 -08:00
parent dd9ddc7231
commit c8180754cc
5 changed files with 355 additions and 137 deletions

View File

@ -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<std::pair<AstVarScope*, AstVarScope*>> 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

View File

@ -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<AstNodeExpr*> 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<AstNodeExpr*, AstNodeExpr*>
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

View File

@ -301,6 +301,10 @@ class TimingKit final {
// Additional var sensitivities for V3Order
std::map<const AstVarScope*, std::set<AstSenTree*>> 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

View File

@ -53,44 +53,93 @@ TimingKit::remapDomains(const std::unordered_map<const AstSenTree*, AstSenTree*>
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;

View File

@ -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