Fix virtual interface member trigger convergence (#5116) (#7323)

This commit is contained in:
Yilou Wang 2026-04-01 22:42:42 +02:00 committed by GitHub
parent 6aa6c45c73
commit 894f6c4c58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 267 additions and 191 deletions

View File

@ -85,37 +85,28 @@ void invertAndMergeSenTreeMap(
for (const auto& pair : senTreeMap) result.emplace(pair.second, pair.first); for (const auto& pair : senTreeMap) result.emplace(pair.second, pair.first);
} }
std::vector<AstSenTree*> // Find VIF triggers that a given VarScope should be sensitive to.
findTriggeredIface(const AstVarScope* vscp, // Case 2 (non-virtual interface read): sensitive to that specific VarScope's trigger only
const VirtIfaceTriggers::IfaceMemberSensMap& vifMemberTriggered) { // Case 3 (virtual interface read): sensitive to all member triggers of the same interface type
const AstIface* ifacep; std::vector<AstSenTree*> findTriggeredIface(const AstVarScope* vscp,
if (vscp->varp()->isVirtIface()) { const VirtIfaceTriggers::VscpSensMap& vscpToSens,
// If `vscp->varp()->isVirtIface()` is true then the interface type that viface is pointing const VirtIfaceTriggers& virtIfaceTriggers) {
// to is under `VN_AS(vscp->varp()->dtypep(), IfaceRefDType)->ifacep()`
ifacep = VN_AS(vscp->varp()->dtypep(), IfaceRefDType)->ifacep();
// Virtual interface is sensitive to a different interface type than it is a virtual type
// of - this may be a valid behaviour but this function does not expects that
UASSERT_OBJ(vscp->varp()->sensIfacep() == nullptr, vscp,
"Virtual interface has an ambiguous type - "
<< vscp->varp()->sensIfacep()->prettyTypeName()
<< " != " << ifacep->prettyTypeName());
} else {
// If `vscp->varp()` is of a non-virtual interface type it has `sensIfacep()` set to
// interface it is sensitive to
ifacep = vscp->varp()->sensIfacep();
}
UASSERT_OBJ(ifacep, vscp, "Variable is not sensitive for any interface");
std::vector<AstSenTree*> result; std::vector<AstSenTree*> result;
for (const auto& memberIt : vifMemberTriggered) { if (vscp->varp()->isVirtIface()) {
// Interface member variables already identify the exact member that can // Virtual interface variable -- sensitive to all member triggers of this interface type
// change externally. Sensitizing them to every triggered member of the interface causes const AstIface* const ifacep = VN_AS(vscp->varp()->dtypep(), IfaceRefDType)->ifacep();
// false feedback paths, e.g. a block reading one signal becoming spuriously sensitive to for (const auto& entry : virtIfaceTriggers.m_triggers) {
// another signal just because both belong to the same interface. if (entry.m_ifacep == ifacep) {
if (memberIt.first.m_memberp != vscp->varp()) continue; const auto it = vscpToSens.find(entry.m_vscp);
if (memberIt.first.m_ifacep == ifacep) result.push_back(memberIt.second); if (it != vscpToSens.end()) result.push_back(it->second);
}
}
} else {
// Non-virtual interface member -- sensitive to this VarScope's trigger only
const auto it = vscpToSens.find(vscp);
if (it != vscpToSens.end()) result.push_back(it->second);
} }
// May be empty if sensIfacep() is set but no VIF write targets this member
return result; return result;
} }
@ -426,14 +417,15 @@ void createFinal(AstNetlist* netlistp, const LogicClasses& logicClasses) {
} }
//============================================================================ //============================================================================
// Helper that creates virtual interface trigger resets // Helper that creates virtual interface value-change triggers
void addVirtIfaceTriggerAssignments(const VirtIfaceTriggers& virtIfaceTriggers, void addVirtIfaceTriggerAssignments(AstNetlist* netlistp, AstCFunc* initFuncp,
uint32_t vifTriggerIndex, uint32_t vifMemberTriggerIndex, const VirtIfaceTriggers& virtIfaceTriggers,
const TriggerKit& trigKit) { uint32_t firstIndex, const TriggerKit& trigKit) {
for (const auto& p : virtIfaceTriggers.m_memberTriggers) { uint32_t index = firstIndex;
trigKit.addExtraTriggerAssignment(p.second, vifMemberTriggerIndex); for (const auto& entry : virtIfaceTriggers.m_triggers) {
++vifMemberTriggerIndex; trigKit.addValueChangeTriggerAssignment(netlistp, initFuncp, entry.m_vscp, index);
++index;
} }
} }
@ -531,12 +523,10 @@ AstNode* createInputCombLoop(AstNetlist* netlistp, AstCFunc* const initFuncp,
const uint32_t dpiExportTriggerIndex = dpiExportTriggerVscp const uint32_t dpiExportTriggerIndex = dpiExportTriggerVscp
? extraTriggers.allocate("DPI export trigger") ? extraTriggers.allocate("DPI export trigger")
: std::numeric_limits<uint32_t>::max(); : std::numeric_limits<uint32_t>::max();
const size_t firstVifTriggerIndex = extraTriggers.size(); const uint32_t firstVifTriggerIndex = extraTriggers.size();
const size_t firstVifMemberTriggerIndex = extraTriggers.size(); for (const auto& entry : virtIfaceTriggers.m_triggers) {
for (const auto& p : virtIfaceTriggers.m_memberTriggers) { extraTriggers.allocate("virtual interface member: " + entry.m_ifacep->name() + "."
const auto& item = p.first; + entry.m_memberp->name());
extraTriggers.allocate("virtual interface member: " + item.m_ifacep->name() + "."
+ item.m_memberp->name());
} }
// Gather the relevant sensitivity expressions and create the trigger kit // Gather the relevant sensitivity expressions and create the trigger kit
@ -548,8 +538,8 @@ AstNode* createInputCombLoop(AstNetlist* netlistp, AstCFunc* const initFuncp,
if (dpiExportTriggerVscp) { if (dpiExportTriggerVscp) {
trigKit.addExtraTriggerAssignment(dpiExportTriggerVscp, dpiExportTriggerIndex); trigKit.addExtraTriggerAssignment(dpiExportTriggerVscp, dpiExportTriggerIndex);
} }
addVirtIfaceTriggerAssignments(virtIfaceTriggers, firstVifTriggerIndex, addVirtIfaceTriggerAssignments(netlistp, initFuncp, virtIfaceTriggers, firstVifTriggerIndex,
firstVifMemberTriggerIndex, trigKit); trigKit);
// Remap sensitivities // Remap sensitivities
remapSensitivities(logic, trigKit.mapVec()); remapSensitivities(logic, trigKit.mapVec());
@ -567,21 +557,21 @@ AstNode* createInputCombLoop(AstNetlist* netlistp, AstCFunc* const initFuncp,
= dpiExportTriggerVscp = dpiExportTriggerVscp
? trigKit.newExtraTriggerSenTree(trigKit.vscp(), dpiExportTriggerIndex) ? trigKit.newExtraTriggerSenTree(trigKit.vscp(), dpiExportTriggerIndex)
: nullptr; : nullptr;
const auto& vifMemberTriggeredIco = virtIfaceTriggers.makeMemberToSensMap( const auto& vifVscpToSensIco
trigKit, firstVifMemberTriggerIndex, trigKit.vscp()); = virtIfaceTriggers.makeVscpToSensMap(trigKit, firstVifTriggerIndex, trigKit.vscp());
// Create and Order the body function // Create and Order the body function
AstCFunc* const icoFuncp = V3Order::order( AstCFunc* const icoFuncp = V3Order::order(
netlistp, {&logic}, trigToSen, "ico", false, false, netlistp, {&logic}, trigToSen, "ico", false, false,
[=](const AstVarScope* vscp, std::vector<AstSenTree*>& out) { [&](const AstVarScope* vscp, std::vector<AstSenTree*>& out) {
AstVar* const varp = vscp->varp(); AstVar* const varp = vscp->varp();
if (varp->isPrimaryInish() || varp->isSigUserRWPublic()) { if (varp->isPrimaryInish() || varp->isSigUserRWPublic()) {
out.push_back(inputChanged); out.push_back(inputChanged);
} }
if (varp->isWrittenByDpi()) out.push_back(dpiExportTriggered); if (varp->isWrittenByDpi()) out.push_back(dpiExportTriggered);
if (vscp->varp()->isVirtIface()) { if (vscp->varp()->sensIfacep() || vscp->varp()->isVirtIface()) {
std::vector<AstSenTree*> ifaceTriggered const auto& ifaceTriggered
= findTriggeredIface(vscp, vifMemberTriggeredIco); = findTriggeredIface(vscp, vifVscpToSensIco, virtIfaceTriggers);
out.insert(out.end(), ifaceTriggered.begin(), ifaceTriggered.end()); out.insert(out.end(), ifaceTriggered.begin(), ifaceTriggered.end());
} }
}); });
@ -844,13 +834,14 @@ void createEval(AstNetlist* netlistp, //
//============================================================================ //============================================================================
// Helper that builds virtual interface trigger sentrees // Helper that builds virtual interface trigger sentrees
VirtIfaceTriggers::IfaceMemberSensMap VirtIfaceTriggers::VscpSensMap VirtIfaceTriggers::makeVscpToSensMap(const TriggerKit& trigKit,
VirtIfaceTriggers::makeMemberToSensMap(const TriggerKit& trigKit, uint32_t vifTriggerIndex, uint32_t firstIndex,
AstVarScope* trigVscp) const { AstVarScope* trigVscp) const {
IfaceMemberSensMap map; VscpSensMap map;
for (const auto& p : m_memberTriggers) { uint32_t index = firstIndex;
map.emplace(p.first, trigKit.newExtraTriggerSenTree(trigVscp, vifTriggerIndex)); for (const auto& entry : m_triggers) {
++vifTriggerIndex; map.emplace(entry.m_vscp, trigKit.newExtraTriggerSenTree(trigVscp, index));
++index;
} }
return map; return map;
} }
@ -981,11 +972,9 @@ void schedule(AstNetlist* netlistp) {
? extraTriggers.allocate("DPI export trigger") ? extraTriggers.allocate("DPI export trigger")
: std::numeric_limits<uint32_t>::max(); : std::numeric_limits<uint32_t>::max();
const uint32_t firstVifTriggerIndex = extraTriggers.size(); const uint32_t firstVifTriggerIndex = extraTriggers.size();
const uint32_t firstVifMemberTriggerIndex = extraTriggers.size(); for (const auto& entry : virtIfaceTriggers.m_triggers) {
for (const auto& p : virtIfaceTriggers.m_memberTriggers) { extraTriggers.allocate("virtual interface member: " + entry.m_ifacep->name() + "."
const auto& item = p.first; + entry.m_memberp->name());
extraTriggers.allocate("virtual interface member: " + item.m_ifacep->name() + "."
+ item.m_memberp->name());
} }
const auto& preTreeps = getSenTreesUsedBy({&logicRegions.m_pre}); const auto& preTreeps = getSenTreesUsedBy({&logicRegions.m_pre});
@ -1004,8 +993,8 @@ void schedule(AstNetlist* netlistp) {
if (dpiExportTriggerVscp) { if (dpiExportTriggerVscp) {
trigKit.addExtraTriggerAssignment(dpiExportTriggerVscp, dpiExportTriggerIndex); trigKit.addExtraTriggerAssignment(dpiExportTriggerVscp, dpiExportTriggerIndex);
} }
addVirtIfaceTriggerAssignments(virtIfaceTriggers, firstVifTriggerIndex, addVirtIfaceTriggerAssignments(netlistp, staticp, virtIfaceTriggers, firstVifTriggerIndex,
firstVifMemberTriggerIndex, trigKit); trigKit);
if (v3Global.opt.stats()) V3Stats::statsStage("sched-create-triggers"); if (v3Global.opt.stats()) V3Stats::statsStage("sched-create-triggers");
// Note: Experiments so far show that running the Act (or Ico) regions on // Note: Experiments so far show that running the Act (or Ico) regions on
@ -1034,8 +1023,8 @@ void schedule(AstNetlist* netlistp) {
? trigKit.newExtraTriggerSenTree(trigKit.vscp(), dpiExportTriggerIndex) ? trigKit.newExtraTriggerSenTree(trigKit.vscp(), dpiExportTriggerIndex)
: nullptr; : nullptr;
const auto& vifMemberTriggeredAct = virtIfaceTriggers.makeMemberToSensMap( const auto& vifVscpToSensAct
trigKit, firstVifMemberTriggerIndex, trigKit.vscp()); = virtIfaceTriggers.makeVscpToSensMap(trigKit, firstVifTriggerIndex, trigKit.vscp());
AstCFunc* const actFuncp = V3Order::order( AstCFunc* const actFuncp = V3Order::order(
netlistp, {&logicRegions.m_pre, &logicRegions.m_act, &logicReplicas.m_act}, trigToSenAct, netlistp, {&logicRegions.m_pre, &logicRegions.m_act, &logicReplicas.m_act}, trigToSenAct,
@ -1043,9 +1032,9 @@ void schedule(AstNetlist* netlistp) {
auto it = actTimingDomains.find(vscp); auto it = actTimingDomains.find(vscp);
if (it != actTimingDomains.end()) out = it->second; if (it != actTimingDomains.end()) out = it->second;
if (vscp->varp()->isWrittenByDpi()) out.push_back(dpiExportTriggeredAct); if (vscp->varp()->isWrittenByDpi()) out.push_back(dpiExportTriggeredAct);
if (vscp->varp()->isVirtIface()) { if (vscp->varp()->sensIfacep() || vscp->varp()->isVirtIface()) {
std::vector<AstSenTree*> ifaceTriggered const auto& ifaceTriggered
= findTriggeredIface(vscp, vifMemberTriggeredAct); = findTriggeredIface(vscp, vifVscpToSensAct, virtIfaceTriggers);
out.insert(out.end(), ifaceTriggered.begin(), ifaceTriggered.end()); out.insert(out.end(), ifaceTriggered.begin(), ifaceTriggered.end());
} }
}); });
@ -1071,8 +1060,8 @@ void schedule(AstNetlist* netlistp) {
= dpiExportTriggerVscp = dpiExportTriggerVscp
? trigKit.newExtraTriggerSenTree(trigVscp, dpiExportTriggerIndex) ? trigKit.newExtraTriggerSenTree(trigVscp, dpiExportTriggerIndex)
: nullptr; : nullptr;
const auto& vifMemberTriggered const auto& vifVscpToSens
= virtIfaceTriggers.makeMemberToSensMap(trigKit, firstVifMemberTriggerIndex, trigVscp); = virtIfaceTriggers.makeVscpToSensMap(trigKit, firstVifTriggerIndex, trigVscp);
const auto& timingDomains = timingKit.remapDomains(trigMap); const auto& timingDomains = timingKit.remapDomains(trigMap);
AstCFunc* const funcp = V3Order::order( AstCFunc* const funcp = V3Order::order(
@ -1081,11 +1070,9 @@ void schedule(AstNetlist* netlistp) {
auto it = timingDomains.find(vscp); auto it = timingDomains.find(vscp);
if (it != timingDomains.end()) out = it->second; if (it != timingDomains.end()) out = it->second;
if (vscp->varp()->isWrittenByDpi()) out.push_back(dpiExportTriggered); if (vscp->varp()->isWrittenByDpi()) out.push_back(dpiExportTriggered);
// Sometimes virtual interfaces mix with non-virtual one so, here both have to be
// detected - look `t_virtual_interface_nba_assign`
if (vscp->varp()->sensIfacep() || vscp->varp()->isVirtIface()) { if (vscp->varp()->sensIfacep() || vscp->varp()->isVirtIface()) {
std::vector<AstSenTree*> ifaceTriggered const auto& ifaceTriggered
= findTriggeredIface(vscp, vifMemberTriggered); = findTriggeredIface(vscp, vifVscpToSens, virtIfaceTriggers);
out.insert(out.end(), ifaceTriggered.begin(), ifaceTriggered.end()); out.insert(out.end(), ifaceTriggered.begin(), ifaceTriggered.end());
} }
}); });

View File

@ -312,6 +312,11 @@ public:
// Set then extra trigger bit at 'index' to the value of 'vscp', then set 'vscp' to 0 // Set then extra trigger bit at 'index' to the value of 'vscp', then set 'vscp' to 0
void addExtraTriggerAssignment(AstVarScope* vscp, uint32_t index, bool clear = true) const; void addExtraTriggerAssignment(AstVarScope* vscp, uint32_t index, bool clear = true) const;
// Set trigger bit at 'index' when vscp's value changes from previous evaluation.
// Creates a prev variable: trigger[bit] = (vscp != prev); prev = vscp
void addValueChangeTriggerAssignment(AstNetlist* netlistp, AstCFunc* initFuncp,
AstVarScope* vscp, uint32_t index) const;
}; };
// Everything needed for combining timing with static scheduling. // Everything needed for combining timing with static scheduling.
@ -348,37 +353,27 @@ public:
}; };
class VirtIfaceTriggers final { class VirtIfaceTriggers final {
// Represents a specific member in a virtual interface public:
struct IfaceMember final { // One entry per VarScope of an interface member that may be written through a virtual
// interface. Each gets its own extra trigger bit with value-change detection.
struct TriggerEntry final {
const AstIface* m_ifacep; // Interface type const AstIface* m_ifacep; // Interface type
const AstVar* m_memberp; // Member variable const AstVar* m_memberp; // Member variable in the interface definition
AstVarScope* m_vscp; // VarScope to monitor
// TODO: sorting by pointer is non-deterministic
bool operator<(const IfaceMember& other) const {
if (m_ifacep != other.m_ifacep) return m_ifacep < other.m_ifacep;
return m_memberp < other.m_memberp;
}
}; };
public: std::vector<TriggerEntry> m_triggers;
using IfaceMemberSensMap = std::map<IfaceMember, AstSenTree*>;
std::vector<std::pair<IfaceMember, AstVarScope*>> m_memberTriggers; void addTrigger(const AstIface* ifacep, const AstVar* memberp, AstVarScope* vscp) {
m_triggers.push_back(TriggerEntry{ifacep, memberp, vscp});
void addMemberTrigger(const AstIface* ifacep, const AstVar* memberp, AstVarScope* vscp) {
m_memberTriggers.emplace_back(IfaceMember{ifacep, memberp}, vscp);
} }
AstVarScope* findMemberTrigger(const AstIface* ifacep, const AstVar* memberp) const { // Maps each triggered VarScope to its AstSenTree. findTriggeredIface() uses this
for (const auto& pair : m_memberTriggers) { // to look up triggers -- by specific VarScope for non-virtual reads, or by iterating
const IfaceMember& item = pair.first; // all entries of the same interface type for virtual interface reads.
if (item.m_ifacep == ifacep && item.m_memberp == memberp) return pair.second; using VscpSensMap = std::map<const AstVarScope*, AstSenTree*>;
} VscpSensMap makeVscpToSensMap(const TriggerKit& trigKit, uint32_t firstIndex,
return nullptr; AstVarScope* trigVscp) const;
}
IfaceMemberSensMap makeMemberToSensMap(const TriggerKit& trigKit, uint32_t vifTriggerIndex,
AstVarScope* trigVscp) const;
VL_UNCOPYABLE(VirtIfaceTriggers); VL_UNCOPYABLE(VirtIfaceTriggers);
VirtIfaceTriggers() = default; VirtIfaceTriggers() = default;

View File

@ -426,6 +426,91 @@ void TriggerKit::addExtraTriggerAssignment(AstVarScope* vscp, uint32_t index, bo
m_compVecp->addStmtsp(setp); m_compVecp->addStmtsp(setp);
} }
// Value-change detection for a single interface member VarScope written through a VIF.
// Creates: trigger[bit] = (vscp != prev); prev = vscp;
void TriggerKit::addValueChangeTriggerAssignment(AstNetlist* netlistp, AstCFunc* initFuncp,
AstVarScope* instVscp, uint32_t index) const {
index += m_nSenseWords * WORD_SIZE;
const uint32_t wordIndex = index / WORD_SIZE;
const uint32_t bitIndex = index % WORD_SIZE;
AstScope* const scopeTopp = netlistp->topScopep()->scopep();
FileLine* const flp = netlistp->fileline();
AstNodeDType* const dtypep = instVscp->dtypep()->skipRefp();
// Reject types that do not support value-change comparison
if (const AstBasicDType* const bdtypep = VN_CAST(dtypep, BasicDType)) {
if (bdtypep->isEvent() || bdtypep->isString()
|| bdtypep->keyword() == VBasicDTypeKwd::CHANDLE) {
instVscp->v3warn(E_UNSUPPORTED, "Unsupported: virtual interface trigger on "
<< dtypep->prettyDTypeNameQ() << " type");
return;
}
} else if (!VN_IS(dtypep, PackArrayDType) && !VN_IS(dtypep, UnpackArrayDType)
&& !VN_IS(dtypep, NodeUOrStructDType) && !VN_IS(dtypep, ClassRefDType)) {
instVscp->v3warn(E_UNSUPPORTED, "Unsupported: virtual interface trigger on "
<< dtypep->prettyDTypeNameQ() << " type");
return;
}
const auto rdInst = [flp](AstVarScope* vp) { return new AstVarRef{flp, vp, VAccess::READ}; };
const auto wrPrev = [flp](AstVarScope* vp) { return new AstVarRef{flp, vp, VAccess::WRITE}; };
const auto rdPrev = [flp](AstVarScope* vp) { return new AstVarRef{flp, vp, VAccess::READ}; };
// Create prev variable
const std::string prevName = "__Vtrigprevvif_" + m_name + "_"
+ instVscp->scopep()->nameDotless() + "__"
+ instVscp->varp()->name();
AstVarScope* const prevVscp = scopeTopp->createTemp(prevName, instVscp->dtypep());
// Initialize prev = inst
if (VN_IS(dtypep, UnpackArrayDType)) {
AstCMethodHard* const cmhp = new AstCMethodHard{
flp, wrPrev(prevVscp), VCMethod::UNPACKED_ASSIGN, rdInst(instVscp)};
cmhp->dtypeSetVoid();
initFuncp->addStmtsp(cmhp->makeStmt());
} else {
initFuncp->addStmtsp(new AstAssign{flp, wrPrev(prevVscp), rdInst(instVscp)});
}
// Build comparison: inst != prev
AstNodeExpr* neqp;
if (VN_IS(dtypep, UnpackArrayDType)) {
AstCMethodHard* const cmhp
= new AstCMethodHard{flp, rdPrev(prevVscp), VCMethod::UNPACKED_NEQ, rdInst(instVscp)};
cmhp->dtypeSetBit();
neqp = cmhp;
} else {
neqp = new AstNeq{flp, rdInst(instVscp), rdPrev(prevVscp)};
}
// Build post-update: prev = inst
AstNode* updp;
if (VN_IS(dtypep, UnpackArrayDType)) {
AstCMethodHard* const cmhp = new AstCMethodHard{
flp, wrPrev(prevVscp), VCMethod::UNPACKED_ASSIGN, rdInst(instVscp)};
cmhp->dtypeSetVoid();
updp = cmhp->makeStmt();
} else {
updp = new AstAssign{flp, wrPrev(prevVscp), rdInst(instVscp)};
}
// Set trigger bit
AstVarRef* const refp = new AstVarRef{flp, m_vscp, VAccess::WRITE};
AstNodeExpr* const wordp = new AstArraySel{flp, refp, static_cast<int>(wordIndex)};
AstNodeExpr* const trigLhsp = new AstSel{flp, wordp, static_cast<int>(bitIndex), 1};
AstNode* const setp = new AstAssign{flp, trigLhsp, neqp};
// Chain: set trigger bit -> update prev
setp->addNext(updp);
// Prepend before existing statements
if (AstNode* const nodep = m_compVecp->stmtsp()) {
setp->addNext(setp, nodep->unlinkFrBackWithNext());
}
m_compVecp->addStmtsp(setp);
}
TriggerKit::TriggerKit(const std::string& name, bool slow, uint32_t nSenseWords, TriggerKit::TriggerKit(const std::string& name, bool slow, uint32_t nSenseWords,
uint32_t nExtraWords, uint32_t nPreWords, uint32_t nExtraWords, uint32_t nPreWords,
std::unordered_map<VNRef<const AstSenItem>, size_t> senItem2TrigIdx, std::unordered_map<VNRef<const AstSenItem>, size_t> senItem2TrigIdx,

View File

@ -16,13 +16,12 @@
//************************************************************************* //*************************************************************************
// V3SchedVirtIface's Transformations: // V3SchedVirtIface's Transformations:
// //
// Each interface type written to via virtual interface, or written to normally but read via // Identify interface members written through virtual interface variables (VIF writes).
// virtual interface: // For each such member, collect all VarScopes across all instantiations of that
// Create a trigger var for it // interface type. Each VarScope gets its own value-change trigger in the scheduler.
// Each statement: //
// If it writes to a virtual interface, or to a variable read via virtual interface: // Also disables lifetime optimization for variables in AlwaysPost blocks that
// Set the corresponding trigger to 1 // write through virtual interfaces.
// If the write is done by an AssignDly, the trigger is also set by AssignDly
// //
//************************************************************************* //*************************************************************************
@ -30,7 +29,6 @@
#include "V3AstNodeExpr.h" #include "V3AstNodeExpr.h"
#include "V3Sched.h" #include "V3Sched.h"
#include "V3UniqueNames.h"
VL_DEFINE_DEBUG_FUNCTIONS; VL_DEFINE_DEBUG_FUNCTIONS;
@ -40,103 +38,72 @@ namespace {
class VirtIfaceVisitor final : public VNVisitor { class VirtIfaceVisitor final : public VNVisitor {
private: private:
// NODE STATE
// AstVarRef::user1() -> bool. Whether it has been visited
// AstMemberSel::user1() -> bool. Whether it has been visited
const VNUser1InUse m_user1InUse;
// TYPES
using OnWriteToVirtIface = std::function<void(AstVarRef*, AstIface*)>;
using OnWriteToVirtIfaceMember
= std::function<void(AstVarRef*, AstIface*, const std::string&)>;
// STATE // STATE
AstNetlist* const m_netlistp; // Root node // Set of (iface, member) pairs written through VIF -- defines which members need triggers
V3UniqueNames m_vifTriggerNames{"__VvifTrigger"}; // Unique names for virt iface std::set<std::pair<const AstIface*, const std::string>> m_vifWrittenMembers;
// triggers // All candidate VarScopes of interface members (keyed by interface type + member name)
VirtIfaceTriggers m_triggers; // Interfaces and corresponding trigger vars std::map<std::pair<const AstIface*, const std::string>, std::vector<AstVarScope*>>
m_candidateVscps;
VirtIfaceTriggers m_triggers;
// METHODS // METHODS
// For each write across a virtual interface boundary // Returns true if statement writes across a virtual interface boundary
// Returns true if there is a write across a virtual interface boundary
static bool writesToVirtIface(const AstNode* const nodep) { static bool writesToVirtIface(const AstNode* const nodep) {
return nodep->exists([](const AstVarRef* const refp) { return nodep->exists([](const AstMemberSel* const memberSelp) {
if (!refp->access().isWriteOrRW()) return false; if (!memberSelp->access().isWriteOrRW()) return false;
AstIfaceRefDType* const dtypep = VN_CAST(refp->varp()->dtypep(), IfaceRefDType); AstIfaceRefDType* const dtypep
const bool writesToVirtIfaceMember = VN_CAST(memberSelp->fromp()->dtypep()->skipRefp(), IfaceRefDType);
= (dtypep && dtypep->isVirtual() && VN_IS(refp->firstAbovep(), MemberSel)); return dtypep && dtypep->isVirtual();
const bool writesToIfaceSensVar = refp->varp()->isVirtIface();
return writesToVirtIfaceMember || writesToIfaceSensVar;
}); });
} }
// Create trigger reference for a specific interface member
AstVarRef* createVirtIfaceMemberTriggerRefp(FileLine* const flp, AstIface* ifacep,
const AstVar* memberVarp) {
// Check if we already have a trigger for this specific member
AstVarScope* existingTrigger = m_triggers.findMemberTrigger(ifacep, memberVarp);
if (!existingTrigger) {
AstScope* const scopeTopp = m_netlistp->topScopep()->scopep();
// Create a unique name for this member trigger
const std::string triggerName
= m_vifTriggerNames.get(ifacep) + "_Vtrigm_" + memberVarp->name();
AstVarScope* const vscp = scopeTopp->createTemp(triggerName, 1);
m_triggers.addMemberTrigger(ifacep, memberVarp, vscp);
existingTrigger = vscp;
}
return new AstVarRef{flp, existingTrigger, VAccess::WRITE};
}
template <typename T>
void handleIface(T nodep) {
static_assert(std::is_same<typename std::remove_cv<T>::type,
typename std::add_pointer<AstVarRef>::type>::value
|| std::is_same<typename std::remove_cv<T>::type,
typename std::add_pointer<AstMemberSel>::type>::value,
"Node has to be of AstVarRef* or AstMemberSel* type");
if (nodep->access().isReadOnly()) return;
if (nodep->user1SetOnce()) return;
AstIface* ifacep = nullptr;
AstVar* memberVarp = nullptr;
if (nodep->varp()->isVirtIface()) {
if (AstMemberSel* const memberSelp = VN_CAST(nodep->firstAbovep(), MemberSel)) {
ifacep = VN_AS(nodep->varp()->dtypep(), IfaceRefDType)->ifacep();
memberVarp = memberSelp->varp();
}
} else if ((ifacep = nodep->varp()->sensIfacep())) {
memberVarp = nodep->varp();
}
if (ifacep && memberVarp) {
FileLine* const flp = nodep->fileline();
VNRelinker relinker;
nodep->unlinkFrBack(&relinker);
relinker.relink(new AstExprStmt{
flp,
new AstAssign{flp, createVirtIfaceMemberTriggerRefp(flp, ifacep, memberVarp),
new AstConst{flp, AstConst::BitTrue{}}},
nodep});
}
}
// VISITORS // 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(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);
}
}
void visit(AstNodeProcedure* nodep) override { void visit(AstNodeProcedure* nodep) override {
// Not sure if needed, but be paranoid to match previous behavior as didn't optimize // Disable lifetime optimization for variables in AlwaysPost blocks
// before .. // that write to virtual interface members, as VIF reads may observe them
if (VN_IS(nodep, AlwaysPost) && writesToVirtIface(nodep)) { if (VN_IS(nodep, AlwaysPost) && writesToVirtIface(nodep)) {
nodep->foreach([](AstVarRef* refp) { refp->varScopep()->optimizeLifePost(false); }); nodep->foreach([](AstVarRef* refp) { refp->varScopep()->optimizeLifePost(false); });
} }
iterateChildren(nodep); iterateChildren(nodep);
} }
void visit(AstMemberSel* const nodep) override { handleIface(nodep); }
void visit(AstVarRef* const nodep) override { handleIface(nodep); }
void visit(AstNode* nodep) override { iterateChildren(nodep); } void visit(AstNode* nodep) override { iterateChildren(nodep); }
// 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: public:
// CONSTRUCTORS // CONSTRUCTORS
explicit VirtIfaceVisitor(AstNetlist* nodep) explicit VirtIfaceVisitor(AstNetlist* nodep) {
: m_netlistp{nodep} {
iterate(nodep); iterate(nodep);
buildTriggers();
} }
~VirtIfaceVisitor() override = default; ~VirtIfaceVisitor() override = default;
@ -151,7 +118,7 @@ VirtIfaceTriggers makeVirtIfaceTriggers(AstNetlist* nodep) {
VirtIfaceTriggers triggers{}; VirtIfaceTriggers triggers{};
if (v3Global.hasVirtIfaces()) { if (v3Global.hasVirtIfaces()) {
triggers = VirtIfaceVisitor{nodep}.take_triggers(); triggers = VirtIfaceVisitor{nodep}.take_triggers();
// Dump afer destructor so VNDeleter runs // Dump after destructor so VNDeleter runs
V3Global::dumpCheckGlobalTree("sched_vif", 0, dumpTreeEitherLevel() >= 6); V3Global::dumpCheckGlobalTree("sched_vif", 0, dumpTreeEitherLevel() >= 6);
} }
return triggers; return triggers;

View File

@ -4499,6 +4499,11 @@ class WidthVisitor final : public VNVisitor {
if (AstNodeFTask* const ftaskp if (AstNodeFTask* const ftaskp
= VN_CAST(m_memberMap.findMember(ifacep, nodep->name()), NodeFTask)) { = VN_CAST(m_memberMap.findMember(ifacep, nodep->name()), NodeFTask)) {
UINFO(5, __FUNCTION__ << "AstNodeFTask" << nodep); UINFO(5, __FUNCTION__ << "AstNodeFTask" << nodep);
if (adtypep->isVirtual()) {
for (AstNode* itemp = ifacep->stmtsp(); itemp; itemp = itemp->nextp()) {
if (AstVar* const mvarp = VN_CAST(itemp, Var)) { mvarp->sensIfacep(ifacep); }
}
}
userIterate(ftaskp, nullptr); userIterate(ftaskp, nullptr);
if (ftaskp->isStatic()) { if (ftaskp->isStatic()) {
AstArg* const argsp = nodep->argsp(); AstArg* const argsp = nodep->argsp();

View File

@ -9,8 +9,6 @@
import vltest_bootstrap import vltest_bootstrap
test.skip("Test is broken, see #6613")
test.scenarios('simulator') test.scenarios('simulator')
test.compile(verilator_flags2=['--binary']) test.compile(verilator_flags2=['--binary'])

View File

@ -9,8 +9,6 @@
import vltest_bootstrap import vltest_bootstrap
test.skip("Test is broken, see #6613")
test.scenarios('simulator') test.scenarios('simulator')
test.compile(verilator_flags2=['--binary']) test.compile(verilator_flags2=['--binary'])

View File

@ -0,0 +1,6 @@
%Error-UNSUPPORTED: t/t_virtual_interface_trigger_unsup.v:8:10: Unsupported: virtual interface trigger on 'string' type
: ... note: In instance 't.sif'
8 | string s;
| ^
... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest
%Error: Exiting due to

View File

@ -0,0 +1,16 @@
#!/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('vlt')
test.compile(verilator_flags2=['--binary'], fails=True, expect_filename=test.golden_filename)
test.passes()

View File

@ -0,0 +1,19 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 PlanV GmbH
// SPDX-License-Identifier: CC0-1.0
interface str_if;
string s;
endinterface
module t;
str_if sif();
virtual str_if vif = sif;
initial begin
vif.s = "hello";
$finish;
end
endmodule