Lower bit-scan loop idioms to $mostsetbitp1 / $countones

Recognize the common single-bit scan loop idioms in V3Unroll (before it
unrolls) and lower them to bit-reduction primitives, replacing a literal
W-iteration loop with one intrinsic-backed expression:

  target=0; for (i=0;i<W;i++) if (vec[i]) target = i + 1;      -> $mostsetbitp1(vec)
  target=0; for (i=0;i<W;i++) if (vec[i]) target = target + 1; -> $countones(vec)

The leading-one form lowers to a new AstMostSetBitP1 node, emitted as
VL_MOSTSETBITP1_{I,Q,W}; those runtime helpers now use __builtin_clz where
available (same pattern as VL_REDXOR's __builtin_parity), with the existing
bit scan as fallback.  The count-ones form reuses AstCountOnes ($countones,
popcount); as the DFG requires a 32-bit countones result it is built at 32
bits and narrowed to the accumulator width with a select.

Matching is structural to stay sound: the index must start at 0, increment
by exactly 1, and scan all W==width(vec) bits via a single 1-bit select of a
distinct vector, with the target pre-zeroed and no else branch.  The loop
bound is accepted as a strict ascending 'idx < W' written either way and
signed or unsigned (Gt/GtS/Lt/LtS).  Gated by -fbit-scan-loops (on at -O).

Adds t_bit_scan_loops (I/Q/W, count-ones and unsigned-index positives;
step-2, start-1, idx*2+1, vec[idx+1], target=idx and W!=width negatives, all
self-checked and asserted via --stats not to lower) plus t_bit_scan_loops_off
for the disable flag.

Motivated by a transformer inference design whose 80-bit leading-one detector
ran every cycle (~37% of runtime); the lowering is worth ~39% there.
This commit is contained in:
Thomas Santerre 2026-06-20 12:01:24 -04:00
parent a37e2ee94b
commit 6c2cfb31e9
12 changed files with 349 additions and 6 deletions

View File

@ -272,6 +272,7 @@ Teng Huang
Thomas Aldrian Thomas Aldrian
Thomas Brown Thomas Brown
Thomas Dybdahl Ahle Thomas Dybdahl Ahle
Thomas Santerre
Tim Hutt Tim Hutt
Tim Snyder Tim Snyder
Tobias Jensen Tobias Jensen

View File

@ -658,6 +658,11 @@ Summary:
.. option:: -fno-assemble .. option:: -fno-assemble
.. option:: -fno-bit-scan-loops
Rarely needed. Disable converting leading-one and count-ones loops into
bit-reduction operations.
.. option:: -fno-case .. option:: -fno-case
Rarely needed. Disable all case statement optimizations. Rarely needed. Disable all case statement optimizations.

View File

@ -903,15 +903,32 @@ static inline IData VL_CLOG2_W(int words, WDataInP const lwp) VL_PURE {
return 0; return 0;
} }
static inline IData VL_MOSTSETBITP1_I(IData lhs) VL_PURE {
if (VL_UNLIKELY(!lhs)) return 0; // __builtin_clz is undefined for 0
#if defined(__GNUC__) && (__GNUC__ >= 4) && !defined(VL_NO_BUILTINS)
return VL_EDATASIZE - __builtin_clz(lhs);
#else
for (int bit = VL_EDATASIZE - 1; bit >= 0; --bit) {
if (VL_BITISSET_E(lhs, bit)) return bit + 1;
}
return 0; // LCOV_EXCL_LINE // Can't get here - one bit must be set
#endif
}
static inline IData VL_MOSTSETBITP1_Q(QData lhs) VL_PURE {
if (VL_UNLIKELY(!lhs)) return 0;
#if defined(__GNUC__) && (__GNUC__ >= 4) && !defined(VL_NO_BUILTINS)
return 64 - __builtin_clzll(static_cast<unsigned long long>(lhs));
#else
const IData hi = static_cast<IData>(lhs >> 32ULL);
return hi ? (VL_EDATASIZE + VL_MOSTSETBITP1_I(hi))
: VL_MOSTSETBITP1_I(static_cast<IData>(lhs));
#endif
}
static inline IData VL_MOSTSETBITP1_W(int words, WDataInP const lwp) VL_PURE { static inline IData VL_MOSTSETBITP1_W(int words, WDataInP const lwp) VL_PURE {
// MSB set bit plus one; similar to FLS. 0=value is zero // MSB set bit plus one; similar to FLS. 0=value is zero
for (int i = words - 1; i >= 0; --i) { for (int i = words - 1; i >= 0; --i) {
if (VL_UNLIKELY(lwp[i])) { // Shorter worst case if predict not taken // Shorter worst case if predict not taken
for (int bit = VL_EDATASIZE - 1; bit >= 0; --bit) { if (VL_UNLIKELY(lwp[i])) return i * VL_EDATASIZE + VL_MOSTSETBITP1_I(lwp[i]);
if (VL_UNLIKELY(VL_BITISSET_E(lwp[i], bit))) return i * VL_EDATASIZE + bit + 1;
}
// Can't get here - one bit must be set
}
} }
return 0; return 0;
} }

