argotorg / argotorg/fe

Sonatina EVM backend rejects functions / recv arms with more than 16 parameters

Closed
#1,500 2 comments 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.