@twinkle-lang/twinkle 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/boot.wasm +0 -0
- package/bridge.wasm +0 -0
- package/index.mjs +138 -0
- package/node.mjs +94 -0
- package/package.json +13 -0
- package/runtime.mjs +598 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @twinkle-lang/twinkle
|
|
2
|
+
|
|
3
|
+
Twinkle is a statically typed language targeting WebAssembly GC. This package
|
|
4
|
+
ships both the `twk` command-line compiler and an embeddable JS library for
|
|
5
|
+
compiling and running Twinkle programs from Node.js.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @twinkle-lang/twinkle
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## CLI
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx twk run path/to/program.tw
|
|
17
|
+
npx twk build path/to/program.tw -o out.wasm
|
|
18
|
+
npx twk fmt path/to/program.tw
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Library
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { compile, run, runFile } from "@twinkle-lang/twinkle";
|
|
25
|
+
|
|
26
|
+
// Host functions declared in Twinkle as `extern canvas { fn draw_rect(...) }`
|
|
27
|
+
// are wired by passing a scoped imports object — no globalThis pollution.
|
|
28
|
+
await runFile("game.tw", {
|
|
29
|
+
imports: {
|
|
30
|
+
canvas: { draw_rect: (x, y, w, h) => { /* ... */ } },
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Host globals (Math, console, crypto, ...) resolve automatically:
|
|
35
|
+
await runFile("calc.tw");
|
|
36
|
+
|
|
37
|
+
// Compile once, run many times:
|
|
38
|
+
const wasm = await compile("game.tw");
|
|
39
|
+
await run(wasm, { imports: { canvas } });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
A missing extern import produces a clear error naming the exact `module.fn`.
|
|
43
|
+
|
|
44
|
+
Requires Node.js ≥ 22.
|
package/boot.wasm
ADDED
|
Binary file
|
package/bridge.wasm
ADDED
|
Binary file
|
package/index.mjs
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Library API for embedding Twinkle in JavaScript.
|
|
2
|
+
//
|
|
3
|
+
// import { compile, run, runFile } from "@twinkle-lang/twinkle";
|
|
4
|
+
//
|
|
5
|
+
// compile(input) -> Uint8Array (loads boot.wasm)
|
|
6
|
+
// run(wasmBytes, opts) -> exitCode (loads only bridge.wasm)
|
|
7
|
+
// runFile(path, opts) -> exitCode (compile + run)
|
|
8
|
+
|
|
9
|
+
import { readFileSync, writeFileSync, rmSync, mkdtempSync } from "node:fs";
|
|
10
|
+
import { resolve, dirname, join, basename } from "node:path";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { runWasmBytesAsync } from "./runtime.mjs";
|
|
13
|
+
|
|
14
|
+
const here = import.meta.dirname;
|
|
15
|
+
|
|
16
|
+
function readFirst(paths) {
|
|
17
|
+
let lastError;
|
|
18
|
+
for (const p of paths) {
|
|
19
|
+
try { return readFileSync(p); } catch (e) { lastError = e; }
|
|
20
|
+
}
|
|
21
|
+
throw lastError ?? new Error("no paths provided");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function loadBootWasm() {
|
|
25
|
+
const override = process.env.BOOT_WASM;
|
|
26
|
+
if (override) return readFileSync(resolve(override));
|
|
27
|
+
return readFirst([
|
|
28
|
+
`${here}/boot.wasm`,
|
|
29
|
+
`${here}/../../target/boot.wasm`,
|
|
30
|
+
]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadBridgeWasm() {
|
|
34
|
+
const override = process.env.BRIDGE_WASM;
|
|
35
|
+
if (override) return readFileSync(resolve(override));
|
|
36
|
+
return readFirst([
|
|
37
|
+
`${here}/bridge.wasm`,
|
|
38
|
+
`${here}/../bridge.wasm`,
|
|
39
|
+
]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function collectingStream() {
|
|
43
|
+
const chunks = [];
|
|
44
|
+
// Stream-decode so a multi-byte UTF-8 sequence split across writes is not
|
|
45
|
+
// corrupted; flush the decoder in text().
|
|
46
|
+
const dec = new TextDecoder();
|
|
47
|
+
return {
|
|
48
|
+
text() {
|
|
49
|
+
return chunks.join("") + dec.decode();
|
|
50
|
+
},
|
|
51
|
+
write(chunk) {
|
|
52
|
+
chunks.push(typeof chunk === "string" ? chunk : dec.decode(chunk, { stream: true }));
|
|
53
|
+
return true;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Compile Twinkle source to wasm bytes.
|
|
60
|
+
* @param {string | {source: string, path?: string}} input
|
|
61
|
+
* A file path string — full project/import support (relative `use .sibling`,
|
|
62
|
+
* walk-up to `twinkle.toml`). Or `{ source, path? }` — written to a temp dir
|
|
63
|
+
* and compiled single-file only; relative imports and project-root discovery
|
|
64
|
+
* will NOT resolve as they would at the original location.
|
|
65
|
+
* @returns {Promise<Uint8Array>}
|
|
66
|
+
*/
|
|
67
|
+
export async function compile(input, opts = {}) {
|
|
68
|
+
const bootBytes = loadBootWasm();
|
|
69
|
+
const bridgeBytes = loadBridgeWasm();
|
|
70
|
+
|
|
71
|
+
let srcPath;
|
|
72
|
+
let cleanupDir;
|
|
73
|
+
if (typeof input === "string") {
|
|
74
|
+
srcPath = resolve(input);
|
|
75
|
+
} else if (input && typeof input.source === "string") {
|
|
76
|
+
cleanupDir = mkdtempSync(join(tmpdir(), "twinkle-"));
|
|
77
|
+
srcPath = join(cleanupDir, basename(input.path ?? "main.tw"));
|
|
78
|
+
writeFileSync(srcPath, input.source);
|
|
79
|
+
} else {
|
|
80
|
+
throw new TypeError("compile: input must be a path string or { source, path? }");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// A dedicated temp dir per call: mkdtempSync's random suffix guarantees a
|
|
84
|
+
// unique output path even for concurrent same-process compiles.
|
|
85
|
+
const outDir = mkdtempSync(join(tmpdir(), "twinkle-out-"));
|
|
86
|
+
const outPath = join(outDir, "out.wasm");
|
|
87
|
+
const out = collectingStream();
|
|
88
|
+
const err = collectingStream();
|
|
89
|
+
try {
|
|
90
|
+
const code = await runWasmBytesAsync(bootBytes, {
|
|
91
|
+
programPath: "twk.wasm",
|
|
92
|
+
guestArgs: ["build", srcPath, "-o", outPath],
|
|
93
|
+
cwd: opts.cwd ?? dirname(srcPath),
|
|
94
|
+
env: process.env,
|
|
95
|
+
stdout: out,
|
|
96
|
+
stderr: err,
|
|
97
|
+
bridgeBytes,
|
|
98
|
+
});
|
|
99
|
+
if (code !== 0) {
|
|
100
|
+
throw new Error(`Twinkle compilation failed (exit ${code}):\n${err.text() || out.text()}`);
|
|
101
|
+
}
|
|
102
|
+
return new Uint8Array(readFileSync(outPath));
|
|
103
|
+
} finally {
|
|
104
|
+
try { rmSync(outDir, { recursive: true, force: true }); } catch {}
|
|
105
|
+
if (cleanupDir) { try { rmSync(cleanupDir, { recursive: true, force: true }); } catch {} }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Run pre-compiled wasm bytes with optional scoped extern imports.
|
|
111
|
+
* @param {Uint8Array} wasmBytes
|
|
112
|
+
* @param {{imports?, args?, cwd?, env?, stdout?, stderr?, path?}} opts
|
|
113
|
+
* @returns {Promise<number>} exit code
|
|
114
|
+
*/
|
|
115
|
+
export async function run(wasmBytes, opts = {}) {
|
|
116
|
+
return runWasmBytesAsync(wasmBytes, {
|
|
117
|
+
programPath: opts.path ?? "<memory>.wasm",
|
|
118
|
+
guestArgs: opts.args ?? [],
|
|
119
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
120
|
+
env: opts.env ?? process.env,
|
|
121
|
+
stdout: opts.stdout ?? process.stdout,
|
|
122
|
+
stderr: opts.stderr ?? process.stderr,
|
|
123
|
+
bridgeBytes: loadBridgeWasm(),
|
|
124
|
+
imports: opts.imports ?? {},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Compile a file then run it. */
|
|
129
|
+
export async function runFile(path, opts = {}) {
|
|
130
|
+
const wasm = await compile(path, opts);
|
|
131
|
+
return run(wasm, { ...opts, path: resolve(path) });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Compile source text then run it. */
|
|
135
|
+
export async function runSource(source, opts = {}) {
|
|
136
|
+
const wasm = await compile({ source, path: opts.path }, opts);
|
|
137
|
+
return run(wasm, opts);
|
|
138
|
+
}
|
package/node.mjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Node.js entry wrapper for the Twinkle CLI (twk).
|
|
3
|
+
//
|
|
4
|
+
// Mirrors tools/js_runtime/deno_main.mjs using Node APIs. The full self-hosted
|
|
5
|
+
// compiler (boot.wasm) handles every subcommand (build/run/ir/fmt/check/lsp);
|
|
6
|
+
// this wrapper only loads the embedded payloads, adapts stdio, and forwards
|
|
7
|
+
// process.argv into the shared runtime.
|
|
8
|
+
|
|
9
|
+
import { readFileSync, writeSync } from "node:fs";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { runWasmBytesAsync } from "./runtime.mjs";
|
|
12
|
+
|
|
13
|
+
const textEncoder = new TextEncoder();
|
|
14
|
+
const here = import.meta.dirname;
|
|
15
|
+
|
|
16
|
+
function writeAllFd(fd, bytes) {
|
|
17
|
+
let offset = 0;
|
|
18
|
+
while (offset < bytes.byteLength) {
|
|
19
|
+
const written = writeSync(fd, bytes, offset, bytes.byteLength - offset);
|
|
20
|
+
if (written <= 0) throw new Error("stdout write made no progress");
|
|
21
|
+
offset += written;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function nodeStream(fd) {
|
|
26
|
+
return {
|
|
27
|
+
fd,
|
|
28
|
+
write(chunk) {
|
|
29
|
+
const bytes = typeof chunk === "string"
|
|
30
|
+
? textEncoder.encode(chunk)
|
|
31
|
+
: new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
32
|
+
writeAllFd(fd, bytes);
|
|
33
|
+
return true;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readFirst(paths) {
|
|
39
|
+
let lastError;
|
|
40
|
+
for (const p of paths) {
|
|
41
|
+
try { return readFileSync(p); } catch (e) { lastError = e; }
|
|
42
|
+
}
|
|
43
|
+
throw lastError ?? new Error("no paths provided");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadBootWasm() {
|
|
47
|
+
const override = process.env.BOOT_WASM;
|
|
48
|
+
if (override) return readFileSync(resolve(override));
|
|
49
|
+
try {
|
|
50
|
+
return readFirst([
|
|
51
|
+
`${here}/boot.wasm`, // packaged (flat layout)
|
|
52
|
+
`${here}/../../target/boot.wasm`, // dev fallback
|
|
53
|
+
]);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.error(`Error: boot compiler wasm not found: ${e.message}`);
|
|
56
|
+
console.error("Build it with: make stage2");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadBridgeWasm() {
|
|
62
|
+
const override = process.env.BRIDGE_WASM;
|
|
63
|
+
if (override) return readFileSync(resolve(override));
|
|
64
|
+
try {
|
|
65
|
+
return readFirst([
|
|
66
|
+
`${here}/bridge.wasm`, // packaged
|
|
67
|
+
`${here}/../bridge.wasm`, // dev fallback (tools/bridge.wasm)
|
|
68
|
+
]);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error(`Error: bridge wasm not found: ${e.message}`);
|
|
71
|
+
console.error("Regenerate with: ./target/release/twk run boot/tests/gen_bridge_wasm.tw");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function main() {
|
|
77
|
+
const bootOverride = process.env.BOOT_WASM;
|
|
78
|
+
const exitCode = await runWasmBytesAsync(loadBootWasm(), {
|
|
79
|
+
programPath: bootOverride ? resolve(bootOverride) : "twk.wasm",
|
|
80
|
+
guestArgs: process.argv.slice(2),
|
|
81
|
+
cwd: process.cwd(),
|
|
82
|
+
env: process.env,
|
|
83
|
+
stdout: nodeStream(1),
|
|
84
|
+
stderr: nodeStream(2),
|
|
85
|
+
bridgeBytes: loadBridgeWasm(),
|
|
86
|
+
});
|
|
87
|
+
process.exit(exitCode);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
main().catch((e) => {
|
|
91
|
+
if (e.message?.startsWith("host.error:")) process.exit(1);
|
|
92
|
+
console.error(e.stack || e.message || e);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@twinkle-lang/twinkle",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Twinkle — a statically typed language targeting WebAssembly GC. CLI (twk) plus an embeddable compile/run library.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "twk": "node.mjs" },
|
|
7
|
+
"exports": { ".": "./index.mjs" },
|
|
8
|
+
"files": ["node.mjs", "index.mjs", "runtime.mjs", "boot.wasm", "bridge.wasm", "README.md"],
|
|
9
|
+
"engines": { "node": ">=22" },
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"publishConfig": { "access": "public" },
|
|
12
|
+
"repository": { "type": "git", "url": "git+https://github.com/curist/twinkle.git" }
|
|
13
|
+
}
|
package/runtime.mjs
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
// Shared Wasm GC runtime library for the Twinkle JavaScript host.
|
|
2
|
+
//
|
|
3
|
+
// Provides the "host" imports that Twinkle's compiler emits, using a small
|
|
4
|
+
// bridge Wasm module to create/read Wasm GC values (since JS cannot directly
|
|
5
|
+
// construct or inspect Wasm GC arrays/structs).
|
|
6
|
+
//
|
|
7
|
+
// Used by:
|
|
8
|
+
// - tools/js_runtime/deno_main.mjs (Deno standalone CLI)
|
|
9
|
+
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, readSync, writeSync } from "node:fs";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Constants
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const RESULT_TYPE_ID = 1; // matches src/types/ty.rs RESULT_TYPE_ID
|
|
18
|
+
const RESULT_OK = 0;
|
|
19
|
+
const RESULT_ERR = 1;
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// HostExit
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export class HostExit extends Error {
|
|
26
|
+
constructor(code) {
|
|
27
|
+
super(`host.exit(${code})`);
|
|
28
|
+
this.name = "HostExit";
|
|
29
|
+
this.code = code;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// String / array marshaling
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
const textDecoder = new TextDecoder();
|
|
38
|
+
const textEncoder = new TextEncoder();
|
|
39
|
+
|
|
40
|
+
const PAGE_SIZE = 65536;
|
|
41
|
+
|
|
42
|
+
// Ensure the bridge's linear memory can hold at least `needed` bytes.
|
|
43
|
+
// Returns a Uint8Array view of the memory (may be invalidated by future grows).
|
|
44
|
+
function ensureMemory(b, needed) {
|
|
45
|
+
const buf = b.memory.buffer;
|
|
46
|
+
if (buf.byteLength >= needed) return new Uint8Array(buf);
|
|
47
|
+
const pages = Math.ceil((needed - buf.byteLength) / PAGE_SIZE);
|
|
48
|
+
b.memory.grow(pages);
|
|
49
|
+
return new Uint8Array(b.memory.buffer);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function decodeString(b, ref) {
|
|
53
|
+
if (!ref) return "";
|
|
54
|
+
const len = b.string_len(ref);
|
|
55
|
+
if (len === 0) return "";
|
|
56
|
+
ensureMemory(b, len);
|
|
57
|
+
b.bulk_string_read(ref);
|
|
58
|
+
const view = new Uint8Array(b.memory.buffer, 0, len);
|
|
59
|
+
return textDecoder.decode(view);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function encodeString(b, str) {
|
|
63
|
+
const bytes = textEncoder.encode(str);
|
|
64
|
+
if (bytes.length === 0) return b.string_new(0);
|
|
65
|
+
const mem = ensureMemory(b, bytes.length);
|
|
66
|
+
mem.set(bytes);
|
|
67
|
+
return b.bulk_string_new(bytes.length);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function makeResultOk(b, value) {
|
|
71
|
+
const payload = b.array_new(1);
|
|
72
|
+
b.array_set(payload, 0, value);
|
|
73
|
+
return b.variant_new(RESULT_TYPE_ID, RESULT_OK, payload);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function makeResultErr(b, value) {
|
|
77
|
+
const payload = b.array_new(1);
|
|
78
|
+
b.array_set(payload, 0, value);
|
|
79
|
+
return b.variant_new(RESULT_TYPE_ID, RESULT_ERR, payload);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function makeStringArray(b, strings) {
|
|
83
|
+
const arr = b.array_new(strings.length);
|
|
84
|
+
for (let i = 0; i < strings.length; i++) {
|
|
85
|
+
b.array_set(arr, i, encodeString(b, strings[i]));
|
|
86
|
+
}
|
|
87
|
+
return arr;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function makeByteArray(b, bytes) {
|
|
91
|
+
if (bytes.length === 0) return b.array_new(0);
|
|
92
|
+
const mem = ensureMemory(b, bytes.length);
|
|
93
|
+
mem.set(bytes);
|
|
94
|
+
return b.bulk_bytes_new(bytes.length);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function decodeStringArray(b, arrRef) {
|
|
98
|
+
const len = b.array_len(arrRef);
|
|
99
|
+
const out = new Array(len);
|
|
100
|
+
for (let i = 0; i < len; i++) {
|
|
101
|
+
out[i] = decodeString(b, b.array_get(arrRef, i));
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function decodeByteArray(b, arrRef) {
|
|
107
|
+
const len = b.array_len(arrRef);
|
|
108
|
+
if (len === 0) return Buffer.alloc(0);
|
|
109
|
+
ensureMemory(b, len);
|
|
110
|
+
b.bulk_bytes_read(arrRef);
|
|
111
|
+
// ArrayBuffer.slice copies (unlike Buffer.slice which creates a view)
|
|
112
|
+
return Buffer.from(b.memory.buffer.slice(0, len));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Stdin helpers
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
function sleepSyncMs(ms) {
|
|
120
|
+
if (ms <= 0) return;
|
|
121
|
+
const sab = new SharedArrayBuffer(4);
|
|
122
|
+
Atomics.wait(new Int32Array(sab), 0, 0, ms);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readStdinTimeout(maxBytes, timeoutMs, runtime) {
|
|
126
|
+
const n = Number(maxBytes);
|
|
127
|
+
const timeout = Number(timeoutMs);
|
|
128
|
+
if (n <= 0) return Buffer.alloc(0);
|
|
129
|
+
|
|
130
|
+
// Accessing process.stdin asks Node/libuv to put fd 0 in non-blocking mode
|
|
131
|
+
// for pipes/ttys, which lets fs.readSync report EAGAIN instead of blocking
|
|
132
|
+
// forever when no LSP bytes are currently available.
|
|
133
|
+
void process.stdin;
|
|
134
|
+
|
|
135
|
+
const deadline = performance.now() + Math.max(0, timeout);
|
|
136
|
+
const buf = Buffer.allocUnsafe(n);
|
|
137
|
+
while (true) {
|
|
138
|
+
try {
|
|
139
|
+
const read = readSync(0, buf, 0, n, null);
|
|
140
|
+
if (read === 0) runtime.stdinEof = true;
|
|
141
|
+
return buf.subarray(0, read);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
if (e?.code === "EAGAIN" || e?.code === "EWOULDBLOCK") {
|
|
144
|
+
const remaining = deadline - performance.now();
|
|
145
|
+
if (remaining <= 0) return Buffer.alloc(0);
|
|
146
|
+
sleepSyncMs(Math.min(10, remaining));
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
throw e;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readStdinTimeoutAsync(maxBytes, timeoutMs, runtime) {
|
|
155
|
+
const n = Number(maxBytes);
|
|
156
|
+
const timeout = Number(timeoutMs);
|
|
157
|
+
if (n <= 0) return Promise.resolve(Buffer.alloc(0));
|
|
158
|
+
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
let timer = null;
|
|
161
|
+
|
|
162
|
+
let settled = false;
|
|
163
|
+
|
|
164
|
+
const finish = (chunk) => {
|
|
165
|
+
if (settled) return;
|
|
166
|
+
settled = true;
|
|
167
|
+
if (timer !== null) { clearTimeout(timer); timer = null; }
|
|
168
|
+
process.stdin.removeListener("readable", onReadable);
|
|
169
|
+
process.stdin.removeListener("end", onEnd);
|
|
170
|
+
resolve(chunk);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const tryRead = () => {
|
|
174
|
+
// read() with no argument returns whatever is buffered (1..any bytes),
|
|
175
|
+
// matching the sync path's "read up to n" semantics.
|
|
176
|
+
const chunk = process.stdin.read();
|
|
177
|
+
if (chunk !== null) {
|
|
178
|
+
// If the stream returned more than n bytes, push the excess back.
|
|
179
|
+
if (chunk.length > n) {
|
|
180
|
+
process.stdin.unshift(chunk.subarray(n));
|
|
181
|
+
finish(chunk.subarray(0, n));
|
|
182
|
+
} else {
|
|
183
|
+
finish(chunk);
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const onReadable = () => { tryRead(); };
|
|
191
|
+
|
|
192
|
+
const onEnd = () => {
|
|
193
|
+
runtime.stdinEof = true;
|
|
194
|
+
finish(Buffer.alloc(0));
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Try immediate read from the stream buffer
|
|
198
|
+
if (tryRead()) return;
|
|
199
|
+
|
|
200
|
+
if (process.stdin.readableEnded) {
|
|
201
|
+
runtime.stdinEof = true;
|
|
202
|
+
resolve(Buffer.alloc(0));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
timer = setTimeout(() => finish(Buffer.alloc(0)), Math.max(0, timeout));
|
|
207
|
+
|
|
208
|
+
process.stdin.once("readable", onReadable);
|
|
209
|
+
process.stdin.once("end", onEnd);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Extern auto-bridging
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Resolve each wasm extern import to a JS function.
|
|
219
|
+
* Resolution order per import: scoped `imports[module][name]`, then
|
|
220
|
+
* `globals[module][name]`. Imports already satisfied by `hostImports`, or that
|
|
221
|
+
* are not functions, are skipped. Returns the resolved bindings plus a list of
|
|
222
|
+
* unresolved "module.name" strings.
|
|
223
|
+
*/
|
|
224
|
+
export function resolveExternImports(importList, hostImports, imports = {}, globals = globalThis) {
|
|
225
|
+
const found = [];
|
|
226
|
+
const missing = [];
|
|
227
|
+
for (const imp of importList) {
|
|
228
|
+
if (hostImports[imp.module]?.[imp.name] !== undefined) continue;
|
|
229
|
+
if (imp.kind !== "function") continue;
|
|
230
|
+
|
|
231
|
+
const scopedRecv = imports[imp.module];
|
|
232
|
+
const scoped = scopedRecv?.[imp.name];
|
|
233
|
+
if (typeof scoped === "function") {
|
|
234
|
+
found.push({ module: imp.module, name: imp.name, fn: scoped, recv: scopedRecv });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const globalRecv = globals[imp.module];
|
|
239
|
+
const global = globalRecv?.[imp.name];
|
|
240
|
+
if (typeof global === "function") {
|
|
241
|
+
found.push({ module: imp.module, name: imp.name, fn: global, recv: globalRecv });
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
missing.push(`${imp.module}.${imp.name}`);
|
|
246
|
+
}
|
|
247
|
+
return { found, missing };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Auto-bridge extern imports by resolving each to `imports[module][name]` (a
|
|
252
|
+
* scoped per-run object) or `globalThis[module][name]`, wrapping with type
|
|
253
|
+
* conversions for Twinkle's extern-safe types:
|
|
254
|
+
* - String params (GC refs) are decoded via bridge
|
|
255
|
+
* - String returns from JS are encoded via bridge
|
|
256
|
+
* - Int (bigint), Float (number), Bool (i32) pass through
|
|
257
|
+
* Throws a single aggregated error if any extern import is unsatisfied.
|
|
258
|
+
*/
|
|
259
|
+
function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, imports = {}) {
|
|
260
|
+
let importList;
|
|
261
|
+
try {
|
|
262
|
+
importList = WebAssembly.Module.imports(wasmModule);
|
|
263
|
+
} catch {
|
|
264
|
+
// Module.imports may fail on GC modules in some runtimes; nothing to bridge.
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const { found, missing } = resolveExternImports(importList, hostImports, imports);
|
|
269
|
+
|
|
270
|
+
if (missing.length > 0) {
|
|
271
|
+
const [m0, f0] = missing[0].split(".");
|
|
272
|
+
throw new Error(
|
|
273
|
+
`Missing host import(s): ${missing.join(", ")}\n` +
|
|
274
|
+
`Provide them via the run() "imports" option ` +
|
|
275
|
+
`(e.g. { imports: { ${m0}: { ${f0}: fn } } }) or define them on globalThis.`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const marshalArgs = (args) => args.map((arg) => {
|
|
280
|
+
if (typeof arg === "bigint") return Number(arg);
|
|
281
|
+
if (typeof arg === "number") return arg;
|
|
282
|
+
// GC ref — assume string
|
|
283
|
+
return decodeString(b, arg);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const marshalReturn = (result) => {
|
|
287
|
+
if (result === undefined || result === null) return;
|
|
288
|
+
if (typeof result === "string") return encodeString(b, result);
|
|
289
|
+
if (typeof result === "number") return result;
|
|
290
|
+
if (typeof result === "bigint") return result;
|
|
291
|
+
return result;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
for (const { module, name, fn, recv } of found) {
|
|
295
|
+
let bridgedFn;
|
|
296
|
+
if (jspi) {
|
|
297
|
+
// JSPI mode: async wrapper so Promise-returning JS functions suspend
|
|
298
|
+
// Wasm. Non-Promise returns pass through without suspension.
|
|
299
|
+
const asyncWrapper = async (...args) =>
|
|
300
|
+
marshalReturn(await fn.apply(recv, marshalArgs(args)));
|
|
301
|
+
bridgedFn = new WebAssembly.Suspending(asyncWrapper);
|
|
302
|
+
} else {
|
|
303
|
+
// Sync mode: detect and reject Promise returns
|
|
304
|
+
bridgedFn = (...args) => {
|
|
305
|
+
const result = fn.apply(recv, marshalArgs(args));
|
|
306
|
+
if (result instanceof Promise) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
`Extern ${module}.${name} returned a Promise, but JSPI is not available. ` +
|
|
309
|
+
`Promise-returning externs require a runtime with WebAssembly.Suspending/promising support.`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return marshalReturn(result);
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
if (!hostImports[module]) hostImports[module] = {};
|
|
316
|
+
hostImports[module][name] = bridgedFn;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// Host imports
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
function writeAllFdSync(fd, bytes) {
|
|
325
|
+
let offset = 0;
|
|
326
|
+
while (offset < bytes.byteLength) {
|
|
327
|
+
const written = writeSync(fd, bytes, offset, bytes.byteLength - offset);
|
|
328
|
+
if (written <= 0) {
|
|
329
|
+
throw new Error("stdout write made no progress");
|
|
330
|
+
}
|
|
331
|
+
offset += written;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function write(stream, text) {
|
|
336
|
+
stream.write(text);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function makeHostImports(b, runtime, bridgeBytes) {
|
|
340
|
+
return {
|
|
341
|
+
host: {
|
|
342
|
+
// --- I/O ---
|
|
343
|
+
print: (s) => write(runtime.stdout, decodeString(b, s)),
|
|
344
|
+
println: (s) => write(runtime.stdout, decodeString(b, s) + "\n"),
|
|
345
|
+
error: (s) => {
|
|
346
|
+
const msg = decodeString(b, s);
|
|
347
|
+
write(runtime.stderr, msg + "\n");
|
|
348
|
+
throw new Error("host.error: " + msg);
|
|
349
|
+
},
|
|
350
|
+
eprint: (s) => write(runtime.stderr, decodeString(b, s)),
|
|
351
|
+
eprintln: (s) => write(runtime.stderr, decodeString(b, s) + "\n"),
|
|
352
|
+
|
|
353
|
+
// --- String conversion ---
|
|
354
|
+
f64_to_string: (n) => encodeString(b, n.toString()),
|
|
355
|
+
|
|
356
|
+
// --- Process ---
|
|
357
|
+
args: () => makeStringArray(b, runtime.programArgs),
|
|
358
|
+
env: (keyRef) => {
|
|
359
|
+
const key = decodeString(b, keyRef);
|
|
360
|
+
const val = runtime.env[key];
|
|
361
|
+
return val === undefined ? makeStringArray(b, []) : makeStringArray(b, [val]);
|
|
362
|
+
},
|
|
363
|
+
cwd: () => encodeString(b, runtime.cwd),
|
|
364
|
+
exit: (code) => {
|
|
365
|
+
const c = typeof code === "bigint" ? Number(code) : code;
|
|
366
|
+
throw new HostExit(c);
|
|
367
|
+
},
|
|
368
|
+
now: () => performance.now(),
|
|
369
|
+
run_wasm: (bytesRef, argvRef) => {
|
|
370
|
+
const childBytes = decodeByteArray(b, bytesRef);
|
|
371
|
+
const childArgv = decodeStringArray(b, argvRef);
|
|
372
|
+
const [programPath, ...guestArgs] = childArgv;
|
|
373
|
+
const exitCode = runWasmBytes(childBytes, {
|
|
374
|
+
programPath: programPath ?? "<memory>.wasm",
|
|
375
|
+
guestArgs,
|
|
376
|
+
cwd: runtime.cwd,
|
|
377
|
+
env: runtime.env,
|
|
378
|
+
stdout: runtime.stdout,
|
|
379
|
+
stderr: runtime.stderr,
|
|
380
|
+
imports: runtime.imports,
|
|
381
|
+
bridgeBytes,
|
|
382
|
+
});
|
|
383
|
+
return BigInt(exitCode);
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
// --- File system ---
|
|
387
|
+
read_file: (pathRef) => {
|
|
388
|
+
const filePath = resolve(runtime.cwd, decodeString(b, pathRef));
|
|
389
|
+
try {
|
|
390
|
+
const bytes = readFileSync(filePath);
|
|
391
|
+
return makeResultOk(b, makeByteArray(b, bytes));
|
|
392
|
+
} catch (e) {
|
|
393
|
+
const msg = `host.read_file failed for '${filePath}': ${e.message}`;
|
|
394
|
+
return makeResultErr(b, encodeString(b, msg));
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
write_file: (pathRef, contentRef) => {
|
|
398
|
+
const filePath = resolve(runtime.cwd, decodeString(b, pathRef));
|
|
399
|
+
writeFileSync(filePath, decodeString(b, contentRef));
|
|
400
|
+
},
|
|
401
|
+
write_bytes: (pathRef, bytesRef) => {
|
|
402
|
+
const filePath = resolve(runtime.cwd, decodeString(b, pathRef));
|
|
403
|
+
writeFileSync(filePath, decodeByteArray(b, bytesRef));
|
|
404
|
+
},
|
|
405
|
+
stdin_read_chunk: (maxBytes) => makeByteArray(b, readStdinTimeout(maxBytes, 2147483647, runtime)),
|
|
406
|
+
stdin_read_timeout: (maxBytes, timeoutMs) => makeByteArray(b, readStdinTimeout(maxBytes, timeoutMs, runtime)),
|
|
407
|
+
stdin_eof: () => runtime.stdinEof ? 1 : 0,
|
|
408
|
+
stdout_write_bytes: (bytesRef) => {
|
|
409
|
+
const bytes = decodeByteArray(b, bytesRef);
|
|
410
|
+
if (runtime.stdout?.fd !== undefined) {
|
|
411
|
+
writeAllFdSync(runtime.stdout.fd, bytes);
|
|
412
|
+
} else {
|
|
413
|
+
runtime.stdout.write(Buffer.from(bytes));
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
mkdirp: (pathRef) => {
|
|
417
|
+
const dirPath = resolve(runtime.cwd, decodeString(b, pathRef));
|
|
418
|
+
mkdirSync(dirPath, { recursive: true });
|
|
419
|
+
},
|
|
420
|
+
list_dir: (pathRef) => {
|
|
421
|
+
const dirPath = resolve(runtime.cwd, decodeString(b, pathRef));
|
|
422
|
+
return makeStringArray(b, readdirSync(dirPath));
|
|
423
|
+
},
|
|
424
|
+
exists: (pathRef) => {
|
|
425
|
+
const filePath = resolve(runtime.cwd, decodeString(b, pathRef));
|
|
426
|
+
return existsSync(filePath) ? 1 : 0;
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
// --- Parsing ---
|
|
430
|
+
parse_int: (sRef) => {
|
|
431
|
+
const s = decodeString(b, sRef);
|
|
432
|
+
const n = parseInt(s, 10);
|
|
433
|
+
return isNaN(n) ? 0n : BigInt(n);
|
|
434
|
+
},
|
|
435
|
+
parse_float: (sRef) => {
|
|
436
|
+
const s = decodeString(b, sRef);
|
|
437
|
+
const f = parseFloat(s);
|
|
438
|
+
return isNaN(f) ? [0.0, 0] : [f, 1];
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
// Bridge instantiation
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
|
|
448
|
+
function instantiateBridge(bridgeBytes) {
|
|
449
|
+
const bridgeModule = new WebAssembly.Module(bridgeBytes);
|
|
450
|
+
const bridgeInstance = new WebAssembly.Instance(bridgeModule);
|
|
451
|
+
return bridgeInstance.exports;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
// JSPI feature detection
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
|
|
458
|
+
export const hasJspi =
|
|
459
|
+
typeof WebAssembly.Suspending === "function" &&
|
|
460
|
+
typeof WebAssembly.promising === "function";
|
|
461
|
+
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
// Wasm preparation (shared by sync and async paths)
|
|
464
|
+
// ---------------------------------------------------------------------------
|
|
465
|
+
|
|
466
|
+
function prepareWasm(wasmBytes, opts, { jspi = false } = {}) {
|
|
467
|
+
const {
|
|
468
|
+
programPath = "<memory>.wasm",
|
|
469
|
+
guestArgs = [],
|
|
470
|
+
cwd = process.cwd(),
|
|
471
|
+
env = process.env,
|
|
472
|
+
stdout = process.stdout,
|
|
473
|
+
stderr = process.stderr,
|
|
474
|
+
bridgeBytes,
|
|
475
|
+
imports = {},
|
|
476
|
+
} = opts;
|
|
477
|
+
|
|
478
|
+
if (!bridgeBytes) {
|
|
479
|
+
throw new Error("runWasmBytes: bridgeBytes is required");
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const b = instantiateBridge(bridgeBytes);
|
|
483
|
+
const runtime = {
|
|
484
|
+
programArgs: [programPath, ...guestArgs],
|
|
485
|
+
cwd,
|
|
486
|
+
env,
|
|
487
|
+
stdout,
|
|
488
|
+
stderr,
|
|
489
|
+
stdinEof: false,
|
|
490
|
+
imports,
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const hostImports = makeHostImports(b, runtime, bridgeBytes);
|
|
494
|
+
const mainModule = new WebAssembly.Module(wasmBytes);
|
|
495
|
+
autoBridgeExternImports(mainModule, hostImports, b, jspi, imports);
|
|
496
|
+
|
|
497
|
+
return { mainModule, hostImports, b, runtime };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
// Public API — synchronous
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
export function runWasmBytes(wasmBytes, opts = {}) {
|
|
505
|
+
const { mainModule, hostImports } = prepareWasm(wasmBytes, opts);
|
|
506
|
+
try {
|
|
507
|
+
const instance = new WebAssembly.Instance(mainModule, hostImports);
|
|
508
|
+
// Boot-compiled modules export __twinkle_start instead of using a Wasm
|
|
509
|
+
// start section. Stage0-compiled modules still use the start section and
|
|
510
|
+
// run during instantiation above.
|
|
511
|
+
if (instance.exports.__twinkle_start) {
|
|
512
|
+
instance.exports.__twinkle_start();
|
|
513
|
+
}
|
|
514
|
+
return 0;
|
|
515
|
+
} catch (e) {
|
|
516
|
+
if (e instanceof HostExit) {
|
|
517
|
+
return e.code;
|
|
518
|
+
}
|
|
519
|
+
throw e;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export function runWasmFile(wasmPath, opts = {}) {
|
|
524
|
+
return runWasmBytes(readFileSync(wasmPath), {
|
|
525
|
+
programPath: resolve(wasmPath),
|
|
526
|
+
...opts,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
// Public API — async (JSPI-aware)
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
|
|
534
|
+
export async function runWasmBytesAsync(wasmBytes, opts = {}) {
|
|
535
|
+
const { mainModule, hostImports, b, runtime } = prepareWasm(wasmBytes, opts, { jspi: hasJspi });
|
|
536
|
+
|
|
537
|
+
if (hasJspi) {
|
|
538
|
+
// Phase 3: wrap stdin reads as suspending imports so the Node event loop
|
|
539
|
+
// stays free while Twinkle waits for LSP input. Keep chunk and timeout
|
|
540
|
+
// reads on the same stream-based path; mixing process.stdin.read() with
|
|
541
|
+
// fs.readSync(0, ...) can strand bytes in Node's stream buffer.
|
|
542
|
+
hostImports.host.stdin_read_chunk = new WebAssembly.Suspending(
|
|
543
|
+
async (maxBytes) =>
|
|
544
|
+
makeByteArray(b, await readStdinTimeoutAsync(maxBytes, 2147483647, runtime)),
|
|
545
|
+
);
|
|
546
|
+
hostImports.host.stdin_read_timeout = new WebAssembly.Suspending(
|
|
547
|
+
async (maxBytes, timeoutMs) =>
|
|
548
|
+
makeByteArray(b, await readStdinTimeoutAsync(maxBytes, timeoutMs, runtime)),
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
// Phase 4: wrap run_wasm as a suspending import so child programs can
|
|
552
|
+
// themselves use JSPI suspending imports.
|
|
553
|
+
const childBridgeBytes = opts.bridgeBytes;
|
|
554
|
+
hostImports.host.run_wasm = new WebAssembly.Suspending(
|
|
555
|
+
async (bytesRef, argvRef) => {
|
|
556
|
+
const childBytes = decodeByteArray(b, bytesRef);
|
|
557
|
+
const childArgv = decodeStringArray(b, argvRef);
|
|
558
|
+
const [programPath, ...guestArgs] = childArgv;
|
|
559
|
+
const exitCode = await runWasmBytesAsync(childBytes, {
|
|
560
|
+
programPath: programPath ?? "<memory>.wasm",
|
|
561
|
+
guestArgs,
|
|
562
|
+
cwd: runtime.cwd,
|
|
563
|
+
env: runtime.env,
|
|
564
|
+
stdout: runtime.stdout,
|
|
565
|
+
stderr: runtime.stderr,
|
|
566
|
+
imports: runtime.imports,
|
|
567
|
+
bridgeBytes: childBridgeBytes,
|
|
568
|
+
});
|
|
569
|
+
return BigInt(exitCode);
|
|
570
|
+
},
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
const instance = new WebAssembly.Instance(mainModule, hostImports);
|
|
576
|
+
if (instance.exports.__twinkle_start) {
|
|
577
|
+
if (hasJspi) {
|
|
578
|
+
const start = WebAssembly.promising(instance.exports.__twinkle_start);
|
|
579
|
+
await start();
|
|
580
|
+
} else {
|
|
581
|
+
instance.exports.__twinkle_start();
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return 0;
|
|
585
|
+
} catch (e) {
|
|
586
|
+
if (e instanceof HostExit) {
|
|
587
|
+
return e.code;
|
|
588
|
+
}
|
|
589
|
+
throw e;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export async function runWasmFileAsync(wasmPath, opts = {}) {
|
|
594
|
+
return runWasmBytesAsync(readFileSync(wasmPath), {
|
|
595
|
+
programPath: resolve(wasmPath),
|
|
596
|
+
...opts,
|
|
597
|
+
});
|
|
598
|
+
}
|