// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
// DESCRIPTION: Verilator: Deals with tristate logic
//
// 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
//
//*************************************************************************
// V3Tristate's Transformations:
//
// Modify the design to expand tristate logic into its
// corresponding two state representation. At the lowest levels,
//
// In detail:
//
// Over each module, from child to parent:
//   Build a graph, connecting signals together so we can propagate tristates
//     Variable becomes tristate with
//       VAR->isInout
//       VAR->isPullup/isPulldown (converted to AstPullup/AstPulldown
//       BufIf0/1
//   All variables on the LHS need to become tristate when there is:
//       CONST-> with Z value on the RHS of an assignment
//       AstPin with lower connection a tristate
//       A tristate signal on the RHS
//       (this can't generally be determined until that signal is resolved)
//   When LHS becomes tristate, then mark all RHS nodes as tristate
//   so any tristate varrefs on the right will propagate.
//
// Walk graph's tristate indication on each logic block with tristates
// propagating downstream to every other logic block.
//
// Expressions that have Z in them are converted into two state
// drivers and corresponding output enable signals are generated.
// These enable signals get transformed and regenerated through any
// logic that they may go through until they hit the module level.  At
// the module level, all the output enable signals from what can be
// many tristate drivers are combined together to produce a single
// driver and output enable. If the signal propagates up into higher
// modules, then new ports are created for the signal with
// suffixes __en and __out. The original port is turned from an inout
// to an input and the __out port carries the output driver signal and
// the __en port carried the output enable for that driver.
//
// Note 1800-2012 adds user defined resolution functions. This suggests
// long term this code should be scoped-based and resolve all nodes at once
// rather than hierarchically. If/when that is done, make sure to avoid
// duplicating vars and logic that is common between each instance of a
// module.
//
//----------------------------------------------------------------------
//
// Another thing done in this phase is signal strength handling.
//
// Currently they are only supported in assignments and gates parsed as assignments (see verilog.y)
// when any of the cases occurs:
// - it is possible to statically resolve all drivers,
// - all assignments that passed the static resolution have symmetric strengths (the same strength
// is related to both 0 and 1 values).
//
// It is possible to statically resolve all drivers when the strongest assignment has RHS marked as
// non-tristate. If the RHS is equal to z, that assignment has to be skipped. Since the value may
// be not known at Verilation time, cases with tristates on RHS can't be handled statically.
//
// Static resolution is split into 2 parts.
// First part can be done before tristate propagation. It is about removing assignments that are
// weaker or equally strong as the strongest assignment with constant on RHS that has all bits
// the same (equal to 0 or 1). It is done in the following way:
// - The assignment of value 0 (size may be greater than 1), that has greatest strength (the
// one corresponding to 0) of all other assignments of 0, has to be found.
// - The same is done for value '1 and strength corresponding to value 1.
// - The greater of these two strengths is chosen. If they are equal the one that corresponds
// to 1 is taken as the greatest.
// - All assignments, that have strengths weaker or equal to the one that was found before, are
// removed. They are the assignments with constants on the RHS and also all assignments that have
// both strengths non-greater that the one was found, because they are weaker no matter what is on
// RHS.
//
// Second part of static resolution is done after tristate propagation.
// At that moment it is known that some expressions can't be equal to z. The exact value is
// unknown (except the ones with constants that were handled before), so weaker of both strengths
// has to be taken into account. All weaker assignments can be safely removed. It is done in
// similar way to the first part:
// - The assignment with non-tristate RHS with the greatest weaker strength has to be found.
// - Then all not stronger assignments can be removed.
//
// All assignments that are stronger than the strongest with non-tristate RHS are then tried to be
// handled dynamically. Currently it is supported only on assignments with symmetric strengths.
// In this case, the exact value of the RHS doesn't matter. It only matters if it equals z or not.
// Such assignments are handled by changing the values to z of these bits that are overwritten by
// stronger assignments. Then all assignments can be aggregated as they would have equal strengths
// (by | on them and their __en expressions). To change the value to z, the RHS should be & with
// negation of __en expression of stronger assignments. Changing RHS's __en expression is not
// needed, because it will be then aggregated with __en expression of stronger assignments using |,
// so & with the negation can be safely skipped.
// So the values of overwritten bits are actually changed to 0, which doesn't affect stronger
// assignments, because | operation was used.
//
// Dynamic handling is implemented in the following way:
// - group the assignments by their strengths,
// - handle assignments of the same strength by aggregating values with |
// - assign results to var__strength and var__strength__en variables
// - aggregate the results:
// orp = orp | (var__strength & ~enp)
// enp = enp | var__strength__en,
// where orp is aggregated value and enp is aggregated __en value.
//
// There is a possible problem with equally strong assignments, because multiple assignments with
// the same strength, but different values should result in x value, but these values are
// unsupported.
//*************************************************************************

#include "V3PchAstNoMT.h"  // VL_MT_DISABLED_CODE_UNIT

#include "V3Tristate.h"

#include "V3AstUserAllocator.h"
#include "V3Graph.h"
#include "V3Inst.h"
#include "V3Stats.h"

VL_DEFINE_DEBUG_FUNCTIONS;

//######################################################################

class TristateBaseVisitor VL_NOT_FINAL : public VNVisitor {
public:
    // METHODS
};

//######################################################################
// Graph support classes

class TristateVertex final : public V3GraphVertex {
    VL_RTTI_IMPL(TristateVertex, V3GraphVertex)
    AstNode* const m_nodep;
    bool m_isTristate = false;  // Logic indicates a tristate
    bool m_feedsTri = false;  // Propagates to a tristate node (on RHS)
    bool m_processed = false;  // Tristating was cleaned up
public:
    TristateVertex(V3Graph* graphp, AstNode* nodep)
        : V3GraphVertex{graphp}
        , m_nodep{nodep} {}
    ~TristateVertex() override = default;
    // ACCESSORS
    AstNode* nodep() const VL_MT_STABLE { return m_nodep; }
    const AstVar* varp() const { return VN_CAST(nodep(), Var); }
    string name() const override {
        return ((isTristate() ? "tri\\n"
                 : feedsTri() ? "feed\\n"
                              : "-\\n")
                + (nodep()->prettyTypeName() + " " + cvtToHex(nodep())));
    }
    string dotColor() const override {
        return (varp() ? (isTristate() ? "darkblue"
                          : feedsTri() ? "blue"
                                       : "lightblue")
                       : (isTristate() ? "darkgreen"
                          : feedsTri() ? "green"
                                       : "lightgreen"));
    }
    FileLine* fileline() const override { return nodep()->fileline(); }
    bool isTristate() const VL_MT_SAFE { return m_isTristate; }
    void isTristate(bool flag) { m_isTristate = flag; }
    bool feedsTri() const VL_MT_SAFE { return m_feedsTri; }
    void feedsTri(bool flag) { m_feedsTri = flag; }
    bool processed() const { return m_processed; }
    void processed(bool flag) { m_processed = flag; }
};

//######################################################################

class TristateGraph final {
    // NODE STATE
    //   AstVar::user4p         -> TristateVertex* for variable being built
    // VNUser4InUse     m_inuser4;   // In visitor below

    // TYPES
public:
    using VarVec = std::vector<AstVar*>;

private:
    // MEMBERS
    V3Graph m_graph;  // Logic graph

public:
    // CONSTRUCTORS
    TristateGraph() { clearAndCheck(); }
    virtual ~TristateGraph() { clearAndCheck(); }
    VL_UNCOPYABLE(TristateGraph);

private:
    // METHODS

    TristateVertex* makeVertex(AstNode* nodep) {
        TristateVertex* vertexp = reinterpret_cast<TristateVertex*>(nodep->user4p());
        if (!vertexp) {
            UINFO(6, "         New vertex " << nodep);
            vertexp = new TristateVertex{&m_graph, nodep};
            nodep->user4p(vertexp);
        }
        return vertexp;
    }

