+- **v0.5.158** — Fix #133: five compounding `--target web` (WASM) bugs reported while porting a Bloom Engine game. (1) **String methods unrouted** — `"hello".charCodeAt(i)` / `str.codePointAt(i)` / any `String.prototype` method the codegen doesn't special-case fell through `__classDispatch` (primitives have no `__class__`, aren't in `__uiMethodMap`) and returned `undefined`. Added a typeof fast-path at the top of `__classDispatch` in `crates/perry-codegen-wasm/src/wasm_runtime.js` that dispatches to `String.prototype` / `Number.prototype` / `Array.prototype` for any method whose `typeof obj` matches — strings/numbers now get the full JS prototype surface for free, arrays get the same as a fallback for methods not already routed through `__memDispatch`. (2) **`Math.sin`/`cos`/`tan`/`atan2`/`exp`/etc. returned `undefined`** — HIR lower.rs desugars `Math.sin(x)` to `Expr::MathSin(x)` (etc.) but the WASM codegen only handled `MathFloor/Ceil/Sqrt/Log/Pow/Min/Max/Log2/Log10/Imul/Cbrt/Fround/Clz32/Expm1/Log1p/Sinh/Cosh/Tanh/Asinh/Acosh/Atanh`, so `MathSin/Cos/Tan/Asin/Acos/Atan/Atan2/MathExp` (+ variadic `MathHypot`) all hit the catch-all in `emit_expr` that emits `I64Const(TAG_UNDEFINED)`. Added explicit `Expr::Math*` handlers in `emit.rs` that route each through `emit_memcall("math_<fn>", N)` (Firefox-NaN-safe bridge), added a new `Expr::MathExp` variant to `perry-hir/src/ir.rs` + `lower.rs` lowering for `Math.exp` (the one trig-family function that wasn't already represented) + matching handlers in the JS and LLVM emitters (`emit_math_unary("Math.exp", x)` / `blk.call("llvm.exp.f64", …)`), and added the JS-side `math_sin/cos/tan/asin/acos/atan/atan2/sinh/cosh/tanh/asinh/acosh/atanh/exp/expm1/log1p/cbrt/hypot/fround/clz32` entries to `__memDispatch` + the `bridge_names` pre-intern list so `mem_call`'s name→fn lookup succeeds. Native-LLVM and pure-JS targets were already covered for most of these; this just closes the WASM gap. (3) **`xs.push(1); xs.length === 0`** on any top-level `const xs: number[] = []` — root cause was `Expr::ArrayPush / ArrayPop / ArrayShift / ArrayUnshift / ArrayPushSpread / ArraySplice` all doing `self.local_map.get(array_id).map_or(TAG_UNDEFINED, LocalGet)` to resolve the receiver, which only checks WASM locals. But top-level `let`/`const` in a module compiles to a WASM *global* (via `module_let_globals`), not a local — so the array-ref lookup missed, emitted `I64Const(TAG_UNDEFINED)` actually (wait: it went through the fall-through path and ended up zero because of a subsequent `LocalSet` chain; debug logs showed `mem_call IN array_push arg 0 bits= 0`). `array_push(0, 1)` saw a non-array arg 0, silently no-op'd, returned the input unchanged; `array_length(0)` returned 0. Introduced `emit_local_or_global_get(func, &id)` helper that mirrors the existing `Expr::LocalGet` lookup order (`module_let_globals` first, then `local_map`, then `TAG_UNDEFINED`), replaced the 7 duplicated ad-hoc lookups in the Array*/ArraySplice handlers with a single call. Verified end-to-end: smoke test `/tmp/test_array.ts` now prints `xs.length=3` after three pushes + correct indexed reads, Node shim + Perry web runtime. (4) **NaN-box tag canonicalization on Firefox/Safari** — `wrapForI64` decoded WASM i64 (BigInt) args to f64 via `_u64[0] = a; return _f64[0]`, handing a NaN Number to the callee. On SpiderMonkey, Numbers crossing a JS function-call boundary are collapsed to canonical `0x7FF8_0000_0000_0000`, stripping our STRING_TAG (0x7FFF) / POINTER_TAG (0x7FFD) / INT32_TAG (0x7FFE) payload — handles that originated in FFI glue (`__perryFromJsValue` → NaN-boxed f64 → passed to a WASM i64 export) got canonicalized at the `ffi` namespace boundary, arrived in Perry as plain NaN, and Perry's downstream `string_len` / `isPointer` / `getHandle` helpers treated them as invalid (`.length` → 0, array mutations no-op'd). Split namespace wrapping into a new `wrapFfiForI64` for the `ffi` namespace that decodes BigInt args via `__bitsToJsValue` (UNDEFINED/NULL/BOOL → JS equivalents, STRING_TAG → `stringTable[id]`, POINTER_TAG → `handleStore.get(id)`, INT32_TAG → `Number(BigInt.asIntN(32, low))`, other → f64 reinterpret) — skipping the canonicalizing f64 round-trip entirely; `rt` namespace keeps the legacy `wrapForI64` (unchanged for async-function-impl compatibility, which relies on the f64 NaN-box calling convention). Both wrappers honour an `fn.__rawBigint = true` escape hatch for callees that want raw BigInt. Also exposed `__perryJsValueToBits(v) -> BigInt` and `__perryBitsToJsValue(bits) -> any` as the Firefox-safe companion to the existing `__perryFromJsValue` / `__perryToJsValue` f64 helpers — FFI glue that wants to thread values through WASM exports should prefer the BigInt pair and avoid f64 entirely. (5) **INT32-tagged constants crashed wasm-bindgen** — `const FILTER_NEAREST = 1` NaN-boxed as `0x7FFE_0000_0000_0001`; passed to a wasm-bindgen FFI import it failed with `TypeError: Cannot convert BigInt value to a number` because the bindgen wrapper's implicit coercion couldn't handle the tagged BigInt. `__bitsToJsValue` didn't decode INT32_TAG (it only handled STRING/POINTER + plain numbers), so the tagged bits fell through to `_u64[0] = bits; _f64[0]` → NaN. Added INT32_TAG decoding to both `__bitsToJsValue` instances (main + worker): `tag === INT32_TAG ? Number(BigInt.asIntN(32, bits & 0xFFFFFFFFn)) : …` — with the new `wrapFfiForI64` running every ffi arg through `__bitsToJsValue`, INT32-tagged values now arrive at wasm-bindgen as plain Numbers (sign-extended 32-bit). Item 2 from the issue (imported `Key.ENTER` from `bloom/core` resolving to TAG_UNDEFINED) is deferred — likely a module-init-order or cross-module-property-lookup issue in HIR, needs a concrete repro against Bloom's module structure. Verified via node shim against `/tmp/test_web_issue133.ts`: all five fixes land, smoke test passes end-to-end.
0 commit comments