Fix wait fork trigger temporary split across generated functions (#7985) (#7986)

splitCheck() cuts a function's top level statement list on node count
alone, ignoring AstVar declarations in that list. localizeVars() puts the
dynamic trigger temporaries there, which a 'wait fork' reaches, so the
declaration could land in one sub-function and its references in another.
Sub-functions are emitted as separate C++ functions, so the output failed
to compile:

    error: '__Vtrigprevexpr_h5d9da2ce__0' was not declared in this scope

V3InlineCFuncs could also inline the sub-function holding the declaration
and free the AstVar while other sub-functions still referenced it, which
--debug reports as a broken link and which segfaults an -O3 build.

The existing "Can't split function with local variables" assertion only
checked AstCFunc::varsp(), not declarations among the statements.

Only allow a sub-function boundary where it does not separate a local
declaration from a reference to it. This keeps the temporaries function
local, as #6859 requires, while restoring the guarantee #5822 made that
splitting cannot orphan them.

Signed-off-by: Marco Brambilla <marco@hairyotter.com>
This commit is contained in:
Marco Brambilla 2026-08-05 09:33:58 -07:00 committed by GitHub
parent 3fa88ae7f3
commit 1f164d7ee0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 106 additions and 2 deletions

View File

@ -201,6 +201,7 @@ Maarten De Braekeleer
Maciej Sobkowski Maciej Sobkowski
Marcel Chang Marcel Chang
Marco Bartoli Marco Bartoli
Marco Brambilla
Marco Widmer Marco Widmer
Mariusz Glebocki Mariusz Glebocki
Markus Krause Markus Krause

View File

@ -157,6 +157,40 @@ void splitCheckFinishSubFunc(AstCFunc* ofuncp, AstCFunc* subFuncp,
} }
} }
// Compute, for each top level statement of 'ofuncp', whether a new sub-function may begin there.
// Sub-functions are emitted as separate C++ functions, so they cannot see each other's automatic
// storage. A function-local AstVar declared among the top level statements must therefore end up
// in the same sub-function as every reference to it, otherwise the emitted C++ refers to an
// undeclared identifier (and the AstVar can be deleted from under the still-live references).
static std::vector<bool> splitCheckBreakable(const AstCFunc* ofuncp) {
// Gather the top level statements, and where each locally declared variable is declared
std::vector<const AstNode*> stmtps;
std::unordered_map<const AstVar*, size_t> declIdx; // Local var -> index it is declared at
for (const AstNode* nodep = ofuncp->stmtsp(); nodep; nodep = nodep->nextp()) {
if (const AstVar* const varp = VN_CAST(nodep, Var)) declIdx.emplace(varp, stmtps.size());
stmtps.push_back(nodep);
}
// Find the last statement referencing each locally declared variable
std::vector<size_t> lastUse(stmtps.size());
for (size_t i = 0; i < stmtps.size(); ++i) {
lastUse[i] = i; // A declaration is live at least where it is declared
stmtps[i]->foreach([&](const AstNodeVarRef* refp) {
const auto it = declIdx.find(refp->varp()); // 'end()' if not one of our locals
if (it != declIdx.end()) lastUse[it->second] = std::max(lastUse[it->second], i);
});
}
// A break before statement 'i' is allowed only if no local declared before 'i' is still live
std::vector<bool> breakable(stmtps.size(), true);
size_t liveEnd = 0; // Last index any so far declared local is referenced at
for (size_t i = 0; i < stmtps.size(); ++i) {
breakable[i] = liveEnd < i;
if (VN_IS(stmtps[i], Var)) liveEnd = std::max(liveEnd, lastUse[i]);
}
return breakable;
}
// Split large function according to --output-split-cfuncs // Split large function according to --output-split-cfuncs
void splitCheck(AstCFunc* const ofuncp) { void splitCheck(AstCFunc* const ofuncp) {
if (!ofuncp) return; if (!ofuncp) return;
@ -164,6 +198,9 @@ void splitCheck(AstCFunc* const ofuncp) {
if (!v3Global.opt.outputSplitCFuncs() || !ofuncp->stmtsp()) return; if (!v3Global.opt.outputSplitCFuncs() || !ofuncp->stmtsp()) return;
if (ofuncp->nodeCount() < v3Global.opt.outputSplitCFuncs()) return; if (ofuncp->nodeCount() < v3Global.opt.outputSplitCFuncs()) return;
// Statement boundaries that would separate a local declaration from a reference to it
const std::vector<bool> breakable = splitCheckBreakable(ofuncp);
// Need to find the AstVarScopes for the function arguments. They should be in the same Scope. // Need to find the AstVarScopes for the function arguments. They should be in the same Scope.
std::unordered_map<const AstVar*, AstVarScope*> argVscps; std::unordered_map<const AstVar*, AstVarScope*> argVscps;
for (AstVar* argp = ofuncp->argsp(); argp; argp = VN_AS(argp->nextp(), Var)) { for (AstVar* argp = ofuncp->argsp(); argp; argp = VN_AS(argp->nextp(), Var)) {
@ -187,13 +224,13 @@ void splitCheck(AstCFunc* const ofuncp) {
// Move statements one by one to the new sub-functions // Move statements one by one to the new sub-functions
AstNode* stmtsp = ofuncp->stmtsp()->unlinkFrBackWithNext(); AstNode* stmtsp = ofuncp->stmtsp()->unlinkFrBackWithNext();
while (AstNode* const itemp = stmtsp) { for (size_t index = 0; AstNode* const itemp = stmtsp; ++index) {
stmtsp = stmtsp->nextp(); stmtsp = stmtsp->nextp();
if (stmtsp) stmtsp->unlinkFrBackWithNext(); if (stmtsp) stmtsp->unlinkFrBackWithNext();
const size_t itemSize = static_cast<size_t>(itemp->nodeCount()); const size_t itemSize = static_cast<size_t>(itemp->nodeCount());
size += itemSize; size += itemSize;
if (size > static_cast<size_t>(v3Global.opt.outputSplitCFuncs())) { if (size > static_cast<size_t>(v3Global.opt.outputSplitCFuncs()) && breakable[index]) {
if (subFuncp) splitCheckFinishSubFunc(ofuncp, subFuncp, argVscps); if (subFuncp) splitCheckFinishSubFunc(ofuncp, subFuncp, argVscps);
subFuncp = nullptr; subFuncp = nullptr;
size = itemSize; size = itemSize;

View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# 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')
# --output-split-cfuncs forces the process to be split into sub-functions
test.compile(verilator_flags2=["--binary", "--output-split-cfuncs", "20"])
# Confirm the split actually happened: the 'initial' timing process is emitted
# as function sub-parts (__Vtiming__0__0 / __Vtiming__0__1). Without the split
# this test would not exercise the bug it guards against -- a dynamic 'wait
# fork' trigger temporary separated from its uses across a sub-function boundary.
if test.vlt_all:
test.file_grep(test.obj_dir + "/V" + test.name + "_t__0.cpp", r'__Vtiming__0__1\b')
test.execute()
test.passes()

View File

@ -0,0 +1,40 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// A 'wait fork' needs a dynamic trigger temporary, which is function-local to
// the process it appears in. Splitting the process into sub-functions must not
// separate that declaration from its uses, as sub-functions are emitted as
// separate C++ functions.
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t;
logic clk = 1'b0;
int cnt = 0;
always #5 clk = ~clk;
task automatic phase(int n);
fork begin @(posedge clk); cnt += n; end join_none
wait fork;
endtask
initial begin
fork @(posedge clk); join_none
wait fork;
phase(1);
fork @(negedge clk); join_none
wait fork;
phase(2);
fork @(posedge clk); join_none
wait fork;
phase(4);
if (cnt != 7) begin
$write("%%Error: cnt=%0d exp=7\n", cnt);
$stop;
end
$write("*-* All Finished *-*\n");
$finish;
end
endmodule