Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions docs/changes/2026-05-16-u256-clz-inline/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Change: Inline handleClz with 4-limb chain-select and VR::U64 tag

- **Status**: Proposed
- **Date**: 2026-05-16
- **Tier**: Light

## Overview

Replace the pure runtime fallback in `EVMMirBuilder::handleClz`
(`src/compiler/evm_frontend/evm_mir_compiler.cpp:3092-3096`) with an inline
4-limb chain-select that computes EIP-7939 CLZ entirely in MIR and tags the
result with `ValueRange::U64`.

## Motivation

`handleClz` currently delegates to a runtime helper via `callRuntimeFor` →
`RuntimeFunctions.GetClz` → `evmGetClz` (intx::clz). Each invocation pays:

- 8x32B copy through `HostArgScratch` + `Uint256ReturnBuffer`,
- one indirect call out of JIT code,
- ~30-70 cycles of spill/restore + call overhead.

The result is also returned as an opaque `EVMType::UINT256` value, which
defeats `EVMRangeAnalyzer` (PR #493) from propagating the static fact that
`CLZ(v) <= 256` fits in a u64. Downstream narrow consumers in subsequent
basic blocks therefore cannot widen-elide.

Inlining is a known-safe pattern: `handleExp`
(`evm_mir_compiler.cpp:2511-2562`) already uses the same chain-select
template for `computeExpByteSize`. The 4 evmone CLZ unittests
(`evm.clz_gas`, `evm.clz_osaka`, `evm.clz_pre_osaka`, `evm.clz_stack_underflow`
in `tests/evmone_unittests/EVMOneMultipassUnitTestsRunList.txt:49-52`) lock
correctness, including `CLZ(0)=256` per EIP-7939.

Expected perf at the 27-bench suite level: < 0.5% (CLZ is rare). Microbench
runs with a CLZ-heavy contract could plausibly show >= 1%, but no
representative bench is staged.

## Impact

- `src/compiler/evm_frontend/evm_mir_compiler.cpp::handleClz` (single function)
- No header changes (signature is unchanged).
- `RuntimeFunctions.GetClz` / `evmGetClz` remain in the runtime table for now;
callsite removal is deferred to a follow-up cleanup commit so this diff
stays focused on the perf change.

## Implementation

1. Strip `callRuntimeFor` from `handleClz` and rewrite it as MIR construction
modeled on `computeExpByteSize` (`evm_mir_compiler.cpp:2511-2562`):

- Extract the 4 limbs via `extractU256Operand`. Limb index `i` holds bits
`[64*i, 64*(i+1))`, so the highest limb is index 3.
- Build three `OP_or`'d "limb-non-zero" predicates: `Has3, Has2, Has1`.
- Build a value-zero predicate `IsZero = (Limb0 | Limb1 | Limb2 | Limb3) == 0`.
- Chain-select the highest non-zero limb:
- `Limb12 = Has1 ? Limb[1] : Limb[0]`,
- `Limb23 = Has2 ? Limb[2] : Limb12`,
- `Limb = Has3 ? Limb[3] : Limb23`.
- Chain-select the matching base offset (limb index 3 -> 0, index 2 -> 64,
index 1 -> 128, index 0 -> 192):
- `Off12 = Has1 ? 128 : 192`,
- `Off23 = Has2 ? 64 : Off12`,
- `Offset = Has3 ? 0 : Off23`.
- **Defense-in-depth against `OP_clz(0)`** — follow `handleExp:2547-2548`
and OR the picked limb with `1` before the unary CLZ. This is dMIR-level
guarding against any backend (target lowering, fallback path) treating
`clz(0)` as undefined. The outer Select still produces the correct
spec value (256) so it does not matter what the underlying CTLZ yields
on zero, but we keep both guards for parity with `handleExp` and to
keep the MIR self-contained.
- `PartialClz = OP_clz(SafeLimb)`,
- `Partial = Offset + PartialClz`,
- `Result0 = IsZero ? 256 : Partial` (outer Select for `CLZ(0) = 256`
per EIP-7939, verified against
`~/evmone/test/unittests/evm_eip7939_clz_test.cpp:33`).

2. Pack the result into a `U256Inst`:
- `Result[0] = protectUnsafeValue(Result0, MirI64Type)`,
- `Result[1..3] = Zero`.

Then return
`Operand(Result, EVMType::UINT256, ValueRange::U64)` so downstream
narrow-consumer analysis (PR #493 `EVMRangeAnalyzer`) sees the tight
range across basic blocks.

3. Do **not** delete `RuntimeFunctions.GetClz` or `evmGetClz` in this commit
— defer to a separate cleanup once we are confident the new path is
stable across CI.

## Checklist

- [ ] Implementation complete (handleClz inlined)
- [ ] `tools/format.sh check` passes
- [ ] `cmake --build build --target dtvmapi` succeeds
- [ ] Multipass `evmone-unittests` 223/223 (including all 4 `evm.clz_*`)
- [ ] Multipass `evmone-statetest -k fork_Cancun` 2723/2723
- [ ] Module specs in `docs/modules/` updated (n/a — single-function change)
- [ ] Parallel impl review: opus + codex on the diff (Phase 4)
8 changes: 0 additions & 8 deletions src/compiler/evm_frontend/evm_imported.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@ const RuntimeFunctions &getRuntimeFunctionTable() {
.HandleUndefined = &evmHandleUndefined,
.HandleSelfDestruct = &evmHandleSelfDestruct,
.GetKeccak256 = &evmGetKeccak256,
.GetClz = &evmGetClz,
.HandleFallback = &evmHandleFallback};
return Table;
}
Expand Down Expand Up @@ -1458,11 +1457,4 @@ void evmHandleSelfDestruct(zen::runtime::EVMInstance *Instance,
}
}

