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:
parent
3fa88ae7f3
commit
1f164d7ee0
|
|
@ -201,6 +201,7 @@ Maarten De Braekeleer
|
|||
Maciej Sobkowski
|
||||
Marcel Chang
|
||||
Marco Bartoli
|
||||
Marco Brambilla
|
||||
Marco Widmer
|
||||
Mariusz Glebocki
|
||||
Markus Krause
|
||||
|
|
|
|||
|
|
@ -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
|
||||
void splitCheck(AstCFunc* const ofuncp) {
|
||||
if (!ofuncp) return;
|
||||
|
|
@ -164,6 +198,9 @@ void splitCheck(AstCFunc* const ofuncp) {
|
|||
if (!v3Global.opt.outputSplitCFuncs() || !ofuncp->stmtsp()) 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.
|
||||
std::unordered_map<const AstVar*, AstVarScope*> argVscps;
|
||||
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
|
||||
AstNode* stmtsp = ofuncp->stmtsp()->unlinkFrBackWithNext();
|
||||
while (AstNode* const itemp = stmtsp) {
|
||||
for (size_t index = 0; AstNode* const itemp = stmtsp; ++index) {
|
||||
stmtsp = stmtsp->nextp();
|
||||
if (stmtsp) stmtsp->unlinkFrBackWithNext();
|
||||
const size_t itemSize = static_cast<size_t>(itemp->nodeCount());
|
||||
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);
|
||||
subFuncp = nullptr;
|
||||
size = itemSize;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue