EVM‑IR Stackifier Specification (v0.1)
The Stackifier lowers canonical EVM‑IR (SSA form) into a linear stack‑machine representation suitable for direct EVM bytecode emission.
It is the most critical backend phase: it removes SSA, eliminates registers, assigns memory frame locations, schedules stack operations, and linearizes control flow.
This document defines the complete stackifier pipeline, algorithms, constraints, and required invariants.
Purpose of the Stackifier
The stackifier transforms canonical EVM‑IR into a form in which:
- All SSA values become stack values, memory slots, or immediate constants
- Control flow is linearized into blocks with explicit jump destinations
- All IR operations are converted into EVM stack instructions (
PUSH,DUP,SWAP, arithmetic ops, etc.) - All local variables are assigned offsets in a single stack frame
- The resulting representation is suitable for direct bytecode generation
Stack IR Model
After stackification, each basic block is lowered to a sequence:
<JUMPDEST label>
<STACK OPS>
<EVM INSTRUCTIONS>
<JUMP or RETURN or REVERT>
This representation is not SSA.
Stack IR Value Kinds
A value in Stack IR can be:
- StackSlot(N) — lives at depth N in the EVM stack
- MemorySlot(offset) — spilled to memory
- Immediate(value) — generated via PUSH
- Void — operations like store or branch
Pipeline Overview
The stackifier runs in 6 stages:
- Block Linearization
- Lifetime Analysis
- Frame Layout Allocation
- SSA to Stack Value Assignment
- Instruction Scheduling & Stack Shaping
- Terminator Lowering
Each stage is detailed below.
Stage 1 — Block Linearization
Canonical IR contains branches between basic blocks; the stackifier:
Assigns numeric labels
Every block receives a deterministic integer label:
^entry → L0
^loop → L1
^exit → L2
Constructs a linear order
A depth‑first ordering is used unless critical for performance.
Jump destinations correspond 1‑to‑1 with labels.
Stage 2 — Lifetime Analysis
The goal is to classify SSA values as:
- Stack‑resident
- Memory‑resident
- Immediate
Stack‑resident rules
A value may remain on the stack if:
- It is used in LIFO order
- It does not escape to another block
- It does not have conflicting lifetime with later values
Memory‑resident rules
Values must be spilled to memory if:
- They are used across blocks
- They are live at merge points
- They appear in loops with backward edges
- They interfere with other stack values in a way that cannot be resolved by DUP/SWAP efficiently
Immediate rules
Constants become PUSH instructions.
Stage 3 — Frame Layout Allocation
The stack frame is a region of linear memory where the stackifier emits evm.alloca slots.
Algorithm:
- Walk IR and collect all memory allocations
- Assign each a static offset
- Merge non‑overlapping lifetimes to reuse space (optional optimization)
- Emit base pointer (usually 0)
- Translate:
into:
evm.alloca : ptr<0>MemorySlot(offset)
The frame grows monotonically.
Stage 4 — SSA Value Assignment
Each SSA value is assigned one of:
- Kept on the stack
- Spilled to memory
- Reconstructed from memory when needed
Stack slots
The top of the stack (TOS) corresponds to the most recently produced value.
Example:
%x = evm.add %a, %b
Becomes:
<load a> // push
<load b> // push
ADD // pops 2, pushes 1
Memory spills
If %x is spilled:
<compute x>
<store x to memory slot S>
Later loads reintroduce it:
LOAD S
Stage 5 — Instruction Scheduling & Stack Shaping
This is the core of stackification.
Stack Discipline
The stackifier must insert:
DUPnto duplicate stack values required laterSWAPnto reorder stack for correct operand positionsPOPto discard unused resultsPUSHfor immediatesMLOAD/MSTOREfor memory traffic
Operand Ordering
Given an op:
%r = evm.sub %a, %b
Stack order must be:
push a
push b
SUB
The stackifier computes if:
aandbare on the stackDUPorSWAPcan expose them- Memory loads are needed
Stack Shaping Examples
Example: simple arithmetic
SSA:
%t1 = evm.add %x, %y
%t2 = evm.mul %t1, %z
Stack IR:
PUSH x
PUSH y
ADD
PUSH z
MUL
Redundant DUPs avoided by stack checker.
Example: spilled value
<t1 computed>
STORE [slot0]
...
LOAD [slot0]
Stage 6 — Terminator Lowering
Branches
evm.br ^L1
→
JUMP to L1
Conditional branches
evm.condbr %cond, ^L1, ^L2
Lowered:
<compute cond>
PUSH L1
JUMPI
PUSH L2
JUMP
Returns
evm.return %value
→
<value on stack>
RETURN
Reverts
evm.revert %ptr, %len
→
<ptr>
<len>
REVERT
Unreachable
evm.unreachable
→
INVALID
Stackifier Invariants
The resulting STACK‑IR must satisfy:
- Stack height never negative
- Stack height remains ≤ 1024 per EVM rules
- No uninitialized stack values
- No dead stack state before terminators
- All computed values eventually consumed
- All JUMP destinations correspond to valid labels
- All memory offsets constant-folded
Examples
Example 1 — Basic function
EVM‑IR:
%x = evm.add %a, %b
evm.return %x
Stack IR:
PUSH a
PUSH b
ADD
RETURN
Example 2 — If/Else
EVM‑IR:
%c = evm.eq %x, %y
evm.condbr %c, ^then, ^else
^then:
evm.return %a
^else:
evm.return %b
Stack IR:
; entry
PUSH x
PUSH y
EQ
PUSH L_then
JUMPI
PUSH L_else
JUMP
L_then:
PUSH a
RETURN
L_else:
PUSH b
RETURN
Summary
The stackifier:
- Removes SSA
- Assigns all values to stack or memory
- Eliminates PHIs (legalizer already removes)
- Schedules stack operations
- Linearizes control flow
- Produces a ready‑to‑encode stack program
This is the version‑complete, canonical specification for the Stackifier in EVM‑IR v0.1.