// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Runs wasi-runtime/runtime.js (argv[2], copied to a .mjs so that node loads // it as a module) once per scenario against the module assembled below, and // prints one line per scenario for the test to check. Between _start and the // call that ends it, the module has the frames C++ built with // -fwasm-exceptions puts there: a destructor (catch_all_ref, then // throw_ref), a catch (...) (catch_all) and a noexcept function (catch_all, // then std::terminate). Each reports through env.trace when it runs, and on // the way out of an exit or a park none of them may. import { pathToFileURL } from "node:url"; const uleb = (n) => { const out = []; do { const byte = n & 0x7f; n >>>= 7; out.push(n ? byte | 0x80 : byte); } while (n); return out; }; const sleb = (n) => { const out = []; for (;;) { const byte = n & 0x7f; n >>= 7; const done = (n === 0 && !(byte & 0x40)) || (n === -1 && (byte & 0x40)); out.push(done ? byte : byte | 0x80); if (done) return out; } }; const vec = (items) => [...uleb(items.length), ...items.flat()]; const name = (text) => vec([...text].map((c) => [c.charCodeAt(0)])); const section = (id, items) => { const bytes = vec(items); return [id, ...uleb(bytes.length), ...bytes]; }; const UNREACHABLE = 0x00, BLOCK = 0x02, IF = 0x04, ELSE = 0x05, THROW = 0x08, THROW_REF = 0x0a, END = 0x0b; const RETURN = 0x0f, CALL = 0x10, TRY_TABLE = 0x1f, LOCAL_GET = 0x20, LOCAL_SET = 0x21, GLOBAL_GET = 0x23; const GLOBAL_SET = 0x24, I32_CONST = 0x41, I32_EQ = 0x46; const I32 = 0x7f, EXNREF = 0x69, NO_RESULT = 0x40, FUNC_TYPE = 0x60, CATCH_ALL = 0x02, CATCH_ALL_REF = 0x03; const code = (locals, instructions) => { const bytes = [...vec(locals), ...instructions, END]; return [...uleb(bytes.length), ...bytes]; }; // Types, imported functions, then the module's own. const TAKES_I32 = 0, NOTHING = 1, GIVES_I32 = 2; const PROC_EXIT = 0, PARK = 1, SCENARIO = 2, TRACE = 3; const END_WITH = 4, NOEXCEPT = 5, CATCHES_ALL = 6, CLEANS_UP = 7, START = 8, FRAME = 9, QUIT = 10; // What env.trace reports. const DESTRUCTOR = 1, CATCH = 2, TERMINATE = 3, AFTER_PARK = 4; // What env.scenario selects in _start. const EXIT = 1, PARK_MAIN = 2, THROW_UNCAUGHT = 4, CRASH = 5; const PARK_CODE = -1; const ifScenario = (scenario, instructions) => [LOCAL_GET, 0, I32_CONST, ...sleb(scenario), I32_EQ, IF, NO_RESULT, ...instructions, END]; const module = new Uint8Array([ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, ...section(1, [ [FUNC_TYPE, ...vec([[I32]]), ...vec([])], [FUNC_TYPE, ...vec([]), ...vec([])], [FUNC_TYPE, ...vec([]), ...vec([[I32]])], ]), ...section(2, [ [...name("wasi_snapshot_preview1"), ...name("proc_exit"), 0x00, TAKES_I32], [...name("crafter_runtime"), ...name("park"), 0x00, NOTHING], [...name("env"), ...name("scenario"), 0x00, GIVES_I32], [...name("env"), ...name("trace"), 0x00, TAKES_I32], ]), ...section(3, [[TAKES_I32], [TAKES_I32], [TAKES_I32], [TAKES_I32], [NOTHING], [GIVES_I32], [TAKES_I32]]), ...section(5, [[0x00, 0x01]]), // One tag, for the exception nothing catches. ...section(13, [[0x00, TAKES_I32]]), ...section(6, [[I32, 0x01, I32_CONST, 0x00, END]]), ...section(7, [ [...name("memory"), 0x02, 0], [...name("_start"), 0x00, START], [...name("frame"), 0x00, FRAME], [...name("quit"), 0x00, QUIT], ]), ...section(10, [ // END_WITH(code): park for PARK_CODE, else proc_exit(code). code([], [LOCAL_GET, 0, I32_CONST, ...sleb(PARK_CODE), I32_EQ, IF, NO_RESULT, CALL, PARK, ELSE, LOCAL_GET, 0, CALL, PROC_EXIT, END]), // NOEXCEPT(code): anything thrown through it is std::terminate. code([], [ BLOCK, NO_RESULT, TRY_TABLE, NO_RESULT, ...vec([[CATCH_ALL, 0]]), LOCAL_GET, 0, CALL, END_WITH, END, RETURN, END, I32_CONST, TERMINATE, CALL, TRACE, UNREACHABLE, ]), // CATCHES_ALL(code): try { NOEXCEPT(code); } catch (...) {} code([], [ BLOCK, NO_RESULT, TRY_TABLE, NO_RESULT, ...vec([[CATCH_ALL, 0]]), LOCAL_GET, 0, CALL, NOEXCEPT, END, RETURN, END, I32_CONST, CATCH, CALL, TRACE, ]), // CLEANS_UP(code): a frame with a destructor around CATCHES_ALL(code). code([[...uleb(1), EXNREF]], [ BLOCK, EXNREF, TRY_TABLE, NO_RESULT, ...vec([[CATCH_ALL_REF, 0]]), LOCAL_GET, 0, CALL, CATCHES_ALL, END, RETURN, END, LOCAL_SET, 1, I32_CONST, DESTRUCTOR, CALL, TRACE, LOCAL_GET, 1, THROW_REF, ]), // _start: what env.scenario says; anything else returns from main. code([[...uleb(1), I32]], [ CALL, SCENARIO, LOCAL_SET, 0, ...ifScenario(EXIT, [I32_CONST, 3, CALL, CLEANS_UP]), ...ifScenario(PARK_MAIN, [I32_CONST, 42, GLOBAL_SET, 0, I32_CONST, ...sleb(PARK_CODE), CALL, CLEANS_UP, I32_CONST, AFTER_PARK, CALL, TRACE]), ...ifScenario(THROW_UNCAUGHT, [I32_CONST, 9, THROW, 0]), ...ifScenario(CRASH, [UNREACHABLE]), ]), // frame(): the state _start left, as a later frame callback reads it. code([], [GLOBAL_GET, 0]), // quit(code): an exit from a later call, through the same frames. code([], [LOCAL_GET, 0, CALL, CLEANS_UP]), ]), ]); // The standardized encoding (try_table) is what the module and runtime.js // both depend on; an older node has only the legacy one. const exnrefProbe = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 10, 8, 1, 6, 0, 31, 64, 0, 11, 11]); if (!WebAssembly.validate(exnrefProbe)) { console.log("SKIP: this node has no WebAssembly exception handling"); process.exit(0); } if (!WebAssembly.validate(module)) { console.log("FAIL: the fixture module does not validate"); process.exit(1); } // What runtime.js expects of a page: no variants.json or files.json, so it // loads CRAFTER_WASM_URL. globalThis.window = globalThis; globalThis.addEventListener = () => {}; window.CRAFTER_WASM_URL = "fixture.wasm"; globalThis.fetch = async (url) => { if (String(url).split("?")[0] === "fixture.wasm") return new Response(module, { headers: { "Content-Type": "application/wasm" } }); return new Response(null, { status: 404 }); }; const runtime = pathToFileURL(process.argv[2]).href; const run = async (label, scenario, after = () => "") => { const trace = []; window.crafter_webbuild_env = { scenario: () => scenario, trace: (what) => { trace.push(what); } }; delete window.crafter_wasi; let outcome = "ended"; try { // A query makes it a module of its own, run afresh. await import(`${runtime}?scenario=${scenario}`); } catch (e) { outcome = `rejected with ${e?.constructor?.name}`; } const wasi = window.crafter_wasi; console.log(`${label}: ${outcome}, exitCode=${wasi?.exitCode} parked=${wasi?.parked} trace=[${trace}]${after(wasi, trace)}`); }; await run("exit", EXIT); await run("park", PARK_MAIN, (wasi, trace) => { const frame = wasi.instance.exports.frame(); let quit = "returned"; try { wasi.instance.exports.quit(7); } catch (e) { quit = `threw ${e.constructor.name}`; } return `; frame()=${frame}; quit(7) ${quit}, exitCode=${wasi.exitCode} trace=[${trace}]`; }); await run("return", 3); await run("uncaught exception", THROW_UNCAUGHT); await run("crash", CRASH);