Skip to content

--target web: string methods unrouted, imported const fields undefined, Array.push no-op, missing trig, NaN-box canonicalization on Firefox #133

@proggeramlug

Description

@proggeramlug

Reporting a compounding set of issues found while shipping a Bloom Engine game to the web via perry compile --target web. Each silently returns undefined or a no-op instead of erroring, so they all manifest as "feature broken, no console error" — hours of bisecting each.

Perry version: 0.5.146
Repro scope: a 2269-line single-file TypeScript game using bloom/core, bloom/shapes, bloom/text, bloom/textures. Generated WASM boots, bloom_web (wgpu) renders, but the game state silently no-ops in several places.

1. String methods not routed through __classDispatch

data.charCodeAt(i) returns undefined on web. __classDispatch(str, "charCodeAt", args) falls through classMethodTable (strings have no __class__) and __uiMethodMap (not a UI method) → return undefined. Any parser loop using charCodeAt / indexOf / slice / split / startsWith on a regular string silently breaks.

Works on native (compiled differently).

Workaround: post-process generated HTML to inject a typeof objVal === 'string' fast-path at the top of __classDispatch with cases for the common methods.

2. Imported const-object fields resolve to undefined

import { Key } from "bloom/core";        // Key.ENTER = 265 in bloom/core/keys.ts
isKeyPressed(Key.ENTER);                  // bloom FFI sees TAG_UNDEFINED

At runtime the FFI function is invoked with BigInt 0x7FFC_0000_0000_0001 (= TAG_UNDEFINED). Workaround: inline every Key.X as a local const K_X = 265;. Works on native.

3. Array.push runs but .length stays 0

const xs: number[] = [];
xs.push(1);
xs.push(2);
console.log(xs.length); // → 0 on web, 2 on native

I believe this is the same NaN-canonicalization root cause as #5getHandle(handle) in rt.array_push / rt.array_length receives a canonicalized NaN Number, isPointer returns false, silent no-op.

Workaround in game code: index-based assignment + manual counter.

4. Math.sin, Math.cos, Math.tan, Math.atan2, Math.asin, Math.acos, Math.atan, Math.exp, Math.sign, Math.trunc missing from rt / __memDispatch

Only math_floor / ceil / round / abs / sqrt / pow / random / log / min / max are shipped. Math.sin(x) * 0.3 returns undefined * 0.3NaN. All animations relying on trig silently go static.

5. NaN-box canonicalization across the JS function boundary (Firefox/SpiderMonkey)

This is the nastiest one. wrapForI64 converts WASM i64 BigInt args to f64 by bit-reinterpret (_u64[0] = a; return _f64[0]). On Firefox, SpiderMonkey's internal NaN-boxing collapses foreign NaNs to canonical 0x7FF8_0000_0000_0000 when the Number crosses a function argument boundary — stripping STRING_TAG/POINTER_TAG/INT32_TAG.

mem_call avoids this by passing via WASM memory, but the direct rt imports (string_len, isPointer, getHandle) go through the canonicalizing f64 path. Result: any .length on a runtime-created string or handle-based array returns 0 in Firefox.

Similarly, my __perryJsValueToBits helper initially went fromJsValue(v) → f64 → _u64[0]. The f64 NaN got canonicalized on the function return (even through Float64Array). Building the BigInt directly ((STRING_TAG << 48n) | BigInt(id)) works. Suggest Perry's __jsValueToBits (the one already in the generated HTML at __classDispatch time) is the correct template — but the rt.* imports called from WASM don't benefit from it because they receive f64, not BigInt.

Workaround used: patched wrapForI64 to pass BigInt unchanged for __rawBigint-flagged functions, plus made f64ToU64(x) BigInt-passthrough so the tag helpers work.

6. INT32-tagged integer constants break bloom FFI wasm-bindgen

const FILTER_NEAREST = 1 is stored NaN-boxed as 0x7FFE_0000_0000_0001. Passed to a wasm-bindgen-generated bloom_set_texture_filter(handle, filter) via wrapForI64, the _u64[0] = a; _f64[0] conversion yields a NaN Number; wasm-bindgen then throws TypeError: Cannot convert BigInt value to a number when it tries to coerce back to f64 for the WASM call.

Workaround: patched wrapForI64 to decode tag === 0x7FFEn BigInts to Number(a & 0xFFFFFFFFn).

Ask

Happy to send PRs for the self-contained HTML template (items 1, 4) if that's useful — the quirks in 2/3/5/6 feel more like codegen + runtime protocol decisions that you'd want to design yourself.

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