diff --git a/src/V3Delayed.cpp b/src/V3Delayed.cpp index f2ed71ebc..80e99d109 100644 --- a/src/V3Delayed.cpp +++ b/src/V3Delayed.cpp @@ -152,6 +152,9 @@ class DelayedVisitor final : public VNVisitor { const AstActive* m_fistActivep = nullptr; bool m_whole = false; // Used on LHS of NBA directly via VarRef bool m_partial = false; // Used on LHS of NBA under a Sel + bool m_multiNbasSameProc = false; // Same process has >1 NBA to this variable + AstNodeProcedure* m_lastSuspNbaProcp = nullptr; // Last process with susp NBA to this var + bool m_hasBlockingWrite = false; // Also has blocking write (not just NBAs) 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 @@ -284,6 +287,17 @@ class DelayedVisitor final : public VNVisitor { AstAssignDly* m_nextDlyp = nullptr; // The nextp of the previous AstAssignDly AstVarScope* m_prevVscp = nullptr; // The target of the previous AstAssignDly + // STATE - for inline commit insertion in suspendable processes + std::set m_suspBlockingWriteVscps; // Arrays with blocking writes in susp + std::map> + m_suspCawaits; // CAwait nodes per suspendable process + struct CommitQueuePair final { + AstVarScope* queueVscp; // The commit queue variable + AstVarScope* targetVscp; // The target variable + }; + std::vector m_suspCommitQueues; // Commit queues for inline commits + std::set m_suspValueQueueProcs; // Processes with ValueQueuePartial NBAs + // STATE - Statistic tracking VDouble0 m_nSchemeShadowVar; // Number of variables using Scheme::ShadowVar VDouble0 m_nSchemeShadowVarMasked; // Number of variabels using Scheme::ShadowVarMasked @@ -417,8 +431,11 @@ class DelayedVisitor final : public VNVisitor { const AstNodeDType* const dtypep = vscp->dtypep()->skipRefp(); // Unpacked arrays if (const AstUnpackArrayDType* const uaDTypep = VN_CAST(dtypep, UnpackArrayDType)) { - // If whole array is target of NBA, use ShadowVar - if (vscpInfo.m_whole) return Scheme::ShadowVar; + // If whole array is target of NBA, use ShadowVar. In a suspendable or fork, + // fall through to the value-queue handling below, which preserves NBA execution + // order across suspend points (whole-array assigns are split into per-element + // enqueues in convertSchemeValueQueue). + if (vscpInfo.m_whole && !vscpInfo.m_inSuspOrFork) return Scheme::ShadowVar; // Basic underlying type of elements, if any. const AstBasicDType* const basicp = uaDTypep->basicp(); // If used in a loop, we must have a dynamic commit queue. (Also works in suspendables) @@ -433,8 +450,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 value queue for same-process multi-NBA + // variables to preserve execution order across suspend points. + // Variables with blocking writes fall back to FlagUnique to avoid + // circular dependencies in the ordering graph. + if (vscpInfo.m_inSuspOrFork) { + if (vscpInfo.m_multiNbasSameProc && basicp && basicp->isIntegralOrPacked() + && !vscpInfo.m_hasBlockingWrite) { + return Scheme::ValueQueuePartial; + } + return Scheme::FlagUnique; + } // 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 @@ -443,10 +469,17 @@ 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, use value queue for same-process multi-NBA + // packed variables ("last NBA wins" with correct execution order). + // Otherwise use the unique flag scheme. + if (vscpInfo.m_inSuspOrFork) { + if (vscpInfo.m_multiNbasSameProc && isIntegralOrPacked) { + return Scheme::ValueQueuePartial; + } + 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 @@ -855,9 +888,12 @@ class DelayedVisitor final : public VNVisitor { queueVscp->varp()->noReset(true); queueVscp->varp()->setIgnorePostWrite(); vscpInfo.valueQueueKit().vscp = queueVscp; + // Record for inline commits if in a suspendable process + if (vscpInfo.m_inSuspOrFork) m_suspCommitQueues.push_back({queueVscp, vscp}); // Create the AstActive for the Post logic AstActive* const activep - = new AstActive{flp, "nba-value-queue-whole", vscpInfo.senTreep()}; + = new AstActive{flp, N_Partial ? "nba-value-queue-partial" : "nba-value-queue-whole", + vscpInfo.senTreep()}; activep->senTreeStorep(vscpInfo.senTreep()); scopep->addBlocksp(activep); // Add 'Post' scheduled process for the commit @@ -871,6 +907,44 @@ class DelayedVisitor final : public VNVisitor { postp->addStmtsp(callp->makeStmt()); } + // Create a properly-sized 32-bit unsigned constant (post-WidthCommit, width==widthMin). + AstConst* makeU32Const(FileLine* flp, const AstNode* contextp, uint32_t value) { + AstConst* const cp = new AstConst{flp, AstConst::DTyped{}, + contextp->findBasicDType(VBasicDTypeKwd::UINT32)}; + cp->num().setLong(value); + return cp; + } + + // Split a whole-(sub-)array NBA ('arr <= rhs_arr') into per-element NBAs and convert each. + // The partial commit queue's enqueue() takes a scalar element, so we recurse through + // convertSchemeValueQueue - each per-element NBA then hits the normal ArraySel path. + void splitAndConvertWholeArrayNba(AstAssignDly* nodep, AstVarScope* vscp, + VarScopeInfo& vscpInfo, + const AstUnpackArrayDType* uaDTypep) { + FileLine* const flp = nodep->fileline(); + AstScope* const scopep = VN_AS(nodep->user2p(), Scope); + AstNodeExpr* const origLhsp = nodep->lhsp()->unlinkFrBack(); + AstNodeExpr* const origRhsp = nodep->rhsp()->unlinkFrBack(); + const int loIdx = uaDTypep->rangep()->loConst(); + const int hiIdx = loIdx + uaDTypep->elementsConst(); + std::vector perElementNbas; + perElementNbas.reserve(hiIdx - loIdx); + for (int i = loIdx; i < hiIdx; ++i) { + AstAssignDly* const elemNbap = new AstAssignDly{ + flp, new AstArraySel{flp, origLhsp->cloneTree(false), makeU32Const(flp, vscp, i)}, + new AstArraySel{flp, origRhsp->cloneTree(false), makeU32Const(flp, vscp, i)}}; + elemNbap->user2p(scopep); + nodep->addHereThisAsNext(elemNbap); + perElementNbas.push_back(elemNbap); + } + VL_DO_DANGLING(pushDeletep(origLhsp), origLhsp); + VL_DO_DANGLING(pushDeletep(origRhsp), origRhsp); + pushDeletep(nodep->unlinkFrBack()); + for (AstAssignDly* const elemNbap : perElementNbas) { + convertSchemeValueQueue(elemNbap, vscp, vscpInfo, /* partial: */ true); + } + } + void convertSchemeValueQueue(AstAssignDly* nodep, AstVarScope* vscp, VarScopeInfo& vscpInfo, bool partial) { UASSERT_OBJ(partial ? vscpInfo.m_scheme == Scheme::ValueQueuePartial @@ -879,6 +953,16 @@ class DelayedVisitor final : public VNVisitor { FileLine* const flp = vscp->fileline(); AstScope* const scopep = VN_AS(nodep->user2p(), Scope); + // A whole-(sub-)array NBA under the partial scheme needs splitting into per-element NBAs, + // as enqueue() takes a scalar element, not a whole VlUnpacked array. + if (partial) { + if (const AstUnpackArrayDType* const uaDTypep + = VN_CAST(nodep->lhsp()->dtypep()->skipRefp(), UnpackArrayDType)) { + splitAndConvertWholeArrayNba(nodep, vscp, vscpInfo, uaDTypep); + return; + } + } + // Base name suffix for signals constructed below const std::string baseName = uniqueTmpName(scopep, vscp, vscpInfo); @@ -916,39 +1000,9 @@ class DelayedVisitor final : public VNVisitor { 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}; @@ -957,17 +1011,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); @@ -986,6 +1039,51 @@ class DelayedVisitor final : public VNVisitor { pushDeletep(nodep->unlinkFrBack()); } + // Create mask and widened value for a partial bit-select update + 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 mask value + maskp = [&]() -> AstNodeExpr* { + // Constant mask we can compute here + 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; + } + + // A non-constant mask we must compute at run-time. + AstConst* const onesp = new AstConst{flp, AstConst::WidthedValue{}, width, 0}; + onesp->num().setAllBits1(); + return createWidened(flp, scopep, eDTypep, lsbExpr, width, "__VdlyMask" + baseName, + onesp, insertBefore); + }(); + + // 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(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; + } + } + + // A non-constant value we must adjust. + 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 @@ -1013,6 +1111,7 @@ class DelayedVisitor final : public VNVisitor { // Decide which scheme to use for each variable and do the 'prepare' step for (AstVarScope* const vscp : m_vscps) { VarScopeInfo& vscpInfo = m_vscpInfo(vscp); + vscpInfo.m_hasBlockingWrite = m_suspBlockingWriteVscps.count(vscp) > 0; vscpInfo.m_scheme = chooseScheme(vscp, vscpInfo); // Run 'prepare' step switch (vscpInfo.m_scheme) { @@ -1051,6 +1150,9 @@ class DelayedVisitor final : public VNVisitor { case Scheme::ValueQueuePartial: { ++m_nSchemeValueQueuesPartial; prepareSchemeValueQueue(vscp, vscpInfo); + if (vscpInfo.m_lastSuspNbaProcp) { + m_suspValueQueueProcs.insert(vscpInfo.m_lastSuspNbaProcp); + } break; } } @@ -1097,6 +1199,33 @@ class DelayedVisitor final : public VNVisitor { break; } } + + // Insert inline commit calls before each CAwait in processes that have + // ValueQueuePartial NBAs. Only target those processes, not other suspendable + // processes (e.g., timed continuous assigns) which would create ordering cycles. + if (!m_suspCommitQueues.empty()) { + for (AstNodeProcedure* const procp : m_suspValueQueueProcs) { + const auto it = m_suspCawaits.find(procp); + if (it == m_suspCawaits.end()) continue; + for (AstCAwait* const awaitp : it->second) { + // Find the enclosing statement + AstNodeStmt* stmtp = nullptr; + for (AstNode* np = awaitp; np; np = np->backp()) { + if ((stmtp = VN_CAST(np, NodeStmt))) break; + } + if (!stmtp) continue; + for (const auto& cq : m_suspCommitQueues) { + FileLine* const flp = stmtp->fileline(); + AstCMethodHard* const callp = new AstCMethodHard{ + flp, new AstVarRef{flp, cq.queueVscp, VAccess::READWRITE}, + VCMethod::SCHED_COMMIT}; + callp->dtypeSetVoid(); + callp->addPinsp(new AstVarRef{flp, cq.targetVscp, VAccess::WRITE}); + stmtp->addHereThisAsNext(callp->makeStmt()); + } + } + } + } } void visit(AstScope* nodep) override { VL_RESTORER(m_scopep); @@ -1182,6 +1311,7 @@ class DelayedVisitor final : public VNVisitor { } void visit(AstCAwait* nodep) override { if (nodep->sentreep()) m_timingDomains.insert(nodep->sentreep()); + if (m_inSuspendableOrFork) m_suspCawaits[m_procp].push_back(nodep); } void visit(AstFireEvent* nodep) override { UASSERT_OBJ(v3Global.hasEvents(), nodep, "Inconsistent"); @@ -1292,6 +1422,11 @@ class DelayedVisitor final : public VNVisitor { vscpInfo.m_partial |= VN_IS(nodep->lhsp(), Sel); vscpInfo.m_inLoop |= m_inLoop; vscpInfo.m_inSuspOrFork |= m_inSuspendableOrFork; + // Track same-process multi-NBA for ValueQueuePartial scheme selection + if (m_inSuspendableOrFork) { + if (vscpInfo.m_lastSuspNbaProcp == m_procp) vscpInfo.m_multiNbasSameProc = true; + vscpInfo.m_lastSuspNbaProcp = m_procp; + } // Sensitivity might be non-clocked, in a suspendable process, which are handled elsewhere if (m_activep->sentreep()->hasClocked()) { if (vscpInfo.m_fistActivep != m_activep) { @@ -1329,6 +1464,11 @@ class DelayedVisitor final : public VNVisitor { if (nodep == m_currNbaLhsRefp) return; // Only care about write refs if (!nodep->access().isWriteOrRW()) return; + // Track blocking writes to arrays in suspendable processes + if (m_inSuspendableOrFork + && VN_IS(nodep->varScopep()->dtypep()->skipRefp(), UnpackArrayDType)) { + m_suspBlockingWriteVscps.insert(nodep->varScopep()); + } // Record write reference recordWriteRef(nodep, false); } diff --git a/test_regress/t/t_timing_nba_3.py b/test_regress/t/t_timing_nba_3.py new file mode 100755 index 000000000..ecf737948 --- /dev/null +++ b/test_regress/t/t_timing_nba_3.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2024 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.compile(verilator_flags2=["--binary", "--assert", "-Wno-ZERODLY"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_timing_nba_3.v b/test_regress/t/t_timing_nba_3.v new file mode 100644 index 000000000..df9751222 --- /dev/null +++ b/test_regress/t/t_timing_nba_3.v @@ -0,0 +1,44 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Ethan Sifferman +// SPDX-License-Identifier: CC0-1.0 + +module t; + + int delay = 0; always #1 delay = int'($time) / 4; + task automatic do_delay; + if (delay > 0) #(delay); + endtask + + // `a` should match `b` + logic a = 0; + always begin + a <= 1; + do_delay(); + a <= 0; + + #1; + end + + logic b = 0; + always begin + b <= 1; + do_delay(); + b <= 0; + + #1; + b <= 1; // this line should do nothing + end + + always #1 assert (a == b); + + initial begin + $dumpfile("dump.vcd"); + $dumpvars; + #20; + $write("*-* All Finished *-*\n"); + $finish; + end + +endmodule diff --git a/test_regress/t/t_timing_nba_whole_partial.py b/test_regress/t/t_timing_nba_whole_partial.py new file mode 100755 index 000000000..ecf737948 --- /dev/null +++ b/test_regress/t/t_timing_nba_whole_partial.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Verilog Test driver/expect definition +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of either the GNU Lesser General Public License Version 3 +# or the Perl Artistic License Version 2.0. +# SPDX-FileCopyrightText: 2024 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('vlt') + +test.compile(verilator_flags2=["--binary", "--assert", "-Wno-ZERODLY"]) + +test.execute() + +test.passes() diff --git a/test_regress/t/t_timing_nba_whole_partial.v b/test_regress/t/t_timing_nba_whole_partial.v new file mode 100644 index 000000000..2d216d2de --- /dev/null +++ b/test_regress/t/t_timing_nba_whole_partial.v @@ -0,0 +1,190 @@ +// DESCRIPTION: Verilator: Verilog Test module +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 Ethan Sifferman +// SPDX-License-Identifier: CC0-1.0 + +// Test that mixing whole-variable and partial NBAs with suspend points +// correctly implements "last NBA wins" semantics for both scalars and arrays. + +module t; + + int delay = 0; always #1 delay = int'($time) / 4; + task automatic do_delay; + if (delay > 0) #(delay); + endtask + + // ---- Scalar tests ---- + + // Test S1: whole then partial with suspend between + logic [7:0] s1 = 0; + always begin + s1 <= 8'hff; + do_delay(); + s1[3:0] <= 4'h0; + #1; + assert (s1 == 8'hf0) else $fatal(0, "S1 whole->partial: expected f0, got %0h", s1); + end + + // Test S2: partial then whole with suspend between + logic [7:0] s2 = 0; + always begin + s2[3:0] <= 4'h0; + do_delay(); + s2 <= 8'hff; + #1; + 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 + #20; + $write("*-* All Finished *-*\n"); + $finish; + end + +endmodule