diff --git a/docs/changes/2026-05-16-u256-clz-inline/README.md b/docs/changes/2026-05-16-u256-clz-inline/README.md new file mode 100644 index 000000000..df9374533 --- /dev/null +++ b/docs/changes/2026-05-16-u256-clz-inline/README.md @@ -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) diff --git a/src/compiler/evm_frontend/evm_imported.cpp b/src/compiler/evm_frontend/evm_imported.cpp index 166b26c8f..9221abf49 100644 --- a/src/compiler/evm_frontend/evm_imported.cpp +++ b/src/compiler/evm_frontend/evm_imported.cpp @@ -200,7 +200,6 @@ const RuntimeFunctions &getRuntimeFunctionTable() { .HandleUndefined = &evmHandleUndefined, .HandleSelfDestruct = &evmHandleSelfDestruct, .GetKeccak256 = &evmGetKeccak256, - .GetClz = &evmGetClz, .HandleFallback = &evmHandleFallback}; return Table; } @@ -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 diff --git a/src/compiler/evm_frontend/evm_imported.h b/src/compiler/evm_frontend/evm_imported.h index 22c67edcb..c579f197b 100644 --- a/src/compiler/evm_frontend/evm_imported.h +++ b/src/compiler/evm_frontend/evm_imported.h @@ -139,7 +139,6 @@ struct RuntimeFunctions { VoidFn HandleUndefined; VoidWithBytes32Fn HandleSelfDestruct; Bytes32WithUInt64UInt64Fn GetKeccak256; - U256WithU256Fn GetClz; FallbackFn HandleFallback; }; @@ -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 diff --git a/src/compiler/evm_frontend/evm_mir_compiler.cpp b/src/compiler/evm_frontend/evm_mir_compiler.cpp index 4b374535c..1e087908a 100644 --- a/src/compiler/evm_frontend/evm_mir_compiler.cpp +++ b/src/compiler/evm_frontend/evm_mir_compiler.cpp @@ -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( - 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( + false, NePred, &Ctx.I64Type, Value[1], Zero); + MInstruction *Has2 = createInstruction( + false, NePred, &Ctx.I64Type, Value[2], Zero); + MInstruction *Has3 = createInstruction( + false, NePred, &Ctx.I64Type, Value[3], Zero); + + // OR all four limbs to detect the all-zero case. + MInstruction *Any01 = createInstruction( + false, OP_or, MirI64Type, Value[0], Value[1]); + MInstruction *Any23 = createInstruction( + false, OP_or, MirI64Type, Value[2], Value[3]); + MInstruction *Any = createInstruction( + false, OP_or, MirI64Type, Any01, Any23); + auto EqPred = CmpInstruction::Predicate::ICMP_EQ; + MInstruction *IsZero = + createInstruction(false, EqPred, &Ctx.I64Type, Any, Zero); + + // Chain-select the highest non-zero limb. + MInstruction *Limb01 = createInstruction( + false, MirI64Type, Has1, Value[1], Value[0]); + MInstruction *Limb02 = createInstruction( + false, MirI64Type, Has2, Value[2], Limb01); + MInstruction *Limb = createInstruction( + false, MirI64Type, Has3, Value[3], Limb02); + + // Chain-select the matching base offset. + MInstruction *Off01 = createInstruction( + false, MirI64Type, Has1, Const128, Const192); + MInstruction *Off02 = createInstruction( + false, MirI64Type, Has2, Const64, Off01); + MInstruction *Offset = createInstruction( + 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(false, OP_or, MirI64Type, Limb, One); + MInstruction *Clz = + createInstruction(false, OP_clz, MirI64Type, SafeLimb); + MInstruction *Partial = createInstruction( + false, OP_add, MirI64Type, Offset, Clz); + + // EIP-7939: CLZ(0) = 256. + MInstruction *FinalResult = createInstruction( + 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 {