Sonatina EVM backend rejects functions / recv arms with more than 16 parameters
- Dominant language
- Rust
- Stars
- 1.7k
- Forks
- 218
- Avg merge
- 1d 4h
- Merged PRs (30d)
- 8
Description
## 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
```fe
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`:
```rust
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
1. **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.
2. **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
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the minimal `fe build min17.fe --standalone` reproduction, then read `sonatina/crates/codegen/src/isa/evm/verify.rs`, especially `collect_unsupported_evm_calls`. Compare the calling-convention boundary with `crates/codegen/src/stackalloc/stackify/spill.rs` and determine how the two possible directions affect Fe lowering or Sonatina. Done means the 17-parameter contract compiles on the EVM backend without the current argument-limit errors.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- blockchain, compilers
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100