Summary
When a TS class method has an optional parameter and is called from Perry-compiled code without that parameter, __classDispatch in wasm_runtime.js passes the missing slot directly to the WASM function. The WASM ABI expects an i64 (BigInt) for each declared parameter; JavaScript's auto-coercion does BigInt(undefined), which throws:
TypeError: Cannot convert undefined to a BigInt
at __classDispatch (.../index.html:4540:40)
at mem_call (.../index.html:2183:22)
This makes every class with optional-param methods unusable on --target web.
Environment
- Perry: 0.5.1006
- Target: web (
--target web)
- macOS 26.4 (Darwin 25.4.0)
- Repro vehicle:
@honeide/editor's CompositeEngine.parse(buffer, changedRanges?) called with one arg.
Minimal repro
// entry.ts
class Tokenizer {
parse(buffer: string, changedRanges?: number[]): number {
return buffer.length + (changedRanges?.length ?? 0);
}
}
const t = new Tokenizer();
console.log(t.parse('hello')); // boom on --target web
perry compile entry.ts --target web --output out.html
open out.html
# DevTools:
# Boot error: TypeError: Cannot convert undefined to a BigInt
# at __classDispatch (...:4540:40)
Adding the second arg (t.parse('hello', [])) avoids the crash. On macOS/iOS/native this works fine.
Root cause
crates/perry-codegen-wasm/src/wasm_runtime.js __classDispatch (around line 4540 in emitted output):
if (objVal && typeof objVal === 'object' && objVal.__class__) {
let cls = objVal.__class__;
while (cls) {
const methods = classMethodTable[cls];
if (methods && mname in methods) {
const fn = wasmInstance?.exports.__indirect_function_table?.get(methods[mname]);
if (fn) return __bitsToJsValue(fn(__jsValueToBits(objVal), ...rawArgs.map(v => __jsValueToBits(v))));
}
cls = classParentTable[cls] || null;
}
}
rawArgs.map(v => __jsValueToBits(v)) produces exactly rawArgs.length BigInt args. When fn expects more (because the TS signature has optional params that compile to required WASM params), the missing slots come through as JS undefined. The WASM call site coerces each arg with BigInt(...) — BigInt(undefined) throws.
__jsValueToBits(undefined) itself returns TAG_UNDEFINED (a BigInt) — so if __classDispatch padded the args array first, the call would succeed.
Suggested fix
Pad rawArgs to match fn.length - 1 (subtract the receiver) before mapping:
if (fn) {
const need = Math.max(0, (fn.length | 0) - 1);
const padded = rawArgs.slice();
while (padded.length < need) padded.push(undefined);
return __bitsToJsValue(fn(__jsValueToBits(objVal), ...padded.map(v => __jsValueToBits(v))));
}
Verified working against the editor's CompositeEngine.parse(buffer) call site via runtime patch.
Worth also auditing the freestanding-function call path (mem_call dispatch table → WASM) for the same issue, since the same ABI mismatch likely affects top-level functions with optional params.
Workaround
Source-patch the emitted HTML's __classDispatch after compile. Concrete example: https://github.com/HoneIDE/editor/blob/main/examples/web/build.ts (search for dispatchPatchTarget).
Summary
When a TS class method has an optional parameter and is called from Perry-compiled code without that parameter,
__classDispatchinwasm_runtime.jspasses the missing slot directly to the WASM function. The WASM ABI expects an i64 (BigInt) for each declared parameter; JavaScript's auto-coercion doesBigInt(undefined), which throws:This makes every class with optional-param methods unusable on
--target web.Environment
--target web)@honeide/editor'sCompositeEngine.parse(buffer, changedRanges?)called with one arg.Minimal repro
Adding the second arg (
t.parse('hello', [])) avoids the crash. On macOS/iOS/native this works fine.Root cause
crates/perry-codegen-wasm/src/wasm_runtime.js__classDispatch(around line 4540 in emitted output):rawArgs.map(v => __jsValueToBits(v))produces exactlyrawArgs.lengthBigInt args. Whenfnexpects more (because the TS signature has optional params that compile to required WASM params), the missing slots come through as JSundefined. The WASM call site coerces each arg withBigInt(...)—BigInt(undefined)throws.__jsValueToBits(undefined)itself returnsTAG_UNDEFINED(a BigInt) — so if__classDispatchpadded the args array first, the call would succeed.Suggested fix
Pad
rawArgsto matchfn.length - 1(subtract the receiver) before mapping:Verified working against the editor's
CompositeEngine.parse(buffer)call site via runtime patch.Worth also auditing the freestanding-function call path (
mem_calldispatch table → WASM) for the same issue, since the same ABI mismatch likely affects top-level functions with optional params.Workaround
Source-patch the emitted HTML's
__classDispatchafter compile. Concrete example: https://github.com/HoneIDE/editor/blob/main/examples/web/build.ts (search fordispatchPatchTarget).