diff --git a/src/V3AstNodeOther.h b/src/V3AstNodeOther.h index d2bbf21e4..eb93407bd 100644 --- a/src/V3AstNodeOther.h +++ b/src/V3AstNodeOther.h @@ -1889,6 +1889,26 @@ public: bool isPure() override { return false; } bool isOutputter() override { return true; } }; +class AstTaskLocalVar final : public AstNode { + // A task activation-local variable before loop unrolling is complete. The local variable is + // deliberately unscoped so cloning this node cannot invalidate an external AstVarScope. The + // temporary scoped variable carries references until V3Task materializes the local variable. + // @astgen op1 := varp : AstVar + // @astgen ptr := m_templateVscp : AstVarScope +public: + AstTaskLocalVar(FileLine* fl, AstVar* varp, AstVarScope* templateVscp) + : ASTGEN_SUPER_TaskLocalVar(fl) + , m_templateVscp{templateVscp} { + if (varp) this->varp(varp); + } + ASTGEN_MEMBERS_AstTaskLocalVar; + AstVarScope* templateVscp() const { return m_templateVscp; } + void dump(std::ostream& str) const override; + void dumpJson(std::ostream& str) const override; + bool sameNode(const AstNode* samep) const override { + return templateVscp() == VN_DBG_AS(samep, TaskLocalVar)->templateVscp(); + } +}; class AstText final : public AstNode { // Represents a piece of text to be emitted into the output // diff --git a/src/V3AstNodes.cpp b/src/V3AstNodes.cpp index 9b94d4e89..dfdbbd819 100644 --- a/src/V3AstNodes.cpp +++ b/src/V3AstNodes.cpp @@ -3077,6 +3077,16 @@ void AstSystemCSection::dumpJson(std::ostream& str) const { dumpJsonStr(str, "sectionType", sectionType().ascii()); dumpJsonGen(str); } +void AstTaskLocalVar::dump(std::ostream& str) const { + this->AstNode::dump(str); + str << " -> "; + if (templateVscp()) { + templateVscp()->dump(str); + } else { + str << "%E:UNLINKED"; + } +} +void AstTaskLocalVar::dumpJson(std::ostream& str) const { dumpJsonGen(str); } void AstTypeTable::dump(std::ostream& str) const { this->AstNode::dump(str); for (int i = 0; i < static_cast(VBasicDTypeKwd::_ENUM_MAX); ++i) { diff --git a/src/V3Sched.cpp b/src/V3Sched.cpp index 6078fbbf0..4de568271 100644 --- a/src/V3Sched.cpp +++ b/src/V3Sched.cpp @@ -50,6 +50,14 @@ VL_DEFINE_DEBUG_FUNCTIONS; namespace V3Sched { +LogicByScope LogicByScope::clone() const { + LogicByScope result; + for (const auto& pair : *this) { + result.emplace_back(pair.first, VN_AS(util::cloneLogic(pair.second), Active)); + } + return result; +} + namespace { //============================================================================ @@ -928,6 +936,28 @@ cloneMapWithNewTriggerReferences(const std::unordered_map{pairs.begin(), pairs.end()}; } +void removeVarScopesForDeletedLogic(AstNetlist* netlistp) { + // Scheduler ordering deletes logic replicas after moving their statements. Remove only + // unreferenced scopes whose cloned local declaration was deleted with such a replica. + std::unordered_set liveVarps; + std::unordered_set referencedVscps; + std::vector staleVscps; + + netlistp->foreach([&](const AstVar* varp) { liveVarps.emplace(varp); }); + netlistp->foreach([&](const AstNodeVarRef* refp) { + if (refp->varScopep()) referencedVscps.emplace(refp->varScopep()); + }); + netlistp->foreach([&](AstVarScope* vscp) { + if (!liveVarps.count(vscp->varp())) staleVscps.push_back(vscp); + }); + + for (AstVarScope* const vscp : staleVscps) { + UASSERT_OBJ(!referencedVscps.count(vscp), vscp, + "Variable scope still referenced after its declaration was deleted"); + VL_DO_DANGLING(vscp->unlinkFrBack()->deleteTree(), vscp); + } +} + //============================================================================ // Top level entry-point to scheduling @@ -1204,6 +1234,7 @@ void schedule(AstNetlist* netlistp) { // Step 16: Clean up netlistp->clearStlFirstIterationp(); + removeVarScopesForDeletedLogic(netlistp); // Haven't split static initializer yet util::splitCheck(staticp); diff --git a/src/V3Sched.h b/src/V3Sched.h index 26697813a..cdad75d40 100644 --- a/src/V3Sched.h +++ b/src/V3Sched.h @@ -37,6 +37,8 @@ namespace V3Sched { namespace util { using VarScopeSet = std::unordered_set; +AstNode* cloneLogic(AstNode* logicp); + inline bool isVlForceVec(const AstVarScope* vscp) { const AstCDType* const dtypep = VN_CAST(vscp->dtypep()->skipRefp(), CDType); return dtypep && dtypep->name() == "VlForceVec"; @@ -92,13 +94,7 @@ struct LogicByScope final : public std::vector> }; // Create copy, with the AstActives cloned - LogicByScope clone() const { - LogicByScope result; - for (const auto& pair : *this) { - result.emplace_back(pair.first, pair.second->cloneTree(false)); - } - return result; - } + LogicByScope clone() const; // Delete actives (they should all be empty) void deleteActives() { diff --git a/src/V3SchedReplicate.cpp b/src/V3SchedReplicate.cpp index 3aa404018..875efc593 100644 --- a/src/V3SchedReplicate.cpp +++ b/src/V3SchedReplicate.cpp @@ -274,7 +274,7 @@ LogicReplicas replicate(Graph* graphp) { for (V3GraphVertex& vtx : graphp->vertices()) { if (SchedReplicateLogicVertex* const lvtxp = vtx.cast()) { const auto replicateTo = [&](LogicByScope& lbs) { - lbs.add(lvtxp->scopep(), lvtxp->senTreep(), lvtxp->logicp()->cloneTree(false)); + lbs.add(lvtxp->scopep(), lvtxp->senTreep(), util::cloneLogic(lvtxp->logicp())); }; const uint8_t targetRegions = lvtxp->drivingRegions() & ~lvtxp->assignedRegion(); UASSERT(!lvtxp->senTreep()->hasClocked() || targetRegions == 0, diff --git a/src/V3SchedUtil.cpp b/src/V3SchedUtil.cpp index 0f3fa30c2..4a32ba5e2 100644 --- a/src/V3SchedUtil.cpp +++ b/src/V3SchedUtil.cpp @@ -33,6 +33,32 @@ VL_DEFINE_DEBUG_FUNCTIONS; namespace V3Sched { namespace util { +AstNode* cloneLogic(AstNode* logicp) { + AstNode* const clonep = logicp->cloneTree(false); + std::map clonedVscps; + clonep->foreach([&](AstNodeVarRef* refp) { + AstVarScope* const oldVscp = refp->varScopep(); + // cloneTree relinks an in-tree local AstVar, but its out-of-tree AstVarScope still points + // at the original declaration. Give each cloned declaration an independent scope. + if (!oldVscp || oldVscp->varp() == refp->varp()) return; + + const auto pair = clonedVscps.emplace(oldVscp, nullptr); + if (pair.second) { + AstVarScope* const newVscp + = new AstVarScope{refp->fileline(), oldVscp->scopep(), refp->varp()}; + newVscp->trace(oldVscp->isTrace()); + newVscp->optimizeLifePost(oldVscp->optimizeLifePost()); + oldVscp->scopep()->addVarsp(newVscp); + pair.first->second = newVscp; + } else { + UASSERT_OBJ(pair.first->second->varp() == refp->varp(), refp, + "Cloned variable scope maps to multiple variables"); + } + refp->varScopep(pair.first->second); + }); + return clonep; +} + AstCFunc* makeSubFunction(AstNetlist* netlistp, const string& name, bool slow) { AstScope* const scopeTopp = netlistp->topScopep()->scopep(); AstCFunc* const funcp = new AstCFunc{netlistp->fileline(), name, scopeTopp, ""}; diff --git a/src/V3Split.cpp b/src/V3Split.cpp index 404383d51..80321e7a9 100644 --- a/src/V3Split.cpp +++ b/src/V3Split.cpp @@ -522,9 +522,8 @@ public: m_addAfter[color] = placeholderp; m_newBlocksp->push_back(alwaysp); } - // Scan the body of the always. We'll handle if/else - // specially, everything else is a leaf node that we can - // just clone into one of the split always blocks. + // Scan the body of the always. We'll handle if/else specially; every other leaf belongs to + // one color and can move into the corresponding split always block. iterateAndNextNull(m_origAlwaysp->stmtsp()); } @@ -547,12 +546,13 @@ protected: // Each leaf must have a user3p UASSERT_OBJ(nodep->user3p(), nodep, "null user3p in V3Split leaf"); - // Clone the leaf into its new always block + // A leaf belongs to exactly one color, so move it into its new always block. Moving also + // preserves lexical declarations whose AstVarScopes are owned outside the leaf subtree. const SplitLogicVertex* const vxp = reinterpret_cast(nodep->user3p()); const uint32_t color = vxp->color(); - AstNode* const clonedp = nodep->cloneTree(false); - m_addAfter[color]->addNextHere(clonedp); - m_addAfter[color] = clonedp; + nodep->unlinkFrBack(); + m_addAfter[color]->addNextHere(nodep); + m_addAfter[color] = nodep; } void visit(AstNodeIf* nodep) override { diff --git a/src/V3Task.cpp b/src/V3Task.cpp index 28768e305..2bf469b11 100644 --- a/src/V3Task.cpp +++ b/src/V3Task.cpp @@ -34,6 +34,7 @@ #include "V3Stats.h" #include "V3UniqueNames.h" +#include #include VL_DEFINE_DEBUG_FUNCTIONS; @@ -483,6 +484,45 @@ class TaskVisitor final : public VNVisitor { return newvscp; } } + AstVarScope* createTaskLocalVarScope(AstVar* invarp, const string& name, + AstTaskLocalVar*& localDeclpr) { + AstVarScope* const templateVscp = createVarScope(invarp, name); + AstVar* const localVarp + = new AstVar{invarp->fileline(), VVarType::BLOCKTEMP, name, invarp}; + localVarp->funcLocal(true); + localVarp->propagateAttrFrom(invarp); + localVarp->isInternal(true); + localDeclpr = new AstTaskLocalVar{localVarp->fileline(), localVarp, templateVscp}; + return templateVscp; + } + AstVarScope* createPortVarScope(AstVar* invarp, const string& name, bool activationLocalPorts, + AstTaskLocalVar*& localDeclpr) { + localDeclpr = nullptr; + if (activationLocalPorts) return createTaskLocalVarScope(invarp, name, localDeclpr); + return createVarScope(invarp, name); + } + + void addToFront(AstNode* const beginp, AstNode* const newp) const { + if (!newp) return; + if (AstNode* const afterp = beginp->nextp()) { + afterp->unlinkFrBackWithNext(); + AstNode::addNext(newp, afterp); + } + beginp->addNext(newp); + } + + static bool isTaskActivationLocal(AstVar* const varp, const bool activationLocalPorts) { + return activationLocalPorts && !varp->direction().isAny() && varp->lifetime().isAutomatic() + && (varp->isFuncLocal() + || (varp->varType() == VVarType::BLOCKTEMP + && VN_IS(varp->dtypep()->skipRefp(), ClassRefDType))); + } + static bool bodyNeedsActivationLocals(const AstNode* const bodyp) { + return bodyp && bodyp->existsAndNext([](const AstNode* const nodep) { + // A join_none child can outlive the task without itself containing a timing control. + return nodep->isTimingControl() || VN_IS(nodep, Fork); + }); + } // Replace varrefs with new var pointer void relink(AstNode* nodep) { @@ -552,8 +592,9 @@ class TaskVisitor final : public VNVisitor { } } - void connectPort(AstVar* portp, AstArg* argp, const string& namePrefix, AstNode* beginp, - bool inlineTask) { + bool connectPort(AstVar* portp, AstArg* argp, const string& namePrefix, AstNode* beginp, + bool inlineTask, bool activationLocalPorts) { + bool needsCLocalScope = false; AstNodeExpr* pinp = argp->exprp(); if (inlineTask) { portp->unlinkFrBack(); @@ -619,8 +660,10 @@ class TaskVisitor final : public VNVisitor { } else if (portp->isInout()) { // UINFOTREE(9, pinp, "", "pinrsize-"); + AstTaskLocalVar* localDeclp = nullptr; AstVarScope* const newvscp - = createVarScope(portp, namePrefix + "__" + portp->shortName()); + = createPortVarScope(portp, namePrefix + "__" + portp->shortName(), + activationLocalPorts, localDeclp); portp->user2p(newvscp); if (!inlineTask) { pinp->replaceWith( @@ -629,42 +672,47 @@ class TaskVisitor final : public VNVisitor { } // Put input assignment in FRONT of all other statements AstAssign* const preassp = connectPortMakeInAssign(pinp, newvscp, true); - if (AstNode* const afterp = beginp->nextp()) { - afterp->unlinkFrBackWithNext(); - AstNode::addNext(preassp, afterp); - } - beginp->addNext(preassp); + AstNode* const frontp + = AstNode::addNextNull(localDeclp, preassp); + addToFront(beginp, frontp); AstAssign* const postassp = connectPortMakeOutAssign(portp, pinp, newvscp, true); beginp->addNext(postassp); + needsCLocalScope = localDeclp != nullptr; // if (debug() >= 9) beginp->dumpTreeAndNext(cout, "-pinrsize-out- "); } else if (portp->isWritable()) { // Even if it's referencing a varref, we still make a temporary // Else task(x,x,x) might produce incorrect results + AstTaskLocalVar* localDeclp = nullptr; AstVarScope* const newvscp - = createVarScope(portp, namePrefix + "__" + portp->shortName()); + = createPortVarScope(portp, namePrefix + "__" + portp->shortName(), + activationLocalPorts, localDeclp); portp->user2p(newvscp); if (!inlineTask) { pinp->replaceWith(new AstVarRef{newvscp->fileline(), newvscp, VAccess::WRITE}); pushDeletep(pinp); // Cloned by connectPortMakeOutAssign } + addToFront(beginp, localDeclp); AstAssign* const postassp = connectPortMakeOutAssign(portp, pinp, newvscp, false); // Put assignment BEHIND of all other statements beginp->addNext(postassp); + needsCLocalScope = localDeclp != nullptr; } else if (inlineTask && portp->isNonOutput()) { // Make input variable + AstTaskLocalVar* localDeclp = nullptr; AstVarScope* const newvscp - = createVarScope(portp, namePrefix + "__" + portp->shortName()); + = createPortVarScope(portp, namePrefix + "__" + portp->shortName(), + activationLocalPorts, localDeclp); portp->user2p(newvscp); AstAssign* const preassp = connectPortMakeInAssign(pinp, newvscp, false); // Put assignment in FRONT of all other statements - if (AstNode* const afterp = beginp->nextp()) { - afterp->unlinkFrBackWithNext(); - AstNode::addNext(preassp, afterp); - } - beginp->addNext(preassp); + AstNode* const frontp + = AstNode::addNextNull(localDeclp, preassp); + addToFront(beginp, frontp); + needsCLocalScope = localDeclp != nullptr; } } + return needsCLocalScope; } bool hasRefArgument(AstNodeFTask* nodep) { @@ -688,13 +736,20 @@ class TaskVisitor final : public VNVisitor { if (debug() >= 9) beginp->dumpTreeAndNext(cout, "- newbegi: "); // // Create input variables + // Task bodies that can suspend or fork need activation-local input temporaries. Inlined + // functions cannot suspend; keeping those temporaries as normal scoped temps avoids + // exposing AstCLocalScope to pre-timing constant/table simulation. + const bool activationLocalPorts + = !refp->taskp()->verilogFunction() && bodyNeedsActivationLocals(newbodysp); + bool needsCLocalScope = false; AstNode::user2ClearTree(); { const V3TaskConnects tconnects = V3Task::taskConnects(refp, beginp); for (const auto& itr : tconnects) { AstVar* const portp = itr.first; AstArg* const argp = itr.second; - connectPort(portp, argp, namePrefix, beginp, true); + needsCLocalScope + |= connectPort(portp, argp, namePrefix, beginp, true, activationLocalPorts); } } UASSERT_OBJ(!refp->argsp(), refp, "Arg wasn't removed by above loop"); @@ -705,6 +760,29 @@ class TaskVisitor final : public VNVisitor { if (AstVar* const portp = VN_CAST(stmtp, Var)) { // Any I/O variables that fell out of above loop were already linked if (!portp->user2p()) { + // Preserve automatic locals introduced by earlier passes inside the + // inlined task body so each activation keeps its own state. + if (isTaskActivationLocal(portp, activationLocalPorts)) { + const string localName = namePrefix + "__" + portp->shortName(); + portp->name(localName); + portp->funcLocal(true); + AstVarScope* const templateVscp = createVarScope(portp, localName); + portp->user2p(templateVscp); + AstTaskLocalVar* const localDeclp + = new AstTaskLocalVar{portp->fileline(), nullptr, templateVscp}; + portp->replaceWith(localDeclp); + localDeclp->varp(portp); + if (portp->needsCReset() && !portp->valuep()) { + // Reset automatic var to its default on each invocation of task. + AstNode* const crstp = new AstAssign{ + portp->fileline(), + new AstVarRef{portp->fileline(), portp, VAccess::WRITE}, + new AstCReset{portp->fileline(), portp, false}}; + localDeclp->addNextHere(crstp); + } + needsCLocalScope = true; + continue; + } // Move it to a new localized variable AstVarScope* const localVscp = createVarScope(portp, namePrefix + "__" + portp->shortName()); @@ -734,7 +812,7 @@ class TaskVisitor final : public VNVisitor { relink(beginp); // if (debug() >= 9) beginp->dumpTreeAndNext(cout, "- iotask: "); - return beginp; + return needsCLocalScope ? new AstCLocalScope{refp->fileline(), beginp} : beginp; } AstNode* createNonInlinedFTask(AstNodeFTaskRef* refp, const string& namePrefix, @@ -769,13 +847,20 @@ class TaskVisitor final : public VNVisitor { ccallp->superReference(taskRefp->superReference()); } - // Convert complicated outputs to temp signals + // Convert complicated outputs to temp signals. Task calls that can suspend or fork need + // staging temps in the caller activation. Non-inlined functions cannot suspend; keep their + // temps as normal scoped temps to avoid exposing AstCLocalScope to earlier function/table + // simulation. + bool needsCLocalScope = false; + const bool activationLocalPorts + = !refp->taskp()->verilogFunction() && bodyNeedsActivationLocals(cfuncp->stmtsp()); { const V3TaskConnects tconnects = V3Task::taskConnects(refp, refp->taskp()->stmtsp()); for (const auto& itr : tconnects) { AstVar* const portp = itr.first; AstArg* const argp = itr.second; - connectPort(portp, argp, namePrefix, beginp, false); + needsCLocalScope + |= connectPort(portp, argp, namePrefix, beginp, false, activationLocalPorts); } } // First argument is symbol table, then output if a function @@ -805,7 +890,7 @@ class TaskVisitor final : public VNVisitor { if (outvscp) ccallp->addArgsp(new AstVarRef{refp->fileline(), outvscp, VAccess::WRITE}); if (debug() >= 9) beginp->dumpTreeAndNext(cout, "- nitask: "); - return beginp; + return needsCLocalScope ? new AstCLocalScope{refp->fileline(), beginp} : beginp; } string dpiSignature(AstNodeFTask* nodep, AstVar* rtnvarp) const { @@ -1879,6 +1964,99 @@ public: } }; +//###################################################################### +// Materialize task activation-local variables after loop unrolling + +using TaskLocalVarScopeMap = std::map; + +class TaskLocalDeclVisitor final : public VNVisitor { + std::vector m_declps; + + void visit(AstCLocalScope*) override { + // A nested C-local scope is a separate task activation. + } + void visit(AstTaskLocalVar* nodep) override { m_declps.push_back(nodep); } + void visit(AstNode* nodep) override { iterateChildren(nodep); } + +public: + explicit TaskLocalDeclVisitor(AstNode* nodep) { iterateAndNextNull(nodep); } + const std::vector& declps() const { return m_declps; } +}; + +class TaskLocalRefVisitor final : public VNVisitor { + const TaskLocalVarScopeMap& m_replacements; + + void visit(AstCLocalScope* nodep) override { + // A nested activation can capture variables from this outer activation. Its own locals + // have different template scopes, so only references to this replacement map are changed. + iterateChildren(nodep); + } + void visit(AstVarRef* nodep) override { + const auto it = m_replacements.find(nodep->varScopep()); + if (it == m_replacements.end()) return; + nodep->varScopep(it->second); + nodep->varp(it->second->varp()); + } + void visit(AstNode* nodep) override { iterateChildren(nodep); } + +public: + TaskLocalRefVisitor(AstNode* nodep, const TaskLocalVarScopeMap& replacements) + : m_replacements{replacements} { + iterateAndNextNull(nodep); + } +}; + +class TaskLocalizeVisitor final : public VNVisitor { + std::set m_templateVscps; + AstCLocalScope* m_localScopep = nullptr; + + void visit(AstNetlist* nodep) override { + iterateChildren(nodep); + + std::set templateVarps; + for (AstVarScope* const vscp : m_templateVscps) { + templateVarps.insert(vscp->varp()); + VL_DO_DANGLING(pushDeletep(vscp->unlinkFrBack()), vscp); + } + for (AstVar* const varp : templateVarps) { + VL_DO_DANGLING(pushDeletep(varp->unlinkFrBack()), varp); + } + } + void visit(AstCLocalScope* nodep) override { + VL_RESTORER(m_localScopep); + m_localScopep = nodep; + + // Process nested task activations before replacing declarations in this one. + iterateChildren(nodep); + + const TaskLocalDeclVisitor declVisitor{nodep->stmtsp()}; + TaskLocalVarScopeMap replacements; + for (AstTaskLocalVar* const declp : declVisitor.declps()) { + AstVarScope* const templateVscp = declp->templateVscp(); + UASSERT_OBJ(templateVscp, declp, "Task-local declaration has no template scope"); + AstVar* const localVarp = declp->varp(); + localVarp->unlinkFrBack(); + UASSERT_OBJ(localVarp->isFuncLocal(), localVarp, "Task-local variable is not local"); + AstVarScope* const localVscp + = new AstVarScope{localVarp->fileline(), templateVscp->scopep(), localVarp}; + templateVscp->scopep()->addVarsp(localVscp); + UASSERT_OBJ(replacements.emplace(templateVscp, localVscp).second, declp, + "Duplicate task-local template in one activation"); + m_templateVscps.insert(templateVscp); + declp->replaceWith(localVarp); + VL_DO_DANGLING(pushDeletep(declp), declp); + } + if (!replacements.empty()) TaskLocalRefVisitor{nodep->stmtsp(), replacements}; + } + void visit(AstTaskLocalVar* nodep) override { + UASSERT_OBJ(m_localScopep, nodep, "Task-local declaration is outside a C-local scope"); + } + void visit(AstNode* nodep) override { iterateChildren(nodep); } + +public: + explicit TaskLocalizeVisitor(AstNetlist* nodep) { iterate(nodep); } +}; + //###################################################################### // Task class functions @@ -2300,3 +2478,9 @@ void V3Task::taskAll(AstNetlist* nodep) { } // Destruct before checking V3Global::dumpCheckGlobalTree("task", 0, dumpTreeEitherLevel() >= 3); } + +void V3Task::taskLocalizeAll(AstNetlist* nodep) { + UINFO(2, __FUNCTION__ << ":"); + { const TaskLocalizeVisitor visitor{nodep}; } + V3Global::dumpCheckGlobalTree("tasklocal", 0, dumpTreeEitherLevel() >= 3); +} diff --git a/src/V3Task.h b/src/V3Task.h index 95739c7ca..38b23a963 100644 --- a/src/V3Task.h +++ b/src/V3Task.h @@ -53,6 +53,7 @@ class V3Task final { public: static void taskAll(AstNetlist* nodep) VL_MT_DISABLED; + static void taskLocalizeAll(AstNetlist* nodep) VL_MT_DISABLED; /// Return vector of [port, pin-connects-to] (SLOW) static V3TaskConnects taskConnects(AstNodeFTaskRef* nodep, AstNode* taskStmtsp, V3TaskConnectState* statep = nullptr, diff --git a/src/V3Timing.cpp b/src/V3Timing.cpp index bbb1227c5..b4a497088 100644 --- a/src/V3Timing.cpp +++ b/src/V3Timing.cpp @@ -478,6 +478,7 @@ class TimingControlVisitor final : public VNVisitor { AstScope* m_scopep = nullptr; // Current scope AstActive* m_activep = nullptr; // Current active AstNode* m_procp = nullptr; // NodeProcedure/CFunc/Begin we're under + AstNode* m_varInsertp = nullptr; // Nearest node that owns function-local variables bool m_hasProcess = false; // True if current scope has a VlProcess handle available int m_forkCnt = 0; // Number of forks inside a module bool m_underJumpBlock = false; // True if we are inside of a jump-block @@ -806,8 +807,8 @@ class TimingControlVisitor final : public VNVisitor { forkp->addNextHere(new AstCAwait{flp, joinp}); } - // `procp` shall be a NodeProcedure/CFunc/Begin and within it vars from `varsp` will be placed. - // `varsp` vector of vars which shall be localized. + // `procp` shall be a NodeProcedure/CFunc/Begin/CLocalScope and within it vars from `varsp` + // will be placed. `varsp` vector of vars which shall be localized. static void localizeVars(AstNode* const procp, const std::vector& varsp) { UASSERT(procp, "procp is nullptr"); AstNode* firstStmtp; @@ -824,15 +825,19 @@ class TimingControlVisitor final : public VNVisitor { firstStmtp = cfuncp->varsp(); } else if (AstBegin* const beginp = VN_CAST(procp, Begin)) { firstStmtp = beginp->stmtsp(); + } else if (AstCLocalScope* const localScopep = VN_CAST(procp, CLocalScope)) { + firstStmtp = localScopep->stmtsp(); } else { procp->v3fatalSrc( procp->prettyNameQ() - << " is not of an expected type NodeProcedure/CFunc/Begin instead it is: " + << " is not of an expected type NodeProcedure/CFunc/Begin/CLocalScope instead it " + "is: " << procp->prettyTypeName()); } UASSERT_OBJ(firstStmtp, procp, - procp->prettyNameQ() << " has no non-var statement. 'localizeVars()' is ment " - "to be called on non-empty NodeProcedure/CFunc/Begin"); + procp->prettyNameQ() + << " has no non-var statement. 'localizeVars()' is meant to be called on " + "non-empty NodeProcedure/CFunc/Begin/CLocalScope"); for (AstVar* const varp : varsp) { varp->funcLocal(true); firstStmtp->addHereThisAsNext(varp->unlinkFrBack()); @@ -889,8 +894,10 @@ class TimingControlVisitor final : public VNVisitor { } void visit(AstNodeProcedure* nodep) override { VL_RESTORER(m_procp); + VL_RESTORER(m_varInsertp); VL_RESTORER(m_hasProcess); m_procp = nodep; + m_varInsertp = nodep; m_hasProcess = hasFlags(nodep, T_HAS_PROC); VL_RESTORER(m_underProcedure); m_underProcedure = true; @@ -913,8 +920,10 @@ class TimingControlVisitor final : public VNVisitor { void visit(AstAlways* nodep) override { if (nodep->user1SetOnce()) return; VL_RESTORER(m_procp); + VL_RESTORER(m_varInsertp); VL_RESTORER(m_hasProcess); m_procp = nodep; + m_varInsertp = nodep; m_hasProcess = hasFlags(nodep, T_HAS_PROC); VL_RESTORER(m_underProcedure); m_underProcedure = true; @@ -947,8 +956,10 @@ class TimingControlVisitor final : public VNVisitor { } void visit(AstCFunc* nodep) override { VL_RESTORER(m_procp); + VL_RESTORER(m_varInsertp); VL_RESTORER(m_hasProcess); m_procp = nodep; + m_varInsertp = nodep; m_hasProcess = hasFlags(nodep, T_HAS_PROC); iterateChildren(nodep); if (hasFlags(nodep, T_HAS_PROC)) nodep->setNeedProcess(); @@ -1101,7 +1112,7 @@ class TimingControlVisitor final : public VNVisitor { m_senExprBuilderp->build(sentreep).first}; // Get the SenExprBuilder results const SenExprBuilder::Results senResults = m_senExprBuilderp->getAndClearResults(); - localizeVars(m_procp, senResults.m_vars); + localizeVars(m_varInsertp, senResults.m_vars); // If post updates are destructive (e.g. clearFired on events), perform a // conservative pre-clear once before entering the wait loop so stale state from a // previous wait does not cause an immediate false-positive trigger. @@ -1419,12 +1430,19 @@ class TimingControlVisitor final : public VNVisitor { } void visit(AstBegin* nodep) override { VL_RESTORER(m_procp); + VL_RESTORER(m_varInsertp); VL_RESTORER(m_hasProcess); m_hasProcess |= hasFlags(nodep, T_HAS_PROC); m_procp = nodep; + m_varInsertp = nodep; if (m_hasProcess) nodep->setNeedProcess(); iterateChildren(nodep); } + void visit(AstCLocalScope* nodep) override { + VL_RESTORER(m_varInsertp); + m_varInsertp = nodep; + iterateChildren(nodep); + } void visit(AstFork* nodep) override { if (nodep->user1SetOnce()) return; UINFO(9, "control-visit " << nodep); diff --git a/src/Verilator.cpp b/src/Verilator.cpp index b694691dd..9e0866f6a 100644 --- a/src/Verilator.cpp +++ b/src/Verilator.cpp @@ -397,6 +397,9 @@ static void process() { // Loop unrolling V3Unroll::unrollAll(v3Global.rootp()); + // Materialize task activation locals after statement-cloning loop transforms. + V3Task::taskLocalizeAll(v3Global.rootp()); + // Expand slices of arrays V3Slice::sliceAll(v3Global.rootp()); diff --git a/test_regress/t/t_always_split.py b/test_regress/t/t_always_split.py index f5ececdbb..16d443be6 100755 --- a/test_regress/t/t_always_split.py +++ b/test_regress/t/t_always_split.py @@ -14,7 +14,7 @@ test.scenarios('simulator') test.compile(verilator_flags2=["--stats"]) if test.vlt_all: - test.file_grep(test.stats, r'Optimizations, Split always\s+(\d+)', 4) + test.file_grep(test.stats, r'Optimizations, Split always\s+(\d+)', 6) test.execute() diff --git a/test_regress/t/t_always_split.v b/test_regress/t/t_always_split.v index 057538c52..5f7543476 100644 --- a/test_regress/t/t_always_split.v +++ b/test_regress/t/t_always_split.v @@ -55,6 +55,24 @@ module t ( l_split_1 <= l_split_2 | m_din; end + task automatic copy_task(input logic [15:0] value, output logic [15:0] result); + result = value; + endtask + + reg [15:0] t_split_1, t_split_2; + always @( /*AS*/ m_din) begin + copy_task(m_din, t_split_1); + t_split_2 = ~m_din; + end + + reg [15:0] n_split_1, n_split_2; + always @( /*AS*/ m_din) begin + for (int i = 0; i < m_din[15]; ++i) begin + copy_task(m_din, n_split_1); + end + n_split_2 = ~m_din; + end + // (The checker block is an exception, it won't split.) always @(posedge clk) begin if (cyc != 0) begin @@ -68,18 +86,24 @@ module t ( m_din <= 16'he11e; //$write(" A %x %x\n", a_split_1, a_split_2); if (!(a_split_1 == 16'hfeed && a_split_2 == 16'hfeed)) $stop; + if (!(t_split_1 == 16'hfeed && t_split_2 == 16'h0112)) $stop; + if (!(n_split_1 == 16'hfeed && n_split_2 == 16'h0112)) $stop; if (!(d_split_1 == 16'h0112 && d_split_2 == 16'h0112)) $stop; if (!(h_split_1 == 16'hfeed && h_split_2 == 16'h0112)) $stop; end if (cyc == 5) begin m_din <= 16'he22e; if (!(a_split_1 == 16'he11e && a_split_2 == 16'he11e)) $stop; + if (!(t_split_1 == 16'he11e && t_split_2 == 16'h1ee1)) $stop; + if (!(n_split_1 == 16'he11e && n_split_2 == 16'h1ee1)) $stop; if (!(d_split_1 == 16'h0112 && d_split_2 == 16'h0112)) $stop; if (!(h_split_1 == 16'hfeed && h_split_2 == 16'h0112)) $stop; end if (cyc == 6) begin m_din <= 16'he33e; if (!(a_split_1 == 16'he22e && a_split_2 == 16'he22e)) $stop; + if (!(t_split_1 == 16'he22e && t_split_2 == 16'h1dd1)) $stop; + if (!(n_split_1 == 16'he22e && n_split_2 == 16'h1dd1)) $stop; if (!(d_split_1 == 16'h1ee1 && d_split_2 == 16'h0112)) $stop; if (!(h_split_1 == 16'he11e && h_split_2 == 16'h1ee1)) $stop; end diff --git a/test_regress/t/t_debug_emitv.out b/test_regress/t/t_debug_emitv.out index 5fcf5c7d7..6f84cc2e6 100644 --- a/test_regress/t/t_debug_emitv.out +++ b/test_regress/t/t_debug_emitv.out @@ -114,7 +114,8 @@ module Vt_debug_emitv_t; logic [11:10] nn3; } [5:4] nibblearray[3:2]; task t; - $display("stmt"); + input int signed value; + $display("stmt %d", value); endtask function f; input int signed v; @@ -137,7 +138,7 @@ module Vt_debug_emitv_t; other = f(i); $display("stmt %d %d", i, other); - t(); + t(i); end i = (i + 'h1); end diff --git a/test_regress/t/t_debug_emitv.v b/test_regress/t/t_debug_emitv.v index 7e474c305..2a5329d75 100644 --- a/test_regress/t/t_debug_emitv.v +++ b/test_regress/t/t_debug_emitv.v @@ -129,8 +129,8 @@ module t (/*AUTOARG*/ } nibble_t; nibble_t [5:4] nibblearray[3:2]; - task t; - $display("stmt"); + task t(input int value); + $display("stmt %d", value); endtask function int f(input int v); $display("stmt"); @@ -146,7 +146,7 @@ module t (/*AUTOARG*/ for (int i = 0; i < 3; ++i) begin other = f(i); $display("stmt %d %d", i, other); - t(); + t(i); end end begin : named diff --git a/test_regress/t/t_fork_join_none_capture.v b/test_regress/t/t_fork_join_none_capture.v index 5f4ecb464..bb2f348c2 100644 --- a/test_regress/t/t_fork_join_none_capture.v +++ b/test_regress/t/t_fork_join_none_capture.v @@ -4,9 +4,162 @@ // SPDX-FileCopyrightText: 2026 by Antmicro Ltd. // SPDX-License-Identifier: CC0-1.0 +// verilog_format: off +`define stop $stop; +`define checkd(gotv, expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got=%0d exp=%0d\n", `__FILE__, `__LINE__, (gotv), (expv)); `stop; end while (0); +// verilog_format: on + module test; mailbox #(int) mbox; + int seen_10 = 0; + int seen_30 = 0; + + task automatic check_mbox_item(input int item); + #0; + if (item == 10) begin + seen_10++; + end + else if (item == 30) begin + seen_30++; + end + else begin + $write("%%Error: unexpected item '%0d'\n", item); + `stop; + end + endtask + + class driver; + string names[string]; + int seen_a = 0; + int seen_b = 0; + + task automatic check_name(input string name); + #0; + if (name == "a") begin + seen_a++; + end + else if (name == "b") begin + seen_b++; + end + else begin + $write("%%Error: unexpected name '%0s'\n", name); + `stop; + end + endtask + + task automatic make_name(input bit pick_b, output string name); + name = pick_b ? "b" : "a"; + #0; + endtask + + task automatic append_name(inout string name); + // verilator no_inline_task + #0; + name = {name, "_done"}; + endtask + + task automatic check_arg_capture(); + names["a"] = "alpha"; + names["b"] = "bravo"; + + foreach (names[name]) begin + fork + check_name(name); + join_none + end + + wait fork; + + `checkd(seen_a, 1); + `checkd(seen_b, 1); + endtask + + task automatic check_local_lifetime(); + seen_a = 0; + seen_b = 0; + + for (int i = 0; i < 2; ++i) begin + automatic bit pick_b = i[0]; + fork + begin + string name; + name = pick_b ? "b" : "a"; + #0; + check_name(name); + end + join_none + end + + wait fork; + + `checkd(seen_a, 1); + `checkd(seen_b, 1); + endtask + + task automatic check_output_capture(); + seen_a = 0; + seen_b = 0; + + for (int i = 0; i < 2; ++i) begin + automatic bit pick_b = i[0]; + fork + begin + string name; + make_name(pick_b, name); + #0; + check_name(name); + end + join_none + end + + wait fork; + + `checkd(seen_a, 1); + `checkd(seen_b, 1); + endtask + + task automatic check_inout_capture(); + seen_a = 0; + seen_b = 0; + + for (int i = 0; i < 2; ++i) begin + automatic bit pick_b = i[0]; + fork + begin + string name = pick_b ? "b" : "a"; + append_name(name); + #0; + if (name == "a_done") begin + seen_a++; + end + else if (name == "b_done") begin + seen_b++; + end + else begin + $write("%%Error: unexpected inout name '%0s'\n", name); + `stop; + end + end + join_none + end + + wait fork; + + `checkd(seen_a, 1); + `checkd(seen_b, 1); + endtask + + task automatic run(); + check_arg_capture(); + check_local_lifetime(); + check_output_capture(); + check_inout_capture(); + endtask + endclass + initial begin + driver drv; + mbox = new(); mbox.put(10); mbox.put(30); @@ -15,14 +168,19 @@ module test; automatic int item; mbox.get(item); fork - begin - $display("got", item); - if (item == 10) $finish; - end + check_mbox_item(item); join_none end - #0; - $stop; + wait fork; + + `checkd(seen_10, 1); + `checkd(seen_30, 1); + + drv = new; + drv.run(); + + $write("*-* All Finished *-*\n"); + $finish; end endmodule diff --git a/test_regress/t/t_fork_task_local_replicate.py b/test_regress/t/t_fork_task_local_replicate.py new file mode 100755 index 000000000..4e0de8ceb --- /dev/null +++ b/test_regress/t/t_fork_task_local_replicate.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# DESCRIPTION: Verilator: Task-local variables survive scheduler logic replication +# +# 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: 2026 Wilson Snyder +# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 + +import vltest_bootstrap + +test.scenarios('simulator') + +test.lint(verilator_flags2=["--timing"]) + +test.passes() diff --git a/test_regress/t/t_fork_task_local_replicate.v b/test_regress/t/t_fork_task_local_replicate.v new file mode 100644 index 000000000..cb9e83eab --- /dev/null +++ b/test_regress/t/t_fork_task_local_replicate.v @@ -0,0 +1,20 @@ +// DESCRIPTION: Verilator: Task-local variables survive scheduler logic replication +// +// This file ONLY is placed under the Creative Commons Public Domain. +// SPDX-FileCopyrightText: 2026 by Wilson Snyder +// SPDX-License-Identifier: CC0-1.0 + +module t ( + input logic a, + output logic unused +); + + task automatic spawn(input logic value); + fork + unused = value; + join_none + endtask + + always @* spawn(a); + +endmodule diff --git a/test_regress/t/t_unroll_stmt.py b/test_regress/t/t_unroll_stmt.py index 6d15f7c2e..86bb38325 100755 --- a/test_regress/t/t_unroll_stmt.py +++ b/test_regress/t/t_unroll_stmt.py @@ -26,7 +26,7 @@ test.file_grep(test.stats, test.file_grep(test.stats, r'Optimizations, Loop unrolling, Failed - unknown loop condition\s+(\d+)', 0) test.file_grep(test.stats, r'Optimizations, Loop unrolling, Pragma unroll_disable\s+(\d+)', 0) -test.file_grep(test.stats, r'Optimizations, Loop unrolling, Unrolled loops\s+(\d+)', 13) -test.file_grep(test.stats, r'Optimizations, Loop unrolling, Unrolled iterations\s+(\d+)', 77) +test.file_grep(test.stats, r'Optimizations, Loop unrolling, Unrolled loops\s+(\d+)', 14) +test.file_grep(test.stats, r'Optimizations, Loop unrolling, Unrolled iterations\s+(\d+)', 80) test.passes() diff --git a/test_regress/t/t_unroll_stmt.v b/test_regress/t/t_unroll_stmt.v index a40495dfd..0a806797d 100644 --- a/test_regress/t/t_unroll_stmt.v +++ b/test_regress/t/t_unroll_stmt.v @@ -14,11 +14,21 @@ module t; int nonConst3 = $c("3"); + task automatic copy_task(input int value, output int result); + result = value; + endtask + initial begin // Basic loop for (int i = 0; i < 3; ++i) begin : loop_0 $display("loop_0 %0d", i); end + // Loop containing activation-local task staging + for (int i = 0; i < 2; ++i) begin : loop_task + int result; + copy_task(i, result); + if (result != i) $stop; + end // Loop with 2 init/step for (int i = 0, j = 5; i < j; i += 2, j += 1) begin : loop_1 $display("loop_1 %0d %0d", i, j);