    // METHODS - Graph optimization
    void graphWalkRecurseFwd(TristateVertex* vtxp, int level) {
        // Propagate tristate forward to all sinks
        // For example if on a CONST, propagate through CONCATS to ASSIGN
        // to LHS VARREF of signal to tristate
        if (!vtxp->isTristate()) return;  // tristate involved
        if (vtxp->user() == 1) return;
        vtxp->user(1);  // Recursed
        UINFO(9, "  Mark tri " << level << "  " << vtxp);
        if (!vtxp->varp()) {  // not a var where we stop the recursion
            for (V3GraphEdge& edge : vtxp->outEdges()) {
                TristateVertex* const vvertexp = static_cast<TristateVertex*>(edge.top());
                // Doesn't hurt to not check if already set, but by doing so when we
                // print out the debug messages, we'll see this node at level 0 instead.
                if (!vvertexp->isTristate()) {
                    vvertexp->isTristate(true);
                    graphWalkRecurseFwd(vvertexp, level + 1);
                }
            }
        } else {
            // A variable is tristated.  Find all of the LHS VARREFs that
            // drive this signal; they now need tristate drivers
            for (V3GraphEdge& edge : vtxp->inEdges()) {
                TristateVertex* const vvertexp = static_cast<TristateVertex*>(edge.fromp());
                if (const AstVarRef* const refp = VN_CAST(vvertexp->nodep(), VarRef)) {
                    if (refp->access().isWriteOrRW()
                        // Doesn't hurt to not check if already set, but by doing so when we
                        // print out the debug messages, we'll see this node at level 0 instead.
                        && !vvertexp->isTristate()) {
                        vvertexp->isTristate(true);
                        graphWalkRecurseFwd(vvertexp, level + 1);
                    }
                }
            }
        }
    }
    void graphWalkRecurseBack(TristateVertex* vtxp, int level) {
        // Called only on a tristate node; propagate a feedsTri attribute "backwards"
        // towards any driving nodes, i.e. from a LHS VARREF back to a driving RHS VARREF
        // This way if the RHS VARREF is also tristated we'll connect the
        // enables up to the LHS VARREF. Otherwise if not marked
        // feedsTri() we'll drop the LHS' enables, if any
        if (!(vtxp->isTristate() || vtxp->feedsTri())) return;  // tristate involved
        if (vtxp->user() == 3) return;
        vtxp->user(3);  // Recursed
        UINFO(9, "  Mark feedstri " << level << "  " << vtxp);
        if (!vtxp->varp()) {  // not a var where we stop the recursion
            for (V3GraphEdge& edge : vtxp->inEdges()) {
                TristateVertex* const vvertexp = static_cast<TristateVertex*>(edge.fromp());
                // Doesn't hurt to not check if already set, but by doing so when we
                // print out the debug messages, we'll see this node at level 0 instead.
                if (!vvertexp->feedsTri()) {
                    vvertexp->feedsTri(true);
                    graphWalkRecurseBack(vvertexp, level + 1);
                }
            }
        }
    }

public:
    // METHODS
    bool empty() const { return m_graph.empty(); }
    void clearAndCheck() {
        for (V3GraphVertex& vtx : m_graph.vertices()) {
            const TristateVertex& vvertex = static_cast<TristateVertex&>(vtx);
            if (vvertex.isTristate() && !vvertex.processed()) {
                // Not v3errorSrc as no reason to stop the world
                vvertex.nodep()->v3warn(E_UNSUPPORTED, "Unsupported tristate construct"
                                                       " (in graph; not converted): "
                                                           << vvertex.nodep()->prettyTypeName());
            }
        }
        m_graph.clear();
        AstNode::user4ClearTree();  // Wipe all node user4p's that point to vertexes
    }
    void graphWalk(AstNodeModule* nodep) {
        UINFO(9, " Walking " << nodep);
        for (V3GraphVertex& vtx : m_graph.vertices()) {
            graphWalkRecurseFwd(static_cast<TristateVertex*>(&vtx), 0);
        }
        for (V3GraphVertex& vtx : m_graph.vertices()) {
            graphWalkRecurseBack(static_cast<TristateVertex*>(&vtx), 0);
        }
        if (dumpGraphLevel() >= 9) m_graph.dumpDotFilePrefixed("tri_pos__" + nodep->name());
    }
    void associate(AstNode* fromp, AstNode* top) {
        new V3GraphEdge{&m_graph, makeVertex(fromp), makeVertex(top), 1};
    }
    void deleteVerticesFromSubtreeRecurse(AstNode* nodep) {
        if (!nodep) return;
        // Skip vars, because they may be connected to more than one varref
        if (!VN_IS(nodep, Var)) {
            TristateVertex* const vertexp = reinterpret_cast<TristateVertex*>(nodep->user4p());
            if (vertexp) vertexp->unlinkDelete(&m_graph);
        }
        if (AstNode* const refp = nodep->op1p()) deleteVerticesFromSubtreeRecurse(refp);
        if (AstNode* const refp = nodep->op2p()) deleteVerticesFromSubtreeRecurse(refp);
        if (AstNode* const refp = nodep->op3p()) deleteVerticesFromSubtreeRecurse(refp);
        if (AstNode* const refp = nodep->op4p()) deleteVerticesFromSubtreeRecurse(refp);
    }
    void setTristate(AstNode* nodep) { makeVertex(nodep)->isTristate(true); }
    bool isTristate(const AstNode* nodep) {
        const TristateVertex* const vertexp = reinterpret_cast<TristateVertex*>(nodep->user4p());
        return vertexp && vertexp->isTristate();
    }
    bool feedsTri(const AstNode* nodep) {
        const TristateVertex* const vertexp = reinterpret_cast<TristateVertex*>(nodep->user4p());
        return vertexp && vertexp->feedsTri();
    }
    void didProcess(AstNode* nodep, bool quiet = false) {
        if (TristateVertex* const vertexp = reinterpret_cast<TristateVertex*>(nodep->user4p())) {
            // We don't warn if no vertexp->isTristate() as the creation
            // process makes midling nodes that don't have it set
            vertexp->processed(true);
        } else if (!quiet) {
            // Not v3errorSrc as no reason to stop the world
            nodep->v3warn(E_UNSUPPORTED,
                          "Unsupported tristate construct (not in propagation graph): "
                              << nodep->prettyTypeName());
        }
    }
    // ITERATOR METHODS
    VarVec tristateVars() {
        // Return all tristate variables
        VarVec v;
        for (const V3GraphVertex& vtx : m_graph.vertices()) {
            const TristateVertex& vvertex = static_cast<const TristateVertex&>(vtx);
            if (vvertex.isTristate()) {
                if (AstVar* const nodep = VN_CAST(vvertex.nodep(), Var)) v.push_back(nodep);
            }
        }
        return v;
    }
};

//######################################################################
// Given a node, flip any VarRef from LValue to RValue (i.e. make it an input)
// See also V3LinkLValue::linkLValueSet

class TristatePinVisitor final : public TristateBaseVisitor {
    TristateGraph& m_tgraph;
    const bool m_lvalue;  // Flip to be an LVALUE
    // VISITORS
    void visit(AstVarRef* nodep) override {
        UASSERT_OBJ(!nodep->access().isRW(), nodep, "Tristate unexpected on R/W access flip");
        if (m_lvalue && !nodep->access().isWriteOrRW()) {
            UINFO(9, "  Flip-to-LValue " << nodep);
            nodep->access(VAccess::WRITE);
        } else if (!m_lvalue && !nodep->access().isReadOnly()) {
            UINFO(9, "  Flip-to-RValue " << nodep);
            nodep->access(VAccess::READ);
            // Mark the ex-output as tristated
            UINFO(9, "  setTristate-subpin " << nodep->varp());
            m_tgraph.setTristate(nodep->varp());
        }
    }
    void visit(AstArraySel* nodep) override {
        // Doesn't work because we'd set lvalue on the array index's var
        UASSERT_OBJ(!m_lvalue, nodep, "ArraySel conversion to output, under tristate node");
        iterateChildren(nodep);
    }
    void visit(AstSliceSel* nodep) override {
        // Doesn't work because we'd set lvalue on the array index's var
        UASSERT_OBJ(!m_lvalue, nodep, "SliceSel conversion to output, under tristate node");
        iterateChildren(nodep);
    }
    void visit(AstNode* nodep) override { iterateChildren(nodep); }

public:
    // CONSTRUCTORS
    TristatePinVisitor(AstNode* nodep, TristateGraph& tgraph, bool lvalue)
        : m_tgraph(tgraph)  // Need () or GCC 4.8 false warning
        , m_lvalue{lvalue} {
        iterate(nodep);
    }
    ~TristatePinVisitor() override = default;
};

//######################################################################

class TristateVisitor final : public TristateBaseVisitor {
    // NODE STATE
    //   *::user1p              -> pointer to output enable __en expressions
    //   *::user2               -> int - already visited, see U2_ enum
    //   AstVar::user3p         -> See AuxAstVar via m_varAux
    // See TristateGraph:
    //   AstVar::user4p         -> TristateVertex* for variable being built
    //   AstStmt::user4p        -> TristateVertex* for this statement
    const VNUser1InUse m_inuser1;
    const VNUser2InUse m_inuser2;
    const VNUser3InUse m_inuser3;
    const VNUser4InUse m_inuser4;

    struct AuxAstVar final {
        AstPull* pullp = nullptr;  // pullup/pulldown direction (whole variable)
        AstVar* outVarp = nullptr;  // output __out var
        std::unordered_map<int, int>
            bitPulls;  // Per-bit pull: bit_index -> direction (1=up, 0=down)
    };

    AstUser3Allocator<AstVar, AuxAstVar> m_varAux;

    // TYPES
    struct RefStrength final {
        AstNodeVarRef* m_varrefp;
        VStrength m_strength;
        RefStrength(AstNodeVarRef* varrefp, VStrength strength)
            : m_varrefp{varrefp}
            , m_strength{strength} {}
    };
    using RefStrengthVec = std::vector<RefStrength>;
    using VarMap = std::map<AstVar*, RefStrengthVec*>;
    using Assigns = std::vector<AstAssignW*>;
    using VarToAssignsMap = std::map<AstVar*, Assigns>;
    enum : uint8_t {
        U2_GRAPHING = 1,  // bit[0] if did m_graphing visit
        U2_NONGRAPH = 2,  // bit[1] if did !m_graphing visit
        U2_BOTH = 3  // Both bits set
    };

    // MEMBERS
    bool m_graphing = false;  // Major mode - creating graph
    //
    AstNodeModule* m_modp = nullptr;  // Current module
    AstCell* m_cellp = nullptr;  // current cell
    VarMap m_lhsmap;  // Tristate left-hand-side driver map
    VarToAssignsMap m_assigns;  // Assigns in current module
    int m_unique = 0;
    bool m_alhs = false;  // On LHS of assignment
    bool m_inAlias = false;  // Inside alias statement
    VStrength m_currentStrength = VStrength::STRONG;  // Current strength of assignment,
                                                      // 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

