// Perry WASM Runtime Bridge // Provides JavaScript runtime functions imported by the WASM module. // Handles NaN-boxing, string management, handle store, and browser API access. // NaN-boxing constants (matching perry-runtime/src/value.rs) const __hostGlobal = globalThis; const TAG_UNDEFINED = 0x7FFC_0000_0000_0001n; const TAG_NULL = 0x7FFC_0000_0000_0002n; const TAG_FALSE = 0x7FFC_0000_0000_0003n; const TAG_TRUE = 0x7FFC_0000_0000_0004n; const STRING_TAG = 0x7FFFn; const POINTER_TAG = 0x7FFDn; const INT32_TAG = 0x7FFEn; // f64 <-> u64 conversion via shared buffer const _convBuf = new ArrayBuffer(8); const _f64 = new Float64Array(_convBuf); const _u64 = new BigUint64Array(_convBuf); function f64ToU64(f) { _f64[0] = f; return _u64[0]; } function u64ToF64(u) { _u64[0] = u; return _f64[0]; } // Issue #1035: pad a BigInt arg list to match the WASM function's declared arity. // Missing trailing slots correspond to TS optional params the caller omitted; the // WASM ABI expects an i64 (BigInt) per declared slot, so we fill with TAG_UNDEFINED. // Without this, V8/SpiderMonkey auto-coerce `undefined` via BigInt(undefined) and // throw "Cannot convert undefined to a BigInt". `prefix` is the count of leading // args already on the call (e.g. 1 for the receiver, or closure capture count). function __padBigintArgs(fn, prefix, bigintArgs) { const need = Math.max(0, (fn.length | 0) - (prefix | 0)); while (bigintArgs.length < need) bigintArgs.push(TAG_UNDEFINED); return bigintArgs; } // NaN-box helpers function nanboxString(id) { return u64ToF64((STRING_TAG << 48n) | BigInt(id)); } function nanboxPointer(id) { return u64ToF64((POINTER_TAG << 48n) | BigInt(id)); } function isString(val) { return (f64ToU64(val) >> 48n) === STRING_TAG; } function isPointer(val) { return (f64ToU64(val) >> 48n) === POINTER_TAG; } function getStringId(val) { return Number(f64ToU64(val) & 0xFFFFFFFFn); } function getPointerId(val) { return Number(f64ToU64(val) & 0xFFFFFFFFn); } function isUndefined(val) { return f64ToU64(val) === TAG_UNDEFINED; } function isNull(val) { return f64ToU64(val) === TAG_NULL; } function isTrue(val) { return f64ToU64(val) === TAG_TRUE; } function isFalse(val) { return f64ToU64(val) === TAG_FALSE; } // String table — maps string_id (index) to JS string const stringTable = []; // Handle store — maps handle_id to JS objects/arrays/closures/etc. const handleStore = new Map(); let nextHandleId = 1; const objectHandles = new WeakMap(); function allocHandle(obj) { const reference = obj !== null && (typeof obj === 'object' || typeof obj === 'function'); if (reference && objectHandles.has(obj)) return objectHandles.get(obj); const id = nextHandleId++; if (reference) objectHandles.set(obj, id); handleStore.set(id, obj); return id; } function getHandle(val) { if (!isPointer(val)) return undefined; return handleStore.get(getPointerId(val)); } function getHandleId(val) { return getPointerId(val); } // Convert a NaN-boxed f64 value to a JS value function toJsValue(val) { const bits = f64ToU64(val); if (bits === TAG_UNDEFINED) return undefined; if (bits === TAG_NULL) return null; if (bits === TAG_TRUE) return true; if (bits === TAG_FALSE) return false; const tag = bits >> 48n; if (tag === STRING_TAG) return stringTable[Number(bits & 0xFFFFFFFFn)]; if (tag === POINTER_TAG) { const obj = handleStore.get(Number(bits & 0xFFFFFFFFn)); if (obj !== undefined) return obj; } return val; // plain number } // Convert a JS value to a NaN-boxed f64 function fromJsValue(v) { if (v === undefined) return u64ToF64(TAG_UNDEFINED); if (v === null) return u64ToF64(TAG_NULL); if (v === true) return u64ToF64(TAG_TRUE); if (v === false) return u64ToF64(TAG_FALSE); if (typeof v === 'number') return v; if (typeof v === 'string') { const id = stringTable.length; stringTable.push(v); return nanboxString(id); } // Object/Array/Function — store as handle const id = allocHandle(v); return nanboxPointer(id); } // globalThis as a NaN-boxed value, boxed once: each fromJsValue of an object takes a // handle that is never released, so boxing it per read grows handleStore without bound // in an async loop that reads builtins. let __globalThisBoxed; function globalThisValue() { if (__globalThisBoxed === undefined) __globalThisBoxed = fromJsValue(globalThis); return __globalThisBoxed; } // Get string from NaN-boxed value (supports both string tag and pointer to string) function getString(val) { if (isString(val)) return stringTable[getStringId(val)]; const js = toJsValue(val); return String(js); } let wasmMemory = null; let wasmInstance = null; // Expose NaN-box conversion for FFI bridge (bloom_glue.js etc.) // These allow external code to convert between Perry's NaN-boxed i64 values and JS values. globalThis.__perryToJsValue = function(nanboxedF64) { return toJsValue(nanboxedF64); }; globalThis.__perryFromJsValue = function(jsValue) { return fromJsValue(jsValue); }; globalThis.__perryI64ToF64 = function(i64BigInt) { return u64ToF64(i64BigInt); }; globalThis.__perryF64ToI64 = function(f64Val) { return f64ToU64(f64Val); }; // BigInt-native helpers (Issue #133 item 5). Prefer these on Firefox/Safari: // going through f64 loses NaN-box tag payloads when the Number crosses a JS // function-call boundary (SpiderMonkey collapses foreign NaNs to the canonical // 0x7FF8_0000_0000_0000). __perry*Bits helpers skip the f64 round-trip entirely. // __perryJsValueToBits(v) -> BigInt (raw NaN-box bits, ready to pass to a WASM // i64 export). __perryBitsToJsValue(bits) -> JS value (handle, string, number, …). globalThis.__perryJsValueToBits = function(v) { return __jsValueToBits(v); }; globalThis.__perryBitsToJsValue = function(bits) { return __bitsToJsValue(bits); }; // Build the import object for WASM instantiation function buildImports() { return { rt: { // ===== Core (Phase 0) ===== string_new: (offset, len) => { const bytes = new Uint8Array(wasmMemory.buffer, offset, len); stringTable.push(new TextDecoder().decode(bytes)); }, console_log: (val) => { console.log(toJsValue(val)); }, console_warn: (val) => { console.warn(toJsValue(val)); }, console_error: (val) => { console.error(toJsValue(val)); }, string_concat: (a, b) => { const s = stringTable[getStringId(a)] + stringTable[getStringId(b)]; stringTable.push(s); return nanboxString(stringTable.length - 1); }, js_add: (a, b) => fromJsValue(toJsValue(a) + toJsValue(b)), string_eq: (a, b) => stringTable[getStringId(a)] === stringTable[getStringId(b)] ? 1 : 0, string_len: (val) => { if (isString(val)) return stringTable[getStringId(val)].length; // Array length const obj = getHandle(val); if (Array.isArray(obj)) return obj.length; return 0; }, jsvalue_to_string: (val) => { stringTable.push(String(toJsValue(val))); return nanboxString(stringTable.length - 1); }, jsvalue_to_template_string: (val) => { stringTable.push(`${toJsValue(val)}`); return nanboxString(stringTable.length - 1); }, is_truthy: (val) => { const bits = f64ToU64(val); if (bits === TAG_FALSE || bits === TAG_NULL || bits === TAG_UNDEFINED) return 0; if (bits === TAG_TRUE) return 1; const tag = bits >> 48n; if (tag === STRING_TAG) return stringTable[Number(bits & 0xFFFFFFFFn)].length > 0 ? 1 : 0; if (tag === POINTER_TAG) return 1; // objects are truthy return (val === 0 || Number.isNaN(val)) ? 0 : 1; }, js_strict_eq: (a, b) => toJsValue(a) === toJsValue(b) ? 1 : 0, // Math math_floor: (x) => Math.floor(x), math_ceil: (x) => Math.ceil(x), math_round: (x) => Math.round(x), math_abs: (x) => Math.abs(x), math_sqrt: (x) => Math.sqrt(x), math_pow: (base, exp) => Math.pow(base, exp), math_random: () => Math.random(), math_log: (x) => Math.log(x), date_now: () => Date.now(), js_typeof: (val) => { const t = __typeOf(toJsValue(val)); stringTable.push(t); return nanboxString(stringTable.length - 1); }, math_min: (a, b) => Math.min(a, b), math_max: (a, b) => Math.max(a, b), parse_int: (val) => { const s = isString(val) ? stringTable[getStringId(val)] : String(toJsValue(val)); return parseInt(s, 10); }, parse_float: (val) => { const s = isString(val) ? stringTable[getStringId(val)] : String(toJsValue(val)); return parseFloat(s); }, // Phase 0 fixes js_mod: (a, b) => a % b, is_null_or_undefined: (val) => { const bits = f64ToU64(val); return (bits === TAG_NULL || bits === TAG_UNDEFINED) ? 1 : 0; }, // ===== Phase 1: Objects ===== object_new: () => nanboxPointer(allocHandle({})), // object_set(handle, key_str, value) -> handle (for chaining) object_set: (handle, key, value) => { const obj = getHandle(handle); if (obj) obj[getString(key)] = toJsValue(value); return handle; }, object_get: (handle, key) => { const obj = getHandle(handle); if (!obj) return u64ToF64(TAG_UNDEFINED); return fromJsValue(obj[getString(key)]); }, object_get_dynamic: (handle, key) => { const obj = getHandle(handle); if (!obj) return u64ToF64(TAG_UNDEFINED); const k = toJsValue(key); return fromJsValue(obj[k]); }, object_set_dynamic: (handle, key, value) => { const obj = getHandle(handle); if (obj) obj[toJsValue(key)] = toJsValue(value); }, object_delete: (handle, key) => { const obj = getHandle(handle); if (obj) delete obj[getString(key)]; }, object_delete_dynamic: (handle, key) => { const obj = getHandle(handle); if (obj) delete obj[toJsValue(key)]; }, object_keys: (handle) => { const obj = getHandle(handle); return nanboxPointer(allocHandle(obj ? Object.keys(obj) : [])); }, object_values: (handle) => { const obj = getHandle(handle); return nanboxPointer(allocHandle(obj ? Object.keys(obj).map(k => obj[k]) : [])); }, object_entries: (handle) => { const obj = getHandle(handle); return nanboxPointer(allocHandle(obj ? Object.entries(obj) : [])); }, object_has_property: (handle, key) => { const obj = getHandle(handle); if (!obj) return 0; const k = toJsValue(key); return (k in obj) ? 1 : 0; }, // object_assign(target, source) -> target handle object_assign: (target, source) => { const rawTarget = toJsValue(target); if (rawTarget == null) throw new TypeError('Cannot convert undefined or null to object'); const t = Object(rawTarget); const s = toJsValue(source); if (s != null) Object.assign(t, s); return isPointer(target) ? target : fromJsValue(t); }, // ===== Phase 1: Arrays ===== array_new: () => nanboxPointer(allocHandle([])), // `new Array(x)` — ES2015 §22.1.1: a single NUMBER argument is a // length (must be a non-negative integer < 2^32), anything else is a // one-element array. Mirrors js_array_constructor_single in the // native runtime. array_constructor_single: (value) => { const v = toJsValue(value); if (typeof v === 'number') { if (!Number.isInteger(v) || v < 0 || v > 0xFFFFFFFF) { throw new RangeError('Invalid array length'); } return nanboxPointer(allocHandle(new Array(v))); } return nanboxPointer(allocHandle([v])); }, // array_push(handle, value) -> handle (for chaining) array_push: (handle, value) => { const arr = getHandle(handle); if (arr) arr.push(toJsValue(value)); return handle; }, array_pop: (handle) => { const arr = getHandle(handle); if (!arr || arr.length === 0) return u64ToF64(TAG_UNDEFINED); return fromJsValue(arr.pop()); }, array_get: (handle, index) => { const arr = getHandle(handle); if (!arr) return u64ToF64(TAG_UNDEFINED); const i = typeof index === 'number' ? index : toJsValue(index); return fromJsValue(arr[i]); }, array_set: (handle, index, value) => { const arr = getHandle(handle); if (arr) arr[typeof index === 'number' ? index : toJsValue(index)] = toJsValue(value); }, array_length: (handle) => { const arr = getHandle(handle); return arr ? arr.length : 0; }, array_slice: (handle, start, end) => { const arr = getHandle(handle); if (!arr) return nanboxPointer(allocHandle([])); const s = typeof start === 'number' ? start : 0; const e = isUndefined(end) ? undefined : (typeof end === 'number' ? end : toJsValue(end)); return nanboxPointer(allocHandle(arr.slice(s, e))); }, array_splice: (handle, start, deleteCount) => { const arr = getHandle(handle); if (!arr) return nanboxPointer(allocHandle([])); const s = typeof start === 'number' ? start : 0; const dc = isUndefined(deleteCount) ? arr.length - s : deleteCount; return nanboxPointer(allocHandle(arr.splice(s, dc))); }, array_shift: (handle) => { const arr = getHandle(handle); if (!arr || arr.length === 0) return u64ToF64(TAG_UNDEFINED); return fromJsValue(arr.shift()); }, array_unshift: (handle, value) => { const arr = getHandle(handle); if (arr) arr.unshift(toJsValue(value)); }, array_join: (handle, separator) => { const arr = getHandle(handle); if (!arr) return nanboxString(allocHandle('')); const sep = getString(separator); const result = arr.map(v => typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v)).join(sep); stringTable.push(result); return nanboxString(stringTable.length - 1); }, array_index_of: (handle, value) => { const arr = getHandle(handle); if (!arr) return -1; const v = toJsValue(value); return arr.indexOf(v); }, array_includes: (handle, value) => { const arr = getHandle(handle); if (!arr) return 0; return arr.includes(toJsValue(value)) ? 1 : 0; }, array_concat: (h1, h2) => { const a1 = getHandle(h1) || []; const a2 = getHandle(h2) || []; return nanboxPointer(allocHandle(a1.concat(a2))); }, array_reverse: (handle) => { const arr = getHandle(handle); if (arr) arr.reverse(); return handle; }, array_flat: (handle) => { const arr = getHandle(handle); return nanboxPointer(allocHandle(arr ? arr.flat() : [])); }, array_is_array: (val) => { if (!isPointer(val)) return 0; const obj = getHandle(val); return Array.isArray(obj) ? 1 : 0; }, array_from: (val) => { const v = toJsValue(val); return nanboxPointer(allocHandle(Array.from(v))); }, array_push_spread: (target, source) => { const t = getHandle(target); const s = getHandle(source); if (t && s) t.push(...s); return target; }, // ===== Phase 1: String methods ===== string_charAt: (str, idx) => { const s = stringTable[getStringId(str)]; stringTable.push(s.charAt(idx)); return nanboxString(stringTable.length - 1); }, string_substring: (str, start, end) => { const s = stringTable[getStringId(str)]; stringTable.push(s.substring(start, end)); return nanboxString(stringTable.length - 1); }, string_indexOf: (str, search) => { return stringTable[getStringId(str)].indexOf(stringTable[getStringId(search)]); }, string_slice: (str, start, end) => { const s = stringTable[getStringId(str)]; stringTable.push(s.slice(start, end)); return nanboxString(stringTable.length - 1); }, string_toLowerCase: (str) => { stringTable.push(stringTable[getStringId(str)].toLowerCase()); return nanboxString(stringTable.length - 1); }, string_toUpperCase: (str) => { stringTable.push(stringTable[getStringId(str)].toUpperCase()); return nanboxString(stringTable.length - 1); }, string_trim: (str) => { stringTable.push(stringTable[getStringId(str)].trim()); return nanboxString(stringTable.length - 1); }, string_includes: (str, search) => { return stringTable[getStringId(str)].includes(stringTable[getStringId(search)]) ? 1 : 0; }, string_startsWith: (str, search) => { return stringTable[getStringId(str)].startsWith(stringTable[getStringId(search)]) ? 1 : 0; }, string_endsWith: (str, search) => { return stringTable[getStringId(str)].endsWith(stringTable[getStringId(search)]) ? 1 : 0; }, string_replace: (str, pattern, replacement) => { const s = stringTable[getStringId(str)]; // pattern might be a regex handle or a string let p, r; if (isPointer(pattern)) { p = getHandle(pattern); // RegExp object } else { p = getString(pattern); } r = getString(replacement); stringTable.push(s.replace(p, r)); return nanboxString(stringTable.length - 1); }, string_split: (str, delim) => { const s = stringTable[getStringId(str)]; const d = getString(delim); return nanboxPointer(allocHandle(s.split(d))); }, string_fromCharCode: (code) => { stringTable.push(String.fromCharCode(code)); return nanboxString(stringTable.length - 1); }, string_padStart: (str, len, fill) => { const s = stringTable[getStringId(str)]; stringTable.push(s.padStart(len, getString(fill))); return nanboxString(stringTable.length - 1); }, string_padEnd: (str, len, fill) => { const s = stringTable[getStringId(str)]; stringTable.push(s.padEnd(len, getString(fill))); return nanboxString(stringTable.length - 1); }, string_repeat: (str, count) => { stringTable.push(stringTable[getStringId(str)].repeat(count)); return nanboxString(stringTable.length - 1); }, string_match: (str, regex) => { const s = stringTable[getStringId(str)]; const re = isPointer(regex) ? getHandle(regex) : new RegExp(getString(regex)); const result = s.match(re); if (!result) return u64ToF64(TAG_NULL); return nanboxPointer(allocHandle(Array.from(result))); }, math_log2: (x) => Math.log2(x), math_log10: (x) => Math.log10(x), // ===== Phase 2: Closures ===== // closure_new(func_table_idx, capture_count) -> handle closure_new: (funcIdx, captureCount) => { return nanboxPointer(allocHandle(__newClosure(funcIdx, captureCount))); }, // closure_set_capture(handle, idx, value) -> handle (chaining) // value is NaN-boxed f64 — convert to BigInt for WASM i64 ABI closure_set_capture: (handle, idx, value) => { const c = getHandle(handle); if (c) c.captures[idx | 0] = f64ToU64(value); // store as BigInt for i64 WASM calls return handle; }, // closure_call_N(handle, args...) -> result // WASM functions use i64 (BigInt) params/returns. Captures are already BigInt. // Args (a0, a1, ...) are NaN-boxed f64 from the old rt: import ABI — convert via f64ToU64. // Return is BigInt from WASM — convert back to f64 via u64ToF64. closure_call_0: (handle) => { const c = getHandle(handle); if (!c || !wasmInstance) return u64ToF64(TAG_UNDEFINED); const fn = wasmInstance.exports.__indirect_function_table?.get(c.funcIdx | 0); if (!fn) return u64ToF64(TAG_UNDEFINED); // Pad for omitted TS optional params (Issue #1035). const padded = __padBigintArgs(fn, c.captures.length, []); return u64ToF64(fn(...c.captures, ...padded)); }, closure_call_1: (handle, a0) => { const c = getHandle(handle); if (!c || !wasmInstance) return u64ToF64(TAG_UNDEFINED); const fn = wasmInstance.exports.__indirect_function_table?.get(c.funcIdx | 0); if (!fn) return u64ToF64(TAG_UNDEFINED); const padded = __padBigintArgs(fn, c.captures.length, [f64ToU64(a0)]); return u64ToF64(fn(...c.captures, ...padded)); }, closure_call_2: (handle, a0, a1) => { const c = getHandle(handle); if (!c || !wasmInstance) return u64ToF64(TAG_UNDEFINED); const fn = wasmInstance.exports.__indirect_function_table?.get(c.funcIdx | 0); if (!fn) return u64ToF64(TAG_UNDEFINED); const padded = __padBigintArgs(fn, c.captures.length, [f64ToU64(a0), f64ToU64(a1)]); return u64ToF64(fn(...c.captures, ...padded)); }, closure_call_3: (handle, a0, a1, a2) => { const c = getHandle(handle); if (!c || !wasmInstance) return u64ToF64(TAG_UNDEFINED); const fn = wasmInstance.exports.__indirect_function_table?.get(c.funcIdx | 0); if (!fn) return u64ToF64(TAG_UNDEFINED); const padded = __padBigintArgs(fn, c.captures.length, [f64ToU64(a0), f64ToU64(a1), f64ToU64(a2)]); return u64ToF64(fn(...c.captures, ...padded)); }, // closure_call_spread(handle, args_array_handle) -> result closure_call_spread: (handle, argsHandle) => { const c = getHandle(handle); const args = getHandle(argsHandle) || []; if (!c || !wasmInstance) return u64ToF64(TAG_UNDEFINED); const fn = wasmInstance.exports.__indirect_function_table?.get(c.funcIdx | 0); if (!fn) return u64ToF64(TAG_UNDEFINED); const padded = __padBigintArgs(fn, c.captures.length, args.map(v => f64ToU64(fromJsValue(v)))); return u64ToF64(fn(...c.captures, ...padded)); }, // ===== Phase 2: Array higher-order methods ===== // Helper: call a closure/wasm function with an element // The closure is a handle, the callback takes (element, index, array) array_map: (handle, cbHandle) => { const arr = getHandle(handle); const cb = getHandle(cbHandle); if (!arr || !cb || !wasmInstance) return nanboxPointer(allocHandle([])); const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) return nanboxPointer(allocHandle([])); const result = arr.map((v, i) => { const r = __invokeWasmCallback(fn, ...cb.captures, __jsValueToBits(v), __jsValueToBits(i), __jsValueToBits(arr)); return __bitsToJsValue(r); }); return nanboxPointer(allocHandle(result)); }, array_filter: (handle, cbHandle) => { const arr = getHandle(handle); const cb = getHandle(cbHandle); if (!arr || !cb || !wasmInstance) return nanboxPointer(allocHandle([])); const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) return nanboxPointer(allocHandle([])); const result = arr.filter((v, i) => { const r = __invokeWasmCallback(fn, ...cb.captures, __jsValueToBits(v), __jsValueToBits(i), __jsValueToBits(arr)); const rv = __bitsToJsValue(r); if (rv === true) return true; if (rv === false || rv === null || rv === undefined) return false; return !!rv; }); return nanboxPointer(allocHandle(result)); }, array_forEach: (handle, cbHandle) => { const arr = getHandle(handle); const cb = getHandle(cbHandle); if (!arr || !cb || !wasmInstance) return; const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) return; arr.forEach((v, i) => __invokeWasmCallback(fn, ...cb.captures, __jsValueToBits(v), __jsValueToBits(i), __jsValueToBits(arr))); }, array_reduce: (handle, cbHandle, initial) => { const arr = getHandle(handle); const cb = getHandle(cbHandle); if (!arr || !cb || !wasmInstance) return u64ToF64(TAG_UNDEFINED); const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) return u64ToF64(TAG_UNDEFINED); let acc = isUndefined(initial) ? __jsValueToBits(arr[0]) : f64ToU64(initial); const startIdx = isUndefined(initial) ? 1 : 0; for (let i = startIdx; i < arr.length; i++) { acc = __invokeWasmCallback(fn, ...cb.captures, acc, __jsValueToBits(arr[i]), __jsValueToBits(i), __jsValueToBits(arr)); } return u64ToF64(acc); }, array_find: (handle, cbHandle) => { const arr = getHandle(handle); const cb = getHandle(cbHandle); if (!arr || !cb || !wasmInstance) return u64ToF64(TAG_UNDEFINED); const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) return u64ToF64(TAG_UNDEFINED); for (let i = 0; i < arr.length; i++) { const vBits = __jsValueToBits(arr[i]); const r = __invokeWasmCallback(fn, ...cb.captures, vBits, __jsValueToBits(i), __jsValueToBits(arr)); if (__bitsToJsValue(r)) return fromJsValue(arr[i]); } return u64ToF64(TAG_UNDEFINED); }, array_find_index: (handle, cbHandle) => { const arr = getHandle(handle); const cb = getHandle(cbHandle); if (!arr || !cb || !wasmInstance) return -1; const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) return -1; for (let i = 0; i < arr.length; i++) { const r = __invokeWasmCallback(fn, ...cb.captures, __jsValueToBits(arr[i]), __jsValueToBits(i), __jsValueToBits(arr)); if (__bitsToJsValue(r)) return i; } return -1; }, array_sort: (handle, cbHandle) => { const arr = getHandle(handle); if (!arr) return handle; if (isUndefined(cbHandle) || isNull(cbHandle)) { arr.sort(); return handle; } const cb = getHandle(cbHandle); if (!cb || !wasmInstance) { arr.sort(); return handle; } const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) { arr.sort(); return handle; } arr.sort((a, b) => { const r = __invokeWasmCallback(fn, ...cb.captures, __jsValueToBits(a), __jsValueToBits(b)); return __bitsToJsValue(r); }); return handle; }, array_some: (handle, cbHandle) => { const arr = getHandle(handle); const cb = getHandle(cbHandle); if (!arr || !cb || !wasmInstance) return 0; const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) return 0; return arr.some((v, i) => __bitsToJsValue(__invokeWasmCallback(fn, ...cb.captures, __jsValueToBits(v), __jsValueToBits(i), __jsValueToBits(arr)))) ? 1 : 0; }, array_every: (handle, cbHandle) => { const arr = getHandle(handle); const cb = getHandle(cbHandle); if (!arr || !cb || !wasmInstance) return 0; const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) return 0; return arr.every((v, i) => __bitsToJsValue(__invokeWasmCallback(fn, ...cb.captures, __jsValueToBits(v), __jsValueToBits(i), __jsValueToBits(arr)))) ? 1 : 0; }, // ===== Phase 3: Classes ===== // Class registry: class_name -> { methods: Map, statics: Map, parent: string|null } // class_new(class_name_str, field_count) -> handle class_new: (classNameVal, fieldCount) => { return nanboxPointer(allocHandle(__classInstance(getString(classNameVal)))); }, // class_set_method(class_id_str, method_name_str, func_table_idx) class_set_method: (classId, methodName, funcIdx) => { const cls = getString(classId); if (!classMethodTable[cls]) classMethodTable[cls] = {}; classMethodTable[cls][getString(methodName)] = funcIdx | 0; }, // class_call_method(handle, method_name_id, args_array_handle) -> result // method_name_id is a plain f64 string table index (not NaN-boxed). // WASM functions use i64 (BigInt) params/returns. class_call_method: (handle, methodNameId, argsHandle) => { const obj = getHandle(handle); const mname = stringTable[methodNameId | 0]; // Try class method dispatch first if (obj && obj.__class__) { let cls = obj.__class__; while (cls) { const methods = classMethodTable[cls]; if (methods && mname in methods) { const fn = wasmInstance?.exports.__indirect_function_table?.get(methods[mname]); if (fn) { const args = getHandle(argsHandle) || []; // Pad for omitted TS optional params (Issue #1035). const padded = __padBigintArgs(fn, 1, args.map(v => f64ToU64(fromJsValue(v)))); return u64ToF64(fn(f64ToU64(handle), ...padded)); } } cls = classParentTable[cls] || null; } } // Fallback: UI widget method dispatch // Map common method names to perry_ui_* bridge functions const uiMethodMap = { addChild: "perry_ui_widget_add_child", removeAllChildren: "perry_ui_widget_remove_all_children", setBackground: "perry_ui_set_background", setForeground: "perry_ui_set_foreground", setFontSize: "perry_ui_set_font_size", setFontWeight: "perry_ui_set_font_weight", setFontFamily: "perry_ui_set_font_family", setPadding: "perry_ui_set_padding", setFrame: "perry_ui_set_frame", setCornerRadius: "perry_ui_set_corner_radius", setBorder: "perry_ui_set_border", setOpacity: "perry_ui_set_opacity", setEnabled: "perry_ui_set_enabled", setTooltip: "perry_ui_set_tooltip", setControlSize: "perry_ui_set_control_size", animateOpacity: "perry_ui_animate_opacity", animatePosition: "perry_ui_animate_position", setOnClick: "perry_ui_set_on_click", setOnHover: "perry_ui_set_on_hover", setOnDoubleClick: "perry_ui_set_on_double_click", // State methods get: "perry_ui_state_get", set: "perry_ui_state_set", create: "perry_ui_state_create", bindText: "perry_ui_state_bind_text", bindTextNumeric: "perry_ui_state_bind_text_numeric", bindSlider: "perry_ui_state_bind_slider", bindToggle: "perry_ui_state_bind_toggle", bindVisibility: "perry_ui_state_bind_visibility", bindForEach: "perry_ui_state_bind_foreach", onChange: "perry_ui_state_on_change", // Text/Button ops setString: "perry_ui_text_set_string", setSelectable: "perry_ui_text_set_selectable", setBordered: "perry_ui_button_set_bordered", setTitle: "perry_ui_button_set_title", setTextColor: "perry_ui_button_set_text_color", setImage: "perry_ui_button_set_image", // Canvas fillRect: "perry_ui_canvas_fill_rect", strokeRect: "perry_ui_canvas_stroke_rect", clearRect: "perry_ui_canvas_clear_rect", setFillColor: "perry_ui_canvas_set_fill_color", setStrokeColor: "perry_ui_canvas_set_stroke_color", beginPath: "perry_ui_canvas_begin_path", moveTo: "perry_ui_canvas_move_to", lineTo: "perry_ui_canvas_line_to", arc: "perry_ui_canvas_arc", closePath: "perry_ui_canvas_close_path", fill: "perry_ui_canvas_fill", stroke: "perry_ui_canvas_stroke", setLineWidth: "perry_ui_canvas_set_line_width", fillText: "perry_ui_canvas_fill_text", setFont: "perry_ui_canvas_set_font", drawImage: "perry_ui_canvas_draw_image", // ScrollView setChild: "perry_ui_scrollview_set_child", // TextField focus: "perry_ui_textfield_focus", // Widget sizing setWidth: "perry_ui_widget_set_width", setHeight: "perry_ui_widget_set_height", matchParentWidth: "perry_ui_widget_match_parent_width", matchParentHeight: "perry_ui_widget_match_parent_height", setHidden: "perry_ui_set_widget_hidden", setEdgeInsets: "perry_ui_widget_set_edge_insets", setShadow: "perry_ui_widget_set_shadow", // App run: "perry_ui_app_run", setBody: "perry_ui_app_set_body", }; const uiFnName = uiMethodMap[mname]; if (uiFnName) { const fn = __perryUiDispatch[uiFnName]; if (fn) { const args = getHandle(argsHandle) || []; // First arg to the UI function is the handle/object, rest are the call args const objVal = toJsValue(handle); // Check if any args are closures (POINTER_TAG) and keep them raw for callWasmClosure const closureMethods = new Set(["setOnClick","setOnHover","setOnDoubleClick","onChange","bindForEach","set"]); if (closureMethods.has(mname)) { const jsArgs = args.map(v => { const bits = f64ToU64(v); if ((bits >> 48n) === POINTER_TAG) return v; return toJsValue(v); }); return fromJsValue(fn(objVal, ...jsArgs)); } const jsArgs = args.map(v => toJsValue(v)); return fromJsValue(fn(objVal, ...jsArgs)); } } return u64ToF64(TAG_UNDEFINED); }, class_get_field: (handle, name) => { const obj = getHandle(handle); if (!obj) return u64ToF64(TAG_UNDEFINED); const fname = getString(name); // Check for compiled getter method — WASM uses i64 (BigInt) if (obj.__class__) { let cls = obj.__class__; while (cls) { const methods = classMethodTable[cls]; if (methods && ('__get_' + fname) in methods) { const fn = wasmInstance?.exports.__indirect_function_table?.get(methods['__get_' + fname]); if (fn) return u64ToF64(fn(f64ToU64(handle))); } cls = classParentTable[cls] || null; } } return fromJsValue(obj[fname]); }, class_set_field: (handle, name, value) => { const obj = getHandle(handle); if (!obj) return; const fname = getString(name); // Check for compiled setter method — WASM uses i64 (BigInt) if (obj.__class__) { let cls = obj.__class__; while (cls) { const methods = classMethodTable[cls]; if (methods && ('__set_' + fname) in methods) { const fn = wasmInstance?.exports.__indirect_function_table?.get(methods['__set_' + fname]); if (fn) { fn(f64ToU64(handle), f64ToU64(value)); return; } } cls = classParentTable[cls] || null; } } obj[fname] = toJsValue(value); }, class_set_static: (classId, name, value) => { const cls = getString(classId); if (!classStaticTable[cls]) classStaticTable[cls] = {}; classStaticTable[cls][getString(name)] = toJsValue(value); }, class_get_static: (classId, name) => { const cls = getString(classId); const statics = classStaticTable[cls]; if (!statics) return u64ToF64(TAG_UNDEFINED); return fromJsValue(statics[getString(name)]); }, class_instanceof: (handle, classId) => { const obj = getHandle(handle); if (!obj) return 0; let cls = obj.__class__; const target = getString(classId); while (cls) { if (cls === target) return 1; cls = classParentTable[cls] || null; } return 0; }, // ===== Phase 4: JSON ===== json_parse: (str) => { try { const s = getString(str); return fromJsValue(JSON.parse(s)); } catch (e) { if (tryDepth > 0) { currentException = fromJsValue(e); } return u64ToF64(TAG_UNDEFINED); } }, json_stringify: (val) => { const js = toJsValue(val); stringTable.push(JSON.stringify(js)); return nanboxString(stringTable.length - 1); }, // ===== Phase 4: Map ===== map_new: () => nanboxPointer(allocHandle(new Map())), map_set: (handle, key, value) => { const m = getHandle(handle); if (m) m.set(toJsValue(key), toJsValue(value)); }, map_get: (handle, key) => { const m = getHandle(handle); if (!m) return u64ToF64(TAG_UNDEFINED); return fromJsValue(m.get(toJsValue(key))); }, map_has: (handle, key) => { const m = getHandle(handle); return m?.has(toJsValue(key)) ? 1 : 0; }, map_delete: (handle, key) => { const m = getHandle(handle); if (m) m.delete(toJsValue(key)); }, map_size: (handle) => { const m = getHandle(handle); return m ? m.size : 0; }, map_clear: (handle) => { const m = getHandle(handle); if (m) m.clear(); }, map_entries: (handle) => { const m = getHandle(handle); return nanboxPointer(allocHandle(m ? [...m.entries()] : [])); }, map_keys: (handle) => { const m = getHandle(handle); return nanboxPointer(allocHandle(m ? [...m.keys()] : [])); }, map_values: (handle) => { const m = getHandle(handle); return nanboxPointer(allocHandle(m ? [...m.values()] : [])); }, // ===== Phase 4: Set ===== set_new: () => nanboxPointer(allocHandle(new Set())), set_new_from_array: (arrHandle) => { const arr = getHandle(arrHandle) || toJsValue(arrHandle); return nanboxPointer(allocHandle(new Set(Array.isArray(arr) ? arr : []))); }, set_add: (handle, value) => { const s = getHandle(handle); if (s) s.add(toJsValue(value)); }, set_has: (handle, value) => { const s = getHandle(handle); return s?.has(toJsValue(value)) ? 1 : 0; }, set_delete: (handle, value) => { const s = getHandle(handle); if (s) s.delete(toJsValue(value)); }, set_size: (handle) => { const s = getHandle(handle); return s ? s.size : 0; }, set_clear: (handle) => { const s = getHandle(handle); if (s) s.clear(); }, set_values: (handle) => { const s = getHandle(handle); return nanboxPointer(allocHandle(s ? [...s.values()] : [])); }, // ===== Phase 4: Date ===== date_new_val: (arg) => { const d = isUndefined(arg) ? new Date() : new Date(toJsValue(arg)); return nanboxPointer(allocHandle(d)); }, date_get_time: (handle) => { const d = getHandle(handle); return d instanceof Date ? d.getTime() : 0; }, date_to_iso_string: (handle) => { const d = getHandle(handle); if (!(d instanceof Date)) return u64ToF64(TAG_UNDEFINED); stringTable.push(d.toISOString()); return nanboxString(stringTable.length - 1); }, date_get_full_year: (h) => { const d = getHandle(h); return d instanceof Date ? d.getFullYear() : 0; }, date_get_month: (h) => { const d = getHandle(h); return d instanceof Date ? d.getMonth() : 0; }, date_get_date: (h) => { const d = getHandle(h); return d instanceof Date ? d.getDate() : 0; }, date_get_day: (h) => { const d = getHandle(h); return d instanceof Date ? d.getDay() : 0; }, date_get_hours: (h) => { const d = getHandle(h); return d instanceof Date ? d.getHours() : 0; }, date_get_minutes: (h) => { const d = getHandle(h); return d instanceof Date ? d.getMinutes() : 0; }, date_get_seconds: (h) => { const d = getHandle(h); return d instanceof Date ? d.getSeconds() : 0; }, date_get_milliseconds: (h) => { const d = getHandle(h); return d instanceof Date ? d.getMilliseconds() : 0; }, // ===== Phase 4: Error ===== error_new: (msg) => { const message = isUndefined(msg) ? undefined : getString(msg); return nanboxPointer(allocHandle(new Error(message))); }, error_message: (handle) => { const e = getHandle(handle); const msg = e instanceof Error ? e.message : ''; stringTable.push(msg); return nanboxString(stringTable.length - 1); }, // ===== Phase 4: RegExp ===== regexp_new: (pattern, flags) => { try { const p = getString(pattern); const f = getString(flags); return nanboxPointer(allocHandle(new RegExp(p, f))); } catch (e) { if (tryDepth > 0) { currentException = fromJsValue(e); } return u64ToF64(TAG_UNDEFINED); } }, regexp_test: (regex, str) => { const re = getHandle(regex); if (!(re instanceof RegExp)) return 0; return re.test(getString(str)) ? 1 : 0; }, // ===== Phase 4: Globals ===== number_coerce: (val) => { const v = toJsValue(val); return Number(v); }, is_nan: (val) => Number.isNaN(val) ? 1 : 0, is_finite: (val) => Number.isFinite(val) ? 1 : 0, // ===== Phase 5: Misc ===== console_log_multi: (argsHandle) => { const args = getHandle(argsHandle); if (args) console.log(...args.map(toJsValue)); }, // ===== Phase 1 Addition: Class inheritance ===== class_set_parent: (childId, parentId) => { const child = getString(childId); const parent = getString(parentId); classParentTable[child] = parent; }, // ===== Phase 3: Try/Catch ===== try_start: () => { tryDepth++; }, try_end: () => { if (tryDepth > 0) tryDepth--; }, throw_value: (val) => { currentException = val; }, has_exception: () => currentException !== NO_EXCEPTION ? 1 : 0, get_exception: () => { const e = currentException; currentException = NO_EXCEPTION; return e !== NO_EXCEPTION ? e : u64ToF64(TAG_UNDEFINED); }, // ===== Phase 4: URL ===== url_parse: (urlStr) => { try { return nanboxPointer(allocHandle(new URL(getString(urlStr)))); } catch (e) { if (tryDepth > 0) { currentException = fromJsValue(e); } return u64ToF64(TAG_UNDEFINED); } }, url_get_href: (handle) => { const u = getHandle(handle); if (u instanceof URL) { stringTable.push(u.href); return nanboxString(stringTable.length - 1); } return u64ToF64(TAG_UNDEFINED); }, url_get_pathname: (handle) => { const u = getHandle(handle); if (u instanceof URL) { stringTable.push(u.pathname); return nanboxString(stringTable.length - 1); } return u64ToF64(TAG_UNDEFINED); }, url_get_hostname: (handle) => { const u = getHandle(handle); if (u instanceof URL) { stringTable.push(u.hostname); return nanboxString(stringTable.length - 1); } return u64ToF64(TAG_UNDEFINED); }, url_get_port: (handle) => { const u = getHandle(handle); if (u instanceof URL) { stringTable.push(u.port); return nanboxString(stringTable.length - 1); } return u64ToF64(TAG_UNDEFINED); }, url_get_search: (handle) => { const u = getHandle(handle); if (u instanceof URL) { stringTable.push(u.search); return nanboxString(stringTable.length - 1); } return u64ToF64(TAG_UNDEFINED); }, url_get_hash: (handle) => { const u = getHandle(handle); if (u instanceof URL) { stringTable.push(u.hash); return nanboxString(stringTable.length - 1); } return u64ToF64(TAG_UNDEFINED); }, url_get_origin: (handle) => { const u = getHandle(handle); if (u instanceof URL) { stringTable.push(u.origin); return nanboxString(stringTable.length - 1); } return u64ToF64(TAG_UNDEFINED); }, url_get_protocol: (handle) => { const u = getHandle(handle); if (u instanceof URL) { stringTable.push(u.protocol); return nanboxString(stringTable.length - 1); } return u64ToF64(TAG_UNDEFINED); }, url_get_search_params: (handle) => { const u = getHandle(handle); if (u instanceof URL) return nanboxPointer(allocHandle(u.searchParams)); return u64ToF64(TAG_UNDEFINED); }, searchparams_get: (handle, key) => { const sp = getHandle(handle); if (!sp || typeof sp.get !== 'function') return u64ToF64(TAG_NULL); const v = sp.get(getString(key)); if (v === null) return u64ToF64(TAG_NULL); stringTable.push(v); return nanboxString(stringTable.length - 1); }, searchparams_has: (handle, key) => { const sp = getHandle(handle); return (sp && typeof sp.has === 'function' && sp.has(getString(key))) ? 1 : 0; }, searchparams_set: (handle, key, val) => { const sp = getHandle(handle); if (sp && typeof sp.set === 'function') sp.set(getString(key), getString(val)); }, searchparams_append: (handle, key, val) => { const sp = getHandle(handle); if (sp && typeof sp.append === 'function') sp.append(getString(key), getString(val)); }, searchparams_delete: (handle, key) => { const sp = getHandle(handle); if (sp && typeof sp.delete === 'function') sp.delete(getString(key)); }, searchparams_to_string: (handle) => { const sp = getHandle(handle); stringTable.push(sp ? sp.toString() : ''); return nanboxString(stringTable.length - 1); }, // ===== Phase 4: Crypto ===== crypto_random_uuid: () => { stringTable.push(crypto.randomUUID()); return nanboxString(stringTable.length - 1); }, crypto_random_bytes: (n) => { return nanboxPointer(allocHandle(crypto.getRandomValues(new Uint8Array(n)))); }, // ===== Phase 4: Path ===== path_join: (a, b) => { const sa = getString(a), sb = getString(b); const joined = (sa + '/' + sb).replace(/\/+/g, '/'); stringTable.push(joined); return nanboxString(stringTable.length - 1); }, path_dirname: (str) => { const s = getString(str); const idx = s.lastIndexOf('/'); stringTable.push(idx >= 0 ? (s.substring(0, idx) || '/') : '.'); return nanboxString(stringTable.length - 1); }, path_basename: (str) => { const s = getString(str); const idx = s.lastIndexOf('/'); stringTable.push(idx >= 0 ? s.substring(idx + 1) : s); return nanboxString(stringTable.length - 1); }, path_extname: (str) => { const s = getString(str); const base = s.substring(s.lastIndexOf('/') + 1); const idx = base.lastIndexOf('.'); stringTable.push(idx > 0 ? base.substring(idx) : ''); return nanboxString(stringTable.length - 1); }, path_resolve: (str) => { stringTable.push(getString(str)); return nanboxString(stringTable.length - 1); }, // ===== Phase 4: Process/OS ===== os_platform: () => { stringTable.push('wasm'); return nanboxString(stringTable.length - 1); }, process_argv: () => nanboxPointer(allocHandle([])), process_cwd: () => { stringTable.push('/'); return nanboxString(stringTable.length - 1); }, // ===== Phase 6: Buffer/Uint8Array ===== buffer_alloc: (size) => nanboxPointer(allocHandle(new Uint8Array(size))), buffer_from_string: (str, encoding) => { const s = getString(str); return nanboxPointer(allocHandle(new TextEncoder().encode(s))); }, buffer_to_string: (handle, encoding) => { const buf = getHandle(handle); if (!buf) { stringTable.push(''); return nanboxString(stringTable.length - 1); } stringTable.push(new TextDecoder().decode(buf)); return nanboxString(stringTable.length - 1); }, buffer_get: (handle, idx) => { const buf = getHandle(handle); return buf ? buf[idx] : 0; }, buffer_set: (handle, idx, val) => { const buf = getHandle(handle); if (buf) buf[idx] = val; }, buffer_length: (handle) => { const buf = getHandle(handle); return buf ? buf.length : 0; }, buffer_slice: (handle, start, end) => { const buf = getHandle(handle); if (!buf) return nanboxPointer(allocHandle(new Uint8Array(0))); const e = isUndefined(end) ? undefined : end; return nanboxPointer(allocHandle(buf.slice(start, e))); }, buffer_concat: (arrHandle) => { const arr = getHandle(arrHandle); if (!arr || !Array.isArray(arr)) return nanboxPointer(allocHandle(new Uint8Array(0))); const bufs = arr.map(v => { const h = typeof v === 'number' ? getHandle(v) : v; return h instanceof Uint8Array ? h : new Uint8Array(0); }); const total = bufs.reduce((s, b) => s + b.length, 0); const result = new Uint8Array(total); let offset = 0; bufs.forEach(b => { result.set(b, offset); offset += b.length; }); return nanboxPointer(allocHandle(result)); }, uint8array_new: (size) => nanboxPointer(allocHandle(new Uint8Array(size))), uint8array_from: (val) => { const v = toJsValue(val); return nanboxPointer(allocHandle(Uint8Array.from(Array.isArray(v) ? v : []))); }, uint8array_length: (handle) => { const buf = getHandle(handle); return buf ? buf.length : 0; }, uint8array_get: (handle, idx) => { const buf = getHandle(handle); return buf ? buf[idx] : 0; }, uint8array_set: (handle, idx, val) => { const buf = getHandle(handle); if (buf) buf[idx] = val; }, // ===== Timers ===== set_timeout: (closureHandle, delay) => { const cb = getHandle(closureHandle); if (!cb || !wasmInstance) return 0; const id = setTimeout(() => { const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (fn) fn(...cb.captures); // captures are already BigInt }, delay); return id; }, set_interval: (closureHandle, delay) => { const cb = getHandle(closureHandle); if (!cb || !wasmInstance) return 0; const id = setInterval(() => { const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (fn) fn(...cb.captures); // captures are already BigInt }, delay); return id; }, clear_timeout: (id) => { clearTimeout(id); }, clear_interval: (id) => { clearInterval(id); }, // ===== Response properties ===== response_status: (handle) => { const r = getHandle(handle); return (r && typeof r.status === 'number') ? r.status : 0; }, response_ok: (handle) => { const r = getHandle(handle); return (r && r.ok) ? 1 : 0; }, response_headers_get: (handle, name) => { const r = getHandle(handle); if (!r || !r.headers) return u64ToF64(TAG_NULL); const v = r.headers.get(getString(name)); if (v === null) return u64ToF64(TAG_NULL); stringTable.push(v); return nanboxString(stringTable.length - 1); }, response_url: (handle) => { const r = getHandle(handle); if (!r || typeof r.url !== 'string') return u64ToF64(TAG_UNDEFINED); stringTable.push(r.url); return nanboxString(stringTable.length - 1); }, // ===== Buffer extras ===== buffer_copy: (source, target, targetStart, sourceStart, sourceEnd) => { const src = getHandle(source); const tgt = getHandle(target); if (!src || !tgt) return 0; const ts = isUndefined(targetStart) ? 0 : targetStart; const ss = isUndefined(sourceStart) ? 0 : sourceStart; const se = isUndefined(sourceEnd) ? src.length : sourceEnd; let copied = 0; for (let i = ss; i < se && (ts + copied) < tgt.length; i++) { tgt[ts + copied] = src[i]; copied++; } return copied; }, buffer_write: (handle, str, offset, encoding) => { const buf = getHandle(handle); if (!buf) return 0; const s = getString(str); const encoded = new TextEncoder().encode(s); const off = isUndefined(offset) ? 0 : offset; let written = 0; for (let i = 0; i < encoded.length && (off + i) < buf.length; i++) { buf[off + i] = encoded[i]; written++; } return written; }, buffer_equals: (handle, other) => { const a = getHandle(handle); const b = getHandle(other); if (!a || !b) return 0; if (a.length !== b.length) return 0; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return 0; } return 1; }, buffer_is_buffer: (val) => { if (!isPointer(val)) return 0; const obj = getHandle(val); return (obj instanceof Uint8Array || (obj && obj.constructor && obj.constructor.name === 'Buffer')) ? 1 : 0; }, buffer_byte_length: (val) => { if (isString(val)) return new TextEncoder().encode(getString(val)).length; if (isPointer(val)) { const obj = getHandle(val); if (obj instanceof Uint8Array) return obj.length; } return 0; }, // ===== Crypto extras ===== crypto_sha256: (data) => { const str = getString(data); const p = (typeof crypto !== 'undefined' && crypto.subtle) ? crypto.subtle.digest('SHA-256', new TextEncoder().encode(str)) .then(buf => { const hex = [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join(''); stringTable.push(hex); return nanboxString(stringTable.length - 1); }) : Promise.resolve(u64ToF64(TAG_UNDEFINED)); return nanboxPointer(allocHandle(p)); }, crypto_md5: (data) => { // MD5 not available in Web Crypto API; return undefined // Users should use SHA-256 instead return u64ToF64(TAG_UNDEFINED); }, // ===== Path extras ===== path_is_absolute: (str) => { const s = getString(str); return (s.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(s)) ? 1 : 0; }, // ===== Phase 5: Async/Promise/Fetch ===== fetch_url: (urlStr) => { const url = getString(urlStr); const p = fetch(url); return nanboxPointer(allocHandle(p)); }, fetch_with_options: (urlStr, methodVal, bodyVal, headersVal, redirectVal) => { const url = getString(urlStr); const opts = {}; if (!isUndefined(methodVal)) opts.method = getString(methodVal); if (!isUndefined(bodyVal)) opts.body = getString(bodyVal); if (isPointer(headersVal)) { const h = getHandle(headersVal); if (h && typeof h === 'object') opts.headers = h; } if (!isUndefined(redirectVal)) opts.redirect = getString(redirectVal); const p = fetch(url, opts); return nanboxPointer(allocHandle(p)); }, response_json: (handle) => { const resp = getHandle(handle); if (!resp || typeof resp.json !== 'function') return u64ToF64(TAG_UNDEFINED); const p = resp.json().then(v => fromJsValue(v)); return nanboxPointer(allocHandle(p)); }, response_text: (handle) => { const resp = getHandle(handle); if (!resp || typeof resp.text !== 'function') return u64ToF64(TAG_UNDEFINED); const p = resp.text().then(v => fromJsValue(v)); return nanboxPointer(allocHandle(p)); }, promise_new: () => { let resolve; const p = new Promise(r => { resolve = r; }); p.__resolve = resolve; return nanboxPointer(allocHandle(p)); }, promise_resolve: (handle, value) => { const p = getHandle(handle); if (p && p.__resolve) p.__resolve(toJsValue(value)); }, promise_then: (handle, closureHandle) => { const p = getHandle(handle); const cb = getHandle(closureHandle); if (!p || !(p instanceof Promise)) return handle; const newP = p.then(val => { if (cb && wasmInstance) { const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (fn) return u64ToF64(fn(...cb.captures, __jsValueToBits(val))); } return fromJsValue(val); }); return nanboxPointer(allocHandle(newP)); }, await_promise: (val) => { // In synchronous WASM context, we can't truly await. // If it's not a promise handle, return as-is. // If it IS a promise, return the handle (caller gets a promise handle back). // True awaiting happens in JS async functions. if (!isPointer(val)) return val; const obj = getHandle(val); if (obj instanceof Promise) { // Can't await synchronously; return the promise handle return val; } return val; }, // Memory-based bridge: ALL bridge function calls go through here. // Args are written to WASM memory at 0xFF00 as raw f64, preserving NaN payloads // that Firefox would canonicalize if passed as function parameters. // nameId = plain f64 string table index, argCount = number of f64 slots written. // Returns dummy f64 (0.0). Actual result is written to memory at 0xFF00. mem_call: (nameId, argCount, baseAddr) => { const name = stringTable[nameId | 0]; const argc = argCount | 0; const base = baseAddr | 0; const u64View = new BigUint64Array(wasmMemory.buffer, base, Math.max(argc, 1)); const args = []; for (let i = 0; i < argc; i++) args.push(__bitsToJsValue(u64View[i])); let result; const coreFn = __memDispatch[name]; try { if (coreFn) { result = coreFn(...args); } else { const uiFn = __perryUiDispatch[name]; if (uiFn) { result = uiFn(...args); } else if (argc > 0) { result = __classDispatch(args[0], name, args.slice(1)); } } } catch (error) { if (tryDepth === 0) throw error; currentException = error; } // Write result back to same base address const outU64 = new BigUint64Array(wasmMemory.buffer, base, 1); outU64[0] = __jsValueToBits(result); return 0; }, mem_call_i32: (nameId, argCount, baseAddr) => { const name = stringTable[nameId | 0]; const argc = argCount | 0; const base = baseAddr | 0; const u64View = new BigUint64Array(wasmMemory.buffer, base, Math.max(argc, 1)); const args = []; for (let i = 0; i < argc; i++) args.push(__bitsToJsValue(u64View[i])); const fn = __memDispatch[name]; if (!fn) return 0; try { return fn(...args) | 0; } catch (error) { if (tryDepth === 0) throw error; currentException = error; return 0; } }, // Async function implementations (merged from generated code) ...(typeof __asyncFuncImpls !== 'undefined' ? __asyncFuncImpls : {}), }, // FFI namespace: external native functions (e.g., bloom_init_window, bloom_draw_rect) // Wrapped in Proxy so missing imports are auto-stubbed with no-ops returning // NaN-boxed undefined as BigInt, allowing the WASM module to instantiate. ffi: new Proxy(typeof __ffiImports !== 'undefined' ? __ffiImports : {}, { get(target, prop) { if (prop in target) return target[prop]; return function ffiStub() { return TAG_UNDEFINED; }; }, }), }; } // rt-namespace imports whose WASM-declared return type is NOT i64. // `wrapForI64` MUST NOT BigInt-coerce Number returns for these — the // WASM caller expects an i32 or f64, and a BigInt return throws // "Cannot convert a BigInt value to a number" at the import boundary // (#1049 instances 2/3). Built from the type tables in `emit.rs`: // every entry with a `t_*_i32` signature plus the f64-return `mem_call`. const __NON_I64_RETURN_IMPORTS = new Set([ // i32 return 'string_eq', 'is_truthy', 'js_strict_eq', 'is_null_or_undefined', 'object_has_property', 'array_includes', 'array_is_array', 'string_includes', 'string_startsWith', 'string_endsWith', 'array_some', 'array_every', 'class_instanceof', 'map_has', 'set_has', 'regexp_test', 'is_nan', 'is_finite', 'has_exception', 'searchparams_has', 'response_ok', 'buffer_equals', 'buffer_is_buffer', 'path_is_absolute', // f64 return — dummy 0, real result is in WASM memory 'mem_call', // i32-return memory-bridge 'mem_call_i32', // void return — wrapper passes through `undefined`, but listing here // makes the contract explicit and guards against future changes to the // wrapper that might try to coerce `0` for a void callee. 'string_new', 'console_log', 'console_warn', 'console_error', 'console_log_multi', 'object_delete', 'object_delete_dynamic', 'array_unshift', 'array_forEach', 'try_start', 'try_end', 'throw_value', 'map_clear', 'map_delete', 'set_clear', 'set_add', 'set_delete', 'searchparams_set', 'searchparams_append', 'searchparams_delete', 'class_set_method', 'class_set_field', 'class_set_static', 'class_set_parent', 'object_set_dynamic', 'array_set', 'buffer_set', 'buffer_copy', 'buffer_write', 'uint8array_set', 'set_timeout', 'set_interval', 'clear_timeout', 'clear_interval', 'promise_resolve', ]); // Wrap a JS function so it interoperates with WASM i64 params/results. // // Default wrapping (used for the rt namespace): convert BigInt i64 args to f64 // via bit-reinterpret, so legacy rt.* functions that expect NaN-boxed f64 keep // working. Number results are coerced back to BigInt for WASM i64 return. // Necessary because BigInt(NaN) throws. Escape hatch: set `fn.__rawBigint = true` // on the callee to skip the wrapper entirely and receive raw BigInt bits. // // `coerceNumberToBigInt` (default true) is set to false at the wrap site for // imports whose declared WASM return is `i32`/`f64`/void — see // `__NON_I64_RETURN_IMPORTS`. Without this guard, #1049 instance 1's // `BigInt(result)` would mis-encode every i32-return import (is_truthy, // js_strict_eq, mem_call_i32, …) and the f64-return `mem_call`, throwing // "Cannot convert a BigInt value to a number" on the very first call // (instances 2/3 reproduce as soon as `_start` invokes `class_set_method` // via `mem_call`). function wrapForI64(fn, coerceNumberToBigInt = true) { if (fn.__rawBigint === true) return fn; return function(...args) { const convertedArgs = args.map(a => { if (typeof a === 'bigint') { _u64[0] = a; return _f64[0]; } return a; }); const result = fn.apply(this, convertedArgs); if (typeof result === 'bigint') return result; if (typeof result === 'number') { // #1049 instance 1: this wrapper exists because most callees here are // imported from a WASM site declared `i64`. A plain Number return — // even a small integer that JS would happily round-trip — throws // `TypeError: Cannot convert X to a BigInt` at the import boundary. // Coerce integers via `BigInt(n)` and non-integers (NaN-boxed f64 // payloads) via bit-reinterpret. Skipped for non-i64-return imports // (#1049 instances 2/3) — those declare i32/f64 returns and would // reject a BigInt with the inverse error. if (coerceNumberToBigInt && Number.isInteger(result) && Math.abs(result) < 2147483648) { return BigInt(result); } if (!coerceNumberToBigInt) return result; _f64[0] = result; return _u64[0]; } return result; }; } // Wrapper for the ffi namespace: FFI imports are wasm-bindgen-generated functions // and similar externals that expect PLAIN JS values, not NaN-boxed f64. Decode // BigInt i64 args (NaN-box bits) to JS values via __bitsToJsValue before calling. // Fixes Issue #133 items 3/5/6: Firefox/Safari canonicalize foreign NaNs when a // Number crosses a JS function-call boundary, stripping STRING/POINTER/INT32 tags, // so bit-reinterpreting BigInt → f64 here would lose the payload. Decoding bypasses // the f64 round-trip entirely and also gives wasm-bindgen a plain Number for // INT32-tagged values (avoiding `Cannot convert BigInt value to a number`). function wrapFfiForI64(fn) { if (fn.__rawBigint === true) return fn; return function(...args) { const convertedArgs = args.map(a => (typeof a === 'bigint') ? __bitsToJsValue(a) : a); const result = fn.apply(this, convertedArgs); if (typeof result === 'bigint') return result; if (typeof result === 'number') { // #1049 instance 1: the WASM import side declares these as `i64` // returns (handles, NaN-box bits, etc.). A plain Number return — // even a small integer JS could round-trip — throws // `TypeError: Cannot convert X to a BigInt` at the import // boundary. NaN-box the Number via f64 bit-reinterpret for ALL // integers (not just non-integers): a raw `BigInt(1)` round-trips // as i64=1 which Perry then mis-reads as f64=5e-324 inside the WASM // body (#1049 instance 3 with `add_numbers` returning 7 surfacing // as `3.5e-323`). NaN-boxing via __jsValueToBits encodes the value // as a proper JSValue so downstream WASM code that reinterprets // i64 → f64 sees the original Number. return __jsValueToBits(result); } // Non-numeric result: encode via __jsValueToBits so callers that thread results // back into Perry get proper NaN-box bits. if (result === undefined) return TAG_UNDEFINED; return __jsValueToBits(result); }; } function wrapNamespace(ns, wrapper) { return new Proxy(ns, { get(target, prop) { const v = target[prop]; if (typeof v === 'function') return wrapper(v); return v; }, }); } // rt-namespace Proxy: pass each import's name into `wrapForI64` so it knows // whether to BigInt-coerce Number returns. Names listed in // `__NON_I64_RETURN_IMPORTS` have a non-i64 WASM-declared return (#1049 // instances 2/3) and must pass Numbers through unchanged. function wrapRtNamespace(ns) { return new Proxy(ns, { get(target, prop) { const v = target[prop]; if (typeof v === 'function') { const coerce = !__NON_I64_RETURN_IMPORTS.has(prop); return wrapForI64(v, coerce); } return v; }, }); } function wrapImportsForI64(imports) { const wrapped = {}; for (const nsName in imports) { if (nsName === 'ffi') { wrapped[nsName] = wrapNamespace(imports[nsName], wrapFfiForI64); } else { wrapped[nsName] = wrapRtNamespace(imports[nsName]); } } return wrapped; } // A Perry closure is a `{funcIdx, captures}` record, not a JS function. This set marks // the records closure_new makes, so `typeof` and a call through an object property // (`o.h()`) can tell a closure from a plain object that merely has those keys. const __perryClosures = new WeakSet(); function __newClosure(funcIdx, captureCount) { const closure = { funcIdx: funcIdx, captures: new Array(captureCount | 0) }; __perryClosures.add(closure); return closure; } function __isPerryClosure(v) { return __perryClosures.has(v); } function __typeOf(v) { return __isPerryClosure(v) ? 'function' : typeof v; } // Class method/static/parent tables const classMethodTable = {}; const classStaticTable = {}; const classParentTable = {}; // Core bridge dispatch table — maps bridge function names to their implementations. // Args are decoded from NaN-boxed BigInt bits to plain JS values by __bitsToJsValue before dispatch. // Return values are plain JS values, converted back to BigInt bits by __jsValueToBits in mem_call. // The class name is non-enumerable, so Object.keys, for-in, spread and // JSON.stringify see only the fields the program declared. function __classInstance(className) { return Object.defineProperty({}, "__class__", { value: className, writable: true, configurable: true }); } // A lowered sync generator drives its resume record, a wasm closure taking // `(op, arg)` (op 0 next, 1 return, 2 throw), or `(arg)` when it has no catch, // finally or `yield*`. The record's first five captures are the cells it shares // with this driver: state, done, sent, executing, and step (how the call ended: // 0 yield, 1 done, 2 an inner iterator result returned as-is). const __generatorRecords = new WeakMap(); function __generatorResume(gen, op, arg, method) { const entry = __generatorRecords.get(gen); if (!entry) throw new TypeError(`Generator.prototype.${method} called on incompatible receiver`); const { record, nextOnly } = entry; const [state, done, sent, executing, step] = record.captures.slice(0, 5).map(__bitsToJsValue); if (executing.value) throw new TypeError("Generator is already executing"); if (done.value || (op !== 0 && (nextOnly || state.value === 0))) { done.value = true; if (op === 2) throw arg; step.value = 1; return { value: op === 1 ? arg : undefined, done: true }; } executing.value = true; if (op === 0) sent.value = arg; let value; try { const fn = wasmInstance.exports.__indirect_function_table.get(record.funcIdx | 0); const args = (nextOnly ? [arg] : [op, arg]).map(__jsValueToBits); value = __bitsToJsValue(__invokeWasmCallback(fn, ...record.captures, ...args)); } catch (error) { done.value = true; throw error; } finally { executing.value = false; } return step.value === 2 ? value : { value, done: step.value === 1 }; } const __GeneratorPrototype = Object.create( Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())), { next: { value(v) { return __generatorResume(this, 0, v, "next"); }, writable: true, configurable: true }, return: { value(v) { return __generatorResume(this, 1, v, "return"); }, writable: true, configurable: true }, throw: { value(e) { return __generatorResume(this, 2, e, "throw"); }, writable: true, configurable: true }, [Symbol.toStringTag]: { value: "Generator", configurable: true }, }, ); // The `__perry_runtime` iterator-record intrinsics the HIR lowers for...of and array // destructuring to (perry-hir/src/iterator_record.rs). `iteratorStep` returns `done` // and leaves the value for the `iteratorStepOutput()` read emitted right after it. const __ArrayIteratorPrototype = Object.getPrototypeOf([][Symbol.iterator]()); const __intrinsicArrayValues = Array.prototype[Symbol.iterator]; const __intrinsicArrayIteratorNext = __ArrayIteratorPrototype.next; const __accessorMarker = {}; let __iteratorStepValue; // The data value `key` resolves to on `obj`'s chain; an accessor is never invoked. function __lookupData(obj, key) { for (let o = obj; o !== null && o !== undefined; o = Object.getPrototypeOf(o)) { const d = Object.getOwnPropertyDescriptor(o, key); if (d) return "value" in d ? d.value : __accessorMarker; } return undefined; } function __arrayIterationPatched() { return __lookupData(Array.prototype, Symbol.iterator) !== __intrinsicArrayValues || __lookupData(__ArrayIteratorPrototype, "next") !== __intrinsicArrayIteratorNext; } function __isObject(v) { return v !== null && (typeof v === "object" || typeof v === "function"); } function __iteratorClose(iter) { const ret = iter.return; if (ret === undefined || ret === null) return; if (typeof ret !== "function") throw new TypeError(`${String(ret)} is not a function`); const result = ret.call(iter); if (!__isObject(result)) throw new TypeError(`Iterator result ${String(result)} is not an object`); } function __iteratorStep(iter, next, read) { if (typeof next !== "function") throw new TypeError(`${String(next)} is not a function`); const result = next.call(iter); if (!__isObject(result)) throw new TypeError(`Iterator result ${String(result)} is not an object`); const done = Boolean(result.done); __iteratorStepValue = !done && read !== false ? result.value : undefined; return done; } function __iterableToArray(v) { if (Array.isArray(v)) return v; if (v !== null && v !== undefined && typeof v[Symbol.iterator] === "function") return Array.from(v); const out = []; if (__isObject(v)) { const next = v.next; while (!__iteratorStep(v, next, true)) out.push(__iteratorStepValue); } return out; } const __memDispatch = { // `()` asks only whether array iteration is patched; `(value[, packed])` whether // `value` must use the iterator protocol instead of its indices. rt_arrayRecordNeedsIterator: (...args) => args.length === 0 ? __arrayIterationPatched() : !(Array.isArray(args[0]) && Object.getPrototypeOf(args[0]) === Array.prototype && __lookupData(args[0], Symbol.iterator) === __intrinsicArrayValues && __lookupData(__ArrayIteratorPrototype, "next") === __intrinsicArrayIteratorNext), rt_arrayRecordLength: (array) => array.length, // A numeric typed array may be walked by index (HIR `typedArrayForOfAdmits`). rt_typedArrayForOfAdmits: (v) => ArrayBuffer.isView(v) && !(v instanceof DataView) && !(v instanceof BigInt64Array) && !(v instanceof BigUint64Array), rt_arrayRecordLiteral: (...values) => values, rt_arrayRecordCloseAbsent: () => !("return" in __ArrayIteratorPrototype), // ponytail: closes a fresh intrinsic iterator over `src`; JS cannot place its cursor at `index`. rt_arrayRecordClose: (src, index, done, error, throwing) => { if (done || !("return" in __ArrayIteratorPrototype)) return throwing ? error : undefined; const iter = __intrinsicArrayValues.call(src); if (!throwing) return __iteratorClose(iter); try { __iteratorClose(iter); } catch (_) {} return error; }, rt_iteratorNextMethod: (iter) => iter.next, rt_iteratorStep: (iter, next, read) => __iteratorStep(iter, next, read), rt_iteratorStepOutput: () => __iteratorStepValue, rt_iteratorCloseIfNotDone: (iter, done) => { if (!done) __iteratorClose(iter); }, rt_iteratorCloseOnThrow: (iter, done, error) => { if (!done) { try { __iteratorClose(iter); } catch (_) {} } return error; }, rt_iteratorRestAppend: (array, value) => { array.push(value); return array; }, rt_iteratorRestToArray: (iter, next, done) => { const out = []; if (!done) while (!__iteratorStep(iter, next, true)) out.push(__iteratorStepValue); return out; }, rt_getIterator: (v) => v !== null && v !== undefined && typeof v[Symbol.iterator] === "function" ? v[Symbol.iterator]() : v, rt_iterableToArray: (v) => __iterableToArray(v), rt_arrayIterationPatched: () => __arrayIterationPatched(), generator_new: (record, paramCount) => { const gen = Object.create(__GeneratorPrototype); __generatorRecords.set(gen, { record, nextOnly: paramCount === 1 }); return gen; }, // A cell holds a local that closures capture and mutate, so all of them share it. box_new: (value) => ({ value }), box_get: (cell) => cell.value, box_set: (cell, value) => { cell.value = value; return value; }, // Console — args are already decoded by __bitsToJsValue console_log: (...args) => { console.log(...args); }, console_warn: (...args) => { console.warn(...args); }, console_error: (...args) => { console.error(...args); }, console_log_multi: (args) => { if (Array.isArray(args)) console.log(...args); }, // String core — args are already decoded JS values via __bitsToJsValue string_concat: (a, b) => String(a) + String(b), js_add: (a, b) => a + b, string_eq: (a, b) => a === b ? 1 : 0, string_len: (val) => { if (typeof val === 'string') return val.length; if (Array.isArray(val)) return val.length; return 0; }, jsvalue_to_string: (val) => String(val), jsvalue_to_template_string: (val) => `${val}`, is_truthy: (val) => { if (val === false || val === null || val === undefined || val === 0 || val === '') return 0; if (Number.isNaN(val)) return 0; return 1; }, js_strict_eq: (a, b) => a === b ? 1 : 0, // `==` / `!=`: the emitter calls this for CompareOp::LooseEq / LooseNe. js_loose_eq: (a, b) => a == b ? 1 : 0, // Math math_floor: (x) => Math.floor(x), math_ceil: (x) => Math.ceil(x), math_round: (x) => Math.round(x), math_abs: (x) => Math.abs(x), math_sqrt: (x) => Math.sqrt(x), math_pow: (base, exp) => Math.pow(base, exp), math_random: () => Math.random(), math_log: (x) => Math.log(x), math_log2: (x) => Math.log2(x), math_log10: (x) => Math.log10(x), math_min: (a, b) => Math.min(a, b), math_max: (a, b) => Math.max(a, b), // Trig / exp / sign / trunc / cbrt / hypot / etc. (Issue #133 item 4) math_sin: (x) => Math.sin(x), math_cos: (x) => Math.cos(x), math_tan: (x) => Math.tan(x), math_asin: (x) => Math.asin(x), math_acos: (x) => Math.acos(x), math_atan: (x) => Math.atan(x), math_atan2: (y, x) => Math.atan2(y, x), math_sinh: (x) => Math.sinh(x), math_cosh: (x) => Math.cosh(x), math_tanh: (x) => Math.tanh(x), math_asinh: (x) => Math.asinh(x), math_acosh: (x) => Math.acosh(x), math_atanh: (x) => Math.atanh(x), math_exp: (x) => Math.exp(x), math_expm1: (x) => Math.expm1(x), math_log1p: (x) => Math.log1p(x), math_sign: (x) => Math.sign(x), math_trunc: (x) => Math.trunc(x), math_cbrt: (x) => Math.cbrt(x), math_hypot: (a, b) => Math.hypot(a, b), math_fround: (x) => Math.fround(x), math_clz32: (x) => Math.clz32(x), math_imul: (a, b) => Math.imul(a, b), date_now: () => Date.now(), js_typeof: (val) => __typeOf(val), parse_int: (val) => parseInt(String(val), 10), parse_float: (val) => parseFloat(String(val)), js_mod: (a, b) => a % b, is_null_or_undefined: (val) => (val === null || val === undefined) ? 1 : 0, // Objects — args are plain JS values (obj is the object itself, key is a string, etc.) object_new: () => ({}), object_set: (obj, key, value) => { if (obj && typeof obj === 'object') obj[String(key)] = value; return obj; }, object_get: (obj, key) => { if (!obj || typeof obj !== 'object') return undefined; return obj[String(key)]; }, object_get_dynamic: (obj, key) => { if (obj === null || obj === undefined) __throwNullReceiver(obj, key); if (!obj || typeof obj !== 'object') return undefined; return obj[key]; }, object_set_dynamic: (obj, key, value) => { if (obj && typeof obj === 'object') obj[key] = value; }, object_delete: (obj, key) => { if (obj && typeof obj === 'object') delete obj[String(key)]; }, object_delete_dynamic: (obj, key) => { if (obj && typeof obj === 'object') delete obj[key]; }, object_keys: (obj) => obj && typeof obj === 'object' ? Object.keys(obj) : [], object_values: (obj) => obj && typeof obj === 'object' ? Object.keys(obj).map(k => obj[k]) : [], object_entries: (obj) => obj && typeof obj === 'object' ? Object.entries(obj) : [], object_has_property: (obj, key) => { if (!obj || typeof obj !== 'object') return 0; return (key in obj) ? 1 : 0; }, object_assign: (target, source) => { if (target == null) throw new TypeError('Cannot convert undefined or null to object'); const t = Object(target); if (source != null) Object.assign(t, source); return t; }, // Arrays — args are plain JS values (arr is the array itself, etc.) array_new: () => [], // `new Array(x)`: single number = length (ES2015 §22.1.1), else element. array_constructor_single: (value) => { if (typeof value === 'number') { if (!Number.isInteger(value) || value < 0 || value > 0xFFFFFFFF) { throw new RangeError('Invalid array length'); } return new Array(value); } return [value]; }, array_push: (arr, value) => { if (Array.isArray(arr)) arr.push(value); return arr; }, array_pop: (arr) => { if (!Array.isArray(arr) || arr.length === 0) return undefined; return arr.pop(); }, array_get: (arr, index) => { if (!Array.isArray(arr)) return undefined; return arr[typeof index === 'number' ? index : index]; }, array_set: (arr, index, value) => { if (Array.isArray(arr)) arr[typeof index === 'number' ? index : index] = value; }, array_length: (arr) => Array.isArray(arr) ? arr.length : 0, array_slice: (arr, start, end) => { if (!Array.isArray(arr)) return []; const s = typeof start === 'number' ? start : 0; const e = end === undefined ? undefined : (typeof end === 'number' ? end : end); return arr.slice(s, e); }, array_splice: (arr, start, deleteCount, ...items) => { if (!Array.isArray(arr)) return []; const s = typeof start === 'number' ? start : 0; const dc = deleteCount === undefined ? arr.length - s : deleteCount; // Non-numeric start must reach splice uncoerced: JS ToIntegerOrInfinity // coerces "1" to 1, while the pre-coerced 0 silently shifts insertions. return arr.splice(typeof start === 'number' ? s : start, dc, ...items); }, array_shift: (arr) => { if (!Array.isArray(arr) || arr.length === 0) return undefined; return arr.shift(); }, array_unshift: (arr, value) => { if (Array.isArray(arr)) arr.unshift(value); }, array_join: (arr, separator) => { if (!Array.isArray(arr)) return ''; const sep = String(separator); return arr.map(v => typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v)).join(sep); }, array_index_of: (arr, value) => { if (!Array.isArray(arr)) return -1; return arr.indexOf(value); }, array_includes: (arr, value) => { if (!Array.isArray(arr)) return 0; return arr.includes(value) ? 1 : 0; }, array_concat: (a1, a2) => (Array.isArray(a1) ? a1 : []).concat(Array.isArray(a2) ? a2 : []), array_reverse: (arr) => { if (Array.isArray(arr)) arr.reverse(); return arr; }, array_flat: (arr) => Array.isArray(arr) ? arr.flat() : [], array_is_array: (val) => Array.isArray(val) ? 1 : 0, array_from: (val) => Array.from(val), array_push_spread: (target, source) => { if (Array.isArray(target) && Array.isArray(source)) target.push(...source); return target; }, // String methods — args are plain JS strings string_charAt: (str, idx) => String(str).charAt(idx), string_substring: (str, start, end) => String(str).substring(start, end), string_indexOf: (str, search) => String(str).indexOf(String(search)), string_slice: (str, start, end) => String(str).slice(start, end), string_toLowerCase: (str) => String(str).toLowerCase(), string_toUpperCase: (str) => String(str).toUpperCase(), string_trim: (str) => String(str).trim(), string_includes: (str, search) => String(str).includes(String(search)) ? 1 : 0, string_startsWith: (str, search) => String(str).startsWith(String(search)) ? 1 : 0, string_endsWith: (str, search) => String(str).endsWith(String(search)) ? 1 : 0, string_replace: (str, pattern, replacement) => { const s = String(str); const p = (pattern instanceof RegExp) ? pattern : String(pattern); return s.replace(p, String(replacement)); }, string_split: (str, delim) => String(str).split(String(delim)), string_fromCharCode: (code) => String.fromCharCode(code), string_padStart: (str, len, fill) => String(str).padStart(len, String(fill)), string_padEnd: (str, len, fill) => String(str).padEnd(len, String(fill)), string_repeat: (str, count) => String(str).repeat(count), string_match: (str, regex) => { const s = String(str); const re = (regex instanceof RegExp) ? regex : new RegExp(String(regex)); const result = s.match(re); if (!result) return null; return Array.from(result); }, // Closures — WASM functions now use i64 params/returns (BigInt in JS). // Captures are stored as BigInt (i64 NaN-boxed bits). // __jsValueToBits converts JS values to BigInt for WASM calls. // __bitsToJsValue converts BigInt return values back to JS. closure_new: (funcIdx, captureCount) => __newClosure(funcIdx, captureCount), closure_set_capture: (closure, idx, value) => { if (closure) closure.captures[idx | 0] = __jsValueToBits(value); return closure; }, closure_call_0: (closure) => { if (typeof closure === 'function') return __callHostFunction(closure, []); if (!closure || typeof closure.funcIdx === 'undefined' || !wasmInstance) return undefined; const fn = wasmInstance.exports.__indirect_function_table?.get(closure.funcIdx | 0); if (!fn) return undefined; // Pad for omitted TS optional params (Issue #1035). const padded = __padBigintArgs(fn, closure.captures.length, []); return __bitsToJsValue(fn(...closure.captures, ...padded)); }, closure_call_1: (closure, a0) => { if (typeof closure === 'function') return __callHostFunction(closure, [a0]); if (!closure || typeof closure.funcIdx === 'undefined' || !wasmInstance) return undefined; const fn = wasmInstance.exports.__indirect_function_table?.get(closure.funcIdx | 0); if (!fn) return undefined; const padded = __padBigintArgs(fn, closure.captures.length, [__jsValueToBits(a0)]); return __bitsToJsValue(fn(...closure.captures, ...padded)); }, closure_call_2: (closure, a0, a1) => { if (typeof closure === 'function') return __callHostFunction(closure, [a0, a1]); if (!closure || typeof closure.funcIdx === 'undefined' || !wasmInstance) return undefined; const fn = wasmInstance.exports.__indirect_function_table?.get(closure.funcIdx | 0); if (!fn) return undefined; const padded = __padBigintArgs(fn, closure.captures.length, [__jsValueToBits(a0), __jsValueToBits(a1)]); return __bitsToJsValue(fn(...closure.captures, ...padded)); }, closure_call_3: (closure, a0, a1, a2) => { if (typeof closure === 'function') return __callHostFunction(closure, [a0, a1, a2]); if (!closure || typeof closure.funcIdx === 'undefined' || !wasmInstance) return undefined; const fn = wasmInstance.exports.__indirect_function_table?.get(closure.funcIdx | 0); if (!fn) return undefined; const padded = __padBigintArgs(fn, closure.captures.length, [__jsValueToBits(a0), __jsValueToBits(a1), __jsValueToBits(a2)]); return __bitsToJsValue(fn(...closure.captures, ...padded)); }, // `new callee(...args)` with a callee known only at run time (`new Map()` reaches here // through `global_property`). A host constructor must be constructed: called without // `new`, Map / Set / URLSearchParams throw. A Perry closure keeps the plain call the // closure_call_N bridges make, for any number of arguments. new_dynamic: (callee, ...args) => { if (typeof callee === 'function') return __constructHostFunction(callee, args); if (!callee || typeof callee.funcIdx === 'undefined' || !wasmInstance) return undefined; const fn = wasmInstance.exports.__indirect_function_table?.get(callee.funcIdx | 0); if (!fn) return undefined; const padded = __padBigintArgs(fn, callee.captures.length, args.map(__jsValueToBits)); return __bitsToJsValue(fn(...callee.captures, ...padded)); }, closure_call_spread: (closure, args) => { const argArr = Array.isArray(args) ? args : []; if (typeof closure === 'function') return __callHostFunction(closure, argArr); if (!closure || typeof closure.funcIdx === 'undefined' || !wasmInstance) return undefined; const fn = wasmInstance.exports.__indirect_function_table?.get(closure.funcIdx | 0); if (!fn) return undefined; const padded = __padBigintArgs(fn, closure.captures.length, argArr.map(v => __jsValueToBits(v))); return __bitsToJsValue(fn(...closure.captures, ...padded)); }, // Array higher-order methods — WASM callbacks use i64 (BigInt) params/returns. array_map: (arr, cb) => { if (!Array.isArray(arr) || !cb || typeof cb.funcIdx === 'undefined' || !wasmInstance) return []; const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) return []; return arr.map((v, i) => __bitsToJsValue(__invokeWasmCallback(fn, ...cb.captures, __jsValueToBits(v), __jsValueToBits(i), __jsValueToBits(arr)))); }, array_filter: (arr, cb) => { if (!Array.isArray(arr) || !cb || typeof cb.funcIdx === 'undefined' || !wasmInstance) return []; const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) return []; return arr.filter((v, i) => { const r = __invokeWasmCallback(fn, ...cb.captures, __jsValueToBits(v), __jsValueToBits(i), __jsValueToBits(arr)); const rv = __bitsToJsValue(r); if (rv === true) return true; if (rv === false || rv === null || rv === undefined) return false; return !!rv; }); }, array_forEach: (arr, cb) => { if (!Array.isArray(arr) || !cb || typeof cb.funcIdx === 'undefined' || !wasmInstance) return; const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) return; arr.forEach((v, i) => __invokeWasmCallback(fn, ...cb.captures, __jsValueToBits(v), __jsValueToBits(i), __jsValueToBits(arr))); }, array_reduce: (arr, cb, initial) => { if (!Array.isArray(arr) || !cb || typeof cb.funcIdx === 'undefined' || !wasmInstance) return undefined; const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) return undefined; let acc = initial === undefined ? __jsValueToBits(arr[0]) : __jsValueToBits(initial); const startIdx = initial === undefined ? 1 : 0; for (let i = startIdx; i < arr.length; i++) acc = __invokeWasmCallback(fn, ...cb.captures, acc, __jsValueToBits(arr[i]), __jsValueToBits(i), __jsValueToBits(arr)); return __bitsToJsValue(acc); }, array_find: (arr, cb) => { if (!Array.isArray(arr) || !cb || typeof cb.funcIdx === 'undefined' || !wasmInstance) return undefined; const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) return undefined; for (let i = 0; i < arr.length; i++) { const vBits = __jsValueToBits(arr[i]); if (__bitsToJsValue(__invokeWasmCallback(fn, ...cb.captures, vBits, __jsValueToBits(i), __jsValueToBits(arr)))) return arr[i]; } return undefined; }, array_find_index: (arr, cb) => { if (!Array.isArray(arr) || !cb || typeof cb.funcIdx === 'undefined' || !wasmInstance) return -1; const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) return -1; for (let i = 0; i < arr.length; i++) { if (__bitsToJsValue(__invokeWasmCallback(fn, ...cb.captures, __jsValueToBits(arr[i]), __jsValueToBits(i), __jsValueToBits(arr)))) return i; } return -1; }, array_sort: (arr, cb) => { if (!Array.isArray(arr)) return arr; if (cb === undefined || cb === null) { arr.sort(); return arr; } if (!cb || typeof cb.funcIdx === 'undefined' || !wasmInstance) { arr.sort(); return arr; } const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) { arr.sort(); return arr; } arr.sort((a, b) => { const r = __invokeWasmCallback(fn, ...cb.captures, __jsValueToBits(a), __jsValueToBits(b)); return __bitsToJsValue(r); }); return arr; }, array_some: (arr, cb) => { if (!Array.isArray(arr) || !cb || typeof cb.funcIdx === 'undefined' || !wasmInstance) return 0; const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) return 0; return arr.some((v, i) => __bitsToJsValue(__invokeWasmCallback(fn, ...cb.captures, __jsValueToBits(v), __jsValueToBits(i), __jsValueToBits(arr)))) ? 1 : 0; }, array_every: (arr, cb) => { if (!Array.isArray(arr) || !cb || typeof cb.funcIdx === 'undefined' || !wasmInstance) return 0; const fn = wasmInstance.exports.__indirect_function_table?.get(cb.funcIdx | 0); if (!fn) return 0; return arr.every((v, i) => __bitsToJsValue(__invokeWasmCallback(fn, ...cb.captures, __jsValueToBits(v), __jsValueToBits(i), __jsValueToBits(arr)))) ? 1 : 0; }, // Classes — args are plain JS values. obj is the object, className/methodName are strings. // class_new returns a plain object carrying its class name (see __classInstance). // class_set_method/class_call_method interact with WASM indirect call table. class_new: (className, fieldCount) => __classInstance(String(className)), class_set_method: (classId, methodName, funcIdx) => { const cls = String(classId); if (!classMethodTable[cls]) classMethodTable[cls] = {}; classMethodTable[cls][String(methodName)] = funcIdx | 0; }, class_call_method: (obj, methodNameId, argsArr) => { // methodNameId is a plain number (string table index), not a decoded JS value const mname = stringTable[methodNameId | 0]; if (obj && obj.__class__) { let cls = obj.__class__; while (cls) { const methods = classMethodTable[cls]; if (methods && mname in methods) { const fn = wasmInstance?.exports.__indirect_function_table?.get(methods[mname]); if (fn) { const args = Array.isArray(argsArr) ? argsArr : []; // WASM functions use i64 (BigInt) params/returns. // Pad for omitted TS optional params (Issue #1035). const padded = __padBigintArgs(fn, 1, args.map(v => __jsValueToBits(v))); return __bitsToJsValue(fn(__jsValueToBits(obj), ...padded)); } } cls = classParentTable[cls] || null; } } // Fallback to UI method dispatch const uiMethodMap = { addChild: "perry_ui_widget_add_child", removeAllChildren: "perry_ui_widget_remove_all_children", setBackground: "perry_ui_set_background", setForeground: "perry_ui_set_foreground", setFontSize: "perry_ui_set_font_size", setFontWeight: "perry_ui_set_font_weight", setFontFamily: "perry_ui_set_font_family", setPadding: "perry_ui_set_padding", setFrame: "perry_ui_set_frame", setCornerRadius: "perry_ui_set_corner_radius", setBorder: "perry_ui_set_border", setOpacity: "perry_ui_set_opacity", setEnabled: "perry_ui_set_enabled", setTooltip: "perry_ui_set_tooltip", setControlSize: "perry_ui_set_control_size", animateOpacity: "perry_ui_animate_opacity", animatePosition: "perry_ui_animate_position", setOnClick: "perry_ui_set_on_click", setOnHover: "perry_ui_set_on_hover", setOnDoubleClick: "perry_ui_set_on_double_click", get: "perry_ui_state_get", set: "perry_ui_state_set", create: "perry_ui_state_create", bindText: "perry_ui_state_bind_text", bindTextNumeric: "perry_ui_state_bind_text_numeric", bindSlider: "perry_ui_state_bind_slider", bindToggle: "perry_ui_state_bind_toggle", bindVisibility: "perry_ui_state_bind_visibility", bindForEach: "perry_ui_state_bind_foreach", onChange: "perry_ui_state_on_change", setString: "perry_ui_text_set_string", setSelectable: "perry_ui_text_set_selectable", setBordered: "perry_ui_button_set_bordered", setTitle: "perry_ui_button_set_title", setTextColor: "perry_ui_button_set_text_color", setImage: "perry_ui_button_set_image", fillRect: "perry_ui_canvas_fill_rect", strokeRect: "perry_ui_canvas_stroke_rect", clearRect: "perry_ui_canvas_clear_rect", setFillColor: "perry_ui_canvas_set_fill_color", setStrokeColor: "perry_ui_canvas_set_stroke_color", beginPath: "perry_ui_canvas_begin_path", moveTo: "perry_ui_canvas_move_to", lineTo: "perry_ui_canvas_line_to", arc: "perry_ui_canvas_arc", closePath: "perry_ui_canvas_close_path", fill: "perry_ui_canvas_fill", stroke: "perry_ui_canvas_stroke", setLineWidth: "perry_ui_canvas_set_line_width", fillText: "perry_ui_canvas_fill_text", setFont: "perry_ui_canvas_set_font", drawImage: "perry_ui_canvas_draw_image", setChild: "perry_ui_scrollview_set_child", focus: "perry_ui_textfield_focus", setWidth: "perry_ui_widget_set_width", setHeight: "perry_ui_widget_set_height", matchParentWidth: "perry_ui_widget_match_parent_width", matchParentHeight: "perry_ui_widget_match_parent_height", setHidden: "perry_ui_set_widget_hidden", setEdgeInsets: "perry_ui_widget_set_edge_insets", setShadow: "perry_ui_widget_set_shadow", run: "perry_ui_app_run", setBody: "perry_ui_app_set_body", }; const uiFnName = uiMethodMap[mname]; if (uiFnName) { const fn = __perryUiDispatch[uiFnName]; if (!fn) return undefined; const args = Array.isArray(argsArr) ? argsArr : []; return fn(obj, ...args); } return undefined; }, class_get_field: (obj, name) => { if (obj === null || obj === undefined) __throwNullReceiver(obj, name); if (!obj || typeof obj !== 'object') return undefined; const fname = String(name); // Check for compiled getter method — WASM uses i64 (BigInt) if (obj.__class__) { let cls = obj.__class__; while (cls) { const methods = classMethodTable[cls]; if (methods && ('__get_' + fname) in methods) { const fn = wasmInstance?.exports.__indirect_function_table?.get(methods['__get_' + fname]); if (fn) return __bitsToJsValue(fn(__jsValueToBits(obj))); } cls = classParentTable[cls] || null; } // A compiled method read as a value (`o.m`, and the `o.m?.()` guard, which is // `o.m == null`): it lives in the WASM table, not on the object. if (!Object.prototype.hasOwnProperty.call(obj, fname)) { for (let c = obj.__class__; c; c = classParentTable[c] || null) { const methods = classMethodTable[c]; if (methods && Object.prototype.hasOwnProperty.call(methods, fname)) { return __classMethodValue(c, fname); } } } } return obj[fname]; }, class_set_field: (obj, name, value) => { if (!obj || typeof obj !== 'object') return; const fname = String(name); // Check for compiled setter method — WASM uses i64 (BigInt) if (obj.__class__) { let cls = obj.__class__; while (cls) { const methods = classMethodTable[cls]; if (methods && ('__set_' + fname) in methods) { const fn = wasmInstance?.exports.__indirect_function_table?.get(methods['__set_' + fname]); if (fn) { fn(__jsValueToBits(obj), __jsValueToBits(value)); return; } } cls = classParentTable[cls] || null; } } obj[fname] = value; }, class_set_static: (classId, name, value) => { const cls = String(classId); if (!classStaticTable[cls]) classStaticTable[cls] = {}; classStaticTable[cls][String(name)] = value; }, class_get_static: (classId, name) => { const cls = String(classId); const statics = classStaticTable[cls]; if (!statics) return undefined; return statics[String(name)]; }, class_instanceof: (obj, classId) => { if (!obj || typeof obj !== 'object') return 0; const target = String(classId); // A user-class instance is a `{__class__}` object: its chain is the class table, // and as a plain object it is also an instance of Object. if (obj.__class__) { let cls = obj.__class__; while (cls) { if (cls === target) return 1; cls = classParentTable[cls] || null; } return target === 'Object' ? 1 : 0; } // A host object (Error, Array, Map, ...): ask the host constructor of that name. const ctor = globalThis[target]; return typeof ctor === 'function' && obj instanceof ctor ? 1 : 0; }, class_set_parent: (childId, parentId) => { classParentTable[String(childId)] = String(parentId); }, // JSON — args are plain JS values json_parse: (str) => { try { return JSON.parse(String(str)); } catch (e) { if (tryDepth > 0) currentException = e; return undefined; } }, // A Perry closure arrives as {funcIdx, captures}; JSON.stringify needs a function. json_stringify: (val, replacer, space) => JSON.stringify(val, replacer && typeof replacer.funcIdx !== 'undefined' ? (key, value) => callWasmClosure(replacer, key, value) : replacer, space), // Map — args are plain JS values (m is the Map object itself) map_new: () => new Map(), map_set: (m, key, value) => { if (m instanceof Map) m.set(key, value); }, map_get: (m, key) => { if (!(m instanceof Map)) return undefined; return m.get(key); }, map_has: (m, key) => (m instanceof Map && m.has(key)) ? 1 : 0, map_delete: (m, key) => { if (m instanceof Map) m.delete(key); }, map_size: (m) => (m instanceof Map) ? m.size : 0, map_clear: (m) => { if (m instanceof Map) m.clear(); }, map_entries: (m) => (m instanceof Map) ? [...m.entries()] : [], map_keys: (m) => (m instanceof Map) ? [...m.keys()] : [], map_values: (m) => (m instanceof Map) ? [...m.values()] : [], // Set — args are plain JS values (s is the Set object itself) set_new: () => new Set(), set_new_from_array: (arr) => new Set(Array.isArray(arr) ? arr : []), set_add: (s, value) => { if (s instanceof Set) s.add(value); }, set_has: (s, value) => (s instanceof Set && s.has(value)) ? 1 : 0, set_delete: (s, value) => { if (s instanceof Set) s.delete(value); }, set_size: (s) => (s instanceof Set) ? s.size : 0, set_clear: (s) => { if (s instanceof Set) s.clear(); }, set_values: (s) => (s instanceof Set) ? [...s.values()] : [], // Date — arg is a plain JS value (the Date object itself once created) date_new: () => new Date(), date_new_val: (...args) => (args.length === 0) ? new Date() : new Date(args[0]), date_now: () => Date.now(), date_parse: (s) => Date.parse(String(s)), date_utc: (...args) => Date.UTC(...args.map(Number)), date_get_time: (d) => (d instanceof Date) ? d.getTime() : 0, date_value_of: (d) => (d instanceof Date) ? d.valueOf() : 0, date_to_iso_string: (d) => { if (!(d instanceof Date)) return undefined; return d.toISOString(); }, date_to_date_string: (d) => (d instanceof Date) ? d.toDateString() : '', date_to_time_string: (d) => (d instanceof Date) ? d.toTimeString() : '', date_to_locale_string: (d) => (d instanceof Date) ? d.toLocaleString() : '', date_to_locale_date_string: (d) => (d instanceof Date) ? d.toLocaleDateString() : '', date_to_locale_time_string: (d) => (d instanceof Date) ? d.toLocaleTimeString() : '', date_to_json: (d) => (d instanceof Date) ? d.toJSON() : null, date_get_full_year: (d) => (d instanceof Date) ? d.getFullYear() : 0, date_get_month: (d) => (d instanceof Date) ? d.getMonth() : 0, date_get_date: (d) => (d instanceof Date) ? d.getDate() : 0, date_get_day: (d) => (d instanceof Date) ? d.getDay() : 0, date_get_hours: (d) => (d instanceof Date) ? d.getHours() : 0, date_get_minutes: (d) => (d instanceof Date) ? d.getMinutes() : 0, date_get_seconds: (d) => (d instanceof Date) ? d.getSeconds() : 0, date_get_milliseconds: (d) => (d instanceof Date) ? d.getMilliseconds() : 0, date_get_utc_full_year: (d) => (d instanceof Date) ? d.getUTCFullYear() : 0, date_get_utc_month: (d) => (d instanceof Date) ? d.getUTCMonth() : 0, date_get_utc_date: (d) => (d instanceof Date) ? d.getUTCDate() : 0, date_get_utc_day: (d) => (d instanceof Date) ? d.getUTCDay() : 0, date_get_utc_hours: (d) => (d instanceof Date) ? d.getUTCHours() : 0, date_get_utc_minutes: (d) => (d instanceof Date) ? d.getUTCMinutes() : 0, date_get_utc_seconds: (d) => (d instanceof Date) ? d.getUTCSeconds() : 0, date_get_utc_milliseconds: (d) => (d instanceof Date) ? d.getUTCMilliseconds() : 0, date_get_timezone_offset: (d) => (d instanceof Date) ? d.getTimezoneOffset() : 0, date_set_utc_full_year: (d, v) => (d instanceof Date) ? d.setUTCFullYear(Number(v)) : 0, date_set_utc_month: (d, v) => (d instanceof Date) ? d.setUTCMonth(Number(v)) : 0, date_set_utc_date: (d, v) => (d instanceof Date) ? d.setUTCDate(Number(v)) : 0, date_set_utc_hours: (d, v) => (d instanceof Date) ? d.setUTCHours(Number(v)) : 0, date_set_utc_minutes: (d, v) => (d instanceof Date) ? d.setUTCMinutes(Number(v)) : 0, date_set_utc_seconds: (d, v) => (d instanceof Date) ? d.setUTCSeconds(Number(v)) : 0, date_set_utc_milliseconds: (d, v) => (d instanceof Date) ? d.setUTCMilliseconds(Number(v)) : 0, // Error — args are plain JS values error_new: (msg) => new Error(msg === undefined ? undefined : String(msg)), error_message: (e) => (e instanceof Error) ? e.message : '', global_receiver: () => __hostGlobal, global_property: (name) => __hostGlobal[name], // RegExp — args are plain JS values regexp_new: (pattern, flags) => { try { return new RegExp(String(pattern), String(flags)); } catch (e) { if (tryDepth > 0) currentException = e; return undefined; } }, regexp_test: (re, str) => { if (!(re instanceof RegExp)) return 0; return re.test(String(str)) ? 1 : 0; }, // Globals — args are plain JS values number_coerce: (val) => Number(val), is_nan: (val) => Number.isNaN(val) ? 1 : 0, is_finite: (val) => Number.isFinite(val) ? 1 : 0, number_is_integer: (val) => Number.isInteger(val) ? 1 : 0, number_is_safe_integer: (val) => Number.isSafeInteger(val) ? 1 : 0, global_is_nan: (val) => isNaN(val) ? 1 : 0, global_is_finite: (val) => isFinite(val) ? 1 : 0, // A malformed sequence throws URIError, which mem_call routes to an enclosing try. encode_uri: (val) => encodeURI(val), decode_uri: (val) => decodeURI(val), encode_uri_component: (val) => encodeURIComponent(val), decode_uri_component: (val) => decodeURIComponent(val), // Try/Catch — args are plain JS values try_start: () => { tryDepth++; }, try_end: () => { if (tryDepth > 0) tryDepth--; }, throw_value: (val) => { currentException = val; }, propagate_exception: (callbackBoundary = false) => { if ((callbackBoundary || tryDepth === 0) && currentException !== NO_EXCEPTION) { const error = currentException; currentException = NO_EXCEPTION; throw error; } }, has_exception: () => currentException !== NO_EXCEPTION ? 1 : 0, get_exception: () => { const e = currentException; currentException = NO_EXCEPTION; return e !== NO_EXCEPTION ? e : undefined; }, // URL — args are plain JS values (u is the URL object itself) url_parse: (urlStr) => { try { return new URL(String(urlStr)); } catch (e) { if (tryDepth > 0) currentException = e; return undefined; } }, url_get_href: (u) => (u instanceof URL) ? u.href : undefined, url_get_pathname: (u) => (u instanceof URL) ? u.pathname : undefined, url_get_hostname: (u) => (u instanceof URL) ? u.hostname : undefined, url_get_port: (u) => (u instanceof URL) ? u.port : undefined, url_get_search: (u) => (u instanceof URL) ? u.search : undefined, url_get_hash: (u) => (u instanceof URL) ? u.hash : undefined, url_get_origin: (u) => (u instanceof URL) ? u.origin : undefined, url_get_protocol: (u) => (u instanceof URL) ? u.protocol : undefined, url_get_search_params: (u) => (u instanceof URL) ? u.searchParams : undefined, searchparams_get: (sp, key) => { if (!sp || typeof sp.get !== 'function') return null; const v = sp.get(String(key)); return v === null ? null : v; }, searchparams_has: (sp, key) => (sp && typeof sp.has === 'function' && sp.has(String(key))) ? 1 : 0, searchparams_set: (sp, key, val) => { if (sp && typeof sp.set === 'function') sp.set(String(key), String(val)); }, searchparams_append: (sp, key, val) => { if (sp && typeof sp.append === 'function') sp.append(String(key), String(val)); }, searchparams_delete: (sp, key) => { if (sp && typeof sp.delete === 'function') sp.delete(String(key)); }, searchparams_to_string: (sp) => sp ? sp.toString() : '', // Crypto — args are plain JS values crypto_random_uuid: () => crypto.randomUUID(), crypto_random_uuidv7: () => crypto.randomUUIDv7(), crypto_random_bytes: (n) => crypto.getRandomValues(new Uint8Array(n)), crypto_sha256: (data) => { const str = String(data); const p = (typeof crypto !== 'undefined' && crypto.subtle) ? crypto.subtle.digest('SHA-256', new TextEncoder().encode(str)) .then(buf => [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('')) : Promise.resolve(undefined); return p; }, crypto_md5: () => undefined, // Path — args are plain JS strings path_join: (a, b) => (String(a) + '/' + String(b)).replace(/\/+/g, '/'), path_dirname: (str) => { const s = String(str); const idx = s.lastIndexOf('/'); return idx >= 0 ? (s.substring(0, idx) || '/') : '.'; }, path_basename: (str) => { const s = String(str); const idx = s.lastIndexOf('/'); return idx >= 0 ? s.substring(idx + 1) : s; }, path_extname: (str) => { const s = String(str); const base = s.substring(s.lastIndexOf('/') + 1); const idx = base.lastIndexOf('.'); return idx > 0 ? base.substring(idx) : ''; }, path_resolve: (str) => String(str), path_is_absolute: (str) => { const s = String(str); return (s.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(s)) ? 1 : 0; }, // Process/OS — return plain JS values os_platform: () => 'wasm', process_argv: () => [], process_cwd: () => '/', // Buffer/Uint8Array — args are plain JS values (buf is the Uint8Array itself) buffer_alloc: (size) => new Uint8Array(size), buffer_from_string: (str, encoding) => new TextEncoder().encode(String(str)), buffer_to_string: (buf, encoding) => { if (!buf || !(buf instanceof Uint8Array)) return ''; return new TextDecoder().decode(buf); }, buffer_get: (buf, idx) => (buf instanceof Uint8Array) ? buf[idx] : 0, buffer_set: (buf, idx, val) => { if (buf instanceof Uint8Array) buf[idx] = val; }, buffer_length: (buf) => (buf instanceof Uint8Array) ? buf.length : 0, buffer_slice: (buf, start, end) => { if (!(buf instanceof Uint8Array)) return new Uint8Array(0); return buf.slice(start, end === undefined ? undefined : end); }, buffer_concat: (arr) => { if (!Array.isArray(arr)) return new Uint8Array(0); const bufs = arr.map(v => (v instanceof Uint8Array) ? v : new Uint8Array(0)); const total = bufs.reduce((s, b) => s + b.length, 0); const result = new Uint8Array(total); let offset = 0; bufs.forEach(b => { result.set(b, offset); offset += b.length; }); return result; }, uint8array_new: (size) => new Uint8Array(size), uint8array_from: (val) => Uint8Array.from(Array.isArray(val) ? val : []), uint8array_length: (buf) => (buf instanceof Uint8Array) ? buf.length : 0, uint8array_get: (buf, idx) => (buf instanceof Uint8Array) ? buf[idx] : 0, uint8array_set: (buf, idx, val) => { if (buf instanceof Uint8Array) buf[idx] = val; }, // Timers — closure arg is the closure object itself (decoded from POINTER_TAG) // Captures are already BigInt from closure_set_capture. set_timeout: (closure, delay) => { if (!closure || typeof closure.funcIdx === 'undefined' || !wasmInstance) return 0; return setTimeout(() => { const fn = wasmInstance.exports.__indirect_function_table?.get(closure.funcIdx | 0); if (fn) fn(...closure.captures); }, delay); }, set_interval: (closure, delay) => { if (!closure || typeof closure.funcIdx === 'undefined' || !wasmInstance) return 0; return setInterval(() => { const fn = wasmInstance.exports.__indirect_function_table?.get(closure.funcIdx | 0); if (fn) fn(...closure.captures); }, delay); }, clear_timeout: (id) => { clearTimeout(id); }, clear_interval: (id) => { clearInterval(id); }, // Response — args are plain JS values (r is the Response object itself) response_status: (r) => (r && typeof r.status === 'number') ? r.status : 0, response_ok: (r) => (r && r.ok) ? 1 : 0, response_headers_get: (r, name) => { if (!r || !r.headers) return null; const v = r.headers.get(String(name)); return v === null ? null : v; }, response_url: (r) => (r && typeof r.url === 'string') ? r.url : undefined, // Buffer extras — args are plain JS values (src/tgt are Uint8Array objects) buffer_copy: (src, tgt, targetStart, sourceStart, sourceEnd) => { if (!(src instanceof Uint8Array) || !(tgt instanceof Uint8Array)) return 0; const ts = targetStart === undefined ? 0 : targetStart; const ss = sourceStart === undefined ? 0 : sourceStart; const se = sourceEnd === undefined ? src.length : sourceEnd; let copied = 0; for (let i = ss; i < se && (ts + copied) < tgt.length; i++) { tgt[ts + copied] = src[i]; copied++; } return copied; }, buffer_write: (buf, str, offset, encoding) => { if (!(buf instanceof Uint8Array)) return 0; const encoded = new TextEncoder().encode(String(str)); const off = offset === undefined ? 0 : offset; let written = 0; for (let i = 0; i < encoded.length && (off + i) < buf.length; i++) { buf[off + i] = encoded[i]; written++; } return written; }, buffer_equals: (a, b) => { if (!(a instanceof Uint8Array) || !(b instanceof Uint8Array) || a.length !== b.length) return 0; for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return 0; return 1; }, buffer_is_buffer: (val) => (val instanceof Uint8Array || (val && val.constructor && val.constructor.name === 'Buffer')) ? 1 : 0, buffer_byte_length: (val) => { if (typeof val === 'string') return new TextEncoder().encode(val).length; if (val instanceof Uint8Array) return val.length; return 0; }, // Fetch/Promise — args are plain JS values fetch_url: (urlStr) => fetch(String(urlStr)), fetch_with_options: (urlStr, methodVal, bodyVal, headersVal, redirectVal) => { const url = String(urlStr); const opts = {}; if (methodVal !== undefined) opts.method = String(methodVal); if (bodyVal !== undefined) opts.body = String(bodyVal); if (headersVal && typeof headersVal === 'object') opts.headers = headersVal; if (redirectVal !== undefined) opts.redirect = String(redirectVal); return fetch(url, opts); }, response_json: (resp) => { if (!resp || typeof resp.json !== 'function') return undefined; return resp.json(); }, response_text: (resp) => { if (!resp || typeof resp.text !== 'function') return undefined; return resp.text(); }, promise_new: () => { let resolve; const p = new Promise(r => { resolve = r; }); p.__resolve = resolve; return p; }, promise_resolve: (p, value) => { if (p && p.__resolve) p.__resolve(value); }, promise_then: (p, closure) => { if (!p || !(p instanceof Promise)) return p; const newP = p.then(val => { if (closure && typeof closure.funcIdx !== 'undefined' && wasmInstance) { const fn = wasmInstance.exports.__indirect_function_table?.get(closure.funcIdx | 0); if (fn) return __bitsToJsValue(fn(...closure.captures, __jsValueToBits(val))); } return val; }); return newP; }, await_promise: (val) => val, // === perry/thread — Web Worker parallelism === thread_parallel_map: (array, closure) => { if (!Array.isArray(array)) return []; return __threadPool.map(array, closure); }, thread_parallel_filter: (array, closure) => { if (!Array.isArray(array)) return []; return __threadPool.filter(array, closure); }, thread_spawn: (closure) => { return __threadPool.spawn(closure); }, }; // --- perry/thread Worker Pool --- // Manages a pool of Web Workers that each instantiate the WASM module. // Workers call compiled WASM functions via the indirect function table. const __threadPool = (() => { const MIN_PARALLEL_SIZE = 1024; let workers = null; let wasmBinary = null; // Minimal runtime bridge for workers — they need NaN-boxing, string table, handle store const WORKER_BOOTSTRAP = ` "use strict"; const TAG_UNDEFINED = 0x7FFC0000_00000001n; const TAG_NULL = 0x7FFC0000_00000002n; const TAG_FALSE = 0x7FFC0000_00000003n; const TAG_TRUE = 0x7FFC0000_00000004n; const STRING_TAG = 0x7FFFn; const POINTER_TAG = 0x7FFDn; const INT32_TAG = 0x7FFEn; const _convBuf = new ArrayBuffer(8); const _f64 = new Float64Array(_convBuf); const _u64 = new BigUint64Array(_convBuf); function f64ToU64(f) { _f64[0] = f; return _u64[0]; } function u64ToF64(u) { _u64[0] = u; return _f64[0]; } const stringTable = []; const handleStore = new Map(); let nextHandleId = 1; const objectHandles = new WeakMap(); function allocHandle(obj) { const reference = obj !== null && (typeof obj === 'object' || typeof obj === 'function'); if (reference && objectHandles.has(obj)) return objectHandles.get(obj); const id = nextHandleId++; if (reference) objectHandles.set(obj, id); handleStore.set(id, obj); return id; } function __bitsToJsValue(bits) { if (bits === TAG_UNDEFINED) return undefined; if (bits === TAG_NULL) return null; if (bits === TAG_TRUE) return true; if (bits === TAG_FALSE) return false; const tag = bits >> 48n; if (tag === STRING_TAG) return stringTable[Number(bits & 0xFFFFFFFFn)]; if (tag === POINTER_TAG) return handleStore.get(Number(bits & 0xFFFFFFFFn)); if (tag === INT32_TAG) return Number(BigInt.asIntN(32, bits & 0xFFFFFFFFn)); _u64[0] = bits; return _f64[0]; } function __jsValueToBits(v) { if (v === undefined) return TAG_UNDEFINED; if (v === null) return TAG_NULL; if (v === true) return TAG_TRUE; if (v === false) return TAG_FALSE; if (typeof v === "number") { _f64[0] = v; return _u64[0]; } if (typeof v === "string") { const id = stringTable.length; stringTable.push(v); return (STRING_TAG << 48n) | BigInt(id); } if (Array.isArray(v)) { const id = allocHandle(v); return (POINTER_TAG << 48n) | BigInt(id); } if (typeof v === "object") { const id = allocHandle(v); return (POINTER_TAG << 48n) | BigInt(id); } _f64[0] = Number(v); return _u64[0]; } let wasmInst = null; let wasmMem = null; self.onmessage = async function(e) { const msg = e.data; try { if (msg.type === "init") { // Build minimal imports for the WASM module const nan_temp = { value: 0x10000 }; const imports = { rt: {} }; // String init — workers need this for __init_strings imports.rt.string_new = (offset, len) => { const bytes = new Uint8Array(wasmMem.buffer, offset, len); stringTable.push(new TextDecoder().decode(bytes)); }; // Stubs for other imports — workers only call user functions via indirect table const noop = () => 0; const noopVoid = () => {}; // Create a Proxy to provide stubs for any import the WASM module needs imports.rt = new Proxy(imports.rt, { get(target, prop) { if (prop in target) return target[prop]; return noop; } }); const module = await WebAssembly.compile(msg.binary); const instance = await WebAssembly.instantiate(module, imports); wasmInst = instance; wasmMem = instance.exports.memory; // Initialize string table if (instance.exports.__init_strings) instance.exports.__init_strings(); self.postMessage({ type: "ready" }); return; } if (!wasmInst) { self.postMessage({ type: "error", message: "Worker not initialized" }); return; } const table = wasmInst.exports.__indirect_function_table; const fn = table.get(msg.funcIdx | 0); if (!fn) { self.postMessage({ type: "error", message: "Function not found at index " + msg.funcIdx }); return; } const captures = (msg.captures || []).map(c => __jsValueToBits(c)); if (msg.type === "map") { const results = []; for (let i = 0; i < msg.chunk.length; i++) { const argBits = __jsValueToBits(msg.chunk[i]); const resultBits = fn(...captures, argBits); results.push(__bitsToJsValue(resultBits)); } self.postMessage({ type: "result", data: results }); } else if (msg.type === "filter") { const filtered = []; for (let i = 0; i < msg.chunk.length; i++) { const argBits = __jsValueToBits(msg.chunk[i]); const resultBits = fn(...captures, argBits); const jsResult = __bitsToJsValue(resultBits); // Truthy check matching Perry semantics if (jsResult !== undefined && jsResult !== null && jsResult !== false && jsResult !== 0 && jsResult !== "") { filtered.push(msg.chunk[i]); } } self.postMessage({ type: "result", data: filtered }); } else if (msg.type === "exec") { const resultBits = fn(...captures); self.postMessage({ type: "result", data: __bitsToJsValue(resultBits) }); } } catch (err) { self.postMessage({ type: "error", message: String(err), stack: err && err.stack }); } }; `; function getConcurrency() { return (typeof navigator !== 'undefined' && navigator.hardwareConcurrency) || 4; } function getWasmBinary() { if (wasmBinary) return wasmBinary; // Decode the base64 WASM that's stored on window by the HTML wrapper const b64 = (typeof window !== 'undefined' && window.__perryWasmB64) || null; if (!b64) return null; const raw = atob(b64); const bytes = new Uint8Array(raw.length); for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i); wasmBinary = bytes.buffer; return wasmBinary; } function ensureWorkers(count) { if (workers && workers.length >= count) return workers; const binary = getWasmBinary(); if (!binary) return null; const blob = new Blob([WORKER_BOOTSTRAP], { type: 'text/javascript' }); const url = URL.createObjectURL(blob); workers = workers || []; const initPromises = []; while (workers.length < count) { const w = new Worker(url); const ready = new Promise((resolve, reject) => { const handler = (e) => { w.removeEventListener('message', handler); if (e.data.type === 'ready') resolve(); else reject(new Error(e.data.message || 'Worker init failed')); }; w.addEventListener('message', handler); }); w.postMessage({ type: 'init', binary }); workers.push({ worker: w, ready }); initPromises.push(ready); } return workers; } function dispatchToWorker(w, msg) { return new Promise((resolve, reject) => { const handler = (e) => { w.worker.removeEventListener('message', handler); if (e.data.type === 'error') reject(new Error(e.data.message)); else resolve(e.data.data); }; w.worker.addEventListener('message', handler); w.worker.postMessage(msg); }); } // Convert a closure object { funcIdx, captures } to plain JS captures for the worker function serializeClosure(closure) { if (!closure || typeof closure !== 'object') return { funcIdx: 0, captures: [] }; const captures = (closure.captures || []).map(c => { // captures are BigInt (NaN-boxed i64) — convert to plain JS values return __bitsToJsValue(typeof c === 'bigint' ? c : BigInt(c || 0)); }); return { funcIdx: closure.funcIdx | 0, captures }; } // Sequential fallback — runs on main thread, no workers function mapSequential(array, closure) { const table = wasmInstance?.exports?.__indirect_function_table; if (!table) return array; const fn = table.get(closure.funcIdx | 0); if (!fn) return array; const captures = (closure.captures || []); const results = []; for (let i = 0; i < array.length; i++) { const resultBits = fn(...captures, __jsValueToBits(array[i])); results.push(__bitsToJsValue(resultBits)); } return results; } function filterSequential(array, closure) { const table = wasmInstance?.exports?.__indirect_function_table; if (!table) return array; const fn = table.get(closure.funcIdx | 0); if (!fn) return array; const captures = (closure.captures || []); const results = []; for (let i = 0; i < array.length; i++) { const resultBits = fn(...captures, __jsValueToBits(array[i])); const jsResult = __bitsToJsValue(resultBits); if (jsResult !== undefined && jsResult !== null && jsResult !== false && jsResult !== 0 && jsResult !== "") { results.push(array[i]); } } return results; } function spawnSequential(closure) { const table = wasmInstance?.exports?.__indirect_function_table; if (!table) return undefined; const fn = table.get(closure.funcIdx | 0); if (!fn) return undefined; const captures = (closure.captures || []); return __bitsToJsValue(fn(...captures)); } return { map(array, closure) { const concurrency = getConcurrency(); if (typeof Worker === 'undefined' || array.length < MIN_PARALLEL_SIZE || concurrency <= 1 || !getWasmBinary()) { return mapSequential(array, closure); } const numChunks = Math.min(concurrency, array.length); const ws = ensureWorkers(numChunks); if (!ws) return mapSequential(array, closure); const { funcIdx, captures } = serializeClosure(closure); const chunkSize = Math.ceil(array.length / numChunks); const promises = []; for (let i = 0; i < numChunks; i++) { const chunk = array.slice(i * chunkSize, (i + 1) * chunkSize); if (chunk.length === 0) continue; const p = ws[i].ready.then(() => dispatchToWorker(ws[i], { type: 'map', funcIdx, captures, chunk }) ); promises.push(p); } return Promise.all(promises).then(results => { const out = []; for (const r of results) for (const item of r) out.push(item); return out; }); }, filter(array, closure) { const concurrency = getConcurrency(); if (typeof Worker === 'undefined' || array.length < MIN_PARALLEL_SIZE || concurrency <= 1 || !getWasmBinary()) { return filterSequential(array, closure); } const numChunks = Math.min(concurrency, array.length); const ws = ensureWorkers(numChunks); if (!ws) return filterSequential(array, closure); const { funcIdx, captures } = serializeClosure(closure); const chunkSize = Math.ceil(array.length / numChunks); const promises = []; for (let i = 0; i < numChunks; i++) { const chunk = array.slice(i * chunkSize, (i + 1) * chunkSize); if (chunk.length === 0) continue; const p = ws[i].ready.then(() => dispatchToWorker(ws[i], { type: 'filter', funcIdx, captures, chunk }) ); promises.push(p); } return Promise.all(promises).then(results => { const out = []; for (const r of results) for (const item of r) out.push(item); return out; }); }, spawn(closure) { if (typeof Worker === 'undefined' || !getWasmBinary()) { return Promise.resolve(spawnSequential(closure)); } const ws = ensureWorkers(1); if (!ws) return Promise.resolve(spawnSequential(closure)); const { funcIdx, captures } = serializeClosure(closure); return ws[0].ready.then(() => dispatchToWorker(ws[0], { type: 'exec', funcIdx, captures }) ); }, }; })(); // Exception state for try/catch bridge let tryDepth = 0; const NO_EXCEPTION = Symbol('no exception'); let currentException = NO_EXCEPTION; // ===== Perry UI Runtime (DOM-based, for --target wasm) ===== // Ported from perry-codegen-js/web_runtime.js — maps perry/ui widgets to DOM elements. const uiHandles = new Map(); // handle_id -> DOM element or state object const uiStates = new Map(); // handle_id -> { _value, subscribers[] } let uiNextHandle = 1; const perryImageCache = new Map(); function uiAlloc(el) { const h = uiNextHandle++; uiHandles.set(h, el); return h; } function uiGet(h) { return uiHandles.get(h); } function __invokeWasmCallback(fn, ...bigintArgs) { const result = fn(...__padBigintArgs(fn, 0, bigintArgs)); __memDispatch.propagate_exception(true); return result; } // Accept a boxed handle or decoded closure; extra arguments are plain JS values. // Convert them once to i64: boxing strings through f64 can erase NaN payloads. function callWasmClosure(closureVal, ...extraArgs) { let closure; if (typeof closureVal === 'object' && closureVal !== null) { // Already a JS closure object (came through toJsValue) closure = closureVal; } else if (typeof closureVal === 'number') { if (isUndefined(closureVal) || isNull(closureVal)) return u64ToF64(TAG_UNDEFINED); closure = isPointer(closureVal) ? handleStore.get(getPointerId(closureVal)) : undefined; } if (!closure) return u64ToF64(TAG_UNDEFINED); if (typeof closure === 'function') return fromJsValue(closure(...extraArgs.map(v => typeof v === 'number' ? v : fromJsValue(v)))); if (typeof closure.funcIdx !== 'undefined' && wasmInstance) { const fn = wasmInstance.exports.__indirect_function_table.get(closure.funcIdx | 0); // Extra args need to be BigInt (i64) for WASM functions. Captures are already BigInt. const wasmArgs = extraArgs.map(v => __jsValueToBits(v)); if (fn) { const captures = closure.captures || []; const result = fn(...captures, ...__padBigintArgs(fn, captures.length, wasmArgs)); return (typeof result === 'bigint') ? __bitsToJsValue(result) : undefined; } } return u64ToF64(TAG_UNDEFINED); } // CSS Reset for UI (function() { const s = document.createElement("style"); s.textContent = ` *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } html, body { width: 100vw; height: 100vh; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } #perry-root { display: flex; flex-direction: column; width: 100%; flex: 1 1 0%; min-height: 0; overflow: hidden; } button { cursor: pointer; padding: 6px 16px; border: 1px solid #ccc; border-radius: 6px; background: transparent; font: inherit; color: inherit; } button:hover { opacity: 0.85; } button:active { opacity: 0.7; } input[type="text"], input[type="password"], select, textarea { padding: 6px 10px; border: 1px solid #ccc; border-radius: 6px; font: inherit; } input[type="range"] { width: 100%; } hr { border: none; border-top: 1px solid #ddd; margin: 4px 0; } fieldset { border: 1px solid #ddd; border-radius: 8px; padding: 12px; } legend { font-weight: 600; padding: 0 6px; } progress { width: 100%; } `; document.head.appendChild(s); })(); function uiGetRoot() { let r = document.getElementById("perry-root"); if (!r) { r = document.createElement("div"); r.id = "perry-root"; document.body.appendChild(r); } return r; } // State system function uiStateCreate(initial) { const h = uiNextHandle++; uiStates.set(h, { _value: initial, subscribers: [] }); return h; } function uiStateGet(h) { const s = uiStates.get(h); return s ? s._value : undefined; } function uiStateSet(h, value) { const s = uiStates.get(h); if (!s) return; s._value = value; const subs = s.subscribers.slice(); for (const sub of subs) { try { sub(value); } catch(e) { console.error("State subscriber error:", e); } } } // ---------- Widget creation functions (take JS values, return handle_id) ---------- // #1546: showToast(msg) — bottom-center fade-in/out toast for --target web. // HarmonyOS routes through `promptAction.showToast` via a runtime drain queue; // on web the previous behavior was a documented no-op. The implementation here // stays minimal: one host-managed div, queued messages, default 3s visible. function perry_ui_show_toast(message) { if (typeof document === "undefined") return; const msg = (message === null || message === undefined) ? "" : String(message); if (!perry_ui_show_toast._q) perry_ui_show_toast._q = []; perry_ui_show_toast._q.push(msg); if (perry_ui_show_toast._busy) return; perry_ui_show_toast._busy = true; const drain = () => { const next = perry_ui_show_toast._q.shift(); if (next === undefined) { perry_ui_show_toast._busy = false; return; } let el = perry_ui_show_toast._el; if (!el) { el = document.createElement("div"); el.setAttribute("data-perry-toast", ""); el.style.cssText = "position:fixed;left:50%;bottom:32px;transform:translateX(-50%);" + "background:rgba(28,28,30,0.92);color:#fff;padding:10px 18px;" + "border-radius:18px;font:14px -apple-system,system-ui,Segoe UI,Roboto,sans-serif;" + "max-width:80vw;box-shadow:0 6px 24px rgba(0,0,0,0.25);" + "opacity:0;transition:opacity .18s ease;pointer-events:none;z-index:2147483647;"; document.body.appendChild(el); perry_ui_show_toast._el = el; } el.textContent = next; requestAnimationFrame(() => { el.style.opacity = "1"; }); setTimeout(() => { el.style.opacity = "0"; setTimeout(drain, 200); }, 3000); }; drain(); } function perry_ui_app_create(titleOrOpts, width, height) { let title = "Perry App", bodyH, w = 800, ht = 600; if (typeof titleOrOpts === "object" && titleOrOpts !== null) { title = titleOrOpts.title || title; w = titleOrOpts.width || w; ht = titleOrOpts.height || ht; bodyH = titleOrOpts.body; } else { title = titleOrOpts || title; w = width || w; ht = height || ht; } document.title = title; const root = uiGetRoot(); root.style.width = w + "px"; root.style.maxWidth = "100%"; root.style.height = ht + "px"; root.style.maxHeight = "100%"; const h = uiAlloc(root); if (bodyH !== undefined) { const bodyEl = uiGet(bodyH); if (bodyEl) root.appendChild(bodyEl); } return h; } function perry_ui_vstack_create(spacing, children) { const el = document.createElement("div"); el.style.display = "flex"; el.style.flexDirection = "column"; if (spacing) el.style.gap = spacing + "px"; el.style.flex = "1 1 0%"; el.style.minHeight = "0"; el.style.overflow = "auto"; const h = uiAlloc(el); if (Array.isArray(children)) { for (var i = 0; i < children.length; i++) { var c = uiGet(children[i]); if (c) el.appendChild(c); } } return h; } function perry_ui_hstack_create(spacing, children) { const el = document.createElement("div"); el.style.display = "flex"; el.style.flexDirection = "row"; el.style.alignItems = "center"; if (spacing) el.style.gap = spacing + "px"; const h = uiAlloc(el); if (Array.isArray(children)) { for (var i = 0; i < children.length; i++) { var c = uiGet(children[i]); if (c) el.appendChild(c); } } return h; } function perry_ui_zstack_create() { const el = document.createElement("div"); el.style.position = "relative"; el.style.display = "flex"; el.style.flex = "1 1 0%"; return uiAlloc(el); } function perry_ui_text_create(text, id) { const el = document.createElement("span"); el.textContent = (text !== undefined && text !== null) ? String(text) : ""; const h = uiAlloc(el); // Issue #1392 — 2-arg `Text(content, id)` form. The state_desugar pass // emits `count.text()` as `Text("", "__state_N")`; the id string // is reused as the keyed-state key, so registering the widget here lets // `setText(id, …)` and `__state_set("__state_N", …)` find and re-render it. if (typeof id === "string" && id.length > 0) uiTextRegister(h, id); return h; } function perry_ui_button_create(label, callback) { const el = document.createElement("button"); el.textContent = (typeof label === 'string') ? label : String(label ?? ""); el._perryCallback = callback; el.addEventListener("click", () => { if (el._perryCallback !== undefined) callWasmClosure(el._perryCallback); }); return uiAlloc(el); } function perry_ui_textfield_create(placeholder, callback) { const el = document.createElement("input"); el.type = "text"; el.placeholder = placeholder || ""; el._perryCallback = callback; el.addEventListener("input", () => { if (el._perryCallback !== undefined) callWasmClosure(el._perryCallback, el.value); }); return uiAlloc(el); } function perry_ui_securefield_create(placeholder, callback) { const el = document.createElement("input"); el.type = "password"; el.placeholder = placeholder || ""; el._perryCallback = callback; el.addEventListener("input", () => { if (el._perryCallback !== undefined) callWasmClosure(el._perryCallback, el.value); }); return uiAlloc(el); } function perry_ui_toggle_create(label, callback) { const wrap = document.createElement("label"); wrap.style.display = "flex"; wrap.style.alignItems = "center"; wrap.style.gap = "8px"; const inp = document.createElement("input"); inp.type = "checkbox"; wrap.appendChild(inp); if (label) { const sp = document.createElement("span"); sp.textContent = label; wrap.appendChild(sp); } wrap._perryCallback = callback; wrap._inp = inp; inp.addEventListener("change", () => { if (wrap._perryCallback !== undefined) callWasmClosure(wrap._perryCallback, inp.checked ? 1.0 : 0.0); }); return uiAlloc(wrap); } function perry_ui_slider_create(min, max, callback) { // Codegen emits 3-arg Slider(min, max, onChange); default initial=min const el = document.createElement("input"); el.type = "range"; el.min = min || 0; el.max = max || 100; el.value = el.min; el.step = "any"; el._perryCallback = callback; el.addEventListener("input", () => { if (el._perryCallback !== undefined) callWasmClosure(el._perryCallback, parseFloat(el.value)); }); return uiAlloc(el); } function perry_ui_scrollview_create() { const el = document.createElement("div"); el.style.overflow = "auto"; el.style.flex = "1 1 0%"; el.style.minHeight = "0"; return uiAlloc(el); } function perry_ui_spacer_create() { const el = document.createElement("div"); el.style.flex = "1 1 0%"; return uiAlloc(el); } function perry_ui_divider_create() { return uiAlloc(document.createElement("hr")); } function perry_ui_progressview_create(value) { const el = document.createElement("progress"); if (value !== undefined && value > 0) { el.max = 1; el.value = value; } return uiAlloc(el); } function perry_ui_image_create(src, width, height) { const el = document.createElement("img"); if (src) el.src = src; if (width) el.style.width = width + "px"; if (height) el.style.height = height + "px"; return uiAlloc(el); } function perry_ui_picker_create(items_json, selected, callback) { const el = document.createElement("select"); el._perryCallback = callback; el.addEventListener("change", () => { if (el._perryCallback !== undefined) callWasmClosure(el._perryCallback, el.selectedIndex); }); if (selected !== undefined) el.selectedIndex = selected; return uiAlloc(el); } function perry_ui_wheel_picker_create(callback) { const el = document.createElement("select"); el.size = 5; el.className = "perry-wheel-picker"; el.style.overflowY = "auto"; el.style.scrollSnapType = "y mandatory"; el._perryCallback = callback; el.addEventListener("change", () => { if (el._perryCallback !== undefined) callWasmClosure(el._perryCallback, el.selectedIndex); }); return uiAlloc(el); } function perry_ui_form_create() { const el = document.createElement("fieldset"); el.style.display = "flex"; el.style.flexDirection = "column"; el.style.gap = "8px"; return uiAlloc(el); } function perry_ui_section_create(title) { const el = document.createElement("fieldset"); el.style.display = "flex"; el.style.flexDirection = "column"; el.style.gap = "8px"; if (title) { const lg = document.createElement("legend"); lg.textContent = title; el.appendChild(lg); } return uiAlloc(el); } function perry_ui_navigationstack_create() { const el = document.createElement("div"); el.style.display = "flex"; el.style.flexDirection = "column"; el.style.flex = "1 1 0%"; el._navStack = []; return uiAlloc(el); } function perry_ui_canvas_create(width, height) { const el = document.createElement("canvas"); el.width = width || 300; el.height = height || 150; el._ctx = el.getContext("2d"); return uiAlloc(el); } // BloomView (issue #2395) — a render-surface host for an external GPU // renderer. The web has no HWND; bloomViewGetHwnd returns the handle id (the // surrogate the renderer keys off of) or 0 if unknown. function perry_ui_bloomview_create(width, height) { const el = document.createElement("canvas"); el.width = width || 300; el.height = height || 150; return uiAlloc(el); } function perry_ui_bloomview_get_hwnd(h) { return uiGet(h) ? h : 0; } function perry_ui_lazyvstack_create(count, renderClosure) { const el = document.createElement("div"); el.style.display = "flex"; el.style.flexDirection = "column"; el.style.overflow = "auto"; el.style.flex = "1 1 0%"; el._renderClosure = renderClosure; el._count = count; for (let i = 0; i < count; i++) { const childVal = callWasmClosure(renderClosure, i); const childEl = uiGet(typeof childVal === 'number' ? childVal : 0); if (childEl) el.appendChild(childEl); } return uiAlloc(el); } function perry_ui_lazyvstack_update(h, count) { const el = uiGet(h); if (!el) return; el.innerHTML = ''; el._count = count; for (let i = 0; i < count; i++) { const childVal = callWasmClosure(el._renderClosure, i); const childEl = uiGet(typeof childVal === 'number' ? childVal : 0); if (childEl) el.appendChild(childEl); } } function perry_ui_table_create(rowCount, colCount, renderClosure) { const table = document.createElement("table"); table.style.width = "100%"; table.style.borderCollapse = "collapse"; table._renderClosure = renderClosure; table._rowCount = rowCount; table._colCount = colCount; table._selectedRow = -1; const thead = document.createElement("thead"); table.appendChild(thead); const headerRow = document.createElement("tr"); thead.appendChild(headerRow); for (let c = 0; c < colCount; c++) { const th = document.createElement("th"); th.textContent = "Col " + c; headerRow.appendChild(th); } const tbody = document.createElement("tbody"); table.appendChild(tbody); for (let r = 0; r < rowCount; r++) { const tr = document.createElement("tr"); tr.style.cursor = "pointer"; tr.addEventListener("click", () => { table._selectedRow = r; if (table._onRowSelect) callWasmClosure(table._onRowSelect, r); }); for (let c = 0; c < colCount; c++) { const td = document.createElement("td"); td.style.padding = "4px 8px"; td.style.borderBottom = "1px solid #eee"; const cellVal = callWasmClosure(renderClosure, r, c); const cellEl = uiGet(typeof cellVal === 'number' ? cellVal : 0); if (cellEl) td.appendChild(cellEl); else td.textContent = String(toJsValue(cellVal) ?? ""); tr.appendChild(td); } tbody.appendChild(tr); } return uiAlloc(table); } function perry_ui_table_set_column_header(h, col, title) { const t = uiGet(h); if (!t) return; const th = t.querySelector("thead tr")?.children[col]; if (th) th.textContent = title; } function perry_ui_table_set_column_width(h, col, width) { const t = uiGet(h); if (!t) return; const th = t.querySelector("thead tr")?.children[col]; if (th) th.style.width = width + "px"; } function perry_ui_table_update_row_count(h, count) { /* simplified: just update count */ } function perry_ui_table_set_on_row_select(h, cb) { const t = uiGet(h); if (t) t._onRowSelect = cb; } function perry_ui_table_get_selected_row(h) { const t = uiGet(h); return t ? t._selectedRow : -1; } function perry_ui_textarea_create(placeholder, callback) { const el = document.createElement("textarea"); el.placeholder = placeholder || ""; el._perryCallback = callback; el.addEventListener("input", () => { if (el._perryCallback !== undefined) callWasmClosure(el._perryCallback, el.value); }); return uiAlloc(el); } function perry_ui_textarea_set_string(h, text) { const el = uiGet(h); if (el) el.value = text; } function perry_ui_textarea_get_string(h) { const el = uiGet(h); return el ? el.value : ""; } // ---------- Child management ---------- function perry_ui_widget_add_child(parentH, childH) { const p = uiGet(parentH), c = uiGet(childH); if (p && c) p.appendChild(c); } function perry_ui_widget_remove_all_children(h) { const el = uiGet(h); if (el) el.innerHTML = ""; } function perry_ui_widget_remove_child(parentH, childH) { const p = uiGet(parentH), c = uiGet(childH); if (p && c && p.contains(c)) p.removeChild(c); } function perry_ui_widget_reorder_child(parentH, fromIdx, toIdx) { const p = uiGet(parentH); if (!p) return; const children = Array.from(p.children); if (fromIdx >= 0 && fromIdx < children.length) { const child = children[fromIdx]; p.removeChild(child); if (toIdx >= p.children.length) p.appendChild(child); else p.insertBefore(child, p.children[toIdx]); } } function perry_ui_widget_add_overlay(parentH, overlayH) { const p = uiGet(parentH), o = uiGet(overlayH); if (p && o) { o.style.position = "absolute"; p.appendChild(o); } } function perry_ui_widget_set_overlay_frame(overlayH, x, y, width, height) { const o = uiGet(overlayH); if (!o) return; o.style.left = x + "px"; o.style.top = y + "px"; o.style.width = width + "px"; o.style.height = height + "px"; } // ---------- Styling ---------- function perry_ui_set_background(h, r, g, b, a) { const el = uiGet(h); if (el) el.style.backgroundColor = `rgba(${r*255|0},${g*255|0},${b*255|0},${a})`; } function perry_ui_set_foreground(h, r, g, b, a) { const el = uiGet(h); if (el) el.style.color = `rgba(${r*255|0},${g*255|0},${b*255|0},${a})`; } function perry_ui_set_font_size(h, size) { const el = uiGet(h); if (el) el.style.fontSize = size + "px"; } function perry_ui_set_font_weight(h, weight) { const el = uiGet(h); if (el) el.style.fontWeight = weight; } function perry_ui_set_font_family(h, family) { const el = uiGet(h); if (el) el.style.fontFamily = family; } function perry_ui_set_padding(h, top, left, bottom, right) { const el = uiGet(h); if (!el) return; if (left === undefined) left = bottom = right = top; el.style.padding = `${top}px ${right}px ${bottom}px ${left}px`; } function perry_ui_set_frame(h, width, height) { const el = uiGet(h); if (!el) return; if (width > 0) el.style.width = width + "px"; if (height > 0) el.style.height = height + "px"; } function perry_ui_set_corner_radius(h, radius) { const el = uiGet(h); if (el) el.style.borderRadius = radius + "px"; } function perry_ui_set_border(h, width, r, g, b, a) { const el = uiGet(h); if (el) el.style.border = `${width}px solid rgba(${r*255|0},${g*255|0},${b*255|0},${a})`; } // Joint border state for the Apple-style split setters // (`widgetSetBorderColor` / `widgetSetBorderWidth`). CSS won't render a // border unless style + color + width are all set in the same rule, so // we cache (color, width) per handle and re-apply `el.style.border` on // every change. Defaults match CALayer-ish behavior: missing color = // black, missing width = 1px. Issue #185 Phase B closure. const __perryBorderState = new Map(); function __perry_apply_border(h) { const el = uiGet(h); if (!el) return; const s = __perryBorderState.get(h) || {}; const color = s.color || [0, 0, 0, 1]; const width = s.width != null ? s.width : 1; const [r, g, b, a] = color; el.style.border = `${width}px solid rgba(${r*255|0},${g*255|0},${b*255|0},${a})`; } function perry_ui_widget_set_border_color(h, r, g, b, a) { const s = __perryBorderState.get(h) || {}; s.color = [r, g, b, a]; __perryBorderState.set(h, s); __perry_apply_border(h); } function perry_ui_widget_set_border_width(h, width) { const s = __perryBorderState.get(h) || {}; s.width = width; __perryBorderState.set(h, s); __perry_apply_border(h); } // Text decoration (issue #185 Phase B closure). 0=none, 1=underline, // 2=strikethrough. CSS `text-decoration` values map cleanly. function perry_ui_text_set_decoration(h, decoration) { const el = uiGet(h); if (!el) return; const css = decoration === 1 ? "underline" : decoration === 2 ? "line-through" : "none"; el.style.textDecoration = css; } // Issue #3621 — horizontal text alignment. Canonical Perry/AppKit values: // 0=left, 1=right, 2=center, 3=justified, 4=natural. Maps to CSS // `text-align` (natural → `start`, which follows the element's direction). function perry_ui_text_set_text_alignment(h, alignment) { const el = uiGet(h); if (!el) return; const css = alignment === 1 ? "right" : alignment === 2 ? "center" : alignment === 3 ? "justify" : alignment === 4 ? "start" : "left"; el.style.textAlign = css; } // Issue #185 Phase B closure 11 — TextField borderless. Drops the // rendered border so the input visually matches a non-bordered // borderless setter on the Apple side. function perry_ui_textfield_set_borderless(h, borderless) { const el = uiGet(h); if (!el) return; el.style.border = borderless ? "none" : ""; // Also strip the default browser focus outline so "borderless" // really feels borderless. User can override with explicit // border / outline rules afterwards. el.style.outline = borderless ? "none" : ""; } // Issue #185 Phase B closure 11 — Stack alignment via CSS flex. // Numeric enum: 0=fill, 1=center, 2=leading, 3=trailing, 4=baseline. // Maps to CSS `align-items` (cross-axis on flex containers). function perry_ui_stack_set_alignment(h, alignment) { const el = uiGet(h); if (!el) return; const value = alignment === 0 ? "stretch" : alignment === 1 ? "center" : alignment === 2 ? "flex-start" : alignment === 3 ? "flex-end" : alignment === 4 ? "baseline" : "stretch"; el.style.alignItems = value; } // Drop shadow (issue #185 Phase B closure 2). Same arg shape as the // Apple CALayer twin — `(r,g,b,a)` is shadow color, `blur` is shadow // radius, `(offsetX, offsetY)` is shadow offset (positive y = downward, // matching CALayer + native HTML semantics). function perry_ui_widget_set_shadow(h, r, g, b, a, blur, offsetX, offsetY) { const el = uiGet(h); if (!el) return; el.style.boxShadow = `${offsetX}px ${offsetY}px ${blur}px rgba(${r*255|0},${g*255|0},${b*255|0},${a})`; } function perry_ui_set_opacity(h, opacity) { const el = uiGet(h); if (el) el.style.opacity = opacity; } function perry_ui_set_enabled(h, enabled) { const el = uiGet(h); if (el) { el.disabled = !enabled; el.style.pointerEvents = enabled ? "" : "none"; el.style.opacity = enabled ? "" : "0.5"; } } function perry_ui_set_tooltip(h, text) { const el = uiGet(h); if (el) el.title = text || ""; } function perry_ui_set_control_size(h, size) { /* 0=regular 1=small 2=mini 3=large */ const el = uiGet(h); if (!el) return; const sizes = ["", "0.85em", "0.75em", "1.2em"]; el.style.fontSize = sizes[size] || ""; } function perry_ui_set_widget_hidden(h, hidden) { const el = uiGet(h); if (el) el.style.display = hidden ? "none" : ""; } function perry_ui_widget_set_background_gradient(h, r1, g1, b1, a1, r2, g2, b2, a2, direction) { const el = uiGet(h); if (!el) return; const c1 = `rgba(${r1*255|0},${g1*255|0},${b1*255|0},${a1})`, c2 = `rgba(${r2*255|0},${g2*255|0},${b2*255|0},${a2})`; const dir = direction === 1 ? "to right" : "to bottom"; el.style.background = `linear-gradient(${dir}, ${c1}, ${c2})`; } function perry_ui_widget_set_max_width(h, maxWidth) { if (!Number.isFinite(maxWidth) || maxWidth < 0) return; const el = uiGet(h); if (!el) return; el.style.boxSizing = "border-box"; el.style.width = "100%"; el.style.minWidth = "0"; el.style.maxWidth = maxWidth + "px"; el.style.flexShrink = "1"; el.style.marginLeft = "auto"; el.style.marginRight = "auto"; } function perry_ui_widget_set_width(h, w) { const el = uiGet(h); if (el) { el.style.width = w + "px"; el.style.minWidth = w + "px"; el.style.maxWidth = w + "px"; el.style.flexShrink = "0"; } } function perry_ui_widget_set_height(h, height) { const el = uiGet(h); if (el) el.style.height = height + "px"; } function perry_ui_widget_set_hugging(h) { const el = uiGet(h); if (el) el.style.flex = "0 0 auto"; } function perry_ui_widget_match_parent_width(h) { const el = uiGet(h); if (el) el.style.width = "100%"; } function perry_ui_widget_match_parent_height(h) { const el = uiGet(h); if (el) el.style.height = "100%"; } function perry_ui_widget_set_edge_insets(h, top, left, bottom, right) { perry_ui_set_padding(h, top, left, bottom, right); } function perry_ui_stack_set_detaches_hidden(h) { /* no-op in web */ } function perry_ui_stack_set_distribution(h) { /* no-op in web */ } function perry_ui_widget_set_context_menu(widgetH, menuH) { /* simplified stub */ } // ---------- Animations ---------- // Canonical API: animateOpacity(target, durationSecs), animatePosition(dx, dy, durationSecs). function perry_ui_animate_opacity(h, target, durationSecs) { const el = uiGet(h); if (!el) return; const cur = el.style.opacity === "" ? "1" : el.style.opacity; el.style.opacity = cur; el.style.transition = `opacity ${durationSecs}s ease`; requestAnimationFrame(() => { el.style.opacity = target; }); } function perry_ui_animate_position(h, dx, dy, durationSecs) { const el = uiGet(h); if (!el) return; const m = /translate\(([-\d.]+)px,\s*([-\d.]+)px\)/.exec(el.style.transform || ""); const curX = m ? parseFloat(m[1]) : 0; const curY = m ? parseFloat(m[2]) : 0; el.style.transform = `translate(${curX}px, ${curY}px)`; el.style.transition = `transform ${durationSecs}s ease`; requestAnimationFrame(() => { el.style.transform = `translate(${curX + dx}px, ${curY + dy}px)`; }); } // ---------- Events ---------- // Issue #1865: display-link callbacks. One-shot per registration, like // requestAnimationFrame. The id we return matches the rAF id so cancelFrame // can hand it straight to cancelAnimationFrame. timestampMs is rAF's // DOMHighResTimeStamp; deltaMs is computed per stable WASM function-table // index because the JS closure wrapper crossing the WASM boundary is freshly // allocated on each call. This keeps the idiomatic // `function loop(t,dt){ ...; onFrame(loop); }` pattern accurate. const __perryFrameLastByFuncIdx = new Map(); function perry_ui_on_frame(callback) { if (!callback || typeof callback.funcIdx === 'undefined') return 0; const key = callback.funcIdx | 0; const id = requestAnimationFrame((ts) => { const prev = __perryFrameLastByFuncIdx.get(key); const dt = (typeof prev === 'number' && ts >= prev) ? ts - prev : 0; __perryFrameLastByFuncIdx.set(key, ts); callWasmClosure(callback, ts, dt); }); return id; } function perry_ui_cancel_frame(id) { cancelAnimationFrame(id); } function perry_ui_set_on_click(h, callback) { const el = uiGet(h); if (el) el.addEventListener("click", () => callWasmClosure(callback)); } function perry_ui_set_on_hover(h, callback) { const el = uiGet(h); if (!el) return; el.addEventListener("mouseenter", () => callWasmClosure(callback, 1.0)); el.addEventListener("mouseleave", () => callWasmClosure(callback, 0.0)); } function perry_ui_set_on_double_click(h, callback) { const el = uiGet(h); if (el) el.addEventListener("dblclick", () => callWasmClosure(callback)); } // ---------- State ---------- function perry_ui_state_create(initial) { return uiStateCreate(initial); } function perry_ui_state_get(h) { return uiStateGet(h); } function perry_ui_state_set(h, v) { uiStateSet(h, v); } function perry_ui_state_on_change(stateH, callback) { const s = uiStates.get(stateH); if (s) s.subscribers.push((val) => callWasmClosure(callback, val)); } function perry_ui_state_bind_text(stateH, widgetH) { const el = uiGet(widgetH), s = uiStates.get(stateH); if (el && s) { el.textContent = String(s._value ?? ""); s.subscribers.push(v => { el.textContent = String(v ?? ""); }); } } function perry_ui_state_bind_text_numeric(stateH, widgetH) { perry_ui_state_bind_text(stateH, widgetH); } function perry_ui_state_bind_slider(stateH, widgetH) { const el = uiGet(widgetH), s = uiStates.get(stateH); if (el && s) { el.value = s._value; s.subscribers.push(v => { el.value = v; }); el.addEventListener("input", () => uiStateSet(stateH, parseFloat(el.value))); } } function perry_ui_state_bind_toggle(stateH, widgetH) { const wrap = uiGet(widgetH), s = uiStates.get(stateH); if (wrap && s) { const inp = wrap._inp || wrap.querySelector("input"); if (inp) { inp.checked = !!s._value; s.subscribers.push(v => { inp.checked = !!v; }); } } } // Issue #5076: programmatically set a Toggle's on/off state (0 = off, non-zero = on). function perry_ui_toggle_set_state(widgetH, on) { const wrap = uiGet(widgetH); if (!wrap) return; const inp = wrap._inp || wrap.querySelector("input"); if (inp) inp.checked = !!on; } function perry_ui_state_bind_visibility(stateH, widgetH) { const el = uiGet(widgetH), s = uiStates.get(stateH); if (el && s) { el.style.display = s._value ? "" : "none"; s.subscribers.push(v => { el.style.display = v ? "" : "none"; }); } } function perry_ui_state_bind_foreach(stateH, parentH, templateClosure) { const s = uiStates.get(stateH); if (!s) return; function rebuild(val) { const p = uiGet(parentH); if (!p) return; p.innerHTML = ""; const count = typeof val === 'number' ? val : (Array.isArray(val) ? val.length : 0); for (let i = 0; i < count; i++) { const childVal = callWasmClosure(templateClosure, i); const childEl = uiGet(typeof childVal === 'number' ? childVal : 0); if (childEl) p.appendChild(childEl); } } rebuild(s._value); s.subscribers.push(rebuild); } function perry_ui_state_bind_textfield(stateH, widgetH) { const el = uiGet(widgetH), s = uiStates.get(stateH); if (el && s) { el.value = String(s._value ?? ""); s.subscribers.push(v => { el.value = String(v ?? ""); }); el.addEventListener("input", () => uiStateSet(stateH, el.value)); } } // ---------- Issue #1392: keyed state + setText registry ---------- // The state_desugar pass (crates/perry-transform/src/state_desugar.rs) lowers // `state` to a synthetic, string-keyed registry API rather than the // handle-based `perry_ui_state_create` surface above: // let c = state(0) -> __state_init("__state_0", 0) // c.value / c.get() -> __state_get("__state_0") // c.set(v) -> __state_set("__state_0", v) // c.text() -> Text("0", "__state_0") (registers via uiTextRegister) // The synth id ("__state_0") is reused as both the state key and the Text // widget's setText id, so a write fans out to every bound widget. This // mirrors the native runtime (perry-runtime/src/ui_text_registry.rs: // js_state_init / js_state_get / js_state_set + perry_arkts_set_text). const keyedStates = new Map(); // synth id -> current value const uiTextIds = new Map(); // text id -> [widget handle, ...] const keyedNavstack = new Map(); // synth id -> [{ name, handle }, ...] const keyedForeach = new Map(); // synth id -> [{ host, render }, ...] function uiTextRegister(handle, id) { let list = uiTextIds.get(id); if (!list) { list = []; uiTextIds.set(id, list); } list.push(handle); } function perry_ui_set_text(id, value) { const list = uiTextIds.get(id); if (!list) return; const str = (value === undefined || value === null) ? "" : String(value); for (const h of list) { const el = uiGet(h); if (el) el.textContent = str; } } function perry_ui_state_init(id, initial) { keyedStates.set(id, initial); } function perry_ui_state_get_keyed(id) { return keyedStates.has(id) ? keyedStates.get(id) : undefined; } function perry_ui_state_set_keyed(id, value) { keyedStates.set(id, value); perry_ui_set_text(id, value); // reactive Text re-render navstackDispatchKeyed(id, value); // route visibility foreachDispatchKeyed(id, value); // dynamic-list re-render } function foreachRenderKeyed(host, render, value) { const p = uiGet(host); if (!p) return; p.innerHTML = ""; const count = typeof value === 'number' ? value : (Array.isArray(value) ? value.length : 0); for (let i = 0; i < count; i++) { const childVal = callWasmClosure(render, i); const childEl = uiGet(typeof childVal === 'number' ? childVal : 0); if (childEl) p.appendChild(childEl); } } function foreachDispatchKeyed(id, value) { const binds = keyedForeach.get(id); if (!binds) return; for (const b of binds) foreachRenderKeyed(b.host, b.render, value); } function perry_ui_foreach_register(id, host, render) { let list = keyedForeach.get(id); if (!list) { list = []; keyedForeach.set(id, list); } list.push({ host, render }); if (keyedStates.has(id)) foreachRenderKeyed(host, render, keyedStates.get(id)); } function navstackDispatchKeyed(id, value) { const routes = keyedNavstack.get(id); if (!routes) return; const cur = (value === undefined || value === null) ? "" : String(value); for (const r of routes) { const el = uiGet(r.handle); if (el) el.style.display = (r.name === cur) ? "" : "none"; } } function perry_ui_navstack_register_route(id, name, body) { let list = keyedNavstack.get(id); if (!list) { list = []; keyedNavstack.set(id, list); } list.push({ name, handle: body }); if (keyedStates.has(id)) { const cur = String(keyedStates.get(id)); const el = uiGet(body); if (el && name !== cur) el.style.display = "none"; } } // ---------- Text/Button/TextField ops ---------- function perry_ui_text_set_string(h, text) { const el = uiGet(h); if (el) el.textContent = String(text ?? ""); } function perry_ui_text_set_selectable(h, selectable) { const el = uiGet(h); if (el) el.style.userSelect = selectable ? "text" : "none"; } function perry_ui_text_set_wraps(h) { const el = uiGet(h); if (el) el.style.wordWrap = "break-word"; } function perry_ui_text_set_color(h, r, g, b, a) { perry_ui_set_foreground(h, r, g, b, a); } function perry_ui_text_set_letter_spacing(h, points) { const el = uiGet(h); if (el) el.style.letterSpacing = points === 0 ? "normal" : points + "px"; } function perry_ui_text_set_line_height(h, multiple) { const el = uiGet(h); if (el && multiple > 0) el.style.lineHeight = multiple === 1 ? "normal" : String(multiple); } function perry_ui_button_set_bordered(h, bordered) { const el = uiGet(h); if (el) el.style.border = bordered ? "" : "none"; } function perry_ui_button_set_title(h, title) { const el = uiGet(h); if (el) el.textContent = title; } function perry_ui_button_set_text_color(h, r, g, b, a) { perry_ui_set_foreground(h, r, g, b, a); } function perry_ui_button_set_image(h, name, pointSize) { /* SF symbols not available in web */ } function perry_ui_button_set_content_tint_color(h, r, g, b, a) { perry_ui_set_foreground(h, r, g, b, a); } function perry_ui_button_set_image_position() { /* no-op in web */ } function perry_ui_textfield_focus(h) { const el = uiGet(h); if (el) el.focus(); } function perry_ui_textfield_set_string(h, text) { const el = uiGet(h); if (el) el.value = String(text ?? ""); } function perry_ui_textfield_get_string(h) { const el = uiGet(h); return el ? el.value : ""; } function perry_ui_textfield_blur_all() { if (document.activeElement) document.activeElement.blur(); } function perry_ui_textfield_set_on_submit(h, callback) { const el = uiGet(h); if (el) el.addEventListener("keydown", (e) => { if (e.key === "Enter") callWasmClosure(callback); }); } function perry_ui_textfield_set_on_focus(h, callback) { const el = uiGet(h); if (el) el.addEventListener("focus", () => callWasmClosure(callback)); } // ---------- ScrollView ---------- function perry_ui_scrollview_set_child(scrollH, childH) { const s = uiGet(scrollH), c = uiGet(childH); if (s && c) { s.innerHTML = ""; s.appendChild(c); } } function perry_ui_scrollview_scroll_to(scrollH, childH) { const c = uiGet(childH); if (c) c.scrollIntoView({ behavior: "smooth" }); } function perry_ui_scrollview_get_offset(scrollH) { const s = uiGet(scrollH); return s ? s.scrollTop : 0; } function perry_ui_scrollview_set_offset(scrollH, offset) { const s = uiGet(scrollH); if (s) s.scrollTop = offset; } // ---------- Layout ---------- function perry_ui_vstack_create_with_insets(spacing, top, left, bottom, right) { const h = perry_ui_vstack_create(spacing); const el = uiGet(h); if (el) el.style.padding = `${top}px ${right}px ${bottom}px ${left}px`; return h; } function perry_ui_hstack_create_with_insets(spacing, top, left, bottom, right) { const h = perry_ui_hstack_create(spacing); const el = uiGet(h); if (el) el.style.padding = `${top}px ${right}px ${bottom}px ${left}px`; return h; } // ---------- Navigation ---------- function perry_ui_navstack_push(h, bodyH) { const nav = uiGet(h), body = uiGet(bodyH); if (nav && body) { nav._navStack = nav._navStack || []; nav._navStack.push(Array.from(nav.children)); nav.innerHTML = ""; nav.appendChild(body); } } function perry_ui_navstack_pop(h) { const nav = uiGet(h); if (!nav || !nav._navStack?.length) return; const prev = nav._navStack.pop(); nav.innerHTML = ""; prev.forEach(c => nav.appendChild(c)); } // ---------- Picker ---------- function perry_ui_picker_add_item(h, title) { const el = uiGet(h); if (!el) return; const opt = document.createElement("option"); opt.textContent = title; el.appendChild(opt); } function perry_ui_picker_set_selected(h, index) { const el = uiGet(h); if (el) el.selectedIndex = index; } function perry_ui_picker_get_selected(h) { const el = uiGet(h); return el ? el.selectedIndex : -1; } function perry_ui_wheel_picker_add_item(h, title) { const el = uiGet(h); if (!el) return; const opt = document.createElement("option"); opt.textContent = title; opt.style.scrollSnapAlign = "center"; el.appendChild(opt); } function perry_ui_wheel_picker_set_selected(h, index) { const el = uiGet(h); if (el) el.selectedIndex = index; } function perry_ui_wheel_picker_get_selected(h) { const el = uiGet(h); return el ? el.selectedIndex : -1; } // ---------- Image ---------- function perry_ui_image_create_symbol(name) { return perry_ui_text_create("⬜ " + name); } function perry_ui_image_create_url(url, alt) { const el = document.createElement("img"); el.src = url || ""; if (alt) el.alt = alt; el.style.objectFit = "cover"; return uiAlloc(el); } function perry_ui_image_set_size(h, width, height) { const el = uiGet(h); if (!el) return; if (width) el.style.width = width + "px"; if (height) el.style.height = height + "px"; } function perry_ui_image_set_tint(h, r, g, b, a) { perry_ui_set_foreground(h, r, g, b, a); } // ---------- WebView (issue #658 Phase 5 web target) ---------- // Renders an