Skip to content

[native/gba] #663 follow-up: an array forwarded to a cargo: native is still boxed — let a declare fn assert its parameter is read-only #672

Description

@spacedevin

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.

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

    nativeNative (tish->Rust->cargo) backendperfPerformance: benchmarks, optimization roadmap

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions