@twinkle-lang/twinkle 0.4.0 → 0.6.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.
Files changed (3) hide show
  1. package/boot.wasm +0 -0
  2. package/package.json +1 -1
  3. package/runtime.mjs +57 -13
package/boot.wasm CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinkle-lang/twinkle",
3
- "version": "0.4.0",
3
+ "version": "0.6.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" },
package/runtime.mjs CHANGED
@@ -181,7 +181,34 @@ export function resolveExternImports(importList, hostImports, imports = {}, glob
181
181
  * - Int (bigint), Float (number), Bool (i32) pass through
182
182
  * Throws a single aggregated error if any extern import is unsatisfied.
183
183
  */
184
- function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, imports = {}) {
184
+ /**
185
+ * Read the compiler-emitted `twinkle.externs` custom section into a
186
+ * `module → name → { args, ret }` map. Best-effort: `customSections` may throw
187
+ * on GC modules in some engines (same risk class as `Module.imports`), and the
188
+ * section is absent for non-Twinkle wasm — either way we return `{}` and the
189
+ * manual override / string-default path takes over.
190
+ */
191
+ function readExternMeta(wasmModule) {
192
+ let sections;
193
+ try {
194
+ sections = WebAssembly.Module.customSections(wasmModule, "twinkle.externs");
195
+ } catch {
196
+ return {};
197
+ }
198
+ if (!sections || sections.length === 0) return {};
199
+ try {
200
+ const list = JSON.parse(new TextDecoder().decode(new Uint8Array(sections[0])));
201
+ const map = {};
202
+ for (const e of list) {
203
+ (map[e.module] ??= {})[e.name] = { args: e.args, ret: e.ret };
204
+ }
205
+ return map;
206
+ } catch {
207
+ return {};
208
+ }
209
+ }
210
+
211
+ function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, imports = {}, externMeta = {}) {
185
212
  let importList;
186
213
  try {
187
214
  importList = WebAssembly.Module.imports(wasmModule);
@@ -201,21 +228,34 @@ function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, impor
201
228
  );
202
229
  }
203
230
 
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.
231
+ // Per-import arg marshaling honors a per-position kind spec. Two vocabularies
232
+ // are accepted and treated identically: the compiler-emitted twinkle.externs
233
+ // kinds ("str" | "ref" | "i64" | "f64" | "i32") and the manual override's
234
+ // ("raw" | "string"). Numbers pass through (handled before the spec). "ref" /
235
+ // "raw" pass the value untouched essential for externref args (e.g. a canvas
236
+ // 2D context), since decodeString on an opaque host object recurses until a
237
+ // stack overflow in some engines (notably Safari). Anything else (incl. no
238
+ // entry) is assumed to be a Wasm GC string and decoded.
210
239
  const makeMarshalArgs = (spec) => (args) => args.map((arg, i) => {
211
240
  if (typeof arg === "bigint") return Number(arg);
212
241
  if (typeof arg === "number") return arg;
213
- if (spec?.[i] === "raw") return arg;
242
+ const k = spec?.[i];
243
+ if (k === "ref" || k === "raw") return arg;
214
244
  return decodeString(b, arg);
215
245
  });
216
246
 
217
- const marshalReturn = (result) => {
247
+ // Return marshaling uses the compiler-emitted `ret` kind when available, so
248
+ // we never guess from the JS value's type. Falls back to a generic guess when
249
+ // there is no section (manual-only callers).
250
+ const marshalReturn = (result, ret) => {
218
251
  if (result === undefined || result === null) return;
252
+ switch (ret) {
253
+ case "ref": return result;
254
+ case "str": return typeof result === "string" ? encodeString(b, result) : result;
255
+ case "i64": return typeof result === "number" ? BigInt(result) : result;
256
+ case "f64": case "i32": return result;
257
+ case "void": return undefined;
258
+ }
219
259
  if (typeof result === "string") return encodeString(b, result);
220
260
  if (typeof result === "number") return result;
221
261
  if (typeof result === "bigint") return result;
@@ -223,13 +263,16 @@ function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, impor
223
263
  };
224
264
 
225
265
  for (const { module, name, fn, recv, args } of found) {
226
- const marshalArgs = makeMarshalArgs(args);
266
+ // Precedence: a manual `args` override wins over the section's kinds.
267
+ const meta = externMeta[module]?.[name];
268
+ const marshalArgs = makeMarshalArgs(args ?? meta?.args);
269
+ const ret = meta?.ret;
227
270
  let bridgedFn;
228
271
  if (jspi) {
229
272
  // JSPI mode: async wrapper so Promise-returning JS functions suspend
230
273
  // Wasm. Non-Promise returns pass through without suspension.
231
274
  const asyncWrapper = async (...args) =>
232
- marshalReturn(await fn.apply(recv, marshalArgs(args)));
275
+ marshalReturn(await fn.apply(recv, marshalArgs(args)), ret);
233
276
  bridgedFn = new WebAssembly.Suspending(asyncWrapper);
234
277
  } else {
235
278
  // Sync mode: detect and reject Promise returns
@@ -241,7 +284,7 @@ function autoBridgeExternImports(wasmModule, hostImports, b, jspi = false, impor
241
284
  `Promise-returning externs require a runtime with WebAssembly.Suspending/promising support.`,
242
285
  );
243
286
  }
244
- return marshalReturn(result);
287
+ return marshalReturn(result, ret);
245
288
  };
246
289
  }
247
290
  if (!hostImports[module]) hostImports[module] = {};
@@ -475,7 +518,8 @@ function prepareWasm(wasmBytes, opts, { jspi = false } = {}) {
475
518
 
476
519
  const hostImports = makeHostImports(b, runtime, bridgeBytes);
477
520
  const mainModule = new WebAssembly.Module(wasmBytes);
478
- autoBridgeExternImports(mainModule, hostImports, b, jspi, imports);
521
+ const externMeta = readExternMeta(mainModule);
522
+ autoBridgeExternImports(mainModule, hostImports, b, jspi, imports, externMeta);
479
523
 
480
524
  return { mainModule, hostImports, b, runtime };
481
525
  }