View File

@ -5734,6 +5734,23 @@ public:
void dump(std::ostream& str) const override; void dump(std::ostream& str) const override;
void dumpJson(std::ostream& str) const override; void dumpJson(std::ostream& str) const override;
}; };
class AstMostSetBitP1 final : public AstNodeUniop {
// Most-significant set bit plus one (bit-width); 0 if value is zero
public:
AstMostSetBitP1(FileLine* fl, AstNodeExpr* lhsp)
: ASTGEN_SUPER_MostSetBitP1(fl, lhsp) {
dtypeSetInteger2State();
}
ASTGEN_MEMBERS_AstMostSetBitP1;
void numberOperate(V3Number& out, const V3Number& lhs) override { out.opMostSetBitP1(lhs); }
string emitVerilog() override { return "%f$mostsetbitp1(%l)"; }
string emitC() override { return "VL_MOSTSETBITP1_%lq(%lW, %P, %li)"; }
bool cleanOut() const override { return false; }
bool cleanLhs() const override { return true; }
bool sizeMattersLhs() const override { return false; }
int instrCount() const override { return widthInstrs() * 16; }
bool isSystemFunc() const override { return true; }
};
class AstNToI final : public AstNodeUniop { class AstNToI final : public AstNodeUniop {
// String to any-size integral // String to any-size integral
public: public:

View File

@ -1456,6 +1456,20 @@ V3Number& V3Number::opCLog2(const V3Number& lhs) {
setZero(); setZero();
return *this; return *this;
} }
V3Number& V3Number::opMostSetBitP1(const V3Number& lhs) {
// Most-significant set bit plus one (bit-width / find-last-set); 0 if value is zero
NUM_ASSERT_OP_ARGS1(lhs);
NUM_ASSERT_LOGIC_ARGS1(lhs);
if (lhs.isFourState()) return setAllBitsX();
for (int bit = lhs.width() - 1; bit >= 0; bit--) {
if (lhs.bitIs1(bit)) {
setLong(bit + 1);
return *this;
}
}
setZero();
return *this;
}
V3Number& V3Number::opLogNot(const V3Number& lhs) { V3Number& V3Number::opLogNot(const V3Number& lhs) {
NUM_ASSERT_OP_ARGS1(lhs); NUM_ASSERT_OP_ARGS1(lhs);

View File

@ -760,6 +760,7 @@ public:
V3Number& opOneHot(const V3Number& lhs); V3Number& opOneHot(const V3Number& lhs);
V3Number& opOneHot0(const V3Number& lhs); V3Number& opOneHot0(const V3Number& lhs);
V3Number& opCLog2(const V3Number& lhs); V3Number& opCLog2(const V3Number& lhs);
V3Number& opMostSetBitP1(const V3Number& lhs);
V3Number& opClean(const V3Number& lhs, uint32_t bits); V3Number& opClean(const V3Number& lhs, uint32_t bits);
V3Number& opConcat(const V3Number& lhs, const V3Number& rhs); V3Number& opConcat(const V3Number& lhs, const V3Number& rhs);
V3Number& opLenN(const V3Number& lhs); V3Number& opLenN(const V3Number& lhs);

View File

@ -1448,6 +1448,7 @@ void V3Options::parseOptsList(FileLine* fl, const string& optdir, int argc,
DECL_OPTION("-facyc-simp", FOnOff, &m_fAcycSimp); DECL_OPTION("-facyc-simp", FOnOff, &m_fAcycSimp);
DECL_OPTION("-fassemble", FOnOff, &m_fAssemble); DECL_OPTION("-fassemble", FOnOff, &m_fAssemble);
DECL_OPTION("-fbit-scan-loops", FOnOff, &m_fBitScanLoops);
DECL_OPTION("-fcase", CbFOnOff, [this](bool flag) { DECL_OPTION("-fcase", CbFOnOff, [this](bool flag) {
m_fCaseDecoder = flag; m_fCaseDecoder = flag;
m_fCaseTable = flag; m_fCaseTable = flag;
@ -2358,6 +2359,7 @@ void V3Options::optimize(int level) {
const bool flag = level > 0; const bool flag = level > 0;
m_fAcycSimp = flag; m_fAcycSimp = flag;
m_fAssemble = flag; m_fAssemble = flag;
m_fBitScanLoops = flag;
m_fCaseDecoder = flag; m_fCaseDecoder = flag;
m_fCaseTable = flag; m_fCaseTable = flag;
m_fCaseTree = flag; m_fCaseTree = flag;

View File

@ -392,6 +392,7 @@ private:
// MEMBERS (optimizations) // MEMBERS (optimizations)
bool m_fAcycSimp; // main switch: -fno-acyc-simp: acyclic pre-optimizations bool m_fAcycSimp; // main switch: -fno-acyc-simp: acyclic pre-optimizations
bool m_fAssemble; // main switch: -fno-assemble: assign assemble bool m_fAssemble; // main switch: -fno-assemble: assign assemble
bool m_fBitScanLoops; // main switch: -fno-bit-scan-loops: lower priority-encoder loops to CLZ
bool m_fCaseDecoder; // main switch: -fno-case-decoder: case decoder conversion bool m_fCaseDecoder; // main switch: -fno-case-decoder: case decoder conversion
bool m_fCaseTable; // main switch: -fno-case-table: case table conversion bool m_fCaseTable; // main switch: -fno-case-table: case table conversion
bool m_fCaseTree; // main switch: -fno-case-tree: case tree conversion bool m_fCaseTree; // main switch: -fno-case-tree: case tree conversion
@ -727,6 +728,7 @@ public:
// ACCESSORS (optimization options) // ACCESSORS (optimization options)
bool fAcycSimp() const { return m_fAcycSimp; } bool fAcycSimp() const { return m_fAcycSimp; }
bool fAssemble() const { return m_fAssemble; } bool fAssemble() const { return m_fAssemble; }
bool fBitScanLoops() const { return m_fBitScanLoops; }
bool fCaseDecoder() const { return m_fCaseDecoder; } bool fCaseDecoder() const { return m_fCaseDecoder; }
bool fCaseTable() const { return m_fCaseTable; } bool fCaseTable() const { return m_fCaseTable; }
bool fCaseTree() const { return m_fCaseTree; } bool fCaseTree() const { return m_fCaseTree; }

View File

@ -62,6 +62,8 @@ struct UnrollStats final {
Stat m_nPragmaDisabled{"Pragma unroll_disable"}; Stat m_nPragmaDisabled{"Pragma unroll_disable"};
Stat m_nUnrolledLoops{"Unrolled loops"}; Stat m_nUnrolledLoops{"Unrolled loops"};
Stat m_nUnrolledIters{"Unrolled iterations"}; Stat m_nUnrolledIters{"Unrolled iterations"};
Stat m_bitScanLowered{"Lowered priority-encoder to mostsetbitp1"};
Stat m_countOnesLowered{"Lowered count-set-bits to countones"};
}; };
//###################################################################### //######################################################################
@ -420,6 +422,152 @@ class UnrollAllVisitor final : VNVisitor {
UnrollStats m_stats; // Statistic tracking UnrollStats m_stats; // Statistic tracking
UnrolllBindings m_bindings; // Variable bindings UnrolllBindings m_bindings; // Variable bindings
// METHODS
// Peel value-preserving width casts/truncations (Extend/ExtendS, or a low-bits Sel
// with constant lsb 0) to reach the underlying VarRef; nullptr for anything else
// (e.g. an arithmetic offset like idx+1 or idx*2).
static AstVarRef* unwrapToVarRef(AstNodeExpr* nodep) {
while (nodep) {
if (AstVarRef* const refp = VN_CAST(nodep, VarRef)) return refp;
if (AstExtend* const ep = VN_CAST(nodep, Extend)) {
nodep = ep->lhsp();
} else if (AstExtendS* const ep = VN_CAST(nodep, ExtendS)) {
nodep = ep->lhsp();
} else if (AstSel* const sp = VN_CAST(nodep, Sel)) {
const AstConst* const lsbp = VN_CAST(sp->lsbp(), Const);
if (!lsbp || lsbp->toUInt() != 0) return nullptr;
nodep = sp->fromp();
} else {
return nullptr;
}
}
return nullptr;
}
// True if 'nodep' is exactly 'var + 1' (commutative), where the var operand is the
// given variable reached through value-preserving casts only.
bool isVarPlus1(AstNode* nodep, const AstVarScope* vscp) {
AstAdd* const addp = VN_CAST(nodep, Add);
if (!addp) return false;
const auto isOne = [](AstNodeExpr* p) -> bool {
const AstConst* const cp = VN_CAST(p, Const);
return cp && !cp->num().isFourState() && cp->toUInt() == 1;
};
const AstVarRef* const l = unwrapToVarRef(addp->lhsp());
const AstVarRef* const r = unwrapToVarRef(addp->rhsp());
if (isOne(addp->rhsp()) && l && l->varScopep() == vscp) return true;
if (isOne(addp->lhsp()) && r && r->varScopep() == vscp) return true;
return false;
}
// Match a strict ascending loop bound 'idx < W', written either way and signed or
// unsigned: (W > idx) [Gt/GtS] or (idx < W) [Lt/LtS]. Sets wp and idxRefp.
static bool ascendingBound(AstNodeExpr* condp, AstConst*& wp, AstVarRef*& idxRefp) {
if (VN_IS(condp, Gt) || VN_IS(condp, GtS)) {
AstNodeBiop* const bp = VN_AS(condp, NodeBiop);
wp = VN_CAST(bp->lhsp(), Const);
idxRefp = VN_CAST(bp->rhsp(), VarRef);
} else if (VN_IS(condp, Lt) || VN_IS(condp, LtS)) {
AstNodeBiop* const bp = VN_AS(condp, NodeBiop);
idxRefp = VN_CAST(bp->lhsp(), VarRef);
wp = VN_CAST(bp->rhsp(), Const);
} else {
return false;
}
return wp && idxRefp && !wp->num().isFourState();
}
// Recognize a single-bit scan loop over all W bits of 'vec' (idx 0..W-1, target
// pre-zeroed) and lower it to a bit-reduction primitive. Two idioms are matched:
// target = 0; idx = 0;
// loop { looptest(W > idx); if (...vec[idx]...) target = <e>; idx = idx + 1; }
// where, when W == width(vec):
// <e> = idx + 1 => target = $mostsetbitp1(vec) (leading-one / bit-width)
// <e> = target + 1 => target = $countones(vec) (population count)
bool tryLowerBitScanLoop(AstLoop* loopp) {
// Body must be exactly: LoopTest, If, Assign(increment)
AstLoopTest* const testp = VN_CAST(loopp->stmtsp(), LoopTest);
if (!testp) return false;
AstIf* const ifp = VN_CAST(testp->nextp(), If);
if (!ifp) return false;
AstAssign* const incp = VN_CAST(ifp->nextp(), Assign);
if (!incp || incp->nextp()) return false;
// Loop test: a strict ascending bound 'idx < W' (W > idx), signed or unsigned
AstConst* wp = nullptr;
AstVarRef* idxRefp = nullptr;
if (!ascendingBound(testp->condp(), wp, idxRefp)) return false;
AstVarScope* const idxVscp = idxRefp->varScopep();
const uint32_t width = wp->toUInt();
// Index must start at 0 (so it takes exactly the values 0..W-1)
const AstConst* const idxInitp = m_bindings.get(idxVscp);
if (!idxInitp || idxInitp->num().isFourState() || idxInitp->toUInt() != 0) return false;
// Increment must be exactly: idx = idx + 1
AstVarRef* const incLhsp = VN_CAST(incp->lhsp(), VarRef);
if (!incLhsp || incLhsp->varScopep() != idxVscp) return false;
if (!isVarPlus1(incp->rhsp(), idxVscp)) return false;
// If: no else, and 'then' is exactly one assignment 'target = <expr>'
if (ifp->elsesp()) return false;
AstAssign* const thenp = VN_CAST(ifp->thensp(), Assign);
if (!thenp || thenp->nextp()) return false;
AstVarRef* const targetRefp = VN_CAST(thenp->lhsp(), VarRef);
if (!targetRefp) return false;
AstVarScope* const targetVscp = targetRefp->varScopep();
if (targetVscp == idxVscp) return false;
const bool isLeadingOne = isVarPlus1(thenp->rhsp(), idxVscp);
const bool isCountOnes = !isLeadingOne && isVarPlus1(thenp->rhsp(), targetVscp);
if (!isLeadingOne && !isCountOnes) return false;
// $countones yields a 32-bit result; only fold when the accumulator fits in 32 bits.
if (isCountOnes && targetRefp->width() > 32) return false;
// If-cond must select exactly bit 'idx' of some vector 'vec' (vec != idx/target)
AstNodeExpr* vecExprp = nullptr;
ifp->condp()->foreach([&](AstSel* selp) {
if (vecExprp || selp->width() != 1) return;
const AstVarRef* const fromp = VN_CAST(selp->fromp(), VarRef);
if (!fromp) return;
const AstVarScope* const fromVscp = fromp->varScopep();
if (fromVscp == idxVscp || fromVscp == targetVscp) return;
const AstVarRef* const idxInSel = unwrapToVarRef(selp->lsbp());
if (idxInSel && idxInSel->varScopep() == idxVscp) vecExprp = selp->fromp();
});
if (!vecExprp) return false;
// The loop must scan exactly all bits of 'vec'
if (static_cast<int>(width) != vecExprp->width()) return false;
// 'target' must be const-0 immediately before the loop (collected in m_bindings),
// so that an all-zero 'vec' yields 0, matching $mostsetbitp1's definition.
const AstConst* const targetInitp = m_bindings.get(targetVscp);
if (!targetInitp || targetInitp->num().isFourState() || targetInitp->toUInt() != 0) {
return false;
}
// Lower the loop to: target = <reduction>(vec); idx = W;
// The 'idx = W' store preserves the counted loop's exit value, so the rewrite is
// sound even if idx is read after the loop (dead-code elimination drops it if not).
FileLine* const flp = loopp->fileline();
AstNodeExpr* reducep;
if (isLeadingOne) {
AstMostSetBitP1* const msbp = new AstMostSetBitP1{flp, vecExprp->cloneTree(false)};
msbp->dtypeFrom(targetRefp);
reducep = msbp;
} else {
// $countones must yield a 32-bit result (DFG invariant); narrow afterwards to
// the accumulator width to match the loop's wrap-around behaviour.
AstCountOnes* const conep = new AstCountOnes{flp, vecExprp->cloneTree(false)};
conep->dtypeSetInteger2State();
reducep
= (targetRefp->width() < 32)
? static_cast<AstNodeExpr*>(new AstSel{flp, conep, 0, targetRefp->width()})
: static_cast<AstNodeExpr*>(conep);
}
AstAssign* const newp = new AstAssign{flp, targetRefp->cloneTree(false), reducep};
newp->addNext(new AstAssign{flp, incLhsp->cloneTree(false), wp->cloneTree(false)});
loopp->replaceWith(newp);
VL_DO_DANGLING(pushDeletep(loopp), loopp);
if (isLeadingOne) {
UINFO(4, "Lowered priority-encoder loop to $mostsetbitp1: " << newp);
++m_stats.m_bitScanLowered;
} else {
UINFO(4, "Lowered count-set-bits loop to $countones: " << newp);
++m_stats.m_countOnesLowered;
}
return true;
}
// VISIT // VISIT
void visit(AstLoop* nodep) override { void visit(AstLoop* nodep) override {
// Gather variable bindings from the preceding statements // Gather variable bindings from the preceding statements
@ -448,6 +596,9 @@ class UnrollAllVisitor final : VNVisitor {
m_bindings.set(lhsp->varScopep(), valp); m_bindings.set(lhsp->varScopep(), valp);
} }
// Recognize a leading-one / priority-encoder loop and lower it to $mostsetbitp1
if (v3Global.opt.fBitScanLoops() && tryLowerBitScanLoop(nodep)) return;
// Attempt to unroll this loop // Attempt to unroll this loop
const std::pair<AstNode*, bool> pair = UnrollOneVisitor::apply(m_stats, m_bindings, nodep); const std::pair<AstNode*, bool> pair = UnrollOneVisitor::apply(m_stats, m_bindings, nodep);

View File

@ -0,0 +1,29 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of either the GNU Lesser General Public License Version 3
# or the Perl Artistic License Version 2.0.
# SPDX-FileCopyrightText: 2024 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('simulator')
test.compile(verilator_flags2=['--stats'])
# Exactly 4 loops must lower to $mostsetbitp1: the 3 canonical I/Q/W leading-one
# loops plus the unsigned-index one. The 6 negative loops (incl. W != width) must
# be left alone -- a wrong lowering would push the count above 4.
test.file_grep(test.stats,
r'Optimizations, Loop unrolling, Lowered priority-encoder to mostsetbitp1\s+(\d+)',
4)
# And the one count-ones loop must lower to $countones.
test.file_grep(test.stats,
r'Optimizations, Loop unrolling, Lowered count-set-bits to countones\s+(\d+)',
1)
test.execute()
test.passes()

View File

@ -0,0 +1,79 @@
// DESCRIPTION: Verilator: Verilog Test module
//
// Exercises the priority-encoder / leading-one ("bit-width", find-last-set) loop
// idiom that V3Unroll lowers to $mostsetbitp1 (CLZ).
//
// Positive cases (MUST lower): canonical for(b=0;b<W;b++) if(v[b]) n=b+1
// covering the I (<=32b), Q (33-64b) and W (>64b) runtime paths.
// Negative cases (MUST NOT lower): non-canonical scans whose result differs
// from $mostsetbitp1(vec). These both (a) self-check their own values and
// (b) are counted by the .py via --stats so a wrong lowering is caught.
//
// This file ONLY is placed under the Creative Commons Public Domain.
// SPDX-FileCopyrightText: 2024 Wilson Snyder
// SPDX-License-Identifier: CC0-1.0
module t (
input clk
);
// ---- positive: should lower (I / Q / W paths) ----
logic [31:0] p32; logic [5:0] n32;
logic [47:0] p48; logic [6:0] n48;
logic [79:0] p80; logic [6:0] n80;
logic [31:0] pc; logic [5:0] nc; // population count (count-ones) -> $countones
logic [31:0] pu; logic [5:0] nu; // unsigned loop var (still $mostsetbitp1)
always_comb begin n32 = 0; for (int b = 0; b < 32; b++) if (p32[b]) n32 = 6'(b + 1); end
always_comb begin n48 = 0; for (int b = 0; b < 48; b++) if (p48[b]) n48 = 7'(b + 1); end
always_comb begin n80 = 0; for (int b = 0; b < 80; b++) if (p80[b]) n80 = 7'(b + 1); end
always_comb begin nc = 0; for (int b = 0; b < 32; b++) if (pc[b]) nc = nc + 1; end
always_comb begin nu = 0; for (int unsigned b = 0; b < 32; b++) if (pu[b]) nu = 6'(b + 1); end
// ---- negative: must NOT lower (each scans a subset / computes a different value) ----
logic [31:0] vn;
logic [5:0] e_step2; // step 2 (even bits only)
logic [6:0] e_start1; // starts at 1 (skips bit 0)
logic [6:0] e_mul; // target = idx*2 + 1
logic [5:0] e_off; // selects vec[idx+1]
logic [5:0] e_noP1; // target = idx (no +1)
logic [31:0] vw; // separate vec with a set bit above the scan bound
logic [5:0] e_narrow; // bound W=16 != width(vec)=32 (scans only a prefix)
always_comb begin e_step2 = 0; for (int b = 0; b < 32; b += 2) if (vn[b]) e_step2 = 6'(b + 1); end
always_comb begin e_start1 = 0; for (int b = 1; b < 32; b++) if (vn[b]) e_start1 = 7'(b + 1); end
always_comb begin e_mul = 0; for (int b = 0; b < 32; b++) if (vn[b]) e_mul = 7'(2 * b + 1); end
always_comb begin e_off = 0; for (int b = 0; b < 31; b++) if (vn[b + 1]) e_off = 6'(b + 1); end
always_comb begin e_noP1 = 0; for (int b = 0; b < 32; b++) if (vn[b]) e_noP1 = 6'(b); end
always_comb begin e_narrow = 0; for (int b = 0; b < 16; b++) if (vw[b]) e_narrow = 6'(b + 1); end
integer cyc = 0;
integer fails = 0;
always @(posedge clk) begin
cyc <= cyc + 1;
if (cyc == 0) begin
p32 <= 32'h8000_0000; // MSB -> 32
p48 <= 48'h0; p48[47] <= 1'b1; // MSB -> 48 (Q path)
p80 <= 80'h0; p80[79] <= 1'b1; // MSB -> 80 (W path)
pc <= 32'hf0f0_f0f0; // 16 set bits
pu <= 32'h0001_0000; // bit 16 -> 17
vn <= 32'h0000_00b4; // bits {2,4,5,7} set
vw <= 32'h0010_0008; // bits {3,20}: prefix-scan(16)->4, full->21
end
else if (cyc == 1) begin
if (n32 !== 6'd32) fails = fails + 1;
if (n48 !== 7'd48) fails = fails + 1;
if (n80 !== 7'd80) fails = fails + 1;
if (nc !== 6'd16) fails = fails + 1; // popcount(0xF0F0F0F0)
if (nu !== 6'd17) fails = fails + 1; // unsigned-loop leading-one, bit 16 -> 17
// negatives, hand-computed for vn = 0xB4 (set bits 2,4,5,7):
if (e_step2 !== 6'd5) fails = fails + 1; // highest even set bit (4) + 1
if (e_start1 !== 7'd8) fails = fails + 1; // highest set bit in [1,32) (7) + 1
if (e_mul !== 7'd15) fails = fails + 1; // 2*7 + 1
if (e_off !== 6'd7) fails = fails + 1; // idx where vec[idx+1]; highest = 6 -> 7
if (e_noP1 !== 6'd7) fails = fails + 1; // highest set bit (7), no +1
if (e_narrow !== 6'd4) fails = fails + 1; // W != width: only low 16 bits scanned (bit 3)
if (fails == 0) $write("*-* All Finished *-*\n");
else $write("FAILED: %0d mismatches\n", fails);
$finish;
end
end
endmodule

View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
# DESCRIPTION: Verilator: Verilog Test driver/expect definition
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of either the GNU Lesser General Public License Version 3
# or the Perl Artistic License Version 2.0.
# SPDX-FileCopyrightText: 2024 Wilson Snyder
# SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
import vltest_bootstrap
test.scenarios('simulator')
# Reuse the same design; only the optimization switch differs.
test.top_filename = "t/t_bit_scan_loops.v"
test.compile(verilator_flags2=['--stats', '-fno-bit-scan-loops'])
# With the optimization disabled, no loop may be lowered (count 0 / line absent).
test.file_grep_not(test.stats,
r'Lowered priority-encoder to mostsetbitp1\s+[1-9]')
test.execute()
test.passes()