Summary
On the web target, a write to a module-level array element performed inside a function is silently dropped — subsequent reads return the array's initializer. The same code is correct on native. This makes any Perry program that keeps mutable state in module-level arrays (the idiom recommended in our own pitfalls doc to avoid module-let issues) non-functional on web: menus don't advance, game/UI state never updates, etc.
Discovered shipping Bloom Jump to web — the title menu renders but never responds: the nav handler runs (its playSound fires) and executes GS[GI_SEL] += 1, but the draw function still reads GS[GI_SEL] as its initial value, so the selection never moves and no screen can be entered.
Minimal repro
const A: number[] = [0.0];
function bump(n: number): void { A[0] = A[0] + n; } // write module array inside a fn
function readA(): number { return A[0]; }
const step: number = Date.now() > 0.0 ? 5.0 : 1.0; // runtime value → defeats const-folding
bump(step);
console.log("fnRead=" + readA().toString() + " topRead=" + A[0].toString());
| Target |
Output |
native (perry compile) |
fnRead=5 topRead=5 ✅ |
web (perry compile --target web) |
fnRead=0 topRead=0 ❌ |
The write inside bump() never lands; both the in-function read and the top-level read see the initializer 0.
Characterization
- Non-foldable: the increment is derived from
Date.now(), so this is not the optimizer constant-folding a trivially-known array — the runtime write is genuinely lost.
- Top-level writes affected too: a variant doing
A[0] = 99 then reading at top level also returns the initializer on web (native returns 99).
- Write-through-parameter affected:
fn(arr) { arr[0] = ... } called with the module array likewise no-ops on web.
- Reads of module arrays that are not written (pure constants) work fine — it's specifically the mutation that doesn't persist.
Environment
- Reproduced locally with perry 0.5.1125.
- Symptom present in 0.5.1159 as well (the deployed Bloom Jump web build, compiled by 0.5.1159, exhibits the dead-menu behavior).
Impact
This is the documented-and-recommended state pattern (const S = [0.0, 0.0, ...]; mutate S[i]) — our own Perry-pitfalls guidance steers games toward it because module-let mutation is unreliable. With module-array writes also dropped on web, there's no working module-scope mutable-state pattern on the web target.
Repro files (native + web HTML) available; happy to attach.
Summary
On the web target, a write to a module-level array element performed inside a function is silently dropped — subsequent reads return the array's initializer. The same code is correct on native. This makes any Perry program that keeps mutable state in module-level arrays (the idiom recommended in our own pitfalls doc to avoid module-
letissues) non-functional on web: menus don't advance, game/UI state never updates, etc.Discovered shipping Bloom Jump to web — the title menu renders but never responds: the nav handler runs (its
playSoundfires) and executesGS[GI_SEL] += 1, but the draw function still readsGS[GI_SEL]as its initial value, so the selection never moves and no screen can be entered.Minimal repro
perry compile)fnRead=5 topRead=5✅perry compile --target web)fnRead=0 topRead=0❌The write inside
bump()never lands; both the in-function read and the top-level read see the initializer0.Characterization
Date.now(), so this is not the optimizer constant-folding a trivially-known array — the runtime write is genuinely lost.A[0] = 99then reading at top level also returns the initializer on web (native returns 99).fn(arr) { arr[0] = ... }called with the module array likewise no-ops on web.Environment
Impact
This is the documented-and-recommended state pattern (
const S = [0.0, 0.0, ...]; mutateS[i]) — our own Perry-pitfalls guidance steers games toward it because module-letmutation is unreliable. With module-array writes also dropped on web, there's no working module-scope mutable-state pattern on the web target.Repro files (native + web HTML) available; happy to attach.