From 42dddd72a266de691317b110cd02ecf5c2c8e9c8 Mon Sep 17 00:00:00 2001 From: ZR74 <2401889661@qq.com> Date: Sat, 11 Apr 2026 16:10:29 +0800 Subject: [PATCH 1/3] refactor(evm): extract shared jump target resolution pass into bytecode cache --- src/action/evm_bytecode_visitor.h | 3 + src/compiler/evm_compiler.cpp | 1 + src/compiler/evm_frontend/evm_analyzer.h | 58 +++- src/compiler/evm_frontend/evm_mir_compiler.h | 10 + src/evm/evm_cache.cpp | 268 +++++++++++++++++-- src/evm/evm_cache.h | 6 + 6 files changed, 328 insertions(+), 18 deletions(-) diff --git a/src/action/evm_bytecode_visitor.h b/src/action/evm_bytecode_visitor.h index 07a1726ad..1b38e1d1f 100644 --- a/src/action/evm_bytecode_visitor.h +++ b/src/action/evm_bytecode_visitor.h @@ -144,6 +144,9 @@ template class EVMByteCodeVisitor { reinterpret_cast(Ctx->getBytecode()); size_t BytecodeSize = Ctx->getBytecodeSize(); EVMAnalyzer Analyzer(Ctx->getRevision()); + if (Ctx->getResolvedJumpTargets()) { + Analyzer.setResolvedJumpTargets(Ctx->getResolvedJumpTargets()); + } Analyzer.analyze(Bytecode, BytecodeSize); initializeLiftedBlocks(Analyzer); diff --git a/src/compiler/evm_compiler.cpp b/src/compiler/evm_compiler.cpp index 04d45ad60..b20d4f222 100644 --- a/src/compiler/evm_compiler.cpp +++ b/src/compiler/evm_compiler.cpp @@ -69,6 +69,7 @@ void EagerEVMJITCompiler::compile() { const auto &Cache = EVMMod->getBytecodeCache(); Ctx.setGasChunkInfo(Cache.GasChunkEnd.data(), Cache.GasChunkCost.data(), EVMMod->CodeSize); + Ctx.setResolvedJumpTargets(&Cache.ResolvedJumpTargets); MModule Mod(Ctx); buildEVMFunction(Ctx, Mod, *EVMMod); diff --git a/src/compiler/evm_frontend/evm_analyzer.h b/src/compiler/evm_frontend/evm_analyzer.h index efda727ee..c3260367f 100644 --- a/src/compiler/evm_frontend/evm_analyzer.h +++ b/src/compiler/evm_frontend/evm_analyzer.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -391,6 +392,11 @@ class EVMAnalyzer { return It == JumpDestCanonicalPCs.end() ? PC : It->second; } + void setResolvedJumpTargets( + const std::unordered_map *Targets) { + SharedResolvedJumpTargets = Targets; + } + bool hasUnknownDynamicJumpTargets() const { return HasUnknownDynamicJump; } bool analyzeSuitabilityOnly(const uint8_t *Bytecode, size_t BytecodeSize) { @@ -414,6 +420,44 @@ class EVMAnalyzer { } private: + // Fallback: look up pre-resolved jump target from the shared cache. + // Used when local abstract stack analysis cannot resolve the jump. + // Overload for JUMP (unconditional). + bool tryResolveFromSharedMap(size_t JumpPC, BlockInfo &Info) { + if (!SharedResolvedJumpTargets) { + return false; + } + auto It = SharedResolvedJumpTargets->find(static_cast(JumpPC)); + if (It == SharedResolvedJumpTargets->end()) { + return false; + } + uint64_t TargetPC = static_cast(It->second); + Info.HasConstantJump = true; + Info.ConstantJumpTargetPC = TargetPC; + Info.Successors.push_back(TargetPC); + return true; + } + + // Overload for JUMPI (conditional): also receives FallthroughEntryPC to + // avoid adding a duplicate successor edge. + bool tryResolveFromSharedMap(size_t JumpPC, BlockInfo &Info, + uint64_t FallthroughEntryPC) { + if (!SharedResolvedJumpTargets) { + return false; + } + auto It = SharedResolvedJumpTargets->find(static_cast(JumpPC)); + if (It == SharedResolvedJumpTargets->end()) { + return false; + } + uint64_t TargetPC = static_cast(It->second); + Info.HasConstantJump = true; + Info.ConstantJumpTargetPC = TargetPC; + if (TargetPC != FallthroughEntryPC) { + Info.Successors.push_back(TargetPC); + } + return true; + } + void resetAnalysisState() { BlockInfos.clear(); DynamicJumpRegions.clear(); @@ -665,6 +709,8 @@ class EVMAnalyzer { Info.HasConstantJump = true; Info.ConstantJumpTargetPC = getCanonicalJumpDestPC(Dest.Low); Info.Successors.push_back(Info.ConstantJumpTargetPC); + } else if (tryResolveFromSharedMap(ScanPC - 1, Info)) { + // Resolved via shared cache (covers patterns like SWAPn→JUMP). } else { Info.HasDynamicJump = true; HasUnknownDynamicJump = true; @@ -702,9 +748,13 @@ class EVMAnalyzer { Info.Successors.push_back(Info.ConstantJumpTargetPC); } } else if (!Dest.KnownConst || !Dest.FitsU64) { - Info.HasDynamicJump = true; - HasUnknownDynamicJump = true; - Info.DynamicJumpTargetRegionEntryPC = FallthroughEntryPC; + if (tryResolveFromSharedMap(ScanPC - 1, Info, FallthroughEntryPC)) { + // Resolved via shared cache. + } else { + Info.HasDynamicJump = true; + HasUnknownDynamicJump = true; + Info.DynamicJumpTargetRegionEntryPC = FallthroughEntryPC; + } } NextEntryPC = FallthroughEntryPC; NextBodyStartPC = FallthroughBodyStartPC; @@ -1332,6 +1382,8 @@ class EVMAnalyzer { bool HasUnknownDynamicJump = false; evmc_revision Revision = zen::evm::DEFAULT_REVISION; JITSuitabilityResult JITResult; + const std::unordered_map *SharedResolvedJumpTargets = + nullptr; }; } // namespace COMPILER diff --git a/src/compiler/evm_frontend/evm_mir_compiler.h b/src/compiler/evm_frontend/evm_mir_compiler.h index 34b88c1e9..b1868c22b 100644 --- a/src/compiler/evm_frontend/evm_mir_compiler.h +++ b/src/compiler/evm_frontend/evm_mir_compiler.h @@ -12,6 +12,7 @@ #include "evm/evm.h" #include "evmc/instructions.h" #include "intx/intx.hpp" +#include #include // Forward declaration to avoid circular dependency @@ -78,6 +79,14 @@ class EVMFrontendContext final : public CompileContext { return GasChunkEnd && GasChunkCost && GasChunkSize > 0; } + void setResolvedJumpTargets( + const std::unordered_map *Targets) { + ResolvedJumpTargets = Targets; + } + const std::unordered_map *getResolvedJumpTargets() const { + return ResolvedJumpTargets; + } + void setRevision(evmc_revision Rev) { Revision = Rev; } evmc_revision getRevision() const { return Revision; } @@ -93,6 +102,7 @@ class EVMFrontendContext final : public CompileContext { const uint32_t *GasChunkEnd = nullptr; const uint64_t *GasChunkCost = nullptr; size_t GasChunkSize = 0; + const std::unordered_map *ResolvedJumpTargets = nullptr; evmc_revision Revision = zen::evm::DEFAULT_REVISION; #ifdef ZEN_ENABLE_EVM_GAS_REGISTER bool GasRegisterEnabled = false; diff --git a/src/evm/evm_cache.cpp b/src/evm/evm_cache.cpp index cc5a6208e..0926664dd 100644 --- a/src/evm/evm_cache.cpp +++ b/src/evm/evm_cache.cpp @@ -906,12 +906,243 @@ static bool lemma614Update(uint32_t NodeId, const std::vector &Blocks, return true; } -static bool buildGasChunksSPP(const zen::common::Byte *Code, size_t CodeSize, - const evmc_instruction_metrics *MetricsTable, - const std::vector &JumpDestMap, - const std::vector &PushValueMap, - std::vector &GasChunkEnd, - std::vector &GasChunkCost) { +// ============== Shared Jump Target Resolution ================================ +// +// Abstract stack simulation that resolves JUMP/JUMPI targets across the entire +// bytecode in a single linear pass. The result is stored in the cache and +// consumed by both the SPP gas optimizer and the SSA liftability analyzer. +// +// Unlike the pattern-matching approach in resolveConstantJumpTarget (which only +// handles adjacent PUSH+JUMP), this pass tracks constant values through DUP and +// SWAP instructions, resolving strictly more jump targets. + +struct AbstractValue { + bool KnownConst = false; + bool FitsU64 = false; + uint64_t Low = 0; + + static AbstractValue unknown() { return {}; } + + static AbstractValue fromPush(const zen::common::Byte *Code, size_t CodeSize, + size_t ImmStart, size_t ImmSize) { + AbstractValue V; + V.KnownConst = true; + V.FitsU64 = true; + V.Low = 0; + if (ImmSize == 0) { + return V; + } + const size_t Available = ImmStart < CodeSize ? (CodeSize - ImmStart) : 0; + const size_t ReadCount = std::min(ImmSize, Available); + auto readByte = [&](size_t Index) -> uint8_t { + return Index < ReadCount ? static_cast(Code[ImmStart + Index]) + : uint8_t{0}; + }; + if (ImmSize > sizeof(uint64_t)) { + for (size_t I = 0; I < ImmSize - sizeof(uint64_t); ++I) { + if (readByte(I) != 0) { + V.FitsU64 = false; + break; + } + } + } + size_t ValueStart = + ImmSize > sizeof(uint64_t) ? ImmSize - sizeof(uint64_t) : size_t{0}; + for (size_t I = ValueStart; I < ImmSize; ++I) { + V.Low = (V.Low << 8) | static_cast(readByte(I)); + } + return V; + } +}; + +// Returns the number of immediate bytes for an opcode (PUSH0=0, PUSH1=1, ...). +static size_t pushImmediateSize(uint8_t OpcodeU8) { + if (OpcodeU8 >= static_cast(evmc_opcode::OP_PUSH0) && + OpcodeU8 <= static_cast(evmc_opcode::OP_PUSH32)) { + return static_cast(OpcodeU8 - + static_cast(evmc_opcode::OP_PUSH0)); + } + return 0; +} + +static bool isBlockTerminatorForJumpResolution(uint8_t OpcodeU8) { + switch (static_cast(OpcodeU8)) { + case evmc_opcode::OP_JUMP: + case evmc_opcode::OP_STOP: + case evmc_opcode::OP_RETURN: + case evmc_opcode::OP_INVALID: + case evmc_opcode::OP_REVERT: + case evmc_opcode::OP_SELFDESTRUCT: + return true; + default: + return false; + } +} + +static void ensureAbstractDepth(std::vector &Stack, + size_t &EntryDepth, size_t Required) { + if (Stack.size() >= Required) { + return; + } + size_t Deficit = Required - Stack.size(); + Stack.insert(Stack.begin(), Deficit, AbstractValue::unknown()); + EntryDepth += Deficit; +} + +// Resolve jump targets by simulating the abstract stack per control-flow block. +// Writes results into ResolvedTargets: JumpPC → canonical target JUMPDEST PC. +static void resolveJumpTargetsByAbstractStack( + const zen::common::Byte *Code, size_t CodeSize, + const std::vector &JumpDestMap, + std::unordered_map &ResolvedTargets) { + if (CodeSize == 0) { + return; + } + + // Build canonical JUMPDEST mapping: consecutive JUMPDEST runs share the + // last PC as canonical representative (same logic as evm_analyzer.h). + std::unordered_map CanonicalJumpDest; + { + size_t Pc = 0; + while (Pc < CodeSize) { + uint8_t Op = static_cast(Code[Pc]); + if (Op == static_cast(evmc_opcode::OP_JUMPDEST)) { + size_t RunStart = Pc; + size_t RunEnd = Pc; + while (RunEnd + 1 < CodeSize && + static_cast(Code[RunEnd + 1]) == + static_cast(evmc_opcode::OP_JUMPDEST)) { + ++RunEnd; + } + for (size_t P = RunStart; P <= RunEnd; ++P) { + CanonicalJumpDest[static_cast(P)] = + static_cast(RunEnd); + } + Pc = RunEnd + 1; + continue; + } + Pc += opcodeLen(Op); + } + } + + auto hasCanonicalJumpDest = [&](uint64_t Dest) -> bool { + return Dest < CodeSize && JumpDestMap[Dest] != 0; + }; + auto getCanonicalJumpDestPC = [&](uint64_t Dest) -> uint32_t { + auto It = CanonicalJumpDest.find(static_cast(Dest)); + return It != CanonicalJumpDest.end() ? It->second + : static_cast(Dest); + }; + + const evmc_instruction_metrics *Metrics = + evmc_get_instruction_metrics_table(zen::evm::DEFAULT_REVISION); + + // Scan bytecode block by block. + size_t ScanPC = 0; + while (ScanPC < CodeSize) { + // Skip leading JUMPDEST(s) at block entry. + while (ScanPC < CodeSize && + static_cast(Code[ScanPC]) == + static_cast(evmc_opcode::OP_JUMPDEST)) { + ++ScanPC; + } + + // Simulate abstract stack within this block. + std::vector Stack; + size_t EntryDepth = 0; + + while (ScanPC < CodeSize) { + uint8_t Op = static_cast(Code[ScanPC]); + + // A JUMPDEST in the middle of scanning means a new block starts. + if (Op == static_cast(evmc_opcode::OP_JUMPDEST)) { + break; + } + + ++ScanPC; // advance past opcode byte + size_t ImmSize = pushImmediateSize(Op); + + if (Op == static_cast(evmc_opcode::OP_JUMP)) { + ensureAbstractDepth(Stack, EntryDepth, 1); + AbstractValue Dest = Stack.back(); + Stack.pop_back(); + if (Dest.KnownConst && Dest.FitsU64 && hasCanonicalJumpDest(Dest.Low)) { + uint32_t JumpPC = static_cast(ScanPC - 1); + ResolvedTargets[JumpPC] = getCanonicalJumpDestPC(Dest.Low); + } + // Skip dead code until next JUMPDEST or end. + while (ScanPC < CodeSize && + static_cast(Code[ScanPC]) != + static_cast(evmc_opcode::OP_JUMPDEST)) { + ScanPC += opcodeLen(static_cast(Code[ScanPC])); + } + break; + } + + if (Op == static_cast(evmc_opcode::OP_JUMPI)) { + ensureAbstractDepth(Stack, EntryDepth, 2); + AbstractValue Dest = Stack.back(); + Stack.pop_back(); + Stack.pop_back(); // condition + if (Dest.KnownConst && Dest.FitsU64 && hasCanonicalJumpDest(Dest.Low)) { + uint32_t JumpPC = static_cast(ScanPC - 1); + ResolvedTargets[JumpPC] = getCanonicalJumpDestPC(Dest.Low); + } + // Fallthrough continues to the next instruction (new block). + break; + } + + if (isBlockTerminatorForJumpResolution(Op)) { + // Non-jump terminator (STOP, RETURN, REVERT, etc.): skip dead code. + while (ScanPC < CodeSize && + static_cast(Code[ScanPC]) != + static_cast(evmc_opcode::OP_JUMPDEST)) { + ScanPC += opcodeLen(static_cast(Code[ScanPC])); + } + break; + } + + // Stack-manipulating instructions. + if (Op >= static_cast(evmc_opcode::OP_DUP1) && + Op <= static_cast(evmc_opcode::OP_DUP16)) { + size_t Depth = static_cast( + Op - static_cast(evmc_opcode::OP_DUP1) + 1); + ensureAbstractDepth(Stack, EntryDepth, Depth); + Stack.push_back(Stack[Stack.size() - Depth]); + } else if (Op >= static_cast(evmc_opcode::OP_SWAP1) && + Op <= static_cast(evmc_opcode::OP_SWAP16)) { + size_t Depth = static_cast( + Op - static_cast(evmc_opcode::OP_SWAP1) + 2); + ensureAbstractDepth(Stack, EntryDepth, Depth); + std::swap(Stack.back(), Stack[Stack.size() - Depth]); + } else if (Op >= static_cast(evmc_opcode::OP_PUSH0) && + Op <= static_cast(evmc_opcode::OP_PUSH32)) { + Stack.push_back( + AbstractValue::fromPush(Code, CodeSize, ScanPC, ImmSize)); + ScanPC += ImmSize; + } else { + // Generic instruction: pop inputs, push unknown outputs. + int PopCount = Metrics[Op].stack_height_required; + int PushCount = PopCount + Metrics[Op].stack_height_change; + ensureAbstractDepth(Stack, EntryDepth, static_cast(PopCount)); + for (int I = 0; I < PopCount; ++I) { + Stack.pop_back(); + } + for (int I = 0; I < PushCount; ++I) { + Stack.push_back(AbstractValue::unknown()); + } + } + } + } +} + +static bool buildGasChunksSPP( + const zen::common::Byte *Code, size_t CodeSize, + const evmc_instruction_metrics *MetricsTable, + const std::vector &JumpDestMap, + const std::vector &PushValueMap, + const std::unordered_map &ResolvedJumpTargets, + std::vector &GasChunkEnd, std::vector &GasChunkCost) { std::vector Blocks; std::vector BlockAtPc; buildGasBlocks(Code, CodeSize, MetricsTable, Blocks, BlockAtPc); @@ -920,14 +1151,14 @@ static bool buildGasChunksSPP(const zen::common::Byte *Code, size_t CodeSize, return true; } + // Check if ALL jumps are resolved. If any remains dynamic, fall back to + // per-block metering (no SPP benefit) to preserve correctness. bool HasDynamicJump = false; for (const auto &Block : Blocks) { if (!isJumpOpcode(Block.LastOpcode)) { continue; } - uint32_t DestPc = 0; - if (!resolveConstantJumpTarget(JumpDestMap, PushValueMap, CodeSize, Block, - DestPc)) { + if (ResolvedJumpTargets.count(Block.LastPc) == 0) { HasDynamicJump = true; break; } @@ -974,12 +1205,11 @@ static bool buildGasChunksSPP(const zen::common::Byte *Code, size_t CodeSize, } } - // Add jump edge (if static jump) + // Add jump edge using pre-resolved targets from abstract stack simulation. if (isJumpOpcode(Block.LastOpcode)) { - uint32_t DestPc = 0; - if (resolveConstantJumpTarget(JumpDestMap, PushValueMap, CodeSize, Block, - DestPc)) { - const uint32_t SuccId = BlockAtPc[DestPc]; + auto It = ResolvedJumpTargets.find(Block.LastPc); + if (It != ResolvedJumpTargets.end()) { + const uint32_t SuccId = BlockAtPc[It->second]; if (SuccId != UINT32_MAX) { addEdge(Blocks, static_cast(BlockId), SuccId); } @@ -1135,13 +1365,21 @@ void buildBytecodeCache(EVMBytecodeCache &Cache, const common::Byte *Code, buildJumpDestMapAndPushCache(Code, CodeSize, Cache.JumpDestMap, Cache.PushValueMap); + + // Shared jump target resolution: abstract stack simulation run once, + // results consumed by both SPP gas optimizer and SSA liftability analyzer. + Cache.ResolvedJumpTargets.clear(); + resolveJumpTargetsByAbstractStack(Code, CodeSize, Cache.JumpDestMap, + Cache.ResolvedJumpTargets); + const auto *MetricsTable = evmc_get_instruction_metrics_table(Rev); if (!MetricsTable) { MetricsTable = evmc_get_instruction_metrics_table(DEFAULT_REVISION); } buildGasChunksSPP(Code, CodeSize, MetricsTable, Cache.JumpDestMap, - Cache.PushValueMap, Cache.GasChunkEnd, Cache.GasChunkCost); + Cache.PushValueMap, Cache.ResolvedJumpTargets, + Cache.GasChunkEnd, Cache.GasChunkCost); } } // namespace zen::evm diff --git a/src/evm/evm_cache.h b/src/evm/evm_cache.h index cd7dd69ec..38573fa40 100644 --- a/src/evm/evm_cache.h +++ b/src/evm/evm_cache.h @@ -11,6 +11,7 @@ #include #include +#include #include namespace zen::evm { @@ -20,6 +21,11 @@ struct EVMBytecodeCache { std::vector PushValueMap; std::vector GasChunkEnd; std::vector GasChunkCost; + /// Pre-resolved jump targets via abstract stack simulation. + /// Key: PC of the JUMP/JUMPI opcode. + /// Value: canonical target JUMPDEST PC. + /// Only constant jumps are present; absence means dynamic/unresolved. + std::unordered_map ResolvedJumpTargets; }; void buildBytecodeCache(EVMBytecodeCache &Cache, const common::Byte *Code, From e7f27fa88b918b9a3dab284dc1ca233a86c150c6 Mon Sep 17 00:00:00 2001 From: ZR74 <2401889661@qq.com> Date: Sat, 11 Apr 2026 17:07:36 +0800 Subject: [PATCH 2/3] fix(evm): address review feedback on shared jump resolution pass - Store raw (non-canonicalized) JUMPDEST PCs in ResolvedJumpTargets so SPP gas blocks correctly account for intermediate JUMPDESTs in consecutive runs. The SSA analyzer canonicalizes in its own consumer. - Pass the caller's evmc_revision metrics table into the shared pass instead of hardcoding DEFAULT_REVISION, so opcodes like PUSH0 get correct stack effects under their respective revisions. - Add clarifying comment for the JUMPI case where a known-constant destination is not a valid JUMPDEST (intentional: traps at runtime). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/compiler/evm_frontend/evm_analyzer.h | 12 ++++- src/evm/evm_cache.cpp | 63 +++++++----------------- 2 files changed, 27 insertions(+), 48 deletions(-) diff --git a/src/compiler/evm_frontend/evm_analyzer.h b/src/compiler/evm_frontend/evm_analyzer.h index c3260367f..3ff17097e 100644 --- a/src/compiler/evm_frontend/evm_analyzer.h +++ b/src/compiler/evm_frontend/evm_analyzer.h @@ -422,6 +422,8 @@ class EVMAnalyzer { private: // Fallback: look up pre-resolved jump target from the shared cache. // Used when local abstract stack analysis cannot resolve the jump. + // The shared map stores raw (non-canonicalized) PCs; we canonicalize here + // because the SSA analyzer uses canonical JUMPDEST PCs for block identity. // Overload for JUMP (unconditional). bool tryResolveFromSharedMap(size_t JumpPC, BlockInfo &Info) { if (!SharedResolvedJumpTargets) { @@ -431,7 +433,8 @@ class EVMAnalyzer { if (It == SharedResolvedJumpTargets->end()) { return false; } - uint64_t TargetPC = static_cast(It->second); + uint64_t RawPC = static_cast(It->second); + uint64_t TargetPC = getCanonicalJumpDestPC(RawPC); Info.HasConstantJump = true; Info.ConstantJumpTargetPC = TargetPC; Info.Successors.push_back(TargetPC); @@ -449,7 +452,8 @@ class EVMAnalyzer { if (It == SharedResolvedJumpTargets->end()) { return false; } - uint64_t TargetPC = static_cast(It->second); + uint64_t RawPC = static_cast(It->second); + uint64_t TargetPC = getCanonicalJumpDestPC(RawPC); Info.HasConstantJump = true; Info.ConstantJumpTargetPC = TargetPC; if (TargetPC != FallthroughEntryPC) { @@ -756,6 +760,10 @@ class EVMAnalyzer { Info.DynamicJumpTargetRegionEntryPC = FallthroughEntryPC; } } + // Note: the implicit else (KnownConst && FitsU64 but NOT a valid + // JUMPDEST) is intentionally left without a successor for the taken + // branch — at runtime that path traps with BAD_JUMP_DESTINATION, + // so it is effectively dead. Only the fallthrough is reachable. NextEntryPC = FallthroughEntryPC; NextBodyStartPC = FallthroughBodyStartPC; HasNextBlock = true; diff --git a/src/evm/evm_cache.cpp b/src/evm/evm_cache.cpp index 0926664dd..25da89948 100644 --- a/src/evm/evm_cache.cpp +++ b/src/evm/evm_cache.cpp @@ -990,52 +990,23 @@ static void ensureAbstractDepth(std::vector &Stack, } // Resolve jump targets by simulating the abstract stack per control-flow block. -// Writes results into ResolvedTargets: JumpPC → canonical target JUMPDEST PC. +// Writes results into ResolvedTargets: JumpPC → exact target JUMPDEST PC. +// The stored PC is the raw PUSH value (NOT canonicalized), so SPP gas blocks +// correctly account for intermediate JUMPDEST instructions in consecutive runs. +// Consumers that need canonical PCs (e.g. the SSA analyzer) must canonicalize +// the value themselves. static void resolveJumpTargetsByAbstractStack( const zen::common::Byte *Code, size_t CodeSize, const std::vector &JumpDestMap, + const evmc_instruction_metrics *Metrics, std::unordered_map &ResolvedTargets) { if (CodeSize == 0) { return; } - // Build canonical JUMPDEST mapping: consecutive JUMPDEST runs share the - // last PC as canonical representative (same logic as evm_analyzer.h). - std::unordered_map CanonicalJumpDest; - { - size_t Pc = 0; - while (Pc < CodeSize) { - uint8_t Op = static_cast(Code[Pc]); - if (Op == static_cast(evmc_opcode::OP_JUMPDEST)) { - size_t RunStart = Pc; - size_t RunEnd = Pc; - while (RunEnd + 1 < CodeSize && - static_cast(Code[RunEnd + 1]) == - static_cast(evmc_opcode::OP_JUMPDEST)) { - ++RunEnd; - } - for (size_t P = RunStart; P <= RunEnd; ++P) { - CanonicalJumpDest[static_cast(P)] = - static_cast(RunEnd); - } - Pc = RunEnd + 1; - continue; - } - Pc += opcodeLen(Op); - } - } - - auto hasCanonicalJumpDest = [&](uint64_t Dest) -> bool { + auto isValidJumpDest = [&](uint64_t Dest) -> bool { return Dest < CodeSize && JumpDestMap[Dest] != 0; }; - auto getCanonicalJumpDestPC = [&](uint64_t Dest) -> uint32_t { - auto It = CanonicalJumpDest.find(static_cast(Dest)); - return It != CanonicalJumpDest.end() ? It->second - : static_cast(Dest); - }; - - const evmc_instruction_metrics *Metrics = - evmc_get_instruction_metrics_table(zen::evm::DEFAULT_REVISION); // Scan bytecode block by block. size_t ScanPC = 0; @@ -1066,9 +1037,9 @@ static void resolveJumpTargetsByAbstractStack( ensureAbstractDepth(Stack, EntryDepth, 1); AbstractValue Dest = Stack.back(); Stack.pop_back(); - if (Dest.KnownConst && Dest.FitsU64 && hasCanonicalJumpDest(Dest.Low)) { + if (Dest.KnownConst && Dest.FitsU64 && isValidJumpDest(Dest.Low)) { uint32_t JumpPC = static_cast(ScanPC - 1); - ResolvedTargets[JumpPC] = getCanonicalJumpDestPC(Dest.Low); + ResolvedTargets[JumpPC] = static_cast(Dest.Low); } // Skip dead code until next JUMPDEST or end. while (ScanPC < CodeSize && @@ -1084,9 +1055,9 @@ static void resolveJumpTargetsByAbstractStack( AbstractValue Dest = Stack.back(); Stack.pop_back(); Stack.pop_back(); // condition - if (Dest.KnownConst && Dest.FitsU64 && hasCanonicalJumpDest(Dest.Low)) { + if (Dest.KnownConst && Dest.FitsU64 && isValidJumpDest(Dest.Low)) { uint32_t JumpPC = static_cast(ScanPC - 1); - ResolvedTargets[JumpPC] = getCanonicalJumpDestPC(Dest.Low); + ResolvedTargets[JumpPC] = static_cast(Dest.Low); } // Fallthrough continues to the next instruction (new block). break; @@ -1366,17 +1337,17 @@ void buildBytecodeCache(EVMBytecodeCache &Cache, const common::Byte *Code, buildJumpDestMapAndPushCache(Code, CodeSize, Cache.JumpDestMap, Cache.PushValueMap); - // Shared jump target resolution: abstract stack simulation run once, - // results consumed by both SPP gas optimizer and SSA liftability analyzer. - Cache.ResolvedJumpTargets.clear(); - resolveJumpTargetsByAbstractStack(Code, CodeSize, Cache.JumpDestMap, - Cache.ResolvedJumpTargets); - const auto *MetricsTable = evmc_get_instruction_metrics_table(Rev); if (!MetricsTable) { MetricsTable = evmc_get_instruction_metrics_table(DEFAULT_REVISION); } + // Shared jump target resolution: abstract stack simulation run once, + // results consumed by both SPP gas optimizer and SSA liftability analyzer. + Cache.ResolvedJumpTargets.clear(); + resolveJumpTargetsByAbstractStack(Code, CodeSize, Cache.JumpDestMap, + MetricsTable, Cache.ResolvedJumpTargets); + buildGasChunksSPP(Code, CodeSize, MetricsTable, Cache.JumpDestMap, Cache.PushValueMap, Cache.ResolvedJumpTargets, Cache.GasChunkEnd, Cache.GasChunkCost); From dfbd3632f42274aba64ba27edacd8caa7e0d02ff Mon Sep 17 00:00:00 2001 From: ZR74 <2401889661@qq.com> Date: Mon, 27 Apr 2026 18:15:17 +0800 Subject: [PATCH 3/3] docs: add change doc for shared jump target resolution pass (#462) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../README.md | 47 +++++++++++++++++++ docs/changes/README.md | 1 + 2 files changed, 48 insertions(+) create mode 100644 docs/changes/2026-04-11-evm-shared-jump-resolution/README.md diff --git a/docs/changes/2026-04-11-evm-shared-jump-resolution/README.md b/docs/changes/2026-04-11-evm-shared-jump-resolution/README.md new file mode 100644 index 000000000..75b8bdfb2 --- /dev/null +++ b/docs/changes/2026-04-11-evm-shared-jump-resolution/README.md @@ -0,0 +1,47 @@ +# Change: extract shared jump target resolution pass into bytecode cache + +- **Status**: Implemented +- **Date**: 2026-04-11 +- **Tier**: Light +- **PR**: #462 + +## Overview + +Extract a shared abstract-stack-based jump target resolution pass into the bytecode cache (`EVMBytecodeCache`), so that both the SPP gas optimizer and the SSA liftability analyzer consume the same pre-resolved jump targets instead of each running independent (and divergent) resolution logic. + +## Motivation + +Prior to this change, jump target resolution was performed independently in two places: + +1. **SPP gas optimizer** (`buildGasChunksSPP`) — used a simple pattern-match (`resolveConstantJumpTarget`) that only recognized adjacent `PUSHn + JUMP/JUMPI` sequences. +2. **SSA liftability analyzer** (`EVMAnalyzer`) — ran its own per-block abstract stack simulation, which could track values through DUP/SWAP but operated in isolation. + +This duplication had two problems: + +- **Inconsistency**: the two passes could disagree on whether a jump was resolved, leading to the SPP optimizer falling back to per-block metering while the SSA analyzer considered the same jump constant. This mismatch could leave optimization opportunities on the table. +- **Missed resolutions**: patterns like `PUSHn ... SWAPn ... JUMP` (where the push and jump are separated by stack manipulation) were invisible to the simple pattern-matcher in SPP, even though the abstract stack simulation could resolve them. + +By running a single abstract stack simulation once during cache construction, both consumers get a strictly larger set of resolved targets from a single source of truth. + +## Impact + +### Affected Modules + +- `src/evm/evm_cache.{h,cpp}` — new `ResolvedJumpTargets` field and `resolveJumpTargetsByAbstractStack` pass +- `src/compiler/evm_frontend/evm_analyzer.h` — fallback lookup into shared map via `tryResolveFromSharedMap` +- `src/compiler/evm_frontend/evm_mir_compiler.h` — wires `setResolvedJumpTargets` on the analyzer +- `src/compiler/evm_compiler.cpp` — passes cache to MIR compiler +- `src/action/evm_bytecode_visitor.h` — passes `evmc_revision` to cache builder + +### Key Design Decisions + +- **Raw PCs in shared map**: `ResolvedJumpTargets` stores the raw (non-canonicalized) JUMPDEST PCs from the PUSH immediate value, not the canonical PC after consecutive JUMPDESTs. This is necessary because the SPP gas optimizer needs the original PC to correctly account for intermediate JUMPDEST instructions in gas block boundaries. The SSA analyzer canonicalizes in its own consumer (`tryResolveFromSharedMap` calls `getCanonicalJumpDestPC`). +- **Caller-provided revision**: the shared pass receives the caller's `evmc_revision` metrics table rather than hardcoding `DEFAULT_REVISION`, so opcodes like PUSH0 (introduced in Shanghai) get correct stack effects under their respective revisions. +- **Graceful fallback**: the SSA analyzer first attempts its own local abstract stack simulation; only when that fails does it consult the shared map. This preserves the existing resolution quality while strictly extending coverage. + +## Checklist + +- [x] Implementation complete +- [x] Tests added/updated +- [x] Module specs in `docs/modules/` updated (if affected) +- [x] Build and tests pass diff --git a/docs/changes/README.md b/docs/changes/README.md index a6af4f933..aec0b03ed 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -48,6 +48,7 @@ Typical triggers: | Date | Name | Status | Tier | Description | |------|------|--------|------|-------------| | 2026-03-10 | [evm-stack-ssa-lifting](2026-03-10-evm-stack-ssa-lifting/README.md) | Implemented | Full | True-SSA stack lifting for EVM multipass JIT | +| 2026-04-11 | [evm-shared-jump-resolution](2026-04-11-evm-shared-jump-resolution/README.md) | Implemented | Light | Extract shared jump target resolution pass into bytecode cache | | 2026-04-14 | [handlecompare-bounds-check](2026-04-14-handlecompare-bounds-check/README.md) | Implemented | Light | Add bounds check before macro-fusion read in handleCompare | Each active proposal lives in its own subdirectory. Browse `docs/changes/*/README.md`