@twinkle-lang/twinkle 0.1.0 → 0.3.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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # @twinkle-lang/twinkle
2
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.
3
+ Twinkle is a statically typed, value-oriented language targeting Wasm GC. This
4
+ package ships both the `twk` command-line compiler and an embeddable JS library
5
+ for compiling and running Twinkle programs from Node.js.
6
6
 
7
7
  ## Install
8
8
 
package/boot.wasm CHANGED
Binary file
package/index.mjs CHANGED
@@ -10,6 +10,7 @@ import { readFileSync, writeFileSync, rmSync, mkdtempSync } from "node:fs";
10
10
  import { resolve, dirname, join, basename } from "node:path";
11
11
  import { tmpdir } from "node:os";
12
12
  import { runWasmBytesAsync } from "./runtime.mjs";
13
+ import { nodeHost } from "./node_host.mjs";
13
14
 
14
15
  const here = import.meta.dirname;
15
16
 
@@ -95,6 +96,7 @@ export async function compile(input, opts = {}) {
95
96
  stdout: out,
96
97
  stderr: err,
97
98
  bridgeBytes,
99
+ host: nodeHost,
98
100
  });
99
101
  if (code !== 0) {
100
102
  throw new Error(`Twinkle compilation failed (exit ${code}):\n${err.text() || out.text()}`);
@@ -121,7 +123,9 @@ export async function run(wasmBytes, opts = {}) {
121
123
  stdout: opts.stdout ?? process.stdout,
122
124
  stderr: opts.stderr ?? process.stderr,
123
125
  bridgeBytes: loadBridgeWasm(),
126
+ host: nodeHost,
124
127
  imports: opts.imports ?? {},
128
+ marshalSpec: opts.marshalSpec ?? {},
125
129
  });
126
130
  }
127
131
 
package/node.mjs CHANGED
@@ -9,6 +9,7 @@
9
9
  import { readFileSync, writeSync } from "node:fs";
10
10
  import { resolve } from "node:path";
11
11
  import { runWasmBytesAsync } from "./runtime.mjs";
12
+ import { nodeHost } from "./node_host.mjs";
12
13
 
13
14
  const textEncoder = new TextEncoder();
14
15
  const here = import.meta.dirname;
@@ -58,6 +59,23 @@ function loadBootWasm() {
58
59
  }
59
60
  }
60
61
 
62
+ function loadPackageVersion() {
63
+ if (process.env.TWK_VERSION) return process.env.TWK_VERSION;
64
+ try {
65
+ const pkg = JSON.parse(readFileSync(`${here}/package.json`, "utf8"));
66
+ if (typeof pkg.version === "string") return pkg.version;
67
+ } catch (_) {
68
+ // Not running from the packaged npm layout.
69
+ }
70
+ try {
71
+ const pkg = JSON.parse(readFileSync(`${here}/../npm/package.json`, "utf8"));
72
+ if (typeof pkg.version === "string") return pkg.version;
73
+ } catch (_) {
74
+ // Development fallback failed; let the compiler report "dev".
75
+ }
76
+ return undefined;
77
+ }
78
+
61
79
  function loadBridgeWasm() {
62
80
  const override = process.env.BRIDGE_WASM;
63
81
  if (override) return readFileSync(resolve(override));
@@ -75,14 +93,19 @@ function loadBridgeWasm() {
75
93
 
76
94
  async function main() {
77
95
  const bootOverride = process.env.BOOT_WASM;
96
+ const env = { ...process.env };
97
+ const version = loadPackageVersion();
98
+ if (version !== undefined) env.TWK_VERSION = version;
99
+
78
100
  const exitCode = await runWasmBytesAsync(loadBootWasm(), {
79
101
  programPath: bootOverride ? resolve(bootOverride) : "twk.wasm",
80
102
  guestArgs: process.argv.slice(2),
81
103
  cwd: process.cwd(),
82
- env: process.env,
104
+ env,
83
105
  stdout: nodeStream(1),
84
106
  stderr: nodeStream(2),
85
107
  bridgeBytes: loadBridgeWasm(),
108
+ host: nodeHost,
86
109
  });
87
110
  process.exit(exitCode);
88
111
  }
package/node_host.mjs ADDED
@@ -0,0 +1,133 @@
1
+ // Node/Deno host adapter for the shared Twinkle runtime.
2
+ //
3
+ // runtime.mjs is host-agnostic: it routes every filesystem and stdin host
4
+ // import through an injected `host` object. This module provides that object
5
+ // for Node and Deno (Deno satisfies node:fs / node:path via its node-compat
6
+ // layer). A browser supplies its own adapter with the same shape.
7
+ //
8
+ // Host interface:
9
+ // resolvePath(cwd, p) -> string
10
+ // readFile(path) -> Uint8Array (throws on missing)
11
+ // writeFile(path, text)
12
+ // writeBytes(path, bytes: Uint8Array)
13
+ // exists(path) -> boolean
14
+ // listDir(path) -> string[]
15
+ // mkdirp(path)
16
+ // readStdin(maxBytes, timeoutMs, runtime) -> Uint8Array (sync)
17
+ // readStdinAsync(maxBytes, timeoutMs, runtime) -> Promise<Uint8Array>
18
+ //
19
+ // The stdin helpers set runtime.stdinEof when the stream reaches EOF.
20
+
21
+ import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, readSync } from "node:fs";
22
+ import { resolve } from "node:path";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Stdin helpers
26
+ // ---------------------------------------------------------------------------
27
+
28
+ function sleepSyncMs(ms) {
29
+ if (ms <= 0) return;
30
+ const sab = new SharedArrayBuffer(4);
31
+ Atomics.wait(new Int32Array(sab), 0, 0, ms);
32
+ }
33
+
34
+ function readStdinTimeout(maxBytes, timeoutMs, runtime) {
35
+ const n = Number(maxBytes);
36
+ const timeout = Number(timeoutMs);
37
+ if (n <= 0) return Buffer.alloc(0);
38
+
39
+ // Accessing process.stdin asks Node/libuv to put fd 0 in non-blocking mode
40
+ // for pipes/ttys, which lets fs.readSync report EAGAIN instead of blocking
41
+ // forever when no LSP bytes are currently available.
42
+ void process.stdin;
43
+
44
+ const deadline = performance.now() + Math.max(0, timeout);
45
+ const buf = Buffer.allocUnsafe(n);
46
+ while (true) {
47
+ try {
48
+ const read = readSync(0, buf, 0, n, null);
49
+ if (read === 0) runtime.stdinEof = true;
50
+ return buf.subarray(0, read);
51
+ } catch (e) {
52
+ if (e?.code === "EAGAIN" || e?.code === "EWOULDBLOCK") {
53
+ const remaining = deadline - performance.now();
54
+ if (remaining <= 0) return Buffer.alloc(0);
55
+ sleepSyncMs(Math.min(10, remaining));
56
+ continue;
57
+ }
58
+ throw e;
59
+ }
60
+ }
61
+ }
62
+
63
+ function readStdinTimeoutAsync(maxBytes, timeoutMs, runtime) {
64
+ const n = Number(maxBytes);
65
+ const timeout = Number(timeoutMs);
66
+ if (n <= 0) return Promise.resolve(Buffer.alloc(0));
67
+
68
+ return new Promise((resolve) => {
69
+ let timer = null;
70
+ let settled = false;
71
+
72
+ const finish = (chunk) => {
73
+ if (settled) return;
74
+ settled = true;
75
+ if (timer !== null) { clearTimeout(timer); timer = null; }
76
+ process.stdin.removeListener("readable", onReadable);
77
+ process.stdin.removeListener("end", onEnd);
78
+ resolve(chunk);
79
+ };
80
+
81
+ const tryRead = () => {
82
+ // read() with no argument returns whatever is buffered (1..any bytes),
83
+ // matching the sync path's "read up to n" semantics.
84
+ const chunk = process.stdin.read();
85
+ if (chunk !== null) {
86
+ // If the stream returned more than n bytes, push the excess back.
87
+ if (chunk.length > n) {
88
+ process.stdin.unshift(chunk.subarray(n));
89
+ finish(chunk.subarray(0, n));
90
+ } else {
91
+ finish(chunk);
92
+ }
93
+ return true;
94
+ }
95
+ return false;
96
+ };
97
+
98
+ const onReadable = () => { tryRead(); };
99
+
100
+ const onEnd = () => {
101
+ runtime.stdinEof = true;
102
+ finish(Buffer.alloc(0));
103
+ };
104
+
105
+ if (tryRead()) return;
106
+
107
+ if (process.stdin.readableEnded) {
108
+ runtime.stdinEof = true;
109
+ resolve(Buffer.alloc(0));
110
+ return;
111
+ }
112
+
113
+ timer = setTimeout(() => finish(Buffer.alloc(0)), Math.max(0, timeout));
114
+ process.stdin.once("readable", onReadable);
115
+ process.stdin.once("end", onEnd);
116
+ });
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Adapter
121
+ // ---------------------------------------------------------------------------
122
+
123
+ export const nodeHost = {
124
+ resolvePath(cwd, p) { return resolve(cwd, p); },
125
+ readFile(path) { return readFileSync(path); },
126
+ writeFile(path, text) { writeFileSync(path, text); },
127
+ writeBytes(path, bytes) { writeFileSync(path, bytes); },
128
+ exists(path) { return existsSync(path); },
129
+ listDir(path) { return readdirSync(path); },
130
+ mkdirp(path) { mkdirSync(path, { recursive: true }); },
131
+ readStdin: readStdinTimeout,
132
+ readStdinAsync: readStdinTimeoutAsync,
133
+ };
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "@twinkle-lang/twinkle",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Twinkle — a statically typed language targeting WebAssembly GC. CLI (twk) plus an embeddable compile/run library.",
5
5
  "type": "module",
6
6
  "bin": { "twk": "node.mjs" },
7
- "exports": { ".": "./index.mjs" },
8
- "files": ["node.mjs", "index.mjs", "runtime.mjs", "boot.wasm", "bridge.wasm", "README.md"],
7
+ "exports": {
8
+ ".": "./index.mjs",
9
+ "./runtime.mjs": "./runtime.mjs",
10
+ "./node_host.mjs": "./node_host.mjs",
11
+ "./boot.wasm": "./boot.wasm",
12
+ "./bridge.wasm": "./bridge.wasm"
13
+ },
14
+ "files": ["node.mjs", "index.mjs", "runtime.mjs", "node_host.mjs", "boot.wasm", "bridge.wasm", "README.md"],
9
15
  "engines": { "node": ">=22" },
10
16
  "license": "MIT",
11
17
  "publishConfig": { "access": "public" },
package/runtime.mjs CHANGED
@@ -4,11 +4,15 @@
4
4
  // bridge Wasm module to create/read Wasm GC values (since JS cannot directly
5
5
  // construct or inspect Wasm GC arrays/structs).
6
6
  //
7
+ // Host-agnostic: all filesystem and stdin host imports are routed through an
8
+ // injected `host` adapter (see tools/js_runtime/node_host.mjs for Node/Deno,
9
+ // or the playground's browser adapter). This module contains no `node:`
10
+ // imports, so it loads unchanged in a browser/worker.
11
+ //
7
12
  // 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";
13
+ // - tools/js_runtime/deno_main.mjs (Deno standalone CLI, host: nodeHost)
14
+ // - tools/js_runtime/node_main.mjs (Node CLI, host: nodeHost)
15
+ // - tools/js_runtime/index.mjs (embeddable library, host: nodeHost)
12
16
 
13
17
  // ---------------------------------------------------------------------------
14
18
  // Constants
@@ -105,109 +109,11 @@ function decodeStringArray(b, arrRef) {
105
109
 
106
110
  function decodeByteArray(b, arrRef) {
107
111
  const len = b.array_len(arrRef);
108
- if (len === 0) return Buffer.alloc(0);
112
+ if (len === 0) return new Uint8Array(0);
109
113
  ensureMemory(b, len);
110
114
  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
- });
115
+ // ArrayBuffer.slice copies, so the returned view owns its bytes.
116
+ return new Uint8Array(b.memory.buffer.slice(0, len));
211
117
  }
212
118
 
213
119
  // ---------------------------------------------------------------------------
@@ -256,7 +162,7 @@ export function resolveExternImports(importList, hostImports, imports = {}, glob
256
162
  * - Int (bigint), Float (number), Bool (i32) pass through
257
163
  * Throws a single aggregated error if any extern import is unsatisfied.
258
164
  */
259
- function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, imports = {}) {
165
+ function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, imports = {}, marshalSpec = {}) {
260
166
  let importList;
261
167
  try {
262
168
  importList = WebAssembly.Module.imports(wasmModule);
@@ -276,10 +182,16 @@ function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, impor
276
182
  );
277
183
  }
278
184
 
279
- const marshalArgs = (args) => args.map((arg) => {
185
+ // Per-import arg marshaling honors an optional spec: `marshalSpec[module][name]`
186
+ // is an array of `"raw" | "string"` keyed by arg position. `"raw"` passes the
187
+ // value through untouched — essential for externref args (e.g. a canvas 2D
188
+ // context), since calling decodeString on an opaque host object recurses until
189
+ // a stack overflow in some engines (notably Safari). Without a spec entry, a
190
+ // non-numeric arg is assumed to be a Wasm GC string and decoded.
191
+ const makeMarshalArgs = (spec) => (args) => args.map((arg, i) => {
280
192
  if (typeof arg === "bigint") return Number(arg);
281
193
  if (typeof arg === "number") return arg;
282
- // GC ref assume string
194
+ if (spec?.[i] === "raw") return arg;
283
195
  return decodeString(b, arg);
284
196
  });
285
197
 
@@ -292,6 +204,7 @@ function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, impor
292
204
  };
293
205
 
294
206
  for (const { module, name, fn, recv } of found) {
207
+ const marshalArgs = makeMarshalArgs(marshalSpec[module]?.[name]);
295
208
  let bridgedFn;
296
209
  if (jspi) {
297
210
  // JSPI mode: async wrapper so Promise-returning JS functions suspend
@@ -321,17 +234,6 @@ function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, impor
321
234
  // Host imports
322
235
  // ---------------------------------------------------------------------------
323
236
 
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
237
  function write(stream, text) {
336
238
  stream.write(text);
337
239
  }
@@ -378,16 +280,18 @@ function makeHostImports(b, runtime, bridgeBytes) {
378
280
  stdout: runtime.stdout,
379
281
  stderr: runtime.stderr,
380
282
  imports: runtime.imports,
283
+ marshalSpec: runtime.marshalSpec,
284
+ host: runtime.host,
381
285
  bridgeBytes,
382
286
  });
383
287
  return BigInt(exitCode);
384
288
  },
385
289
 
386
- // --- File system ---
290
+ // --- File system (routed through the injected host adapter) ---
387
291
  read_file: (pathRef) => {
388
- const filePath = resolve(runtime.cwd, decodeString(b, pathRef));
292
+ const filePath = runtime.host.resolvePath(runtime.cwd, decodeString(b, pathRef));
389
293
  try {
390
- const bytes = readFileSync(filePath);
294
+ const bytes = runtime.host.readFile(filePath);
391
295
  return makeResultOk(b, makeByteArray(b, bytes));
392
296
  } catch (e) {
393
297
  const msg = `host.read_file failed for '${filePath}': ${e.message}`;
@@ -395,35 +299,33 @@ function makeHostImports(b, runtime, bridgeBytes) {
395
299
  }
396
300
  },
397
301
  write_file: (pathRef, contentRef) => {
398
- const filePath = resolve(runtime.cwd, decodeString(b, pathRef));
399
- writeFileSync(filePath, decodeString(b, contentRef));
302
+ const filePath = runtime.host.resolvePath(runtime.cwd, decodeString(b, pathRef));
303
+ runtime.host.writeFile(filePath, decodeString(b, contentRef));
400
304
  },
401
305
  write_bytes: (pathRef, bytesRef) => {
402
- const filePath = resolve(runtime.cwd, decodeString(b, pathRef));
403
- writeFileSync(filePath, decodeByteArray(b, bytesRef));
306
+ const filePath = runtime.host.resolvePath(runtime.cwd, decodeString(b, pathRef));
307
+ runtime.host.writeBytes(filePath, decodeByteArray(b, bytesRef));
404
308
  },
405
- stdin_read_chunk: (maxBytes) => makeByteArray(b, readStdinTimeout(maxBytes, 2147483647, runtime)),
406
- stdin_read_timeout: (maxBytes, timeoutMs) => makeByteArray(b, readStdinTimeout(maxBytes, timeoutMs, runtime)),
309
+ stdin_read_chunk: (maxBytes) => makeByteArray(b, runtime.host.readStdin(maxBytes, 2147483647, runtime)),
310
+ stdin_read_timeout: (maxBytes, timeoutMs) => makeByteArray(b, runtime.host.readStdin(maxBytes, timeoutMs, runtime)),
407
311
  stdin_eof: () => runtime.stdinEof ? 1 : 0,
408
312
  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
- }
313
+ // Streams accept a Uint8Array chunk; each adapter's write() handles the
314
+ // platform write (fd write on Node, writeSync on Deno, postMessage in a
315
+ // worker), so no Buffer/fd handling is needed here.
316
+ runtime.stdout.write(decodeByteArray(b, bytesRef));
415
317
  },
416
318
  mkdirp: (pathRef) => {
417
- const dirPath = resolve(runtime.cwd, decodeString(b, pathRef));
418
- mkdirSync(dirPath, { recursive: true });
319
+ const dirPath = runtime.host.resolvePath(runtime.cwd, decodeString(b, pathRef));
320
+ runtime.host.mkdirp(dirPath);
419
321
  },
420
322
  list_dir: (pathRef) => {
421
- const dirPath = resolve(runtime.cwd, decodeString(b, pathRef));
422
- return makeStringArray(b, readdirSync(dirPath));
323
+ const dirPath = runtime.host.resolvePath(runtime.cwd, decodeString(b, pathRef));
324
+ return makeStringArray(b, runtime.host.listDir(dirPath));
423
325
  },
424
326
  exists: (pathRef) => {
425
- const filePath = resolve(runtime.cwd, decodeString(b, pathRef));
426
- return existsSync(filePath) ? 1 : 0;
327
+ const filePath = runtime.host.resolvePath(runtime.cwd, decodeString(b, pathRef));
328
+ return runtime.host.exists(filePath) ? 1 : 0;
427
329
  },
428
330
 
429
331
  // --- Parsing ---
@@ -467,17 +369,22 @@ function prepareWasm(wasmBytes, opts, { jspi = false } = {}) {
467
369
  const {
468
370
  programPath = "<memory>.wasm",
469
371
  guestArgs = [],
470
- cwd = process.cwd(),
471
- env = process.env,
472
- stdout = process.stdout,
473
- stderr = process.stderr,
372
+ cwd = "/",
373
+ env = {},
374
+ stdout,
375
+ stderr,
474
376
  bridgeBytes,
377
+ host,
475
378
  imports = {},
379
+ marshalSpec = {},
476
380
  } = opts;
477
381
 
478
382
  if (!bridgeBytes) {
479
383
  throw new Error("runWasmBytes: bridgeBytes is required");
480
384
  }
385
+ if (!host) {
386
+ throw new Error("runWasmBytes: host adapter is required (see node_host.mjs)");
387
+ }
481
388
 
482
389
  const b = instantiateBridge(bridgeBytes);
483
390
  const runtime = {
@@ -487,12 +394,14 @@ function prepareWasm(wasmBytes, opts, { jspi = false } = {}) {
487
394
  stdout,
488
395
  stderr,
489
396
  stdinEof: false,
397
+ host,
490
398
  imports,
399
+ marshalSpec,
491
400
  };
492
401
 
493
402
  const hostImports = makeHostImports(b, runtime, bridgeBytes);
494
403
  const mainModule = new WebAssembly.Module(wasmBytes);
495
- autoBridgeExternImports(mainModule, hostImports, b, jspi, imports);
404
+ autoBridgeExternImports(mainModule, hostImports, b, jspi, imports, marshalSpec);
496
405
 
497
406
  return { mainModule, hostImports, b, runtime };
498
407
  }
@@ -520,13 +429,6 @@ export function runWasmBytes(wasmBytes, opts = {}) {
520
429
  }
521
430
  }
522
431
 
523
- export function runWasmFile(wasmPath, opts = {}) {
524
- return runWasmBytes(readFileSync(wasmPath), {
525
- programPath: resolve(wasmPath),
526
- ...opts,
527
- });
528
- }
529
-
530
432
  // ---------------------------------------------------------------------------
531
433
  // Public API — async (JSPI-aware)
532
434
  // ---------------------------------------------------------------------------
@@ -535,21 +437,21 @@ export async function runWasmBytesAsync(wasmBytes, opts = {}) {
535
437
  const { mainModule, hostImports, b, runtime } = prepareWasm(wasmBytes, opts, { jspi: hasJspi });
536
438
 
537
439
  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.
440
+ // Wrap stdin reads as suspending imports so the event loop stays free while
441
+ // Twinkle waits for LSP input. Keep chunk and timeout reads on the same
442
+ // stream-based path; mixing process.stdin.read() with fs.readSync(0, ...)
443
+ // can strand bytes in Node's stream buffer.
542
444
  hostImports.host.stdin_read_chunk = new WebAssembly.Suspending(
543
445
  async (maxBytes) =>
544
- makeByteArray(b, await readStdinTimeoutAsync(maxBytes, 2147483647, runtime)),
446
+ makeByteArray(b, await runtime.host.readStdinAsync(maxBytes, 2147483647, runtime)),
545
447
  );
546
448
  hostImports.host.stdin_read_timeout = new WebAssembly.Suspending(
547
449
  async (maxBytes, timeoutMs) =>
548
- makeByteArray(b, await readStdinTimeoutAsync(maxBytes, timeoutMs, runtime)),
450
+ makeByteArray(b, await runtime.host.readStdinAsync(maxBytes, timeoutMs, runtime)),
549
451
  );
550
452
 
551
- // Phase 4: wrap run_wasm as a suspending import so child programs can
552
- // themselves use JSPI suspending imports.
453
+ // Wrap run_wasm as a suspending import so child programs can themselves use
454
+ // JSPI suspending imports.
553
455
  const childBridgeBytes = opts.bridgeBytes;
554
456
  hostImports.host.run_wasm = new WebAssembly.Suspending(
555
457
  async (bytesRef, argvRef) => {
@@ -564,6 +466,8 @@ export async function runWasmBytesAsync(wasmBytes, opts = {}) {
564
466
  stdout: runtime.stdout,
565
467
  stderr: runtime.stderr,
566
468
  imports: runtime.imports,
469
+ marshalSpec: runtime.marshalSpec,
470
+ host: runtime.host,
567
471
  bridgeBytes: childBridgeBytes,
568
472
  });
569
473
  return BigInt(exitCode);
@@ -589,10 +493,3 @@ export async function runWasmBytesAsync(wasmBytes, opts = {}) {
589
493
  throw e;
590
494
  }
591
495
  }
592
-
593
- export async function runWasmFileAsync(wasmPath, opts = {}) {
594
- return runWasmBytesAsync(readFileSync(wasmPath), {
595
- programPath: resolve(wasmPath),
596
- ...opts,
597
- });
598
- }