You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 binary — run() 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:
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.
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.
Summary
fn_sig_all_i32(crates/tish_compile/src/codegen.rs:13004) requires the RETURN annotation to be: i32:So a function that returns nothing can never be promoted to a native
fn, no matter how its parameters are annotated.: voidfailsann_is_i32, and an omitted return failsis_some_and. Every such function keeps a boxedValue::nativewrapper plus aVmRef<Value>cell, and every call to it goes throughvalue_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:
#647 already carved out zero-parameter functions on exactly this basis, in the same comment:
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)fnValue::nativewrappers inrun()value_callsites inrun()Annotating all 107 with
: voidand rebuilding produced a byte-identical binary —run()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:
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 gamerun()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 thevalue_calltemporaries around each call site.Ask
fn_sig_all_i32when 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.fixedsignature (one convention, andfixedis already first-class fordeclare fnexterns — 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 thanfn <name>_native(...).crates/tish_compile/tests/regr_gba_lowered_call_sites.rsis the natural home for a test asserting the promoted form.