[#99211] Fix scheduler lost trigger - virtualization

Signed-off-by: Igor Zaworski <izaworski@antmicro.com>
This commit is contained in:
Igor Zaworski 2026-07-06 17:43:53 +02:00
parent da13029b0a
commit 4df63cbeab
7 changed files with 441 additions and 2 deletions

View File

@ -369,6 +369,7 @@ set(COMMON_SOURCES
V3Unroll.cpp
V3UnrollGen.cpp
V3VariableOrder.cpp
V3Virtual.cpp
V3Waiver.cpp
V3Width.cpp
V3WidthCommit.cpp

View File

@ -348,6 +348,7 @@ RAW_OBJS_PCH_ASTNOMT = \
V3Unknown.o \
V3Unroll.o \
V3UnrollGen.o \
V3Virtual.o \
V3Width.o \
V3WidthCommit.o \
V3WidthSel.o \

View File

@ -29,6 +29,7 @@
#include "V3AstUserAllocator.h"
#include "V3EmitCBase.h"
#include "V3Sched.h"
#include "V3Virtual.h"
#include <unordered_map>
@ -192,6 +193,8 @@ class AwaitVisitor final : public VNVisitor {
AstNodeStmt*& m_postUpdatesr; // Post updates for the trigger eval function
// Additional var sensitivities
std::map<const AstVarScope*, std::set<AstSenTree*>>& m_externalDomains;
std::unique_ptr<V3ClassesGraphWrapper>
m_classGraphp; // class graph to get possibly called functions from a virtual call
std::set<AstSenTree*> m_processDomains; // Sentrees from the current process
// Variables written by suspendable processes
std::set<AstVarScope*> m_writtenBySuspendable;
@ -307,7 +310,12 @@ class AwaitVisitor final : public VNVisitor {
void visit(AstNodeCCall* const nodep) override {
iterateChildren(nodep);
// We need to visit bodies of non-inlined functions
visitCalledCFunc(nodep->funcp());
const auto& cfuncps = m_classGraphp->getCallPossibleCFuncs(nodep);
if (cfuncps.empty()) {
visitCalledCFunc(nodep->funcp());
} else {
for (AstCFunc* const cfuncp : cfuncps) visitCalledCFunc(cfuncp);
}
}
void visit(AstCFunc* const nodep) override {
const auto& value = m_cfuncsCache(nodep);
@ -369,7 +377,8 @@ public:
: m_scopeTopp{nodep->topScopep()->scopep()}
, m_lbs{lbs}
, m_postUpdatesr{postUpdatesr}
, m_externalDomains{externalDomains} {
, m_externalDomains{externalDomains}
, m_classGraphp{V3Virtual::buildClassGraph(nodep)} {
iterate(nodep);
}
~AwaitVisitor() override {

226
src/V3Virtual.cpp Normal file
View File

@ -0,0 +1,226 @@
// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Virtual function calls - function finder
//
// Code available from: https://verilator.org
//
//*************************************************************************
//
// 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: 2003-2026 Wilson Snyder
// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
//
//*************************************************************************
//
// Builds a graph of inheritance (implements & extends)
// Allows for lookups of what AstCFunc may be called
// due to a virtual function call
//
// Current limitation: This only may be called after V3Task
// when AstNodeFTasks are substituted with AstCFuncs
//
//*************************************************************************
// TODO:
// - Probably in future this phase shall have a check for overriding
// virtual functions with static functions - currently
// there is no such check - it shall happen before V3Task
// since it moves static functions outside classes
// - This shall check for error that is currently emitted in V3LinkDot
// `Class 'Derived' implements 'Base' but is missing implementation
// for 'function' (IEEE 1800-2023 8.26)`
// Sometimes this error is emitted and it is a false positive
// since the following snipped is correct but Verilator emits an error:
//
// class Base;
// virtual task foo();
// endtask
// endclass
//
// interface class Iface;
// pure virtual task foo();
// endclass
//
// class Derived extends Base implements Iface;
// // This is legal as it is but Verilator fails
// endclass
//
// Mentioned `IEEE 1800-2023 8.26` says:
// `When an interface class is implemented by a class, the required
// implementations of interface class methods may be
// provided by inherited virtual method implementations.`
//
// Therefore, this shall probably become a full phase
// and check for those errors somewhere before V3Task
//*************************************************************************
#include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT
#include "V3Virtual.h"
VL_DEFINE_DEBUG_FUNCTIONS;
class V3ClassGraphVertex final : public V3GraphVertex {
VL_RTTI_IMPL(V3ClassGraphVertex, V3GraphVertex)
friend class ClassGraphBuilderVisitor;
enum Virtualization : uint8_t {
NON_VIRTUAL = 0, // Not a virtual member function
VIRTUAL_IMPLICIT, // virtual keyword is not next to a function definition the ancestor
// class has such virtual function therefore, it is treated as an
// virtual
VIRTUAL, // function explicitly defined as virtual with a keyword
UNDEFINED_VIRTUAL, // Function not declared in a current class however, it is inherited
// and it is virtual
// Note: non-virtual inherited function may not be described with this enum - no need for
// now as well as static functions
};
struct FuncInfo final {
AstCFunc* const m_cfuncp; // pointer to a described function
std::unordered_set<AstCFunc*>
m_cache; // set of functions that may be called due to a virtual call
Virtualization m_virtualization; // Whether function is virtual (see Virtualization)
bool m_cacheInitialized; // true when m_cache stores a valid value
FuncInfo()
: m_cfuncp{nullptr}
, m_virtualization{UNDEFINED_VIRTUAL}
, m_cacheInitialized{false} {}
FuncInfo(AstCFunc* const cfuncp, Virtualization virtualization)
: m_cfuncp{cfuncp}
, m_virtualization{virtualization}
, m_cacheInitialized{false} {}
};
std::unordered_map<std::string, FuncInfo>
m_funcsVirtualization; // Map from function names to their description
const AstClass* const m_classp; // A class of which the function is a member of
public:
explicit V3ClassGraphVertex(V3Graph* const graphp, const AstClass* const classp)
: V3GraphVertex{graphp}
, m_classp{classp} {}
~V3ClassGraphVertex() override = default;
std::string name() const override { return m_classp->name(); }
void addFunc(AstCFunc* const cfuncp) {
m_funcsVirtualization.emplace(cfuncp->name(),
FuncInfo{cfuncp, cfuncp->isVirtual()
? V3ClassGraphVertex::VIRTUAL
: V3ClassGraphVertex::NON_VIRTUAL});
}
void propagateVirt(const V3ClassGraphVertex& from) {
for (auto funcs : from.m_funcsVirtualization) {
if (funcs.second.m_virtualization == NON_VIRTUAL) continue;
auto& vertex = m_funcsVirtualization[funcs.first];
if (vertex.m_virtualization == NON_VIRTUAL) vertex.m_virtualization = VIRTUAL_IMPLICIT;
}
}
const std::unordered_set<AstCFunc*>& getCallPossibleCFuncs(const std::string& name) & {
auto it = m_funcsVirtualization.find(name);
UASSERT(it != m_funcsVirtualization.end(),
"Unexpected call - CFunc with name '" << name << "' not found");
FuncInfo& info = it->second;
if (!info.m_cacheInitialized) {
if (info.m_cfuncp) info.m_cache.insert(info.m_cfuncp);
if (info.m_virtualization != V3ClassGraphVertex::NON_VIRTUAL) {
for (V3GraphEdge& edge : outEdges()) {
V3ClassGraphVertex* const other = edge.top()->as<V3ClassGraphVertex>();
const std::unordered_set<AstCFunc*> otherCalls
= other->getCallPossibleCFuncs(name);
info.m_cache.insert(otherCalls.begin(), otherCalls.end());
}
}
info.m_cacheInitialized = true;
}
return info.m_cache;
}
};
V3ClassesGraphWrapper::V3ClassesGraphWrapper(
std::unique_ptr<V3Graph> graph,
std::unordered_map<const AstClass*, V3ClassGraphVertex*> vertexMap)
: m_graphp{std::move(graph)}
, m_vertexMap{std::move(vertexMap)} {}
V3ClassesGraphWrapper::~V3ClassesGraphWrapper()
= default; // Must be here because only this translation unit actually knows size
// of the V3ClassGraphVertex which is needed for ~unique_ptr()
const std::unordered_set<AstCFunc*>&
V3ClassesGraphWrapper::getCallPossibleCFuncs(const AstNodeCCall* const callp) const& {
// super references and AstCNew shall have AstNodeCCall::funcp() pointing to the only possible
// AstCFunc that may be called
if (callp->superReference() || VN_IS(callp, CNew)) return m_emptySet;
AstNode* nodep = callp->funcp();
while (nodep && nodep->backp()
&& (nodep->backp()->nextp() == nodep || VN_IS(nodep->backp(), Scope))) {
nodep = nodep->backp();
}
AstClass* const classp = VN_CAST(nodep->backp(), Class);
if (!classp) return m_emptySet; // Not under class return empty set
auto it = m_vertexMap.find(classp);
UASSERT_OBJ(it != m_vertexMap.end(), classp, "Class not mapped");
return it->second->getCallPossibleCFuncs(callp->funcp()->name());
}
// Build a Class graph and returns it in a V3ClassesGraphWrapper
class ClassGraphBuilderVisitor final : VNVisitor {
std::unique_ptr<V3Graph> m_graphp; // Graph of classes
std::unordered_map<const AstClass*, V3ClassGraphVertex*>
m_vertexMap; // Maps classes to their vertices
AstClass* m_classp = nullptr; // Current class
V3ClassGraphVertex* m_classVertexp = nullptr; // vertex corresponding to a current class -
// getClassVertex(m_classp) == m_classVertexp
V3ClassGraphVertex* getClassVertex(const AstClass* const classp) {
auto it = m_vertexMap.find(classp);
if (it != m_vertexMap.end()) return it->second;
return m_vertexMap.emplace(classp, new V3ClassGraphVertex{m_graphp.get(), classp})
.first->second;
}
void visit(AstCFunc* const nodep) override {
if (!m_classp || nodep->isConstructor() || nodep->isDestructor()) return;
UASSERT_OBJ(m_classVertexp, nodep,
"If function is under class m_classVertexp shall be set");
m_classVertexp->addFunc(nodep);
}
void visit(AstClassExtends* const nodep) override {
UASSERT_OBJ(m_classVertexp, nodep,
"m_classVertexp shall be set while visiting AstClassExtends");
new V3GraphEdge{m_graphp.get(),
getClassVertex(VN_AS(nodep->childDTypep(), ClassRefDType)->classp()),
m_classVertexp, 1};
}
void visit(AstNodeModule* const nodep) override {
VL_RESTORER(m_classp);
VL_RESTORER(m_classVertexp);
if ((m_classp = VN_CAST(nodep, Class))) m_classVertexp = getClassVertex(m_classp);
iterateChildren(nodep);
}
void visit(AstNode* const nodep) override { iterateChildren(nodep); }
public:
explicit ClassGraphBuilderVisitor(AstNetlist* const nodep)
: m_graphp{new V3Graph} {
iterate(nodep);
// Propagate virtualization
m_graphp->order();
for (V3GraphVertex& graphVertex : m_graphp->vertices()) {
V3ClassGraphVertex* const vertexp = graphVertex.as<V3ClassGraphVertex>();
for (V3GraphEdge& outEdge : vertexp->outEdges()) {
V3ClassGraphVertex* const successorp = outEdge.top()->as<V3ClassGraphVertex>();
successorp->propagateVirt(*vertexp);
}
}
}
~ClassGraphBuilderVisitor() override = default;
std::unique_ptr<V3ClassesGraphWrapper> takeGraph() && {
return std::unique_ptr<V3ClassesGraphWrapper>(
new V3ClassesGraphWrapper{std::move(m_graphp), std::move(m_vertexMap)});
}
};
std::unique_ptr<V3ClassesGraphWrapper> V3Virtual::buildClassGraph(AstNetlist* netlistp) {
return ClassGraphBuilderVisitor{netlistp}.takeGraph();
}

64
src/V3Virtual.h Normal file
View File

@ -0,0 +1,64 @@
// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Virtual function calls - function finder
//
// Code available from: https://verilator.org
//
//*************************************************************************
//
// 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: 2003-2026 Wilson Snyder
// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
//
//*************************************************************************
#ifndef VERILATOR_V3VIRTUAL_H_
#define VERILATOR_V3VIRTUAL_H_
#include "config_build.h"
#include "verilatedos.h"
#include "V3Ast.h"
#include "V3Graph.h"
#include <memory>
#include <unordered_map>
#include <unordered_set>
//============================================================================
class V3ClassGraphVertex;
// Graph of classes - build by V3Virtual::buildClassGraph() and used to get CFuncs that may be
// called by a AstNodeCCall (due to virtualization)
class V3ClassesGraphWrapper final {
friend class ClassGraphBuilderVisitor;
const std::unique_ptr<V3Graph> m_graphp; // graph of classes
const std::unordered_map<const AstClass*, V3ClassGraphVertex*>
m_vertexMap; // Map from AstClass to a corresponding vertex
const std::unordered_set<AstCFunc*>
m_emptySet; // Always empty set - used to return a reference to an empty set
V3ClassesGraphWrapper(std::unique_ptr<V3Graph> graph,
std::unordered_map<const AstClass*, V3ClassGraphVertex*> vertexMap);
public:
~V3ClassesGraphWrapper();
// Returns possible CFuncs that may be called due to a NodeCCall passed as a callp argument
// Warning: returned set may be empty if NodeCCall is a New call or a super call - in such
// cases only `callp->funcp()` may be called. Also, if callp is to a non-member function the
// set will be empty - post V3Task static functions are not member functions anymore.
const std::unordered_set<AstCFunc*>&
getCallPossibleCFuncs(const AstNodeCCall* const callp) const&;
};
class V3Virtual final {
public:
static std::unique_ptr<V3ClassesGraphWrapper> buildClassGraph(AstNetlist*);
};
#endif // Guard

View File

@ -0,0 +1,21 @@
#!/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')
test.compile(verilator_flags2=['--binary', '--stats', '-fno-dfg'])
test.execute()
test.file_grep(test.stats,
r'Scheduling, count of non-inlined signal writes in suspendables\s+(\d+)', 7)
test.passes()

View File

@ -0,0 +1,117 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain
// SPDX-FileCopyrightText: 2026 Antmicro
// SPDX-License-Identifier: CC0-1.0
// verilog_format: off
`define stop $stop
`define checkh(gotv,expv) do if ((gotv) !== (expv)) begin $write("%%Error: %s:%0d: got='h%x exp='h%x\n", `__FILE__,`__LINE__, (gotv), (expv)); `stop; end while(0)
// verilog_format: off
class TestBase;
virtual function int bar();
endfunction
endclass
interface class D1;
pure virtual task run();
endclass
interface class D2 implements D1;
pure virtual task a(bit x = 0);
endclass
interface class D3 implements D1;
pure virtual task b(bit x = 0);
endclass
interface class D4 implements D2, D3;
endclass
package pkg;
bit [2:0] y3;
endpackage
module t;
bit [2:0] y;
bit [2:0] z;
assign z[0] = 1'b1;
assign z[1] = !(y[0]);
assign z[2] = !(|y[1:0]);
bit [2:0] y2;
bit [2:0] z2;
assign z2[0] = 1'b1;
assign z2[1] = !(y2[0]);
assign z2[2] = !(|y2[1:0]);
import pkg::y3;
bit [2:0] z3;
assign z3[0] = 1'b1;
assign z3[1] = !(y3[0]);
assign z3[2] = !(|y3[1:0]);
bit [2:0] y4;
bit [2:0] z4;
assign z4[0] = 1'b1;
assign z4[1] = !(y4[0]);
assign z4[2] = !(|y4[1:0]);
class Foo extends TestBase implements D4;
function automatic int bar();
// verilator no_inline_task
y2 = 3'b111;
y3 = 3'b111;
return 1;
endfunction
task run();
y = 3'b111;
#1;
`checkh(z, 3'b001);
`checkh(z2, 3'b001);
`checkh(z3, 3'b001);
`checkh(z4, 3'b111);
endtask
task a(bit x = 0);
// verilator no_inline_task
y4 = ~y4;
#1;
if (!x) b(!x);
endtask
task b(bit x = 0);
// verilator no_inline_task
if (!x) a(!x);
endtask
endclass
initial begin
static Foo inst = new;
static TestBase foo = inst;
static D3 d3 = inst;
static D1 d1 = inst;
static D4 d4 = inst;
#1;
`checkh(z, 3'b111);
`checkh(z2, 3'b111);
`checkh(z3, 3'b111);
`checkh(z4, 3'b111);
void'(foo.bar());
#1;
`checkh(z, 3'b111);
`checkh(z2, 3'b001);
`checkh(z3, 3'b001);
`checkh(z4, 3'b111);
d1.run();
d4.a();
`checkh(z, 3'b001);
`checkh(z2, 3'b001);
`checkh(z3, 3'b001);
`checkh(z4, 3'b001);
d3.b();
`checkh(z, 3'b001);
`checkh(z2, 3'b001);
`checkh(z3, 3'b001);
`checkh(z4, 3'b111);
$write("*-* All Finished *-*\n");
$finish;
end
endmodule