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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/changes/2026-04-11-evm-shared-jump-resolution/README.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions docs/changes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
3 changes: 3 additions & 0 deletions src/action/evm_bytecode_visitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ template <typename IRBuilder> class EVMByteCodeVisitor {
reinterpret_cast<const uint8_t *>(Ctx->getBytecode());
size_t BytecodeSize = Ctx->getBytecodeSize();
EVMAnalyzer Analyzer(Ctx->getRevision());
if (Ctx->getResolvedJumpTargets()) {
Analyzer.setResolvedJumpTargets(Ctx->getResolvedJumpTargets());
}
Analyzer.analyze(Bytecode, BytecodeSize);
initializeLiftedBlocks(Analyzer);

Expand Down
1 change: 1 addition & 0 deletions src/compiler/evm_compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
66 changes: 63 additions & 3 deletions src/compiler/evm_frontend/evm_analyzer.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <cstdint>
#include <map>
#include <queue>
#include <unordered_map>
#include <utility>
#include <vector>

Expand Down Expand Up @@ -391,6 +392,11 @@ class EVMAnalyzer {
return It == JumpDestCanonicalPCs.end() ? PC : It->second;
}

void setResolvedJumpTargets(
const std::unordered_map<uint32_t, uint32_t> *Targets) {
SharedResolvedJumpTargets = Targets;
}

bool hasUnknownDynamicJumpTargets() const { return HasUnknownDynamicJump; }

bool analyzeSuitabilityOnly(const uint8_t *Bytecode, size_t BytecodeSize) {
Expand All @@ -414,6 +420,48 @@ 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) {
return false;
}
auto It = SharedResolvedJumpTargets->find(static_cast<uint32_t>(JumpPC));
if (It == SharedResolvedJumpTargets->end()) {
return false;
}
uint64_t RawPC = static_cast<uint64_t>(It->second);
uint64_t TargetPC = getCanonicalJumpDestPC(RawPC);
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<uint32_t>(JumpPC));
if (It == SharedResolvedJumpTargets->end()) {
return false;
}
uint64_t RawPC = static_cast<uint64_t>(It->second);
uint64_t TargetPC = getCanonicalJumpDestPC(RawPC);
Info.HasConstantJump = true;
Info.ConstantJumpTargetPC = TargetPC;
if (TargetPC != FallthroughEntryPC) {
Info.Successors.push_back(TargetPC);
}
return true;
}

void resetAnalysisState() {
BlockInfos.clear();
DynamicJumpRegions.clear();
Expand Down Expand Up @@ -665,6 +713,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;
Expand Down Expand Up @@ -702,10 +752,18 @@ 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;
}
}
// 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;
Expand Down Expand Up @@ -1332,6 +1390,8 @@ class EVMAnalyzer {
bool HasUnknownDynamicJump = false;
evmc_revision Revision = zen::evm::DEFAULT_REVISION;
JITSuitabilityResult JITResult;
const std::unordered_map<uint32_t, uint32_t> *SharedResolvedJumpTargets =
nullptr;
};

} // namespace COMPILER
Expand Down
10 changes: 10 additions & 0 deletions src/compiler/evm_frontend/evm_mir_compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "evm/evm.h"
#include "evmc/instructions.h"
#include "intx/intx.hpp"
#include <unordered_map>
#include <vector>

// Forward declaration to avoid circular dependency
Expand Down Expand Up @@ -78,6 +79,14 @@ class EVMFrontendContext final : public CompileContext {
return GasChunkEnd && GasChunkCost && GasChunkSize > 0;
}

void setResolvedJumpTargets(
const std::unordered_map<uint32_t, uint32_t> *Targets) {
ResolvedJumpTargets = Targets;
}
const std::unordered_map<uint32_t, uint32_t> *getResolvedJumpTargets() const {
return ResolvedJumpTargets;
}

void setRevision(evmc_revision Rev) { Revision = Rev; }
evmc_revision getRevision() const { return Revision; }

Expand All @@ -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<uint32_t, uint32_t> *ResolvedJumpTargets = nullptr;
evmc_revision Revision = zen::evm::DEFAULT_REVISION;
#ifdef ZEN_ENABLE_EVM_GAS_REGISTER
bool GasRegisterEnabled = false;
Expand Down
Loading
Loading