This commit is contained in:
Jeffrey Song 2026-07-14 04:19:28 +00:00 committed by GitHub
commit 95e7feef6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 573 additions and 55 deletions

View File

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

View File

@ -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<int>(VBasicDTypeKwd::_ENUM_MAX); ++i) {

View File

@ -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<const AstSenTree*, Ast
return std::unordered_map<const AstSenTree*, AstSenTree*>{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<const AstVar*> liveVarps;
std::unordered_set<const AstVarScope*> referencedVscps;
std::vector<AstVarScope*> 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);

View File

@ -37,6 +37,8 @@ namespace V3Sched {
namespace util {
using VarScopeSet = std::unordered_set<const AstVarScope*>;
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<std::pair<AstScope*, AstActive*>>
};
// 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() {

View File

@ -274,7 +274,7 @@ LogicReplicas replicate(Graph* graphp) {
for (V3GraphVertex& vtx : graphp->vertices()) {
if (SchedReplicateLogicVertex* const lvtxp = vtx.cast<SchedReplicateLogicVertex>()) {
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,

View File

@ -33,6 +33,32 @@ VL_DEFINE_DEBUG_FUNCTIONS;
namespace V3Sched {
namespace util {
AstNode* cloneLogic(AstNode* logicp) {
AstNode* const clonep = logicp->cloneTree(false);
std::map<AstVarScope*, AstVarScope*> 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, ""};

View File

@ -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<SplitLogicVertex*>(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 {

View File

@ -34,6 +34,7 @@
#include "V3Stats.h"
#include "V3UniqueNames.h"
#include <set>
#include <tuple>
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<AstNode, AstNode>(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<AstNode, AstNode>(preassp, afterp);
}
beginp->addNext(preassp);
AstNode* const frontp
= AstNode::addNextNull<AstNode, AstNode>(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<AstNode, AstNode>(preassp, afterp);
}
beginp->addNext(preassp);
AstNode* const frontp
= AstNode::addNextNull<AstNode, AstNode>(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<AstVarScope*, AstVarScope*>;
class TaskLocalDeclVisitor final : public VNVisitor {
std::vector<AstTaskLocalVar*> 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<AstTaskLocalVar*>& 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<AstVarScope*> m_templateVscps;
AstCLocalScope* m_localScopep = nullptr;
void visit(AstNetlist* nodep) override {
iterateChildren(nodep);
std::set<AstVar*> 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);
}

View File

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

View File

@ -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<AstVar*>& 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);

View File

@ -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());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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