Skip to content

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

Description

@cburgdorf

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

  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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions