@twinkle-lang/twinkle 0.3.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 +0 -1
- package/package.json +3 -2
- package/runtime.mjs +100 -27
- package/web.mjs +77 -0
package/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,17 +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
7
|
"exports": {
|
|
8
8
|
".": "./index.mjs",
|
|
9
|
+
"./web": "./web.mjs",
|
|
9
10
|
"./runtime.mjs": "./runtime.mjs",
|
|
10
11
|
"./node_host.mjs": "./node_host.mjs",
|
|
11
12
|
"./boot.wasm": "./boot.wasm",
|
|
12
13
|
"./bridge.wasm": "./bridge.wasm"
|
|
13
14
|
},
|
|
14
|
-
"files": ["node.mjs", "index.mjs", "runtime.mjs", "node_host.mjs", "boot.wasm", "bridge.wasm", "README.md"],
|
|
15
|
+
"files": ["node.mjs", "index.mjs", "web.mjs", "runtime.mjs", "node_host.mjs", "boot.wasm", "bridge.wasm", "README.md"],
|
|
15
16
|
"engines": { "node": ">=22" },
|
|
16
17
|
"license": "MIT",
|
|
17
18
|
"publishConfig": { "access": "public" },
|
package/runtime.mjs
CHANGED
|
@@ -121,11 +121,18 @@ function decodeByteArray(b, arrRef) {
|
|
|
121
121
|
// ---------------------------------------------------------------------------
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
|
-
* 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
|
+
*
|
|
125
132
|
* Resolution order per import: scoped `imports[module][name]`, then
|
|
126
133
|
* `globals[module][name]`. Imports already satisfied by `hostImports`, or that
|
|
127
|
-
* are not functions, are skipped. Returns the resolved bindings
|
|
128
|
-
* 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.
|
|
129
136
|
*/
|
|
130
137
|
export function resolveExternImports(importList, hostImports, imports = {}, globals = globalThis) {
|
|
131
138
|
const found = [];
|
|
@@ -134,21 +141,33 @@ export function resolveExternImports(importList, hostImports, imports = {}, glob
|
|
|
134
141
|
if (hostImports[imp.module]?.[imp.name] !== undefined) continue;
|
|
135
142
|
if (imp.kind !== "function") continue;
|
|
136
143
|
|
|
137
|
-
const
|
|
138
|
-
|
|
144
|
+
const scoped = imports[imp.module]?.[imp.name];
|
|
145
|
+
let fn;
|
|
146
|
+
let recv;
|
|
147
|
+
let args;
|
|
148
|
+
|
|
139
149
|
if (typeof scoped === "function") {
|
|
140
|
-
|
|
141
|
-
|
|
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];
|
|
142
164
|
}
|
|
143
165
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
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}`);
|
|
149
170
|
}
|
|
150
|
-
|
|
151
|
-
missing.push(`${imp.module}.${imp.name}`);
|
|
152
171
|
}
|
|
153
172
|
return { found, missing };
|
|
154
173
|
}
|
|
@@ -162,7 +181,7 @@ export function resolveExternImports(importList, hostImports, imports = {}, glob
|
|
|
162
181
|
* - Int (bigint), Float (number), Bool (i32) pass through
|
|
163
182
|
* Throws a single aggregated error if any extern import is unsatisfied.
|
|
164
183
|
*/
|
|
165
|
-
function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, imports = {}
|
|
184
|
+
function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, imports = {}) {
|
|
166
185
|
let importList;
|
|
167
186
|
try {
|
|
168
187
|
importList = WebAssembly.Module.imports(wasmModule);
|
|
@@ -182,11 +201,11 @@ function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, impor
|
|
|
182
201
|
);
|
|
183
202
|
}
|
|
184
203
|
|
|
185
|
-
// Per-import arg marshaling honors
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
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
|
|
190
209
|
// non-numeric arg is assumed to be a Wasm GC string and decoded.
|
|
191
210
|
const makeMarshalArgs = (spec) => (args) => args.map((arg, i) => {
|
|
192
211
|
if (typeof arg === "bigint") return Number(arg);
|
|
@@ -203,8 +222,8 @@ function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, impor
|
|
|
203
222
|
return result;
|
|
204
223
|
};
|
|
205
224
|
|
|
206
|
-
for (const { module, name, fn, recv } of found) {
|
|
207
|
-
const marshalArgs = makeMarshalArgs(
|
|
225
|
+
for (const { module, name, fn, recv, args } of found) {
|
|
226
|
+
const marshalArgs = makeMarshalArgs(args);
|
|
208
227
|
let bridgedFn;
|
|
209
228
|
if (jspi) {
|
|
210
229
|
// JSPI mode: async wrapper so Promise-returning JS functions suspend
|
|
@@ -280,7 +299,6 @@ function makeHostImports(b, runtime, bridgeBytes) {
|
|
|
280
299
|
stdout: runtime.stdout,
|
|
281
300
|
stderr: runtime.stderr,
|
|
282
301
|
imports: runtime.imports,
|
|
283
|
-
marshalSpec: runtime.marshalSpec,
|
|
284
302
|
host: runtime.host,
|
|
285
303
|
bridgeBytes,
|
|
286
304
|
});
|
|
@@ -343,6 +361,64 @@ function makeHostImports(b, runtime, bridgeBytes) {
|
|
|
343
361
|
};
|
|
344
362
|
}
|
|
345
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
|
+
|
|
346
422
|
// ---------------------------------------------------------------------------
|
|
347
423
|
// Bridge instantiation
|
|
348
424
|
// ---------------------------------------------------------------------------
|
|
@@ -376,7 +452,6 @@ function prepareWasm(wasmBytes, opts, { jspi = false } = {}) {
|
|
|
376
452
|
bridgeBytes,
|
|
377
453
|
host,
|
|
378
454
|
imports = {},
|
|
379
|
-
marshalSpec = {},
|
|
380
455
|
} = opts;
|
|
381
456
|
|
|
382
457
|
if (!bridgeBytes) {
|
|
@@ -396,12 +471,11 @@ function prepareWasm(wasmBytes, opts, { jspi = false } = {}) {
|
|
|
396
471
|
stdinEof: false,
|
|
397
472
|
host,
|
|
398
473
|
imports,
|
|
399
|
-
marshalSpec,
|
|
400
474
|
};
|
|
401
475
|
|
|
402
476
|
const hostImports = makeHostImports(b, runtime, bridgeBytes);
|
|
403
477
|
const mainModule = new WebAssembly.Module(wasmBytes);
|
|
404
|
-
autoBridgeExternImports(mainModule, hostImports, b, jspi, imports
|
|
478
|
+
autoBridgeExternImports(mainModule, hostImports, b, jspi, imports);
|
|
405
479
|
|
|
406
480
|
return { mainModule, hostImports, b, runtime };
|
|
407
481
|
}
|
|
@@ -466,7 +540,6 @@ export async function runWasmBytesAsync(wasmBytes, opts = {}) {
|
|
|
466
540
|
stdout: runtime.stdout,
|
|
467
541
|
stderr: runtime.stderr,
|
|
468
542
|
imports: runtime.imports,
|
|
469
|
-
marshalSpec: runtime.marshalSpec,
|
|
470
543
|
host: runtime.host,
|
|
471
544
|
bridgeBytes: childBridgeBytes,
|
|
472
545
|
});
|
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
|
+
}
|