From b1f6fa60fb4ed2f4be762c4f16817a2fc5793a5d Mon Sep 17 00:00:00 2001 From: ECNUyhy <93872942+ECNUyhy@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:19:07 +0000 Subject: [PATCH 1/2] refactor(evm): retain terminating memory opcode identity Record RETURN and REVERT helper PCs after closing their source block so later lowering can query the exact per-op memory facts. Previously handleEndBlock cleared the current memory opcode identity before the terminating helper was lowered. --- src/action/evm_bytecode_visitor.h | 2 ++ .../evm_frontend/evm_mir_compiler.cpp | 1 + src/tests/evm_jit_frontend_tests.cpp | 31 ++++++++++++++++++- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/action/evm_bytecode_visitor.h b/src/action/evm_bytecode_visitor.h index 20d88056..14aff508 100644 --- a/src/action/evm_bytecode_visitor.h +++ b/src/action/evm_bytecode_visitor.h @@ -916,6 +916,7 @@ template class EVMByteCodeVisitor { Operand MemOffset = pop(); Operand Length = pop(); handleEndBlock(); + Builder.noteHelperOpcodeInBlock(Opcode, PC); Builder.handleReturn(MemOffset, Length); break; } @@ -924,6 +925,7 @@ template class EVMByteCodeVisitor { Operand OffsetOp = pop(); Operand SizeOp = pop(); handleEndBlock(); + Builder.noteHelperOpcodeInBlock(Opcode, PC); Builder.handleRevert(OffsetOp, SizeOp); break; } diff --git a/src/compiler/evm_frontend/evm_mir_compiler.cpp b/src/compiler/evm_frontend/evm_mir_compiler.cpp index 9ff0e22f..aaf5d3bf 100644 --- a/src/compiler/evm_frontend/evm_mir_compiler.cpp +++ b/src/compiler/evm_frontend/evm_mir_compiler.cpp @@ -8639,6 +8639,7 @@ void EVMMirBuilder::noteMemoryOpcodeInBlock(evmc_opcode Opcode, uint64_t PC) { } void EVMMirBuilder::noteHelperOpcodeInBlock(evmc_opcode Opcode, uint64_t PC) { + CurrentMemoryOpPC = PC; if (!CurBlockMemStats.Active) { return; } diff --git a/src/tests/evm_jit_frontend_tests.cpp b/src/tests/evm_jit_frontend_tests.cpp index f046c7ad..74985f34 100644 --- a/src/tests/evm_jit_frontend_tests.cpp +++ b/src/tests/evm_jit_frontend_tests.cpp @@ -1370,6 +1370,11 @@ struct MockMeterOpcodeRangeRecord { uint64_t EndPCExclusive = 0; }; +struct MockHelperOpcodeRecord { + evmc_opcode Opcode = OP_STOP; + uint64_t PC = 0; +}; + struct MockConstPrecheckPlanRecord { uint64_t MaxRequiredSize = 0; uint64_t CoveredDirectOps = 0; @@ -1694,7 +1699,9 @@ class MockEVMBuilder { LinearPrecheckPrepareCount++; } void noteMemoryOpcodeInBlock(evmc_opcode, uint64_t) {} - void noteHelperOpcodeInBlock(evmc_opcode, uint64_t) {} + void noteHelperOpcodeInBlock(evmc_opcode Opcode, uint64_t PC) { + HelperOpcodes.push_back({Opcode, PC}); + } void endMemoryCompileBlock() {} void handleJump(Operand) {} @@ -1721,6 +1728,10 @@ class MockEVMBuilder { return MeteredRanges; } + const std::vector &helperOpcodes() const { + return HelperOpcodes; + } + const std::vector &constPrecheckPlans() const { return ConstPrecheckPlans; } @@ -1826,6 +1837,7 @@ class MockEVMBuilder { std::array Stats = {}; std::array MeteredOpcodeCounts = {}; std::vector MeteredRanges; + std::vector HelperOpcodes; std::vector ConstPrecheckPlans; MockMStoreRecord LastMStore = {}; uint32_t MStoreCount = 0; @@ -1957,6 +1969,23 @@ bool compileWithMockBuilder(const std::vector &Bytecode, return Visitor.compile(); } +TEST(EVMJITFrontendVisitorTest, TerminatingMemoryHelpersRetainExactOpcodePC) { + const std::vector ReturnBytecode = {OP_PUSH0, OP_PUSH0, OP_RETURN}; + const std::vector RevertBytecode = {OP_PUSH0, OP_PUSH0, OP_REVERT}; + + MockEVMBuilder ReturnBuilder; + ASSERT_TRUE(compileWithMockBuilder(ReturnBytecode, ReturnBuilder)); + ASSERT_EQ(ReturnBuilder.helperOpcodes().size(), 1u); + EXPECT_EQ(ReturnBuilder.helperOpcodes()[0].Opcode, OP_RETURN); + EXPECT_EQ(ReturnBuilder.helperOpcodes()[0].PC, 2u); + + MockEVMBuilder RevertBuilder; + ASSERT_TRUE(compileWithMockBuilder(RevertBytecode, RevertBuilder)); + ASSERT_EQ(RevertBuilder.helperOpcodes().size(), 1u); + EXPECT_EQ(RevertBuilder.helperOpcodes()[0].Opcode, OP_REVERT); + EXPECT_EQ(RevertBuilder.helperOpcodes()[0].PC, 2u); +} + TEST(EVMJITFrontendVisitorTest, DynamicJumpConsistencyErrorEscapesVisitor) { const std::vector Bytecode = { 0x60, 0x04, // PC0 PUSH1 0x04 (analyzer resolves a constant destination) From d14a06ab32b17565c47b4fd8787a90a6131ec666 Mon Sep 17 00:00:00 2001 From: ECNUyhy <93872942+ECNUyhy@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:19:56 +0000 Subject: [PATCH 2/2] perf(evm): reuse CFG proofs for terminating memory Let RETURN and REVERT reuse guaranteed memory-size facts produced by the CFG analysis when their constant output range is already covered. Previously both terminators emitted another expansion check even after a predecessor had established the same range; dynamic, uncovered, overflow-prone, and zero-size cases keep the existing fallback semantics. --- .../evm_frontend/evm_mir_compiler.cpp | 39 +++++- src/compiler/evm_frontend/evm_mir_compiler.h | 5 +- src/tests/CMakeLists.txt | 5 +- src/tests/evm_differential_tests.cpp | 41 ++++++ src/tests/evm_terminating_memory_tests.cpp | 122 ++++++++++++++++++ 5 files changed, 204 insertions(+), 8 deletions(-) create mode 100644 src/tests/evm_terminating_memory_tests.cpp diff --git a/src/compiler/evm_frontend/evm_mir_compiler.cpp b/src/compiler/evm_frontend/evm_mir_compiler.cpp index aaf5d3bf..8a066571 100644 --- a/src/compiler/evm_frontend/evm_mir_compiler.cpp +++ b/src/compiler/evm_frontend/evm_mir_compiler.cpp @@ -5656,7 +5656,13 @@ void EVMMirBuilder::handleReturn(Operand MemOffsetComponents, #ifdef ZEN_ENABLE_EVM_GAS_REGISTER syncGasToMemoryFull(); #endif - preExpandMemoryRange(MemOffsetComponents, LengthComponents); + const bool UsedGuaranteedElision = + preExpandMemoryRange(MemOffsetComponents, LengthComponents, true); +#ifdef ZEN_ENABLE_MULTIPASS_JIT_LOGGING + if (UsedGuaranteedElision) { + ++MemStats.ReturnGuaranteedElisionCount; + } +#endif callRuntimeForWithErrorCheck( RuntimeFunctions.SetReturnNoExpand, MemOffsetComponents, LengthComponents); @@ -5732,7 +5738,13 @@ void EVMMirBuilder::handleRevert(Operand OffsetOp, Operand SizeOp) { #ifdef ZEN_ENABLE_EVM_GAS_REGISTER syncGasToMemoryFull(); #endif - preExpandMemoryRange(OffsetOp, SizeOp); + const bool UsedGuaranteedElision = + preExpandMemoryRange(OffsetOp, SizeOp, true); +#ifdef ZEN_ENABLE_MULTIPASS_JIT_LOGGING + if (UsedGuaranteedElision) { + ++MemStats.RevertGuaranteedElisionCount; + } +#endif callRuntimeForWithErrorCheck( RuntimeFunctions.SetRevertNoExpand, OffsetOp, SizeOp); @@ -6737,15 +6749,24 @@ void EVMMirBuilder::checkStaticModeIR() { setInsertBlock(ContinueBB); } -void EVMMirBuilder::preExpandMemoryRange(Operand &Offset, Operand &Size) { +bool EVMMirBuilder::preExpandMemoryRange(Operand &Offset, Operand &Size, + bool AllowGuaranteedElision) { if (Size.isZeroConstant()) { const U256Value ZeroValue = {0, 0, 0, 0}; Offset = Operand(ZeroValue); Size = Operand(ZeroValue); - return; + return false; } + const bool OffsetWasConst = Offset.isConstU64(); + const uint64_t ConstOffset = OffsetWasConst ? Offset.getConstValue()[0] : 0; + const bool SizeWasConst = Size.isConstU64(); + const uint64_t ConstSize = SizeWasConst ? Size.getConstValue()[0] : 0; normalizeOffsetWithSize(Offset, Size); + if (AllowGuaranteedElision && OffsetWasConst && SizeWasConst && + tryUseGuaranteedMinBytesExpansionElision(true, ConstOffset, ConstSize)) { + return true; + } MType *I64Type = &Ctx.I64Type; MInstruction *OffsetLow = extractKnownU64LowOperand(Offset); @@ -6756,6 +6777,7 @@ void EVMMirBuilder::preExpandMemoryRange(Operand &Offset, Operand &Size) { false, CmpInstruction::Predicate::ICMP_ULT, I64Type, RequiredSize, OffsetLow); expandMemoryIR(RequiredSize, Overflow); + return false; } void EVMMirBuilder::preExpandKeccakTwoWordMemory(Operand &OffsetComponents) { @@ -7327,6 +7349,8 @@ bool EVMMirBuilder::hasMemoryCompileStats() const { MemStats.ReloadMemorySizeCount != 0 || MemStats.GetMemoryDataPointerCount != 0 || MemStats.ExpandNeedExpandCFGCount != 0 || + MemStats.ReturnGuaranteedElisionCount != 0 || + MemStats.RevertGuaranteedElisionCount != 0 || MemStats.MemoryExpansionPlanGroupingCandidates != 0 || MemStats.MemoryExpansionPlanLinearRegionCandidates != 0 || MemStats.MemoryExpansionPlanPrecheckCandidates != 0 || @@ -7762,7 +7786,8 @@ void EVMMirBuilder::dumpMemoryCompileStats() const { "disp_bytes32_mload_ops=%llu disp_bytes32_mstore_ops=%llu " "mstore_zero_limb_stores=%llu mstore_overlap_elided_limbs=%llu " "mstore_addr_value_alias_reuse=%llu " - "need_expand_cfg=%llu", + "need_expand_cfg=%llu return_guaranteed_elision=%llu " + "revert_guaranteed_elision=%llu", static_cast(MemStats.MLoadExpandCount), static_cast(MemStats.MStoreExpandCount), static_cast(MemStats.MStore8ExpandCount), @@ -7930,7 +7955,9 @@ void EVMMirBuilder::dumpMemoryCompileStats() const { static_cast(MemStats.MStoreZeroLimbStoreCount), static_cast(MemStats.MStoreOverlapElidedLimbCount), static_cast(MemStats.MStoreAddrValueAliasReuseCount), - static_cast(MemStats.ExpandNeedExpandCFGCount)); + static_cast(MemStats.ExpandNeedExpandCFGCount), + static_cast(MemStats.ReturnGuaranteedElisionCount), + static_cast(MemStats.RevertGuaranteedElisionCount)); ZEN_LOG_DEBUG( "[EVM-MEM-SUMMARY] small_frame_candidate_total=%llu " diff --git a/src/compiler/evm_frontend/evm_mir_compiler.h b/src/compiler/evm_frontend/evm_mir_compiler.h index b2c045f3..e521a9bd 100644 --- a/src/compiler/evm_frontend/evm_mir_compiler.h +++ b/src/compiler/evm_frontend/evm_mir_compiler.h @@ -1340,7 +1340,8 @@ class EVMMirBuilder final { MInstruction *extractKnownU64LowOperand(const Operand &Opnd); void checkStaticModeIR(); void normalizeOffsetWithSize(Operand &Offset, Operand &Size); - void preExpandMemoryRange(Operand &Offset, Operand &Size); + bool preExpandMemoryRange(Operand &Offset, Operand &Size, + bool AllowGuaranteedElision = false); Operand convertSingleInstrToU256Operand(MInstruction *SingleInstr); Operand convertU256InstrToU256Operand(MInstruction *U256Instr); @@ -1557,6 +1558,8 @@ class EVMMirBuilder final { uint64_t MemoryBaseCacheUseCount = 0; uint64_t ExpandNeedExpandCFGCount = 0; + uint64_t ReturnGuaranteedElisionCount = 0; + uint64_t RevertGuaranteedElisionCount = 0; uint64_t SmallFrameCandidateTotal = 0; uint64_t SmallFramePrecheckedTotal = 0; diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 009cc6e2..f1cd5f25 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -66,7 +66,10 @@ if(ZEN_ENABLE_SPEC_TEST) add_executable(evmCacheComplexityDemo evm_cache_complexity_demo.cpp) target_link_libraries(evmCacheComplexityDemo PRIVATE dtvmcore) if(ZEN_ENABLE_MULTIPASS_JIT) - add_executable(evmJitFrontendTests evm_jit_frontend_tests.cpp) + add_executable( + evmJitFrontendTests evm_jit_frontend_tests.cpp + evm_terminating_memory_tests.cpp + ) add_executable(evmRangeAnalyzerTests evm_range_analyzer_tests.cpp) add_executable(evmDifferentialTests evm_differential_tests.cpp) add_executable( diff --git a/src/tests/evm_differential_tests.cpp b/src/tests/evm_differential_tests.cpp index 82dcecd9..4f447516 100644 --- a/src/tests/evm_differential_tests.cpp +++ b/src/tests/evm_differential_tests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "compiler/evm_frontend/evm_analyzer.h" @@ -350,6 +351,46 @@ TEST(EVMMemoryTerminationDifferential, DynamicEmptyRevertIgnoresHighOffset) { "dynamic_empty_revert_high_offset", Bytecode, EVMC_REVERT)); } +TEST(EVMMemoryTerminationDifferential, + CrossBlockCoveredRangeMatchesStatusOutputAndGas) { + const std::vector Prefix = { + 0x60, 0x01, // PC0 PUSH1 1: value + 0x60, 0x80, // PC2 PUSH1 0x80: memory offset + 0x52, // PC4 MSTORE: prove memory covers [0, 0xa0) + 0x60, 0x08, // PC5 PUSH1 8 + 0x56, // PC7 JUMP + 0x5b, // PC8 JUMPDEST + 0x60, 0x20, // PC9 PUSH1 32: output size + 0x60, 0x80, // PC11 PUSH1 0x80: output offset + }; + const std::string ExpectedOutput = + "0000000000000000000000000000000000000000000000000000000000000001"; + + for (const auto &[Name, Terminator, ExpectedStatus] : + std::vector>{ + {"return", 0xf3, EVMC_SUCCESS}, + {"revert", 0xfd, EVMC_REVERT}, + }) { + std::vector Bytecode = Prefix; + Bytecode.push_back(Terminator); + + const auto Interp = + runEvmBytecode(std::string(Name) + "_covered_range_interp", Bytecode, + common::RunMode::InterpMode, {}, 0u, true); + const auto Multi = + runEvmBytecode(std::string(Name) + "_covered_range_multipass", Bytecode, + common::RunMode::MultipassMode, {}, 0u, true); +#ifdef ZEN_ENABLE_JIT + EXPECT_TRUE(Multi.JITCompiled); +#endif + EXPECT_EQ(Interp.Status, ExpectedStatus); + EXPECT_EQ(Multi.Status, Interp.Status); + EXPECT_EQ(Multi.GasLeft, Interp.GasLeft); + EXPECT_EQ(Multi.OutputHex, Interp.OutputHex); + EXPECT_EQ(Interp.OutputHex, ExpectedOutput); + } +} + // Range-narrowed lowering paths: the range-narrowed ISZERO/JUMPI folds, // U64-tagged OR/XOR, and the signed-compare (SLT/SGT) u64-const fast paths in // the multipass lowering. Each fixture feeds a dynamic operand so the narrowed diff --git a/src/tests/evm_terminating_memory_tests.cpp b/src/tests/evm_terminating_memory_tests.cpp new file mode 100644 index 00000000..d1352188 --- /dev/null +++ b/src/tests/evm_terminating_memory_tests.cpp @@ -0,0 +1,122 @@ +// Copyright (C) 2025 the DTVM authors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "compiler/evm_frontend/evm_imported.h" +#include "compiler/evm_frontend/evm_mir_compiler.h" +#include "compiler/mir/module.h" + +#include "llvm/Support/raw_ostream.h" +#include + +#include +#include +#include +#include + +namespace { + +using COMPILER::EVMMirBuilder; + +#ifdef ZEN_ENABLE_EVM_MEMORY_PLAN_FRAMEWORK +std::optional +countTerminatingExpandMemoryCalls(const std::vector &Bytecode) { + COMPILER::EVMFrontendContext Ctx; + Ctx.setRevision(EVMC_CANCUN); + Ctx.setBytecode(reinterpret_cast(Bytecode.data()), + Bytecode.size()); + + COMPILER::MModule Mod(Ctx); + std::array ParamTypes = { + COMPILER::MPointerType::create(Ctx, Ctx.VoidType)}; + COMPILER::MFunctionType *FuncType = COMPILER::MFunctionType::create( + Ctx, Ctx.VoidType, llvm::ArrayRef(ParamTypes)); + Mod.addFuncType(FuncType); + + COMPILER::MFunction Func(Ctx, 0); + Func.setFunctionType(FuncType); + EVMMirBuilder Builder(Ctx, Func); + if (!Builder.compile(&Ctx)) { + return std::nullopt; + } + + std::string Mir; + llvm::raw_string_ostream OS(Mir); + Func.print(OS); + OS.flush(); + + const auto &RuntimeFunctions = COMPILER::getRuntimeFunctionTable(); + const auto Address = + COMPILER::getFunctionAddress(RuntimeFunctions.ExpandMemoryNoGas); + const std::string Needle = + "target = const.i64 " + std::to_string(Address) + ", "; + size_t Count = 0; + for (size_t Pos = Mir.find(Needle); Pos != std::string::npos; + Pos = Mir.find(Needle, Pos + Needle.size())) { + ++Count; + } + return Count; +} + +TEST(EVMMirBuilderTerminatingMemoryProofTest, + ReusesCrossBlockProofForReturnAndRevert) { + const std::vector ZeroSizeReturn = { + OP_PUSH1, 0x01, OP_PUSH1, 0x80, OP_MSTORE, OP_PUSH1, + 0x08, OP_JUMP, OP_JUMPDEST, OP_PUSH0, OP_PUSH0, OP_RETURN}; + const std::vector CoveredReturn = { + OP_PUSH1, 0x01, OP_PUSH1, 0x80, OP_MSTORE, OP_PUSH1, 0x08, + OP_JUMP, OP_JUMPDEST, OP_PUSH1, 0x20, OP_PUSH1, 0x80, OP_RETURN}; + const std::vector ZeroSizeRevert = { + OP_PUSH1, 0x01, OP_PUSH1, 0x80, OP_MSTORE, OP_PUSH1, + 0x08, OP_JUMP, OP_JUMPDEST, OP_PUSH0, OP_PUSH0, OP_REVERT}; + const std::vector CoveredRevert = { + OP_PUSH1, 0x01, OP_PUSH1, 0x80, OP_MSTORE, OP_PUSH1, 0x08, + OP_JUMP, OP_JUMPDEST, OP_PUSH1, 0x20, OP_PUSH1, 0x80, OP_REVERT}; + + const auto ZeroReturnCalls = + countTerminatingExpandMemoryCalls(ZeroSizeReturn); + const auto ReturnCalls = countTerminatingExpandMemoryCalls(CoveredReturn); + const auto ZeroRevertCalls = + countTerminatingExpandMemoryCalls(ZeroSizeRevert); + const auto RevertCalls = countTerminatingExpandMemoryCalls(CoveredRevert); + ASSERT_TRUE(ZeroReturnCalls.has_value()); + ASSERT_TRUE(ReturnCalls.has_value()); + ASSERT_TRUE(ZeroRevertCalls.has_value()); + ASSERT_TRUE(RevertCalls.has_value()); + EXPECT_EQ(*ReturnCalls, *ZeroReturnCalls); + EXPECT_EQ(*RevertCalls, *ZeroRevertCalls); +} + +TEST(EVMMirBuilderTerminatingMemoryProofTest, + RetainsUncoveredFallbackAndZeroSizeSemantics) { + const std::vector ZeroSizeReturn = { + OP_PUSH1, 0x01, OP_PUSH1, 0x80, OP_MSTORE, OP_PUSH1, + 0x08, OP_JUMP, OP_JUMPDEST, OP_PUSH0, OP_PUSH0, OP_RETURN}; + const std::vector UncoveredReturn = { + OP_PUSH1, 0x01, OP_PUSH1, 0x80, OP_MSTORE, OP_PUSH1, 0x08, + OP_JUMP, OP_JUMPDEST, OP_PUSH1, 0x20, OP_PUSH1, 0xa0, OP_RETURN}; + const std::vector HugeOffsetZeroSizeRevert = { + OP_PUSH1, 0x01, OP_PUSH1, 0x80, OP_MSTORE, OP_PUSH1, 0x08, + OP_JUMP, OP_JUMPDEST, OP_PUSH0, OP_PUSH8, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, OP_REVERT}; + const std::vector ZeroSizeRevert = { + OP_PUSH1, 0x01, OP_PUSH1, 0x80, OP_MSTORE, OP_PUSH1, + 0x08, OP_JUMP, OP_JUMPDEST, OP_PUSH0, OP_PUSH0, OP_REVERT}; + + const auto ZeroReturnCalls = + countTerminatingExpandMemoryCalls(ZeroSizeReturn); + const auto UncoveredCalls = + countTerminatingExpandMemoryCalls(UncoveredReturn); + const auto HugeOffsetZeroSizeCalls = + countTerminatingExpandMemoryCalls(HugeOffsetZeroSizeRevert); + const auto ZeroRevertCalls = + countTerminatingExpandMemoryCalls(ZeroSizeRevert); + ASSERT_TRUE(ZeroReturnCalls.has_value()); + ASSERT_TRUE(UncoveredCalls.has_value()); + ASSERT_TRUE(HugeOffsetZeroSizeCalls.has_value()); + ASSERT_TRUE(ZeroRevertCalls.has_value()); + EXPECT_EQ(*UncoveredCalls, *ZeroReturnCalls + 1); + EXPECT_EQ(*HugeOffsetZeroSizeCalls, *ZeroRevertCalls); +} +#endif + +} // namespace