Skip to content

[native/gba] A void-returning fn can never be promoted: fn_sig_all_i32 demands : i32 in the return, so 107 of 256 fns in a real game stay boxed #685

Description

@spacedevin

Summary

fn_sig_all_i32 (crates/tish_compile/src/codegen.rs:13004) requires the RETURN annotation to be : i32:

fn fn_sig_all_i32(params: &[FunParam], return_type: &Option<TypeAnnotation>) -> bool {
    return_type.as_ref().is_some_and(Self::ann_is_i32)
        && params.iter().all(|p| /* … ann_is_i32 … */)
}

So a function that returns nothing can never be promoted to a native fn, no matter how its parameters are annotated. : void fails ann_is_i32, and an omitted return fails is_some_and. Every such function keeps a boxed Value::native wrapper plus a VmRef<Value> cell, and every call to it goes through value_call.

The rule's own reasoning already excludes this case

The doc comment explains the all-or-nothing rule as being about coercion consistency across a signature:

An : i32 parameter reached through the boxed path is ToInt32-coerced on entry; promoted to a native f64 it would not be, so 3.7 would arrive as 3 one way and 3.7 the other. […] A function whose whole signature is i32 has one convention.

#647 already carved out zero-parameter functions on exactly this basis, in the same comment:

ZERO PARAMETERS QUALIFY. The rule above is about keeping ONE coercion convention across a signature — and a signature with no parameters has nothing to be inconsistent about, so the reason the rule exists cannot apply to it.

A void return is the same argument in the return position: there is no returned value, so there is nothing to coerce and no convention to be inconsistent about. The body proof still runs, so a void fn touching something unlowerable is still rejected on its body — this only stops rejecting it on a return type that carries no value.

Measured on a real game (examples/example-zelda, tish-gba)

functions in game source 256
promoted to native fn 13 (5%)
promoted natives returning unit 0
fns with fully-typed params that return no value 107
boxed Value::native wrappers in run() 478
value_call sites in run() 841

Annotating all 107 with : void and rebuilding produced a byte-identical binaryrun() 27,236 B before and after, same callee frames. There is currently no way to express "this is promotable and returns nothing".

Concrete example — zero parameters, returns nothing, still boxed:

export function castClear(): void {  }      // game source
let castClear_cell: VmRef<Value> = VmRef::new(Value::Null);   // generated
let castClear = {Value::native(move |args| …)};

Why it matters beyond call cost

On GBA every one of those wrappers is a permanent slot in run()'s single stack frame. In this game run() reserves 27,236 B of a 32,512 B stack; its deepest path is 232 B past the stack-guard floor, so the ROM boots but cannot complete its own test suite. Promoting the void-returning half of the program removes wrappers, cells, and the value_call temporaries around each call site.

Ask

  1. Admit a void / absent return in fn_sig_all_i32 when every parameter qualifies — the [native/gba] Typed lowering is all-or-nothing: touching ANY module state forces a boxed value_call — 1 of hundreds of functions qualified in a real ROM #647 argument, applied to the return position.
  2. More generally, the gate reads as "ints only". Worth considering in the same area, as separate follow-ups: an all-fixed signature (one convention, and fixed is already first-class for declare fn externs — e.g. declare fn set_flicker_caster(e: i32, hide: i32, vis: i32, shotSpeed: fixed)), and mixed signatures where each parameter keeps its own coercion at the boundary.

Repro

Any GBA build with a void-returning, fully-parameter-annotated fn: it emits let <name>_cell: VmRef<Value> + Value::native(move |args| …) rather than fn <name>_native(...). crates/tish_compile/tests/regr_gba_lowered_call_sites.rs is the natural home for a test asserting the promoted form.

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