@twinkle-lang/twinkle 0.2.0 → 0.4.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/index.mjs +3 -0
- package/node.mjs +2 -0
- package/node_host.mjs +133 -0
- package/package.json +10 -3
- package/runtime.mjs +150 -180
- package/web.mjs +77 -0
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,6 +123,7 @@ 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 ?? {},
|
|
125
128
|
});
|
|
126
129
|
}
|
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;
|
|
@@ -104,6 +105,7 @@ async function main() {
|
|
|
104
105
|
stdout: nodeStream(1),
|
|
105
106
|
stderr: nodeStream(2),
|
|
106
107
|
bridgeBytes: loadBridgeWasm(),
|
|
108
|
+
host: nodeHost,
|
|
107
109
|
});
|
|
108
110
|
process.exit(exitCode);
|
|
109
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,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinkle-lang/twinkle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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
|
+
"./web": "./web.mjs",
|
|
10
|
+
"./runtime.mjs": "./runtime.mjs",
|
|
11
|
+
"./node_host.mjs": "./node_host.mjs",
|
|
12
|
+
"./boot.wasm": "./boot.wasm",
|
|
13
|
+
"./bridge.wasm": "./bridge.wasm"
|
|
14
|
+
},
|
|
15
|
+
"files": ["node.mjs", "index.mjs", "web.mjs", "runtime.mjs", "node_host.mjs", "boot.wasm", "bridge.wasm", "README.md"],
|
|
9
16
|
"engines": { "node": ">=22" },
|
|
10
17
|
"license": "MIT",
|
|
11
18
|
"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
|
// ---------------------------------------------------------------------------
|
|
@@ -215,11 +121,18 @@ function readStdinTimeoutAsync(maxBytes, timeoutMs, runtime) {
|
|
|
215
121
|
// ---------------------------------------------------------------------------
|
|
216
122
|
|
|
217
123
|
/**
|
|
218
|
-
* Resolve each wasm extern import to a JS function.
|
|
124
|
+
* Resolve each wasm extern import to a JS function (and its optional arg spec).
|
|
125
|
+
*
|
|
126
|
+
* A scoped `imports[module][name]` entry is either:
|
|
127
|
+
* - a function — the implementation, with default arg marshaling, or
|
|
128
|
+
* - `{ fn?, args? }` — `fn` is the implementation (omit it to fall back to
|
|
129
|
+
* `globals[module][name]`), and `args` is the per-arg marshal spec, an array
|
|
130
|
+
* of `"raw" | "string"` keyed by position.
|
|
131
|
+
*
|
|
219
132
|
* Resolution order per import: scoped `imports[module][name]`, then
|
|
220
133
|
* `globals[module][name]`. Imports already satisfied by `hostImports`, or that
|
|
221
|
-
* are not functions, are skipped. Returns the resolved bindings
|
|
222
|
-
* unresolved "module.name" strings.
|
|
134
|
+
* are not functions, are skipped. Returns the resolved bindings (each with
|
|
135
|
+
* `fn`, `recv`, and `args`) plus a list of unresolved "module.name" strings.
|
|
223
136
|
*/
|
|
224
137
|
export function resolveExternImports(importList, hostImports, imports = {}, globals = globalThis) {
|
|
225
138
|
const found = [];
|
|
@@ -228,21 +141,33 @@ export function resolveExternImports(importList, hostImports, imports = {}, glob
|
|
|
228
141
|
if (hostImports[imp.module]?.[imp.name] !== undefined) continue;
|
|
229
142
|
if (imp.kind !== "function") continue;
|
|
230
143
|
|
|
231
|
-
const
|
|
232
|
-
|
|
144
|
+
const scoped = imports[imp.module]?.[imp.name];
|
|
145
|
+
let fn;
|
|
146
|
+
let recv;
|
|
147
|
+
let args;
|
|
148
|
+
|
|
233
149
|
if (typeof scoped === "function") {
|
|
234
|
-
|
|
235
|
-
|
|
150
|
+
fn = scoped;
|
|
151
|
+
recv = imports[imp.module];
|
|
152
|
+
} else if (scoped && typeof scoped === "object") {
|
|
153
|
+
args = scoped.args;
|
|
154
|
+
if (typeof scoped.fn === "function") {
|
|
155
|
+
fn = scoped.fn;
|
|
156
|
+
recv = imports[imp.module];
|
|
157
|
+
} else {
|
|
158
|
+
fn = globals[imp.module]?.[imp.name];
|
|
159
|
+
recv = globals[imp.module];
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
fn = globals[imp.module]?.[imp.name];
|
|
163
|
+
recv = globals[imp.module];
|
|
236
164
|
}
|
|
237
165
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
continue;
|
|
166
|
+
if (typeof fn === "function") {
|
|
167
|
+
found.push({ module: imp.module, name: imp.name, fn, recv, args });
|
|
168
|
+
} else {
|
|
169
|
+
missing.push(`${imp.module}.${imp.name}`);
|
|
243
170
|
}
|
|
244
|
-
|
|
245
|
-
missing.push(`${imp.module}.${imp.name}`);
|
|
246
171
|
}
|
|
247
172
|
return { found, missing };
|
|
248
173
|
}
|
|
@@ -276,10 +201,16 @@ function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, impor
|
|
|
276
201
|
);
|
|
277
202
|
}
|
|
278
203
|
|
|
279
|
-
|
|
204
|
+
// Per-import arg marshaling honors the extern's optional `args` spec — an
|
|
205
|
+
// array of `"raw" | "string"` keyed by arg position. `"raw"` passes the value
|
|
206
|
+
// through untouched, essential for externref args (e.g. a canvas 2D context),
|
|
207
|
+
// since calling decodeString on an opaque host object recurses until a stack
|
|
208
|
+
// overflow in some engines (notably Safari). Without a spec entry, a
|
|
209
|
+
// non-numeric arg is assumed to be a Wasm GC string and decoded.
|
|
210
|
+
const makeMarshalArgs = (spec) => (args) => args.map((arg, i) => {
|
|
280
211
|
if (typeof arg === "bigint") return Number(arg);
|
|
281
212
|
if (typeof arg === "number") return arg;
|
|
282
|
-
|
|
213
|
+
if (spec?.[i] === "raw") return arg;
|
|
283
214
|
return decodeString(b, arg);
|
|
284
215
|
});
|
|
285
216
|
|
|
@@ -291,7 +222,8 @@ function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, impor
|
|
|
291
222
|
return result;
|
|
292
223
|
};
|
|
293
224
|
|
|
294
|
-
for (const { module, name, fn, recv } of found) {
|
|
225
|
+
for (const { module, name, fn, recv, args } of found) {
|
|
226
|
+
const marshalArgs = makeMarshalArgs(args);
|
|
295
227
|
let bridgedFn;
|
|
296
228
|
if (jspi) {
|
|
297
229
|
// JSPI mode: async wrapper so Promise-returning JS functions suspend
|
|
@@ -321,17 +253,6 @@ function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, impor
|
|
|
321
253
|
// Host imports
|
|
322
254
|
// ---------------------------------------------------------------------------
|
|
323
255
|
|
|
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
256
|
function write(stream, text) {
|
|
336
257
|
stream.write(text);
|
|
337
258
|
}
|
|
@@ -378,16 +299,17 @@ function makeHostImports(b, runtime, bridgeBytes) {
|
|
|
378
299
|
stdout: runtime.stdout,
|
|
379
300
|
stderr: runtime.stderr,
|
|
380
301
|
imports: runtime.imports,
|
|
302
|
+
host: runtime.host,
|
|
381
303
|
bridgeBytes,
|
|
382
304
|
});
|
|
383
305
|
return BigInt(exitCode);
|
|
384
306
|
},
|
|
385
307
|
|
|
386
|
-
// --- File system ---
|
|
308
|
+
// --- File system (routed through the injected host adapter) ---
|
|
387
309
|
read_file: (pathRef) => {
|
|
388
|
-
const filePath =
|
|
310
|
+
const filePath = runtime.host.resolvePath(runtime.cwd, decodeString(b, pathRef));
|
|
389
311
|
try {
|
|
390
|
-
const bytes =
|
|
312
|
+
const bytes = runtime.host.readFile(filePath);
|
|
391
313
|
return makeResultOk(b, makeByteArray(b, bytes));
|
|
392
314
|
} catch (e) {
|
|
393
315
|
const msg = `host.read_file failed for '${filePath}': ${e.message}`;
|
|
@@ -395,35 +317,33 @@ function makeHostImports(b, runtime, bridgeBytes) {
|
|
|
395
317
|
}
|
|
396
318
|
},
|
|
397
319
|
write_file: (pathRef, contentRef) => {
|
|
398
|
-
const filePath =
|
|
399
|
-
|
|
320
|
+
const filePath = runtime.host.resolvePath(runtime.cwd, decodeString(b, pathRef));
|
|
321
|
+
runtime.host.writeFile(filePath, decodeString(b, contentRef));
|
|
400
322
|
},
|
|
401
323
|
write_bytes: (pathRef, bytesRef) => {
|
|
402
|
-
const filePath =
|
|
403
|
-
|
|
324
|
+
const filePath = runtime.host.resolvePath(runtime.cwd, decodeString(b, pathRef));
|
|
325
|
+
runtime.host.writeBytes(filePath, decodeByteArray(b, bytesRef));
|
|
404
326
|
},
|
|
405
|
-
stdin_read_chunk: (maxBytes) => makeByteArray(b,
|
|
406
|
-
stdin_read_timeout: (maxBytes, timeoutMs) => makeByteArray(b,
|
|
327
|
+
stdin_read_chunk: (maxBytes) => makeByteArray(b, runtime.host.readStdin(maxBytes, 2147483647, runtime)),
|
|
328
|
+
stdin_read_timeout: (maxBytes, timeoutMs) => makeByteArray(b, runtime.host.readStdin(maxBytes, timeoutMs, runtime)),
|
|
407
329
|
stdin_eof: () => runtime.stdinEof ? 1 : 0,
|
|
408
330
|
stdout_write_bytes: (bytesRef) => {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
runtime.stdout.write(Buffer.from(bytes));
|
|
414
|
-
}
|
|
331
|
+
// Streams accept a Uint8Array chunk; each adapter's write() handles the
|
|
332
|
+
// platform write (fd write on Node, writeSync on Deno, postMessage in a
|
|
333
|
+
// worker), so no Buffer/fd handling is needed here.
|
|
334
|
+
runtime.stdout.write(decodeByteArray(b, bytesRef));
|
|
415
335
|
},
|
|
416
336
|
mkdirp: (pathRef) => {
|
|
417
|
-
const dirPath =
|
|
418
|
-
|
|
337
|
+
const dirPath = runtime.host.resolvePath(runtime.cwd, decodeString(b, pathRef));
|
|
338
|
+
runtime.host.mkdirp(dirPath);
|
|
419
339
|
},
|
|
420
340
|
list_dir: (pathRef) => {
|
|
421
|
-
const dirPath =
|
|
422
|
-
return makeStringArray(b,
|
|
341
|
+
const dirPath = runtime.host.resolvePath(runtime.cwd, decodeString(b, pathRef));
|
|
342
|
+
return makeStringArray(b, runtime.host.listDir(dirPath));
|
|
423
343
|
},
|
|
424
344
|
exists: (pathRef) => {
|
|
425
|
-
const filePath =
|
|
426
|
-
return
|
|
345
|
+
const filePath = runtime.host.resolvePath(runtime.cwd, decodeString(b, pathRef));
|
|
346
|
+
return runtime.host.exists(filePath) ? 1 : 0;
|
|
427
347
|
},
|
|
428
348
|
|
|
429
349
|
// --- Parsing ---
|
|
@@ -441,6 +361,64 @@ function makeHostImports(b, runtime, bridgeBytes) {
|
|
|
441
361
|
};
|
|
442
362
|
}
|
|
443
363
|
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// In-memory host adapter
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* A host adapter backed by an in-memory file map — the default for browsers and
|
|
370
|
+
* other environments without a real filesystem. Reads/writes a `Map<string,
|
|
371
|
+
* Uint8Array>`; stdin is always EOF. Pure JS (no `node:` deps), so it is safe to
|
|
372
|
+
* load anywhere.
|
|
373
|
+
*
|
|
374
|
+
* @param {Map<string,Uint8Array> | Iterable<[string,Uint8Array]>} [initialFiles]
|
|
375
|
+
*/
|
|
376
|
+
export function createMemoryHost(initialFiles) {
|
|
377
|
+
const files = initialFiles instanceof Map ? initialFiles : new Map(initialFiles ?? []);
|
|
378
|
+
const norm = (p) => (p.startsWith("/") ? p : "/" + p).replace(/\/+/g, "/");
|
|
379
|
+
return {
|
|
380
|
+
files,
|
|
381
|
+
resolvePath(cwd, p) {
|
|
382
|
+
if (p.startsWith("/")) return norm(p);
|
|
383
|
+
return norm((cwd.endsWith("/") ? cwd : cwd + "/") + p);
|
|
384
|
+
},
|
|
385
|
+
readFile(path) {
|
|
386
|
+
const data = files.get(norm(path));
|
|
387
|
+
if (data === undefined) throw new Error(`file not found: ${path}`);
|
|
388
|
+
return data;
|
|
389
|
+
},
|
|
390
|
+
writeFile(path, text) { files.set(norm(path), textEncoder.encode(text)); },
|
|
391
|
+
writeBytes(path, bytes) { files.set(norm(path), bytes); },
|
|
392
|
+
exists(path) {
|
|
393
|
+
const np = norm(path);
|
|
394
|
+
if (files.has(np)) return true;
|
|
395
|
+
const prefix = np.endsWith("/") ? np : np + "/";
|
|
396
|
+
for (const k of files.keys()) if (k.startsWith(prefix)) return true;
|
|
397
|
+
return false;
|
|
398
|
+
},
|
|
399
|
+
listDir(path) {
|
|
400
|
+
const prefix = norm(path).replace(/\/?$/, "/");
|
|
401
|
+
const names = new Set();
|
|
402
|
+
for (const k of files.keys()) {
|
|
403
|
+
if (k.startsWith(prefix)) {
|
|
404
|
+
const name = k.slice(prefix.length).split("/")[0];
|
|
405
|
+
if (name) names.add(name);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return [...names].sort();
|
|
409
|
+
},
|
|
410
|
+
mkdirp() { /* virtual dirs are implicit */ },
|
|
411
|
+
readStdin(_maxBytes, _timeoutMs, runtime) {
|
|
412
|
+
runtime.stdinEof = true;
|
|
413
|
+
return new Uint8Array(0);
|
|
414
|
+
},
|
|
415
|
+
readStdinAsync(_maxBytes, _timeoutMs, runtime) {
|
|
416
|
+
runtime.stdinEof = true;
|
|
417
|
+
return Promise.resolve(new Uint8Array(0));
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
444
422
|
// ---------------------------------------------------------------------------
|
|
445
423
|
// Bridge instantiation
|
|
446
424
|
// ---------------------------------------------------------------------------
|
|
@@ -467,17 +445,21 @@ function prepareWasm(wasmBytes, opts, { jspi = false } = {}) {
|
|
|
467
445
|
const {
|
|
468
446
|
programPath = "<memory>.wasm",
|
|
469
447
|
guestArgs = [],
|
|
470
|
-
cwd =
|
|
471
|
-
env =
|
|
472
|
-
stdout
|
|
473
|
-
stderr
|
|
448
|
+
cwd = "/",
|
|
449
|
+
env = {},
|
|
450
|
+
stdout,
|
|
451
|
+
stderr,
|
|
474
452
|
bridgeBytes,
|
|
453
|
+
host,
|
|
475
454
|
imports = {},
|
|
476
455
|
} = opts;
|
|
477
456
|
|
|
478
457
|
if (!bridgeBytes) {
|
|
479
458
|
throw new Error("runWasmBytes: bridgeBytes is required");
|
|
480
459
|
}
|
|
460
|
+
if (!host) {
|
|
461
|
+
throw new Error("runWasmBytes: host adapter is required (see node_host.mjs)");
|
|
462
|
+
}
|
|
481
463
|
|
|
482
464
|
const b = instantiateBridge(bridgeBytes);
|
|
483
465
|
const runtime = {
|
|
@@ -487,6 +469,7 @@ function prepareWasm(wasmBytes, opts, { jspi = false } = {}) {
|
|
|
487
469
|
stdout,
|
|
488
470
|
stderr,
|
|
489
471
|
stdinEof: false,
|
|
472
|
+
host,
|
|
490
473
|
imports,
|
|
491
474
|
};
|
|
492
475
|
|
|
@@ -520,13 +503,6 @@ export function runWasmBytes(wasmBytes, opts = {}) {
|
|
|
520
503
|
}
|
|
521
504
|
}
|
|
522
505
|
|
|
523
|
-
export function runWasmFile(wasmPath, opts = {}) {
|
|
524
|
-
return runWasmBytes(readFileSync(wasmPath), {
|
|
525
|
-
programPath: resolve(wasmPath),
|
|
526
|
-
...opts,
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
|
|
530
506
|
// ---------------------------------------------------------------------------
|
|
531
507
|
// Public API — async (JSPI-aware)
|
|
532
508
|
// ---------------------------------------------------------------------------
|
|
@@ -535,21 +511,21 @@ export async function runWasmBytesAsync(wasmBytes, opts = {}) {
|
|
|
535
511
|
const { mainModule, hostImports, b, runtime } = prepareWasm(wasmBytes, opts, { jspi: hasJspi });
|
|
536
512
|
|
|
537
513
|
if (hasJspi) {
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
//
|
|
514
|
+
// Wrap stdin reads as suspending imports so the event loop stays free while
|
|
515
|
+
// Twinkle waits for LSP input. Keep chunk and timeout reads on the same
|
|
516
|
+
// stream-based path; mixing process.stdin.read() with fs.readSync(0, ...)
|
|
517
|
+
// can strand bytes in Node's stream buffer.
|
|
542
518
|
hostImports.host.stdin_read_chunk = new WebAssembly.Suspending(
|
|
543
519
|
async (maxBytes) =>
|
|
544
|
-
makeByteArray(b, await
|
|
520
|
+
makeByteArray(b, await runtime.host.readStdinAsync(maxBytes, 2147483647, runtime)),
|
|
545
521
|
);
|
|
546
522
|
hostImports.host.stdin_read_timeout = new WebAssembly.Suspending(
|
|
547
523
|
async (maxBytes, timeoutMs) =>
|
|
548
|
-
makeByteArray(b, await
|
|
524
|
+
makeByteArray(b, await runtime.host.readStdinAsync(maxBytes, timeoutMs, runtime)),
|
|
549
525
|
);
|
|
550
526
|
|
|
551
|
-
//
|
|
552
|
-
//
|
|
527
|
+
// Wrap run_wasm as a suspending import so child programs can themselves use
|
|
528
|
+
// JSPI suspending imports.
|
|
553
529
|
const childBridgeBytes = opts.bridgeBytes;
|
|
554
530
|
hostImports.host.run_wasm = new WebAssembly.Suspending(
|
|
555
531
|
async (bytesRef, argvRef) => {
|
|
@@ -564,6 +540,7 @@ export async function runWasmBytesAsync(wasmBytes, opts = {}) {
|
|
|
564
540
|
stdout: runtime.stdout,
|
|
565
541
|
stderr: runtime.stderr,
|
|
566
542
|
imports: runtime.imports,
|
|
543
|
+
host: runtime.host,
|
|
567
544
|
bridgeBytes: childBridgeBytes,
|
|
568
545
|
});
|
|
569
546
|
return BigInt(exitCode);
|
|
@@ -589,10 +566,3 @@ export async function runWasmBytesAsync(wasmBytes, opts = {}) {
|
|
|
589
566
|
throw e;
|
|
590
567
|
}
|
|
591
568
|
}
|
|
592
|
-
|
|
593
|
-
export async function runWasmFileAsync(wasmPath, opts = {}) {
|
|
594
|
-
return runWasmBytesAsync(readFileSync(wasmPath), {
|
|
595
|
-
programPath: resolve(wasmPath),
|
|
596
|
-
...opts,
|
|
597
|
-
});
|
|
598
|
-
}
|
package/web.mjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Browser entry for embedding Twinkle.
|
|
2
|
+
//
|
|
3
|
+
// import { run } from "@twinkle-lang/twinkle/web";
|
|
4
|
+
// await run(source, { stdout, stderr });
|
|
5
|
+
//
|
|
6
|
+
// Unlike index.mjs (Node: temp files, node:fs), this is browser-first: it
|
|
7
|
+
// self-loads the compiler wasm that ships beside it and uses an in-memory
|
|
8
|
+
// filesystem by default, so callers never touch boot.wasm/bridge.wasm or
|
|
9
|
+
// construct a host adapter.
|
|
10
|
+
|
|
11
|
+
import { runWasmBytesAsync, createMemoryHost } from "./runtime.mjs";
|
|
12
|
+
|
|
13
|
+
const textEncoder = new TextEncoder();
|
|
14
|
+
|
|
15
|
+
// boot.wasm / bridge.wasm are published next to this module. `new URL(...,
|
|
16
|
+
// import.meta.url)` lets the consumer's bundler emit them as assets (Vite,
|
|
17
|
+
// webpack, native ESM all understand this), so no `?url` import is needed.
|
|
18
|
+
let assetsPromise;
|
|
19
|
+
|
|
20
|
+
function loadAssets() {
|
|
21
|
+
if (!assetsPromise) {
|
|
22
|
+
assetsPromise = Promise.all([
|
|
23
|
+
fetch(new URL("./boot.wasm", import.meta.url)).then((r) => r.arrayBuffer()),
|
|
24
|
+
fetch(new URL("./bridge.wasm", import.meta.url)).then((r) => r.arrayBuffer()),
|
|
25
|
+
]).then(([boot, bridge]) => ({
|
|
26
|
+
bootBytes: new Uint8Array(boot),
|
|
27
|
+
bridgeBytes: new Uint8Array(bridge),
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
return assetsPromise;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Pre-fetch the compiler wasm so the first run() doesn't pay the load latency. */
|
|
34
|
+
export function load() {
|
|
35
|
+
return loadAssets();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const defaultStream = (sink) => ({
|
|
39
|
+
write(chunk) {
|
|
40
|
+
sink(typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk));
|
|
41
|
+
return true;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compile and run Twinkle source in the browser.
|
|
47
|
+
*
|
|
48
|
+
* @param {string | Uint8Array} source Twinkle source for the entry module.
|
|
49
|
+
* @param {object} [opts]
|
|
50
|
+
* @param {object} [opts.stdout] Stream with write(chunk); defaults to console.log.
|
|
51
|
+
* @param {object} [opts.stderr] Stream with write(chunk); defaults to console.error.
|
|
52
|
+
* @param {object} [opts.env] Environment map exposed to the program.
|
|
53
|
+
* @param {object} [opts.imports] Extern imports — `module → fn | { fn?, args? }`
|
|
54
|
+
* (else resolved via globalThis). `args` is the per-arg spec, e.g.
|
|
55
|
+
* `{ canvas: { fill_rect: { fn, args: ['raw','raw','raw','raw','raw'] } } }`.
|
|
56
|
+
* @param {Iterable<[string,Uint8Array]>} [opts.files] Extra in-memory files (multi-file projects).
|
|
57
|
+
* @param {object} [opts.host] Override the host adapter entirely (advanced).
|
|
58
|
+
* @returns {Promise<number>} exit code
|
|
59
|
+
*/
|
|
60
|
+
export async function run(source, opts = {}) {
|
|
61
|
+
const { bootBytes, bridgeBytes } = await loadAssets();
|
|
62
|
+
|
|
63
|
+
const files = new Map(opts.files ?? []);
|
|
64
|
+
files.set("/input/main.tw", typeof source === "string" ? textEncoder.encode(source) : source);
|
|
65
|
+
|
|
66
|
+
return runWasmBytesAsync(bootBytes, {
|
|
67
|
+
programPath: "twk.wasm",
|
|
68
|
+
guestArgs: ["run", "/input/main.tw"],
|
|
69
|
+
cwd: "/",
|
|
70
|
+
env: opts.env ?? {},
|
|
71
|
+
stdout: opts.stdout ?? defaultStream((s) => console.log(s)),
|
|
72
|
+
stderr: opts.stderr ?? defaultStream((s) => console.error(s)),
|
|
73
|
+
bridgeBytes,
|
|
74
|
+
host: opts.host ?? createMemoryHost(files),
|
|
75
|
+
imports: opts.imports ?? {},
|
|
76
|
+
});
|
|
77
|
+
}
|