This commit is contained in:
Nick Brereton 2026-03-03 09:07:13 -05:00 committed by GitHub
commit df278b7008
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1280 additions and 125 deletions

View File

@ -211,4 +211,13 @@ AstVarXRef::AstVarXRef(FileLine* fl, AstVar* varp, const string& dotted, const V
AstStmtExpr* AstNodeExpr::makeStmt() { return new AstStmtExpr{fileline(), this}; }
// Walk up the AST via backp() to find the containing AstNodeModule.
// Returns nullptr if not found.
inline AstNodeModule* findParentModule(AstNode* nodep) {
for (AstNode* curp = nodep; curp; curp = curp->backp()) {
if (AstNodeModule* const modp = VN_CAST(curp, NodeModule)) return modp;
}
return nullptr;
}
#endif // Guard

View File

@ -1946,6 +1946,8 @@ class AstVar final : public AstNode {
bool m_ignorePostWrite : 1; // Ignore writes in 'Post' blocks during ordering
bool m_ignoreSchedWrite : 1; // Ignore writes in scheduling (for special optimizations)
bool m_dfgMultidriven : 1; // Singal is multidriven, used by DFG to avoid repeat processing
bool m_dfgTriLowered : 1; // Signal/temporary introduced by tristate lowering
bool m_dfgAllowMultidriveTri : 1; // Allow DFG MULTIDRIVEN warning for intentional tri nets
bool m_globalConstrained : 1; // Global constraint per IEEE 1800-2023 18.5.8
bool m_isStdRandomizeArg : 1; // Argument variable created for std::randomize (__Varg*)
void init() {
@ -2001,6 +2003,8 @@ class AstVar final : public AstNode {
m_ignorePostWrite = false;
m_ignoreSchedWrite = false;
m_dfgMultidriven = false;
m_dfgTriLowered = false;
m_dfgAllowMultidriveTri = false;
m_globalConstrained = false;
m_isStdRandomizeArg = false;
}
@ -2174,6 +2178,10 @@ public:
void setIgnoreSchedWrite() { m_ignoreSchedWrite = true; }
bool dfgMultidriven() const { return m_dfgMultidriven; }
void setDfgMultidriven() { m_dfgMultidriven = true; }
bool dfgTriLowered() const { return m_dfgTriLowered; }
void setDfgTriLowered() { m_dfgTriLowered = true; }
bool dfgAllowMultidriveTri() const { return m_dfgAllowMultidriveTri; }
void setDfgAllowMultidriveTri() { m_dfgAllowMultidriveTri = true; }
void globalConstrained(bool flag) { m_globalConstrained = flag; }
bool globalConstrained() const { return m_globalConstrained; }
bool isStdRandomizeArg() const { return m_isStdRandomizeArg; }

View File

@ -662,6 +662,39 @@ class AstToDfgSynthesize final {
return drivers;
}
// Returns true if the driver cone contains any variable introduced by
// tristate lowering. Used to distinguish intentional tristate contributor
// overlap from accidental multidrive.
static bool containsTriLoweredVar(DfgVertex* rootp) {
std::vector<const DfgVertex*> stack;
std::vector<const DfgVertex*> visited;
stack.emplace_back(rootp);
while (!stack.empty()) {
const DfgVertex* const vtxp = stack.back();
stack.pop_back();
if (std::find(visited.begin(), visited.end(), vtxp) != visited.end()) continue;
visited.emplace_back(vtxp);
if (const DfgVertexVar* const varp = vtxp->cast<DfgVertexVar>()) {
AstVar* const astVarp = [&]() -> AstVar* {
if VL_CONSTEXPR_CXX17 (T_Scoped) {
return reinterpret_cast<AstVarScope*>(varp->nodep())->varp();
} else {
return reinterpret_cast<AstVar*>(varp->nodep());
}
}();
if (astVarp->dfgTriLowered()) return true;
}
vtxp->foreachSource([&](const DfgVertex& src) {
stack.emplace_back(&src);
return false;
});
}
return false;
}
// Returns true if the driver is a direct variable forward (no logic).
static bool isDirectVarDriver(const DfgVertex* vtxp) { return vtxp->is<DfgVertexVar>(); }
// Gather all synthesized drivers of an unresolved variable
static std::vector<Driver> gatherDriversUnresolved(DfgUnresolved* vtxp) {
std::vector<Driver> drivers;
@ -822,6 +855,17 @@ class AstToDfgSynthesize final {
// Loop index often abused, so suppress
if (getAstVar(varp)->isUsedLoopIdx()) continue;
// Tristate lowering can intentionally create overlapping contributors.
// Keep the signal marked multidriven for DFG fallback, but suppress
// warning only when both overlapping driver cones look tri-lowered.
if (getAstVar(varp)->dfgAllowMultidriveTri()) {
const bool iTri = containsTriLoweredVar(iD.m_vtxp);
const bool jTri = containsTriLoweredVar(jD.m_vtxp);
const bool triPair = iTri && jTri;
const bool triAndBridge = (iTri && isDirectVarDriver(jD.m_vtxp))
|| (jTri && isDirectVarDriver(iD.m_vtxp));
if (triPair || triAndBridge) continue;
}
// Warn the user now
const std::string lo = std::to_string(jD.m_lo);

View File

@ -411,9 +411,9 @@ class TristateVisitor final : public TristateBaseVisitor {
// TYPES
struct RefStrength final {
AstVarRef* m_varrefp;
AstNodeVarRef* m_varrefp;
VStrength m_strength;
RefStrength(AstVarRef* varrefp, VStrength strength)
RefStrength(AstNodeVarRef* varrefp, VStrength strength)
: m_varrefp{varrefp}
, m_strength{strength} {}
};
@ -441,6 +441,8 @@ class TristateVisitor final : public TristateBaseVisitor {
// Used only on LHS of assignment
const AstNode* m_logicp = nullptr; // Current logic being built
TristateGraph m_tgraph; // Logic graph
// Map: interface AstVar* -> list of per-module (enVarp, outVarp) contribution pairs
std::map<AstVar*, std::vector<std::pair<AstVar*, AstVar*>>> m_ifaceContribs;
// STATS
VDouble0 m_statTriSigs; // stat tracking
@ -469,9 +471,32 @@ class TristateVisitor final : public TristateBaseVisitor {
AstConst* const newp = new AstConst{nodep->fileline(), num};
return newp;
}
// Create AstVarXRef with inlinedDots copied from a source xref
AstVarXRef* newVarXRef(FileLine* fl, AstVar* varp, const string& dotted, VAccess access,
const string& inlinedDots) {
AstVarXRef* const xrefp = new AstVarXRef{fl, varp, dotted, access};
xrefp->inlinedDots(inlinedDots);
return xrefp;
}
// Check if a module accesses an interface through a modport, by looking up
// the first component of the dotted path among the module's interface port vars.
// No symbol table is available at V3Tristate time, so linear scan is required.
AstModport* findModportForDotted(AstNodeModule* modp, const string& dottedPath) {
if (dottedPath.empty()) return nullptr;
const string firstComp = dottedPath.substr(0, dottedPath.find('.'));
for (AstNode* stmtp = modp->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
AstVar* const ivarp = VN_CAST(stmtp, Var);
if (!ivarp || ivarp->name() != firstComp) continue;
const AstIfaceRefDType* const ifaceDtp
= VN_CAST(ivarp->dtypep()->skipRefp(), IfaceRefDType);
if (ifaceDtp && ifaceDtp->modportp()) return ifaceDtp->modportp();
break;
}
return nullptr;
}
AstNodeExpr* getEnp(AstNode* nodep) {
if (nodep->user1p()) {
if (AstVarRef* const refp = VN_CAST(nodep, VarRef)) {
if (AstNodeVarRef* const refp = VN_CAST(nodep, NodeVarRef)) {
if (refp->varp()->isIO()) {
// When reading a tri-state port, we can always use the value
// because such port will have resolution logic in upper module.
@ -486,6 +511,16 @@ class TristateVisitor final : public TristateBaseVisitor {
// Otherwise return the previous output enable
return VN_AS(nodep->user1p(), NodeExpr);
}
// Add a ModportVarRef for varp to modportp if not already present
void addModportVarRefIfMissing(AstModport* modportp, AstVar* varp, VDirection dir) {
for (AstNode* mrp = modportp->varsp(); mrp; mrp = mrp->nextp()) {
AstModportVarRef* const mvrp = VN_CAST(mrp, ModportVarRef);
if (mvrp && mvrp->varp() == varp) return; // Already present
}
AstModportVarRef* const mvrp = new AstModportVarRef{varp->fileline(), varp->name(), dir};
mvrp->varp(varp);
modportp->addVarsp(mvrp);
}
AstVar* getCreateEnVarp(AstVar* invarp, bool isTop) {
// Return the master __en for the specified input variable
if (!invarp->user1p()) {
@ -494,7 +529,27 @@ class TristateVisitor final : public TristateBaseVisitor {
invarp->name() + "__en", invarp};
// Inherited VDirection::INPUT
UINFO(9, " newenv " << newp);
modAddStmtp(invarp, newp);
// If the variable belongs to a different module (e.g. interface),
// create __en in that module so VarXRefs with the same dotted path can reach it.
AstNodeModule* const ownerModp = findParentModule(invarp);
if (ownerModp && ownerModp != m_modp) {
ownerModp->addStmtsp(newp);
// Add __en to any modports that reference the original var,
// so VarXRefs through modport paths can resolve it.
for (AstNode* stmtp = ownerModp->stmtsp(); stmtp; stmtp = stmtp->nextp()) {
AstModport* const modportp = VN_CAST(stmtp, Modport);
if (!modportp) continue;
for (AstNode* mrp = modportp->varsp(); mrp; mrp = mrp->nextp()) {
AstModportVarRef* const mvrp = VN_CAST(mrp, ModportVarRef);
if (mvrp && mvrp->varp() == invarp) {
addModportVarRefIfMissing(modportp, newp, VDirection::INPUT);
break;
}
}
}
} else {
modAddStmtp(invarp, newp);
}
invarp->user1p(newp); // find envar given invarp
}
return VN_AS(invarp->user1p(), Var);
@ -511,6 +566,10 @@ class TristateVisitor final : public TristateBaseVisitor {
if (AstVarRef* const varrefp = VN_CAST(nodep, VarRef)) {
return new AstVarRef{varrefp->fileline(), getCreateEnVarp(varrefp->varp(), false),
VAccess::READ};
} else if (AstVarXRef* const xrefp = VN_CAST(nodep, VarXRef)) {
AstVar* const enVarp = getCreateEnVarp(xrefp->varp(), false);
return newVarXRef(xrefp->fileline(), enVarp, xrefp->dotted(), VAccess::READ,
xrefp->inlinedDots());
} else if (AstConst* const constp = VN_CAST(nodep, Const)) {
return getNonZConstp(constp);
} else if (AstExtend* const extendp = VN_CAST(nodep, Extend)) {
@ -561,7 +620,7 @@ class TristateVisitor final : public TristateBaseVisitor {
return newp;
}
void mapInsertLhsVarRef(AstVarRef* nodep) {
void mapInsertLhsVarRef(AstNodeVarRef* nodep) {
UINFO(9, " mapInsertLhsVarRef " << nodep);
AstVar* const key = nodep->varp();
const auto pair = m_lhsmap.emplace(key, nullptr);
@ -636,14 +695,62 @@ class TristateVisitor final : public TristateBaseVisitor {
// Now go through the lhs driver map and generate the output
// enable logic for any tristates.
// Note there might not be any drivers.
for (auto varp : vars) { // Use vector instead of m_lhsmap iteration for node stability
const auto it = m_lhsmap.find(varp);
for (AstVar* varp : vars) { // Use vector instead of m_lhsmap iteration for stability
const std::map<AstVar*, RefStrengthVec*>::iterator it = m_lhsmap.find(varp);
if (it == m_lhsmap.end()) continue;
AstVar* const invarp = it->first;
RefStrengthVec* refsp = it->second;
// Figure out if this var needs tristate expanded.
if (m_tgraph.isTristate(invarp)) {
insertTristatesSignal(nodep, invarp, refsp);
// Check if the var is owned by a different module (cross-module reference).
// For interface vars this is expected; for regular modules it's unsupported.
AstNodeModule* const ownerModp = findParentModule(invarp);
const bool isCrossModule = ownerModp && ownerModp != nodep && !invarp->isIO();
const bool isIfaceTri = isCrossModule && VN_IS(ownerModp, Iface);
if (isCrossModule && !isIfaceTri) {
// Hierarchical writes to non-interface module tri wires are unsupported
for (RefStrength& rs : *refsp) {
if (VN_IS(rs.m_varrefp, VarXRef)) {
rs.m_varrefp->v3warn(
E_UNSUPPORTED,
"Unsupported tristate construct: hierarchical driver"
" of non-interface tri signal: "
<< invarp->prettyNameQ());
break;
}
}
} else if (isIfaceTri) {
// Interface tristate vars: drivers from different interface instances
// (different VarXRef dotted paths) must be processed separately.
// E.g. io_ifc.d and io_ifc_local.d both target the same AstVar d in
// the ifc interface, but each instance needs its own contribution slot.
struct PartitionInfo final {
RefStrengthVec refs;
string inlinedDots;
};
std::map<string, PartitionInfo> partitions;
for (RefStrength& rs : *refsp) {
if (AstVarXRef* const xrefp = VN_CAST(rs.m_varrefp, VarXRef)) {
PartitionInfo& pi = partitions[xrefp->dotted()];
pi.refs.push_back(rs);
if (pi.inlinedDots.empty()) { pi.inlinedDots = xrefp->inlinedDots(); }
} else {
partitions[""].refs.push_back(rs);
}
}
for (auto& kv : partitions) {
insertTristatesSignal(nodep, invarp, &kv.second.refs, true, kv.first,
kv.second.inlinedDots,
findModportForDotted(nodep, kv.first));
}
} else if (VN_IS(nodep, Iface)) {
// Local driver in an interface module - use contribution mechanism
// so it can be combined with any external drivers later
insertTristatesSignal(nodep, invarp, refsp, true, "", "", nullptr);
} else {
insertTristatesSignal(nodep, invarp, refsp, false, "", "", nullptr);
}
} else {
UINFO(8, " NO TRISTATE ON:" << invarp);
}
@ -663,20 +770,35 @@ class TristateVisitor final : public TristateBaseVisitor {
AstNodeExpr* enp = nullptr;
for (auto it = beginStrength; it != endStrength; it++) {
AstVarRef* refp = it->m_varrefp;
AstNodeVarRef* refp = it->m_varrefp;
// create the new lhs driver for this var
AstVar* const newLhsp = new AstVar{varp->fileline(), VVarType::MODULETEMP,
varp->name() + "__out" + cvtToStr(m_unique),
varp}; // 2-state ok; sep enable
newLhsp->setDfgTriLowered();
UINFO(9, " newout " << newLhsp);
nodep->addStmtsp(newLhsp);
refp->varp(newLhsp);
// When retargeting a VarXRef to a local __out var, the dotted path
// becomes inconsistent. Replace the VarXRef with a local VarRef.
if (VN_IS(refp, VarXRef)) {
AstVarRef* const localRefp
= new AstVarRef{refp->fileline(), newLhsp, VAccess::WRITE};
localRefp->user1p(refp->user1p());
refp->user1p(nullptr);
refp->replaceWith(localRefp);
VL_DO_DANGLING(pushDeletep(refp), refp);
refp = localRefp;
it->m_varrefp = localRefp;
} else {
refp->varp(newLhsp);
}
// create a new var for this drivers enable signal
AstVar* const newEnLhsp
= new AstVar{varp->fileline(), VVarType::MODULETEMP,
varp->name() + "__en" + cvtToStr(m_unique++), envarp}; // 2-state ok
newEnLhsp->setDfgTriLowered();
UINFO(9, " newenlhsp " << newEnLhsp);
nodep->addStmtsp(newEnLhsp);
@ -708,7 +830,12 @@ class TristateVisitor final : public TristateBaseVisitor {
nodep->addStmtsp(new AstAlways{enAssp});
}
void insertTristatesSignal(AstNodeModule* nodep, AstVar* const invarp, RefStrengthVec* refsp) {
// isIfaceTri: true when the var is a tristate in an interface module (local or external).
// ifaceDottedPath/ifaceInlinedDots/ifaceModportp are non-empty only for external
// (cross-module) drivers; empty for local drivers within the interface itself.
void insertTristatesSignal(AstNodeModule* nodep, AstVar* const invarp, RefStrengthVec* refsp,
bool isIfaceTri, const string& ifaceDottedPath,
const string& ifaceInlinedDots, AstModport* ifaceModportp) {
UINFO(8, " TRISTATE EXPANDING:" << invarp);
++m_statTriSigs;
m_tgraph.didProcess(invarp);
@ -721,29 +848,31 @@ class TristateVisitor final : public TristateBaseVisitor {
// Or if this is a top-level inout, do tristate expand if requested
// by pinsInoutEnables(). The resolution will be done outside of
// verilator.
// Interface tristate vars skip port conversion (they use contribution vars instead).
AstVar* envarp = nullptr;
AstVar* outvarp = nullptr; // __out
AstVar* lhsp = invarp; // Variable to assign drive-value to (<in> or __out)
bool isTopInout
= (invarp->direction() == VDirection::INOUT) && invarp->isIO() && nodep->isTop();
if ((v3Global.opt.pinsInoutEnables() && isTopInout)
|| (!nodep->isTop() && invarp->isIO())) {
// This var becomes an input
invarp->varType2In(); // convert existing port to type input
// Create an output port (__out)
outvarp = getCreateOutVarp(invarp, isTopInout);
outvarp->varType2Out();
lhsp = outvarp; // Must assign to __out, not to normal input signal
UINFO(9, " TRISTATE propagates up with " << lhsp);
// Create an output enable port (__en)
// May already be created if have foo === 1'bz somewhere
envarp = getCreateEnVarp(invarp, isTopInout); // direction to be set in visit(AstPin*)
//
outvarp->user1p(envarp);
m_varAux(outvarp).pullp = m_varAux(invarp).pullp; // AstPull* propagation
if (m_varAux(invarp).pullp) UINFO(9, "propagate pull to " << outvarp);
} else if (invarp->user1p()) {
envarp = VN_AS(invarp->user1p(), Var); // From CASEEQ, foo === 1'bz
const bool isTopInout = !isIfaceTri && (invarp->direction() == VDirection::INOUT)
&& invarp->isIO() && nodep->isTop();
if (!isIfaceTri) {
if ((v3Global.opt.pinsInoutEnables() && isTopInout)
|| (!nodep->isTop() && invarp->isIO())) {
// This var becomes an input
invarp->varType2In(); // convert existing port to type input
// Create an output port (__out)
outvarp = getCreateOutVarp(invarp, isTopInout);
outvarp->varType2Out();
lhsp = outvarp; // Must assign to __out, not to normal input signal
UINFO(9, " TRISTATE propagates up with " << lhsp);
// Create an output enable port (__en)
// May already be created if have foo === 1'bz somewhere
envarp = getCreateEnVarp(invarp, isTopInout); // dir set in visit(AstPin*)
outvarp->user1p(envarp);
m_varAux(outvarp).pullp = m_varAux(invarp).pullp; // AstPull* propagation
if (m_varAux(invarp).pullp) UINFO(9, "propagate pull to " << outvarp);
} else if (invarp->user1p()) {
envarp = VN_AS(invarp->user1p(), Var); // From CASEEQ, foo === 1'bz
}
}
AstNodeExpr* orp = nullptr;
@ -752,25 +881,38 @@ class TristateVisitor final : public TristateBaseVisitor {
std::sort(refsp->begin(), refsp->end(),
[](RefStrength a, RefStrength b) { return a.m_strength > b.m_strength; });
auto beginStrength = refsp->begin();
RefStrengthVec::iterator beginStrength = refsp->begin();
while (beginStrength != refsp->end()) {
auto endStrength = beginStrength + 1;
RefStrengthVec::iterator endStrength = beginStrength + 1;
while (endStrength != refsp->end()
&& endStrength->m_strength == beginStrength->m_strength)
endStrength++;
FileLine* const fl = beginStrength->m_varrefp->fileline();
const string strengthVarName = lhsp->name() + "__" + beginStrength->m_strength.ascii();
// For interface tristate vars, uniquify strength var names by including the
// interface instance path, since multiple interface instances may have
// identically-named tristate signals (e.g. io_ifc.d and io_ifc_mc.d both
// create d__strong in the driving module)
string strengthPrefix;
if (isIfaceTri && !ifaceDottedPath.empty()) {
strengthPrefix = ifaceDottedPath;
std::replace(strengthPrefix.begin(), strengthPrefix.end(), '.', '_');
strengthPrefix += "__";
}
const string strengthVarName
= strengthPrefix + lhsp->name() + "__" + beginStrength->m_strength.ascii();
// var__strength variable
AstVar* const varStrengthp = new AstVar{fl, VVarType::MODULETEMP, strengthVarName,
invarp}; // 2-state ok; sep enable;
varStrengthp->setDfgTriLowered();
UINFO(9, " newstrength " << varStrengthp);
nodep->addStmtsp(varStrengthp);
// var__strength__en variable
AstVar* const enVarStrengthp = new AstVar{
fl, VVarType::MODULETEMP, strengthVarName + "__en", invarp}; // 2-state ok;
enVarStrengthp->setDfgTriLowered();
UINFO(9, " newenstrength " << enVarStrengthp);
nodep->addStmtsp(enVarStrengthp);
@ -796,11 +938,74 @@ class TristateVisitor final : public TristateBaseVisitor {
beginStrength = endStrength;
}
// For interface tristate vars, store per-module contributions for later combining.
// The interface module owns the contribution vars; resolution happens in
// combineIfaceContribs() after all modules are processed.
if (isIfaceTri) {
// This net intentionally has multiple contributors; DFG should not emit
// MULTIDRIVEN warnings for this lowered tristate pattern.
invarp->setDfgAllowMultidriveTri();
AstNodeModule* const ifaceModp = findParentModule(invarp);
UASSERT_OBJ(ifaceModp, invarp, "Interface tristate var has no parent module");
const int contribIdx = static_cast<int>(m_ifaceContribs[invarp].size());
FileLine* const fl = invarp->fileline();
// Create contribution vars in the interface module
AstVar* const contribOutp = new AstVar{
fl, VVarType::MODULETEMP, invarp->name() + "__out" + cvtToStr(contribIdx), invarp};
contribOutp->setDfgTriLowered();
UINFO(9, " iface contribOut " << contribOutp);
ifaceModp->addStmtsp(contribOutp);
AstVar* const contribEnp = new AstVar{
fl, VVarType::MODULETEMP, invarp->name() + "__en" + cvtToStr(contribIdx), invarp};
contribEnp->setDfgTriLowered();
UINFO(9, " iface contribEn " << contribEnp);
ifaceModp->addStmtsp(contribEnp);
// If the driving module accesses the interface through a modport,
// add the new contribution vars to the modport so VarXRefs
// can be resolved by V3LinkDot through the modport path.
if (ifaceModportp) {
UINFO(9, " adding to modport " << ifaceModportp->name());
addModportVarRefIfMissing(ifaceModportp, contribOutp, VDirection::OUTPUT);
addModportVarRefIfMissing(ifaceModportp, contribEnp, VDirection::OUTPUT);
}
// Assign drive value and enable to contribution vars.
// External drivers use VarXRef; local drivers use VarRef.
{
AstNodeVarRef* const lhsp
= ifaceDottedPath.empty() ? static_cast<AstNodeVarRef*>(
new AstVarRef{fl, contribOutp, VAccess::WRITE})
: static_cast<AstNodeVarRef*>(
newVarXRef(fl, contribOutp, ifaceDottedPath,
VAccess::WRITE, ifaceInlinedDots));
AstAssignW* const assp = new AstAssignW{fl, lhsp, orp};
assp->user2Or(U2_BOTH);
nodep->addStmtsp(new AstAlways{assp});
}
{
AstNodeVarRef* const lhsp
= ifaceDottedPath.empty() ? static_cast<AstNodeVarRef*>(
new AstVarRef{fl, contribEnp, VAccess::WRITE})
: static_cast<AstNodeVarRef*>(
newVarXRef(fl, contribEnp, ifaceDottedPath,
VAccess::WRITE, ifaceInlinedDots));
AstAssignW* const assp = new AstAssignW{fl, lhsp, enp};
assp->user2Or(U2_BOTH);
nodep->addStmtsp(new AstAlways{assp});
}
m_ifaceContribs[invarp].push_back({contribEnp, contribOutp});
return;
}
if (!outvarp) {
// This is the final pre-forced resolution of the tristate, so we apply
// the pull direction to any undriven pins.
const AstPull* const pullp = m_varAux(lhsp).pullp;
bool pull1 = pullp && pullp->direction() == 1; // Else default is down
const bool pull1 = pullp && pullp->direction() == 1; // Else default is down
AstNodeExpr* undrivenp;
if (envarp) {
@ -1753,7 +1958,7 @@ class TristateVisitor final : public TristateBaseVisitor {
}
}
void visit(AstVarRef* nodep) override {
void handleNodeVarRef(AstNodeVarRef* nodep) {
UINFO(9, dbgState() << nodep);
if (m_graphing) {
if (nodep->access().isWriteOrRW()) associateLogic(nodep, nodep->varp());
@ -1789,12 +1994,21 @@ class TristateVisitor final : public TristateBaseVisitor {
&& m_tgraph.feedsTri(nodep)) {
// Then propagate the enable from the original variable
UINFO(9, " Ref-to-tri " << nodep);
FileLine* const fl = nodep->fileline();
AstVar* const enVarp = getCreateEnVarp(nodep->varp(), false);
nodep->user1p(new AstVarRef{nodep->fileline(), enVarp, VAccess::READ});
if (AstVarXRef* const xrefp = VN_CAST(nodep, VarXRef)) {
nodep->user1p(newVarXRef(fl, enVarp, xrefp->dotted(), VAccess::READ,
xrefp->inlinedDots()));
} else {
nodep->user1p(new AstVarRef{fl, enVarp, VAccess::READ});
}
}
}
}
void visit(AstVarRef* nodep) override { handleNodeVarRef(nodep); }
void visit(AstVarXRef* nodep) override { handleNodeVarRef(nodep); }
void visit(AstVar* nodep) override {
iterateChildren(nodep);
UINFO(9, dbgState() << nodep);
@ -1897,12 +2111,79 @@ class TristateVisitor final : public TristateBaseVisitor {
checkUnhandled(nodep);
}
void combineIfaceContribs() {
// After all modules have been processed, combine per-module contributions
// for each interface tristate signal into final resolution logic.
// Key is the canonical AstVar in the interface module (shared across instances).
for (std::pair<AstVar* const, std::vector<std::pair<AstVar*, AstVar*>>>& kv :
m_ifaceContribs) {
AstVar* const invarp = kv.first;
std::vector<std::pair<AstVar*, AstVar*>>& contribs = kv.second;
AstNodeModule* const ifaceModp = findParentModule(invarp);
UASSERT_OBJ(ifaceModp, invarp, "Interface tristate var has no parent module");
FileLine* const fl = invarp->fileline();
UINFO(8, " IFACE COMBINE " << contribs.size() << " contribs for " << invarp);
// Build resolution: d = (out0 & en0) | (out1 & en1) | ... | (~combined_en & pull)
AstNodeExpr* orp = nullptr;
AstNodeExpr* enp = nullptr;
for (std::pair<AstVar*, AstVar*>& contrib : contribs) {
AstVar* const contribEnp = contrib.first;
AstVar* const contribOutp = contrib.second;
AstNodeExpr* const outRefp = new AstVarRef{fl, contribOutp, VAccess::READ};
AstNodeExpr* const enRefp = new AstVarRef{fl, contribEnp, VAccess::READ};
AstNodeExpr* const andp = new AstAnd{fl, outRefp, enRefp};
orp = (!orp) ? andp : new AstOr{fl, orp, andp};
AstNodeExpr* const enRef2p = new AstVarRef{fl, contribEnp, VAccess::READ};
enp = (!enp) ? enRef2p : new AstOr{fl, enp, enRef2p};
}
// Create or get __en var for this interface signal
AstVar* envarp = VN_CAST(invarp->user1p(), Var);
if (!envarp) {
envarp = new AstVar{fl, VVarType::MODULETEMP, invarp->name() + "__en", invarp};
envarp->setDfgTriLowered();
ifaceModp->addStmtsp(envarp);
invarp->user1p(envarp);
}
// Assign combined enable
AstAssignW* const enAssp
= new AstAssignW{fl, new AstVarRef{fl, envarp, VAccess::WRITE}, enp};
UINFOTREE(9, enAssp, "", "iface-enAssp");
ifaceModp->addStmtsp(new AstAlways{enAssp});
// Pull resolution
const AstPull* const pullp = m_varAux(invarp).pullp;
const bool pull1 = pullp && pullp->direction() == 1; // Else default is down
AstNodeExpr* undrivenp = new AstNot{fl, new AstVarRef{fl, envarp, VAccess::READ}};
undrivenp = new AstAnd{fl, undrivenp, newAllZerosOrOnes(invarp, pull1)};
orp = new AstOr{fl, orp, undrivenp};
// Assign final value to invarp
AstAssignW* const assp
= new AstAssignW{fl, new AstVarRef{fl, invarp, VAccess::WRITE}, orp};
assp->user2Or(U2_BOTH);
UINFOTREE(9, assp, "", "iface-lhsp-eqn");
ifaceModp->addStmtsp(new AstAlways{assp});
}
}
public:
// CONSTRUCTORS
explicit TristateVisitor(AstNetlist* netlistp) {
m_tgraph.clearAndCheck();
iterateChildrenBackwardsConst(netlistp);
// Combine interface tristate contributions after all modules processed
combineIfaceContribs();
#ifdef VL_LEAK_CHECKS
// It's a bit chaotic up there
std::vector<AstNode*> unusedRootps;

View File

@ -5,88 +5,111 @@
// SPDX-License-Identifier: CC0-1.0
interface counter_if;
logic [3:0] value;
logic reset;
modport counter_mp (input reset, output value);
modport core_mp (output reset, input value);
logic [3:0] value;
logic reset;
modport counter_mp (input reset, output value);
modport core_mp (output reset, input value);
endinterface
// Check can have inst module before top module
module counter_ansi
(
input clkm,
counter_if c_data,
input logic [3:0] i_value
);
module counter_ansi (
input clkm,
counter_if c_data,
input logic [3:0] i_value
);
always @ (posedge clkm) begin
c_data.value <= c_data.reset ? i_value : c_data.value + 1;
end
always @ (posedge clkm) begin
c_data.value <= c_data.reset ? i_value : c_data.value + 1;
end
endmodule : counter_ansi
// Issue 3466: inout inside interface modport
interface inout_if;
wire we;
wire d;
modport ctrl (output we, inout d);
modport prph (input we, inout d);
endinterface
module inout_mod(inout_if.prph io_if);
assign io_if.d = io_if.we ? 1'b1 : 1'bz;
endmodule
module inout_mod_wrap(input we, inout d);
inout_if io_if();
assign io_if.we = we;
assign io_if.d = d;
inout_mod prph (.*);
endmodule
module t (/*AUTOARG*/
// Inputs
clk
);
// Inputs
clk
);
input clk;
integer cyc=1;
input clk;
integer cyc=1;
counter_if c1_data();
counter_if c2_data();
counter_if c3_data();
counter_if c4_data();
counter_if c1_data();
counter_if c2_data();
counter_if c3_data();
counter_if c4_data();
counter_ansi c1 (.clkm(clk),
.c_data(c1_data.counter_mp),
.i_value(4'h1));
counter_ansi c1 (.clkm(clk),
.c_data(c1_data.counter_mp),
.i_value(4'h1));
`ifdef VERILATOR counter_ansi `else counter_nansi `endif
/**/ c2 (.clkm(clk),
.c_data(c2_data.counter_mp),
.i_value(4'h2));
counter_ansi_m c3 (.clkm(clk),
.c_data(c3_data),
.i_value(4'h3));
/**/ c2 (.clkm(clk),
.c_data(c2_data.counter_mp),
.i_value(4'h2));
counter_ansi_m c3 (.clkm(clk),
.c_data(c3_data),
.i_value(4'h3));
`ifdef VERILATOR counter_ansi_m `else counter_nansi_m `endif
/**/ c4 (.clkm(clk),
.c_data(c4_data),
.i_value(4'h4));
/**/ c4 (.clkm(clk),
.c_data(c4_data),
.i_value(4'h4));
initial begin
c1_data.value = 4'h4;
c2_data.value = 4'h5;
c3_data.value = 4'h6;
c4_data.value = 4'h7;
end
logic inout_we;
tri inout_d;
inout_mod_wrap inout_u (.we(inout_we), .d(inout_d));
always @ (posedge clk) begin
cyc <= cyc + 1;
if (cyc<2) begin
c1_data.reset <= 1;
c2_data.reset <= 1;
c3_data.reset <= 1;
c4_data.reset <= 1;
end
if (cyc==2) begin
c1_data.reset <= 0;
c2_data.reset <= 0;
c3_data.reset <= 0;
c4_data.reset <= 0;
end
if (cyc==20) begin
$write("[%0t] cyc%0d: c1 %0x %0x c2 %0x %0x c3 %0x %0x c4 %0x %0x\n", $time, cyc,
c1_data.value, c1_data.reset,
c2_data.value, c2_data.reset,
c3_data.value, c3_data.reset,
c4_data.value, c4_data.reset);
if (c1_data.value != 2) $stop;
if (c2_data.value != 3) $stop;
if (c3_data.value != 4) $stop;
if (c4_data.value != 5) $stop;
$write("*-* All Finished *-*\n");
$finish;
end
end
initial begin
c1_data.value = 4'h4;
c2_data.value = 4'h5;
c3_data.value = 4'h6;
c4_data.value = 4'h7;
inout_we = 1'b0;
end
always @ (posedge clk) begin
cyc <= cyc + 1;
if (cyc<2) begin
c1_data.reset <= 1;
c2_data.reset <= 1;
c3_data.reset <= 1;
c4_data.reset <= 1;
end
if (cyc==2) begin
c1_data.reset <= 0;
c2_data.reset <= 0;
c3_data.reset <= 0;
c4_data.reset <= 0;
end
if (cyc==20) begin
$write("[%0t] cyc%0d: c1 %0x %0x c2 %0x %0x c3 %0x %0x c4 %0x %0x\n", $time, cyc,
c1_data.value, c1_data.reset,
c2_data.value, c2_data.reset,
c3_data.value, c3_data.reset,
c4_data.value, c4_data.reset);
if (c1_data.value != 2) $stop;
if (c2_data.value != 3) $stop;
if (c3_data.value != 4) $stop;
if (c4_data.value != 5) $stop;
$write("*-* All Finished *-*\n");
$finish;
end
end
endmodule
`ifndef VERILATOR
@ -94,26 +117,26 @@ endmodule
module counter_nansi
(clkm, c_data, i_value);
input clkm;
counter_if c_data;
input logic [3:0] i_value;
input clkm;
counter_if c_data;
input logic [3:0] i_value;
always @ (posedge clkm) begin
c_data.value <= c_data.reset ? i_value : c_data.value + 1;
end
always @ (posedge clkm) begin
c_data.value <= c_data.reset ? i_value : c_data.value + 1;
end
endmodule : counter_nansi
`endif
module counter_ansi_m
(
input clkm,
counter_if.counter_mp c_data,
input logic [3:0] i_value
);
(
input clkm,
counter_if.counter_mp c_data,
input logic [3:0] i_value
);
always @ (posedge clkm) begin
c_data.value <= c_data.reset ? i_value : c_data.value + 1;
end
always @ (posedge clkm) begin
c_data.value <= c_data.reset ? i_value : c_data.value + 1;
end
endmodule : counter_ansi_m
`ifndef VERILATOR
@ -121,12 +144,12 @@ endmodule : counter_ansi_m
module counter_nansi_m
(clkm, c_data, i_value);
input clkm;
counter_if.counter_mp c_data;
input logic [3:0] i_value;
input clkm;
counter_if.counter_mp c_data;
input logic [3:0] i_value;
always @ (posedge clkm) begin
c_data.value <= c_data.reset ? i_value : c_data.value + 1;
end
always @ (posedge clkm) begin
c_data.value <= c_data.reset ? i_value : c_data.value + 1;
end
endmodule : counter_nansi_m
`endif

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(timing_loop=True, verilator_flags2=['--timing'])
test.execute()
test.passes()

View File

@ -0,0 +1,258 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// 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
// 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: on
// verilator lint_off MULTIDRIVEN
interface ifc;
wire we0, we2;
tri [15:0] d;
endinterface
interface ifc_multi;
wire we0, wea, web;
tri [15:0] d;
endinterface
module bot (
ifc io_ifc
);
assign io_ifc.d = io_ifc.we2 ? 16'hd2 : 16'hzzzz;
endmodule
module passthru (
ifc io_ifc
);
bot u_bot (.*);
endmodule
module bot_a (
ifc_multi io_ifc
);
assign io_ifc.d = io_ifc.wea ? 16'hd2 : 16'hzzzz;
endmodule
module bot_b (
ifc_multi io_ifc
);
assign io_ifc.d = io_ifc.web ? 16'hd2 : 16'hzzzz;
endmodule
module passthru_multi_c (
ifc_multi io_ifc
);
bot_a u_bot_a (.*);
bot_b u_bot_b (.*);
endmodule
module passthru_deep (
ifc io_ifc
);
passthru u_inner (.*);
endmodule
module t;
ifc io_ifc ();
ifc io_ifc_local ();
ifc io_ifc_b0 ();
ifc io_ifc_b1 ();
ifc_multi io_ifc_mc ();
ifc io_arr [1:0]();
ifc io_ifc_deep ();
// Test top assignment
assign io_ifc.d = io_ifc.we0 ? 16'hd0 : 16'hzzzz;
assign io_ifc_local.d = io_ifc_local.we0 ? 16'hd0 : 16'hzzzz;
assign io_ifc_mc.d = io_ifc_mc.we0 ? 16'hd0 : 16'hzzzz;
logic we0, we2;
logic we2_b0, we2_b1;
logic we0_mc, wea_mc, web_mc;
assign io_ifc.we0 = we0;
assign io_ifc.we2 = we2;
assign io_ifc_local.we0 = we0;
assign io_ifc_local.we2 = 1'b0;
assign io_ifc_b0.we0 = 1'b0;
assign io_ifc_b0.we2 = we2_b0;
assign io_ifc_b1.we0 = 1'b0;
assign io_ifc_b1.we2 = we2_b1;
assign io_ifc_mc.we0 = we0_mc;
assign io_ifc_mc.wea = wea_mc;
assign io_ifc_mc.web = web_mc;
// Interface array signals
logic we2_arr0, we2_arr1;
assign io_arr[0].we0 = 1'b0;
assign io_arr[0].we2 = we2_arr0;
assign io_arr[1].we0 = 1'b0;
assign io_arr[1].we2 = we2_arr1;
// Deep nesting signals
logic we0_deep, we2_deep;
assign io_ifc_deep.we0 = we0_deep;
assign io_ifc_deep.we2 = we2_deep;
assign io_ifc_deep.d = io_ifc_deep.we0 ? 16'hd0 : 16'hzzzz;
passthru u_passthru (.*);
passthru u_passthru_b0 (.io_ifc(io_ifc_b0));
passthru u_passthru_b1 (.io_ifc(io_ifc_b1));
passthru_multi_c u_passthru_mc (.io_ifc(io_ifc_mc));
passthru u_passthru_arr0 (.io_ifc(io_arr[0]));
passthru u_passthru_arr1 (.io_ifc(io_arr[1]));
passthru_deep u_passthru_deep (.io_ifc(io_ifc_deep));
initial begin
#1;
we0 = 1'b0;
we2 = 1'b0;
we2_b0 = 1'b0;
we2_b1 = 1'b0;
we0_mc = 1'b0;
wea_mc = 1'b0;
web_mc = 1'b0;
we2_arr0 = 1'b0;
we2_arr1 = 1'b0;
we0_deep = 1'b0;
we2_deep = 1'b0;
#1;
`checkh(io_ifc.d, 16'hzzzz);
`checkh(io_ifc_local.d, 16'hzzzz);
`checkh(io_ifc_b0.d, 16'hzzzz);
`checkh(io_ifc_b1.d, 16'hzzzz);
`checkh(io_ifc_mc.d, 16'hzzzz);
`checkh(io_arr[0].d, 16'hzzzz);
`checkh(io_arr[1].d, 16'hzzzz);
`checkh(io_ifc_deep.d, 16'hzzzz);
#1;
we0 = 1'b1;
we2 = 1'b0;
#1;
`checkh(io_ifc.d, 16'hd0);
`checkh(io_ifc_local.d, 16'hd0);
#1;
we0 = 1'b0;
we2 = 1'b0;
#1;
`checkh(io_ifc.d, 16'hzzzz);
`checkh(io_ifc_local.d, 16'hzzzz);
#1;
we0 = 1'b0;
we2 = 1'b1;
#1;
`checkh(io_ifc.d, 16'hd2);
`checkh(io_ifc_local.d, 16'hzzzz);
// Interface passed a->b, where b is instantiated multiple times (separate interfaces)
#1;
we2_b0 = 1'b1;
we2_b1 = 1'b0;
#1;
`checkh(io_ifc_b0.d, 16'hd2);
`checkh(io_ifc_b1.d, 16'hzzzz);
#1;
we2_b0 = 1'b0;
we2_b1 = 1'b1;
#1;
`checkh(io_ifc_b0.d, 16'hzzzz);
`checkh(io_ifc_b1.d, 16'hd2);
#1;
we2_b0 = 1'b0;
we2_b1 = 1'b0;
#1;
`checkh(io_ifc_b0.d, 16'hzzzz);
`checkh(io_ifc_b1.d, 16'hzzzz);
// Interface passed a->b, where c is instantiated multiple times (same interface)
#1;
we0_mc = 1'b1;
wea_mc = 1'b0;
web_mc = 1'b0;
#1;
`checkh(io_ifc_mc.d, 16'hd0);
#1;
we0_mc = 1'b0;
wea_mc = 1'b1;
web_mc = 1'b0;
#1;
`checkh(io_ifc_mc.d, 16'hd2);
#1;
we0_mc = 1'b0;
wea_mc = 1'b0;
web_mc = 1'b1;
#1;
`checkh(io_ifc_mc.d, 16'hd2);
#1;
wea_mc = 1'b1;
web_mc = 1'b1;
#1;
`checkh(io_ifc_mc.d, 16'hd2);
#1;
wea_mc = 1'b0;
web_mc = 1'b0;
#1;
`checkh(io_ifc_mc.d, 16'hzzzz);
// Interface array: each element is independent
#1;
we2_arr0 = 1'b1;
we2_arr1 = 1'b0;
#1;
`checkh(io_arr[0].d, 16'hd2);
`checkh(io_arr[1].d, 16'hzzzz);
#1;
we2_arr0 = 1'b0;
we2_arr1 = 1'b1;
#1;
`checkh(io_arr[0].d, 16'hzzzz);
`checkh(io_arr[1].d, 16'hd2);
#1;
we2_arr0 = 1'b0;
we2_arr1 = 1'b0;
#1;
`checkh(io_arr[0].d, 16'hzzzz);
`checkh(io_arr[1].d, 16'hzzzz);
// Deep nesting: passthru_deep -> passthru -> bot (3 levels)
#1;
we0_deep = 1'b1;
we2_deep = 1'b0;
#1;
`checkh(io_ifc_deep.d, 16'hd0);
#1;
we0_deep = 1'b0;
we2_deep = 1'b1;
#1;
`checkh(io_ifc_deep.d, 16'hd2);
#1;
we0_deep = 1'b0;
we2_deep = 1'b0;
#1;
`checkh(io_ifc_deep.d, 16'hzzzz);
#1;
$write("*-* All Finished *-*\n");
$finish;
end
endmodule

View File

@ -0,0 +1,11 @@
%Warning-MULTIDRIVEN: t/t_lint_multidriven_coverage_bad.v:13:15: Bit [0] of signal 'w' have multiple combinational drivers. This can cause performance degradation.
: ... note: In instance 't'
t/t_lint_multidriven_coverage_bad.v:15:15: ... Location of offending driver
15 | assign w[0] = a;
| ^
t/t_lint_multidriven_coverage_bad.v:16:15: ... Location of offending driver
16 | assign w[0] = b;
| ^
... For warning description see https://verilator.org/warn/MULTIDRIVEN?v=latest
... Use "/* verilator lint_off MULTIDRIVEN */" and lint_on around source to disable this message.
%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('linter')
test.lint(verilator_flags2=["--coverage"], fails=True, expect_filename=test.golden_filename)
test.passes()

View File

@ -0,0 +1,21 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2026 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input logic a,
input logic b,
output logic [1:0] y
);
logic [1:0] w;
assign w[0] = a; // <--- Warning
assign w[0] = b; // <--- Warning
assign w[1] = 1'b0;
assign y = w;
endmodule

View File

@ -0,0 +1,10 @@
%Error-UNSUPPORTED: t/t_tri_hier_ref_unsup.v:34:16: Unsupported tristate construct: hierarchical driver of non-interface tri signal: 'bus'
: ... note: In instance 't'
34 | assign u_sub.bus = hier_we ? 8'hBB : 8'hzz;
| ^~~
... For error description see https://verilator.org/warn/UNSUPPORTED?v=latest
%Error-UNSUPPORTED: t/t_tri_hier_ref_unsup.v:23:13: Unsupported tristate construct (in graph; not converted): VAR 'bus'
: ... note: In instance 't.u_sub'
23 | tri [7:0] bus;
| ^~~
%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('linter')
test.lint(fails=test.vlt_all, expect_filename=test.golden_filename)
test.passes()

View File

@ -0,0 +1,69 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// Test case #1: Hierarchical write to a non-interface submodule's tri wire.
// A parent module drives a child module's internal tri wire via hierarchical
// reference, in addition to the child's own internal driver.
//
// 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
// 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: on
// verilator lint_off MULTIDRIVEN
module sub_with_tri (
input we_internal
);
tri [7:0] bus;
// Internal driver: drives 8'hAA when we_internal is high
assign bus = we_internal ? 8'hAA : 8'hzz;
endmodule
module t;
logic sub_we_internal;
sub_with_tri u_sub(.we_internal(sub_we_internal));
logic hier_we;
// Drive u_sub.bus hierarchically from this module
assign u_sub.bus = hier_we ? 8'hBB : 8'hzz;
initial begin
// All drivers off -> high-Z
#1;
hier_we = 1'b0;
sub_we_internal = 1'b0;
#1;
`checkh(u_sub.bus, 8'hzz);
// External hierarchical driver on
#1;
hier_we = 1'b1;
sub_we_internal = 1'b0;
#1;
`checkh(u_sub.bus, 8'hBB);
// Internal driver on, external off
#1;
hier_we = 1'b0;
sub_we_internal = 1'b1;
#1;
`checkh(u_sub.bus, 8'hAA);
// Both off again
#1;
hier_we = 1'b0;
sub_we_internal = 1'b0;
#1;
`checkh(u_sub.bus, 8'hzz);
#1;
$write("*-* All Finished *-*\n");
$finish;
end
endmodule

View File

@ -0,0 +1,6 @@
%Error: t/t_tri_iface_eqcase_modport_bad.v:23:25: Can't find definition of 'd' in dotted variable/method: 'io_ifc.d'
23 | assign is_z = (io_ifc.d === 8'hzz);
| ^
... Known scopes under 'io_ifc': <no instances found>
... See the manual at https://verilator.org/verilator_doc.html?v=latest for more assistance.
%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('linter')
test.lint(fails=test.vlt_all, expect_filename=test.golden_filename)
test.passes()

View File

@ -0,0 +1,31 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// Verify that modport access control is not relaxed by tristate __en
// auto-insertion: accessing a tri signal not exposed through the modport
// should still produce a linker error.
//
// 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
interface ifc;
logic we;
tri [7:0] d;
modport no_d_mp (input we); // d is NOT exposed
endinterface
module chk_bad (
ifc.no_d_mp io_ifc,
output logic is_z
);
assign is_z = (io_ifc.d === 8'hzz);
endmodule
module t;
ifc i ();
logic is_z;
chk_bad u (.io_ifc(i), .is_z(is_z));
initial $finish;
endmodule

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(timing_loop=True, verilator_flags2=['--timing'])
test.execute()
test.passes()

View File

@ -0,0 +1,231 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// 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
// 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: on
// verilator lint_off MULTIDRIVEN
// ---------------- Compare semantics (=== / !==) ----------------
interface ifc_cmp;
logic we;
tri [7:0] d;
endinterface
module drv_cmp (
ifc_cmp io_ifc
);
assign io_ifc.d = io_ifc.we ? 8'h5A : 8'hzz;
endmodule
module chk_cmp (
ifc_cmp io_ifc,
output logic is_z,
output logic is_5a,
output logic not_z,
output logic not_5a
);
assign is_z = (io_ifc.d === 8'hzz);
assign is_5a = (io_ifc.d === 8'h5A);
assign not_z = (io_ifc.d !== 8'hzz);
assign not_5a = (io_ifc.d !== 8'h5A);
endmodule
interface ifc_cmp_mp;
logic we;
tri [7:0] d;
modport drv_mp (input we, inout d);
modport chk_mp (input we, inout d);
endinterface
module drv_cmp_mp (
ifc_cmp_mp.drv_mp io_ifc
);
assign io_ifc.d = io_ifc.we ? 8'h5A : 8'hzz;
endmodule
module chk_cmp_mp (
ifc_cmp_mp.chk_mp io_ifc,
output logic is_z,
output logic is_5a,
output logic not_z,
output logic not_5a
);
assign is_z = (io_ifc.d === 8'hzz);
assign is_5a = (io_ifc.d === 8'h5A);
assign not_z = (io_ifc.d !== 8'hzz);
assign not_5a = (io_ifc.d !== 8'h5A);
endmodule
module passthru_cmp (
ifc_cmp io_ifc
);
drv_cmp u_drv (.*);
endmodule
// ---------------- Mixed local + external contributors ----------------
interface ifc_mix;
logic we_local;
logic we_ext;
tri [7:0] d;
assign d = we_local ? 8'hA5 : 8'hzz;
endinterface
module drv_mix (
ifc_mix io_ifc
);
assign io_ifc.d = io_ifc.we_ext ? 8'h3C : 8'hzz;
endmodule
interface ifc_mix_mp;
logic we_local;
logic we_ext;
tri [7:0] d;
modport drv_mp (input we_ext, inout d);
assign d = we_local ? 8'hA5 : 8'hzz;
endinterface
module drv_mix_mp (
ifc_mix_mp.drv_mp io_ifc
);
assign io_ifc.d = io_ifc.we_ext ? 8'h3C : 8'hzz;
endmodule
module t;
// ---- Compare semantics: basic ----
ifc_cmp i_cmp ();
logic is_z, is_5a, not_z, not_5a;
drv_cmp u_drv_cmp (.io_ifc(i_cmp));
chk_cmp u_chk_cmp (.io_ifc(i_cmp), .is_z(is_z), .is_5a(is_5a),
.not_z(not_z), .not_5a(not_5a));
// ---- Compare semantics: modport ----
ifc_cmp_mp i_cmp_mp ();
logic mp_is_z, mp_is_5a, mp_not_z, mp_not_5a;
drv_cmp_mp u_drv_cmp_mp (.io_ifc(i_cmp_mp));
chk_cmp_mp u_chk_cmp_mp (.io_ifc(i_cmp_mp), .is_z(mp_is_z), .is_5a(mp_is_5a),
.not_z(mp_not_z), .not_5a(mp_not_5a));
// ---- Compare semantics: deep hierarchy ----
ifc_cmp i_cmp_deep ();
logic deep_is_z, deep_is_5a, deep_not_z, deep_not_5a;
passthru_cmp u_cmp_deep (.io_ifc(i_cmp_deep));
chk_cmp u_chk_cmp_deep (.io_ifc(i_cmp_deep), .is_z(deep_is_z), .is_5a(deep_is_5a),
.not_z(deep_not_z), .not_5a(deep_not_5a));
// ---- Mixed contributors ----
ifc_mix i_mix ();
drv_mix u_mix (.io_ifc(i_mix));
ifc_mix_mp i_mix_mp ();
drv_mix_mp u_mix_mp (.io_ifc(i_mix_mp));
initial begin
// ---- Compare semantics ----
i_cmp.we = 1'b0;
i_cmp_mp.we = 1'b0;
i_cmp_deep.we = 1'b0;
#1;
`checkh(is_z, 1'b1);
`checkh(is_5a, 1'b0);
`checkh(not_z, 1'b0);
`checkh(not_5a, 1'b1);
`checkh(mp_is_z, 1'b1);
`checkh(mp_is_5a, 1'b0);
`checkh(mp_not_z, 1'b0);
`checkh(mp_not_5a, 1'b1);
`checkh(deep_is_z, 1'b1);
`checkh(deep_is_5a, 1'b0);
#1;
i_cmp.we = 1'b1;
i_cmp_mp.we = 1'b1;
i_cmp_deep.we = 1'b1;
#1;
`checkh(is_z, 1'b0);
`checkh(is_5a, 1'b1);
`checkh(not_z, 1'b1);
`checkh(not_5a, 1'b0);
`checkh(mp_is_z, 1'b0);
`checkh(mp_is_5a, 1'b1);
`checkh(mp_not_z, 1'b1);
`checkh(mp_not_5a, 1'b0);
`checkh(deep_is_z, 1'b0);
`checkh(deep_is_5a, 1'b1);
#1;
i_cmp.we = 1'b0;
i_cmp_mp.we = 1'b0;
i_cmp_deep.we = 1'b0;
#1;
`checkh(is_z, 1'b1);
`checkh(is_5a, 1'b0);
`checkh(not_z, 1'b0);
`checkh(not_5a, 1'b1);
`checkh(mp_is_z, 1'b1);
`checkh(mp_is_5a, 1'b0);
`checkh(mp_not_z, 1'b0);
`checkh(mp_not_5a, 1'b1);
`checkh(deep_is_z, 1'b1);
`checkh(deep_is_5a, 1'b0);
// ---- Mixed contributors ----
i_mix.we_local = 1'b0;
i_mix.we_ext = 1'b0;
i_mix_mp.we_local = 1'b0;
i_mix_mp.we_ext = 1'b0;
#1;
`checkh(i_mix.d, 8'hzz);
`checkh(i_mix_mp.d, 8'hzz);
#1;
i_mix.we_local = 1'b1;
i_mix.we_ext = 1'b0;
i_mix_mp.we_local = 1'b1;
i_mix_mp.we_ext = 1'b0;
#1;
`checkh(i_mix.d, 8'hA5);
`checkh(i_mix_mp.d, 8'hA5);
#1;
i_mix.we_local = 1'b0;
i_mix.we_ext = 1'b1;
i_mix_mp.we_local = 1'b0;
i_mix_mp.we_ext = 1'b1;
#1;
`checkh(i_mix.d, 8'h3C);
`checkh(i_mix_mp.d, 8'h3C);
#1;
i_mix.we_local = 1'b1;
i_mix.we_ext = 1'b1;
i_mix_mp.we_local = 1'b1;
i_mix_mp.we_ext = 1'b1;
#1;
`checkh(i_mix.d, 8'hBD);
`checkh(i_mix_mp.d, 8'hBD);
#1;
i_mix.we_local = 1'b0;
i_mix.we_ext = 1'b0;
i_mix_mp.we_local = 1'b0;
i_mix_mp.we_ext = 1'b0;
#1;
`checkh(i_mix.d, 8'hzz);
`checkh(i_mix_mp.d, 8'hzz);
$write("*-* All Finished *-*\n");
$finish;
end
endmodule

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(timing_loop=True, verilator_flags2=['--timing'])
test.execute()
test.passes()

View File

@ -0,0 +1,51 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// Test case #4: $root absolute hierarchical reference to tristate signal.
// A submodule reads a tristate signal from the top module via $root.
//
// 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
// 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: on
module sub_root_reader (
output logic [7:0] val
);
assign val = $root.t.root_bus;
endmodule
module t;
tri [7:0] root_bus;
logic root_we;
assign root_bus = root_we ? 8'hCC : 8'hzz;
logic [7:0] root_readback;
sub_root_reader u_reader(.val(root_readback));
initial begin
#1;
root_we = 1'b0;
#1;
`checkh(root_readback, 8'hzz);
#1;
root_we = 1'b1;
#1;
`checkh(root_readback, 8'hCC);
#1;
root_we = 1'b0;
#1;
`checkh(root_readback, 8'hzz);
#1;
$write("*-* All Finished *-*\n");
$finish;
end
endmodule