From 309369cc054cf1ef84d278a8567aaf9700a94670 Mon Sep 17 00:00:00 2001 From: ZR74 <2401889661@qq.com> Date: Tue, 28 Jul 2026 16:58:24 +0000 Subject: [PATCH] perf(compiler): share full dynamic dispatch in stack SSA Reuse the module-level full-table dispatch for unfiltered dynamic jumps when stack-SSA lifting is enabled. Keep proven filtered target sets source-specific and add a structural regression for dispatch growth. --- .../README.md | 45 ++++++++++++++++ docs/changes/README.md | 1 + docs/modules/compiler/spec.md | 20 +++++++ docs/modules/tests/spec.md | 9 ++++ .../evm_frontend/evm_mir_compiler.cpp | 18 +++---- src/compiler/evm_frontend/evm_mir_compiler.h | 1 - src/tests/evm_jit_frontend_tests.cpp | 52 +++++++++++++++++++ 7 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 docs/changes/2026-07-28-ssa-shared-dynamic-dispatch/README.md diff --git a/docs/changes/2026-07-28-ssa-shared-dynamic-dispatch/README.md b/docs/changes/2026-07-28-ssa-shared-dynamic-dispatch/README.md new file mode 100644 index 000000000..6f3b26e01 --- /dev/null +++ b/docs/changes/2026-07-28-ssa-shared-dynamic-dispatch/README.md @@ -0,0 +1,45 @@ +# Change: Share full dynamic dispatch in stack-SSA builds + +- **Status**: Implemented +- **Date**: 2026-07-28 +- **Tier**: Light + +## Overview + +Reuse the existing module-level full-table dynamic jump dispatch CFG when EVM +stack-SSA lifting is enabled. Sources with a proven smaller target set continue +to use a per-source filtered dispatch. + +## Motivation + +The previous stack-SSA path rebuilt the full dynamic dispatch for every source. +For contracts with many dynamic jump sites and many `JUMPDEST`s, this duplicated +MIR control flow at approximately +`O(dynamic jump sources × dynamic jump targets)`. The duplication increased +frontend time, MIR size, register-allocation work, and emitted code without +changing execution semantics. + +Runtime dynamic targets that use the full dispatch table are already forced to +the materialized runtime-stack fallback. They do not consume source-specific +stack-merge phis, so the full-table dispatch can be shared in stack-SSA and +non-SSA builds alike. + +## Impact + +- `compiler`: remove the stack-SSA exception from full-table dispatch sharing. +- `tests`: assert that many unfiltered dynamic sources produce fewer dispatch + switches than sources. +- `docs`: record the runtime-stack fallback invariant that makes sharing sound. + +Filtered target sets remain source-specific so their predecessor registration +and target filtering are unchanged. EVM semantics and determinism are +unaffected. + +## Validation + +- [x] Implementation complete +- [x] Tests added +- [x] Module specifications updated +- [x] Formatting check passes +- [x] Multipass stack-SSA build passes +- [x] `evmJitFrontendTests` passes (`109/109`) diff --git a/docs/changes/README.md b/docs/changes/README.md index e35be6891..e040f04cb 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -53,6 +53,7 @@ Typical triggers: | 2026-04-14 | [from-raw-pointer-safety-checks](2026-04-14-from-raw-pointer-safety-checks/README.md) | Accepted | Light | Add null/alignment safety checks to `from_raw_pointer` in Rust bindings | | 2026-05-13 | [evm-ngram-macro-ops](2026-05-13-evm-ngram-macro-ops/README.md) | Implemented | Full | Initial EVM n-gram macro-op lowering and specialized keccak helpers for multipass JIT | | 2026-07-21 | [evm-memory-alias-and-expansion](2026-07-21-evm-memory-alias-and-expansion/README.md) | Implemented | Full | Stronger memory alias proofs, wider precheck/expansion coverage, DSE, load forwarding, grouping, and MCOPY roadmap | +| 2026-07-28 | [ssa-shared-dynamic-dispatch](2026-07-28-ssa-shared-dynamic-dispatch/README.md) | Implemented | Light | Share unfiltered full-table dynamic dispatch in stack-SSA builds | Each active proposal lives in its own subdirectory. Browse `docs/changes/*/README.md` to see all current proposals, or use: diff --git a/docs/modules/compiler/spec.md b/docs/modules/compiler/spec.md index e065b1a33..feafdec73 100644 --- a/docs/modules/compiler/spec.md +++ b/docs/modules/compiler/spec.md @@ -179,6 +179,26 @@ and destination ends for expansion elision while retaining the original - Provide bytecode, gas chunk end/cost arrays for chunk-based metering - Use register to hold gas when `ZEN_ENABLE_EVM_GAS_REGISTER` is enabled +### EVM Stack SSA Lift Safety + +- `ZEN_ENABLE_EVM_STACK_SSA_LIFT` permits compatible EVM operand-stack values + to cross basic-block boundaries as SSA state. +- A lifted merge must have one valid incoming for every final MIR predecessor, + and all MIR phi instructions must remain contiguous at block start. +- Dynamic-jump lowering may expand one EVM predecessor PC into multiple MIR + predecessor blocks. Until the merge representation distinguishes those MIR + predecessors, a dynamic-jump target requiring an entry merge is non-lifted + and uses the deterministic runtime-stack fallback. +- An unfiltered full-table dynamic dispatch is shared at module scope in both + stack-SSA and non-SSA builds. This is sound while every runtime + dynamic-dispatch target is non-lifted and consumes the materialized runtime + stack. A source with a proven smaller candidate set retains a per-source + filtered dispatch. Re-enabling lifting for runtime dynamic targets requires + revisiting this predecessor-accounting contract before changing either + invariant. +- Unsupported merge shapes must fall back before MIR phi construction; they + must never emit an incomplete phi, fail module verification, or abort. + ### Machine Code and Module Binding - Machine code is written to `EVMModule::getJITCodeMemPool()` or the corresponding `Module` pool diff --git a/docs/modules/tests/spec.md b/docs/modules/tests/spec.md index 803150d82..b5c471034 100644 --- a/docs/modules/tests/spec.md +++ b/docs/modules/tests/spec.md @@ -72,6 +72,15 @@ This module does not include: EVM interpreter (evm), Host implementation (host), - **evmFallbackExecutionTests**: Requires `ZEN_ENABLE_LIBEVM`; verify interpreter fallback on JIT exception - **Dependency**: dtvmapi library +### 9. EVM JIT Frontend Structural Tests + +- **evmJitFrontendTests**: Validate analyzer facts, stack-SSA boundaries, + memory-plan consumers, MIR verification, and control-flow lowering. +- Full-table runtime dynamic dispatch must remain shared across multiple + unfiltered dynamic sources. The structural regression counts MIR switch + statements so stack-SSA builds cannot silently return to per-source + `O(dynamic sources × JUMPDESTs)` expansion. + ## External Contracts | Dependent Module | Contract | diff --git a/src/compiler/evm_frontend/evm_mir_compiler.cpp b/src/compiler/evm_frontend/evm_mir_compiler.cpp index 9ff0e22f6..82cb5705f 100644 --- a/src/compiler/evm_frontend/evm_mir_compiler.cpp +++ b/src/compiler/evm_frontend/evm_mir_compiler.cpp @@ -316,14 +316,6 @@ void EVMMirBuilder::loadEVMInstanceAttr() { ExceptionReturnBB = CurFunc->createExceptionReturnBB(); } -bool EVMMirBuilder::shouldUseSharedDynamicDispatch() const { -#ifdef ZEN_ENABLE_EVM_STACK_SSA_LIFT - return false; -#else - return true; -#endif -} - MBasicBlock *EVMMirBuilder::getOrCreateIndirectJumpBB( uint64_t SourceBlockPC, const JumpTargetPCList *CandidateTargets) { auto ExistingIt = IndirectJumpBBs.find(SourceBlockPC); @@ -1576,12 +1568,16 @@ void EVMMirBuilder::implementIndirectJump( const bool UseFilteredPerSource = CandidateTargets != nullptr && !CandidateTargets->empty() && CandidateTargets->size() < JumpDestTable.size(); + // Full-table runtime-dispatch targets are forced onto the materialized + // runtime-stack path by EVMAnalyzer::finalizeLiftability(). They therefore + // have no source-aware stack-merge phi consumer and can share one dispatch + // CFG in both stack-SSA and non-SSA builds. A proven smaller candidate set + // remains per-source so its filtered dispatch and predecessor registration + // stay source-specific. MBasicBlock *TargetBB = UseFilteredPerSource ? getOrCreateIndirectJumpBB(CurrentBlockPC, CandidateTargets) - : (shouldUseSharedDynamicDispatch() - ? getOrCreateSharedIndirectJumpBB() - : getOrCreateIndirectJumpBB(CurrentBlockPC)); + : getOrCreateSharedIndirectJumpBB(); createInstruction(true, &(Ctx.VoidType), JumpTarget, JumpTargetVar->getVarIdx()); createInstruction(true, Ctx, TargetBB); diff --git a/src/compiler/evm_frontend/evm_mir_compiler.h b/src/compiler/evm_frontend/evm_mir_compiler.h index b2c045f39..7ac721117 100644 --- a/src/compiler/evm_frontend/evm_mir_compiler.h +++ b/src/compiler/evm_frontend/evm_mir_compiler.h @@ -1367,7 +1367,6 @@ class EVMMirBuilder final { MBasicBlock *buildIndirectJumpBB(uint64_t SourceBlockPC, const JumpTargetPCList *CandidateTargets, bool RegisterDynamicPhi); - bool shouldUseSharedDynamicDispatch() const; void linkJumpDestEntryThunkIfNeeded(MBasicBlock *TargetBB); void registerPhiIncomingBlock(uint64_t TargetBlockPC, uint64_t PredBlockPC, MBasicBlock *PredBB); diff --git a/src/tests/evm_jit_frontend_tests.cpp b/src/tests/evm_jit_frontend_tests.cpp index f046c7ad6..dbcefe0ee 100644 --- a/src/tests/evm_jit_frontend_tests.cpp +++ b/src/tests/evm_jit_frontend_tests.cpp @@ -83,6 +83,43 @@ bool verifyMirForBytecode(const std::vector &Bytecode, return Ok; } +struct MirControlFlowStats { + bool Compiled = false; + uint32_t BasicBlocks = 0; + uint32_t Switches = 0; +}; + +MirControlFlowStats +compileMirControlFlowStats(const std::vector &Bytecode) { + COMPILER::EVMFrontendContext Ctx; + Ctx.setRevision(EVMC_CANCUN); + Ctx.setGasMeteringEnabled(false); + 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); + MirControlFlowStats Stats; + Stats.Compiled = Builder.compile(&Ctx); + Stats.BasicBlocks = Func.getNumBasicBlocks(); + for (COMPILER::MBasicBlock *BB : Func) { + for (COMPILER::MInstruction *Inst : *BB) { + if (Inst->getKind() == COMPILER::MInstruction::SWITCH) { + ++Stats.Switches; + } + } + } + return Stats; +} + const EVMAnalyzer::BlockInfo *findBlock(const EVMAnalyzer &Analyzer, uint64_t EntryPC) { const auto &Blocks = Analyzer.getBlockInfos(); @@ -93,6 +130,21 @@ const EVMAnalyzer::BlockInfo *findBlock(const EVMAnalyzer &Analyzer, return &It->second; } +TEST(EVMMirBuilderControlFlowTest, FullDynamicDispatchIsSharedAcrossSources) { + constexpr uint32_t DynamicSources = 16; + std::vector Bytecode; + for (uint32_t Index = 0; Index < DynamicSources; ++Index) { + Bytecode.push_back(OP_JUMPDEST); + Bytecode.push_back(OP_PUSH0); + Bytecode.push_back(OP_CALLDATALOAD); + Bytecode.push_back(OP_JUMP); + } + + const MirControlFlowStats Stats = compileMirControlFlowStats(Bytecode); + ASSERT_TRUE(Stats.Compiled); + EXPECT_LT(Stats.Switches, DynamicSources); +} + class MirBuilderConstFoldHarness { public: MirBuilderConstFoldHarness() : Func(Ctx, 0), Builder(Ctx, Func) {