Can you please attach an example that shows the issue?
min_wait_fork.sv, self-contained, CC0:
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module tb;
logic clk = 1'b0;
always #5 clk = ~clk;
initial begin
fork @(posedge clk); join_none
wait fork;
$finish;
end
endmodule
What output from that test indicates it is wrong, and what is the correct or expected output?
Expected: it compiles and runs to $finish at 5ps. Actual: the generated C++ does not compile.
Vtb___024root__2.cpp: In function 'VlCoroutine Vtb___024root___eval_initial__TOP__Vtiming__0__2(...)':
Vtb___024root__2.cpp:23:13: error: '__Vtrigprevexpr_h5d9da2ce__0' was not declared in this scope
23 | __Vtrigprevexpr_h5d9da2ce__0 = vlProcess->completedFork();
make: *** [verilated.mk:293: Vtb___024root__2.o] Error 1
The wait fork dynamic-trigger temporary is emitted as a function-local of one sub-function
(...__Vtiming__0__0) while the coroutine that assigns it is a different sub-function
(...__Vtiming__0__2), in a different translation unit.
Two further symptoms on git master:
-
verilator_bin_dbg --debug reports a broken link, because V3InlineCFuncs inlines the
sub-function that received the declaration, renames the local it now owns and frees the original
AstVar, while the other sub-function still holds AstVarRefs to it:
%Error: Internal Error: min_wait_fork.sv:6:5: ../V3Broken.cpp:182: Broken link in node
(or something without maybePointedTo): 'm_varp && !m_varp->brokeExists()'
-
Built -O3, verilator_bin segfaults on this input rather than emitting the bad C++.
An ordinary edge trigger is unaffected: __Vtrigprevexpr___TOP__tb__DOT__clk__0 is a root class
member and survives any split.
What 'verilator' command line do we use to run your example?
verilator --binary --timing --output-split-cfuncs 20 -top tb -o sim.exe min_wait_fork.sv
--output-split-cfuncs 20 is only a size proxy, to force a split on a small example. It also
reproduces on default flags for any single process exceeding the 20000 default before its
wait fork, because localizeVars() places the declaration at the very front of the process. This
generates such a case, which fails with no split flag at all:
lines = ["module tb;", " logic clk = 1'b0;", " int acc = 0;", " int mem [0:4095];",
" always #5 clk = ~clk;", " initial begin"]
for i in range(4096):
lines.append(f" mem[{i}] = {i} * 3 + acc;")
lines.append(f" acc = acc + mem[{i}];")
lines += [" fork @(posedge clk); join_none", " wait fork;",
" if (acc == 0) $stop;",
" $write(\"*-* All Finished *-*\\n\");", " $finish;", " end", "endmodule"]
open("big_wait_fork.sv", "w").write("\n".join(lines) + "\n")
$ verilator --binary --timing -top tb -o sim.exe big_wait_fork.sv
Vtb___024root__2.cpp:4982:13: error: '__Vtrigprevexpr_h5d9da2ce__0' was not declared in this scope
It was originally hit on a 64 KB generated testbench with 4 wait fork sites on stock flags,
producing 17 instances of the error across Vtb_...__12.cpp through Vtb_...__31.cpp.
What 'verilator --version' are you using? Did you try it with the git master version? Did you try it with other simulators?
Reproduced on git master, 5.051 devel rev v5.050-99-gf8fb1d6 (f8fb1d6, 2026-07-24), and on
released 5.048 and 5.049-devel.
Present in every release since 5.046. git diff v5.050 HEAD over V3SchedUtil.cpp,
V3Timing.cpp, V3SenExprBuilder.h and V3Sched.cpp shows only an unrelated refactor of
hasDisableQueuePushSelfPrefix, so 5.050 has the same defect in the same form.
Not tried on other simulators; the input is legal IEEE 1800 and Verilator accepts it through
elaboration, so this is purely a code-generation defect.
What OS and distribution are you using?
Ubuntu 24.04 on WSL2 (Linux 6.6.87.2-microsoft-standard-WSL2), g++ 15.2.0.
May we assist you in trying to fix this in Verilator yourself?
I have a fix and will open a PR referencing this issue.
Root cause: V3Sched::util::splitCheck() cuts a function's top level statement list on node count
alone, ignoring AstVar declarations sitting in that list. Its own
UASSERT_OBJ(!ofuncp->varsp(), ..., "Can't split function with local variables") shows the intent,
but only checks AstCFunc::varsp(), not declarations among the statements, and localizeVars()
puts them in stmtsp().
This is the same failure as #5987, which #5822 fixed by removing function locals from
SenExprBuilder ("function locals are not safe here because we might need to split up the generated
function"). #6926 reintroduced them for the dynamic-trigger path, because #6859 needs the temporary
to be per-process. The two requirements conflict, so rather than removing the locals again the fix
makes splitCheck() honour them: a sub-function boundary is only allowed where it does not separate
a local declaration from a reference to it.
Can you please attach an example that shows the issue?
min_wait_fork.sv, self-contained, CC0:What output from that test indicates it is wrong, and what is the correct or expected output?
Expected: it compiles and runs to
$finishat 5ps. Actual: the generated C++ does not compile.The
wait forkdynamic-trigger temporary is emitted as a function-local of one sub-function(
...__Vtiming__0__0) while the coroutine that assigns it is a different sub-function(
...__Vtiming__0__2), in a different translation unit.Two further symptoms on git master:
verilator_bin_dbg --debugreports a broken link, becauseV3InlineCFuncsinlines thesub-function that received the declaration, renames the local it now owns and frees the original
AstVar, while the other sub-function still holdsAstVarRefs to it:Built
-O3,verilator_binsegfaults on this input rather than emitting the bad C++.An ordinary edge trigger is unaffected:
__Vtrigprevexpr___TOP__tb__DOT__clk__0is a root classmember and survives any split.
What 'verilator' command line do we use to run your example?
--output-split-cfuncs 20is only a size proxy, to force a split on a small example. It alsoreproduces on default flags for any single process exceeding the 20000 default before its
wait fork, becauselocalizeVars()places the declaration at the very front of the process. Thisgenerates such a case, which fails with no split flag at all:
It was originally hit on a 64 KB generated testbench with 4
wait forksites on stock flags,producing 17 instances of the error across
Vtb_...__12.cppthroughVtb_...__31.cpp.What 'verilator --version' are you using? Did you try it with the git master version? Did you try it with other simulators?
Reproduced on git master,
5.051 devel rev v5.050-99-gf8fb1d6(f8fb1d6, 2026-07-24), and onreleased 5.048 and 5.049-devel.
Present in every release since 5.046.
git diff v5.050 HEADoverV3SchedUtil.cpp,V3Timing.cpp,V3SenExprBuilder.handV3Sched.cppshows only an unrelated refactor ofhasDisableQueuePushSelfPrefix, so 5.050 has the same defect in the same form.Not tried on other simulators; the input is legal IEEE 1800 and Verilator accepts it through
elaboration, so this is purely a code-generation defect.
What OS and distribution are you using?
Ubuntu 24.04 on WSL2 (Linux 6.6.87.2-microsoft-standard-WSL2), g++ 15.2.0.
May we assist you in trying to fix this in Verilator yourself?
I have a fix and will open a PR referencing this issue.
Root cause:
V3Sched::util::splitCheck()cuts a function's top level statement list on node countalone, ignoring
AstVardeclarations sitting in that list. Its ownUASSERT_OBJ(!ofuncp->varsp(), ..., "Can't split function with local variables")shows the intent,but only checks
AstCFunc::varsp(), not declarations among the statements, andlocalizeVars()puts them in
stmtsp().This is the same failure as #5987, which #5822 fixed by removing function locals from
SenExprBuilder("function locals are not safe here because we might need to split up the generatedfunction"). #6926 reintroduced them for the dynamic-trigger path, because #6859 needs the temporary
to be per-process. The two requirements conflict, so rather than removing the locals again the fix
makes
splitCheck()honour them: a sub-function boundary is only allowed where it does not separatea local declaration from a reference to it.