ABI Lowering Specification (v0.1)
This document defines how ABI‑level concepts are lowered into canonical EVM‑IR, independent of any high‑level language (Ora, Vyper‑like frontends, custom DSLs).
ABI lowering is the process of handling:
- function dispatch
- argument decoding
- return encoding
- static call‑frame layout
- fallback / receive behavior
- error bubbling
- ABI‑compliant revert payloads
This is the “boundary layer” between contract execution and the EVM call interface.
Goals
ABI lowering must:
- produce deterministic and portable IR
- avoid Solidity‑specific semantics
- handle multi‑entry and single‑entry languages
- support tuples, structs, and arrays before lowering
- produce IR in canonical form (ready for legalizer + stackifier)
- interoperate with standard Ethereum tooling
It must not depend on any specific frontend.
ABI Model Overview
EVM‑IR ABI lowering assumes:
- Every contract has an entrypoint labeled
^entry. - Calldata begins at offset
0. - If the contract uses selector‑based dispatch:
- the first 4 bytes define the selector
- arguments follow according to ABI layout
- If selectorless (e.g., Vyper, single‑function languages), calldata is decoded directly
- Fallback / Receive logic are optional but supported
- Return values must be encoded into memory, then returned
Contract Entrypoint
Every contract begins execution at ^entry.
ABI lowering inserts:
^entry:
%size = evm.calldatasize
<selector detection and dispatch>
The lowering differs depending on:
- selector‑based ABI (Solidity‑style)
- single‑entry ABI (Vyper‑style)
- custom ABI (user‑defined)
We describe the generalized ABI, suitable for all.
Function Selector Dispatch
Extract selector
To extract the 4-byte function selector from calldata:
The frontend must provide a calldata base pointer at offset 0. This is typically done by:
- Frontend creating a calldata pointer constant at offset 0, or
- Using a special operation to obtain the calldata base pointer
Example (assuming frontend provides calldata base):
%calldata_base = <calldata pointer at offset 0> : ptr<2>
%sig_raw = evm.calldataload %calldata_base : u256 ; loads 32 bytes from offset 0
%shift = evm.constant 224 : u256
%sig = evm.shr %shift, %sig_raw : u256 ; extract top 4 bytes (selector)
Note: The exact mechanism for creating calldata pointers is frontend-specific. The key requirement is that %calldata_base is a ptr<2> pointing to offset 0 in calldata.
Match selector
Lowering emits:
evm.switch %sig, default ^fallback
case 0x12345678 → ^fn0
case 0x87654321 → ^fn1
...
The legalizer rewrites this into normalized conditional branches.
Calldata Decoding
ABI lowering takes each function argument (already laid out by the frontend) and emits:
Static argument load
%offset_const = evm.constant <offset> : u256
%p = evm.ptr_add %calldata_base, %offset_const : ptr<2>
%arg = evm.calldataload %p : u256
Where %calldata_base is a ptr<2> (calldata pointer) at offset 0, and <offset> is the byte offset of the argument in calldata (typically 4 + N*32 for the Nth argument after the selector).
Dynamic arrays and bytes
ABI lowering expands:
offset → load offset → load length → copy bytes
Everything must eventually be lowered into:
evm.calldataloadevm.calldatacopy- memory operations
Structs / Tuples
Structs must be:
- analyzed by the frontend
- flattened into memory layout
- expanded into individual loads
ABI lowering does not handle composite types; it only consumes flattened values.
Preparing a Function Frame
Each function lowers into a block:
^fn0:
<argument decoding>
<body>
ABI lowering:
- allocates memory slots for arguments (
evm.alloca) - stores decoded args into memory
- loads them into SSA only when needed
Example:
%slot0 = evm.alloca
evm.mstore %slot0, %arg0
Return Encoding
Return values are written to a contiguous memory region:
%retbuf = evm.alloca : ptr<0>
evm.mstore %retbuf, %value : void
%size = evm.constant <size_in_bytes> : u256
evm.return %retbuf, %size : void
For multi-value returns, values are laid out sequentially in memory according to ABI encoding rules.
ABI flattening ensures:
- multi‑value returns are laid out tightly
- arrays/bytes include length prefix
- user types are fully pre‑lowered
Revert Payloads
ABI‑compatible revert payload:
0x08c379a0 ++ length ++ message
ABI lowering emits:
%ptr = evm.alloca
evm.mstore %ptr, <error signature>
evm.mstore %ptr+4, <string length>
evm.mstore %ptr+36, <string bytes>
evm.revert %ptr, %len
Fallback and Receive
We define language‑neutral semantics:
Fallback Block
A fallback handler is any block that executes when:
- selector is zero OR
- selector doesn’t match any known entry
ABI lowering emits:
default → ^fallback
Receive Block
Executed if calldata is empty (calldatasize = 0) and callvalue > 0.
Example:
%sz = evm.calldatasize
%val = evm.callvalue
%is_empty = evm.eq %sz, 0
%has_value = evm.gt %val, 0
%cond = evm.and %is_empty, %has_value
evm.condbr %cond, ^receive, ^fallback
These are optional for languages that do not define them.
Error Bubbling (Staticcall / Call Return)
When lowering:
%success, %retptr = evm.call ...
ABI lowering ensures error propagation:
evm.condbr %success, ^ok, ^bubble
^bubble:
; revert with returned payload
; %retptr is the memory pointer where return data was written
; %retlen is the length of return data (from evm.call return)
evm.revert %retptr, %retlen : void
Summary
ABI lowering in EVM‑IR v0.1 provides:
- dispatch logic
- selector parsing
- argument extraction
- return encoding
- revert formatting
- fallback/receive logic
- call bubbling
It acts as the contract boundary layer, ensuring all entrypoints produce canonical EVM‑IR suitable for legalization and stackification.