Fix scheduling of virtual interface method writes

Signed-off-by: Artur Bieniek <abieniek@antmicro.com>
This commit is contained in:
Artur Bieniek 2026-06-18 23:00:59 +02:00
parent 6fbc7042a5
commit 7c3fc6601d
3 changed files with 158 additions and 6 deletions

View File

@ -39,11 +39,18 @@ namespace {
class VirtIfaceVisitor final : public VNVisitor {
private:
// STATE
using IfaceMember = std::pair<const AstIface*, std::string>;
using IfaceCallable = std::pair<AstIface*, AstCFunc*>;
// Set of (iface, member) pairs written through VIF -- defines which members need triggers
std::set<std::pair<const AstIface*, const std::string>> m_vifWrittenMembers;
std::set<IfaceMember> m_vifWrittenMembers;
// All candidate VarScopes of interface members (keyed by interface type + member name)
std::map<std::pair<const AstIface*, const std::string>, std::vector<AstVarScope*>>
m_candidateVscps;
std::map<IfaceMember, std::vector<AstVarScope*>> m_candidateVscps;
std::set<std::pair<IfaceMember, AstVarScope*>> m_seenCandidateVscps;
// VarScope index and callable worklist for VIF method-body writes.
std::map<const AstVar*, std::vector<AstVarScope*>> m_vscpsByVar;
std::vector<IfaceCallable> m_reachableIfaceCallables;
std::set<IfaceCallable> m_seenReachableIfaceCallables;
VirtIfaceTriggers m_triggers;
// METHODS
@ -70,12 +77,24 @@ private:
}
iterateChildren(nodep);
}
void visit(AstCMethodCall* nodep) override {
if (const AstIfaceRefDType* const dtypep
= VN_CAST(nodep->fromp()->dtypep()->skipRefp(), IfaceRefDType)) {
if (VL_UNCOVERABLE(!dtypep->isVirtual())) {
// Concrete interface method calls are lowered before this pass.
} else {
addReachableIfaceCallable(dtypep->ifaceViaCellp(), nodep->funcp());
}
}
iterateChildren(nodep);
}
void visit(AstVarScope* nodep) override {
// Collect candidate VarScopes. sensIfacep() is set on interface members
// accessed via any MemberSel (virtual or non-virtual).
if (const AstIface* const ifacep = nodep->varp()->sensIfacep()) {
m_candidateVscps[{ifacep, nodep->varp()->name()}].push_back(nodep);
}
AstVar* const varp = nodep->varp();
if (varp->isTemp()) return;
m_vscpsByVar[varp].push_back(nodep);
if (const AstIface* const ifacep = varp->sensIfacep()) addCandidateVscp(ifacep, nodep);
}
void visit(AstNodeProcedure* nodep) override {
// Disable lifetime optimization for variables in AlwaysPost blocks
@ -87,6 +106,19 @@ private:
}
void visit(AstNode* nodep) override { iterateChildren(nodep); }
void addCandidateVscp(const AstIface* const ifacep, AstVarScope* const vscp) {
const IfaceMember member{ifacep, vscp->varp()->name()};
if (m_seenCandidateVscps.emplace(member, vscp).second)
m_candidateVscps[member].push_back(vscp);
}
void addReachableIfaceCallable(AstIface* const ifacep, AstCFunc* const funcp) {
const IfaceCallable callable{ifacep, funcp};
if (m_seenReachableIfaceCallables.emplace(callable).second) {
m_reachableIfaceCallables.push_back(callable);
}
}
// Build final trigger list by intersecting VIF writes with candidate VarScopes
void buildTriggers() {
for (const auto& written : m_vifWrittenMembers) {
@ -103,6 +135,29 @@ public:
// CONSTRUCTORS
explicit VirtIfaceVisitor(AstNetlist* nodep) {
iterate(nodep);
for (size_t i = 0; i < m_reachableIfaceCallables.size(); ++i) {
const IfaceCallable callable = m_reachableIfaceCallables[i];
callable.second->foreach([this, callable](AstNodeExpr* const nodep) {
if (AstVarRef* const refp = VN_CAST(nodep, VarRef)) {
// Only persistent interface storage is observable through a VIF read.
UASSERT_OBJ(refp->varScopep(), refp, "No var scope");
AstVar* const varp = refp->varp();
if (!refp->access().isWriteOrRW() || varp->isFuncLocal() || varp->isTemp()
|| varp->isEvent() || !VN_IS(refp->varScopep()->scopep()->modp(), Iface)) {
return;
}
varp->sensIfacep(callable.first);
m_vifWrittenMembers.emplace(callable.first, varp->name());
const auto it = m_vscpsByVar.find(varp);
UASSERT_OBJ(it != m_vscpsByVar.end(), varp,
"No VarScope for interface member");
for (AstVarScope* const vscp : it->second)
addCandidateVscp(callable.first, vscp);
} else if (AstNodeCCall* const callp = VN_CAST(nodep, NodeCCall)) {
addReachableIfaceCallable(callable.first, callp->funcp());
}
});
}
buildTriggers();
}
~VirtIfaceVisitor() override = default;

View File

@ -0,0 +1,18 @@
#!/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=["--timing"])
test.execute()
test.passes()

View File

@ -0,0 +1,79 @@
// 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
class msg;
string context_name;
function void set_context(string name);
context_name = name;
endfunction
endclass
package helper_pkg;
int package_counter;
function void bump();
package_counter++;
endfunction
endpackage
interface intf(output logic a, output logic b);
msg m;
event e;
task go();
helper_pkg::package_counter++;
go_helper();
go_helper();
endtask
task go_helper();
// verilator no_inline_task
m.set_context("go_helper");
helper_pkg::bump();
-> e;
a <= 1;
endtask
endinterface
class driver;
virtual intf vif;
function new(virtual intf vif);
this.vif = vif;
endfunction
task go();
vif.go();
endtask
endclass
module t;
wire a;
wire b;
virtual intf vif;
intf i(a, b);
initial begin
driver d;
vif = t.i;
t.i.m = new;
d = new(t.i);
d.go();
end
always @(posedge a) begin
vif.b <= 1;
end
always @(*) begin
if (a && b) begin
$write("*-* All Finished *-*\n");
$finish;
end
end
endmodule