const intx::uint256 *evmGetClz(zen::runtime::EVMInstance *Instance,
const intx::uint256 &Value) {
uint64_t clzResult = intx::clz(Value);
intx::uint256 result;
result[0] = clzResult;
return storeUint256Result(result);
}
} // namespace COMPILER
3 changes: 0 additions & 3 deletions src/compiler/evm_frontend/evm_imported.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ struct RuntimeFunctions {
VoidFn HandleUndefined;
VoidWithBytes32Fn HandleSelfDestruct;
Bytes32WithUInt64UInt64Fn GetKeccak256;
U256WithU256Fn GetClz;
FallbackFn HandleFallback;
};

Expand Down Expand Up @@ -271,8 +270,6 @@ void evmSetTStore(zen::runtime::EVMInstance *Instance,
const intx::uint256 &Index, const intx::uint256 &Value);
void evmHandleSelfDestruct(zen::runtime::EVMInstance *Instance,
const uint8_t *Beneficiary);
const intx::uint256 *evmGetClz(zen::runtime::EVMInstance *Instance,
const intx::uint256 &Value);
} // namespace COMPILER

#endif // EVM_FRONTEND_EVM_IMPORTED_H
74 changes: 71 additions & 3 deletions src/compiler/evm_frontend/evm_mir_compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3090,9 +3090,77 @@ EVMMirBuilder::handleCompareGtRhsU64(const Operand &LHSOp, uint64_t RhsU64) {

typename EVMMirBuilder::Operand
EVMMirBuilder::handleClz(const Operand &ValueOp) {
const auto &RuntimeFunctions = getRuntimeFunctionTable();
return callRuntimeFor<const intx::uint256 *, const intx::uint256 &>(
RuntimeFunctions.GetClz, ValueOp);
// EIP-7939 CLZ inlined: 4-limb chain-select on the highest non-zero limb
// plus a base offset (limb index 3 -> 0, 2 -> 64, 1 -> 128, 0 -> 192).
// CLZ(0) = 256 is enforced by an outer Select(IsZero, 256, partial).
// Pattern mirrors handleExp's computeExpByteSize lambda (see
// evm_mir_compiler.cpp:2511-2562) for parity with proven MIR shapes.
MType *MirI64Type =
EVMFrontendContext::getMIRTypeFromEVMType(EVMType::UINT64);
MInstruction *Zero = createIntConstInstruction(MirI64Type, 0);
MInstruction *One = createIntConstInstruction(MirI64Type, 1);
MInstruction *Const64 = createIntConstInstruction(MirI64Type, 64);
MInstruction *Const128 = createIntConstInstruction(MirI64Type, 128);
MInstruction *Const192 = createIntConstInstruction(MirI64Type, 192);
MInstruction *Const256 = createIntConstInstruction(MirI64Type, 256);

U256Inst Value = extractU256Operand(ValueOp);

auto NePred = CmpInstruction::Predicate::ICMP_NE;
MInstruction *Has1 = createInstruction<CmpInstruction>(
false, NePred, &Ctx.I64Type, Value[1], Zero);
MInstruction *Has2 = createInstruction<CmpInstruction>(
false, NePred, &Ctx.I64Type, Value[2], Zero);
MInstruction *Has3 = createInstruction<CmpInstruction>(
false, NePred, &Ctx.I64Type, Value[3], Zero);

// OR all four limbs to detect the all-zero case.
MInstruction *Any01 = createInstruction<BinaryInstruction>(
false, OP_or, MirI64Type, Value[0], Value[1]);
MInstruction *Any23 = createInstruction<BinaryInstruction>(
false, OP_or, MirI64Type, Value[2], Value[3]);
MInstruction *Any = createInstruction<BinaryInstruction>(
false, OP_or, MirI64Type, Any01, Any23);
auto EqPred = CmpInstruction::Predicate::ICMP_EQ;
MInstruction *IsZero =
createInstruction<CmpInstruction>(false, EqPred, &Ctx.I64Type, Any, Zero);

// Chain-select the highest non-zero limb.
MInstruction *Limb01 = createInstruction<SelectInstruction>(
false, MirI64Type, Has1, Value[1], Value[0]);
MInstruction *Limb02 = createInstruction<SelectInstruction>(
false, MirI64Type, Has2, Value[2], Limb01);
MInstruction *Limb = createInstruction<SelectInstruction>(
false, MirI64Type, Has3, Value[3], Limb02);

// Chain-select the matching base offset.
MInstruction *Off01 = createInstruction<SelectInstruction>(
false, MirI64Type, Has1, Const128, Const192);
MInstruction *Off02 = createInstruction<SelectInstruction>(
false, MirI64Type, Has2, Const64, Off01);
MInstruction *Offset = createInstruction<SelectInstruction>(
false, MirI64Type, Has3, Zero, Off02);

// Defense-in-depth against clz(0) UB on the picked limb (mirrors
// handleExp:2547-2548). The outer Select discards Partial when IsZero,
// but we keep the guard so the MIR is self-contained.
MInstruction *SafeLimb =
createInstruction<BinaryInstruction>(false, OP_or, MirI64Type, Limb, One);
MInstruction *Clz =
createInstruction<UnaryInstruction>(false, OP_clz, MirI64Type, SafeLimb);
MInstruction *Partial = createInstruction<BinaryInstruction>(
false, OP_add, MirI64Type, Offset, Clz);

// EIP-7939: CLZ(0) = 256.
MInstruction *FinalResult = createInstruction<SelectInstruction>(
false, MirI64Type, IsZero, Const256, Partial);

U256Inst Result = {};
Result[0] = protectUnsafeValue(FinalResult, MirI64Type);
for (size_t I = 1; I < EVM_ELEMENTS_COUNT; ++I) {
Result[I] = Zero;
}
return Operand(Result, EVMType::UINT256, ValueRange::U64);
}

namespace {
Expand Down
Loading