Summary
A contract whose recv arm (or any internal function) ends up with more than 16
parameters fails to compile with a Sonatina backend error, regardless of
optimization level. The threshold is exactly 16: 16 parameters compile, 17 do not.
This surfaces in practice with wide ABI functions — e.g. a sum(uint256, …) that
takes many arguments — but it is a general limit on the lowered internal
calling convention, not specific to any one shape.
Minimal reproduction
msg M {
#[selector = sol("sum(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)")]
Sum {
a00: u256, a01: u256, a02: u256, a03: u256, a04: u256, a05: u256,
a06: u256, a07: u256, a08: u256, a09: u256, a10: u256, a11: u256,
a12: u256, a13: u256, a14: u256, a15: u256, a16: u256,
} -> u256,
}
pub contract Min17 {
recv M {
Sum { a00, a01, a02, a03, a04, a05, a06, a07, a08, a09, a10, a11, a12, a13, a14, a15, a16 } -> u256 {
a00 + a01 + a02 + a03 + a04 + a05 + a06 + a07 + a08 + a09
+ a10 + a11 + a12 + a13 + a14 + a15 + a16
}
}
}
fe build min17.fe --standalone
The same source with 16 fields compiles cleanly.
Actual error
Error: Failed to compile Sonatina bytecode: internal error:
BackendError { ..., message: "EVM backend supports at most 16 call operands, but inst1748 has 17 to %__Min17_recv_0_0" }
BackendError { ..., message: "EVM backend supports at most 16 internal arguments, but callee %__Min17_recv_0_0 has 17" }
BackendError { ..., message: "EVM backend supports at most 16 internal arguments, but function %__Min17_recv_0_0 has 17" }
Expected
The contract should compile. Decoding 17 ABI words from calldata is not the problem;
the limit is on how the decoded values are then passed to the lowered handler.
Root cause
Fe lowers each recv arm into a separate internal Sonatina function
(__Min17_recv_0_0) whose parameters are the destructured message fields. The EVM
ISA verifier in Sonatina then rejects any internal function signature (or call
instruction) with more than 16 arguments/operands:
sonatina/crates/codegen/src/isa/evm/verify.rs → collect_unsupported_evm_calls:
if arg_count > 16 {
push_error("EVM backend supports at most 16 internal arguments, ...")
}
...
if call.args().len() > 16 {
push_error("EVM backend supports at most 16 call operands, ...")
}
This is a hard gate on the inter-function calling convention, which passes
arguments on the EVM stack (only 16 slots are reachable via DUP1..16/SWAP1..16).
Why optimization levels and the existing spiller don't help
- The failure is identical at
-O 0, -O 1, -O 2, and -O s — it is not an
optimization decision.
- Sonatina does spill to memory (
crates/codegen/src/stackalloc/stackify/spill.rs),
but that spiller operates on SSA values inside a function body during stack
scheduling. The 16-argument limit is enforced by a verification pass that runs
before stackification, and it concerns the calling convention between
functions. Excess call arguments/returns are not spilled through memory, so the
module is rejected before the spiller ever runs.
Impact
Any function or recv arm that lowers to more than 16 internal parameters (or
returns) cannot be compiled on the current EVM backend. This is reachable from
ordinary high-level code (wide ABI functions, handlers that destructure many
message fields), not just contrived cases. By comparison, Vyper compiles such
functions; Solidity's legacy pipeline also fails (stack-too-deep) but its via-IR
pipeline handles it by spilling to memory.
Possible directions
- Sonatina: extend the EVM internal calling convention to pass arguments /
return values beyond 16 through a memory region (spill at the call boundary),
lifting the limit entirely.
- Fe lowering: avoid producing internal functions with >16 scalar
parameters — e.g. inline the handler, or pass the decoded message as a single
memory pointer/aggregate rather than destructuring into N scalar arguments
before the internal call.
Environment
fe 26.2.0 (1fffb9e)
- sonatina-ir/codegen git rev
039a9f5
- backend:
sonatina (default), target EVM prague
- reproduces at every
--optimize level
Summary
A contract whose
recvarm (or any internal function) ends up with more than 16parameters fails to compile with a Sonatina backend error, regardless of
optimization level. The threshold is exactly 16: 16 parameters compile, 17 do not.
This surfaces in practice with wide ABI functions — e.g. a
sum(uint256, …)thattakes many arguments — but it is a general limit on the lowered internal
calling convention, not specific to any one shape.
Minimal reproduction
The same source with 16 fields compiles cleanly.
Actual error
Expected
The contract should compile. Decoding 17 ABI words from calldata is not the problem;
the limit is on how the decoded values are then passed to the lowered handler.
Root cause
Fe lowers each
recvarm into a separate internal Sonatina function(
__Min17_recv_0_0) whose parameters are the destructured message fields. The EVMISA verifier in Sonatina then rejects any internal function signature (or call
instruction) with more than 16 arguments/operands:
sonatina/crates/codegen/src/isa/evm/verify.rs→collect_unsupported_evm_calls:This is a hard gate on the inter-function calling convention, which passes
arguments on the EVM stack (only 16 slots are reachable via
DUP1..16/SWAP1..16).Why optimization levels and the existing spiller don't help
-O 0,-O 1,-O 2, and-O s— it is not anoptimization decision.
crates/codegen/src/stackalloc/stackify/spill.rs),but that spiller operates on SSA values inside a function body during stack
scheduling. The 16-argument limit is enforced by a verification pass that runs
before stackification, and it concerns the calling convention between
functions. Excess call arguments/returns are not spilled through memory, so the
module is rejected before the spiller ever runs.
Impact
Any function or
recvarm that lowers to more than 16 internal parameters (orreturns) cannot be compiled on the current EVM backend. This is reachable from
ordinary high-level code (wide ABI functions, handlers that destructure many
message fields), not just contrived cases. By comparison, Vyper compiles such
functions; Solidity's legacy pipeline also fails (stack-too-deep) but its via-IR
pipeline handles it by spilling to memory.
Possible directions
return values beyond 16 through a memory region (spill at the call boundary),
lifting the limit entirely.
parameters — e.g. inline the handler, or pass the decoded message as a single
memory pointer/aggregate rather than destructuring into N scalar arguments
before the internal call.
Environment
fe 26.2.0 (1fffb9e)039a9f5sonatina(default), target EVMprague--optimizelevel