@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 +3 -3
- package/boot.wasm +0 -0
- package/index.mjs +4 -0
- package/node.mjs +24 -1
- package/node_host.mjs +133 -0
- package/package.json +9 -3
- package/runtime.mjs +64 -167
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @twinkle-lang/twinkle
|
|
2
2
|
|
|
3
|
-
Twinkle is a statically typed language targeting
|
|
4
|
-
ships both the `twk` command-line compiler and an embeddable JS library
|
|
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
|
|
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.
|
|
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": {
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
112
|
+
if (len === 0) return new Uint8Array(0);
|
|
109
113
|
ensureMemory(b, len);
|
|
110
114
|
b.bulk_bytes_read(arrRef);
|
|
111
|
-
// ArrayBuffer.slice copies
|
|
112
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
292
|
+
const filePath = runtime.host.resolvePath(runtime.cwd, decodeString(b, pathRef));
|
|
389
293
|
try {
|
|
390
|
-
const bytes =
|
|
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 =
|
|
399
|
-
|
|
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 =
|
|
403
|
-
|
|
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,
|
|
406
|
-
stdin_read_timeout: (maxBytes, timeoutMs) => makeByteArray(b,
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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 =
|
|
418
|
-
|
|
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 =
|
|
422
|
-
return makeStringArray(b,
|
|
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 =
|
|
426
|
-
return
|
|
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 =
|
|
471
|
-
env =
|
|
472
|
-
stdout
|
|
473
|
-
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
|
-
//
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
//
|
|
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
|
|
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
|
|
450
|
+
makeByteArray(b, await runtime.host.readStdinAsync(maxBytes, timeoutMs, runtime)),
|
|
549
451
|
);
|
|
550
452
|
|
|
551
|
-
//
|
|
552
|
-
//
|
|
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
|
-
}
|