    // METHODS
    string dbgState() const {
        string o = (m_graphing ? " gr " : " ng ");
        if (m_alhs) o += "alhs ";
        return o;
    }
    void modAddStmtp(AstNode* nodep, AstNode* newp) {
        if (!m_modp) {
            nodep->v3warn(E_UNSUPPORTED,
                          "Unsupported: Creating tristate signal not underneath a module: "
                              << nodep->prettyNameQ());
        } else {
            m_modp->addStmtsp(newp);
        }
    }
    void associateLogic(AstNode* fromp, AstNode* top) {
        if (m_logicp) m_tgraph.associate(fromp, top);
    }
    AstConst* newAllZerosOrOnes(AstNode* nodep, bool ones) {
        V3Number num{nodep, nodep->width()};
        if (ones) num.setAllBits1();
        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 (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.
                    return newAllZerosOrOnes(nodep, true);
                }
            }
        } else {
            // There's no select being built yet, so add what will become a
            // constant output enable driver of all 1's
            nodep->user1p(newAllZerosOrOnes(nodep, true));
        }
        // 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()) {
            AstVar* const newp
                = new AstVar{invarp->fileline(), isTop ? VVarType::VAR : VVarType::MODULETEMP,
                             invarp->name() + "__en", invarp};
            // Inherited VDirection::INPUT
            UINFO(9, "       newenv " << 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);
    }
    AstConst* getNonZConstp(AstConst* const constp) {
        FileLine* const fl = constp->fileline();
        V3Number numz{constp, constp->width()};
        numz.opBitsZ(constp->num());  // Z->1, else 0
        V3Number numz0{constp, constp->width()};
        numz0.opNot(numz);  // Z->0, else 1
        return new AstConst{fl, numz0};
    }
    AstNodeExpr* getEnExprBasedOnOriginalp(AstNodeExpr* const nodep) {
        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)) {
            // Extend inserts 0 at the beginning. 0 in __en variable means that this bit equals z,
            // so in order to preserve the value of the original AstExtend node we should insert 1
            // instead of 0. To extend __en expression we have to negate its lhsp() and then negate
            // whole extend.

            // Unlink lhsp before copying to save unnecessary copy of lhsp
            AstNodeExpr* const lhsp = extendp->lhsp()->unlinkFrBack();
            AstExtend* const enExtendp = extendp->cloneTree(false);
            extendp->lhsp(lhsp);
            AstNodeExpr* const enLhsp = getEnExprBasedOnOriginalp(lhsp);
            enExtendp->lhsp(new AstNot{enLhsp->fileline(), enLhsp});
            return new AstNot{enExtendp->fileline(), enExtendp};
        } else if (AstSel* const selp = VN_CAST(nodep, Sel)) {
            AstNodeExpr* const fromp = selp->fromp()->unlinkFrBack();
            AstSel* const enSelp = selp->cloneTree(false);
            selp->fromp(fromp);
            AstNodeExpr* const enFromp = getEnExprBasedOnOriginalp(fromp);
            enSelp->fromp(enFromp);
            return enSelp;
        } else {
            nodep->v3warn(E_UNSUPPORTED,
                          "Unsupported tristate construct: " << nodep->prettyTypeName()
                                                             << " in function " << __func__);
            return nullptr;
        }
    }
    AstVar* getCreateOutVarp(AstVar* invarp, bool isTop) {
        // Return the master __out for the specified input variable
        if (!m_varAux(invarp).outVarp) {
            AstVar* const newp
                = new AstVar{invarp->fileline(), isTop ? VVarType::VAR : VVarType::MODULETEMP,
                             invarp->name() + "__out", invarp};
            // Inherited VDirection::OUTPUT
            UINFO(9, "       newout " << newp);
            modAddStmtp(invarp, newp);
            m_varAux(invarp).outVarp = newp;  // find outvar given invarp
        }
        return m_varAux(invarp).outVarp;
    }
    AstVar* getCreateUnconnVarp(AstNode* fromp, AstNodeDType* dtypep) {
        AstVar* const newp = new AstVar{fromp->fileline(), VVarType::MODULETEMP,
                                        "__Vtriunconn" + cvtToStr(m_unique++), dtypep};
        UINFO(9, "       newunc " << newp);
        modAddStmtp(newp, newp);
        return newp;
    }

    void mapInsertLhsVarRef(AstNodeVarRef* nodep) {
        UINFO(9, "    mapInsertLhsVarRef " << nodep);
        AstVar* const key = nodep->varp();
        const auto pair = m_lhsmap.emplace(key, nullptr);
        if (pair.second) pair.first->second = new RefStrengthVec;
        pair.first->second->push_back(RefStrength{nodep, m_currentStrength});
    }

    AstNodeExpr* newEnableDeposit(AstSel* selp, AstNodeExpr* enp) {
        // Form a "deposit" instruction for given enable, using existing select as a template.
        // Would be nicer if we made this a new AST type
        AstNodeExpr* const newp = new AstShiftL{
            selp->fileline(), new AstExtend{selp->fileline(), enp, selp->fromp()->width()},
            selp->lsbp()->cloneTreePure(false), selp->fromp()->width()};
        return newp;
    }

    void setPullDirection(AstVar* varp, AstPull* pullp) {
        const AstPull* const oldpullp = m_varAux(varp).pullp;
        if (!oldpullp) {
            m_varAux(varp).pullp = pullp;  // save off to indicate the pull direction
        } else {
            if (oldpullp->direction() != pullp->direction()) {
                pullp->v3warn(E_UNSUPPORTED, "Unsupported: Conflicting pull directions.\n"
                                                 << pullp->warnContextPrimary() << '\n'
                                                 << oldpullp->warnOther()
                                                 << "... Location of conflicting pull.\n"
                                                 << oldpullp->warnContextSecondary());
            }
        }
    }

    void setBitPullDirection(AstVar* varp, int bitIndex, int direction) {
        // Set pull direction for a specific bit of a bus.
        // direction: 1 = pullup, 0 = pulldown
        auto& bitPulls = m_varAux(varp).bitPulls;
        auto it = bitPulls.find(bitIndex);
        if (it != bitPulls.end() && it->second != direction) {
            varp->v3warn(E_UNSUPPORTED, "Conflicting pullup/pulldown direction on bit "
                                            << bitIndex << " of " << varp->prettyNameQ());
        }
        bitPulls[bitIndex] = direction;
    }

    AstConst* createPerBitPullConst(AstVar* varp) {
        // Create a constant with per-bit pull values.
        // Bits without explicit pull default to pulldown (0).
        // V3Number::setBit silently no-ops if bitIndex exceeds the variable width.
        const auto& bitPulls = m_varAux(varp).bitPulls;
        V3Number num{varp, varp->width(), 0};  // Start with all zeros
        for (const auto& pair : bitPulls) {
            if (pair.second == 1) num.setBit(pair.first, 1);
        }  // LCOV_EXCL_LINE
        return new AstConst{varp->fileline(), num};
    }

    bool hasPerBitPulls(AstVar* varp) {
        // Note: Not const - m_varAux allocates on access
        return !m_varAux(varp).bitPulls.empty();
    }

    void checkUnhandled(AstNode* nodep) {
        // Check for unsupported tristate constructs. This is not a 100% check.
        // The best way would be to visit the tree again and find any user1p()
        // pointers that did not get picked up and expanded.
        if (m_alhs && nodep->user1p()) {
            nodep->v3warn(E_UNSUPPORTED,
                          "Unsupported LHS tristate construct: " << nodep->prettyTypeName());
        }
        // Ignore Var because they end up adjacent to statements
        if ((nodep->op1p() && nodep->op1p()->user1p() && !VN_IS(nodep->op1p(), Var))
            || (nodep->op2p() && nodep->op2p()->user1p() && !VN_IS(nodep->op2p(), Var))
            || (nodep->op3p() && nodep->op3p()->user1p() && !VN_IS(nodep->op3p(), Var))
            || (nodep->op4p() && nodep->op4p()->user1p() && !VN_IS(nodep->op4p(), Var))) {
            nodep->v3warn(E_UNSUPPORTED,
                          "Unsupported tristate construct: " << nodep->prettyTypeName());
        }
    }

    void insertTristates(AstNodeModule* nodep) {
        // Go through all the vars and find any that are outputs without drivers
        // or inouts without high-Z logic and put a 1'bz driver on them and add
        // them to the lhs map so they get expanded correctly.
        const TristateGraph::VarVec vars = m_tgraph.tristateVars();
        for (auto varp : vars) {
            if (!m_tgraph.isTristate(varp)) continue;
            const auto it = m_lhsmap.find(varp);
            if (it != m_lhsmap.end()) continue;
            // This variable is floating, set output enable to always be off on this assign.
            // For pullup/pulldown vars, use pull value with en=0 - the final resolution will
            // apply the pull formula: final = (driven_value & en) | (~en & pull_value)
            UINFO(8, "  Adding driver to var " << varp);
            const AstPull* const pullp = m_varAux(varp).pullp;
            const bool pullValue = pullp && pullp->direction() == 1;
            AstConst* const constp = newAllZerosOrOnes(varp, pullValue);
            AstVarRef* const varrefp = new AstVarRef{varp->fileline(), varp, VAccess::WRITE};
            AstAssignW* const newp = new AstAssignW{varp->fileline(), varrefp, constp};
            UINFO(9, "       newoev " << newp);
            varrefp->user1p(newAllZerosOrOnes(varp, false));  // en=0 for floating/pullup vars
            nodep->addStmtsp(new AstAlways{newp});
            mapInsertLhsVarRef(varrefp);  // insertTristates will convert
            //                               // to a varref to the __out# variable
        }

        // Now go through the lhs driver map and generate the output
        // enable logic for any tristates.
        // Note there might not be any drivers.
        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)) {
                // 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 (const 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 (const 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);
            }
            // Delete the map and vector list now that we have expanded it.
            m_lhsmap.erase(invarp);
            VL_DO_DANGLING(delete refsp, refsp);
        }
    }

    void aggregateTriSameStrength(AstNodeModule* nodep, AstVar* const varp, AstVar* const envarp,
                                  RefStrengthVec::iterator beginStrength,
                                  RefStrengthVec::iterator endStrength) {
        // For each driver separate variables (normal and __en) are created and initialized with
        // values. In case of normal variable, the original expression is reused. Their values are
        // aggregated using | to form one expression, which are assigned to varp end envarp.
        AstNodeExpr* orp = nullptr;
        AstNodeExpr* enp = nullptr;

        for (auto it = beginStrength; it != endStrength; it++) {
            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);
            // 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);

            AstAssignW* const enLhspAssignp = new AstAssignW{
                refp->fileline(), new AstVarRef{refp->fileline(), newEnLhsp, VAccess::WRITE},
                getEnp(refp)};
            UINFO(9, "       newenlhspAssignp " << enLhspAssignp);
            nodep->addStmtsp(new AstAlways{enLhspAssignp});

            // now append this driver to the driver logic.
            AstNodeExpr* const ref1p = new AstVarRef{refp->fileline(), newLhsp, VAccess::READ};
            AstNodeExpr* const ref2p = new AstVarRef{refp->fileline(), newEnLhsp, VAccess::READ};
            AstNodeExpr* const andp = new AstAnd{refp->fileline(), ref1p, ref2p};

            // or this to the others
            orp = (!orp) ? andp : new AstOr{refp->fileline(), orp, andp};

            AstNodeExpr* const ref3p = new AstVarRef{refp->fileline(), newEnLhsp, VAccess::READ};
            enp = (!enp) ? ref3p : new AstOr{ref3p->fileline(), enp, ref3p};
        }
        AstAssignW* const assp = new AstAssignW{
            varp->fileline(), new AstVarRef{varp->fileline(), varp, VAccess::WRITE}, orp};
        UINFO(9, "       newassp " << assp);
        nodep->addStmtsp(new AstAlways{assp});

        AstAssignW* const enAssp = new AstAssignW{
            envarp->fileline(), new AstVarRef{envarp->fileline(), envarp, VAccess::WRITE}, enp};
        UINFO(9, "       newenassp " << enAssp);
        nodep->addStmtsp(new AstAlways{enAssp});
    }

    // 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);

        // If the lhs var is a port, then we need to create ports for
        // the output (__out) and output enable (__en) signals. The
        // original port gets converted to an input. Don't tristate expand
        // if this is the top level so that we can force the final
        // tristate resolution at the top.
        // 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)
        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
                m_varAux(outvarp).bitPulls
                    = m_varAux(invarp).bitPulls;  // Per-bit pull propagation
                if (m_varAux(invarp).pullp) UINFO(9, "propagate pull to " << outvarp);
                if (!m_varAux(invarp).bitPulls.empty()) {
                    UINFO(9, "propagate per-bit pulls to " << outvarp);
                }
            } else if (invarp->user1p()) {
                envarp = VN_AS(invarp->user1p(), Var);  // From CASEEQ, foo === 1'bz
            }
        }

        AstNodeExpr* orp = nullptr;
        AstNodeExpr* enp = nullptr;

        std::sort(refsp->begin(), refsp->end(),
                  [](RefStrength a, RefStrength b) { return a.m_strength > b.m_strength; });

        RefStrengthVec::iterator beginStrength = refsp->begin();
        while (beginStrength != refsp->end()) {
            RefStrengthVec::iterator endStrength = beginStrength + 1;
            while (endStrength != refsp->end()
                   && endStrength->m_strength == beginStrength->m_strength)
                endStrength++;

            FileLine* const fl = beginStrength->m_varrefp->fileline();
            // 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);

            aggregateTriSameStrength(nodep, varStrengthp, enVarStrengthp, beginStrength,
                                     endStrength);

            AstNodeExpr* exprCurrentStrengthp;
            if (enp) {
                // If weaker driver should be overwritten by a stronger, replace its value with z
                exprCurrentStrengthp
                    = new AstAnd{fl, new AstVarRef{fl, varStrengthp, VAccess::READ},
                                 new AstNot{fl, enp->cloneTreePure(false)}};
            } else {
                exprCurrentStrengthp = new AstVarRef{fl, varStrengthp, VAccess::READ};
            }
            orp = (!orp) ? exprCurrentStrengthp : new AstOr{fl, orp, exprCurrentStrengthp};

            AstNodeExpr* const enVarStrengthRefp
                = new AstVarRef{fl, enVarStrengthp, VAccess::READ};

            enp = (!enp) ? enVarStrengthRefp : new AstOr{fl, enp, enVarStrengthRefp};

            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;
        }

        // Apply pull formula: final = orp | (~enp & pull_value)
        // Always run at top level (!outvarp) so that the strength-enable expression (enp)
        // is consumed into orp even when no pull is present; pull_value is then all-zeros
        // so this is semantically a no-op but it links enp into the tree.
        // Also run at intermediate levels when there's a pull, so the pull is baked into
        // the contribution flowing up the hierarchy.
        const bool hasPull = m_varAux(lhsp).pullp || hasPerBitPulls(lhsp);
        if (!outvarp || hasPull) {
            AstNodeExpr* undrivenp;
            if (envarp) {
                undrivenp = new AstNot{envarp->fileline(),
                                       new AstVarRef{envarp->fileline(), envarp, VAccess::READ}};
            } else if (enp) {
                undrivenp = new AstNot{enp->fileline(), enp};
                enp = nullptr;  // moved into undrivenp
            } else {
                undrivenp = newAllZerosOrOnes(invarp, true);  // LCOV_EXCL_LINE
            }

            AstConst* pullConstp;
            if (hasPerBitPulls(lhsp)) {
                pullConstp = createPerBitPullConst(lhsp);
            } else {
                const AstPull* const pullp = m_varAux(lhsp).pullp;
                const bool pull1 = pullp && pullp->direction() == 1;
                pullConstp = newAllZerosOrOnes(invarp, pull1);
            }

            undrivenp = new AstAnd{invarp->fileline(), undrivenp, pullConstp};
            orp = orp ? new AstOr{invarp->fileline(), orp, undrivenp} : undrivenp;
        }

        // Ensure enp is valid when envarp exists (pullup/pulldown only = no active driver)
        if (envarp && !enp) enp = newAllZerosOrOnes(invarp, false);

        if (envarp) {
            AstAssignW* const enAssp = new AstAssignW{
                enp->fileline(), new AstVarRef{envarp->fileline(), envarp, VAccess::WRITE}, enp};
            UINFOTREE(9, enAssp, "", "enAssp");
            nodep->addStmtsp(new AstAlways{enAssp});
        }

        // __out (child) or <in> (parent) = drive-value expression
        AstAssignW* const assp = new AstAssignW{
            lhsp->fileline(), new AstVarRef{lhsp->fileline(), lhsp, VAccess::WRITE}, orp};
        assp->user2Or(U2_BOTH);  // Don't process further; already resolved
        UINFOTREE(9, assp, "", "lhsp-eqn");
        nodep->addStmtsp(new AstAlways{assp});

        // If this is a top-level inout, make sure that the INOUT pins get __en and __out
        if (v3Global.opt.pinsInoutEnables() && isTopInout) {
            if (envarp) {
                envarp->primaryIO(true);
                envarp->direction(VDirection::OUTPUT);
            }
            if (outvarp) {
                outvarp->primaryIO(true);
                outvarp->direction(VDirection::OUTPUT);
            }
        }
    }

    bool isOnlyAssignmentIsToLhsVar(const AstAssignW* const nodep) {
        if (const AstVarRef* const varRefp = VN_CAST(nodep->lhsp(), VarRef)) {
            auto foundIt = m_assigns.find(varRefp->varp());
            if (foundIt != m_assigns.end()) {
                auto const& assignsToVar = foundIt->second;
                if (assignsToVar.size() == 1 && assignsToVar[0] == nodep) return true;
            }
        }
        return false;
    }

    void addToAssignmentList(AstAssignW* nodep) {
        if (AstVarRef* const varRefp = VN_CAST(nodep->lhsp(), VarRef)) {
            if (varRefp->varp()->isNet()) {
                m_assigns[varRefp->varp()].push_back(nodep);
            } else if (nodep->strengthSpecp()) {
                nodep->strengthSpecp()->unlinkFrBack()->deleteTree();
            }
        } else if (nodep->strengthSpecp()) {
            nodep->v3warn(E_UNSUPPORTED,
                          "Unsupported: Assignments with signal strength with LHS of type: "
                              << nodep->lhsp()->prettyTypeName());
        }
    }

    uint8_t getStrength(const AstAssignW* const nodep, bool value) {
        if (AstStrengthSpec* const strengthSpec = nodep->strengthSpecp()) {
            return value ? strengthSpec->strength1() : strengthSpec->strength0();
        }
        return VStrength::STRONG;  // default strength is strong
    }

    bool assignmentOfValueOnAllBits(const AstAssignW* const nodep, bool value) {
        if (const AstConst* const constp = VN_CAST(nodep->rhsp(), Const)) {
            const V3Number num = constp->num();
            return value ? num.isEqAllOnes() : num.isEqZero();
        }
        return false;
    }

    AstAssignW* getStrongestAssignmentOfValue(const Assigns& assigns, bool value) {
        auto maxIt = std::max_element(
            assigns.begin(), assigns.end(), [&](const AstAssignW* ap, const AstAssignW* bp) {
                const bool valuesOnRhsA = assignmentOfValueOnAllBits(ap, value);
                const bool valuesOnRhsB = assignmentOfValueOnAllBits(bp, value);
                if (!valuesOnRhsA) return valuesOnRhsB;
                if (!valuesOnRhsB) return false;
                return getStrength(ap, value) < getStrength(bp, value);
            });
        // If not all assignments have const with all bits equal to value on the RHS,
        // std::max_element will return one of them anyway, so it has to be checked before
        // returning
        return assignmentOfValueOnAllBits(*maxIt, value) ? *maxIt : nullptr;
    }

    bool isAssignmentNotStrongerThanStrength(const AstAssignW* assignp, uint8_t strength) {
        // If the value of the RHS is known and has all bits equal, only strength corresponding to
        // its value is taken into account. In opposite case, both strengths are compared.
        const uint8_t strength0 = getStrength(assignp, 0);
        const uint8_t strength1 = getStrength(assignp, 1);
        return (strength0 <= strength && strength1 <= strength)
               || (strength0 <= strength && assignmentOfValueOnAllBits(assignp, 0))
               || (strength1 <= strength && assignmentOfValueOnAllBits(assignp, 1));
    }

    AstNodeExpr* newMergeExpr(AstNodeExpr* const lhsp, AstNodeExpr* const rhsp, FileLine* const fl,
                              bool isWor) {
        AstNodeExpr* expr = nullptr;
        if (isWor)
            expr = new AstOr{fl, lhsp, rhsp};
        else
            expr = new AstAnd{fl, lhsp, rhsp};
        return expr;
    }

    void mergeWiredNetsAssignments() {
        // Support for WOR/TRIOR/WAND/TRIAND, by merging the Assignments for the
        // same Net (merge by or for WOR/TIOR and merge by and for WAND/TRIAND).
        for (auto& varpAssigns : m_assigns) {
            Assigns& assigns = varpAssigns.second;
            if (assigns.size() <= 1) continue;
            AstVar* const varp = varpAssigns.first;
            if (!varp->isWiredNet()) continue;
            auto it = assigns.begin();
            AstAssignW* const assignWp0 = *it;
            FileLine* const fl = assignWp0->fileline();
            AstNodeExpr* wExp = nullptr;
            while (++it != assigns.end()) {
                AstAssignW* assignWpi = *it;
                if (!wExp) {
                    wExp
                        = newMergeExpr(assignWp0->rhsp()->cloneTreePure(false),
                                       assignWpi->rhsp()->cloneTreePure(false), fl, varp->isWor());
                } else {
                    wExp = newMergeExpr(wExp, assignWpi->rhsp()->cloneTreePure(false), fl,
                                        varp->isWor());
                }
                VL_DO_DANGLING((assignWpi->unlinkFrBack()->deleteTree()), assignWpi);
            }
            AstVarRef* const wVarRef = new AstVarRef{fl, varp, VAccess::WRITE};
            AstAssignW* const wAssignp = new AstAssignW{fl, wVarRef, wExp};
            assignWp0->replaceWith(wAssignp);
            VL_DO_DANGLING(pushDeletep(assignWp0), assignWp0);
            assigns.clear();
            assigns.push_back(wAssignp);
        }
    }

    void removeNotStrongerAssignments(Assigns& assigns, const AstAssignW* strongestp,
                                      uint8_t greatestKnownStrength) {
        // Weaker assignments are these assignments that can't change the final value of the net.
        // They can be safely removed. Assignments of the same strength are also removed, because
        // duplicates aren't needed. One problem is with 2 assignments of different values and
        // equal strengths. It should result in assignment of x value, but these values aren't
        // supported now.
        auto removedIt = std::remove_if(assigns.begin(), assigns.end(), [&](AstAssignW* assignp) {
            if (assignp == strongestp) return false;
            if (isAssignmentNotStrongerThanStrength(assignp, greatestKnownStrength)) {
                // Vertices corresponding to nodes from removed assignment's subtree have to be
                // removed.
                m_tgraph.deleteVerticesFromSubtreeRecurse(assignp);
                VL_DO_DANGLING(pushDeletep(assignp->unlinkFrBack()), assignp);
                return true;
            }
            return false;
        });
        assigns.erase(removedIt, assigns.end());
    }

    void removeAssignmentsNotStrongerThanUniformConstant() {
        // If a stronger assignment of a constant with all bits equal to the same
        // value (0 or 1), is found, all weaker assignments can be safely removed.
        for (auto& varpAssigns : m_assigns) {
            Assigns& assigns = varpAssigns.second;
            if (assigns.size() <= 1) continue;
            const AstAssignW* const strongest0p = getStrongestAssignmentOfValue(assigns, 0);
            const AstAssignW* const strongest1p = getStrongestAssignmentOfValue(assigns, 1);
            const AstAssignW* strongestp = nullptr;
            uint8_t greatestKnownStrength = 0;
            const auto getIfStrongest
                = [&](const AstAssignW* const strongestCandidatep, bool value) {
                      if (!strongestCandidatep) return;
                      const uint8_t strength = getStrength(strongestCandidatep, value);
                      if (strength >= greatestKnownStrength) {
                          greatestKnownStrength = strength;
                          strongestp = strongestCandidatep;
                      }
                  };
            getIfStrongest(strongest0p, 0);
            getIfStrongest(strongest1p, 1);

            if (strongestp) {
                removeNotStrongerAssignments(assigns, strongestp, greatestKnownStrength);
            }
        }
    }

    void removeAssignmentsNotStrongerThanNonTristate() {
        // Similar function as removeAssignmentsNotStrongerThanUniformConstant, but here the
        // assignments that have strength not stronger than the strongest assignment with
        // non-tristate RHS are removed. Strengths are compared according to their smaller values,
        // because the values of RHSs are unknown. (Assignments not stronger than strongest
        // constant are already removed.)
        for (auto& varpAssigns : m_assigns) {
            Assigns& assigns = varpAssigns.second;
            if (assigns.size() <= 1) continue;
            auto maxIt = std::max_element(
                assigns.begin(), assigns.end(), [&](const AstAssignW* ap, const AstAssignW* bp) {
                    if (m_tgraph.isTristate(ap)) return !m_tgraph.isTristate(bp);
                    if (m_tgraph.isTristate(bp)) return false;
                    const uint8_t minStrengthA = std::min(getStrength(ap, 0), getStrength(ap, 1));
                    const uint8_t minStrengthB = std::min(getStrength(bp, 0), getStrength(bp, 1));
                    return minStrengthA < minStrengthB;
                });
            // If RHSs of all assignments are tristate, 1st element is returned, so it is
            // needed to check if it is non-tristate.
            const AstAssignW* const strongestp = m_tgraph.isTristate(*maxIt) ? nullptr : *maxIt;
            if (strongestp) {
                const uint8_t greatestKnownStrength
                    = std::min(getStrength(strongestp, 0), getStrength(strongestp, 1));
                removeNotStrongerAssignments(assigns, strongestp, greatestKnownStrength);
            }
        }
    }

    // VISITORS
    void visit(AstConst* nodep) override {
        UINFO(9, dbgState() << nodep);
        if (m_graphing) {
            if (!m_alhs && nodep->num().hasZ()) m_tgraph.setTristate(nodep);
        } else {
            // Detect any Z consts and convert them to 0's with an enable that is also 0.
            if (m_alhs && nodep->user1p()) {
                // A pin with 1'b0 or similar connection results in an assign with constant on LHS
                // due to the pinReconnectSimple call in visit AstPin.
                // We can ignore the output override by making a temporary
                AstVar* const varp = getCreateUnconnVarp(nodep, nodep->dtypep());
                AstNode* const newp = new AstVarRef{nodep->fileline(), varp, VAccess::WRITE};
                UINFO(9, " const->" << newp);
                nodep->replaceWith(newp);
                VL_DO_DANGLING(pushDeletep(nodep), nodep);
            } else if (m_tgraph.isTristate(nodep)) {
                m_tgraph.didProcess(nodep);
                FileLine* const fl = nodep->fileline();
                AstConst* const enp = getNonZConstp(nodep);
                V3Number num1{nodep, nodep->width()};
                num1.opAnd(nodep->num(), enp->num());  // 01X->01X, Z->0
                AstConst* const newconstp = new AstConst{fl, num1};
                nodep->replaceWith(newconstp);
                VL_DO_DANGLING(pushDeletep(nodep), nodep);
                newconstp->user1p(enp);  // Propagate up constant with non-Z bits as 1
            }
        }
    }

    void visit(AstCond* nodep) override {
        if (m_graphing) {
            iterateChildren(nodep);
            if (m_alhs) {
                associateLogic(nodep, nodep->thenp());
                associateLogic(nodep, nodep->elsep());
            } else {
                associateLogic(nodep->thenp(), nodep);
                associateLogic(nodep->elsep(), nodep);
            }
        } else {
            if (m_alhs && nodep->user1p()) {
                nodep->v3warn(E_UNSUPPORTED,
                              "Unsupported LHS tristate construct: " << nodep->prettyTypeName());
                return;
            }
            iterateChildren(nodep);
            UINFO(9, dbgState() << nodep);
            // Generate the new output enable signal for this cond if either
            // expression 1 or 2 have an output enable '__en' signal. If the
            // condition has an enable, not sure what to do, so generate an
            // error.
            AstNodeExpr* const condp = nodep->condp();
            if (condp->user1p()) {
                condp->v3warn(E_UNSUPPORTED, "Unsupported: don't know how to deal with "
                                             "tristate logic in the conditional expression");
            }
            AstNodeExpr* const thenp = nodep->thenp();
            AstNodeExpr* const elsep = nodep->elsep();
            if (thenp->user1p() || elsep->user1p()) {  // else no tristates
                m_tgraph.didProcess(nodep);
                AstNodeExpr* const en1p = getEnp(thenp);
                AstNodeExpr* const en2p = getEnp(elsep);
                // The output enable of a cond is a cond of the output enable of the
                // two expressions with the same conditional.
                AstNodeExpr* const enp
                    = new AstCond{nodep->fileline(), condp->cloneTree(false), en1p, en2p};
                UINFO(9, "       newcond " << enp);
                nodep->user1p(enp);  // propagate up COND(lhsp->enable, rhsp->enable)
                if (thenp->user1p() && !thenp->user1p()->backp()) pushDeletep(thenp->user1p());
                if (elsep->user1p() && !elsep->user1p()->backp()) pushDeletep(elsep->user1p());
                thenp->user1p(nullptr);
                elsep->user1p(nullptr);
            }
        }
    }

    void visit(AstExprStmt* nodep) override {
        iterateChildren(nodep);
        if (m_graphing) {
            UASSERT_OBJ(!m_alhs, nodep, "AstExprStmt node on the LHS of assignment");
            associateLogic(nodep->resultp(), nodep);
        } else if (nodep->resultp()->user1p()) {
            nodep->user1p(getEnp(nodep->resultp()));
            nodep->resultp()->user1p(nullptr);
            m_tgraph.didProcess(nodep);
        }
    }

    void visit(AstSel* nodep) override {
        if (m_graphing) {
            iterateChildren(nodep);
            if (m_alhs) {
                associateLogic(nodep, nodep->fromp());
            } else {
                associateLogic(nodep->fromp(), nodep);
            }
        } else {
            if (m_alhs) {
                UINFO(9, dbgState() << nodep);
                if (nodep->user1p()) {
                    // Form a "deposit" instruction.  Would be nicer if we made this a new AST type
                    AstNodeExpr* const newp
                        = newEnableDeposit(nodep, VN_AS(nodep->user1p(), NodeExpr));
                    nodep->fromp()->user1p(newp);  // Push to varref (etc)
                    UINFOTREE(9, newp, "", "assign-sel");
                    m_tgraph.didProcess(nodep);
                }
                iterateChildren(nodep);
            } else {
                iterateChildren(nodep);
                UINFO(9, dbgState() << nodep);
                if (nodep->lsbp()->user1p()) {
                    nodep->v3warn(E_UNSUPPORTED, "Unsupported RHS tristate construct: "
                                                     << nodep->prettyTypeName());
                }
                if (nodep->fromp()->user1p()) {  // SEL(VARREF, lsb)
                    AstNodeExpr* const en1p = getEnp(nodep->fromp());
                    AstNodeExpr* const enp
                        = new AstSel{nodep->fileline(), en1p, nodep->lsbp()->cloneTreePure(true),
                                     nodep->widthConst()};
                    UINFO(9, "       newsel " << enp);
                    nodep->user1p(enp);  // propagate up SEL(fromp->enable, value)
                    m_tgraph.didProcess(nodep);
                }
            }
        }
    }

    void visit(AstConcat* nodep) override {
        if (m_graphing) {
            iterateChildren(nodep);
            if (m_alhs) {
                associateLogic(nodep, nodep->lhsp());
                associateLogic(nodep, nodep->rhsp());
            } else {
                associateLogic(nodep->lhsp(), nodep);
                associateLogic(nodep->rhsp(), nodep);
            }
        } else {
            if (m_alhs) {
                UINFO(9, dbgState() << nodep);
                if (nodep->user1p()) {
                    // Each half of the concat gets a select of the enable expression
                    AstNodeExpr* const enp = VN_AS(nodep->user1p(), NodeExpr);
                    nodep->user1p(nullptr);
                    nodep->lhsp()->user1p(new AstSel{nodep->fileline(), enp->cloneTreePure(true),
                                                     nodep->rhsp()->width(),
                                                     nodep->lhsp()->width()});
                    nodep->rhsp()->user1p(
                        new AstSel{nodep->fileline(), enp, 0, nodep->rhsp()->width()});
                    m_tgraph.didProcess(nodep);
                }
                iterateChildren(nodep);
            } else {
                iterateChildren(nodep);
                UINFO(9, dbgState() << nodep);
                // Generate the new output enable signal, just as a concat
                // identical to the data concat
                AstNodeExpr* const expr1p = nodep->lhsp();
                AstNodeExpr* const expr2p = nodep->rhsp();
                if (expr1p->user1p() || expr2p->user1p()) {  // else no tristates
                    m_tgraph.didProcess(nodep);
                    AstNodeExpr* const en1p = getEnp(expr1p);
                    AstNodeExpr* const en2p = getEnp(expr2p);
                    AstNodeExpr* const enp = new AstConcat{nodep->fileline(), en1p, en2p};
                    UINFO(9, "       newconc " << enp);
                    nodep->user1p(enp);  // propagate up CONCAT(lhsp->enable, rhsp->enable)
                    expr1p->user1p(nullptr);
                    expr2p->user1p(nullptr);
                }
            }
        }
    }

    void visit(AstBufIf1* nodep) override {
        // For BufIf1, the enable is the LHS expression
        iterateChildren(nodep);
        UINFO(9, dbgState() << nodep);
        if (m_graphing) {
            associateLogic(nodep->rhsp(), nodep);
            m_tgraph.setTristate(nodep);
        } else {
            UINFOTREE(9, nodep->backp(), "", "bufif");
            if (m_alhs) {
                nodep->v3warn(E_UNSUPPORTED,
                              "Unsupported LHS tristate construct: " << nodep->prettyTypeName());
                return;
            }
            m_tgraph.didProcess(nodep);
            AstNodeExpr* const expr1p = nodep->lhsp()->unlinkFrBack();
            AstNodeExpr* const expr2p = nodep->rhsp()->unlinkFrBack();
            AstNodeExpr* enp;
            if (AstNodeExpr* const en2p = VN_AS(expr2p->user1p(), NodeExpr)) {
                enp = new AstAnd{nodep->fileline(), expr1p, en2p};
            } else {
                enp = expr1p;
            }
            expr1p->user1p(nullptr);
            expr2p->user1p(enp);  // Becomes new node
            // Don't need the BufIf any more, can just have the data direct
            nodep->replaceWith(expr2p);
            UINFO(9, "   bufif  datap=" << expr2p);
            UINFO(9, "   bufif  enp=" << enp);
            VL_DO_DANGLING(pushDeletep(nodep), nodep);
        }
    }

    void visitAndOr(AstNodeBiop* nodep, bool isAnd) {
        iterateChildren(nodep);
        UINFO(9, dbgState() << nodep);
        if (m_graphing) {
            associateLogic(nodep->lhsp(), nodep);
            associateLogic(nodep->rhsp(), nodep);
        } else {
            if (m_alhs && nodep->user1p()) {
                nodep->v3warn(E_UNSUPPORTED,
                              "Unsupported LHS tristate construct: " << nodep->prettyTypeName());
                return;
            }
            // ANDs and Z's have issues. Earlier optimizations convert
            // expressions like "(COND) ? 1'bz : 1'b0" to "COND & 1'bz". So we
            // have to define what is means to AND 1'bz with other
            // expressions. I don't think this is spec, but here I take the
            // approach that when one expression is 1, that the Z passes. This
            // makes the COND's work. It is probably better to not perform the
            // conditional optimization if the bits are Z.
            //
            // ORs have the same issues as ANDs. Earlier optimizations convert
            // expressions like "(COND) ? 1'bz : 1'b1" to "COND | 1'bz". So we
            // have to define what is means to OR 1'bz with other
            // expressions. Here I take the approach that when one expression
            // is 0, that is passes the other.
            AstNodeExpr* const expr1p = nodep->lhsp();
            AstNodeExpr* const expr2p = nodep->rhsp();
            if (!expr1p->user1p() && !expr2p->user1p()) {
                return;  // no tristates in either expression, so nothing to do
            }
            m_tgraph.didProcess(nodep);
            AstNodeExpr* const en1p = getEnp(expr1p);
            AstNodeExpr* const en2p = getEnp(expr2p);
            AstNodeExpr* subexpr1p = expr1p->cloneTreePure(false);
            AstNodeExpr* subexpr2p = expr2p->cloneTreePure(false);
            if (isAnd) {
                subexpr1p = new AstNot{nodep->fileline(), subexpr1p};
                subexpr2p = new AstNot{nodep->fileline(), subexpr2p};
            }
            // calc new output enable
            AstNodeExpr* const enp = new AstOr{
                nodep->fileline(), new AstAnd{nodep->fileline(), en1p, en2p},
                new AstOr{nodep->fileline(),
                          new AstAnd{nodep->fileline(), en1p->cloneTree(false), subexpr1p},
                          new AstAnd{nodep->fileline(), en2p->cloneTree(false), subexpr2p}}};
            UINFO(9, "       neweqn " << enp);
            nodep->user1p(enp);
            expr1p->user1p(nullptr);
            expr2p->user1p(nullptr);
        }
    }
    void visit(AstAnd* nodep) override { visitAndOr(nodep, true); }
    void visit(AstOr* nodep) override { visitAndOr(nodep, false); }

    void visitAssign(AstNodeAssign* nodep) {
        VL_RESTORER(m_alhs);
        VL_RESTORER(m_currentStrength);
        if (VN_IS(nodep->rhsp(), CReset)) return;
        if (m_graphing) {
            if (AstAssignW* assignWp = VN_CAST(nodep, AssignW)) {
                if (assignWp->timingControlp() || assignWp->getLhsNetDelay()) return;
                addToAssignmentList(assignWp);
            }

            if (nodep->user2() & U2_GRAPHING) return;
            VL_RESTORER(m_logicp);
            m_logicp = nodep;
            nodep->user2Or(U2_GRAPHING);
            iterateAndNextNull(nodep->rhsp());
            m_alhs = true;
            iterateAndNextNull(nodep->lhsp());
            m_alhs = false;
            associateLogic(nodep->rhsp(), nodep);
            associateLogic(nodep, nodep->lhsp());
        } else {
            if (nodep->user2() & U2_NONGRAPH) {
                return;  // Iterated here, or created assignment to ignore
            }
            nodep->user2Or(U2_NONGRAPH);
            iterateAndNextNull(nodep->rhsp());
            UINFO(9, dbgState() << nodep);
            UINFOTREE(9, nodep, "", "assign");
            // if the rhsp of this assign statement has an output enable driver,
            // then propagate the corresponding output enable assign statement.
            // down the lvalue tree by recursion for eventual attachment to
            // the appropriate output signal's VarRef.
            if (nodep->rhsp()->user1p()) {
                nodep->lhsp()->user1p(nodep->rhsp()->user1p());
                nodep->rhsp()->user1p(nullptr);
                UINFO(9, "   enp<-rhs " << nodep->lhsp()->user1p());
                m_tgraph.didProcess(nodep);
            } else {
                // Non-tristate RHS. For SEL assigns to tristate variables, we still
                // need to track which bits are driven by creating a proper enable.
                if (AstSel* const selp = VN_CAST(nodep->lhsp(), Sel)) {
                    if (AstNodeVarRef* const varrefp = VN_CAST(selp->fromp(), NodeVarRef)) {
                        if (m_tgraph.isTristate(varrefp->varp())) {
                            // Create an all-1s enable for the SEL width - this will be
                            // deposited into the correct bit positions by newEnableDeposit
                            nodep->lhsp()->user1p(newAllZerosOrOnes(nodep->lhsp(), true));
                            UINFO(9, "   enp<-nonTri SEL " << nodep->lhsp()->user1p());
                            m_tgraph.didProcess(nodep);
                        } else {
                            m_tgraph.didProcess(nodep, true);
                        }
                    } else {
                        m_tgraph.didProcess(nodep, true);  // LCOV_EXCL_LINE
                    }
                } else {
                    // Don't set user1p here as there are no handlers for many LHS node types
                    // (ArraySel, MemberSel, StructSel, etc.) and checkUnhandled() would error.
                    m_tgraph.didProcess(nodep, true);
                }
            }
            m_alhs = true;  // And user1p() will indicate tristate equation, if any
            if (AstAssignW* const assignWp = VN_CAST(nodep, AssignW)) {
                if (AstStrengthSpec* const specp = assignWp->strengthSpecp()) {
                    if (specp->strength0() != specp->strength1()) {
                        // Unequal strengths are not a problem if the assignment is the only
                        // assignment to its variable. Unfortunately, m_assigns map stores only
                        // assignments to var. Selects are not inserted, so they may be handled
                        // improperly
                        if (!isOnlyAssignmentIsToLhsVar(assignWp)) {
                            assignWp->v3warn(
                                E_UNSUPPORTED,
                                "Unsupported: Unable to resolve unequal strength specifier");
                        }
                    } else {
                        m_currentStrength = specp->strength0();
                    }
                }
            }
            iterateAndNextNull(nodep->lhsp());
        }
    }
    void visit(AstAssignW* nodep) override { visitAssign(nodep); }
    void visit(AstAssign* nodep) override { visitAssign(nodep); }
    void visit(AstAssignDly* nodep) override { visitAssign(nodep); }
    void visit(AstAlias* nodep) override {
        VL_RESTORER(m_alhs);
        VL_RESTORER(m_inAlias);
        m_inAlias = true;
        if (m_graphing) {
            if (nodep->user2() & U2_GRAPHING) return;
            m_alhs = true;  // In AstAlias all operands should be considered as lhs
            iterateChildren(nodep);
            for (AstNode* itemp = nodep->itemsp(); itemp; itemp = itemp->nextp()) {
                associateLogic(itemp, nodep);
                associateLogic(nodep, itemp);
            }
        } else {
            iterateChildren(nodep);
        }
    }

    void visitCaseEq(AstNodeBiop* nodep, bool neq) {
        if (m_graphing) {
            iterateChildren(nodep);
        } else {
            checkUnhandled(nodep);
            // Unsupported: A === 3'b000 should compare with the enables, but we don't do
            // so at present, we only compare if there is a z in the equation.
            // Otherwise we'd need to attach an enable to every signal, then optimize them
            // away later when we determine the signal has no tristate
            iterateChildren(nodep);
            UINFO(9, dbgState() << nodep);
            // Constification always moves const to LHS
            AstConst* const constp = VN_CAST(nodep->lhsp(), Const);
            if (constp && constp->user1p()) {
                // 3'b1z0 -> ((3'b101 == in__en) && (3'b100 == in))
                AstNodeExpr* const rhsp = nodep->rhsp();
                rhsp->unlinkFrBack();
                FileLine* const fl = nodep->fileline();
                AstNodeExpr* enRhsp;
                if (rhsp->user1p()) {
                    enRhsp = VN_AS(rhsp->user1p(), NodeExpr);
                    rhsp->user1p(nullptr);
                } else {
                    enRhsp = getEnExprBasedOnOriginalp(rhsp);
                }
                AstNodeExpr* newp
                    = new AstLogAnd{fl, new AstEq{fl, VN_AS(constp->user1p(), Const), enRhsp},
                                    // Keep the caseeq if there are X's present
                                    new AstEqCase{fl, new AstConst{fl, constp->num()}, rhsp}};
                constp->user1p(nullptr);
                if (neq) newp = new AstLogNot{fl, newp};
                UINFO(9, "       newceq " << newp);
                UINFOTREE(9, nodep, "", "caseeq-old");
                UINFOTREE(9, newp, "", "caseeq-new");
                nodep->replaceWith(newp);
                VL_DO_DANGLING(pushDeletep(nodep), nodep);
            } else if (constp && nodep->rhsp()->user1p()) {
                FileLine* const fl = nodep->fileline();
                constp->unlinkFrBack();
                AstNodeExpr* const rhsp = nodep->rhsp()->unlinkFrBack();
                AstNodeExpr* newp = new AstLogAnd{fl,
                                                  new AstEq{fl, newAllZerosOrOnes(constp, false),
                                                            VN_AS(rhsp->user1p(), NodeExpr)},
                                                  // Keep the caseeq if there are X's present
                                                  new AstEqCase{fl, constp, rhsp}};
                if (neq) newp = new AstLogNot{fl, newp};
                rhsp->user1p(nullptr);
                UINFO(9, "       newceq " << newp);
                UINFOTREE(9, nodep, "", "caseeq-old");
                UINFOTREE(9, newp, "", "caseeq-new");
                nodep->replaceWith(newp);
                VL_DO_DANGLING(pushDeletep(nodep), nodep);
            } else {
                checkUnhandled(nodep);
            }
        }
    }
    void visitEqNeqWild(AstNodeBiop* nodep) {
        if (!VN_IS(nodep->rhsp(), Const) && nodep->rhsp()->dtypep()->isFourstate()) {
            nodep->v3warn(E_UNSUPPORTED,
                          "Unsupported: RHS of ==? or !=? is fourstate but not a constant");
            // rhs we want to keep X/Z intact, so otherwise ignore
        }
        iterateAndNextNull(nodep->lhsp());
        if (nodep->lhsp()->user1p()) {
            nodep->v3warn(E_UNSUPPORTED,
                          "Unsupported LHS tristate construct: " << nodep->prettyTypeName());
            return;
        }
    }
    void visit(AstEqCase* nodep) override { visitCaseEq(nodep, false); }
    void visit(AstNeqCase* nodep) override { visitCaseEq(nodep, true); }
    void visit(AstEqWild* nodep) override { visitEqNeqWild(nodep); }
    void visit(AstNeqWild* nodep) override { visitEqNeqWild(nodep); }

    void visit(AstCountBits* nodep) override {
        std::array<bool, 3> dropop;
        dropop[0] = VN_IS(nodep->rhsp(), Const) && VN_AS(nodep->rhsp(), Const)->num().isAnyZ();
        dropop[1] = VN_IS(nodep->thsp(), Const) && VN_AS(nodep->thsp(), Const)->num().isAnyZ();
        dropop[2] = VN_IS(nodep->fhsp(), Const) && VN_AS(nodep->fhsp(), Const)->num().isAnyZ();
        UINFO(4, " COUNTBITS(" << dropop[0] << dropop[1] << dropop[2] << " " << nodep);
        if (m_graphing) {
            iterateAndNextNull(nodep->lhsp());
            if (!dropop[0]) iterateAndNextNull(nodep->rhsp());
            if (!dropop[1]) iterateAndNextNull(nodep->thsp());
            if (!dropop[2]) iterateAndNextNull(nodep->fhsp());
        } else {
            AstNodeExpr* nonXp = nullptr;
            if (!dropop[0]) {
                nonXp = nodep->rhsp();
            } else if (!dropop[1]) {
                nonXp = nodep->thsp();
            } else if (!dropop[2]) {
                nonXp = nodep->fhsp();
            }
            // Replace 'z with non-Z
            if (dropop[0] || dropop[1] || dropop[2]) {
                // Unsupported: A $countones('0) should compare with the enables, but we don't
                // do so at present, we only compare if there is a z in the equation. Otherwise
                // we'd need to attach an enable to every signal, then optimize them away later
                // when we determine the signal has no tristate
                const AstVarRef* const varrefp = VN_CAST(nodep->lhsp(), VarRef);  // Input variable
                if (!varrefp) {
                    nodep->v3warn(E_UNSUPPORTED, "Unsupported LHS tristate construct: "
                                                     << nodep->prettyTypeName());
                    return;
                }
                AstVar* const envarp = getCreateEnVarp(varrefp->varp(), false);
                // If any drops, we need to add in the count of Zs (from __en)
                UINFO(4, " COUNTBITS('z)-> " << nodep);
                VNRelinker relinkHandle;
                nodep->unlinkFrBack(&relinkHandle);
                AstNodeExpr* newp = new AstCountOnes{
                    nodep->fileline(), new AstVarRef{nodep->fileline(), envarp, VAccess::READ}};
                if (nonXp) {  // Need to still count '0 or '1 or 'x's
                    if (dropop[0]) {
                        nodep->rhsp()->unlinkFrBack()->deleteTree();
                        nodep->rhsp(nonXp->cloneTreePure(true));
                    }
                    if (dropop[1]) {
                        nodep->thsp()->unlinkFrBack()->deleteTree();
                        nodep->thsp(nonXp->cloneTreePure(true));
                    }
                    if (dropop[2]) {
                        nodep->fhsp()->unlinkFrBack()->deleteTree();
                        nodep->fhsp(nonXp->cloneTreePure(true));
                    }
                    newp = new AstAdd{nodep->fileline(), nodep, newp};
                } else {
                    // TODO: looks dubious that we still iterate this below...
                    pushDeletep(nodep);
                }
                UINFOTREE(9, newp, "", "countout");
                relinkHandle.relink(newp);
            }
            iterateChildren(nodep);
        }
    }
    void visit(AstPull* nodep) override {
        UINFO(9, dbgState() << nodep);
        AstVarRef* varrefp = nullptr;
        if (VN_IS(nodep->lhsp(), VarRef)) {
            varrefp = VN_AS(nodep->lhsp(), VarRef);
        } else if (VN_IS(nodep->lhsp(), Sel)
                   && VN_IS(VN_AS(nodep->lhsp(), Sel)->fromp(), VarRef)) {
            varrefp = VN_AS(VN_AS(nodep->lhsp(), Sel)->fromp(), VarRef);
        }
        if (!varrefp) {
            UINFOTREE(4, nodep, "", "");
            nodep->v3warn(E_UNSUPPORTED, "Unsupported pullup/down (weak driver) construct.");
        } else {
            if (m_graphing) {
                VL_RESTORER(m_logicp);
                m_logicp = nodep;
                varrefp->access(VAccess::WRITE);
                m_tgraph.setTristate(nodep);
                associateLogic(nodep, varrefp->varp());
            } else {
                // Replace any pullup/pulldowns with assignw logic and set the
                // direction of the pull in the var aux data.  Given
                // the complexity of merging tristate drivers at any level, the
                // current limitation of this implementation is that a pullup/down
                // gets applied to all bits of a bus and a bus cannot have drivers
                // in opposite directions on individual pins.
                varrefp->access(VAccess::WRITE);
                m_tgraph.didProcess(nodep);
                m_tgraph.didProcess(varrefp->varp());
                setPullDirection(varrefp->varp(), nodep);
            }
        }
        if (!m_graphing) {
            nodep->unlinkFrBack();
            // Node must persist as var aux data points to it
            VL_DO_DANGLING(pushDeletep(nodep), nodep);
        }
    }

    void iteratePinGuts(AstPin* nodep) {
        if (m_graphing) {
            VL_RESTORER(m_logicp);
            m_logicp = nodep;
            if (nodep->exprp()) {
                associateLogic(nodep->exprp(), nodep);
                associateLogic(nodep, nodep->exprp());
            }
            iterateChildren(nodep);
        } else {
            // All heavy lifting completed in graph visitor.
            if (nodep->exprp()) m_tgraph.didProcess(nodep);
            iterateChildren(nodep);
        }
    }

    // .tri(SEL(trisig,x)) becomes
    //   INPUT:   -> (VARREF(trisig__pinin)),
    //               trisig__pinin = SEL(trisig,x)       // via pinReconnectSimple
    //   OUTPUT:  -> (VARREF(trisig__pinout))
    //               SEL(trisig,x) = trisig__pinout
    //                                  ^-- ->user1p() == trisig__pinen
    //   ENABLE:  -> (VARREF(trisig__pinen)
    // Added complication is the signal may be an output/inout or just
    // input with tie off (or not) up top
    //     PIN  PORT    NEW PORTS AND CONNECTIONS
    //     N/C  input   in(from-resolver), __en(to-resolver-only), __out(to-resolver-only)
    //     N/C  inout   Spec says illegal
    //     N/C  output  Unsupported; Illegal?
    //     wire input   in(from-resolver-with-wire-value), __en(from-resolver-wire),
    //                     __out(to-resolver-only)
    //     wire inout   in, __en, __out
    //     wire output  in, __en, __out
    //     const input  in(from-resolver-with-const-value), __en(from-resolver-const),
    //                     __out(to-resolver-only)
    //     const inout  Spec says illegal
    //     const output Unsupported; Illegal?
    void visit(AstPin* nodep) override {
        if (m_graphing) {
            if (nodep->user2() & U2_GRAPHING) return;  // This pin is already expanded
            nodep->user2Or(U2_GRAPHING);
            // Find child module's new variables.
            AstVar* const enModVarp = static_cast<AstVar*>(nodep->modVarp()->user1p());
            if (!enModVarp) {
                // May have an output only that later connects to a tristate, so simplify now.
                V3Inst::pinReconnectSimple(nodep, m_cellp, false);
                iteratePinGuts(nodep);
                return;  // No __en signals on this pin
            }
            // Tristate exists:
            UINFO(9, dbgState() << nodep);
            UINFOTREE(9, nodep, "", "pin-pre");

            // Empty/in-only; need Z to propagate
            const bool inDeclProcessing = (nodep->exprp()
                                           && nodep->modVarp()->direction() == VDirection::INPUT
                                           // Need to consider the original state
                                           // instead of current state as we converted
                                           // tristates to inputs, which do not want
                                           // to have this.
                                           && !nodep->modVarp()->declDirection().isWritable());
            if (!nodep->exprp()) {  // No-connect; covert to empty connection
                UINFO(5, "Unconnected pin terminate " << nodep);
                AstVar* const ucVarp = getCreateUnconnVarp(nodep, nodep->modVarp()->dtypep());
                nodep->exprp(new AstVarRef{nodep->fileline(), ucVarp,
                                           // We converted, so use declaration output state
                                           nodep->modVarp()->declDirection().isWritable()
                                               ? VAccess::WRITE
                                               : VAccess::READ});
                m_tgraph.setTristate(ucVarp);
                // We don't need a driver on the wire; the lack of one will default to tristate
            } else if (inDeclProcessing) {  // Not an input that was a converted tristate
                // Input only may have driver in underneath module which would stomp
                // the input value.  So make a temporary connection.
                const AstAssignW* const reAssignp
                    = V3Inst::pinReconnectSimple(nodep, m_cellp, true, true);
                UINFO(5, "Input pin buffering: " << reAssignp);
                m_tgraph.setTristate(reAssignp->lhsp());
            }

            // pinReconnectSimple needs to presume input or output behavior; we need both
            // Therefore, create the enable, output and separate input pin,
            // then pinReconnectSimple all
            // Create the output enable pin, connect to new signal
            AstVar* const enVarp = new AstVar{nodep->fileline(), VVarType::MODULETEMP,
                                              nodep->name() + "__en" + cvtToStr(m_unique++),
                                              VFlagBitPacked{}, enModVarp->width()};
            if (inDeclProcessing) {  // __en(from-resolver-const) or __en(from-resolver-wire)
                enModVarp->varType2In();
            } else {
                enModVarp->varType2Out();
            }
            UINFO(9, "       newenv " << enVarp);
            AstPin* const enpinp
                = new AstPin{nodep->fileline(), nodep->pinNum(),
                             enModVarp->name(),  // should be {var}"__en"
                             new AstVarRef{nodep->fileline(), enVarp,
                                           inDeclProcessing ? VAccess::READ : VAccess::WRITE}};
            enpinp->modVarp(enModVarp);
            UINFO(9, "       newpin " << enpinp);
            enpinp->user2Or(U2_BOTH);  // don't iterate the pin later
            nodep->addNextHere(enpinp);
            m_modp->addStmtsp(enVarp);
            UINFOTREE(9, enpinp, "", "pin-ena");

            // Create new output pin
            const AstAssignW* outAssignp = nullptr;  // If reconnected, the related assignment
            AstPin* outpinp = nullptr;
            AstVar* const outModVarp = m_varAux(nodep->modVarp()).outVarp;
            if (!outModVarp) {
                // At top, no need for __out as might be input only. Otherwise resolvable.
                if (!m_modp->isTop()) {
                    nodep->v3warn(E_UNSUPPORTED, "Unsupported: tristate in top-level IO: "
                                                     << nodep->prettyNameQ());
                }
            } else {
                AstNodeExpr* const outexprp = VN_AS(nodep->exprp(), NodeExpr)
                                                  ->cloneTreePure(false);  // Note has lvalue() set
                outpinp = new AstPin{nodep->fileline(), nodep->pinNum(),
                                     outModVarp->name(),  // should be {var}"__out"
                                     outexprp};
                outpinp->modVarp(outModVarp);
                UINFO(9, "       newpin " << outpinp);
                outpinp->user2Or(U2_BOTH);  // don't iterate the pin later
                nodep->addNextHere(outpinp);
                // Simplify
                if (inDeclProcessing) {  // Not an input that was a converted tristate
                    // The pin is an input, but we need an output
                    // The if() above is needed because the Visitor is
                    // simple, it will flip ArraySel's and such, but if the
                    // pin is an input the earlier reconnectSimple made it
                    // a VarRef without any ArraySel, etc
                    TristatePinVisitor{outexprp, m_tgraph, true};
                }
                UINFOTREE(9, outpinp, "", "pin-opr");
                outAssignp = V3Inst::pinReconnectSimple(outpinp, m_cellp,
                                                        true);  // Note may change outpinp->exprp()
                UINFOTREE(9, outpinp, "", "pin-out");
                UINFOTREE(9, outAssignp, "", "pin-oas");
                // Must still iterate the outAssignp, as need to build output equation
            }

            // Existing pin becomes an input, and we mark each resulting signal as tristate
            const TristatePinVisitor visitor{nodep->exprp(), m_tgraph, false};
            const AstNode* const inAssignp = V3Inst::pinReconnectSimple(
                nodep, m_cellp, true);  // Note may change nodep->exprp()
            UINFOTREE(9, nodep, "", "pin-in:");
            UINFOTREE(9, inAssignp, "", "pin-as:");

            // Connect enable to output signal
            AstVarRef* exprrefp;  // Tristate variable that the Pin's expression refers to
            if (!outAssignp) {
                if (!outpinp) {
                    exprrefp = nullptr;  // Primary input only
                } else {
                    // pinReconnect should have converted this
                    exprrefp = VN_CAST(outpinp->exprp(), VarRef);
                    if (!exprrefp) {
                        nodep->v3warn(E_UNSUPPORTED, "Unsupported tristate port expression: "
                                                         << nodep->exprp()->prettyTypeName());
                    }
                }
            } else {
                // pinReconnect should have converted this
                exprrefp = VN_CAST(outAssignp->rhsp(),
                                   VarRef);  // This should be the same var as the output pin
                if (!exprrefp) {
                    nodep->v3warn(E_UNSUPPORTED, "Unsupported tristate port expression: "
                                                     << nodep->exprp()->prettyTypeName());
                }
            }
            if (exprrefp) {
                UINFO(9, "outref " << exprrefp);
                // Mark as now tristated; iteration will pick it up from there
                exprrefp->user1p(new AstVarRef{nodep->fileline(), enVarp, VAccess::READ});
                if (!outAssignp) {
                    mapInsertLhsVarRef(exprrefp);  // insertTristates will convert
                    //                     // to a varref to the __out# variable
                }  // else the assignment deals with the connection
            }

            // Propagate any pullups/pulldowns upwards if necessary
            if (exprrefp) {
                AstPull* const pullp = m_varAux(nodep->modVarp()).pullp;
                const auto& srcBitPulls = m_varAux(nodep->modVarp()).bitPulls;

                // For part-select port connections (e.g. .out(bus[23:16])) extract
                // the parent var, LSB, and width once; reused by both propagations.
                AstVar* selVarp = nullptr;
                int selLsb = 0;
                int selWidth = 0;
                if (outAssignp) {
                    if (AstSel* const selp = VN_CAST(outAssignp->lhsp(), Sel)) {
                        AstVarRef* const vrefp = VN_CAST(selp->fromp(), VarRef);
                        AstConst* const lsbp = VN_CAST(selp->lsbp(), Const);
                        if (vrefp && lsbp) {  // LCOV_EXCL_BR_LINE
                            selVarp = vrefp->varp();
                            selLsb = lsbp->toSInt();
                            selWidth = selp->widthConst();
                        }
                    }
                }

                if (pullp) {
                    UINFO(9, "propagate pull on " << exprrefp);
                    setPullDirection(exprrefp->varp(), pullp);
                    // For a part-select target, record per-bit pull direction across
                    // the SEL range instead of setting pull on the whole variable.
                    if (selVarp) {
                        const int direction = pullp->direction();
                        for (int i = 0; i < selWidth; ++i) {
                            setBitPullDirection(selVarp, selLsb + i, direction);
                        }
                    }
                }

                // Per-bit pulls: the source bit indices must be offset by the SEL's
                // LSB so they land on the correct bits of the parent variable.
                if (!srcBitPulls.empty()) {
                    UINFO(9, "propagate per-bit pulls from " << nodep->modVarp());
                    for (const auto& pair : srcBitPulls) {
                        setBitPullDirection(exprrefp->varp(), pair.first, pair.second);
                    }  // LCOV_EXCL_LINE
                    if (selVarp) {
                        UINFO(9, "propagate per-bit pulls to SEL target " << selVarp
                                                                          << " offset=" << selLsb);
                        for (const auto& pair : srcBitPulls) {
                            setBitPullDirection(selVarp, selLsb + pair.first, pair.second);
                        }  // LCOV_EXCL_LINE
                    }
                }
            }
            // Don't need to visit the created assigns, as it was added at
            // the end of the next links and normal iterateChild recursion
            // will come back to them eventually.
            // Mark the original signal as tristated
            iteratePinGuts(nodep);
        }
        // Not graph building
        else {
            if (nodep->user2() & U2_NONGRAPH) return;  // This pin is already expanded
            nodep->user2Or(U2_NONGRAPH);
            UINFO(9, " " << nodep);
            iteratePinGuts(nodep);
        }
    }

    void handleNodeVarRef(AstNodeVarRef* nodep) {
        UINFO(9, dbgState() << nodep);
        UASSERT_OBJ(nodep->varp(), nodep, "Unlinked");
        if (m_graphing) {
            if (nodep->access().isWriteOrRW()) associateLogic(nodep, nodep->varp());
            if (nodep->access().isReadOrRW()) associateLogic(nodep->varp(), nodep);
        } else {
            if (nodep->user2() & U2_NONGRAPH) return;  // Processed
            nodep->user2Or(U2_NONGRAPH);
            // Detect all var lhs drivers and adds them to the
            // VarMap so that after the walk through the module we can expand
            // any tristate logic on the driver.
            if (nodep->access().isWriteOrRW() && m_tgraph.isTristate(nodep->varp())) {
                UINFO(9, "     Ref-to-lvalue " << nodep);
                if (m_inAlias) {
                    if (nodep->varp()->direction().isAny()) {
                        nodep->v3warn(E_UNSUPPORTED, "Unsupported: Port as alias argument: "
                                                         << nodep->prettyNameQ());
                    } else {
                        nodep->v3warn(E_UNSUPPORTED,
                                      "Unsupported: Tristate variable referenced in alias: "
                                          << nodep->prettyNameQ());
                    }
                    return;
                }
                UASSERT_OBJ(!nodep->access().isRW(), nodep, "Tristate unexpected on R/W access");
                m_tgraph.didProcess(nodep);
                mapInsertLhsVarRef(nodep);
            } else if (nodep->access().isReadOnly()
                       // Not already processed, nor varref from visit(AstPin) creation
                       && !nodep->user1p()
                       // Reference to another tristate variable
                       && m_tgraph.isTristate(nodep->varp())
                       // and in a position where it feeds upstream to another tristate
                       && 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);
                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);
        if (m_graphing) {
            // If tri0/1 force a pullup
            if (nodep->user2() & U2_GRAPHING) return;  // Already processed
            nodep->user2Or(U2_GRAPHING);

            if (nodep->isPulldown() || nodep->isPullup()) {
                AstNode* const newp = new AstPull{
                    nodep->fileline(), new AstVarRef{nodep->fileline(), nodep, VAccess::WRITE},
                    nodep->isPullup()};
                UINFO(9, "       newpul " << newp);
                nodep->addNextHere(newp);
                // We'll iterate on the new AstPull later
            }
            if (nodep->isInout()) {
                //|| varp->isOutput()
                // Note unconnected output only changes behavior vs. previous
                // versions and causes outputs that don't come from anywhere to
                // possibly create connection errors.
                // One example of problems is this:  "output z;  task t; z <= {something}; endtask"
                UINFO(9, "  setTristate-inout " << nodep);
                m_tgraph.setTristate(nodep);
            }
        } else {  // !graphing
            if (m_tgraph.isTristate(nodep)) {
                // nodep->isPulldown() || nodep->isPullup() handled in TristateGraphVisitor
                m_tgraph.didProcess(nodep);
            }
        }
    }

    void visit(AstNodeModule* nodep) override {
        UINFO(8, dbgState() << nodep);
        VL_RESTORER(m_modp);
        VL_RESTORER(m_graphing);
        VL_RESTORER(m_unique);
        VL_RESTORER(m_lhsmap);
        VL_RESTORER(m_assigns);
        // Not preserved, needs pointer instead: TristateGraph origTgraph = m_tgraph;
        UASSERT_OBJ(m_tgraph.empty(), nodep, "Unsupported: NodeModule under NodeModule");

        // Clear state
        m_graphing = false;
        m_tgraph.clearAndCheck();
        m_unique = 0;
        m_logicp = nullptr;
        m_lhsmap.clear();
        m_assigns.clear();
        m_modp = nodep;
        // Walk the graph, finding all variables and tristate constructs
        m_graphing = true;
        UINFO(9, dbgState() << "graphing mod " << nodep);
        iterateChildren(nodep);
        m_graphing = false;
        UINFO(9, dbgState() << "processing mod " << nodep);

        // Merge the assignments for very Wired net LHS : wor, trior, wand and triand
        mergeWiredNetsAssignments();
        // Remove all assignments not stronger than the strongest uniform constant
        removeAssignmentsNotStrongerThanUniformConstant();
        // Use graph to find tristate signals
        m_tgraph.graphWalk(nodep);

        // Remove all assignments not stronger than the strongest non-tristate RHS
        removeAssignmentsNotStrongerThanNonTristate();

        // Build the LHS drivers map for this module
        iterateChildren(nodep);
        // Insert new logic for all tristates
        insertTristates(nodep);

        m_tgraph.clearAndCheck();  // Recursion not supported
    }

    void visit(AstClass* nodep) override {
        // don't deal with classes
    }
    void visit(AstNodeFTask* nodep) override {
        // don't deal with functions
    }

    void visit(AstCaseItem* nodep) override {
        // don't deal with casez compare '???? values
        iterateAndNextNull(nodep->stmtsp());
    }

    void visit(AstCell* nodep) override {
        VL_RESTORER(m_cellp);
        VL_RESTORER(m_alhs);
        m_cellp = nodep;
        m_alhs = false;
        iterateChildren(nodep);
    }

    // Default: Just iterate
    void visit(AstNode* nodep) override {
        iterateChildren(nodep);
        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 (const std::pair<AstVar* const, std::vector<std::pair<AstVar*, AstVar*>>>& kv :
             m_ifaceContribs) {
            AstVar* const invarp = kv.first;
            const 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 (const 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;
        netlistp->foreach([&](AstNode* nodep) {
            AstNode* const enp = nodep->user1p();
            if (!enp) return;
            if (enp->backp()) return;
            unusedRootps.emplace_back(enp);
        });
        for (AstNode* const nodep : unusedRootps) VL_DO_DANGLING(nodep->deleteTree(), nodep);
#endif
    }
    ~TristateVisitor() override {
        V3Stats::addStat("Tristate, Tristate resolved nets", m_statTriSigs);
    }
};

//######################################################################
// Tristate class functions

void V3Tristate::tristateAll(AstNetlist* nodep) {
    UINFO(2, __FUNCTION__ << ":");
    { TristateVisitor{nodep}; }  // Destruct before checking
    V3Global::dumpCheckGlobalTree("tristate", 0, dumpTreeEitherLevel() >= 3);
}
