runtime.js had no test that runs it. WasiBrowserRuntimeExit runs the real file under node, once per scenario, against a module assembled in the harness. Between _start and the call that ends it the module has the frames that caught the old exit: a destructor (catch_all_ref, then throw_ref), a catch (...) and a noexcept function, each reporting when it runs. An exit has to run none of them and record its status; a park has to run none of them, nor the code after it, and leave the program callable with its state intact; an exit from a later call has to end the program too, a return from main has to give status 0, and a crash or an exception nothing caught has to stay an error. With the JS throw put back into proc_exit, the exit from main and the exit from a later call both end in the noexcept frame's terminate. It skips without node, or with a node whose engine lacks the standardized exception handling. The Flatpak has no node, so CI does not run it; the native loop does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
167 lines
7.4 KiB
JavaScript
167 lines
7.4 KiB
JavaScript
// 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);
|