Conversation
With `--evm-version @future` (experimental), the optimized IR code transform compiles internal function calls to the call and return instructions of EIP-7979 instead of synthesizing them from jumps: - a call is `PUSH <function tag>; CALLSUB`, with no return label pushed; - a function entry is a CALLDEST label, with no return label on the stack; - a function return is RETURNSUB. Functions that cannot return keep the plain JUMP into their entry, which EIP-7979 permits (a JUMP may land on a CALLDEST). libevmasm: the three instructions with their gas tiers; RETURNSUB terminates control flow and CALLSUB alters it; a Tag item may be marked as a subroutine entry, in which case it assembles as CALLDEST, prints as `tag_N: (calldest)` and round-trips through assembly JSON as `calldest`; CALLDEST costs what JUMPDEST costs. The block deduplicator never merges a CALLDEST block with a JUMPDEST block, so a CALLSUB is never redirected onto a JUMPDEST, and, when subroutines are enabled, it marks every block it merges as a subroutine entry, since shared code reached from several subroutines must be entered as a subroutine (EIP-8337). liblangutil: `EVMVersion::hasSubroutines()`, true for `@future`. libyul: `appendSubroutineLabel`, `appendCallSubTo` and `appendReturnSub` on AbstractAssembly, implemented for the evmasm adapter and the no-output assembly; the CFG builder and stack layout generator stop introducing return-label slots when subroutines are available. Out of scope, deliberately: the legacy code generator, the unoptimized Yul code transform, teaching the evmasm inliner to inline CALLSUB calls, documentation and tests.
… interpreter. CALLSUB, CALLDEST and RETURNSUB are low-level control flow like JUMP, JUMPI and JUMPDEST: the code transform emits them, and they must not be exposed as Yul builtins under `@future`. The Yul test interpreter's exhaustive switch treats them as the other low-level control flow.
This was referenced Sep 12, 2026
Compiles the demo contract with --experimental --evm-version @future --via-ir --optimize --asm and snapshots the assembly: subroutine entries as calldest, calls as callsub, returns as returnsub, no plain jump. Compile-only, since the test EVM does not implement the instructions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Experimental EIP-7979 subroutines for internal calls in the IR pipeline
Draft. A demonstration for the EIP-7979 discussion, not a merge candidate.
EIP-7979 (proposed for Hegotá) gives the EVM a return stack and three instructions:
CALLSUB,CALLDEST,RETURNSUB. This PR makes the optimized IR code transform use them for internal function calls when--evm-version @futureis selected, so that the question "what does it cost a compiler to target this?" has a concrete answer: 159 lines, 21 files.What changes in the generated code
@futurePUSH ret; <args>; PUSH f; JUMP/ret: JUMPDEST<args>; PUSH f; CALLSUBf: JUMPDESTwith the return label on the stackf: CALLDEST, arguments onlyJUMPRETURNSUBPUSH f; JUMPJUMPmay land on aCALLDESTThe return-label slot disappears from every function's stack layout, which also relieves stack-too-deep pressure by one slot per active call.
Results on a small contract
Calls.solin the description below has a plain internal call, a nested one, and a recursive one. Runtime code under@future: 3 functions asCALLDEST, 7CALLSUB, 4RETURNSUB, and no plainJUMPat all; the only remaining control flow isJUMPI.Executed on the EIP-7979 reference implementation in execution-specs (ethereum/execution-specs#3575) with three inputs, the results match the legacy build and the gas per external call is lower:
compute(a, b)@futureThe runtime is 31 bytes larger (317 vs 286): the evmasm inliner recognises
JUMP-based calls but notCALLSUB, so fewer small helpers are inlined. That is a follow-up in the inliner, not a cost of the instructions.The output also validates under EIP-8337 (fully static control flow, no underflow), checked with its reference validator (ethereum/execution-specs#3576). Getting there surfaced one general rule for optimizers targeting these EIPs, implemented here: code shared across subroutines must be entered as a subroutine. The block deduplicator had merged the identical overflow-panic tails of
checked_addandchecked_mulinto oneJUMPDESTblock reached from two subroutines; with subroutines enabled it now marks every block it merges as aCALLDEST.Changes
RETURNSUBterminates control flow,CALLSUBalters it (so the peephole optimizer and CSE treat them correctly); aTagitem can be marked as a subroutine entry — assembled asCALLDEST, printed astag_N: (calldest), exported and imported ascalldestin assembly JSON;CALLDESTcosts whatJUMPDESTcosts; the block deduplicator never merges aCALLDESTblock with aJUMPDESTblock (aCALLSUBmust never be redirected onto aJUMPDEST) and marks merged blocks as entries.EVMVersion::hasSubroutines(), true for@futureonly; the opcode gate knows the three instructions.appendSubroutineLabel,appendCallSubTo,appendReturnSubonAbstractAssembly, implemented forEthAssemblyAdapterandNoOutputAssembly;ControlFlowGraphBuilderstops pushing a return-label slot for calls;StackLayoutGeneratorstops expecting one at returns;OptimizedEVMCodeTransformemits the new instructions.Deliberately out of scope
Legacy (non-IR) code generation; the unoptimized Yul code transform; teaching the evmasm inliner about
CALLSUB; documentation; tests. The change is gated behind an experimental EVM version and does not affect any other version's output.The demo contract
CI status
The semantic-test jobs for @future fail because they execute compiled code on evmone, which does not implement EIP-7979: every test whose IR-pipeline code makes an internal call halts at its first CALLSUB ("the test passed without Yul"). The same tests pass when the bytecode is run on the EIP-7979 reference implementation in execution-specs (checked for selfdestruct_post_cancun.sol, the full test_create2_and_terminate scenario). All other jobs pass. An evmone implementation is at ipsilon/evmone#1706; on it, the same output runs correctly at 10–12% less execution gas.