Follow-up to #663, which is correctly closed — its fix works, and this is the case it deliberately excludes.
70ccc973e stops boxing an escaping array when the callee can be proven not to alias it. Its predicate returns false for "an import, a native, or a value-position closure", so an array forwarded to a cargo: native is still boxed on every read, everywhere in the program.
That exclusion is right by default — a native's body is invisible, and conservative is the only safe guess. But a native's contract is already declared, and that is the part that could be used.
Validated against origin/main @ 56b3b9b32
examples/probe-arrayarg in schlopai/tish-gba. Two arrays in one module, declared identically, filled identically, read by identical loops; the only difference is that B is handed to a native once.
A, never forwarded — typed, and this is #663's fix working:
acc = ((acc).wrapping_add({ let __bi = (i) as usize; let __bg = A.borrow();
(*__bg).get(__bi).copied().unwrap_or(0) })) as i32;
B, forwarded to grid_from_gids once — still boxed:
acc = match &tishlang_runtime::ops::add(&Value::Number(((acc) as f64)),
&tishlang_runtime::get_index(&tishlang_runtime::vm_read(&B), &Value::Number((i) as f64))) { … };
Measured on device (1,024 reads, each span alone in its own frame): 1.55 ticks/read for A, 5.87 for B — 3.8x, unchanged from the original report.
Why this shape is common
An array-taking native is usually a sink: the game builds a buffer and hands it over once, then keeps reading and writing it every frame. The cost lands on the loops, and the call that caused it runs once per level.
In schlopai/tish-gba every array-taking native is read-only in exactly this way:
| native |
what it does with the array |
tilemap_stream(tileset, cols, w, h, gids, priority) |
copies the gids into VRAM |
grid_from_gids(w, h, data, solid, oneway, ladder) |
walks the gids, marks a collision plane |
Neither retains the slice past the call, and neither writes through it. packages/dungeon.tish currently keeps two arrays to work around this — a private one for the generation loops and a copy purely for the handoff.
Ask
The declaration already exists and is the natural place to state it:
declare fn grid_from_gids(w: i32, h: i32, data: i32[], solid: i32[]): void
Some way for the crate author to assert "this parameter is read during the call and not retained or mutated" — a readonly marker on the parameter, or a blanket rule that a declare fn array parameter is read-only unless marked otherwise, since a native that mutates a caller's array is the rarer case and could opt in.
That keeps #663's conservatism where the body is genuinely unknown (method calls, value-position callees) while letting the one case with a written-down contract take the typed path.
The two-array workaround is fine and documented, so no urgency — filing so the native case is tracked separately rather than assumed covered by #663.
Follow-up to #663, which is correctly closed — its fix works, and this is the case it deliberately excludes.
70ccc973estops boxing an escaping array when the callee can be proven not to alias it. Its predicate returns false for "an import, a native, or a value-position closure", so an array forwarded to acargo:native is still boxed on every read, everywhere in the program.That exclusion is right by default — a native's body is invisible, and conservative is the only safe guess. But a native's contract is already declared, and that is the part that could be used.
Validated against
origin/main@56b3b9b32examples/probe-arrayarginschlopai/tish-gba. Two arrays in one module, declared identically, filled identically, read by identical loops; the only difference is thatBis handed to a native once.A, never forwarded — typed, and this is #663's fix working:B, forwarded togrid_from_gidsonce — still boxed:Measured on device (1,024 reads, each span alone in its own frame): 1.55 ticks/read for
A, 5.87 forB— 3.8x, unchanged from the original report.Why this shape is common
An array-taking native is usually a sink: the game builds a buffer and hands it over once, then keeps reading and writing it every frame. The cost lands on the loops, and the call that caused it runs once per level.
In
schlopai/tish-gbaevery array-taking native is read-only in exactly this way:tilemap_stream(tileset, cols, w, h, gids, priority)grid_from_gids(w, h, data, solid, oneway, ladder)Neither retains the slice past the call, and neither writes through it.
packages/dungeon.tishcurrently keeps two arrays to work around this — a private one for the generation loops and a copy purely for the handoff.Ask
The declaration already exists and is the natural place to state it:
Some way for the crate author to assert "this parameter is read during the call and not retained or mutated" — a
readonlymarker on the parameter, or a blanket rule that adeclare fnarray parameter is read-only unless marked otherwise, since a native that mutates a caller's array is the rarer case and could opt in.That keeps #663's conservatism where the body is genuinely unknown (method calls, value-position callees) while letting the one case with a written-down contract take the typed path.
The two-array workaround is fine and documented, so no urgency — filing so the native case is tracked separately rather than assumed covered by #663.