Files
verilator/src/V3SchedVirtIface.cpp
T
Artur Bieniek 350158c857 Fix scheduling of virtual interface method writes (#7641)
Fix scheduling of writes in virtual interfaces, there were missing triggers (see added test).

Make V3SchedVirtIface handle writes done inside methods called through a virtual interface. The pass first records direct vif.member writes, VIF method calls, and candidate interface member VarScopes. It then walks the methods reachable from those VIF calls, writes to persistent interface variables in those method bodies are treated as VIF writes, and nested calls are followed with the same interface context. Function locals, temps, and events are ignored because they are not persistent interface storage observable through a later VIF read. Triggers are still created only from the intersection of (interface type, member name) writes and matching VarScopes, so unrelated interface variables and interfaces with no virtual access do not get extra triggers.
2026-06-24 10:51:42 +01:00

183 lines
7.8 KiB
C++

// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Create triggers necessary for scheduling across
// virtual interfaces
//
// 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
//
//*************************************************************************
// V3SchedVirtIface's Transformations:
//
// Identify interface members written through virtual interface variables (VIF writes).
// For each such member, collect all VarScopes across all instantiations of that
// interface type. Each VarScope gets its own value-change trigger in the scheduler.
//
// Also disables lifetime optimization for variables in AlwaysPost blocks that
// write through virtual interfaces.
//
//*************************************************************************
#include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT
#include "V3AstNodeExpr.h"
#include "V3Sched.h"
VL_DEFINE_DEBUG_FUNCTIONS;
namespace V3Sched {
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<IfaceMember> m_vifWrittenMembers;
// All candidate VarScopes of interface members (keyed by interface type + member name)
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
// Returns true if statement writes across a virtual interface boundary
static bool writesToVirtIface(const AstNode* const nodep) {
return nodep->exists([](const AstMemberSel* const memberSelp) {
if (!memberSelp->access().isWriteOrRW()) return false;
AstIfaceRefDType* const dtypep
= VN_CAST(memberSelp->fromp()->dtypep()->skipRefp(), IfaceRefDType);
return dtypep && dtypep->isVirtual();
});
}
// VISITORS
void visit(AstMemberSel* nodep) override {
// Detect writes through VIF: the MemberSel's fromp resolves to a virtual interface type.
// Handles both direct VIF access (vif.member) and class chain (obj.vif.member).
if (nodep->access().isWriteOrRW()) {
AstIfaceRefDType* const dtypep
= VN_CAST(nodep->fromp()->dtypep()->skipRefp(), IfaceRefDType);
if (dtypep && dtypep->isVirtual()) {
m_vifWrittenMembers.emplace(dtypep->ifacep(), nodep->varp()->name());
}
}
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).
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
// that write to virtual interface members, as VIF reads may observe them
if (VN_IS(nodep, AlwaysPost) && writesToVirtIface(nodep)) {
nodep->foreach([](AstVarRef* refp) { refp->varScopep()->optimizeLifePost(false); });
}
iterateChildren(nodep);
}
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) {
const auto it = m_candidateVscps.find(written);
if (it == m_candidateVscps.end()) continue;
for (AstVarScope* const vscp : it->second) {
const AstIface* const ifacep = written.first;
m_triggers.addTrigger(ifacep, vscp->varp(), vscp);
}
}
}
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;
// METHODS
VirtIfaceTriggers take_triggers() { return std::move(m_triggers); }
};
} //namespace
VirtIfaceTriggers makeVirtIfaceTriggers(AstNetlist* nodep) {
UINFO(2, __FUNCTION__ << ":");
VirtIfaceTriggers triggers{};
if (v3Global.hasVirtIfaces()) {
triggers = VirtIfaceVisitor{nodep}.take_triggers();
// Dump after destructor so VNDeleter runs
V3Global::dumpCheckGlobalTree("sched_vif", 0, dumpTreeEitherLevel() >= 6);
}
return triggers;
}
} //namespace V3Sched