use ValueQueue for suspendable block
This commit is contained in:
parent
dd9ddc7231
commit
c8180754cc
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue