Keep task variables activation-local across forks

Context:
Task inlining lowered automatic task arguments, locals, and output staging temporaries into object-scope storage. Concurrent activations from one call site could then share state. Keeping declarations lexical exposed dangling AstVarScope links when statement subtrees were cloned by unrolling, splitting, or scheduler replication.

Changes:
- Represent activation-local declarations with a transient AstTaskLocalVar whose scoped template survives statement cloning.
- Materialize independent local variables and scopes immediately after loop unrolling, including references captured by nested task activations.
- Move single-color V3Split leaves instead of cloning them, preserving external lexical-scope links without changing split membership.
- Use activation-local storage only for task bodies that can suspend or fork, leaving non-suspending task and function paths unchanged.
- Keep timing sensitivity temporaries in their nearest lexical CLocalScope.
- Relink scheduler logic clones to independent AstVarScopes and remove only unreferenced scopes whose cloned declarations were discarded.
- Cover input, local, output, inout, nested capture, unroll, split, scheduler replication, and AST tree/JSON dump paths.

Evidence:
- Before the fix, the unroll, nested-split, dynamic-process, and scheduler-replication reproducers fail --debug-check with broken or dangling local links.
- The final focused task/fork set passes 31/31 vlt/vltmt scenarios.
- Scheduler/ICO and fork/timing-fork regression groups pass; the sole initial FST failure passes after supplying the Homebrew lz4 include and library paths.
- make format completes with clang-format 18; git diff --check passes.
- make cppcheck and make lint-py exit successfully; lint-py reports only the existing macOS sched_getaffinity mypy diagnostic.

Boundary:
This changes activation-local task lowering and ownership-preserving cloning of task-local declarations. It does not change disable-fork process-queue semantics, non-suspending function storage, or scheduler region assignment.
This commit is contained in:
Jeffrey Song 2026-07-13 02:24:39 -07:00
parent dde8de0a0d
commit 09e20633bb
20 changed files with 573 additions and 55 deletions

View File

@ -1889,6 +1889,26 @@ public:
bool isPure() override { return false; } bool isPure() override { return false; }
bool isOutputter() override { return true; } 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 { class AstText final : public AstNode {
// Represents a piece of text to be emitted into the output // Represents a piece of text to be emitted into the output
// //

View File

@ -3066,6 +3066,16 @@ void AstSystemCSection::dumpJson(std::ostream& str) const {
dumpJsonStr(str, "sectionType", sectionType().ascii()); dumpJsonStr(str, "sectionType", sectionType().ascii());
dumpJsonGen(str); 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 { void AstTypeTable::dump(std::ostream& str) const {
this->AstNode::dump(str); this->AstNode::dump(str);
for (int i = 0; i < static_cast<int>(VBasicDTypeKwd::_ENUM_MAX); ++i) { for (int i = 0; i < static_cast<int>(VBasicDTypeKwd::_ENUM_MAX); ++i) {

View File

@ -50,6 +50,14 @@ VL_DEFINE_DEBUG_FUNCTIONS;
namespace V3Sched { 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 { namespace {
//============================================================================ //============================================================================
@ -928,6 +936,28 @@ cloneMapWithNewTriggerReferences(const std::unordered_map<const AstSenTree*, Ast
return std::unordered_map<const AstSenTree*, AstSenTree*>{pairs.begin(), pairs.end()}; 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 // Top level entry-point to scheduling
@ -1204,6 +1234,7 @@ void schedule(AstNetlist* netlistp) {
// Step 16: Clean up // Step 16: Clean up
netlistp->clearStlFirstIterationp(); netlistp->clearStlFirstIterationp();
removeVarScopesForDeletedLogic(netlistp);
// Haven't split static initializer yet // Haven't split static initializer yet
util::splitCheck(staticp); util::splitCheck(staticp);

View File

@ -37,6 +37,8 @@ namespace V3Sched {
namespace util { namespace util {
using VarScopeSet = std::unordered_set<const AstVarScope*>; using VarScopeSet = std::unordered_set<const AstVarScope*>;
AstNode* cloneLogic(AstNode* logicp);
inline bool isVlForceVec(const AstVarScope* vscp) { inline bool isVlForceVec(const AstVarScope* vscp) {
const AstCDType* const dtypep = VN_CAST(vscp->dtypep()->skipRefp(), CDType); const AstCDType* const dtypep = VN_CAST(vscp->dtypep()->skipRefp(), CDType);
return dtypep && dtypep->name() == "VlForceVec"; 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 // Create copy, with the AstActives cloned
LogicByScope clone() const { LogicByScope clone() const;
LogicByScope result;
for (const auto& pair : *this) {
result.emplace_back(pair.first, pair.second->cloneTree(false));
}
return result;
}
// Delete actives (they should all be empty) // Delete actives (they should all be empty)
void deleteActives() { void deleteActives() {

View File

@ -274,7 +274,7 @@ LogicReplicas replicate(Graph* graphp) {
for (V3GraphVertex& vtx : graphp->vertices()) { for (V3GraphVertex& vtx : graphp->vertices()) {
if (SchedReplicateLogicVertex* const lvtxp = vtx.cast<SchedReplicateLogicVertex>()) { if (SchedReplicateLogicVertex* const lvtxp = vtx.cast<SchedReplicateLogicVertex>()) {
const auto replicateTo = [&](LogicByScope& lbs) { 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(); const uint8_t targetRegions = lvtxp->drivingRegions() & ~lvtxp->assignedRegion();
UASSERT(!lvtxp->senTreep()->hasClocked() || targetRegions == 0, UASSERT(!lvtxp->senTreep()->hasClocked() || targetRegions == 0,

View File

@ -33,6 +33,32 @@ VL_DEFINE_DEBUG_FUNCTIONS;
namespace V3Sched { namespace V3Sched {
namespace util { 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) { AstCFunc* makeSubFunction(AstNetlist* netlistp, const string& name, bool slow) {
AstScope* const scopeTopp = netlistp->topScopep()->scopep(); AstScope* const scopeTopp = netlistp->topScopep()->scopep();
AstCFunc* const funcp = new AstCFunc{netlistp->fileline(), name, scopeTopp, ""}; AstCFunc* const funcp = new AstCFunc{netlistp->fileline(), name, scopeTopp, ""};

View File

@ -522,9 +522,8 @@ public:
m_addAfter[color] = placeholderp; m_addAfter[color] = placeholderp;
m_newBlocksp->push_back(alwaysp); m_newBlocksp->push_back(alwaysp);
} }
// Scan the body of the always. We'll handle if/else // Scan the body of the always. We'll handle if/else specially; every other leaf belongs to
// specially, everything else is a leaf node that we can // one color and can move into the corresponding split always block.
// just clone into one of the split always blocks.
iterateAndNextNull(m_origAlwaysp->stmtsp()); iterateAndNextNull(m_origAlwaysp->stmtsp());
} }
@ -547,12 +546,13 @@ protected:
// Each leaf must have a user3p // Each leaf must have a user3p
UASSERT_OBJ(nodep->user3p(), nodep, "null user3p in V3Split leaf"); 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 SplitLogicVertex* const vxp = reinterpret_cast<SplitLogicVertex*>(nodep->user3p());
const uint32_t color = vxp->color(); const uint32_t color = vxp->color();
AstNode* const clonedp = nodep->cloneTree(false); nodep->unlinkFrBack();
m_addAfter[color]->addNextHere(clonedp); m_addAfter[color]->addNextHere(nodep);
m_addAfter[color] = clonedp; m_addAfter[color] = nodep;
} }
void visit(AstNodeIf* nodep) override { void visit(AstNodeIf* nodep) override {

View File

@ -34,6 +34,7 @@
#include "V3Stats.h" #include "V3Stats.h"
#include "V3UniqueNames.h" #include "V3UniqueNames.h"
#include <set>
#include <tuple> #include <tuple>
VL_DEFINE_DEBUG_FUNCTIONS; VL_DEFINE_DEBUG_FUNCTIONS;
@ -483,6 +484,45 @@ class TaskVisitor final : public VNVisitor {
return newvscp; 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 // Replace varrefs with new var pointer
void relink(AstNode* nodep) { 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 connectPort(AstVar* portp, AstArg* argp, const string& namePrefix, AstNode* beginp,
bool inlineTask) { bool inlineTask, bool activationLocalPorts) {
bool needsCLocalScope = false;
AstNodeExpr* pinp = argp->exprp(); AstNodeExpr* pinp = argp->exprp();
if (inlineTask) { if (inlineTask) {
portp->unlinkFrBack(); portp->unlinkFrBack();
@ -619,8 +660,10 @@ class TaskVisitor final : public VNVisitor {
} else if (portp->isInout()) { } else if (portp->isInout()) {
// UINFOTREE(9, pinp, "", "pinrsize-"); // UINFOTREE(9, pinp, "", "pinrsize-");
AstTaskLocalVar* localDeclp = nullptr;
AstVarScope* const newvscp AstVarScope* const newvscp
= createVarScope(portp, namePrefix + "__" + portp->shortName()); = createPortVarScope(portp, namePrefix + "__" + portp->shortName(),
activationLocalPorts, localDeclp);
portp->user2p(newvscp); portp->user2p(newvscp);
if (!inlineTask) { if (!inlineTask) {
pinp->replaceWith( pinp->replaceWith(
@ -629,42 +672,47 @@ class TaskVisitor final : public VNVisitor {
} }
// Put input assignment in FRONT of all other statements // Put input assignment in FRONT of all other statements
AstAssign* const preassp = connectPortMakeInAssign(pinp, newvscp, true); AstAssign* const preassp = connectPortMakeInAssign(pinp, newvscp, true);
if (AstNode* const afterp = beginp->nextp()) { AstNode* const frontp
afterp->unlinkFrBackWithNext(); = AstNode::addNextNull<AstNode, AstNode>(localDeclp, preassp);
AstNode::addNext<AstNode, AstNode>(preassp, afterp); addToFront(beginp, frontp);
}
beginp->addNext(preassp);
AstAssign* const postassp = connectPortMakeOutAssign(portp, pinp, newvscp, true); AstAssign* const postassp = connectPortMakeOutAssign(portp, pinp, newvscp, true);
beginp->addNext(postassp); beginp->addNext(postassp);
needsCLocalScope = localDeclp != nullptr;
// if (debug() >= 9) beginp->dumpTreeAndNext(cout, "-pinrsize-out- "); // if (debug() >= 9) beginp->dumpTreeAndNext(cout, "-pinrsize-out- ");
} else if (portp->isWritable()) { } else if (portp->isWritable()) {
// Even if it's referencing a varref, we still make a temporary // Even if it's referencing a varref, we still make a temporary
// Else task(x,x,x) might produce incorrect results // Else task(x,x,x) might produce incorrect results
AstTaskLocalVar* localDeclp = nullptr;
AstVarScope* const newvscp AstVarScope* const newvscp
= createVarScope(portp, namePrefix + "__" + portp->shortName()); = createPortVarScope(portp, namePrefix + "__" + portp->shortName(),
activationLocalPorts, localDeclp);
portp->user2p(newvscp); portp->user2p(newvscp);
if (!inlineTask) { if (!inlineTask) {
pinp->replaceWith(new AstVarRef{newvscp->fileline(), newvscp, VAccess::WRITE}); pinp->replaceWith(new AstVarRef{newvscp->fileline(), newvscp, VAccess::WRITE});
pushDeletep(pinp); // Cloned by connectPortMakeOutAssign pushDeletep(pinp); // Cloned by connectPortMakeOutAssign
} }
addToFront(beginp, localDeclp);
AstAssign* const postassp = connectPortMakeOutAssign(portp, pinp, newvscp, false); AstAssign* const postassp = connectPortMakeOutAssign(portp, pinp, newvscp, false);
// Put assignment BEHIND of all other statements // Put assignment BEHIND of all other statements
beginp->addNext(postassp); beginp->addNext(postassp);
needsCLocalScope = localDeclp != nullptr;
} else if (inlineTask && portp->isNonOutput()) { } else if (inlineTask && portp->isNonOutput()) {
// Make input variable // Make input variable
AstTaskLocalVar* localDeclp = nullptr;
AstVarScope* const newvscp AstVarScope* const newvscp
= createVarScope(portp, namePrefix + "__" + portp->shortName()); = createPortVarScope(portp, namePrefix + "__" + portp->shortName(),
activationLocalPorts, localDeclp);
portp->user2p(newvscp); portp->user2p(newvscp);
AstAssign* const preassp = connectPortMakeInAssign(pinp, newvscp, false); AstAssign* const preassp = connectPortMakeInAssign(pinp, newvscp, false);
// Put assignment in FRONT of all other statements // Put assignment in FRONT of all other statements
if (AstNode* const afterp = beginp->nextp()) { AstNode* const frontp
afterp->unlinkFrBackWithNext(); = AstNode::addNextNull<AstNode, AstNode>(localDeclp, preassp);
AstNode::addNext<AstNode, AstNode>(preassp, afterp); addToFront(beginp, frontp);
} needsCLocalScope = localDeclp != nullptr;
beginp->addNext(preassp);
} }
} }
return needsCLocalScope;
} }
bool hasRefArgument(AstNodeFTask* nodep) { bool hasRefArgument(AstNodeFTask* nodep) {
@ -688,13 +736,20 @@ class TaskVisitor final : public VNVisitor {
if (debug() >= 9) beginp->dumpTreeAndNext(cout, "- newbegi: "); if (debug() >= 9) beginp->dumpTreeAndNext(cout, "- newbegi: ");
// //
// Create input variables // 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(); AstNode::user2ClearTree();
{ {
const V3TaskConnects tconnects = V3Task::taskConnects(refp, beginp); const V3TaskConnects tconnects = V3Task::taskConnects(refp, beginp);
for (const auto& itr : tconnects) { for (const auto& itr : tconnects) {
AstVar* const portp = itr.first; AstVar* const portp = itr.first;
AstArg* const argp = itr.second; 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"); 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)) { if (AstVar* const portp = VN_CAST(stmtp, Var)) {
// Any I/O variables that fell out of above loop were already linked // Any I/O variables that fell out of above loop were already linked
if (!portp->user2p()) { 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 // Move it to a new localized variable
AstVarScope* const localVscp AstVarScope* const localVscp
= createVarScope(portp, namePrefix + "__" + portp->shortName()); = createVarScope(portp, namePrefix + "__" + portp->shortName());
@ -734,7 +812,7 @@ class TaskVisitor final : public VNVisitor {
relink(beginp); relink(beginp);
// //
if (debug() >= 9) beginp->dumpTreeAndNext(cout, "- iotask: "); if (debug() >= 9) beginp->dumpTreeAndNext(cout, "- iotask: ");
return beginp; return needsCLocalScope ? new AstCLocalScope{refp->fileline(), beginp} : beginp;
} }
AstNode* createNonInlinedFTask(AstNodeFTaskRef* refp, const string& namePrefix, AstNode* createNonInlinedFTask(AstNodeFTaskRef* refp, const string& namePrefix,
@ -769,13 +847,20 @@ class TaskVisitor final : public VNVisitor {
ccallp->superReference(taskRefp->superReference()); 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()); const V3TaskConnects tconnects = V3Task::taskConnects(refp, refp->taskp()->stmtsp());
for (const auto& itr : tconnects) { for (const auto& itr : tconnects) {
AstVar* const portp = itr.first; AstVar* const portp = itr.first;
AstArg* const argp = itr.second; 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 // 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 (outvscp) ccallp->addArgsp(new AstVarRef{refp->fileline(), outvscp, VAccess::WRITE});
if (debug() >= 9) beginp->dumpTreeAndNext(cout, "- nitask: "); if (debug() >= 9) beginp->dumpTreeAndNext(cout, "- nitask: ");
return beginp; return needsCLocalScope ? new AstCLocalScope{refp->fileline(), beginp} : beginp;
} }
string dpiSignature(AstNodeFTask* nodep, AstVar* rtnvarp) const { 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 // Task class functions
@ -2300,3 +2478,9 @@ void V3Task::taskAll(AstNetlist* nodep) {
} // Destruct before checking } // Destruct before checking
V3Global::dumpCheckGlobalTree("task", 0, dumpTreeEitherLevel() >= 3); 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: public:
static void taskAll(AstNetlist* nodep) VL_MT_DISABLED; static void taskAll(AstNetlist* nodep) VL_MT_DISABLED;
static void taskLocalizeAll(AstNetlist* nodep) VL_MT_DISABLED;
/// Return vector of [port, pin-connects-to] (SLOW) /// Return vector of [port, pin-connects-to] (SLOW)
static V3TaskConnects taskConnects(AstNodeFTaskRef* nodep, AstNode* taskStmtsp, static V3TaskConnects taskConnects(AstNodeFTaskRef* nodep, AstNode* taskStmtsp,
V3TaskConnectState* statep = nullptr, V3TaskConnectState* statep = nullptr,

View File

@ -478,6 +478,7 @@ class TimingControlVisitor final : public VNVisitor {
AstScope* m_scopep = nullptr; // Current scope AstScope* m_scopep = nullptr; // Current scope
AstActive* m_activep = nullptr; // Current active AstActive* m_activep = nullptr; // Current active
AstNode* m_procp = nullptr; // NodeProcedure/CFunc/Begin we're under 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 bool m_hasProcess = false; // True if current scope has a VlProcess handle available
int m_forkCnt = 0; // Number of forks inside a module int m_forkCnt = 0; // Number of forks inside a module
bool m_underJumpBlock = false; // True if we are inside of a jump-block bool m_underJumpBlock = false; // True if we are inside of a jump-block
@ -811,8 +812,8 @@ class TimingControlVisitor final : public VNVisitor {
forkp->addNextHere(new AstCAwait{flp, joinp}); forkp->addNextHere(new AstCAwait{flp, joinp});
} }
// `procp` shall be a NodeProcedure/CFunc/Begin and within it vars from `varsp` will be placed. // `procp` shall be a NodeProcedure/CFunc/Begin/CLocalScope and within it vars from `varsp`
// `varsp` vector of vars which shall be localized. // will be placed. `varsp` vector of vars which shall be localized.
static void localizeVars(AstNode* const procp, const std::vector<AstVar*>& varsp) { static void localizeVars(AstNode* const procp, const std::vector<AstVar*>& varsp) {
UASSERT(procp, "procp is nullptr"); UASSERT(procp, "procp is nullptr");
AstNode* firstStmtp; AstNode* firstStmtp;
@ -829,15 +830,19 @@ class TimingControlVisitor final : public VNVisitor {
firstStmtp = cfuncp->varsp(); firstStmtp = cfuncp->varsp();
} else if (AstBegin* const beginp = VN_CAST(procp, Begin)) { } else if (AstBegin* const beginp = VN_CAST(procp, Begin)) {
firstStmtp = beginp->stmtsp(); firstStmtp = beginp->stmtsp();
} else if (AstCLocalScope* const localScopep = VN_CAST(procp, CLocalScope)) {
firstStmtp = localScopep->stmtsp();
} else { } else {
procp->v3fatalSrc( procp->v3fatalSrc(
procp->prettyNameQ() 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()); << procp->prettyTypeName());
} }
UASSERT_OBJ(firstStmtp, procp, UASSERT_OBJ(firstStmtp, procp,
procp->prettyNameQ() << " has no non-var statement. 'localizeVars()' is ment " procp->prettyNameQ()
"to be called on non-empty NodeProcedure/CFunc/Begin"); << " has no non-var statement. 'localizeVars()' is meant to be called on "
"non-empty NodeProcedure/CFunc/Begin/CLocalScope");
for (AstVar* const varp : varsp) { for (AstVar* const varp : varsp) {
varp->funcLocal(true); varp->funcLocal(true);
firstStmtp->addHereThisAsNext(varp->unlinkFrBack()); firstStmtp->addHereThisAsNext(varp->unlinkFrBack());
@ -894,8 +899,10 @@ class TimingControlVisitor final : public VNVisitor {
} }
void visit(AstNodeProcedure* nodep) override { void visit(AstNodeProcedure* nodep) override {
VL_RESTORER(m_procp); VL_RESTORER(m_procp);
VL_RESTORER(m_varInsertp);
VL_RESTORER(m_hasProcess); VL_RESTORER(m_hasProcess);
m_procp = nodep; m_procp = nodep;
m_varInsertp = nodep;
m_hasProcess = hasFlags(nodep, T_HAS_PROC); m_hasProcess = hasFlags(nodep, T_HAS_PROC);
VL_RESTORER(m_underProcedure); VL_RESTORER(m_underProcedure);
m_underProcedure = true; m_underProcedure = true;
@ -918,8 +925,10 @@ class TimingControlVisitor final : public VNVisitor {
void visit(AstAlways* nodep) override { void visit(AstAlways* nodep) override {
if (nodep->user1SetOnce()) return; if (nodep->user1SetOnce()) return;
VL_RESTORER(m_procp); VL_RESTORER(m_procp);
VL_RESTORER(m_varInsertp);
VL_RESTORER(m_hasProcess); VL_RESTORER(m_hasProcess);
m_procp = nodep; m_procp = nodep;
m_varInsertp = nodep;
m_hasProcess = hasFlags(nodep, T_HAS_PROC); m_hasProcess = hasFlags(nodep, T_HAS_PROC);
VL_RESTORER(m_underProcedure); VL_RESTORER(m_underProcedure);
m_underProcedure = true; m_underProcedure = true;
@ -952,8 +961,10 @@ class TimingControlVisitor final : public VNVisitor {
} }
void visit(AstCFunc* nodep) override { void visit(AstCFunc* nodep) override {
VL_RESTORER(m_procp); VL_RESTORER(m_procp);
VL_RESTORER(m_varInsertp);
VL_RESTORER(m_hasProcess); VL_RESTORER(m_hasProcess);
m_procp = nodep; m_procp = nodep;
m_varInsertp = nodep;
m_hasProcess = hasFlags(nodep, T_HAS_PROC); m_hasProcess = hasFlags(nodep, T_HAS_PROC);
iterateChildren(nodep); iterateChildren(nodep);
if (hasFlags(nodep, T_HAS_PROC)) nodep->setNeedProcess(); if (hasFlags(nodep, T_HAS_PROC)) nodep->setNeedProcess();
@ -1106,7 +1117,7 @@ class TimingControlVisitor final : public VNVisitor {
m_senExprBuilderp->build(sentreep).first}; m_senExprBuilderp->build(sentreep).first};
// Get the SenExprBuilder results // Get the SenExprBuilder results
const SenExprBuilder::Results senResults = m_senExprBuilderp->getAndClearResults(); 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 // 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 // conservative pre-clear once before entering the wait loop so stale state from a
// previous wait does not cause an immediate false-positive trigger. // previous wait does not cause an immediate false-positive trigger.
@ -1424,12 +1435,19 @@ class TimingControlVisitor final : public VNVisitor {
} }
void visit(AstBegin* nodep) override { void visit(AstBegin* nodep) override {
VL_RESTORER(m_procp); VL_RESTORER(m_procp);
VL_RESTORER(m_varInsertp);
VL_RESTORER(m_hasProcess); VL_RESTORER(m_hasProcess);
m_hasProcess |= hasFlags(nodep, T_HAS_PROC); m_hasProcess |= hasFlags(nodep, T_HAS_PROC);
m_procp = nodep; m_procp = nodep;
m_varInsertp = nodep;
if (m_hasProcess) nodep->setNeedProcess(); if (m_hasProcess) nodep->setNeedProcess();
iterateChildren(nodep); iterateChildren(nodep);
} }
void visit(AstCLocalScope* nodep) override {
VL_RESTORER(m_varInsertp);
m_varInsertp = nodep;
iterateChildren(nodep);
}
void visit(AstFork* nodep) override { void visit(AstFork* nodep) override {
if (nodep->user1SetOnce()) return; if (nodep->user1SetOnce()) return;
UINFO(9, "control-visit " << nodep); UINFO(9, "control-visit " << nodep);

View File

@ -397,6 +397,9 @@ static void process() {
// Loop unrolling // Loop unrolling
V3Unroll::unrollAll(v3Global.rootp()); V3Unroll::unrollAll(v3Global.rootp());
// Materialize task activation locals after statement-cloning loop transforms.
V3Task::taskLocalizeAll(v3Global.rootp());
// Expand slices of arrays // Expand slices of arrays
V3Slice::sliceAll(v3Global.rootp()); V3Slice::sliceAll(v3Global.rootp());

View File

@ -14,7 +14,7 @@ test.scenarios('simulator')
test.compile(verilator_flags2=["--stats"]) test.compile(verilator_flags2=["--stats"])
if test.vlt_all: 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() test.execute()

View File

@ -55,6 +55,24 @@ module t (
l_split_1 <= l_split_2 | m_din; l_split_1 <= l_split_2 | m_din;
end 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.) // (The checker block is an exception, it won't split.)
always @(posedge clk) begin always @(posedge clk) begin
if (cyc != 0) begin if (cyc != 0) begin
@ -68,18 +86,24 @@ module t (
m_din <= 16'he11e; m_din <= 16'he11e;
//$write(" A %x %x\n", a_split_1, a_split_2); //$write(" A %x %x\n", a_split_1, a_split_2);
if (!(a_split_1 == 16'hfeed && a_split_2 == 16'hfeed)) $stop; 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 (!(d_split_1 == 16'h0112 && d_split_2 == 16'h0112)) $stop;
if (!(h_split_1 == 16'hfeed && h_split_2 == 16'h0112)) $stop; if (!(h_split_1 == 16'hfeed && h_split_2 == 16'h0112)) $stop;
end end
if (cyc == 5) begin if (cyc == 5) begin
m_din <= 16'he22e; m_din <= 16'he22e;
if (!(a_split_1 == 16'he11e && a_split_2 == 16'he11e)) $stop; 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 (!(d_split_1 == 16'h0112 && d_split_2 == 16'h0112)) $stop;
if (!(h_split_1 == 16'hfeed && h_split_2 == 16'h0112)) $stop; if (!(h_split_1 == 16'hfeed && h_split_2 == 16'h0112)) $stop;
end end
if (cyc == 6) begin if (cyc == 6) begin
m_din <= 16'he33e; m_din <= 16'he33e;
if (!(a_split_1 == 16'he22e && a_split_2 == 16'he22e)) $stop; 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 (!(d_split_1 == 16'h1ee1 && d_split_2 == 16'h0112)) $stop;
if (!(h_split_1 == 16'he11e && h_split_2 == 16'h1ee1)) $stop; if (!(h_split_1 == 16'he11e && h_split_2 == 16'h1ee1)) $stop;
end end

View File

@ -114,7 +114,8 @@ module Vt_debug_emitv_t;
logic [11:10] nn3; logic [11:10] nn3;
} [5:4] nibblearray[3:2]; } [5:4] nibblearray[3:2];
task t; task t;
$display("stmt"); input int signed value;
$display("stmt %d", value);
endtask endtask
function f; function f;
input int signed v; input int signed v;
@ -137,7 +138,7 @@ module Vt_debug_emitv_t;
other = f(i); other = f(i);
$display("stmt %d %d", $display("stmt %d %d",
i, other); i, other);
t(); t(i);
end end
i = (i + 'h1); i = (i + 'h1);
end end

View File

@ -129,8 +129,8 @@ module t (/*AUTOARG*/
} nibble_t; } nibble_t;
nibble_t [5:4] nibblearray[3:2]; nibble_t [5:4] nibblearray[3:2];
task t; task t(input int value);
$display("stmt"); $display("stmt %d", value);
endtask endtask
function int f(input int v); function int f(input int v);
$display("stmt"); $display("stmt");
@ -146,7 +146,7 @@ module t (/*AUTOARG*/
for (int i = 0; i < 3; ++i) begin for (int i = 0; i < 3; ++i) begin
other = f(i); other = f(i);
$display("stmt %d %d", i, other); $display("stmt %d %d", i, other);
t(); t(i);
end end
end end
begin : named begin : named

View File

@ -4,9 +4,162 @@
// SPDX-FileCopyrightText: 2026 by Antmicro Ltd. // SPDX-FileCopyrightText: 2026 by Antmicro Ltd.
// SPDX-License-Identifier: CC0-1.0 // 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; module test;
mailbox #(int) mbox; 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 initial begin
driver drv;
mbox = new(); mbox = new();
mbox.put(10); mbox.put(10);
mbox.put(30); mbox.put(30);
@ -15,14 +168,19 @@ module test;
automatic int item; automatic int item;
mbox.get(item); mbox.get(item);
fork fork
begin check_mbox_item(item);
$display("got", item);
if (item == 10) $finish;
end
join_none join_none
end end
#0; wait fork;
$stop;
`checkd(seen_10, 1);
`checkd(seen_30, 1);
drv = new;
drv.run();
$write("*-* All Finished *-*\n");
$finish;
end end
endmodule 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, test.file_grep(test.stats,
r'Optimizations, Loop unrolling, Failed - unknown loop condition\s+(\d+)', 0) 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, 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 loops\s+(\d+)', 14)
test.file_grep(test.stats, r'Optimizations, Loop unrolling, Unrolled iterations\s+(\d+)', 77) test.file_grep(test.stats, r'Optimizations, Loop unrolling, Unrolled iterations\s+(\d+)', 80)
test.passes() test.passes()

View File

@ -14,11 +14,21 @@ module t;
int nonConst3 = $c("3"); int nonConst3 = $c("3");
task automatic copy_task(input int value, output int result);
result = value;
endtask
initial begin initial begin
// Basic loop // Basic loop
for (int i = 0; i < 3; ++i) begin : loop_0 for (int i = 0; i < 3; ++i) begin : loop_0
$display("loop_0 %0d", i); $display("loop_0 %0d", i);
end 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 // Loop with 2 init/step
for (int i = 0, j = 5; i < j; i += 2, j += 1) begin : loop_1 for (int i = 0, j = 5; i < j; i += 2, j += 1) begin : loop_1
$display("loop_1 %0d %0d", i, j); $display("loop_1 %0d %0d", i, j);