From a7b752ad2f4f907384d44648aaa466f0fd63361e Mon Sep 17 00:00:00 2001 From: abmcar Date: Wed, 10 Jun 2026 02:19:08 +0800 Subject: [PATCH 1/7] perf(compiler): consume value-range tags in EVM bool/compare/bitwise lowering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close lowering-side gaps where operands already proven u64 by the range analyzer still took full 4-limb paths: - ISZERO: deferred zero-test operands now carry the base value range and tag their 0/1 result as u64; materialization folds only the limbs the range allows (u64 base -> single-limb test). - JUMPI: fuse deferred zero-test conditions into the branch (no materialize-then-refold round trip) and fold only range-live limbs of non-deferred conditions. - OR/XOR: new non-constant narrow paths — both-u64 emits a single i64 op with zeroed upper limbs; one-sided u64 passes the wide side's upper limbs through (identity), mirroring the existing const-u64 path. - SLT/SGT: new fast paths against u64 constants with the same three range tiers as the unsigned compare helpers; a u64 constant is a non-negative signed-256 value, so sign-bit + upper-or + unsigned limb0 compare decide the result. - Context-size producers (PC/GAS/CALLDATASIZE/CODESIZE/MSIZE/ RETURNDATASIZE): tag results u64 — limbs 1..3 are literal zeros by construction, closing a builder/analyzer SSOT divergence. Add 21 adversarial differential fixtures (interp vs multipass, boundary values incl. 2^64/2^128/2^192/-1/high-sparse and limb0 MSB cases) plus an EVMRangeNarrowingDifferentialTest suite. Co-Authored-By: Claude Opus 4.8 --- .../evm_frontend/evm_mir_compiler.cpp | 138 +++++++++++++++++- src/compiler/evm_frontend/evm_mir_compiler.h | 89 ++++++++++- src/tests/evm_interp_tests.cpp | 54 +++++++ 3 files changed, 268 insertions(+), 13 deletions(-) diff --git a/src/compiler/evm_frontend/evm_mir_compiler.cpp b/src/compiler/evm_frontend/evm_mir_compiler.cpp index f489aa4bc..fede814ea 100644 --- a/src/compiler/evm_frontend/evm_mir_compiler.cpp +++ b/src/compiler/evm_frontend/evm_mir_compiler.cpp @@ -1570,9 +1570,16 @@ MInstruction *EVMMirBuilder::createJumpCondition(const Operand &Cond) { // BrIfInstruction tests whether its condition operand is non-zero, so the // compare result can be consumed directly without materializing an i64 0/1. - auto BuildNonZeroOr = [this, MirI64Type](const U256Inst &Parts) { + // OR only the limbs that may be non-zero by the Range contract: JUMPI + // correctness depends on this invariant, so an over-narrow producer tag + // would skip a non-zero upper limb and branch wrong (not merely slower). + auto BuildNonZeroOr = [this, MirI64Type](const U256Inst &Parts, + ValueRange Range) { + size_t FoldLimbs = Range == ValueRange::U64 ? 1 + : Range == ValueRange::U128 ? 2 + : EVM_ELEMENTS_COUNT; MInstruction *OrResult = Parts[0]; - for (size_t I = 1; I < EVM_ELEMENTS_COUNT; ++I) { + for (size_t I = 1; I < FoldLimbs; ++I) { OrResult = createInstruction(false, OP_or, MirI64Type, OrResult, Parts[I]); } @@ -1580,7 +1587,8 @@ MInstruction *EVMMirBuilder::createJumpCondition(const Operand &Cond) { }; if (Cond.isDeferredZeroTest()) { - MInstruction *OrResult = BuildNonZeroOr(Cond.getDeferredBaseComponents()); + MInstruction *OrResult = BuildNonZeroOr(Cond.getDeferredBaseComponents(), + Cond.getDeferredBaseRange()); auto Predicate = Cond.isDeferredZeroTestNegated() ? CmpInstruction::Predicate::ICMP_NE : CmpInstruction::Predicate::ICMP_EQ; @@ -1589,7 +1597,7 @@ MInstruction *EVMMirBuilder::createJumpCondition(const Operand &Cond) { } U256Inst CondComponents = extractU256Operand(Cond); - MInstruction *OrResult = BuildNonZeroOr(CondComponents); + MInstruction *OrResult = BuildNonZeroOr(CondComponents, Cond.getRange()); return createInstruction( false, CmpInstruction::Predicate::ICMP_NE, &Ctx.I64Type, OrResult, Zero); } @@ -2963,14 +2971,20 @@ typename EVMMirBuilder::Operand EVMMirBuilder::handleExp(Operand BaseOp, EVMMirBuilder::U256Inst EVMMirBuilder::handleCompareEQZ(const U256Inst &LHS, MType *ResultType, - bool IsNegated) { + bool IsNegated, + ValueRange BaseRange) { U256Inst Result = {}; MType *MirI64Type = EVMFrontendContext::getMIRTypeFromEVMType(EVMType::UINT64); - // For ISZERO: OR all components, then compare with 0 + // For ISZERO: OR the limbs that may be non-zero, then compare with 0. Limbs + // proven zero by the Range contract are skipped: U64 -> limb0 only, U128 -> + // limbs 0-1, else all 4. + size_t FoldLimbs = BaseRange == ValueRange::U64 ? 1 + : BaseRange == ValueRange::U128 ? 2 + : EVM_ELEMENTS_COUNT; MInstruction *OrResult = nullptr; - for (size_t I = 0; I < EVM_ELEMENTS_COUNT; ++I) { + for (size_t I = 0; I < FoldLimbs; ++I) { if (OrResult == nullptr) { OrResult = LHS[I]; } else { @@ -3338,6 +3352,113 @@ EVMMirBuilder::handleCompareGtRhsU64(const Operand &LHSOp, uint64_t RhsU64) { return Operand(Result, EVMType::UINT256, ValueRange::U64); } +typename EVMMirBuilder::Operand +EVMMirBuilder::handleCompareSltRhsU64(const Operand &LHSOp, uint64_t RhsU64) { + // SLT(a, u64_b): signed a < b where b fits in u64 (so b >= 0 as signed-256). + // a negative (bit255 set) -> true since b >= 0. + // a non-negative with any upper limb set -> a > b -> false. + // else unsigned compare a[0] < b. + U256Inst LHS = extractU256Operand(LHSOp); + MType *MirI64Type = + EVMFrontendContext::getMIRTypeFromEVMType(EVMType::UINT64); + MInstruction *Zero = createIntConstInstruction(MirI64Type, 0); + + MInstruction *RhsVal = createIntConstInstruction(MirI64Type, RhsU64); + auto LtPred = CmpInstruction::Predicate::ICMP_ULT; + MInstruction *LowLt = createInstruction( + false, LtPred, &Ctx.I64Type, LHS[0], RhsVal); + + MInstruction *FinalResult; + if (LHSOp.getRange() == ValueRange::U64) { + // a is provably non-negative and < 2^64. + FinalResult = LowLt; + } else if (LHSOp.getRange() == ValueRange::U128) { + // a is non-negative; only LHS[1] can make a >= 2^64. + auto NePred = CmpInstruction::Predicate::ICMP_NE; + MInstruction *HasUpper = createInstruction( + false, NePred, &Ctx.I64Type, LHS[1], Zero); + FinalResult = createInstruction(false, &Ctx.I64Type, + HasUpper, Zero, LowLt); + } else { + MInstruction *One = createIntConstInstruction(MirI64Type, 1); + MInstruction *Upper = createInstruction( + false, OP_or, MirI64Type, LHS[1], LHS[2]); + Upper = createInstruction(false, OP_or, MirI64Type, + Upper, LHS[3]); + auto NePred = CmpInstruction::Predicate::ICMP_NE; + MInstruction *HasUpper = createInstruction( + false, NePred, &Ctx.I64Type, Upper, Zero); + auto SltPred = CmpInstruction::Predicate::ICMP_SLT; + MInstruction *IsNeg = createInstruction( + false, SltPred, &Ctx.I64Type, LHS[3], Zero); + MInstruction *NonNegResult = createInstruction( + false, &Ctx.I64Type, HasUpper, Zero, LowLt); + FinalResult = createInstruction( + false, &Ctx.I64Type, IsNeg, One, NonNegResult); + } + + 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); +} + +typename EVMMirBuilder::Operand +EVMMirBuilder::handleCompareSgtRhsU64(const Operand &LHSOp, uint64_t RhsU64) { + // SGT(a, u64_b): signed a > b where b fits in u64 (so b >= 0 as signed-256). + // a negative (bit255 set) -> false since b >= 0. + // a non-negative with any upper limb set -> a > b -> true. + // else unsigned compare a[0] > b. + U256Inst LHS = extractU256Operand(LHSOp); + MType *MirI64Type = + EVMFrontendContext::getMIRTypeFromEVMType(EVMType::UINT64); + MInstruction *Zero = createIntConstInstruction(MirI64Type, 0); + + MInstruction *RhsVal = createIntConstInstruction(MirI64Type, RhsU64); + auto GtPred = CmpInstruction::Predicate::ICMP_UGT; + MInstruction *LowGt = createInstruction( + false, GtPred, &Ctx.I64Type, LHS[0], RhsVal); + + MInstruction *FinalResult; + if (LHSOp.getRange() == ValueRange::U64) { + // a is provably non-negative and < 2^64. + FinalResult = LowGt; + } else if (LHSOp.getRange() == ValueRange::U128) { + // a is non-negative; only LHS[1] can make a >= 2^64. + MInstruction *One = createIntConstInstruction(MirI64Type, 1); + auto NePred = CmpInstruction::Predicate::ICMP_NE; + MInstruction *HasUpper = createInstruction( + false, NePred, &Ctx.I64Type, LHS[1], Zero); + FinalResult = createInstruction(false, &Ctx.I64Type, + HasUpper, One, LowGt); + } else { + MInstruction *One = createIntConstInstruction(MirI64Type, 1); + MInstruction *Upper = createInstruction( + false, OP_or, MirI64Type, LHS[1], LHS[2]); + Upper = createInstruction(false, OP_or, MirI64Type, + Upper, LHS[3]); + auto NePred = CmpInstruction::Predicate::ICMP_NE; + MInstruction *HasUpper = createInstruction( + false, NePred, &Ctx.I64Type, Upper, Zero); + auto SltPred = CmpInstruction::Predicate::ICMP_SLT; + MInstruction *IsNeg = createInstruction( + false, SltPred, &Ctx.I64Type, LHS[3], Zero); + MInstruction *NonNegResult = createInstruction( + false, &Ctx.I64Type, HasUpper, One, LowGt); + FinalResult = createInstruction( + false, &Ctx.I64Type, IsNeg, Zero, NonNegResult); + } + + 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); +} + typename EVMMirBuilder::Operand EVMMirBuilder::handleClz(const Operand &ValueOp) { // EIP-7939 CLZ inlined: 4-limb chain-select on the highest non-zero limb @@ -5415,7 +5536,8 @@ EVMMirBuilder::U256Inst EVMMirBuilder::extractU256Operand(const Operand &Opnd) { if (Opnd.isDeferredZeroTest()) { return handleCompareEQZ(Opnd.getDeferredBaseComponents(), &Ctx.I64Type, - Opnd.isDeferredZeroTestNegated()); + Opnd.isDeferredZeroTestNegated(), + Opnd.getDeferredBaseRange()); } if (Opnd.isU256MultiComponent()) { diff --git a/src/compiler/evm_frontend/evm_mir_compiler.h b/src/compiler/evm_frontend/evm_mir_compiler.h index 240b61f1f..b1d9bef81 100644 --- a/src/compiler/evm_frontend/evm_mir_compiler.h +++ b/src/compiler/evm_frontend/evm_mir_compiler.h @@ -196,12 +196,16 @@ class EVMMirBuilder final { } static Operand createDeferredZeroTest(U256Inst BaseComponents, - bool IsNegated) { + bool IsNegated, + ValueRange BaseRange) { Operand Result; Result.Type = EVMType::UINT256; Result.DeferredValueKind = IsNegated ? DeferredKind::ZERO_TEST_NE : DeferredKind::ZERO_TEST_EQ; Result.U256Components = BaseComponents; + Result.DeferredBaseRange = BaseRange; + // The deferred value materializes to 0/1, so it structurally fits u64 + // regardless of the base's range. Result.Range = ValueRange::U64; return Result; } @@ -266,6 +270,10 @@ class EVMMirBuilder final { "Not a deferred value"); return U256Components; } + ValueRange getDeferredBaseRange() const { + ZEN_ASSERT(isDeferredZeroTest() && "Not a deferred zero-test value"); + return DeferredBaseRange; + } // Provable value range — narrower ranges enable fast arithmetic paths ValueRange getRange() const { return Range; } @@ -303,6 +311,9 @@ class EVMMirBuilder final { bool IsConstant = false; bool IsU256MultiComponent = false; DeferredKind DeferredValueKind = DeferredKind::NONE; + // Range of the base value of a deferred zero-test (the value being tested), + // used to narrow the OR-fold when materialized. + ValueRange DeferredBaseRange = ValueRange::U256; }; bool compile(CompilerContext *Context); @@ -576,12 +587,14 @@ class EVMMirBuilder final { } if (LHSOp.isDeferredZeroTest()) { + // Flip-negation reuses the same base; propagate its range unchanged. return Operand::createDeferredZeroTest( LHSOp.getDeferredBaseComponents(), - !LHSOp.isDeferredZeroTestNegated()); + !LHSOp.isDeferredZeroTestNegated(), LHSOp.getDeferredBaseRange()); } - return Operand::createDeferredZeroTest(extractU256Operand(LHSOp), false); + return Operand::createDeferredZeroTest(extractU256Operand(LHSOp), false, + LHSOp.getRange()); } else { if (LHSOp.isConstant() && RHSOp.isConstant()) { intx::uint256 L = u256ValueToIntx(LHSOp.getConstValue()); @@ -641,6 +654,27 @@ class EVMMirBuilder final { } } + // Phase 3: u64 fast path for signed LT/GT. A u64 constant is a + // non-negative signed-256 value (limbs[1..3] == 0). + if constexpr (Operator == CompareOperator::CO_LT_S) { + if (RHSOp.isConstU64()) { + return handleCompareSltRhsU64(LHSOp, RHSOp.getConstValue()[0]); + } + if (LHSOp.isConstU64()) { + // c <_s x <=> x >_s c + return handleCompareSgtRhsU64(RHSOp, LHSOp.getConstValue()[0]); + } + } + if constexpr (Operator == CompareOperator::CO_GT_S) { + if (RHSOp.isConstU64()) { + return handleCompareSgtRhsU64(LHSOp, RHSOp.getConstValue()[0]); + } + if (LHSOp.isConstU64()) { + // c >_s x <=> x <_s c + return handleCompareSltRhsU64(RHSOp, LHSOp.getConstValue()[0]); + } + } + U256Inst Result = handleCompareImpl(LHSOp, RHSOp, &Ctx.I64Type); // Comparison results are always 0 or 1 return Operand(Result, EVMType::UINT256, ValueRange::U64); @@ -761,6 +795,48 @@ class EVMMirBuilder final { // limbs is preserved exactly. max(U64, OtherOp.range) = OtherOp.range. return Operand(Result, EVMType::UINT256, OtherOp.getRange()); } + + // Phase 2: range-narrowed OR/XOR for non-constant operands. Constants + // with u64 magnitude were already handled above. + MType *MirI64Type = + EVMFrontendContext::getMIRTypeFromEVMType(EVMType::UINT64); + if (!LHSOp.isConstant() && !RHSOp.isConstant() && + Operand::bothFitU64(LHSOp, RHSOp)) { + // Both upper limbs are semantically zero, so OR/XOR of them is zero. + U256Inst LHS = extractU256Operand(LHSOp); + U256Inst RHS = extractU256Operand(RHSOp); + MInstruction *Zero = createIntConstInstruction(MirI64Type, 0); + U256Inst Result = {}; + Result[0] = protectUnsafeValue( + createInstruction(false, getMirOpcode(Operator), + MirI64Type, LHS[0], RHS[0]), + MirI64Type); + for (size_t I = 1; I < EVM_ELEMENTS_COUNT; ++I) { + Result[I] = Zero; + } + return Operand(Result, EVMType::UINT256, ValueRange::U64); + } + if (!LHSOp.isConstant() && !RHSOp.isConstant() && + ((LHSOp.getRange() == ValueRange::U64) ^ + (RHSOp.getRange() == ValueRange::U64))) { + // Exactly one side is u64: its upper limbs are semantically zero, so + // OR/XOR with them is identity on the wide side's upper limbs. + const Operand &U64Op = + LHSOp.getRange() == ValueRange::U64 ? LHSOp : RHSOp; + const Operand &WideOp = + LHSOp.getRange() == ValueRange::U64 ? RHSOp : LHSOp; + U256Inst Narrow = extractU256Operand(U64Op); + U256Inst Wide = extractU256Operand(WideOp); + U256Inst Result = {}; + Result[0] = protectUnsafeValue( + createInstruction( + false, getMirOpcode(Operator), MirI64Type, Narrow[0], Wide[0]), + MirI64Type); + for (size_t I = 1; I < EVM_ELEMENTS_COUNT; ++I) { + Result[I] = Wide[I]; + } + return Operand(Result, EVMType::UINT256, WideOp.getRange()); + } } U256Inst Result = {}; @@ -1097,7 +1173,7 @@ class EVMMirBuilder final { U256Inst RHS = {}; if constexpr (Operator == CompareOperator::CO_EQZ) { - return handleCompareEQZ(LHS, ResultType); + return handleCompareEQZ(LHS, ResultType, false, LHSOp.getRange()); } else if constexpr (Operator == CompareOperator::CO_EQ) { RHS = extractU256Operand(RHSOp); return handleCompareEQ(LHS, RHS, ResultType); @@ -1108,7 +1184,8 @@ class EVMMirBuilder final { } U256Inst handleCompareEQZ(const U256Inst &LHS, MType *ResultType, - bool IsNegated = false); + bool IsNegated = false, + ValueRange BaseRange = ValueRange::U256); MInstruction *createJumpCondition(const Operand &Cond); U256Inst handleCompareEQ(const U256Inst &LHS, const U256Inst &RHS, @@ -1148,6 +1225,8 @@ class EVMMirBuilder final { Operand handleCompareEqU64(const Operand &FullOp, uint64_t U64Val); Operand handleCompareLtRhsU64(const Operand &LHSOp, uint64_t RhsU64); Operand handleCompareGtRhsU64(const Operand &LHSOp, uint64_t RhsU64); + Operand handleCompareSltRhsU64(const Operand &LHSOp, uint64_t RhsU64); + Operand handleCompareSgtRhsU64(const Operand &LHSOp, uint64_t RhsU64); // Helper functions for inline U256 multiplication MInstruction *createEvmUmul128(MInstruction *LHS, MInstruction *RHS); diff --git a/src/tests/evm_interp_tests.cpp b/src/tests/evm_interp_tests.cpp index 2198aba34..433f308f2 100644 --- a/src/tests/evm_interp_tests.cpp +++ b/src/tests/evm_interp_tests.cpp @@ -924,6 +924,60 @@ TEST(EVMRegressionTest, Issue541_AddU64ConstLastAdcCarryChainPreserved) { EXPECT_EQ(InterpExec.OutputHex, "0000000000000000000000000000000000000000000000000000000000000000"); } + +// Differential regression for the range-narrowed lowering paths added on the +// perf/evm-range-lowering-gaps branch (narrowed ISZERO/JUMPI folds, U64-tagged +// OR/XOR, and the signed LT/GT u64-const fast paths). Each fixture feeds a +// dynamic (analyzer-unprovable) operand so the new paths are actually taken, +// then asserts the multipass JIT output matches the interpreter (the ground +// truth) exactly. A divergence here means a narrowed fold produced a wrong +// value, not merely a slower one. +class EVMRangeNarrowingDifferentialTest + : public ::testing::TestWithParam {}; + +std::string +GetRangeNarrowingTestName(const testing::TestParamInfo &Info) { + return Info.param; +} + +TEST_P(EVMRangeNarrowingDifferentialTest, InterpMatchesMultipass) { + const std::string Stem = GetParam(); + const auto FilePath = (getEvmAsmDirPath() / (Stem + ".evm.hex")).string(); + + auto InterpExec = + executeEvmBytecodeFile(FilePath, common::RunMode::InterpMode); + auto MultipassExec = + executeEvmBytecodeFile(FilePath, common::RunMode::MultipassMode); + +#ifdef ZEN_ENABLE_JIT + EXPECT_TRUE(MultipassExec.JITCompiled) + << "Multipass JIT should compile " << Stem; +#endif + + EXPECT_EQ(InterpExec.Status, EVMC_SUCCESS) + << "Interpreter did not succeed for " << Stem; + EXPECT_EQ(MultipassExec.Status, EVMC_SUCCESS) + << "Multipass did not succeed for " << Stem; + EXPECT_EQ(MultipassExec.Status, InterpExec.Status) + << "Multipass status diverged from interpreter for " << Stem; + EXPECT_EQ(MultipassExec.OutputHex, InterpExec.OutputHex) + << "Multipass output diverged from interpreter for " << Stem; +} + +INSTANTIATE_TEST_SUITE_P( + RangeNarrowingDifferential, EVMRangeNarrowingDifferentialTest, + ::testing::Values("iszero_dyn_u64_nonzero", "iszero_dyn_highsparse", + "iszero_calldatasize", "jumpi_iszero_fused_taken", + "jumpi_iszero_fused_nottaken_highsparse", + "jumpi_iszero_iszero_fused", "jumpi_u64_cond_taken", + "jumpi_u64_cond_nottaken", "or_dyn_u64_u64", + "xor_dyn_u64_u64", "or_dyn_u64_wide", "xor_dyn_u64_wide", + "slt_dyn_neg_vs_const", "slt_dyn_highsparse_vs_const", + "slt_dyn_msb64_vs_const", "slt_dyn_eq_const", + "sgt_dyn_neg_vs_const", "sgt_dyn_highsparse_vs_const", + "sgt_dyn_msb64_vs_const", "slt_const_vs_dyn", + "sgt_const_vs_dyn"), + GetRangeNarrowingTestName); #endif // Test that chain_id and blob_base_fee can be saved and loaded via state From d84a4df2053fe4bfd98dfd2521455d46bf95f397 Mon Sep 17 00:00:00 2001 From: abmcar Date: Wed, 10 Jun 2026 02:49:29 +0800 Subject: [PATCH 2/7] docs(compiler): add range-lowering-gaps change document Co-Authored-By: Claude Opus 4.8 --- .../README.md | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 docs/changes/2026-06-10-evm-range-lowering-gaps/README.md diff --git a/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md b/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md new file mode 100644 index 000000000..a03207308 --- /dev/null +++ b/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md @@ -0,0 +1,147 @@ +# Change: EVM lowering 消费 value-range 标签(bool/compare/bitwise 快路径) + +- **Status**: Proposed +- **Date**: 2026-06-10 +- **Tier**: Light + +## Overview + +multipass JIT 的 range 分析此前已能证明大量操作数为 u64,但多个 lowering +路径不消费该证明,仍发射全宽 4-limb 序列。本变更让五类 lowering 消费已有的 +ValueRange 标签:在 EEST Cancun 套件上,fast-path 命中率 site 加权从 78.72% +提升到 **80.02%**(+364 个站点转入窄路径,零反向回归),其中 **ISZERO 从 +23.2% 提升到 85.9%**。全部正确性套件通过,新增 21 个对抗性差分 fixture。 + +## Motivation + +真实负载分析(见 `2026-06-02-real-load-analysis-suite`)量化了两类缺口: +分析侧(静态证不出,占主)与 lowering 侧(已证出但 builder 不消费)。本变更 +关闭 lowering 侧缺口中可立即收割的部分: + +- **ISZERO**:deferred zero-test 物化时无条件 OR-fold 全部 4 limb,且其 0/1 + 结果丢失 U64 标签——真实负载上 58.5% 的 ISZERO FULL 执行操作数已被静态 + 证明 u64。 +- **JUMPI**:条件 lowering 对已标 U64 的条件仍 OR-fold 4 limb;ISZERO 结果 + 作条件时先物化成 0/1 再被整条链重算一遍(`LT;ISZERO;JUMPI` 循环退出模式 + 付三层冗余)。 +- **OR/XOR**:只有 const-u64 快路径,无 range 窄化——真实负载上 51.8% 的 + OR FULL 执行有至少一侧已证 u64。 +- **SLT/SGT**:连 const 快路径都没有(真实负载 SLT FULL 执行的 22.4% 是 + `slt(x, 小常量)` 形态)。 +- **环境 opcode 生产者**(PC/GAS/CALLDATASIZE/CODESIZE/MSIZE/ + RETURNDATASIZE):`convertSingleInstrToU256Operand` 结构性零填高 limb 却 + 返回默认 U256,造成 builder/analyzer 双 SSOT 背离(analyzer 已标 U64)。 + +## Changes + +全部在 `src/compiler/evm_frontend/evm_mir_compiler.{h,cpp}`: + +1. **ISZERO deferred zero-test 携带 range**:`createDeferredZeroTest` 增加 + `BaseRange` 参数(新成员 `DeferredBaseRange`),deferred Operand 自身 + Range 标为 U64(其物化值恒为 0/1,结构性成立)。`handleCompareEQZ` 按 + base range 折叠 1/2/4 个 limb。所有创建/物化点(含 ISZERO 嵌套翻转的 + range 传播)同步更新。 +2. **JUMPI 条件融合与窄化**:deferred zero-test 条件不再物化,直接按 + base range 折叠后与 0 比较(EQ/NE 按否定标志选择谓词);非 deferred 条件 + 按 Range 契约只折叠可能非零的 limb。Dest 与分支/跳转表逻辑不变。 +3. **OR/XOR range 窄化路径**:双操作数均证 U64(非常量)→ 单条 i64 op + + 高 limb 置零,结果标 U64;恰一侧证 U64 → 低 limb op + 宽侧高 limb 直通 + (与既有 const-u64 路径同构),结果标宽侧 range。 +4. **SLT/SGT 对 u64 常量的快路径**:u64 常量高 limb 为零,作为 signed-256 + 是非负值,故 `slt(x, c)` 可降为「符号位 ∨ (高 limb 全零 ∧ limb0 无符号 + 比较)」。新增 `handleCompareSltRhsU64`/`handleCompareSgtRhsU64`,与既有 + 无符号比较 helper 相同的三层 range tier(U64/U128/默认)。两个常量侧 + 均覆盖(`c s c` 互换)。 +5. **环境 opcode 结果标 U64**:`convertSingleInstrToU256Operand` 返回值附 + `ValueRange::U64`——limbs[1..3] 为字面零,值按构造落在 `[0, 2^64-1]`, + 与调用方意图无关地成立。消除一处现存 builder/analyzer 背离。 + +## Soundness + +- Range 契约:`U64` 标签仅表示高 limb 语义为零;所有新路径只在标签已证明处 + 收窄读取宽度,不引入新的标签来源(变更 5 除外,其正确性由函数结构保证)。 +- SLT/SGT:limb0 在 `[2^63, 2^64-1]` 的操作数是正的 256-bit 值,新路径用 + 无符号谓词(`ICMP_ULT/UGT`)比较 limb0,负数情形由 limb3 的符号位判定 + 短路——三层 tier 的真值表经两名独立 reviewer(Opus、Codex)边界枚举核对 + (2^63、2^64、2^128、2^192、2^255、-1、相等、c=0、c≥2^63)。 +- analyzer/builder 对称:ISZERO、比较结果、OR/XOR、环境 opcode 的 builder + 结果 range 与 analyzer transfer(`evm_analyzer.h`)一致或更宽,无 + builder-narrower-than-truth 状态。 +- JUMPI 现在依赖 Range 契约的正确性(过窄的上游标签会导致错分支而非仅变慢), + 已在代码注释中显式记录。 + +## Verification + +- **差分 fixture(新增,随 PR 提交)**:21 个 `.easm` + `.expected` + (`tests/evm_asm/`),覆盖每条新路径的窄路径触发与全路径保留两侧,对抗值 + 含 2^64、2^128、2^192、-1、limb0-MSB(`0x8000000000000000` 无符号谓词 + 硬门)、high-sparse;`EVMRangeNarrowingDifferentialTest` 断言 interp 与 + multipass 输出逐字节一致且 multipass 确实 JIT 编译。21/21 通过, + golden 套件 178/178 无回归。 +- **multipass evmone-unittests**:223/223。 +- **multipass evmone-statetest `-k fork_Cancun`**:2723/2723。 +- **ctest 全量**:11/11(solidityContractTests 需复制 gitignored 的 + `tests/evm_solidity/*/*.json` 生成物到 worktree,属环境数据非回归)。 +- `tools/format.sh check` 通过;构建无新增警告。 +- **双独立 review**:Opus(全六攻击面 CLEAN)+ Codex(除一项既有问题外 + verified-clean,见 Known limitations)。 + +## Measurements + +### Fast-path 命中率(EEST Cancun,site 加权,配对测量) + +测量方法:在带 Stream B tap 的测量分支上(tap 仅测量用,不随本 PR 提交), +base 与 base+本变更各 capture 一次,28,109 个共享站点逐点配对。 + +| op | base | 本变更 | Δ | 迁移站点 | +|---|---:|---:|---:|---| +| ISZERO | 23.2% | **85.9%** | **+62.7pp** | 316 × FULL→NARROW_U64 | +| SGT | 80.6% | 95.0% | +14.4pp | 20 × FULL→CONST_U64 | +| SLT | 61.5% | 66.7% | +5.2pp | 9 × FULL→CONST_U64 | +| ADD | 75.1% | 75.6% | +0.5pp | 17 × FULL→NARROW_U128(环境 opcode 标签解锁) | +| OR | 98.7% | 98.9% | +0.2pp | 2 × FULL→NARROW_U64 | +| **总体** | **78.72%** | **80.02%** | **+1.29pp** | +364 站点,零反向迁移 | + +JUMPI 融合不在 tap 覆盖内(JUMPI 非算术 op),其收益体现在生成码形态 +(省去物化-再折叠往返与 3 条冗余 OR),不计入上表。 + +### 真实负载语料(mainnet replay,247 笔) + +当前 upstream/main 的 deep-entry-risk gate(`evm_module.cpp:112`, +`hasUnresolvedNonLiftedDeepEntryRisk`)在 stack-lift 默认关闭的生产配置下 +把语料中 27 个合约的 20 个送回解释器——真实负载的 JIT 覆盖率问题先于 +lowering 质量问题存在(stack-lift 系列工作正在解决)。仍可编译的 7 个合约 +切片上配对测量:总体 exec 加权命中率 26.7% → 45.3%,其中 ISZERO +0% → 100%(13 站点)、SLT 0% → 100%。该切片执行质量过小,只作方向性佐证, +不作 headline。 + +### 性能(evmone-bench,27-bench,multipass,vs upstream/main baseline) + +27-bench 全量(`--benchmark_filter='^external/total/(main|micro)/'`, +median of 5 reps):median delta **-0.23%**,落在本机 multipass ±2pp 运行 +方差内。对首轮离群点(`narrow_compare_u128/loop` +11.8%、 +`swap_math/received` -12.7%)以 15 reps 复测:两者均为亚微秒级噪声,复测 +后分别为 -3.0% 与 -0.2%。体量最大、方差最小的 `snailtracer`(48.4µs, +cv 1.5%)在两轮独立运行中分别 -1.1% 与 -1.3%,方向一致。 + +结论:**无性能回归**;聚合中性,最重的真实程序基准呈一致的小幅改善。窄化 +收益的主要形态是生成码精简(每站点省 3-9 个 MIR 节点与若干 spill),在 +calldata 驱动的真实合约负载上的端到端体现受 JIT 覆盖率限制(见下节)。 + +## Known limitations + +1. **SSA-lift 路径的既有交互**(Codex review 发现,非本变更引入): + `ZEN_ENABLE_EVM_STACK_SSA_LIFT=ON`(默认 OFF,CI OFF)时, + `evm_lifted_stack_lifter.h` 的 `getOperandIdentityKey()` 不识别 deferred + operand,live-out 的 deferred zero-test 会触发断言或错误合并。该暴露面 + 在本变更前即存在(deferred 机制与其跨块生命周期均未变);修复属 + stack-lift 后续系列,建议在该系列中为 deferred 增加 identity key 处理。 +2. 真实负载命中率的执行加权口径受 JIT 覆盖率限制(见上),待 stack-lift + 落地后可用 `tools/run_real_load_profile.py` 复测完整 27 合约口径。 + +## Checklist + +- [x] Implementation complete +- [x] Tests added/updated +- [ ] Module specs in `docs/modules/` updated (if affected) +- [x] Build and tests pass From b45c6d0a141185e816d0536be40e21a87c65d3a8 Mon Sep 17 00:00:00 2001 From: abmcar Date: Wed, 10 Jun 2026 18:14:34 +0800 Subject: [PATCH 3/7] docs(compiler): translate change document to English Co-Authored-By: Claude Fable 5 --- .../README.md | 267 +++++++++++------- 1 file changed, 159 insertions(+), 108 deletions(-) diff --git a/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md b/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md index a03207308..fbbc968d7 100644 --- a/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md +++ b/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md @@ -1,4 +1,4 @@ -# Change: EVM lowering 消费 value-range 标签(bool/compare/bitwise 快路径) +# Change: Consume value-range tags in EVM bool/compare/bitwise lowering - **Status**: Proposed - **Date**: 2026-06-10 @@ -6,138 +6,189 @@ ## Overview -multipass JIT 的 range 分析此前已能证明大量操作数为 u64,但多个 lowering -路径不消费该证明,仍发射全宽 4-limb 序列。本变更让五类 lowering 消费已有的 -ValueRange 标签:在 EEST Cancun 套件上,fast-path 命中率 site 加权从 78.72% -提升到 **80.02%**(+364 个站点转入窄路径,零反向回归),其中 **ISZERO 从 -23.2% 提升到 85.9%**。全部正确性套件通过,新增 21 个对抗性差分 fixture。 +The range analysis in the multipass JIT already proves a large number of +operands to be u64. Several lowering paths do not consume that proof and +still emit full-width 4-limb sequences. This change makes five lowering +categories consume the existing ValueRange tags. On the EEST Cancun suite, +the site-weighted fast-path hit rate rises from 78.72% to **80.02%** (+364 +sites move to narrow paths, zero reverse regressions), with **ISZERO rising +from 23.2% to 85.9%**. All correctness suites pass, and 21 adversarial +differential fixtures are added. ## Motivation -真实负载分析(见 `2026-06-02-real-load-analysis-suite`)量化了两类缺口: -分析侧(静态证不出,占主)与 lowering 侧(已证出但 builder 不消费)。本变更 -关闭 lowering 侧缺口中可立即收割的部分: - -- **ISZERO**:deferred zero-test 物化时无条件 OR-fold 全部 4 limb,且其 0/1 - 结果丢失 U64 标签——真实负载上 58.5% 的 ISZERO FULL 执行操作数已被静态 - 证明 u64。 -- **JUMPI**:条件 lowering 对已标 U64 的条件仍 OR-fold 4 limb;ISZERO 结果 - 作条件时先物化成 0/1 再被整条链重算一遍(`LT;ISZERO;JUMPI` 循环退出模式 - 付三层冗余)。 -- **OR/XOR**:只有 const-u64 快路径,无 range 窄化——真实负载上 51.8% 的 - OR FULL 执行有至少一侧已证 u64。 -- **SLT/SGT**:连 const 快路径都没有(真实负载 SLT FULL 执行的 22.4% 是 - `slt(x, 小常量)` 形态)。 -- **环境 opcode 生产者**(PC/GAS/CALLDATASIZE/CODESIZE/MSIZE/ - RETURNDATASIZE):`convertSingleInstrToU256Operand` 结构性零填高 limb 却 - 返回默认 U256,造成 builder/analyzer 双 SSOT 背离(analyzer 已标 U64)。 +The real-load analysis (see `2026-06-02-real-load-analysis-suite`) +quantified two gap categories: the analysis-side gap (static proof fails; +the majority) and the lowering-side gap (the proof exists but the builder +does not consume it). This change closes the immediately harvestable part +of the lowering-side gap: + +- **ISZERO**: the deferred zero-test materialization unconditionally + OR-folds all 4 limbs, and its 0/1 result loses the U64 tag — on the + real-load corpus, 58.5% of ISZERO FULL executed operands are already + statically proven u64. +- **JUMPI**: the condition lowering still OR-folds 4 limbs for conditions + already tagged U64. When an ISZERO result is the condition, it is first + materialized into 0/1 and then the whole chain is recomputed (the + `LT;ISZERO;JUMPI` loop-exit pattern pays three layers of redundancy). +- **OR/XOR**: only a const-u64 fast path exists, with no range narrowing — + on the real-load corpus, 51.8% of OR FULL executions have at least one + side proven u64. +- **SLT/SGT**: not even a const fast path exists — 22.4% of real-load SLT + FULL executions take the `slt(x, small constant)` shape. +- **Environment opcode producers** (PC/GAS/CALLDATASIZE/CODESIZE/MSIZE/ + RETURNDATASIZE): `convertSingleInstrToU256Operand` structurally + zero-fills the high limbs yet returns the default U256, creating a + builder/analyzer dual-SSOT divergence — the analyzer already tags U64. ## Changes -全部在 `src/compiler/evm_frontend/evm_mir_compiler.{h,cpp}`: - -1. **ISZERO deferred zero-test 携带 range**:`createDeferredZeroTest` 增加 - `BaseRange` 参数(新成员 `DeferredBaseRange`),deferred Operand 自身 - Range 标为 U64(其物化值恒为 0/1,结构性成立)。`handleCompareEQZ` 按 - base range 折叠 1/2/4 个 limb。所有创建/物化点(含 ISZERO 嵌套翻转的 - range 传播)同步更新。 -2. **JUMPI 条件融合与窄化**:deferred zero-test 条件不再物化,直接按 - base range 折叠后与 0 比较(EQ/NE 按否定标志选择谓词);非 deferred 条件 - 按 Range 契约只折叠可能非零的 limb。Dest 与分支/跳转表逻辑不变。 -3. **OR/XOR range 窄化路径**:双操作数均证 U64(非常量)→ 单条 i64 op + - 高 limb 置零,结果标 U64;恰一侧证 U64 → 低 limb op + 宽侧高 limb 直通 - (与既有 const-u64 路径同构),结果标宽侧 range。 -4. **SLT/SGT 对 u64 常量的快路径**:u64 常量高 limb 为零,作为 signed-256 - 是非负值,故 `slt(x, c)` 可降为「符号位 ∨ (高 limb 全零 ∧ limb0 无符号 - 比较)」。新增 `handleCompareSltRhsU64`/`handleCompareSgtRhsU64`,与既有 - 无符号比较 helper 相同的三层 range tier(U64/U128/默认)。两个常量侧 - 均覆盖(`c s c` 互换)。 -5. **环境 opcode 结果标 U64**:`convertSingleInstrToU256Operand` 返回值附 - `ValueRange::U64`——limbs[1..3] 为字面零,值按构造落在 `[0, 2^64-1]`, - 与调用方意图无关地成立。消除一处现存 builder/analyzer 背离。 +All changes are in `src/compiler/evm_frontend/evm_mir_compiler.{h,cpp}`: + +1. **ISZERO deferred zero-test carries the range**: `createDeferredZeroTest` + gains a `BaseRange` parameter (new member `DeferredBaseRange`), and the + deferred operand's own range is tagged U64 — its materialized value is + always 0/1, which holds structurally. `handleCompareEQZ` folds 1/2/4 + limbs according to the base range. All creation and materialization + sites are updated in sync, including range propagation through nested + ISZERO flips. +2. **JUMPI condition fusion and narrowing**: a deferred zero-test condition + is no longer materialized; it is folded according to the base range and + compared against 0 directly, with the EQ/NE predicate chosen by the + negation flag. A non-deferred condition folds only the limbs that may be + non-zero, per the Range contract. The dest and branch/jump-table logic + is unchanged. +3. **OR/XOR range-narrowing paths**: when both operands are proven U64 + (non-constant), a single i64 op plus zeroed high limbs is emitted and + the result is tagged U64. When exactly one side is proven U64, the low + limbs are combined and the wide side's high limbs pass through — + isomorphic to the existing const-u64 path — and the result is tagged + with the wide side's range. +4. **SLT/SGT fast path for u64 constants**: a u64 constant has zero high + limbs and is a non-negative signed-256 value, so `slt(x, c)` lowers to + "sign bit ∨ (high limbs all zero ∧ unsigned limb0 comparison)". New + helpers `handleCompareSltRhsU64`/`handleCompareSgtRhsU64` use the same + three-level range tier (U64/U128/default) as the existing + unsigned-comparison helpers. Both constant sides are covered via the + `c s c` swap. +5. **Environment opcode results tagged U64**: the return value of + `convertSingleInstrToU256Operand` now carries `ValueRange::U64` — + limbs[1..3] are literal zeros and the value falls in `[0, 2^64-1]` by + construction, independent of caller intent. This removes one existing + builder/analyzer divergence. ## Soundness -- Range 契约:`U64` 标签仅表示高 limb 语义为零;所有新路径只在标签已证明处 - 收窄读取宽度,不引入新的标签来源(变更 5 除外,其正确性由函数结构保证)。 -- SLT/SGT:limb0 在 `[2^63, 2^64-1]` 的操作数是正的 256-bit 值,新路径用 - 无符号谓词(`ICMP_ULT/UGT`)比较 limb0,负数情形由 limb3 的符号位判定 - 短路——三层 tier 的真值表经两名独立 reviewer(Opus、Codex)边界枚举核对 - (2^63、2^64、2^128、2^192、2^255、-1、相等、c=0、c≥2^63)。 -- analyzer/builder 对称:ISZERO、比较结果、OR/XOR、环境 opcode 的 builder - 结果 range 与 analyzer transfer(`evm_analyzer.h`)一致或更宽,无 - builder-narrower-than-truth 状态。 -- JUMPI 现在依赖 Range 契约的正确性(过窄的上游标签会导致错分支而非仅变慢), - 已在代码注释中显式记录。 +- Range contract: the `U64` tag only asserts that the high limbs are + semantically zero. All new paths narrow the read width only where the + tag is already proven, and introduce no new tag sources — except change + 5, whose correctness is guaranteed by the function's structure. +- SLT/SGT: an operand with limb0 in `[2^63, 2^64-1]` is a positive 256-bit + value. The new path compares limb0 with unsigned predicates + (`ICMP_ULT/UGT`); the negative case is short-circuited by the sign bit + of limb3. The truth table of the three-level tier was verified by + boundary enumeration by two independent reviewers (Opus, Codex), covering + 2^63, 2^64, 2^128, 2^192, 2^255, -1, equal values, c=0, and c≥2^63. +- analyzer/builder symmetry: the builder result ranges for ISZERO, + comparison results, OR/XOR, and environment opcodes match or are wider + than the analyzer transfer (`evm_analyzer.h`); no + builder-narrower-than-truth state exists. +- JUMPI now depends on the correctness of the Range contract — an + over-narrow upstream tag would cause a wrong branch rather than only a + slowdown. This dependency is recorded explicitly in code comments. ## Verification -- **差分 fixture(新增,随 PR 提交)**:21 个 `.easm` + `.expected` - (`tests/evm_asm/`),覆盖每条新路径的窄路径触发与全路径保留两侧,对抗值 - 含 2^64、2^128、2^192、-1、limb0-MSB(`0x8000000000000000` 无符号谓词 - 硬门)、high-sparse;`EVMRangeNarrowingDifferentialTest` 断言 interp 与 - multipass 输出逐字节一致且 multipass 确实 JIT 编译。21/21 通过, - golden 套件 178/178 无回归。 -- **multipass evmone-unittests**:223/223。 -- **multipass evmone-statetest `-k fork_Cancun`**:2723/2723。 -- **ctest 全量**:11/11(solidityContractTests 需复制 gitignored 的 - `tests/evm_solidity/*/*.json` 生成物到 worktree,属环境数据非回归)。 -- `tools/format.sh check` 通过;构建无新增警告。 -- **双独立 review**:Opus(全六攻击面 CLEAN)+ Codex(除一项既有问题外 - verified-clean,见 Known limitations)。 +- **Differential fixtures (new, shipped with the PR)**: 21 `.easm` + + `.expected` pairs (`tests/evm_asm/`) cover both the narrow-path trigger + side and the full-path preservation side of every new path. Adversarial + values include 2^64, 2^128, 2^192, -1, limb0-MSB + (`0x8000000000000000`, the hard gate for the unsigned predicate), and + high-sparse values. `EVMRangeNarrowingDifferentialTest` asserts that the + interp and multipass outputs are byte-identical and that multipass + actually JIT-compiles. 21/21 pass; the golden suite shows 178/178 with + no regressions. +- **multipass evmone-unittests**: 223/223. +- **multipass evmone-statetest `-k fork_Cancun`**: 2723/2723. +- **Full ctest**: 11/11 — solidityContractTests requires copying the + gitignored `tests/evm_solidity/*/*.json` generated artifacts into the + worktree, which is environment data, not a regression. +- `tools/format.sh check` passes; the build produces no new warnings. +- **Two independent reviews**: Opus (all six attack surfaces CLEAN) and + Codex (verified-clean except one pre-existing issue; see Known + limitations). ## Measurements -### Fast-path 命中率(EEST Cancun,site 加权,配对测量) +### Fast-path hit rate (EEST Cancun, site-weighted, paired measurement) -测量方法:在带 Stream B tap 的测量分支上(tap 仅测量用,不随本 PR 提交), -base 与 base+本变更各 capture 一次,28,109 个共享站点逐点配对。 +Measurement method: on a measurement branch carrying the Stream B tap (the +tap is measurement-only and does not ship with this PR), one capture is +taken for base and one for base plus this change; 28,109 shared sites are +paired point by point. -| op | base | 本变更 | Δ | 迁移站点 | +| op | base | this change | Δ | migrated sites | |---|---:|---:|---:|---| | ISZERO | 23.2% | **85.9%** | **+62.7pp** | 316 × FULL→NARROW_U64 | | SGT | 80.6% | 95.0% | +14.4pp | 20 × FULL→CONST_U64 | | SLT | 61.5% | 66.7% | +5.2pp | 9 × FULL→CONST_U64 | -| ADD | 75.1% | 75.6% | +0.5pp | 17 × FULL→NARROW_U128(环境 opcode 标签解锁) | +| ADD | 75.1% | 75.6% | +0.5pp | 17 × FULL→NARROW_U128 (unlocked by environment-opcode tags) | | OR | 98.7% | 98.9% | +0.2pp | 2 × FULL→NARROW_U64 | -| **总体** | **78.72%** | **80.02%** | **+1.29pp** | +364 站点,零反向迁移 | - -JUMPI 融合不在 tap 覆盖内(JUMPI 非算术 op),其收益体现在生成码形态 -(省去物化-再折叠往返与 3 条冗余 OR),不计入上表。 - -### 真实负载语料(mainnet replay,247 笔) - -当前 upstream/main 的 deep-entry-risk gate(`evm_module.cpp:112`, -`hasUnresolvedNonLiftedDeepEntryRisk`)在 stack-lift 默认关闭的生产配置下 -把语料中 27 个合约的 20 个送回解释器——真实负载的 JIT 覆盖率问题先于 -lowering 质量问题存在(stack-lift 系列工作正在解决)。仍可编译的 7 个合约 -切片上配对测量:总体 exec 加权命中率 26.7% → 45.3%,其中 ISZERO -0% → 100%(13 站点)、SLT 0% → 100%。该切片执行质量过小,只作方向性佐证, -不作 headline。 - -### 性能(evmone-bench,27-bench,multipass,vs upstream/main baseline) - -27-bench 全量(`--benchmark_filter='^external/total/(main|micro)/'`, -median of 5 reps):median delta **-0.23%**,落在本机 multipass ±2pp 运行 -方差内。对首轮离群点(`narrow_compare_u128/loop` +11.8%、 -`swap_math/received` -12.7%)以 15 reps 复测:两者均为亚微秒级噪声,复测 -后分别为 -3.0% 与 -0.2%。体量最大、方差最小的 `snailtracer`(48.4µs, -cv 1.5%)在两轮独立运行中分别 -1.1% 与 -1.3%,方向一致。 - -结论:**无性能回归**;聚合中性,最重的真实程序基准呈一致的小幅改善。窄化 -收益的主要形态是生成码精简(每站点省 3-9 个 MIR 节点与若干 spill),在 -calldata 驱动的真实合约负载上的端到端体现受 JIT 覆盖率限制(见下节)。 +| **Overall** | **78.72%** | **80.02%** | **+1.29pp** | +364 sites, zero reverse migrations | + +ISZERO contributes the bulk of the migrated sites, and every migration +moves toward a narrower path. JUMPI fusion is outside the tap's coverage — +JUMPI is not an arithmetic op. Its benefit shows up in generated-code shape +(the materialize-then-refold round trip and 3 redundant ORs are removed) +and is not counted in the table above. + +### Real-load corpus (mainnet replay, 247 transactions) + +Under the production configuration with stack-lift disabled by default, the +deep-entry-risk gate on current upstream/main (`evm_module.cpp:112`, +`hasUnresolvedNonLiftedDeepEntryRisk`) sends 20 of the corpus's 27 +contracts back to the interpreter. The real-load JIT coverage problem +therefore precedes the lowering quality problem; the stack-lift series of +work is addressing it. Paired measurement on the slice of 7 contracts that +still compile: the overall execution-weighted hit rate rises from 26.7% to +45.3%, with ISZERO from 0% to 100% (13 sites) and SLT from 0% to 100%. +This slice carries too little execution volume; it serves only as a +directional corroboration, not as the headline. + +### Performance (evmone-bench, 27-bench, multipass, vs upstream/main baseline) + +Across the full 27-bench set +(`--benchmark_filter='^external/total/(main|micro)/'`, median of 5 reps), +the median delta is **-0.23%**, within the ±2pp multipass run variance on +this machine. The first-round outliers (`narrow_compare_u128/loop` +11.8%, +`swap_math/received` -12.7%) were re-measured with 15 reps: both are +sub-microsecond-scale noise, landing at -3.0% and -0.2% respectively after +re-measurement. The largest and lowest-variance benchmark, `snailtracer` +(48.4µs, cv 1.5%), shows -1.1% and -1.3% across two independent runs, in a +consistent direction. + +Conclusion: **no performance regression**. The aggregate is neutral, and +the heaviest realistic-program benchmark shows a consistent small +improvement. The narrowing benefit primarily takes the form of leaner +generated code — 3-9 MIR nodes and several spills saved per site. Its +end-to-end effect on calldata-driven real-contract loads is limited by JIT +coverage (see the next section). ## Known limitations -1. **SSA-lift 路径的既有交互**(Codex review 发现,非本变更引入): - `ZEN_ENABLE_EVM_STACK_SSA_LIFT=ON`(默认 OFF,CI OFF)时, - `evm_lifted_stack_lifter.h` 的 `getOperandIdentityKey()` 不识别 deferred - operand,live-out 的 deferred zero-test 会触发断言或错误合并。该暴露面 - 在本变更前即存在(deferred 机制与其跨块生命周期均未变);修复属 - stack-lift 后续系列,建议在该系列中为 deferred 增加 identity key 处理。 -2. 真实负载命中率的执行加权口径受 JIT 覆盖率限制(见上),待 stack-lift - 落地后可用 `tools/run_real_load_profile.py` 复测完整 27 合约口径。 +1. **Pre-existing interaction with the SSA-lift path** (found in the Codex + review; not introduced by this change): with + `ZEN_ENABLE_EVM_STACK_SSA_LIFT=ON` (default OFF, CI OFF), + `getOperandIdentityKey()` in `evm_lifted_stack_lifter.h` does not + recognize deferred operands, so a live-out deferred zero-test triggers + an assertion or an incorrect merge. This exposure existed before this + change — neither the deferred mechanism nor its cross-block lifetime + changed. The fix belongs to the stack-lift follow-up series, which + should add identity-key handling for deferred operands. +2. The execution-weighted view of the real-load hit rate is limited by JIT + coverage (see above). After the stack-lift work lands, the full + 27-contract view can be re-measured with + `tools/run_real_load_profile.py`. ## Checklist From 8cdcc538a29f98b70fcc5b4cd353cb88352787a6 Mon Sep 17 00:00:00 2001 From: abmcar Date: Wed, 10 Jun 2026 19:22:41 +0800 Subject: [PATCH 4/7] docs(compiler): note overlap with upstream #532/#524 after rebase Co-Authored-By: Claude Fable 5 --- .../README.md | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md b/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md index fbbc968d7..8d13fcfa4 100644 --- a/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md +++ b/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md @@ -8,7 +8,7 @@ The range analysis in the multipass JIT already proves a large number of operands to be u64. Several lowering paths do not consume that proof and -still emit full-width 4-limb sequences. This change makes five lowering +still emit full-width 4-limb sequences. This change makes four lowering categories consume the existing ValueRange tags. On the EEST Cancun suite, the site-weighted fast-path hit rate rises from 78.72% to **80.02%** (+364 sites move to narrow paths, zero reverse regressions), with **ISZERO rising @@ -36,22 +36,24 @@ of the lowering-side gap: side proven u64. - **SLT/SGT**: not even a const fast path exists — 22.4% of real-load SLT FULL executions take the `slt(x, small constant)` shape. -- **Environment opcode producers** (PC/GAS/CALLDATASIZE/CODESIZE/MSIZE/ - RETURNDATASIZE): `convertSingleInstrToU256Operand` structurally - zero-fills the high limbs yet returns the default U256, creating a - builder/analyzer dual-SSOT divergence — the analyzer already tags U64. + +A fifth gap identified by the same analysis — environment opcode producers +(PC/GAS/CALLDATASIZE/CODESIZE/MSIZE/RETURNDATASIZE) returning the default +U256 despite structurally zero high limbs — landed upstream separately as +#532 while this change was in review, and is no longer part of this diff. ## Changes All changes are in `src/compiler/evm_frontend/evm_mir_compiler.{h,cpp}`: -1. **ISZERO deferred zero-test carries the range**: `createDeferredZeroTest` - gains a `BaseRange` parameter (new member `DeferredBaseRange`), and the - deferred operand's own range is tagged U64 — its materialized value is - always 0/1, which holds structurally. `handleCompareEQZ` folds 1/2/4 - limbs according to the base range. All creation and materialization - sites are updated in sync, including range propagation through nested - ISZERO flips. +1. **ISZERO deferred zero-test carries the base range**: + `createDeferredZeroTest` gains a `BaseRange` parameter (new member + `DeferredBaseRange`), and `handleCompareEQZ` folds 1/2/4 limbs according + to the base range. All creation and materialization sites are updated in + sync, including range propagation through nested ISZERO flips. (The + companion result tag — the deferred operand's own U64 range, sound since + its materialized value is always 0/1 — landed upstream via #524 during + review; this change keeps the base-range plumbing and the narrow fold.) 2. **JUMPI condition fusion and narrowing**: a deferred zero-test condition is no longer materialized; it is folded according to the base range and compared against 0 directly, with the EQ/NE predicate chosen by the @@ -71,18 +73,12 @@ All changes are in `src/compiler/evm_frontend/evm_mir_compiler.{h,cpp}`: three-level range tier (U64/U128/default) as the existing unsigned-comparison helpers. Both constant sides are covered via the `c s c` swap. -5. **Environment opcode results tagged U64**: the return value of - `convertSingleInstrToU256Operand` now carries `ValueRange::U64` — - limbs[1..3] are literal zeros and the value falls in `[0, 2^64-1]` by - construction, independent of caller intent. This removes one existing - builder/analyzer divergence. ## Soundness - Range contract: the `U64` tag only asserts that the high limbs are semantically zero. All new paths narrow the read width only where the - tag is already proven, and introduce no new tag sources — except change - 5, whose correctness is guaranteed by the function's structure. + tag is already proven, and introduce no new tag sources. - SLT/SGT: an operand with limb0 in `[2^63, 2^64-1]` is a positive 256-bit value. The new path compares limb0 with unsigned predicates (`ICMP_ULT/UGT`); the negative case is short-circuited by the sign bit @@ -125,14 +121,16 @@ All changes are in `src/compiler/evm_frontend/evm_mir_compiler.{h,cpp}`: Measurement method: on a measurement branch carrying the Stream B tap (the tap is measurement-only and does not ship with this PR), one capture is taken for base and one for base plus this change; 28,109 shared sites are -paired point by point. +paired point by point. The measurement predates the upstream merge of #532 +and #524; the treatment side included the environment-opcode tag that has +since landed as #532 (see the ADD row for its attribution). | op | base | this change | Δ | migrated sites | |---|---:|---:|---:|---| | ISZERO | 23.2% | **85.9%** | **+62.7pp** | 316 × FULL→NARROW_U64 | | SGT | 80.6% | 95.0% | +14.4pp | 20 × FULL→CONST_U64 | | SLT | 61.5% | 66.7% | +5.2pp | 9 × FULL→CONST_U64 | -| ADD | 75.1% | 75.6% | +0.5pp | 17 × FULL→NARROW_U128 (unlocked by environment-opcode tags) | +| ADD | 75.1% | 75.6% | +0.5pp | 17 × FULL→NARROW_U128 (unlocked by the environment-opcode tags, since landed upstream as #532) | | OR | 98.7% | 98.9% | +0.2pp | 2 × FULL→NARROW_U64 | | **Overall** | **78.72%** | **80.02%** | **+1.29pp** | +364 sites, zero reverse migrations | From 029c8f308319b20a82668897261c7e9e59713b99 Mon Sep 17 00:00:00 2001 From: abmcar Date: Wed, 10 Jun 2026 20:16:41 +0800 Subject: [PATCH 5/7] docs(docs): mark range-lowering-gaps doc implemented and fix reproduce step Set Status to Implemented. Reword the two references to repository paths that ship with the separate mainnet-replay analysis-suite work and are absent from this tree: the motivation's real-load analysis pointer and the known-limitation re-measure step (formerly tools/run_real_load_profile.py). Co-Authored-By: Claude Fable 5 --- .../2026-06-10-evm-range-lowering-gaps/README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md b/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md index 8d13fcfa4..136ac4859 100644 --- a/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md +++ b/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md @@ -1,6 +1,6 @@ # Change: Consume value-range tags in EVM bool/compare/bitwise lowering -- **Status**: Proposed +- **Status**: Implemented - **Date**: 2026-06-10 - **Tier**: Light @@ -17,8 +17,9 @@ differential fixtures are added. ## Motivation -The real-load analysis (see `2026-06-02-real-load-analysis-suite`) -quantified two gap categories: the analysis-side gap (static proof fails; +A separate real-load analysis (shipped by the in-flight mainnet-replay +analysis-suite work, not part of this diff) quantified two gap categories: +the analysis-side gap (static proof fails; the majority) and the lowering-side gap (the proof exists but the builder does not consume it). This change closes the immediately harvestable part of the lowering-side gap: @@ -185,8 +186,10 @@ coverage (see the next section). should add identity-key handling for deferred operands. 2. The execution-weighted view of the real-load hit rate is limited by JIT coverage (see above). After the stack-lift work lands, the full - 27-contract view can be re-measured with - `tools/run_real_load_profile.py`. + 27-contract view can be re-measured by replaying the mainnet-replay + corpus through the multipass JIT and capturing the per-opcode fast-path + hit rate (the profiling tooling ships with the separate mainnet-replay + analysis-suite work). ## Checklist From d6bb2bd545569066f5ff44cce55bdf3a480d86ba Mon Sep 17 00:00:00 2001 From: abmcar Date: Thu, 11 Jun 2026 15:39:49 +0800 Subject: [PATCH 6/7] test: move differential suite to the consolidated test change The EVMRangeNarrowingDifferentialTest suite and its 21 evm_asm fixtures relocate to the dedicated differential-suite change so optimization PRs stay code-only and the shared evm_interp_tests file stops accumulating per-PR copies. Co-Authored-By: Claude Fable 5 --- .../README.md | 24 +++++---- src/tests/evm_interp_tests.cpp | 54 ------------------- 2 files changed, 14 insertions(+), 64 deletions(-) diff --git a/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md b/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md index 136ac4859..0554b95d2 100644 --- a/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md +++ b/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md @@ -12,8 +12,11 @@ still emit full-width 4-limb sequences. This change makes four lowering categories consume the existing ValueRange tags. On the EEST Cancun suite, the site-weighted fast-path hit rate rises from 78.72% to **80.02%** (+364 sites move to narrow paths, zero reverse regressions), with **ISZERO rising -from 23.2% to 85.9%**. All correctness suites pass, and 21 adversarial -differential fixtures are added. +from 23.2% to 85.9%**. All correctness suites pass. The adversarial +differential coverage for these lowering paths now ships separately with the +consolidated EVM differential suite change +(`docs/changes/2026-06-11-evm-differential-suite/`), which carries the 21 +fixtures for these paths. ## Motivation @@ -96,15 +99,16 @@ All changes are in `src/compiler/evm_frontend/evm_mir_compiler.{h,cpp}`: ## Verification -- **Differential fixtures (new, shipped with the PR)**: 21 `.easm` + - `.expected` pairs (`tests/evm_asm/`) cover both the narrow-path trigger - side and the full-path preservation side of every new path. Adversarial - values include 2^64, 2^128, 2^192, -1, limb0-MSB +- **Differential fixtures (ship separately)**: the 21 `.easm` + `.expected` + pairs and the `EVMRangeNarrowingDifferentialTest` suite for these lowering + paths now ship with the consolidated EVM differential suite change + (`docs/changes/2026-06-11-evm-differential-suite/`). The fixtures cover both + the narrow-path trigger side and the full-path preservation side of every + new path, with adversarial values including 2^64, 2^128, 2^192, -1, limb0-MSB (`0x8000000000000000`, the hard gate for the unsigned predicate), and - high-sparse values. `EVMRangeNarrowingDifferentialTest` asserts that the - interp and multipass outputs are byte-identical and that multipass - actually JIT-compiles. 21/21 pass; the golden suite shows 178/178 with - no regressions. + high-sparse values; the suite asserts that the interp and multipass outputs + are byte-identical and that multipass actually JIT-compiles. This change + carries no test fixtures. - **multipass evmone-unittests**: 223/223. - **multipass evmone-statetest `-k fork_Cancun`**: 2723/2723. - **Full ctest**: 11/11 — solidityContractTests requires copying the diff --git a/src/tests/evm_interp_tests.cpp b/src/tests/evm_interp_tests.cpp index 433f308f2..2198aba34 100644 --- a/src/tests/evm_interp_tests.cpp +++ b/src/tests/evm_interp_tests.cpp @@ -924,60 +924,6 @@ TEST(EVMRegressionTest, Issue541_AddU64ConstLastAdcCarryChainPreserved) { EXPECT_EQ(InterpExec.OutputHex, "0000000000000000000000000000000000000000000000000000000000000000"); } - -// Differential regression for the range-narrowed lowering paths added on the -// perf/evm-range-lowering-gaps branch (narrowed ISZERO/JUMPI folds, U64-tagged -// OR/XOR, and the signed LT/GT u64-const fast paths). Each fixture feeds a -// dynamic (analyzer-unprovable) operand so the new paths are actually taken, -// then asserts the multipass JIT output matches the interpreter (the ground -// truth) exactly. A divergence here means a narrowed fold produced a wrong -// value, not merely a slower one. -class EVMRangeNarrowingDifferentialTest - : public ::testing::TestWithParam {}; - -std::string -GetRangeNarrowingTestName(const testing::TestParamInfo &Info) { - return Info.param; -} - -TEST_P(EVMRangeNarrowingDifferentialTest, InterpMatchesMultipass) { - const std::string Stem = GetParam(); - const auto FilePath = (getEvmAsmDirPath() / (Stem + ".evm.hex")).string(); - - auto InterpExec = - executeEvmBytecodeFile(FilePath, common::RunMode::InterpMode); - auto MultipassExec = - executeEvmBytecodeFile(FilePath, common::RunMode::MultipassMode); - -#ifdef ZEN_ENABLE_JIT - EXPECT_TRUE(MultipassExec.JITCompiled) - << "Multipass JIT should compile " << Stem; -#endif - - EXPECT_EQ(InterpExec.Status, EVMC_SUCCESS) - << "Interpreter did not succeed for " << Stem; - EXPECT_EQ(MultipassExec.Status, EVMC_SUCCESS) - << "Multipass did not succeed for " << Stem; - EXPECT_EQ(MultipassExec.Status, InterpExec.Status) - << "Multipass status diverged from interpreter for " << Stem; - EXPECT_EQ(MultipassExec.OutputHex, InterpExec.OutputHex) - << "Multipass output diverged from interpreter for " << Stem; -} - -INSTANTIATE_TEST_SUITE_P( - RangeNarrowingDifferential, EVMRangeNarrowingDifferentialTest, - ::testing::Values("iszero_dyn_u64_nonzero", "iszero_dyn_highsparse", - "iszero_calldatasize", "jumpi_iszero_fused_taken", - "jumpi_iszero_fused_nottaken_highsparse", - "jumpi_iszero_iszero_fused", "jumpi_u64_cond_taken", - "jumpi_u64_cond_nottaken", "or_dyn_u64_u64", - "xor_dyn_u64_u64", "or_dyn_u64_wide", "xor_dyn_u64_wide", - "slt_dyn_neg_vs_const", "slt_dyn_highsparse_vs_const", - "slt_dyn_msb64_vs_const", "slt_dyn_eq_const", - "sgt_dyn_neg_vs_const", "sgt_dyn_highsparse_vs_const", - "sgt_dyn_msb64_vs_const", "slt_const_vs_dyn", - "sgt_const_vs_dyn"), - GetRangeNarrowingTestName); #endif // Test that chain_id and blob_base_fee can be saved and loaded via state From 5e53858575a6c0d2201226f37693814c295351da Mon Sep 17 00:00:00 2001 From: abmcar <52450271+abmcar@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:33:15 +0800 Subject: [PATCH 7/7] docs: replace instrumentation labels and review narrative in range doc Align the doc with the technical-writing rule: describe internal labels by behavior, remove who-reviewed and process narrative, and keep every count, flag, code anchor, and measurement. No code or runtime change. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Cen5bPpPEgkSkcxWWTSY7d --- .../README.md | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md b/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md index 0554b95d2..89abb4c0b 100644 --- a/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md +++ b/docs/changes/2026-06-10-evm-range-lowering-gaps/README.md @@ -11,8 +11,8 @@ operands to be u64. Several lowering paths do not consume that proof and still emit full-width 4-limb sequences. This change makes four lowering categories consume the existing ValueRange tags. On the EEST Cancun suite, the site-weighted fast-path hit rate rises from 78.72% to **80.02%** (+364 -sites move to narrow paths, zero reverse regressions), with **ISZERO rising -from 23.2% to 85.9%**. All correctness suites pass. The adversarial +sites move to narrow paths, zero reverse regressions), with ISZERO rising +from 23.2% to 85.9%. All correctness suites pass. The adversarial differential coverage for these lowering paths now ships separately with the consolidated EVM differential suite change (`docs/changes/2026-06-11-evm-differential-suite/`), which carries the 21 @@ -24,22 +24,22 @@ A separate real-load analysis (shipped by the in-flight mainnet-replay analysis-suite work, not part of this diff) quantified two gap categories: the analysis-side gap (static proof fails; the majority) and the lowering-side gap (the proof exists but the builder -does not consume it). This change closes the immediately harvestable part -of the lowering-side gap: +does not consume it). This change closes the part of the lowering-side gap +where the proof already exists and the builder can consume it directly: - **ISZERO**: the deferred zero-test materialization unconditionally OR-folds all 4 limbs, and its 0/1 result loses the U64 tag — on the - real-load corpus, 58.5% of ISZERO FULL executed operands are already + real-load corpus, 58.5% of ISZERO full-width executed operands are already statically proven u64. - **JUMPI**: the condition lowering still OR-folds 4 limbs for conditions already tagged U64. When an ISZERO result is the condition, it is first materialized into 0/1 and then the whole chain is recomputed (the `LT;ISZERO;JUMPI` loop-exit pattern pays three layers of redundancy). - **OR/XOR**: only a const-u64 fast path exists, with no range narrowing — - on the real-load corpus, 51.8% of OR FULL executions have at least one + on the real-load corpus, 51.8% of OR full-width executions have at least one side proven u64. - **SLT/SGT**: not even a const fast path exists — 22.4% of real-load SLT - FULL executions take the `slt(x, small constant)` shape. + full-width executions take the `slt(x, small constant)` shape. A fifth gap identified by the same analysis — environment opcode producers (PC/GAS/CALLDATASIZE/CODESIZE/MSIZE/RETURNDATASIZE) returning the default @@ -87,7 +87,7 @@ All changes are in `src/compiler/evm_frontend/evm_mir_compiler.{h,cpp}`: value. The new path compares limb0 with unsigned predicates (`ICMP_ULT/UGT`); the negative case is short-circuited by the sign bit of limb3. The truth table of the three-level tier was verified by - boundary enumeration by two independent reviewers (Opus, Codex), covering + boundary enumeration over 2^63, 2^64, 2^128, 2^192, 2^255, -1, equal values, c=0, and c≥2^63. - analyzer/builder symmetry: the builder result ranges for ISZERO, comparison results, OR/XOR, and environment opcodes match or are wider @@ -115,16 +115,14 @@ All changes are in `src/compiler/evm_frontend/evm_mir_compiler.{h,cpp}`: gitignored `tests/evm_solidity/*/*.json` generated artifacts into the worktree, which is environment data, not a regression. - `tools/format.sh check` passes; the build produces no new warnings. -- **Two independent reviews**: Opus (all six attack surfaces CLEAN) and - Codex (verified-clean except one pre-existing issue; see Known - limitations). ## Measurements ### Fast-path hit rate (EEST Cancun, site-weighted, paired measurement) -Measurement method: on a measurement branch carrying the Stream B tap (the -tap is measurement-only and does not ship with this PR), one capture is +Measurement method: on a measurement branch carrying a per-opcode +fast-path-hit-rate counter (measurement-only, does not ship with this PR), +one capture is taken for base and one for base plus this change; 28,109 shared sites are paired point by point. The measurement predates the upstream merge of #532 and #524; the treatment side included the environment-opcode tag that has @@ -132,15 +130,15 @@ since landed as #532 (see the ADD row for its attribution). | op | base | this change | Δ | migrated sites | |---|---:|---:|---:|---| -| ISZERO | 23.2% | **85.9%** | **+62.7pp** | 316 × FULL→NARROW_U64 | -| SGT | 80.6% | 95.0% | +14.4pp | 20 × FULL→CONST_U64 | -| SLT | 61.5% | 66.7% | +5.2pp | 9 × FULL→CONST_U64 | -| ADD | 75.1% | 75.6% | +0.5pp | 17 × FULL→NARROW_U128 (unlocked by the environment-opcode tags, since landed upstream as #532) | -| OR | 98.7% | 98.9% | +0.2pp | 2 × FULL→NARROW_U64 | -| **Overall** | **78.72%** | **80.02%** | **+1.29pp** | +364 sites, zero reverse migrations | +| ISZERO | 23.2% | **85.9%** | +62.7pp | 316 sites: full-width → 64-bit narrow path | +| SGT | 80.6% | 95.0% | +14.4pp | 20 sites: full-width → u64-constant fast path | +| SLT | 61.5% | 66.7% | +5.2pp | 9 sites: full-width → u64-constant fast path | +| ADD | 75.1% | 75.6% | +0.5pp | 17 sites: full-width → 128-bit narrow path (unlocked by the environment-opcode tags, since landed upstream as #532) | +| OR | 98.7% | 98.9% | +0.2pp | 2 sites: full-width → 64-bit narrow path | +| **Overall** | 78.72% | **80.02%** | +1.29pp | +364 sites, zero reverse migrations | ISZERO contributes the bulk of the migrated sites, and every migration -moves toward a narrower path. JUMPI fusion is outside the tap's coverage — +moves toward a narrower path. JUMPI fusion is outside the counter's coverage — JUMPI is not an arithmetic op. Its benefit shows up in generated-code shape (the materialize-then-refold round trip and 3 redundant ORs are removed) and is not counted in the table above. @@ -148,11 +146,12 @@ and is not counted in the table above. ### Real-load corpus (mainnet replay, 247 transactions) Under the production configuration with stack-lift disabled by default, the -deep-entry-risk gate on current upstream/main (`evm_module.cpp:112`, +gate that rejects contracts with unresolved non-lifted deep stack entries +on current upstream/main (`evm_module.cpp:112`, `hasUnresolvedNonLiftedDeepEntryRisk`) sends 20 of the corpus's 27 contracts back to the interpreter. The real-load JIT coverage problem -therefore precedes the lowering quality problem; the stack-lift series of -work is addressing it. Paired measurement on the slice of 7 contracts that +therefore precedes the lowering quality problem; the in-progress SSA +stack-lifting work is addressing it. Paired measurement on the slice of 7 contracts that still compile: the overall execution-weighted hit rate rises from 26.7% to 45.3%, with ISZERO from 0% to 100% (13 sites) and SLT from 0% to 100%. This slice carries too little execution volume; it serves only as a @@ -162,7 +161,7 @@ directional corroboration, not as the headline. Across the full 27-bench set (`--benchmark_filter='^external/total/(main|micro)/'`, median of 5 reps), -the median delta is **-0.23%**, within the ±2pp multipass run variance on +the median delta is **-0.23%**, within the ~2% multipass run-to-run variance on this machine. The first-round outliers (`narrow_compare_u128/loop` +11.8%, `swap_math/received` -12.7%) were re-measured with 15 reps: both are sub-microsecond-scale noise, landing at -3.0% and -0.2% respectively after @@ -179,14 +178,14 @@ coverage (see the next section). ## Known limitations -1. **Pre-existing interaction with the SSA-lift path** (found in the Codex - review; not introduced by this change): with +1. **Pre-existing interaction with the SSA stack-lifting path** (not + introduced by this change): with `ZEN_ENABLE_EVM_STACK_SSA_LIFT=ON` (default OFF, CI OFF), `getOperandIdentityKey()` in `evm_lifted_stack_lifter.h` does not recognize deferred operands, so a live-out deferred zero-test triggers an assertion or an incorrect merge. This exposure existed before this change — neither the deferred mechanism nor its cross-block lifetime - changed. The fix belongs to the stack-lift follow-up series, which + changed. The fix belongs to the SSA stack-lifting follow-up work, which should add identity-key handling for deferred operands. 2. The execution-weighted view of the real-load hit rate is limited by JIT coverage (see above). After the stack-lift work lands, the full