gofront 1.5.1 → 1.6.1

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 (91) hide show
  1. package/README.md +174 -466
  2. package/package.json +16 -20
  3. package/src/backend/js/expressions.js +288 -128
  4. package/src/backend/js/index.js +138 -56
  5. package/src/backend/js/runtime.js +1055 -53
  6. package/src/backend/js/source-map.js +32 -0
  7. package/src/backend/js/statements.js +151 -83
  8. package/src/backend/js/stdlib/base64.js +12 -0
  9. package/src/backend/js/stdlib/binary.js +102 -0
  10. package/src/backend/js/stdlib/bits.js +15 -1
  11. package/src/backend/js/stdlib/builder.js +28 -9
  12. package/src/backend/js/stdlib/bytes.js +75 -18
  13. package/src/backend/js/stdlib/errors.js +22 -3
  14. package/src/backend/js/stdlib/fmt.js +95 -20
  15. package/src/backend/js/stdlib/hex.js +17 -0
  16. package/src/backend/js/stdlib/html.js +1 -1
  17. package/src/backend/js/stdlib/index.js +19 -0
  18. package/src/backend/js/stdlib/io.js +105 -3
  19. package/src/backend/js/stdlib/maps.js +1 -1
  20. package/src/backend/js/stdlib/math.js +13 -1
  21. package/src/backend/js/stdlib/path.js +20 -6
  22. package/src/backend/js/stdlib/rand.js +10 -5
  23. package/src/backend/js/stdlib/sha256.js +12 -0
  24. package/src/backend/js/stdlib/shared.js +17 -0
  25. package/src/backend/js/stdlib/slices.js +53 -27
  26. package/src/backend/js/stdlib/sort.js +1 -1
  27. package/src/backend/js/stdlib/strconv.js +52 -11
  28. package/src/backend/js/stdlib/strings.js +152 -16
  29. package/src/backend/js/stdlib/sync.js +13 -0
  30. package/src/backend/js/stdlib/testing.js +2 -2
  31. package/src/backend/js/stdlib/time.js +93 -22
  32. package/src/backend/js/stdlib/unicode.js +2 -2
  33. package/src/backend/js/templ.js +1 -1
  34. package/src/backend/wasm/boundary.js +1677 -58
  35. package/src/backend/wasm/const-fold.js +205 -0
  36. package/src/backend/wasm/emit-builtins.js +1230 -0
  37. package/src/backend/wasm/emit-exprs.js +2096 -0
  38. package/src/backend/wasm/emit-maps.js +1999 -0
  39. package/src/backend/wasm/emit-shared.js +519 -0
  40. package/src/backend/wasm/emit-stdlib.js +13 -0
  41. package/src/backend/wasm/emit-stmts.js +2466 -0
  42. package/src/backend/wasm/emit.js +656 -5031
  43. package/src/backend/wasm/encode.js +259 -85
  44. package/src/backend/wasm/glue.js +19 -5
  45. package/src/backend/wasm/index.js +2432 -86
  46. package/src/backend/wasm/monomorph.js +711 -0
  47. package/src/backend/wasm/optimize.js +236 -0
  48. package/src/backend/wasm/stdlib/bytes.js +5039 -0
  49. package/src/backend/wasm/stdlib/core.js +8903 -0
  50. package/src/backend/wasm/stdlib/index.js +40 -0
  51. package/src/backend/wasm/stdlib/io.js +2161 -0
  52. package/src/backend/wasm/stdlib/packages.js +2400 -0
  53. package/src/backend/wasm/stdlib/pure.js +720 -0
  54. package/src/backend/wasm/stdlib/time.js +1200 -0
  55. package/src/backend/wasm/types.js +312 -36
  56. package/src/backend/wasm/wat.js +49 -2
  57. package/src/cli-core.js +276 -73
  58. package/src/compiler.js +162 -25
  59. package/src/dts-parser.js +79 -126
  60. package/src/index.js +88 -121
  61. package/src/lexer.js +80 -29
  62. package/src/lower/boxing.js +4 -6
  63. package/src/lower/captures.js +12 -23
  64. package/src/lower/escape.js +4 -9
  65. package/src/lower/functions.js +277 -5
  66. package/src/lower/globals.js +7 -15
  67. package/src/lower/index.js +5 -2
  68. package/src/lower/ownership.js +35 -19
  69. package/src/lower/range.js +2 -5
  70. package/src/lower/walk.js +25 -0
  71. package/src/minifier.js +20 -68
  72. package/src/parser/declarations.js +3 -6
  73. package/src/parser/expressions.js +3 -8
  74. package/src/parser/index.js +18 -3
  75. package/src/parser/statements.js +25 -38
  76. package/src/parser/types.js +1 -2
  77. package/src/resolver.js +3 -27
  78. package/src/templ-lexer.js +29 -63
  79. package/src/templ-parser.js +22 -45
  80. package/src/test-runner.js +20 -0
  81. package/src/typechecker/assignability.js +123 -14
  82. package/src/typechecker/expressions.js +244 -4
  83. package/src/typechecker/index.js +27 -1
  84. package/src/typechecker/resolve.js +71 -12
  85. package/src/typechecker/statements.js +0 -7
  86. package/src/typechecker/stdlib/core.js +292 -103
  87. package/src/typechecker/stdlib/extended.js +471 -4
  88. package/src/typechecker/stdlib/shared.js +53 -0
  89. package/src/typechecker/stdlib.js +2 -0
  90. package/src/typechecker/types.js +545 -8
  91. package/src/vendor.js +2 -2
@@ -20,12 +20,36 @@
20
20
  // TypedArray inputs accepted). No copy-back for slices.
21
21
  // - func values: JS callbacks wrapped into wasm closures via thunks; wasm
22
22
  // closures exposed to JS as callable functions.
23
- // - maps, non-empty interfaces, error, pointers to non-structs: rejected
23
+ // - named non-empty interfaces (Task H5.2): wasm values reach JS as their
24
+ // handle class / live view when the dynamic type is an exported `*T`,
25
+ // otherwise as a facade whose methods call the wasm dispatchers. Only
26
+ // wasm-owned values (`__ref`) go back in; JS-implemented values are
27
+ // rejected (no cross-boundary itab proxies).
28
+ // - maps, anonymous interfaces, error, pointers to non-structs: rejected
24
29
  // with a compile error (planned for a later boundary version).
30
+ // - `gofront/shared` buffers (Task H6.1): `(base, len)` views over the
31
+ // module's exported linear memory; JS sees a zero-copy TypedArray over
32
+ // `memory.buffer`. Only TypedArrays that already alias that memory (ones
33
+ // handed out by this facade) may be passed back in.
25
34
  // - `*testing.T`: the harness's JS test object, passed as an opaque
26
35
  // externref (test methods are routed back to JS via `env.testing_*`).
27
36
 
28
- import { isTestingT } from "./types.js";
37
+ import {
38
+ HELPER_BASE64,
39
+ HELPER_DURATION,
40
+ HELPER_ERROR,
41
+ HELPER_HEX,
42
+ HELPER_PATH_CLEAN,
43
+ HELPER_SHA256,
44
+ HELPER_SPRINTF,
45
+ HELPER_STRCONV,
46
+ } from "../js/runtime.js";
47
+ import {
48
+ anonIfaceName,
49
+ getInterfaceMethodList,
50
+ getSharedInfo,
51
+ isTestingT,
52
+ } from "./types.js";
29
53
 
30
54
  // ── Type classification ──────────────────────────────────────
31
55
 
@@ -79,11 +103,19 @@ function funcSigFromAst(t) {
79
103
 
80
104
  // Classifies a Go type (AST node or checker type) for boundary marshalling.
81
105
  // Returns a descriptor `{ k, ... }`; `k === "unsupported"` carries `what`.
82
- export function classifyType(goType, mod) {
106
+ function classifyType(goType, mod) {
83
107
  if (!goType) return { k: "void" };
84
108
  if (isTestingT(goType)) return { k: "extern", wType: "externref" };
85
109
 
86
110
  if (goType.kind === "TypeName" || goType.kind === "Ident") {
111
+ if (goType.name === "error") return { k: "error", wType: "anyref" };
112
+ if (goType.name === "time.Duration" || goType.name === "Duration")
113
+ return { k: "i64", unsigned: false, wType: "i64" };
114
+ if (goType.name === "time.Time" || goType.name === "Time") {
115
+ const info = mod.getStructType("time.Time");
116
+ if (info)
117
+ return { k: "time", name: "time.Time", info, wType: toRef(info) };
118
+ }
87
119
  const info = mod.getStructType(goType.name);
88
120
  if (info) return structDesc(info, false);
89
121
  const resolved = mod.checker?.types?.get(goType.name);
@@ -95,6 +127,8 @@ export function classifyType(goType, mod) {
95
127
  switch (t.kind) {
96
128
  case "named":
97
129
  return classifyNamed(t, mod);
130
+ case "shared":
131
+ return classifyShared(t, mod);
98
132
  case "untyped":
99
133
  return classifyBasic(t.base);
100
134
  case "basic":
@@ -105,10 +139,14 @@ export function classifyType(goType, mod) {
105
139
  return classifySlice(t, mod);
106
140
  case "array":
107
141
  return classifyArray(t, mod);
142
+ case "map":
143
+ return classifyMap(t, mod);
144
+ case "struct":
145
+ return classifyStruct(t, mod);
108
146
  case "func":
109
147
  return classifyFunc(t, mod);
110
148
  case "interface":
111
- return classifyInterface(t);
149
+ return classifyInterface(t, mod);
112
150
  default:
113
151
  return {
114
152
  k: "unsupported",
@@ -118,11 +156,6 @@ export function classifyType(goType, mod) {
118
156
  }
119
157
 
120
158
  const UNSUPPORTED_KINDS = {
121
- map: "map",
122
- MapType: "map",
123
- struct: "anonymous struct",
124
- StructType: "anonymous struct",
125
- InterfaceType: "interface",
126
159
  ChanType: "chan",
127
160
  chan: "chan",
128
161
  };
@@ -137,13 +170,21 @@ function normalizeAstType(t) {
137
170
  return { kind: "slice", elem: t.elem };
138
171
  case "ArrayType":
139
172
  return { kind: "array", elem: t.elem, size: t.size };
173
+ case "MapType":
174
+ return { kind: "map", key: t.key, val: t.value ?? t.elem };
140
175
  case "FuncType":
141
176
  return funcSigFromAst(t);
142
177
  case "InterfaceType":
143
178
  return {
144
179
  kind: "interface",
145
180
  name: t.name,
146
- methods: new Set((t.methods ?? []).map((m) => m.name ?? m)),
181
+ methods: t.methods,
182
+ };
183
+ case "StructType":
184
+ return {
185
+ kind: "struct",
186
+ name: t.name,
187
+ fields: t.fields,
147
188
  };
148
189
  default:
149
190
  return t;
@@ -157,7 +198,7 @@ const BASIC_DESCS = {
157
198
  float64: { k: "f64", wType: "f64" },
158
199
  string: { k: "string", wType: "externref" },
159
200
  any: { k: "any", wType: "anyref" },
160
- error: { k: "unsupported", what: "error" },
201
+ error: { k: "error", wType: "anyref" },
161
202
  };
162
203
 
163
204
  function classifyBasic(n) {
@@ -170,17 +211,122 @@ function classifyBasic(n) {
170
211
  }
171
212
 
172
213
  function classifyNamed(t, mod) {
214
+ if (t.name === "error") return { k: "error", wType: "anyref" };
215
+ if (t.name === "time.Duration")
216
+ return { k: "i64", unsigned: false, wType: "i64" };
217
+ if (t.name === "time.Time") {
218
+ const info = mod.getStructType("time.Time");
219
+ if (info) return { k: "time", name: "time.Time", info, wType: toRef(info) };
220
+ }
173
221
  const info = mod.getStructType(t.name);
174
222
  if (info) return structDesc(info, false);
175
223
  if (t.underlying) return classifyType(t.underlying, mod);
176
224
  return { k: "unsupported", what: `named type '${t.name}'` };
177
225
  }
178
226
 
227
+ function classifyAnonStruct(t, mod) {
228
+ const rawFields = [];
229
+ if (t.fields instanceof Map) {
230
+ for (const [k, v] of t.fields.entries())
231
+ rawFields.push({ name: k, goType: v });
232
+ } else if (Array.isArray(t.fields)) {
233
+ for (const f of t.fields) {
234
+ const names = f.names ?? (f.name ? [f.name] : []);
235
+ for (const n of names) rawFields.push({ name: n, goType: f.type });
236
+ }
237
+ }
238
+ const info = mod.getAnonStructType(rawFields);
239
+ const fields = [];
240
+ for (const f of info.fields) {
241
+ const fd = classifyType(f.goType, mod);
242
+ if (fd.k === "unsupported") return fd;
243
+ fields.push({ name: f.name, desc: fd });
244
+ }
245
+ return {
246
+ k: "anon_struct",
247
+ name: info.name,
248
+ key: info.name,
249
+ info,
250
+ fields,
251
+ wType: toRef(info),
252
+ };
253
+ }
254
+
255
+ function classifyStruct(t, mod) {
256
+ if (t.name) {
257
+ const info = mod.getStructType(t.name);
258
+ if (info) return structDesc(info, false);
259
+ }
260
+ return classifyAnonStruct(t, mod);
261
+ }
262
+
179
263
  function classifyPointer(t, mod) {
180
264
  const base = classifyType(t.base, mod);
265
+ if (base.k === "unsupported") return base;
181
266
  if (base.k === "struct" && !base.ptr) return { ...base, ptr: true };
182
- const what = base.k === "unsupported" ? base.what : (t.base?.name ?? base.k);
183
- return { k: "unsupported", what: `pointer to ${what}` };
267
+ if (base.k === "anon_struct" && !base.ptr) return { ...base, ptr: true };
268
+ if (base.k === "time" && !base.ptr) return { ...base, ptr: true };
269
+ const boxInfo = mod.getBoxType(t.base);
270
+ const key = `${base.k}${base.unsigned ? "_u" : ""}_${boxInfo.typeIndex}`;
271
+ return {
272
+ k: "ptr_box",
273
+ base,
274
+ boxInfo,
275
+ key,
276
+ wType: { kind: "ref", nullable: true, typeIndex: boxInfo.typeIndex },
277
+ };
278
+ }
279
+
280
+ function isAllowedMapKey(kDesc) {
281
+ if (kDesc.k === "string" || kDesc.k === "bool") return true;
282
+ if (
283
+ kDesc.k === "i32" ||
284
+ kDesc.k === "i64" ||
285
+ kDesc.k === "f32" ||
286
+ kDesc.k === "f64"
287
+ )
288
+ return true;
289
+ return false;
290
+ }
291
+
292
+ function classifyMap(t, mod) {
293
+ const keyType = t.key;
294
+ const valType = t.val ?? t.value ?? t.elem;
295
+ const keyDesc = classifyType(keyType, mod);
296
+ if (keyDesc.k === "unsupported") return keyDesc;
297
+ const valDesc = classifyType(valType, mod);
298
+ if (valDesc.k === "unsupported") return valDesc;
299
+ if (!isAllowedMapKey(keyDesc)) {
300
+ return {
301
+ k: "unsupported",
302
+ what: `map with unsupported key type '${keyType?.name ?? keyDesc.k}'`,
303
+ };
304
+ }
305
+ const mapInfo = mod.getMapType(keyType, valType);
306
+ const keySliceDesc = classifySlice({ kind: "slice", elem: keyType }, mod);
307
+ const valSliceDesc = classifySlice({ kind: "slice", elem: valType }, mod);
308
+ const key = `m_${mapInfo.suffix}`;
309
+ return {
310
+ k: "map",
311
+ key,
312
+ mapInfo,
313
+ keyDesc,
314
+ valDesc,
315
+ keySliceDesc,
316
+ valSliceDesc,
317
+ wType: mapInfo.wType,
318
+ };
319
+ }
320
+
321
+ function classifyShared(t, mod) {
322
+ const info = getSharedInfo(t, mod.checker);
323
+ return {
324
+ k: "shared",
325
+ key: info.key,
326
+ ctor: info.ctor,
327
+ bytes: info.bytes,
328
+ wType: mod.getSharedType().wType,
329
+ };
184
330
  }
185
331
 
186
332
  function classifySlice(t, mod) {
@@ -216,31 +362,104 @@ function classifyArray(t, mod) {
216
362
  };
217
363
  }
218
364
 
219
- function classifyFunc(t, mod) {
365
+ function classifySig(t, mod) {
220
366
  const params = (t.params ?? []).map((p) => classifyType(p, mod));
221
367
  const returns = (t.returns ?? [])
222
368
  .map((r) => classifyType(r, mod))
223
369
  .filter((d) => d.k !== "void");
224
370
  const bad = [...params, ...returns].find((d) => d.k === "unsupported");
225
- if (bad) return bad;
371
+ return bad ?? { params, returns };
372
+ }
373
+
374
+ function classifyFunc(t, mod) {
375
+ const sig = classifySig(t, mod);
376
+ if (sig.k === "unsupported") return sig;
226
377
  const closureInfo = mod.getClosureType(t);
227
378
  return {
228
379
  k: "func",
229
- params,
230
- returns,
380
+ params: sig.params,
381
+ returns: sig.returns,
231
382
  closureInfo,
232
383
  key: `c${closureInfo.typeIndex}`,
233
384
  wType: { kind: "ref", nullable: true, typeIndex: closureInfo.typeIndex },
234
385
  };
235
386
  }
236
387
 
237
- function classifyInterface(t) {
238
- if (t.name === "error") return { k: "unsupported", what: "error" };
239
- if (!t.methods || t.methods.size === 0) return { k: "any", wType: "anyref" };
240
- return {
241
- k: "unsupported",
242
- what: `interface${t.name ? ` '${t.name}'` : ""}`,
388
+ function classifyInterface(t, mod) {
389
+ if (t.name === "error") return { k: "error", wType: "anyref" };
390
+ const mList = getInterfaceMethodList(t);
391
+ if (mList.length === 0) return { k: "any", wType: "anyref" };
392
+ mod._boundaryIfaces ??= new Map();
393
+ if (t.name && t.methods instanceof Map) {
394
+ return mod._boundaryIfaces.get(t.name) ?? classifyNamedInterface(t, mod);
395
+ }
396
+ return classifyAnonInterface(t, mod);
397
+ }
398
+
399
+ function classifyAnonInterface(t, mod) {
400
+ const key = anonIfaceName(t, mod.checker, mod);
401
+ if (mod._boundaryIfaces.has(key)) return mod._boundaryIfaces.get(key);
402
+ const mList = getInterfaceMethodList(t);
403
+ const desc = {
404
+ k: "iface",
405
+ name: key,
406
+ key,
407
+ wType: "anyref",
408
+ methods: [],
409
+ concrete: [],
410
+ isAnon: true,
243
411
  };
412
+ mod._boundaryIfaces.set(key, desc);
413
+ for (const m of mList) {
414
+ const sig = classifySig(
415
+ { kind: "func", params: m.params, returns: m.returns },
416
+ mod,
417
+ );
418
+ if (sig.k === "unsupported") {
419
+ const err = {
420
+ k: "unsupported",
421
+ what: `${sig.what} (in anonymous interface method ${m.name})`,
422
+ };
423
+ mod._boundaryIfaces.set(key, err);
424
+ return err;
425
+ }
426
+ desc.methods.push({
427
+ name: m.name,
428
+ dispatch: `__dispatch_${key}_${m.name}`,
429
+ ...sig,
430
+ });
431
+ }
432
+ return desc;
433
+ }
434
+
435
+ function classifyNamedInterface(t, mod) {
436
+ const desc = {
437
+ k: "iface",
438
+ name: t.name,
439
+ key: t.name,
440
+ wType: "anyref",
441
+ methods: [],
442
+ concrete: [],
443
+ };
444
+ // Cached before the walk so self-referencing method signatures terminate.
445
+ mod._boundaryIfaces.set(t.name, desc);
446
+ for (const [mName, sigType] of t.methods) {
447
+ const sig = classifySig(sigType, mod);
448
+ if (sig.k === "unsupported") {
449
+ const err = {
450
+ k: "unsupported",
451
+ what: `${sig.what} (in method ${t.name}.${mName})`,
452
+ };
453
+ mod._boundaryIfaces.set(t.name, err);
454
+ return err;
455
+ }
456
+ desc.methods.push({
457
+ name: mName,
458
+ dispatch: `__dispatch_${t.name}_${mName}`,
459
+ ...sig,
460
+ });
461
+ }
462
+ return desc;
244
463
  }
245
464
 
246
465
  // ── Export metadata ──────────────────────────────────────────
@@ -267,7 +486,14 @@ function literalConst(spec, i) {
267
486
  }
268
487
  if (node?.kind !== "BasicLit") return null;
269
488
  switch (node.litKind) {
270
- case "INT":
489
+ case "INT": {
490
+ const raw = String(node.value);
491
+ const clean = /^0[0-7]+$/.test(raw) ? `0o${raw.slice(1)}` : raw;
492
+ const bi = BigInt(`${sign}${clean}`);
493
+ return bi > 9007199254740991n || bi < -9007199254740991n
494
+ ? `${sign}${clean}n`
495
+ : `${sign}${clean}`;
496
+ }
271
497
  case "FLOAT":
272
498
  return `${sign}${node.value}`;
273
499
  case "STRING":
@@ -291,6 +517,7 @@ export function collectBoundaryMeta(progs, funcDecls, mod) {
291
517
  structs: [],
292
518
  funcs: [],
293
519
  consts: [],
520
+ vars: [], // exported package-level shared buffers of wasm packages
294
521
  structByName: new Map(),
295
522
  _mod: mod,
296
523
  };
@@ -304,6 +531,8 @@ export function collectBoundaryMeta(progs, funcDecls, mod) {
304
531
  collectStructDecl(d, pkgName, pkgTarget, meta, mod);
305
532
  } else if (d.kind === "ConstDecl" && pkgTarget === "wasm") {
306
533
  collectConstDecl(d, pkgName, meta, errors);
534
+ } else if (d.kind === "VarDecl" && pkgTarget === "wasm") {
535
+ collectSharedVarDecl(d, meta, mod);
307
536
  }
308
537
  }
309
538
  }
@@ -333,14 +562,48 @@ function collectConstDecl(d, pkgName, meta, errors) {
333
562
  const name = spec.names[i];
334
563
  if (!isExported(name)) continue;
335
564
  const value = literalConst(spec, i);
336
- if (value !== null) meta.consts.push({ name, value });
337
- else
565
+ if (value !== null) {
566
+ meta.consts.push({ name, value, dynamic: false });
567
+ continue;
568
+ }
569
+ const folded = meta._mod.constCache?.get(name);
570
+ if (folded) {
571
+ let val = null;
572
+ if (folded.litKind === "INT") {
573
+ const s = String(folded.value);
574
+ const clean = /^0[0-7]+$/.test(s) ? `0o${s.slice(1)}` : s;
575
+ const bi = BigInt(clean);
576
+ val =
577
+ bi > 9007199254740991n || bi < -9007199254740991n
578
+ ? `${clean}n`
579
+ : clean;
580
+ } else if (folded.litKind === "FLOAT") {
581
+ val = folded.value;
582
+ } else if (folded.litKind === "STRING") {
583
+ val = JSON.stringify(String(folded.value));
584
+ } else if (folded.litKind === "BOOL") {
585
+ val = folded.value === "true" ? "true" : "false";
586
+ }
587
+ if (val !== null) {
588
+ meta.consts.push({ name, value: val, dynamic: false });
589
+ continue;
590
+ }
591
+ }
592
+ const glob = meta._mod.resolveGlobal(name);
593
+ const rawType = spec.type ?? spec.value?.[i]?._type ?? glob?.goType;
594
+ let desc = rawType ? classifyType(rawType, meta._mod) : null;
595
+ if (!desc || desc.k === "void") {
596
+ if (glob?.type) {
597
+ desc = { k: glob.type, wType: glob.type };
598
+ }
599
+ }
600
+ if (!desc || desc.k === "unsupported") {
338
601
  errors.push(
339
- unsupportedMsg(
340
- `${pkgName}.${name}`,
341
- "a constant with a non-literal value",
342
- ),
602
+ unsupportedMsg(`${pkgName}.${name}`, desc?.what ?? "unsupported"),
343
603
  );
604
+ } else {
605
+ meta.consts.push({ name, dynamic: true, desc });
606
+ }
344
607
  }
345
608
  }
346
609
  }
@@ -350,6 +613,22 @@ function checkDesc(desc, where, errors) {
350
613
  return desc;
351
614
  }
352
615
 
616
+ // Exported `var Buf = shared.NewFloat32(n)` globals are exposed to JS as
617
+ // TypedArray views (read once after instantiation; sizing is fixed).
618
+ function collectSharedVarDecl(d, meta, mod) {
619
+ for (const spec of d.decls ?? d.specs ?? [d]) {
620
+ const names = spec.names ?? (spec.name ? [spec.name] : []);
621
+ const values = spec.value ?? (spec.init ? [spec.init] : []);
622
+ for (let i = 0; i < names.length; i++) {
623
+ const name = names[i];
624
+ if (!isExported(name)) continue;
625
+ const goType = spec.type ?? values[i]?._type;
626
+ if (!getSharedInfo(goType, mod.checker)) continue;
627
+ meta.vars.push({ name, desc: classifyType(goType, mod) });
628
+ }
629
+ }
630
+ }
631
+
353
632
  function collectMethodMeta(fn, meta, errors, mod) {
354
633
  if (!isExported(fn._methodName)) return;
355
634
  const s = meta.structByName.get(fn._recvTypeName);
@@ -358,6 +637,7 @@ function collectMethodMeta(fn, meta, errors, mod) {
358
637
  s.methods.push({
359
638
  name: fn._methodName,
360
639
  exportName: fn._exportName,
640
+ fnName: fn.name,
361
641
  ptrRecv: fn.params[0].type?.kind === "pointer",
362
642
  params: fn.params
363
643
  .slice(1)
@@ -405,6 +685,17 @@ function visitNeed(desc, needs, errors, mod) {
405
685
  case "struct":
406
686
  visitStructNeed(desc, needs, errors, mod);
407
687
  return;
688
+ case "anon_struct":
689
+ if (needs.anonStructs.has(desc.name)) return;
690
+ needs.anonStructs.set(desc.name, desc);
691
+ for (const f of desc.fields) visitNeed(f.desc, needs, errors, mod);
692
+ return;
693
+ case "error":
694
+ needs.error = true;
695
+ return;
696
+ case "time":
697
+ needs.time = true;
698
+ return;
408
699
  case "slice":
409
700
  case "array": {
410
701
  const map = desc.k === "slice" ? needs.slices : needs.arrays;
@@ -413,16 +704,55 @@ function visitNeed(desc, needs, errors, mod) {
413
704
  visitNeed(desc.elem, needs, errors, mod);
414
705
  return;
415
706
  }
707
+ case "map":
708
+ if (needs.maps.has(desc.key)) return;
709
+ needs.maps.set(desc.key, desc);
710
+ visitNeed(desc.keyDesc, needs, errors, mod);
711
+ visitNeed(desc.valDesc, needs, errors, mod);
712
+ visitNeed(desc.keySliceDesc, needs, errors, mod);
713
+ visitNeed(desc.valSliceDesc, needs, errors, mod);
714
+ return;
715
+ case "ptr_box":
716
+ if (needs.boxes.has(desc.key)) return;
717
+ needs.boxes.set(desc.key, desc);
718
+ visitNeed(desc.base, needs, errors, mod);
719
+ return;
416
720
  case "func":
417
721
  if (needs.funcs.has(desc.key)) return;
418
722
  needs.funcs.set(desc.key, desc);
419
723
  visitSignatureNeeds(desc, needs, errors, mod);
420
724
  return;
725
+ case "iface":
726
+ if (needs.ifaces.has(desc.key)) return;
727
+ needs.ifaces.set(desc.key, desc);
728
+ for (const m of desc.methods) visitSignatureNeeds(m, needs, errors, mod);
729
+ return;
730
+ case "shared":
731
+ needs.shared = true;
732
+ return;
421
733
  default:
422
734
  return;
423
735
  }
424
736
  }
425
737
 
738
+ // Exported `*T` types (already on the boundary surface) whose JS class exposes
739
+ // every interface method: such values reach JS as the handle / live view
740
+ // itself, so type switches and identity still work. `__itag` matches by
741
+ // `ref.test`, so a struct is only chosen for values that really hold it.
742
+ function concreteCandidates(desc, needs, meta, mod) {
743
+ const out = [];
744
+ for (const [name, { info }] of needs.structs) {
745
+ const both = info.pkgTarget === "both";
746
+ const names = both
747
+ ? [...(mod.checker?.types?.get(name)?.underlying?.methods?.keys() ?? [])]
748
+ : (meta.structByName.get(name)?.methods ?? []).map((m) => m.name);
749
+ const have = new Set(names);
750
+ if (!desc.methods.every((m) => have.has(m.name))) continue;
751
+ out.push({ name, typeIndex: info.typeIndex, both });
752
+ }
753
+ return out;
754
+ }
755
+
426
756
  function visitSignatureNeeds(sig, needs, errors, mod) {
427
757
  for (const p of sig.params) visitNeed(p, needs, errors, mod);
428
758
  for (const r of sig.returns) visitNeed(r, needs, errors, mod);
@@ -434,9 +764,16 @@ function collectNeeds(meta) {
434
764
  const mod = meta._mod;
435
765
  const needs = {
436
766
  structs: new Map(), // name -> { info, fields: [{ name, desc }] }
767
+ anonStructs: new Map(), // name -> desc
437
768
  slices: new Map(), // key -> desc
438
769
  arrays: new Map(), // key -> desc
770
+ maps: new Map(), // key -> desc
771
+ boxes: new Map(), // key -> desc
439
772
  funcs: new Map(), // key -> desc
773
+ ifaces: new Map(), // interface name -> desc
774
+ error: false,
775
+ time: false,
776
+ shared: false, // any shared buffer on the surface
440
777
  };
441
778
  const errors = [];
442
779
  for (const s of meta.structs) {
@@ -445,7 +782,13 @@ function collectNeeds(meta) {
445
782
  for (const m of s.methods) visitSignatureNeeds(m, needs, errors, mod);
446
783
  }
447
784
  for (const f of meta.funcs) visitSignatureNeeds(f, needs, errors, mod);
785
+ for (const v of meta.vars ?? []) visitNeed(v.desc, needs, errors, mod);
786
+ for (const c of meta.consts ?? []) {
787
+ if (c.dynamic) visitNeed(c.desc, needs, errors, mod);
788
+ }
448
789
  if (errors.length > 0) throw new Error([...new Set(errors)].join("\n"));
790
+ for (const desc of needs.ifaces.values())
791
+ desc.concrete = concreteCandidates(desc, needs, meta, mod);
449
792
  return needs;
450
793
  }
451
794
 
@@ -800,7 +1143,8 @@ export function addBoundaryHelpers(mod, meta) {
800
1143
  // get a trampoline (`__x_<export>`) with the boundary signature; the facade
801
1144
  // calls that instead of the raw export.
802
1145
  const addTrampoline = (entry) => {
803
- const fnIdx = mod.funcMap.get(entry.exportName);
1146
+ // Methods are keyed `T.M` in funcMap but exported as `T_M`.
1147
+ const fnIdx = mod.funcMap.get(entry.fnName ?? entry.exportName);
804
1148
  if (fnIdx === undefined) return;
805
1149
  const sig = typeAt(mod, mod.funcs[fnIdx - importFuncCount]?.typeIndex);
806
1150
  if (sig?.form !== "func") return;
@@ -823,6 +1167,326 @@ export function addBoundaryHelpers(mod, meta) {
823
1167
  };
824
1168
  for (const s of meta.structs) for (const m of s.methods) addTrampoline(m);
825
1169
  for (const f of meta.funcs) addTrampoline(f);
1170
+ for (const desc of needs.ifaces.values())
1171
+ addIfaceHelpers(desc, mod, addFunc, addTrampoline);
1172
+
1173
+ if (needs.shared) {
1174
+ const { wType, typeIndex } = mod.getSharedType();
1175
+ addFunc(
1176
+ "__shared_base",
1177
+ [EXT],
1178
+ ["i32"],
1179
+ [...getIn(0, wType), { op: "struct.get", typeIndex, fieldIndex: 0 }],
1180
+ );
1181
+ addFunc(
1182
+ "__shared_len",
1183
+ [EXT],
1184
+ ["i32"],
1185
+ [...getIn(0, wType), { op: "struct.get", typeIndex, fieldIndex: 1 }],
1186
+ );
1187
+ // JS-side views (e.g. a `subarray`) go back in as a fresh (base, len).
1188
+ addFunc(
1189
+ "__shared_make",
1190
+ ["i32", "i32"],
1191
+ [EXT],
1192
+ [get(0), get(1), { op: "struct.new", typeIndex }, ...toExt(wType)],
1193
+ );
1194
+ for (const v of meta.vars ?? []) {
1195
+ const g = mod.resolveGlobal(v.name);
1196
+ if (!g) continue;
1197
+ addFunc(
1198
+ `__var_${v.name}`,
1199
+ [],
1200
+ [EXT],
1201
+ [{ op: "global.get", index: g.index }, ...toExt(wType)],
1202
+ );
1203
+ }
1204
+ }
1205
+
1206
+ for (const [name, desc] of needs.anonStructs) {
1207
+ const { info } = desc;
1208
+ const ref = desc.wType;
1209
+ const ti = info.typeIndex;
1210
+ const fieldWTypes = info.fields.map((f) => f.wType);
1211
+ addFunc(
1212
+ `__new_${name}`,
1213
+ fieldWTypes.map(extType),
1214
+ [EXT],
1215
+ [
1216
+ ...fieldWTypes.flatMap((w, i) => getIn(i, w)),
1217
+ { op: "struct.new", typeIndex: ti },
1218
+ ...toExt(ref),
1219
+ ],
1220
+ );
1221
+ addFunc(
1222
+ `__zero_${name}`,
1223
+ [],
1224
+ [EXT],
1225
+ [
1226
+ ...fieldWTypes.map((w) => zeroInstr(w)),
1227
+ { op: "struct.new", typeIndex: ti },
1228
+ ...toExt(ref),
1229
+ ],
1230
+ );
1231
+ for (let i = 0; i < info.fields.length; i++) {
1232
+ const f = info.fields[i];
1233
+ addFunc(
1234
+ `__get_${name}_${f.name}`,
1235
+ [EXT],
1236
+ [extType(f.wType)],
1237
+ [
1238
+ ...getIn(0, ref),
1239
+ { op: "struct.get", typeIndex: ti, fieldIndex: i },
1240
+ ...toExt(f.wType),
1241
+ ],
1242
+ );
1243
+ addFunc(
1244
+ `__set_${name}_${f.name}`,
1245
+ [EXT, extType(f.wType)],
1246
+ [],
1247
+ [
1248
+ ...getIn(0, ref),
1249
+ ...getIn(1, f.wType),
1250
+ { op: "struct.set", typeIndex: ti, fieldIndex: i },
1251
+ ],
1252
+ );
1253
+ }
1254
+ }
1255
+
1256
+ for (const [key, desc] of needs.boxes) {
1257
+ const { boxInfo } = desc;
1258
+ const ref = desc.wType;
1259
+ const ti = boxInfo.typeIndex;
1260
+ const w = boxInfo.wType;
1261
+ addFunc(
1262
+ `__box_new_${key}`,
1263
+ [extType(w)],
1264
+ [EXT],
1265
+ [...getIn(0, w), { op: "struct.new", typeIndex: ti }, ...toExt(ref)],
1266
+ );
1267
+ addFunc(
1268
+ `__box_get_${key}`,
1269
+ [EXT],
1270
+ [extType(w)],
1271
+ [
1272
+ ...getIn(0, ref),
1273
+ { op: "struct.get", typeIndex: ti, fieldIndex: 0 },
1274
+ ...toExt(w),
1275
+ ],
1276
+ );
1277
+ addFunc(
1278
+ `__box_set_${key}`,
1279
+ [EXT, extType(w)],
1280
+ [],
1281
+ [
1282
+ ...getIn(0, ref),
1283
+ ...getIn(1, w),
1284
+ { op: "struct.set", typeIndex: ti, fieldIndex: 0 },
1285
+ ],
1286
+ );
1287
+ }
1288
+
1289
+ for (const [key, desc] of needs.maps) {
1290
+ const { mapInfo, keySliceDesc, valSliceDesc } = desc;
1291
+ const makeIdx = mod.resolveFuncIndex(mapInfo.makeFuncName);
1292
+ addFunc(
1293
+ `__map_new_${key}`,
1294
+ ["i32"],
1295
+ [EXT],
1296
+ [get(0), { op: "call", funcIndex: makeIdx }, ...toExt(desc.wType)],
1297
+ );
1298
+ const setIdx = mod.resolveFuncIndex(mapInfo.setFuncName);
1299
+ addFunc(
1300
+ `__map_set_${key}`,
1301
+ [EXT, extType(mapInfo.keyWType), extType(mapInfo.valWType)],
1302
+ [],
1303
+ [
1304
+ ...getIn(0, desc.wType),
1305
+ ...getIn(1, mapInfo.keyWType),
1306
+ ...getIn(2, mapInfo.valWType),
1307
+ { op: "call", funcIndex: setIdx },
1308
+ ],
1309
+ );
1310
+ const keysIdx = mod.resolveFuncIndex(mapInfo.keysFuncName);
1311
+ addFunc(
1312
+ `__map_keys_${key}`,
1313
+ [EXT],
1314
+ [EXT],
1315
+ [
1316
+ ...getIn(0, desc.wType),
1317
+ { op: "call", funcIndex: keysIdx },
1318
+ ...toExt(keySliceDesc.wType),
1319
+ ],
1320
+ );
1321
+ const valuesIdx = mod.resolveFuncIndex(mapInfo.valuesFuncName);
1322
+ addFunc(
1323
+ `__map_values_${key}`,
1324
+ [EXT],
1325
+ [EXT],
1326
+ [
1327
+ ...getIn(0, desc.wType),
1328
+ { op: "call", funcIndex: valuesIdx },
1329
+ ...toExt(valSliceDesc.wType),
1330
+ ],
1331
+ );
1332
+ }
1333
+
1334
+ if (needs.error) {
1335
+ const errDispatcherIdx = mod.funcMap.get("__dispatch_error_Error");
1336
+ if (errDispatcherIdx !== undefined) {
1337
+ addFunc(
1338
+ "__error_msg",
1339
+ [EXT],
1340
+ [EXT],
1341
+ [
1342
+ get(0),
1343
+ { op: "ref.is_null" },
1344
+ { op: "if", blockType: "externref" },
1345
+ { op: "ref.null", heapType: "extern" },
1346
+ { op: "else" },
1347
+ ...getIn(0, "anyref"),
1348
+ { op: "call", funcIndex: errDispatcherIdx },
1349
+ ...toExt("externref"),
1350
+ { op: "end" },
1351
+ ],
1352
+ );
1353
+ }
1354
+ const errCandidates = mod
1355
+ .findInterfaceCandidates(null, "Error")
1356
+ .map((c) => c.testTypeIndex);
1357
+ addFunc(
1358
+ "__is_error",
1359
+ [EXT],
1360
+ ["i32"],
1361
+ refTestChain(errCandidates, () => 1),
1362
+ );
1363
+ }
1364
+
1365
+ if (needs.time) {
1366
+ const timeInfo = mod.getStructType("time.Time");
1367
+ if (timeInfo) {
1368
+ const timeWType = toRef(timeInfo);
1369
+ addFunc(
1370
+ "__time_make",
1371
+ ["f64", "i32"],
1372
+ [EXT],
1373
+ [
1374
+ get(0),
1375
+ { op: "f64.const", value: 1000000 },
1376
+ { op: "f64.mul" },
1377
+ { op: "i64.trunc_sat_f64_s" },
1378
+ get(1),
1379
+ { op: "struct.new", typeIndex: timeInfo.typeIndex },
1380
+ ...toExt(timeWType),
1381
+ ],
1382
+ );
1383
+ addFunc(
1384
+ "__time_unix_milli",
1385
+ [EXT],
1386
+ ["f64"],
1387
+ [
1388
+ get(0),
1389
+ { op: "ref.is_null" },
1390
+ { op: "if", blockType: "f64" },
1391
+ { op: "f64.const", value: 0 },
1392
+ { op: "else" },
1393
+ ...getIn(0, timeWType),
1394
+ { op: "struct.get", typeIndex: timeInfo.typeIndex, fieldIndex: 0 },
1395
+ { op: "i64.const", value: 1000000n },
1396
+ { op: "i64.div_s" },
1397
+ { op: "f64.convert_i64_s" },
1398
+ { op: "end" },
1399
+ ],
1400
+ );
1401
+ addFunc(
1402
+ "__time_offset",
1403
+ [EXT],
1404
+ ["i32"],
1405
+ [
1406
+ get(0),
1407
+ { op: "ref.is_null" },
1408
+ { op: "if", blockType: "i32" },
1409
+ { op: "i32.const", value: 0 },
1410
+ { op: "else" },
1411
+ ...getIn(0, timeWType),
1412
+ { op: "struct.get", typeIndex: timeInfo.typeIndex, fieldIndex: 1 },
1413
+ { op: "end" },
1414
+ ],
1415
+ );
1416
+ }
1417
+ }
1418
+
1419
+ for (const c of meta.consts ?? []) {
1420
+ if (!c.dynamic) continue;
1421
+ const g = mod.resolveGlobal(c.name);
1422
+ if (!g) continue;
1423
+ addFunc(
1424
+ `__const_${c.name}`,
1425
+ [],
1426
+ [extType(c.desc.wType)],
1427
+ [{ op: "global.get", index: g.index }, ...toExt(c.desc.wType)],
1428
+ );
1429
+ }
1430
+ }
1431
+
1432
+ // `(externref) -> i32`: `ref.test`s param 0 against each type index and
1433
+ // returns `resultFor(i)` for the first match, 0 otherwise.
1434
+ function refTestChain(typeIndices, resultFor) {
1435
+ const body = typeIndices.flatMap((ti, i) => [
1436
+ { op: "local.get", index: 0 },
1437
+ { op: "any.convert_extern" },
1438
+ { op: "ref.test", typeIndex: ti },
1439
+ { op: "if", blockType: "void" },
1440
+ { op: "i32.const", value: resultFor(i) },
1441
+ { op: "return" },
1442
+ { op: "end" },
1443
+ ]);
1444
+ body.push({ op: "i32.const", value: 0 });
1445
+ return body;
1446
+ }
1447
+
1448
+ // Type indices (incl. unexported and boxed-value types) whose method set
1449
+ // covers the interface, from the same candidate search the dispatchers use.
1450
+ function implementingTypeIndices(desc, mod) {
1451
+ let acc = null;
1452
+ for (const m of desc.methods) {
1453
+ const have = new Set(
1454
+ mod.findInterfaceCandidates(null, m.name).map((c) => c.testTypeIndex),
1455
+ );
1456
+ acc = acc === null ? have : new Set([...acc].filter((t) => have.has(t)));
1457
+ }
1458
+ return [...(acc ?? [])];
1459
+ }
1460
+
1461
+ // Interfaces: method calls go through the existing `ref.test` dispatchers;
1462
+ // `__impl$<I>` tells whether a wasm value implements I (guards JS inputs);
1463
+ // `__itag$<I>` reports which exported `*T` (1-based) a value holds, 0 if none.
1464
+ function addIfaceHelpers(desc, mod, addFunc, addTrampoline) {
1465
+ for (const m of desc.methods) {
1466
+ const entry = { exportName: m.dispatch };
1467
+ addTrampoline(entry);
1468
+ if (!entry.callName)
1469
+ throw new Error(
1470
+ `internal: no wasm dispatcher for interface method ${desc.name}.${m.name}`,
1471
+ );
1472
+ m.callName = entry.callName;
1473
+ }
1474
+ addFunc(
1475
+ `__impl$${desc.key}`,
1476
+ ["externref"],
1477
+ ["i32"],
1478
+ refTestChain(implementingTypeIndices(desc, mod), () => 1),
1479
+ );
1480
+ if (desc.concrete.length === 0) return;
1481
+ addFunc(
1482
+ `__itag$${desc.key}`,
1483
+ ["externref"],
1484
+ ["i32"],
1485
+ refTestChain(
1486
+ desc.concrete.map((c) => c.typeIndex),
1487
+ (i) => i + 1,
1488
+ ),
1489
+ );
826
1490
  }
827
1491
 
828
1492
  // ── JS facade generation ─────────────────────────────────────
@@ -845,16 +1509,34 @@ function inExpr(desc, v) {
845
1509
  return `${P}strin(${v})`;
846
1510
  case "any":
847
1511
  return `(${v} === undefined ? null : ${v})`;
1512
+ case "error":
1513
+ return `${P}errin(${v})`;
1514
+ case "time":
1515
+ return desc.ptr ? `${P}timein_ptr(${v})` : `${P}timein(${v})`;
1516
+ case "map":
1517
+ return `${P}mapin_${desc.key}(${v})`;
1518
+ case "ptr_box":
1519
+ return `${P}boxin_${desc.key}(${v})`;
1520
+ case "anon_struct":
1521
+ return desc.ptr
1522
+ ? `(${v} == null ? null : ${P}to_${desc.name}(${v}))`
1523
+ : `${P}to_${desc.name}(${v})`;
848
1524
  case "struct":
849
1525
  if (desc.info.pkgTarget === "both")
850
1526
  return `${P}to_${desc.name}(${v}, ${desc.ptr ? "false" : "true"})`;
851
- return desc.ptr ? `${P}href(${v})` : `${P}hval_${desc.name}(${v})`;
1527
+ return desc.ptr
1528
+ ? `${P}href_${desc.name}(${v})`
1529
+ : `${P}hval_${desc.name}(${v})`;
852
1530
  case "slice":
853
1531
  return `${P}slin_${desc.key}(${v})`;
854
1532
  case "array":
855
1533
  return `${P}arrin_${desc.key}(${v})`;
856
1534
  case "func":
857
1535
  return `${P}fnin_${desc.key}(${v})`;
1536
+ case "iface":
1537
+ return `${P}ifin_${desc.key}(${v})`;
1538
+ case "shared":
1539
+ return `${P}shin(${v}, ${desc.ctor})`;
858
1540
  default:
859
1541
  return v;
860
1542
  }
@@ -871,6 +1553,18 @@ function outExpr(desc, v) {
871
1553
  return desc.unsigned ? `${P}u64out(${v})` : `${P}i64out(${v})`;
872
1554
  case "string":
873
1555
  return `(${v} ?? "")`;
1556
+ case "error":
1557
+ return `${P}errout(${v})`;
1558
+ case "time":
1559
+ return desc.ptr ? `${P}timeout_ptr(${v})` : `${P}timeout(${v})`;
1560
+ case "map":
1561
+ return `${P}mapout_${desc.key}(${v})`;
1562
+ case "ptr_box":
1563
+ return `${P}boxout_${desc.key}(${v})`;
1564
+ case "anon_struct":
1565
+ return desc.ptr
1566
+ ? `(${v} == null ? null : ${P}from_${desc.name}(${v}))`
1567
+ : `${P}from_${desc.name}(${v})`;
874
1568
  case "struct":
875
1569
  return desc.info.pkgTarget === "both"
876
1570
  ? `${P}from_${desc.name}(${v})`
@@ -881,6 +1575,10 @@ function outExpr(desc, v) {
881
1575
  return `${P}arrout_${desc.key}(${v})`;
882
1576
  case "func":
883
1577
  return `${P}fnout_${desc.key}(${v})`;
1578
+ case "iface":
1579
+ return `${P}ifout_${desc.key}(${v})`;
1580
+ case "shared":
1581
+ return `${P}shout(${v}, ${desc.ctor})`;
884
1582
  default:
885
1583
  return v;
886
1584
  }
@@ -897,6 +1595,15 @@ function zeroExpr(desc) {
897
1595
  return "0";
898
1596
  case "string":
899
1597
  return '""';
1598
+ case "error":
1599
+ case "time":
1600
+ case "map":
1601
+ case "ptr_box":
1602
+ return "null";
1603
+ case "anon_struct":
1604
+ return desc.ptr
1605
+ ? "null"
1606
+ : `{ ${desc.fields.map((f) => `"${f.name}": ${zeroExpr(f.desc)}`).join(", ")} }`;
900
1607
  case "struct":
901
1608
  return desc.ptr ? "null" : `new ${desc.name}()`;
902
1609
  case "array":
@@ -932,6 +1639,10 @@ function isBothStruct(d) {
932
1639
  return d.k === "struct" && d.info.pkgTarget === "both";
933
1640
  }
934
1641
 
1642
+ function isPtrWriteBack(d) {
1643
+ return isPtrBoth(d) || d.k === "ptr_box" || (d.k === "anon_struct" && d.ptr);
1644
+ }
1645
+
935
1646
  function returnLines(returns, resultExpr) {
936
1647
  if (returns.length === 0) return [];
937
1648
  if (returns.length === 1)
@@ -946,13 +1657,21 @@ function genFuncBody(exportName, params, returns, leadArgs = []) {
946
1657
  const names = params.map((_, i) => `a${i}`);
947
1658
  const lines = [];
948
1659
  const argExprs = [...leadArgs];
949
- const writeBack = params.some(isPtrBoth);
950
- if (writeBack) lines.push("\tconst __m = new Map();");
1660
+ const writeBack = params.some(isPtrWriteBack);
1661
+ if (params.some(isPtrBoth)) lines.push("\tconst __m = new Map();");
951
1662
  for (let i = 0; i < params.length; i++) {
952
1663
  const d = params[i];
953
1664
  if (isPtrBoth(d)) {
954
1665
  lines.push(`\tconst __p${i} = ${P}to_${d.name}(a${i}, false, __m);`);
955
1666
  argExprs.push(`__p${i}`);
1667
+ } else if (d.k === "ptr_box") {
1668
+ lines.push(`\tconst __p${i} = ${P}boxin_${d.key}(a${i});`);
1669
+ argExprs.push(`__p${i}`);
1670
+ } else if (d.k === "anon_struct" && d.ptr) {
1671
+ lines.push(
1672
+ `\tconst __p${i} = a${i} == null ? null : ${P}to_${d.name}(a${i});`,
1673
+ );
1674
+ argExprs.push(`__p${i}`);
956
1675
  } else {
957
1676
  argExprs.push(inExpr(d, names[i]));
958
1677
  }
@@ -970,8 +1689,12 @@ function genFuncBody(exportName, params, returns, leadArgs = []) {
970
1689
  } else {
971
1690
  lines.push(`\t\tconst __r = ${call};`);
972
1691
  for (let i = 0; i < params.length; i++) {
973
- if (isPtrBoth(params[i]))
974
- lines.push(`\t\t${P}back_${params[i].name}(__p${i}, a${i});`);
1692
+ const d = params[i];
1693
+ if (isPtrBoth(d)) lines.push(`\t\t${P}back_${d.name}(__p${i}, a${i});`);
1694
+ else if (d.k === "ptr_box")
1695
+ lines.push(`\t\t${P}boxback_${d.key}(__p${i}, a${i});`);
1696
+ else if (d.k === "anon_struct" && d.ptr)
1697
+ lines.push(`\t\t${P}back_${d.name}(__p${i}, a${i});`);
975
1698
  }
976
1699
  lines.push(...returnLines(returns, "__r").map((l) => `\t${l}`));
977
1700
  }
@@ -989,15 +1712,40 @@ function indent(text, prefix) {
989
1712
  // quoted, helpers prefixed with `__`.
990
1713
  export const WASM_IMPORTS_JS = `const ${P}MAX = 9007199254740991n;
991
1714
  function ${P}imports(stringTable, extraEnv, tag, write) {
992
- let buf = [];
1715
+ let lineBuf = "";
993
1716
  let targs = [];
994
- const flush = () => { const s = buf.join(" "); buf = []; write(s); };
995
- const print = (v) => { buf.push(String(v)); };
996
- const println = (v) => { buf.push(String(v)); flush(); };
1717
+ let fmtStructMap = new WeakMap();
1718
+ const late = { exports: null };
1719
+ const flush = () => { write(lineBuf); lineBuf = ""; };
1720
+ const print = (v) => {
1721
+ const s = String(v);
1722
+ if (lineBuf.length > 0 && !lineBuf.endsWith(" ") && !s.startsWith(" ")) {
1723
+ lineBuf += " ";
1724
+ }
1725
+ lineBuf += s;
1726
+ };
1727
+ const println = (v) => {
1728
+ const s = String(v);
1729
+ if (lineBuf.length > 0 && !lineBuf.endsWith(" ") && !s.startsWith(" ")) {
1730
+ lineBuf += " ";
1731
+ }
1732
+ lineBuf += s;
1733
+ flush();
1734
+ };
997
1735
  const targ = (v) => { targs.push(v); };
1736
+ let utf8Encoded = null;
1737
+ let convBytes = null, convPos = 0;
998
1738
  const env = {
999
1739
  "panicTag": tag,
1000
- "panic": (msg) => { throw new Error(String(msg)); },
1740
+ "panic": (msg) => {
1741
+ let str;
1742
+ try {
1743
+ str = String(msg);
1744
+ } catch {
1745
+ str = "[panic object]";
1746
+ }
1747
+ throw new Error(str);
1748
+ },
1001
1749
  "str": (i) => stringTable[i] ?? "",
1002
1750
  "str_len": (s) => (s ? s.length : 0),
1003
1751
  "str_concat": (a, b) => (a ?? "") + (b ?? ""),
@@ -1011,7 +1759,53 @@ function ${P}imports(stringTable, extraEnv, tag, write) {
1011
1759
  "str_slice": (s, a, b) => (s ? s.slice(a, b) : ""),
1012
1760
  "str_from_code_point": (c) => String.fromCodePoint(c),
1013
1761
  "str_code_point_at": (s, i) => (s ? s.codePointAt(i) : 0),
1762
+ "str_hash": (s) => {
1763
+ if (!s) return 0;
1764
+ let h = 0;
1765
+ for (let i = 0; i < s.length; i++) {
1766
+ h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
1767
+ }
1768
+ return h;
1769
+ },
1770
+ "str_to_upper": (s) => (s ? s.toUpperCase() : ""),
1771
+ "str_to_lower": (s) => (s ? s.toLowerCase() : ""),
1772
+ "str_trim_space": (s) => (s ? s.trim() : ""),
1773
+ "str_contains": (s, sub) => (s && sub !== undefined ? (s.includes(sub) ? 1 : 0) : 0),
1774
+ "str_has_prefix": (s, pre) => (s && pre !== undefined ? (s.startsWith(pre) ? 1 : 0) : 0),
1775
+ "str_has_suffix": (s, suf) => (s && suf !== undefined ? (s.endsWith(suf) ? 1 : 0) : 0),
1776
+ "str_index": (s, sub) => (s ? s.indexOf(sub) : -1),
1777
+ "str_last_index": (s, sub) => (s ? s.lastIndexOf(sub) : -1),
1778
+ "str_repeat": (s, n) => (s && n > 0 ? s.repeat(Number(n)) : ""),
1779
+ "str_replace_all": (s, o, n) => (s ? s.replaceAll(o, n) : ""),
1780
+ "str_equal_fold": (a, b) => ((a ?? "").toLowerCase() === (b ?? "").toLowerCase() ? 1 : 0),
1781
+ "str_count": (s, sep) => {
1782
+ if (!s) return sep === "" ? 1 : 0;
1783
+ if (sep === "") return s.length + 1;
1784
+ return s.split(sep).length - 1;
1785
+ },
1786
+ "str_from_i64": (n) => String(n),
1787
+ "str_from_i32": (n) => String(n),
1788
+ "str_from_f64": (f) => String(f),
1014
1789
  "is_string": (v) => (typeof v === "string" ? 1 : 0),
1790
+ "str_to_bytes": (s) => {
1791
+ utf8Encoded = new TextEncoder().encode(s ?? "");
1792
+ return utf8Encoded.length;
1793
+ },
1794
+ "str_byte_at": (i) => (utf8Encoded ? (utf8Encoded[i] ?? 0) : 0),
1795
+ "bytes_to_str_init": (len) => {
1796
+ convBytes = new Uint8Array(len);
1797
+ convPos = 0;
1798
+ },
1799
+ "bytes_to_str_push": (b) => {
1800
+ if (convBytes && convPos < convBytes.length) {
1801
+ convBytes[convPos++] = b & 0xFF;
1802
+ }
1803
+ },
1804
+ "bytes_to_str_finish": () => {
1805
+ const res = convBytes ? new TextDecoder().decode(convBytes) : "";
1806
+ convBytes = null;
1807
+ return res;
1808
+ },
1015
1809
  "print_i32": print, "print_i64": print, "print_f32": print, "print_f64": print,
1016
1810
  "print_str": print, "print_any": print,
1017
1811
  "print_bool": (b) => print(b !== 0 ? "true" : "false"),
@@ -1019,22 +1813,631 @@ function ${P}imports(stringTable, extraEnv, tag, write) {
1019
1813
  "println_str": println, "println_any": println,
1020
1814
  "println_bool": (b) => println(b !== 0 ? "true" : "false"),
1021
1815
  "println_empty": flush,
1816
+ "fmt_new_array": () => [],
1817
+ "fmt_array_push": (arr, v) => { arr.push(v); },
1818
+ "fmt_new_map": () => new Map(),
1819
+ "fmt_map_set": (m, k, v) => { m.set(k, v); },
1820
+ "fmt_new_struct": (ref, isPtr) => {
1821
+ if (ref !== null && ref !== undefined && typeof ref === "object" && fmtStructMap.has(ref)) {
1822
+ return fmtStructMap.get(ref);
1823
+ }
1824
+ const o = {};
1825
+ Object.defineProperty(o, "__struct", { value: true, configurable: true });
1826
+ if (isPtr !== 0) Object.defineProperty(o, "__p", { value: true, configurable: true });
1827
+ if (ref !== null && ref !== undefined && typeof ref === "object") fmtStructMap.set(ref, o);
1828
+ return o;
1829
+ },
1830
+ "fmt_struct_set": (o, k, v) => { o[typeof k === "number" ? stringTable[k] : k] = v; },
1831
+ "fmt_box_i32": (v) => v,
1832
+ "fmt_box_i64": (v) => (v > ${P}MAX || v < -${P}MAX ? v : Number(v)),
1833
+ "fmt_box_f64": (v) => v,
1834
+ "fmt_box_bool": (v) => v !== 0,
1835
+ "fmt_new_box": () => ({ value: null, __p: true, __box: true }),
1022
1836
  "testing_arg_i32": targ, "testing_arg_f32": targ, "testing_arg_f64": targ,
1023
1837
  "testing_arg_str": targ, "testing_arg_any": targ,
1024
1838
  "testing_arg_i64": (v) => targ(v > ${P}MAX || v < -${P}MAX ? v : Number(v)),
1025
1839
  "testing_arg_bool": (v) => targ(v !== 0),
1026
- "testing_call": (t, i) => { const a = targs; targs = []; t[stringTable[i]](...a); },
1840
+ "testing_call": (t, i) => { const a = targs; targs = []; fmtStructMap = new WeakMap(); t[stringTable[i]](...a); },
1027
1841
  "testing_name": (t) => t.Name(),
1028
1842
  "testing_flag": (t, i) => (t[stringTable[i]]() ? 1 : 0),
1029
1843
  };
1844
+ // Stdlib imports (strings/strconv/utf8/fmt/t.Run) are a separate block so
1845
+ // modules that never call them do not ship it (see WASM_STDLIB_JS).
1846
+ if (typeof ${P}stdlib === "function") {
1847
+ Object.assign(env, ${P}stdlib(stringTable, {
1848
+ "take": () => { const a = targs; targs = []; fmtStructMap = new WeakMap(); return a; },
1849
+ "printf": (s) => {
1850
+ lineBuf += s;
1851
+ const nl = lineBuf.lastIndexOf("\\n");
1852
+ if (nl >= 0) { write(lineBuf.slice(0, nl)); lineBuf = lineBuf.slice(nl + 1); }
1853
+ },
1854
+ "late": late,
1855
+ }));
1856
+ }
1030
1857
  Object.assign(env, extraEnv);
1031
1858
  const m = {};
1032
- for (const k of ["sin", "cos", "tan", "asin", "acos", "atan", "atan2", "pow", "exp", "log", "log2", "log10", "round"]) m[k] = Math[k];
1033
- return { "env": env, "Math": m };
1859
+ for (const k of ["sin", "cos", "tan", "asin", "acos", "atan", "atan2", "pow", "exp", "log", "log2", "log10", "cbrt", "hypot", "random", "sinh", "cosh", "tanh"]) m[k] = Math[k];
1860
+ m.mod = (x, y) => x % y;
1861
+ // Go rounds half away from zero; Math.round rounds half toward +Infinity.
1862
+ m.round = (x) => Math.sign(x) * Math.round(Math.abs(x));
1863
+ // Exports are needed by imports that call back into the module (t.Run).
1864
+ return { "env": env, "Math": m, "__bind": (ex) => { late.exports = ex; } };
1865
+ }`;
1866
+
1867
+ // JS side of the STDLIB_IMPORTS table (index.js); emitted only when the
1868
+ // module imports one of the packages that use it.
1869
+ export const WASM_STDLIB_JS = `${HELPER_ERROR}
1870
+ ${HELPER_SPRINTF}
1871
+ ${HELPER_STRCONV}
1872
+ ${HELPER_PATH_CLEAN}
1873
+ ${HELPER_DURATION}
1874
+ ${HELPER_SHA256}
1875
+ ${HELPER_HEX}
1876
+ ${HELPER_BASE64}
1877
+ function ${P}stdlib(stringTable, io) {
1878
+ let parts = [];
1879
+ let cutBefore = "", cutAfter = "";
1880
+ let strconvErr = null, strconvErrKind = 0;
1881
+ let decodeSize = 0;
1882
+ let replacerTarget = "", replacerPairs = [];
1883
+ let splitDir = "", splitFile = "";
1884
+ let joinParts = [];
1885
+ let matchHasErr = 0;
1886
+ let trimStart = 0, trimEnd = 0;
1887
+ let sscanTokens = [];
1888
+ let sscanHasErr = 0, sscanErrMsg = "";
1889
+ let timeParseErr = null, timeParseOffset = 0;
1890
+ let rawBytes = new Uint8Array(0);
1891
+ let rawPos = 0;
1892
+ let hexErr = null;
1893
+ let base64Err = null;
1894
+ let customBase64Encodings = [];
1895
+ return {
1896
+ "fmt_sscan_init": (s, mode) => {
1897
+ sscanHasErr = 0;
1898
+ sscanErrMsg = "";
1899
+ const str = s ?? "";
1900
+ sscanTokens = (mode === 1 ? str.split("\\n")[0] : str).trim().split(/\\s+/).filter(Boolean);
1901
+ return sscanTokens.length;
1902
+ },
1903
+ "fmt_sscanf_init": (s, fmt) => {
1904
+ sscanHasErr = 0;
1905
+ sscanErrMsg = "";
1906
+ const str = s ?? "";
1907
+ const fmtStr = fmt ?? "";
1908
+ let si = 0, fi = 0;
1909
+ sscanTokens = [];
1910
+ while (fi < fmtStr.length) {
1911
+ if (/\\s/.test(fmtStr[fi])) {
1912
+ while (fi < fmtStr.length && /\\s/.test(fmtStr[fi])) fi++;
1913
+ while (si < str.length && /\\s/.test(str[si])) si++;
1914
+ continue;
1915
+ }
1916
+ if (fmtStr[fi] === "%") {
1917
+ if (fi + 1 < fmtStr.length && fmtStr[fi + 1] === "%") {
1918
+ if (str[si] !== "%") {
1919
+ sscanHasErr = 1;
1920
+ sscanErrMsg = "input does not match format";
1921
+ break;
1922
+ }
1923
+ si++;
1924
+ fi += 2;
1925
+ continue;
1926
+ }
1927
+ const vm = fmtStr.slice(fi).match(/^%[vdsfqtxXbcoeEfFgG]/);
1928
+ if (vm) {
1929
+ const verb = vm[0];
1930
+ fi += verb.length;
1931
+ if (verb !== "%c") {
1932
+ while (si < str.length && /\\s/.test(str[si])) si++;
1933
+ }
1934
+ if (si >= str.length) break;
1935
+ let tokRe = /^\\S+/;
1936
+ if (verb === "%d" || verb === "%i") tokRe = /^[+-]?\\d+/;
1937
+ else if (verb === "%f" || verb === "%F" || verb === "%e" || verb === "%E" || verb === "%g" || verb === "%G") tokRe = /^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?/;
1938
+ else if (verb === "%t") tokRe = /^(?:true|false|1|0|t|f|T|F|TRUE|FALSE)/;
1939
+ else if (verb === "%x" || verb === "%X") tokRe = /^[+-]?(?:0x)?[0-9a-fA-F]+/;
1940
+ else if (verb === "%c") tokRe = /^[\\s\\S]/;
1941
+ const m = str.slice(si).match(tokRe);
1942
+ if (!m) {
1943
+ sscanHasErr = 1;
1944
+ sscanErrMsg = "input does not match format";
1945
+ break;
1946
+ }
1947
+ sscanTokens.push(m[0]);
1948
+ si += m[0].length;
1949
+ continue;
1950
+ }
1951
+ }
1952
+ if (si >= str.length) break;
1953
+ if (str[si] !== fmtStr[fi]) {
1954
+ sscanHasErr = 1;
1955
+ sscanErrMsg = "input does not match format";
1956
+ break;
1957
+ }
1958
+ si++;
1959
+ fi++;
1960
+ }
1961
+ return sscanTokens.length;
1962
+ },
1963
+ "fmt_sscan_has_err": () => sscanHasErr,
1964
+ "fmt_sscan_err_msg": () => sscanErrMsg || "syntax error",
1965
+ "fmt_sscan_token_str": (i) => sscanTokens[i] ?? "",
1966
+ "fmt_sscan_token_i64": (i) => {
1967
+ if (i >= sscanTokens.length) { sscanHasErr = 1; sscanErrMsg = "unexpected EOF"; return 0n; }
1968
+ const t = sscanTokens[i];
1969
+ if (/^[+-]?(?:0[xX])?[0-9a-fA-F]+$/.test(t)) {
1970
+ try {
1971
+ if (/^[+-]?[0-9a-fA-F]+$/.test(t) && /[a-fA-F]/.test(t)) {
1972
+ return BigInt("0x" + t);
1973
+ }
1974
+ return BigInt(t);
1975
+ } catch {}
1976
+ }
1977
+ if (t.length === 1) {
1978
+ return BigInt(t.charCodeAt(0));
1979
+ }
1980
+ sscanHasErr = 1;
1981
+ sscanErrMsg = "expected integer";
1982
+ return 0n;
1983
+ },
1984
+ "fmt_sscan_token_f64": (i) => {
1985
+ if (i >= sscanTokens.length) { sscanHasErr = 1; sscanErrMsg = "unexpected EOF"; return 0; }
1986
+ const t = sscanTokens[i];
1987
+ const n = Number(t);
1988
+ if (Number.isNaN(n) || !/^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$/.test(t)) {
1989
+ sscanHasErr = 1;
1990
+ sscanErrMsg = "expected float";
1991
+ return 0;
1992
+ }
1993
+ return n;
1994
+ },
1995
+ "fmt_sscan_token_bool": (i) => {
1996
+ if (i >= sscanTokens.length) { sscanHasErr = 1; sscanErrMsg = "unexpected EOF"; return 0; }
1997
+ const t = sscanTokens[i]?.toLowerCase();
1998
+ if (t === "true" || t === "1" || t === "t") return 1;
1999
+ if (t === "false" || t === "0" || t === "f") return 0;
2000
+ sscanHasErr = 1;
2001
+ sscanErrMsg = "expected boolean";
2002
+ return 0;
2003
+ },
2004
+ "testing_run": (t, name, fn) => (t.Run(name, (sub) => { io.late.exports.__testing_run_cb(fn, sub); }) ? 1 : 0),
2005
+ "fmt_sprintf": (f) => __sprintf(f ?? "", ...io.take()),
2006
+ "fmt_printf": (f) => { io.printf(__sprintf(f ?? "", ...io.take())); },
2007
+ "fmt_format_v": (v) => { const res = __sprintf("%v", v); io.take(); return res; },
2008
+ "str_trim": (s, c) => ${P}trim(s ?? "", c ?? "", true, true),
2009
+ "str_trim_left": (s, c) => ${P}trim(s ?? "", c ?? "", true, false),
2010
+ "str_trim_right": (s, c) => ${P}trim(s ?? "", c ?? "", false, true),
2011
+ "str_trim_prefix": (s, p) => (s && p && s.startsWith(p) ? s.slice(p.length) : (s ?? "")),
2012
+ "str_trim_suffix": (s, p) => (s && p && s.endsWith(p) ? s.slice(0, s.length - p.length) : (s ?? "")),
2013
+ "str_cut": (s, sep) => {
2014
+ const i = (s ?? "").indexOf(sep ?? "");
2015
+ if (i < 0) { cutBefore = s ?? ""; cutAfter = ""; return 0; }
2016
+ cutBefore = (s ?? "").slice(0, i);
2017
+ cutAfter = (s ?? "").slice(i + (sep ?? "").length);
2018
+ return 1;
2019
+ },
2020
+ "str_cut_before": () => cutBefore,
2021
+ "str_cut_after": () => cutAfter,
2022
+ "str_cut_prefix": (s, p) => {
2023
+ s = s ?? ""; p = p ?? "";
2024
+ if (s.startsWith(p)) { cutAfter = s.slice(p.length); return 1; }
2025
+ cutAfter = s; return 0;
2026
+ },
2027
+ "str_cut_suffix": (s, suf) => {
2028
+ s = s ?? ""; suf = suf ?? "";
2029
+ if (s.endsWith(suf)) { cutBefore = s.slice(0, s.length - suf.length); return 1; }
2030
+ cutBefore = s; return 0;
2031
+ },
2032
+ "str_replace": (s, o, n, cnt) => {
2033
+ s = s ?? ""; o = o ?? ""; n = n ?? "";
2034
+ if (cnt < 0) return s.replaceAll(o, n);
2035
+ let out = "", from = 0;
2036
+ for (let k = 0; k < cnt; k++) {
2037
+ const at = s.indexOf(o, from);
2038
+ if (at < 0) break;
2039
+ out += s.slice(from, at) + n;
2040
+ from = at + o.length;
2041
+ if (o === "") { if (from >= s.length) break; out += s[from]; from++; }
2042
+ }
2043
+ return out + s.slice(from);
2044
+ },
2045
+ "str_contains_rune": (s, r) => ((s ?? "").includes(String.fromCodePoint(r)) ? 1 : 0),
2046
+ "str_contains_any": (s, cs) => { for (const c of (cs ?? "")) if ((s ?? "").includes(c)) return 1; return 0; },
2047
+ "str_index_byte": (s, b) => (s ?? "").indexOf(String.fromCharCode(b)),
2048
+ "str_index_rune": (s, r) => (s ?? "").indexOf(String.fromCodePoint(r)),
2049
+ "str_index_any": (s, cs) => { s = s ?? ""; let best = -1; for (const c of (cs ?? "")) { const i = s.indexOf(c); if (i >= 0 && (best < 0 || i < best)) best = i; } return best; },
2050
+ "str_last_index_byte": (s, b) => (s ?? "").lastIndexOf(String.fromCharCode(b)),
2051
+ "str_last_index_any": (s, cs) => {
2052
+ s = s ?? ""; let best = -1;
2053
+ for (const c of (cs ?? "")) {
2054
+ const i = s.lastIndexOf(c);
2055
+ if (i > best) best = i;
2056
+ }
2057
+ return best;
2058
+ },
2059
+ "str_title": (s) => (s ?? "").replace(/(^|[^\\p{L}\\p{N}_'])(\\p{Ll})/gu, (m, a, b) => a + b.toUpperCase()),
2060
+ "str_split": (s, sep) => { parts = (s ?? "").split(sep ?? ""); return parts.length; },
2061
+ "str_split_n": (s, sep, n) => {
2062
+ s = s ?? ""; sep = sep ?? "";
2063
+ if (n === 0) return -1;
2064
+ if (n < 0) { parts = sep === "" ? [...s] : s.split(sep); return parts.length; }
2065
+ if (sep === "") {
2066
+ const chars = [...s];
2067
+ if (chars.length <= n) { parts = chars; return parts.length; }
2068
+ const r = chars.slice(0, n - 1);
2069
+ r.push(chars.slice(n - 1).join(""));
2070
+ parts = r;
2071
+ return parts.length;
2072
+ }
2073
+ const r = [];
2074
+ let cur = s;
2075
+ for (let i = 1; i < n && cur.length; i++) {
2076
+ const j = cur.indexOf(sep);
2077
+ if (j < 0) break;
2078
+ r.push(cur.slice(0, j));
2079
+ cur = cur.slice(j + sep.length);
2080
+ }
2081
+ r.push(cur);
2082
+ parts = r;
2083
+ return parts.length;
2084
+ },
2085
+ "str_split_after": (s, sep) => {
2086
+ s = s ?? ""; sep = sep ?? "";
2087
+ if (sep === "") { parts = [...s]; return parts.length; }
2088
+ const r = [];
2089
+ let cur = s;
2090
+ while (true) {
2091
+ const j = cur.indexOf(sep);
2092
+ if (j < 0) { r.push(cur); break; }
2093
+ r.push(cur.slice(0, j + sep.length));
2094
+ cur = cur.slice(j + sep.length);
2095
+ }
2096
+ parts = r;
2097
+ return parts.length;
2098
+ },
2099
+ "str_split_after_n": (s, sep, n) => {
2100
+ s = s ?? ""; sep = sep ?? "";
2101
+ if (n === 0) return -1;
2102
+ if (sep === "") {
2103
+ const chars = [...s];
2104
+ if (n < 0 || chars.length <= n) parts = chars;
2105
+ else {
2106
+ const r = chars.slice(0, n - 1);
2107
+ r.push(chars.slice(n - 1).join(""));
2108
+ parts = r;
2109
+ }
2110
+ return parts.length;
2111
+ }
2112
+ const r = [];
2113
+ let cur = s;
2114
+ for (let i = 1; (n < 0 || i < n) && cur.length; i++) {
2115
+ const j = cur.indexOf(sep);
2116
+ if (j < 0) break;
2117
+ r.push(cur.slice(0, j + sep.length));
2118
+ cur = cur.slice(j + sep.length);
2119
+ }
2120
+ r.push(cur);
2121
+ parts = r;
2122
+ return parts.length;
2123
+ },
2124
+ "str_fields": (s) => { parts = (s ?? "").split(/\\s+/).filter((p) => p !== ""); return parts.length; },
2125
+ "str_part": (i) => parts[i] ?? "",
2126
+ "str_replacer_init": (s) => {
2127
+ replacerTarget = s ?? "";
2128
+ replacerPairs = [];
2129
+ },
2130
+ "str_replacer_add": (o, n) => {
2131
+ replacerPairs.push([o ?? "", n ?? ""]);
2132
+ },
2133
+ "str_replacer_run": () => {
2134
+ let s = replacerTarget;
2135
+ if (!replacerPairs.length || s === "") return s;
2136
+ let out = "";
2137
+ let i = 0;
2138
+ while (i < s.length) {
2139
+ let matched = false;
2140
+ for (let k = 0; k < replacerPairs.length; k++) {
2141
+ const [oldStr, newStr] = replacerPairs[k];
2142
+ if (oldStr.length > 0 && s.startsWith(oldStr, i)) {
2143
+ out += newStr;
2144
+ i += oldStr.length;
2145
+ matched = true;
2146
+ break;
2147
+ }
2148
+ }
2149
+ if (!matched) {
2150
+ out += s[i];
2151
+ i++;
2152
+ }
2153
+ }
2154
+ return out;
2155
+ },
2156
+ "strconv_atoi": (s) => {
2157
+ const r = __strconv_atoi(s ?? "");
2158
+ strconvErr = r[1];
2159
+ strconvErrKind = r[1] ? (r[1].includes("invalid syntax") ? 1 : r[1].includes("value out of range") ? 2 : 0) : 0;
2160
+ return r[0];
2161
+ },
2162
+ "strconv_parse_int": (s, base, bits) => {
2163
+ const r = __strconv_parse_int(s ?? "", base, bits);
2164
+ strconvErr = r[1];
2165
+ strconvErrKind = r[1] ? (r[1].includes("invalid syntax") ? 1 : r[1].includes("value out of range") ? 2 : 0) : 0;
2166
+ return r[0];
2167
+ },
2168
+ "strconv_parse_float": (s) => {
2169
+ const r = __strconv_parse_float(s ?? "");
2170
+ strconvErr = r[1];
2171
+ strconvErrKind = r[1] ? (r[1].includes("invalid syntax") ? 1 : r[1].includes("value out of range") ? 2 : 0) : 0;
2172
+ return r[0];
2173
+ },
2174
+ "strconv_parse_bool": (s) => {
2175
+ const r = __strconv_parse_bool(s ?? "");
2176
+ strconvErr = r[1];
2177
+ strconvErrKind = r[1] ? (r[1].includes("invalid syntax") ? 1 : r[1].includes("value out of range") ? 2 : 0) : 0;
2178
+ return r[0] ? 1 : 0;
2179
+ },
2180
+ "strconv_err": () => strconvErr,
2181
+ "strconv_err_kind": () => strconvErrKind,
2182
+ "strconv_format_float": (f, fmtc, prec, _bits) => __strconv_format_float(f, fmtc, prec),
2183
+ "strconv_format_int": (n, base) => n.toString(base),
2184
+ "strconv_format_uint": (n, base) => BigInt.asUintN(64, BigInt(n)).toString(base),
2185
+ "strconv_parse_uint": (s, base, bits) => {
2186
+ const r = __strconv_parse_uint(s ?? "", base, bits);
2187
+ strconvErr = r[1];
2188
+ strconvErrKind = r[1] ? (r[1].includes("invalid syntax") ? 1 : r[1].includes("value out of range") ? 2 : 0) : 0;
2189
+ return r[0];
2190
+ },
2191
+ "strconv_quote": (s) => JSON.stringify(s ?? ""),
2192
+ "strconv_unquote": (s) => {
2193
+ s = s ?? "";
2194
+ if (s.startsWith("\`") && s.endsWith("\`") && s.length >= 2) {
2195
+ if (s.slice(1, -1).includes("\`")) {
2196
+ strconvErr = "invalid syntax";
2197
+ strconvErrKind = 1;
2198
+ return "";
2199
+ }
2200
+ strconvErr = null;
2201
+ strconvErrKind = 0;
2202
+ return s.slice(1, -1);
2203
+ }
2204
+ try {
2205
+ const v = JSON.parse(s);
2206
+ if (typeof v !== "string") {
2207
+ strconvErr = "invalid syntax";
2208
+ strconvErrKind = 1;
2209
+ return "";
2210
+ }
2211
+ strconvErr = null;
2212
+ strconvErrKind = 0;
2213
+ return v;
2214
+ } catch (e) {
2215
+ strconvErr = "invalid syntax";
2216
+ strconvErrKind = 1;
2217
+ return "";
2218
+ }
2219
+ },
2220
+ "utf8_rune_count": (s) => { let n = 0; for (const _ of (s ?? "")) n++; return n; },
2221
+ "utf8_rune_len": (r) => (r < 0 || r > 0x10ffff || (r >= 0xd800 && r <= 0xdfff) ? -1 : r < 0x80 ? 1 : r < 0x800 ? 2 : r < 0x10000 ? 3 : 4),
2222
+ "utf8_valid_string": (s) => (s ?? "").isWellFormed ? ((s ?? "").isWellFormed() ? 1 : 0) : (/[\\uD800-\\uDFFF]/.test((s ?? "").replace(/[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]/g, "")) ? 0 : 1),
2223
+ "utf8_decode_rune": (s, last) => {
2224
+ s = s ?? "";
2225
+ if (s.length === 0) { decodeSize = 0; return 0xfffd; }
2226
+ let cp;
2227
+ if (last) { const tail = s.codePointAt(s.length - 1); cp = (tail >= 0xdc00 && tail <= 0xdfff && s.length >= 2) ? s.codePointAt(s.length - 2) : tail; }
2228
+ else cp = s.codePointAt(0);
2229
+ if (cp >= 0xd800 && cp <= 0xdfff) { decodeSize = 1; return 0xfffd; }
2230
+ // Size is in UTF-16 code units, matching the JS backend (strings are JS strings).
2231
+ decodeSize = cp > 0xffff ? 2 : 1;
2232
+ return cp;
2233
+ },
2234
+ "utf8_decode_size": () => decodeSize,
2235
+ "utf8_full_rune": (s) => { s = s ?? ""; if (s.length === 0) return 0; const c = s.charCodeAt(0); return (c >= 0xd800 && c <= 0xdbff && s.length < 2) ? 0 : 1; },
2236
+ // unicode
2237
+ "unicode_is_letter": (r) => { const c = (r >= 0 && r <= 0x10ffff) ? String.fromCodePoint(r) : null; return (c && /\\p{L}/u.test(c)) ? 1 : 0; },
2238
+ "unicode_is_digit": (r) => { const c = (r >= 0 && r <= 0x10ffff) ? String.fromCodePoint(r) : null; return (c && /\\p{Nd}/u.test(c)) ? 1 : 0; },
2239
+ "unicode_is_space": (r) => { const c = (r >= 0 && r <= 0x10ffff) ? String.fromCodePoint(r) : null; return (c && /\\s/u.test(c)) ? 1 : 0; },
2240
+ "unicode_is_upper": (r) => { const c = (r >= 0 && r <= 0x10ffff) ? String.fromCodePoint(r) : null; return (c && /\\p{Lu}/u.test(c)) ? 1 : 0; },
2241
+ "unicode_is_lower": (r) => { const c = (r >= 0 && r <= 0x10ffff) ? String.fromCodePoint(r) : null; return (c && /\\p{Ll}/u.test(c)) ? 1 : 0; },
2242
+ "unicode_is_punct": (r) => { const c = (r >= 0 && r <= 0x10ffff) ? String.fromCodePoint(r) : null; return (c && /\\p{P}/u.test(c)) ? 1 : 0; },
2243
+ "unicode_is_control": (r) => { const c = (r >= 0 && r <= 0x10ffff) ? String.fromCodePoint(r) : null; return (c && /\\p{Cc}/u.test(c)) ? 1 : 0; },
2244
+ "unicode_is_print": (r) => { const c = (r >= 0 && r <= 0x10ffff) ? String.fromCodePoint(r) : null; return (c && !/\\p{Cc}/u.test(c)) ? 1 : 0; },
2245
+ "unicode_is_graphic": (r) => { const c = (r >= 0 && r <= 0x10ffff) ? String.fromCodePoint(r) : null; return (c && !/\\p{Cc}/u.test(c)) ? 1 : 0; },
2246
+ "unicode_to_upper": (r) => { const c = (r >= 0 && r <= 0x10ffff) ? String.fromCodePoint(r) : null; return c ? (c.toUpperCase().codePointAt(0) ?? r) : r; },
2247
+ "unicode_to_lower": (r) => { const c = (r >= 0 && r <= 0x10ffff) ? String.fromCodePoint(r) : null; return c ? (c.toLowerCase().codePointAt(0) ?? r) : r; },
2248
+ // html
2249
+ "html_escape": (s) => (s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&#34;").replace(/'/g, "&#39;"),
2250
+ "html_unescape": (s) => (s ?? "").replace(/&(?:quot|apos|#39|#34|gt|lt|amp|#(?:[0-9]+|[xX][0-9a-fA-F]+));/g, (m) => {
2251
+ switch (m) {
2252
+ case "&quot;": case "&#34;": return '"';
2253
+ case "&apos;": case "&#39;": return "'";
2254
+ case "&gt;": return ">";
2255
+ case "&lt;": return "<";
2256
+ case "&amp;": return "&";
2257
+ default: {
2258
+ if (m.startsWith("&#")) {
2259
+ const raw = m.slice(2, -1);
2260
+ const cp = (raw.startsWith("x") || raw.startsWith("X")) ? parseInt(raw.slice(1), 16) : parseInt(raw, 10);
2261
+ return (cp >= 0 && cp <= 0x10FFFF && !(cp >= 0xD800 && cp <= 0xDFFF)) ? String.fromCodePoint(cp) : "\\uFFFD";
2262
+ }
2263
+ return m;
2264
+ }
2265
+ }
2266
+ }),
2267
+ // path
2268
+ "path_base": (p) => { if (!p) return "."; const stripped = p.replace(/\\/+$/, ""); if (!stripped) return "/"; const i = stripped.lastIndexOf("/"); return i < 0 ? stripped : stripped.slice(i + 1) || "/"; },
2269
+ "path_dir": (p) => { const s = p ?? ""; const i = s.lastIndexOf("/"); if (i < 0) return "."; const res = s.slice(0, i); return res === "" ? "/" : __pathClean(res); },
2270
+ "path_ext": (p) => { const s = p ?? ""; const b = s.slice(s.lastIndexOf("/") + 1); const i = b.lastIndexOf("."); return i <= 0 ? "" : b.slice(i); },
2271
+ "path_is_abs": (p) => (p ?? "").startsWith("/") ? 1 : 0,
2272
+ "path_clean": (p) => __pathClean(p ?? ""),
2273
+ "path_split": (p) => {
2274
+ const s = p ?? "";
2275
+ const i = s.lastIndexOf("/");
2276
+ if (i < 0) { splitDir = ""; splitFile = s; }
2277
+ else { splitDir = s.slice(0, i + 1); splitFile = s.slice(i + 1); }
2278
+ },
2279
+ "path_split_dir": () => splitDir,
2280
+ "path_split_file": () => splitFile,
2281
+ "path_join_init": () => { joinParts = []; },
2282
+ "path_join_push": (s) => { if (s) joinParts.push(s); },
2283
+ "path_join_run": () => {
2284
+ if (joinParts.length === 0) return "";
2285
+ const res = __pathClean(joinParts.join("/"));
2286
+ joinParts = [];
2287
+ return res;
2288
+ },
2289
+ "path_match": (pat, name) => {
2290
+ const r = __pathMatch(pat ?? "", name ?? "");
2291
+ matchHasErr = r[1] ? 1 : 0;
2292
+ return r[0] ? 1 : 0;
2293
+ },
2294
+ "path_match_has_err": () => matchHasErr,
2295
+ // bytes trim
2296
+ "bytes_trim": (s, cutset, left, right) => {
2297
+ const cs = new Set(Array.from(cutset ?? ""));
2298
+ const chars = Array.from(s ?? "");
2299
+ let a = 0, b = chars.length;
2300
+ if (left) while (a < b && cs.has(chars[a])) a++;
2301
+ if (right) while (b > a && cs.has(chars[b - 1])) b--;
2302
+ const enc = new TextEncoder();
2303
+ trimStart = a === 0 ? 0 : enc.encode(chars.slice(0, a).join("")).length;
2304
+ trimEnd = b === chars.length ? enc.encode(s ?? "").length : enc.encode(chars.slice(0, b).join("")).length;
2305
+ },
2306
+ "bytes_trim_start": () => trimStart,
2307
+ "bytes_trim_end": () => trimEnd,
2308
+ // time
2309
+ "time_now_ms": () => (typeof globalThis.__GOFRONT_CLOCK === "function" ? globalThis.__GOFRONT_CLOCK() : Date.now()),
2310
+ "time_timezone_offset": (ms) => -new Date(ms).getTimezoneOffset() * 60,
2311
+ "time_duration_string": (d) => __durationString(d),
2312
+ "time_parse": (layout, val) => {
2313
+ try {
2314
+ const d = new Date(val ?? "");
2315
+ if (isNaN(d.getTime())) throw 0;
2316
+ timeParseErr = null;
2317
+ timeParseOffset = -d.getTimezoneOffset() * 60;
2318
+ return d.getTime();
2319
+ } catch {
2320
+ timeParseErr = "parsing time: cannot parse";
2321
+ timeParseOffset = 0;
2322
+ return 0;
2323
+ }
2324
+ },
2325
+ "time_parse_err": () => timeParseErr,
2326
+ "time_parse_offset": () => timeParseOffset,
2327
+ "time_format": (y, m, d, hr, min, sec, wd, ms, offsetSec, layout) => {
2328
+ const DAYS = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
2329
+ const MONTHS = ["January","February","March","April","May","June","July","August","September","October","November","December"];
2330
+ const pad = (n, w = 2) => String(n).padStart(w, "0");
2331
+ return (layout ?? "").replace(/2006|06|Monday|Mon|January|Jan|01|02|15|PM|pm|03|04|05|Z07:00|\\.(?:0+|9+)|1|2|3|4|5/g, t => {
2332
+ if (t.startsWith(".")) {
2333
+ const isZeroPad = t.includes("0");
2334
+ const maxLen = t.length - 1;
2335
+ let frac = pad(ms % 1000, 3);
2336
+ if (maxLen > 3) frac = frac.padEnd(maxLen, "0");
2337
+ else frac = frac.slice(0, maxLen);
2338
+ if (!isZeroPad) {
2339
+ frac = frac.replace(/0+$/, "");
2340
+ return frac.length > 0 ? "." + frac : "";
2341
+ }
2342
+ return "." + frac;
2343
+ }
2344
+ switch (t) {
2345
+ case "2006": return String(y);
2346
+ case "06": return String(y).slice(-2);
2347
+ case "Monday": return DAYS[wd];
2348
+ case "Mon": return DAYS[wd].slice(0, 3);
2349
+ case "January": return MONTHS[m - 1];
2350
+ case "Jan": return MONTHS[m - 1].slice(0, 3);
2351
+ case "01": return pad(m);
2352
+ case "1": return String(m);
2353
+ case "02": return pad(d);
2354
+ case "2": return String(d);
2355
+ case "15": return pad(hr);
2356
+ case "PM": return hr < 12 ? "AM" : "PM";
2357
+ case "pm": return hr < 12 ? "am" : "pm";
2358
+ case "03": return pad(hr % 12 || 12);
2359
+ case "3": return String(hr % 12 || 12);
2360
+ case "04": return pad(min);
2361
+ case "4": return String(min);
2362
+ case "05": return pad(sec);
2363
+ case "5": return String(sec);
2364
+ case "Z07:00": {
2365
+ const o = Math.floor(offsetSec / 60);
2366
+ return o === 0 ? "Z" : (o > 0 ? "+" : "-") + pad(Math.floor(Math.abs(o) / 60)) + ":" + pad(Math.abs(o) % 60);
2367
+ }
2368
+ default: return t;
2369
+ }
2370
+ });
2371
+ },
2372
+ "raw_bytes_init": (len) => {
2373
+ rawBytes = new Uint8Array(len);
2374
+ rawPos = 0;
2375
+ },
2376
+ "raw_bytes_push": (b) => {
2377
+ if (rawPos < rawBytes.length) {
2378
+ rawBytes[rawPos++] = b & 0xff;
2379
+ }
2380
+ },
2381
+ "raw_bytes_len": () => rawBytes.length,
2382
+ "raw_bytes_get": (i) => rawBytes[i] ?? 0,
2383
+ "crypto_rand_fill": (len) => {
2384
+ rawBytes = new Uint8Array(len);
2385
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
2386
+ const CHUNK = 65536;
2387
+ for (let offset = 0; offset < len; offset += CHUNK) {
2388
+ crypto.getRandomValues(rawBytes.subarray(offset, Math.min(offset + CHUNK, len)));
2389
+ }
2390
+ } else {
2391
+ for (let i = 0; i < len; i++) rawBytes[i] = Math.floor(Math.random() * 256);
2392
+ }
2393
+ return len;
2394
+ },
2395
+ "crypto_sha256_sum": () => {
2396
+ const d = __sha256.Sum256(rawBytes);
2397
+ rawBytes = new Uint8Array(d);
2398
+ return 32;
2399
+ },
2400
+ "hex_encode_to_string": () => {
2401
+ return __hex_encode_to_string(rawBytes);
2402
+ },
2403
+ "hex_decode_string": (s) => {
2404
+ const [res, err] = __hex_decode_string(s ?? "");
2405
+ hexErr = err ? (err.message ?? String(err)) : null;
2406
+ rawBytes = new Uint8Array(res);
2407
+ return rawBytes.length;
2408
+ },
2409
+ "hex_get_err": () => hexErr,
2410
+ "base64_new_encoding": (alphabet) => {
2411
+ const id = customBase64Encodings.length + 4;
2412
+ customBase64Encodings.push(__base64_make(alphabet ?? "", "="));
2413
+ return id;
2414
+ },
2415
+ "base64_encode_to_string": (mode) => {
2416
+ const enc = mode === 1 ? __base64_url : mode === 2 ? __base64_raw_std : mode === 3 ? __base64_raw_url : mode >= 4 ? customBase64Encodings[mode - 4] : __base64_std;
2417
+ return (enc ?? __base64_std).EncodeToString(rawBytes);
2418
+ },
2419
+ "base64_decode_string": (mode, s) => {
2420
+ const enc = mode === 1 ? __base64_url : mode === 2 ? __base64_raw_std : mode === 3 ? __base64_raw_url : mode >= 4 ? customBase64Encodings[mode - 4] : __base64_std;
2421
+ const [res, err] = (enc ?? __base64_std).DecodeString(s ?? "");
2422
+ base64Err = err ? (err.message ?? String(err)) : null;
2423
+ rawBytes = new Uint8Array(res);
2424
+ return rawBytes.length;
2425
+ },
2426
+ "base64_get_err": () => base64Err,
2427
+ };
2428
+ }
2429
+ function ${P}trim(s, cutset, left, right) {
2430
+ const cs = new Set(Array.from(cutset));
2431
+ const chars = Array.from(s);
2432
+ let a = 0, b = chars.length;
2433
+ if (left) while (a < b && cs.has(chars[a])) a++;
2434
+ if (right) while (b > a && cs.has(chars[b - 1])) b--;
2435
+ return chars.slice(a, b).join("");
1034
2436
  }`;
1035
2437
 
1036
2438
  // Runtime loader emitted at the top of the facade.
1037
- export const WASM_LOADER_JS = `${WASM_IMPORTS_JS}
2439
+ const wasmLoaderJs = (stdlibEnv) => `${WASM_IMPORTS_JS}
2440
+ ${stdlibEnv ? WASM_STDLIB_JS : ""}
1038
2441
  async function ${P}fetch(url) {
1039
2442
  const res = await fetch(url);
1040
2443
  if (!res.ok) throw new Error("GoFront: failed to fetch " + url + " (" + res.status + ")");
@@ -1043,7 +2446,8 @@ async function ${P}fetch(url) {
1043
2446
  async function ${P}instantiate(imports) {
1044
2447
  const bytes = globalThis.__GOFRONT_WASM_BYTES;
1045
2448
  if (bytes) return WebAssembly.instantiate(bytes, imports);
1046
- const url = globalThis.__GOFRONT_WASM_URL ?? "app.wasm";
2449
+ // Resolve relative to the bundle so the app works from any page path.
2450
+ const url = globalThis.__GOFRONT_WASM_URL ?? new URL("app.wasm", import.meta.url).href;
1047
2451
  if (typeof WebAssembly.instantiateStreaming === "function") {
1048
2452
  try { return await WebAssembly.instantiateStreaming(fetch(url), imports); }
1049
2453
  catch { /* fall through: wrong MIME type or no streaming support */ }
@@ -1060,6 +2464,7 @@ async function ${P}load(stringTable, extraEnv) {
1060
2464
  // start function while package-level initializers run), so exports are
1061
2465
  // returned raw: no try/catch wrapper, which keeps JS→wasm calls inlinable.
1062
2466
  const result = await ${P}instantiate(imports);
2467
+ imports.__bind(result.instance.exports);
1063
2468
  return result.instance.exports;
1064
2469
  }
1065
2470
  // Go int/int64 cross the boundary as f64 (exact within the safe-integer range).
@@ -1074,6 +2479,11 @@ const ${P}u64out = (v) => {
1074
2479
  };
1075
2480
  const ${P}strin = (v) => (v == null ? "" : String(v));
1076
2481
  const ${P}href = (h) => (h == null ? null : h.__ref);
2482
+ // Tags a struct pointer held in an interface (same marker as the JS backend's __ifp).
2483
+ const ${P}ptag = (h) => {
2484
+ if (h !== null && h.__p !== true && Object.isExtensible(h)) Object.defineProperty(h, "__p", { "value": true, "configurable": true });
2485
+ return h;
2486
+ };
1077
2487
  const ${P}NIL_DEREF_PATTERNS = [
1078
2488
  "dereferencing a null pointer", // V8
1079
2489
  "dereferencing null pointer", // SpiderMonkey
@@ -1110,16 +2520,19 @@ function ${P}idxview(n, getAt, setAt) {
1110
2520
  // Generates the JS facade for the linked wasm module.
1111
2521
  export function generateFacade(
1112
2522
  meta,
1113
- { stringTable = [], callMain = false } = {},
2523
+ { stringTable = [], callMain = false, stdlibEnv = true } = {},
1114
2524
  ) {
1115
2525
  const needs = meta._needs ?? collectNeeds(meta);
1116
- const out = [WASM_LOADER_JS];
2526
+ const out = [wasmLoaderJs(stdlibEnv)];
1117
2527
 
1118
2528
  // JS callback imports (registered before instantiation; bodies run after).
1119
2529
  out.push(`const ${P}env = {};`);
1120
2530
  for (const [key, desc] of needs.funcs) {
1121
2531
  const names = desc.params.map((_, i) => `a${i}`);
1122
- const call = `fn(${desc.params.map((d, i) => outExpr(d, names[i])).join(", ")})`;
2532
+ const args = desc.params.map((d, i) =>
2533
+ isPtrBoth(d) ? `${P}view_${d.name}(${names[i]})` : outExpr(d, names[i]),
2534
+ );
2535
+ const call = `fn(${args.join(", ")})`;
1123
2536
  let body;
1124
2537
  if (desc.returns.length === 0) body = `${call};`;
1125
2538
  else if (desc.returns.length === 1)
@@ -1129,7 +2542,9 @@ export function generateFacade(
1129
2542
  .map((d, i) => inExpr(d, `__r[${i}]`))
1130
2543
  .join(", ")}];`;
1131
2544
  const sig = ["fn", ...names].join(", ");
1132
- out.push(`${P}env["__invoke$${key}"] = (${sig}) => { ${body} };`);
2545
+ out.push(
2546
+ `${P}env["__invoke$${key}"] = (${sig}) => { try { ${body} } catch (__e) { if (__w && __w.__push_panic) __w.__push_panic(__e.message ?? String(__e)); throw __e; } };`,
2547
+ );
1133
2548
  }
1134
2549
  out.push(
1135
2550
  `const __w = await ${P}load(${JSON.stringify(stringTable)}, ${P}env);`,
@@ -1143,6 +2558,39 @@ export function generateFacade(
1143
2558
  }
1144
2559
  }
1145
2560
 
2561
+ for (const [name, desc] of needs.anonStructs) {
2562
+ out.push(`function ${P}to_${name}(o) {
2563
+ if (o == null) return __w.__zero_${name}();
2564
+ return __w.__new_${name}(${desc.fields.map((f) => inExpr(f.desc, `(o.${f.name} ?? ${zeroExpr(f.desc)})`)).join(", ")});
2565
+ }
2566
+ function ${P}from_${name}(r) {
2567
+ if (r == null) return null;
2568
+ return {
2569
+ ${desc.fields.map((f) => `\t\t"${f.name}": ${outExpr(f.desc, `__w.__get_${name}_${f.name}(r)`)}`).join(",\n")}
2570
+ };
2571
+ }
2572
+ function ${P}back_${name}(r, o) {
2573
+ if (r == null || o == null) return;
2574
+ ${desc.fields.map((f) => `\to.${f.name} = ${outExpr(f.desc, `__w.__get_${name}_${f.name}(r)`)};`).join("\n")}
2575
+ }`);
2576
+ }
2577
+
2578
+ for (const [key, desc] of needs.boxes) {
2579
+ out.push(`function ${P}boxin_${key}(b) {
2580
+ if (b == null) return null;
2581
+ const v = b.value !== undefined ? b.value : ${zeroExpr(desc.base)};
2582
+ return __w.__box_new_${key}(${inExpr(desc.base, "v")});
2583
+ }
2584
+ function ${P}boxout_${key}(r) {
2585
+ if (r == null) return null;
2586
+ return { "value": ${outExpr(desc.base, `__w.__box_get_${key}(r)`)} };
2587
+ }
2588
+ function ${P}boxback_${key}(r, b) {
2589
+ if (r == null || b == null) return;
2590
+ b.value = ${outExpr(desc.base, `__w.__box_get_${key}(r)`)};
2591
+ }`);
2592
+ }
2593
+
1146
2594
  for (const [key, desc] of needs.slices) {
1147
2595
  out.push(`function ${P}slin_${key}(arr) {
1148
2596
  if (arr == null) return __w.__slice_nil_${key}();
@@ -1161,8 +2609,34 @@ function ${P}slout_${key}(s) {
1161
2609
  function ${P}slview_${key}(s) {
1162
2610
  if (s == null || __w.__slice_is_nil_${key}(s)) return null;
1163
2611
  return ${P}idxview(__w.__slice_len_${key}(s), (i) => ${viewExpr(desc.elem, `__w.__slice_get_${key}(s, i)`)}, (i, v) => __w.__slice_set_${key}(s, i, ${inExpr(desc.elem, "v")}));
2612
+ } `);
2613
+ }
2614
+
2615
+ for (const [key, desc] of needs.maps) {
2616
+ out.push(`function ${P}mapin_${key}(m) {
2617
+ if (m == null) return null;
2618
+ const entries = m instanceof Map ? m.entries() : Object.entries(m);
2619
+ const size = m instanceof Map ? m.size : Object.keys(m).length;
2620
+ const r = __w.__map_new_${key}(size);
2621
+ for (const [k, v] of entries) {
2622
+ __w.__map_set_${key}(r, ${desc.keyDesc.k === "bool" ? `(k === "true" || k === true ? 1 : 0)` : inExpr(desc.keyDesc, "k")}, ${inExpr(desc.valDesc, "v")});
2623
+ }
2624
+ return r;
2625
+ }
2626
+ function ${P}mapout_${key}(m) {
2627
+ if (m == null) return null;
2628
+ const ks = ${P}slout_${desc.keySliceDesc.key}(__w.__map_keys_${key}(m));
2629
+ const vs = ${P}slout_${desc.valSliceDesc.key}(__w.__map_values_${key}(m));
2630
+ const out = new Map();
2631
+ if (ks) {
2632
+ for (let i = 0; i < ks.length; i++) {
2633
+ out.set(ks[i], vs[i]);
2634
+ }
2635
+ }
2636
+ return out;
1164
2637
  }`);
1165
2638
  }
2639
+
1166
2640
  for (const [key, desc] of needs.arrays) {
1167
2641
  out.push(`function ${P}arrzero_${key}(n) {
1168
2642
  const out = new Array(n);
@@ -1186,7 +2660,7 @@ function ${P}arrout_${key}(a) {
1186
2660
  function ${P}arrview_${key}(a) {
1187
2661
  if (a == null) return null;
1188
2662
  return ${P}idxview(__w.__array_len_${key}(a), (i) => ${viewExpr(desc.elem, `__w.__array_get_${key}(a, i)`)}, (i, v) => __w.__array_set_${key}(a, i, ${inExpr(desc.elem, "v")}));
1189
- }`);
2663
+ } `);
1190
2664
  }
1191
2665
 
1192
2666
  for (const [key, desc] of needs.funcs) {
@@ -1196,22 +2670,122 @@ function ${P}arrview_${key}(a) {
1196
2670
  desc.returns,
1197
2671
  ["c"],
1198
2672
  );
1199
- out.push(`const ${P}fnmap_${key} = new WeakMap();
2673
+ out.push(`const ${P}fn_j2w_${key} = new WeakMap();
2674
+ const ${P}fn_w2j_${key} = new WeakMap();
1200
2675
  function ${P}fnin_${key}(fn) {
1201
2676
  if (fn == null) return null;
1202
- let c = ${P}fnmap_${key}.get(fn);
1203
- if (!c) { c = __w.__wrap_fn$${key}(fn); ${P}fnmap_${key}.set(fn, c); }
2677
+ let c = ${P}fn_j2w_${key}.get(fn);
2678
+ if (!c) {
2679
+ c = __w.__wrap_fn$${key}(fn);
2680
+ ${P}fn_j2w_${key}.set(fn, c);
2681
+ ${P}fn_w2j_${key}.set(c, fn);
2682
+ }
1204
2683
  return c;
1205
2684
  }
1206
2685
  function ${P}fnout_${key}(c) {
1207
2686
  if (c == null) return null;
1208
- return (${names.join(", ")}) => {
2687
+ let fn = ${P}fn_w2j_${key}.get(c);
2688
+ if (!fn) {
2689
+ fn = (${names.join(", ")}) => {
1209
2690
  ${indent(body, "\t")}
1210
- };
2691
+ };
2692
+ fn.__ref = c;
2693
+ ${P}fn_w2j_${key}.set(c, fn);
2694
+ ${P}fn_j2w_${key}.set(fn, c);
2695
+ }
2696
+ return fn;
2697
+ }`);
2698
+ }
2699
+
2700
+ for (const [key, desc] of needs.ifaces) genIfaceFacade(out, key, desc);
2701
+
2702
+ if (needs.error) {
2703
+ out.push(`class ${P}Error extends Error {
2704
+ constructor(msg, ref) {
2705
+ super(msg);
2706
+ this.name = "GoError";
2707
+ this._msg = msg;
2708
+ this.__ref = ref;
2709
+ }
2710
+ Error() {
2711
+ return this.message;
2712
+ }
2713
+ }
2714
+ const ${P}err_w2j = new WeakMap();
2715
+ function ${P}errout(r) {
2716
+ if (r == null) return null;
2717
+ let e = ${P}err_w2j.get(r);
2718
+ if (!e) {
2719
+ const msg = __w.__error_msg ? (__w.__error_msg(r) ?? "") : "";
2720
+ e = new ${P}Error(msg, r);
2721
+ ${P}err_w2j.set(r, e);
2722
+ }
2723
+ return e;
2724
+ }
2725
+ function ${P}errin(e) {
2726
+ if (e == null) return null;
2727
+ if (e.__ref === undefined) throw new TypeError("GoFront: expected a WASM-owned error");
2728
+ if (!__w.__is_error(e.__ref)) throw new TypeError("GoFront: value does not implement error");
2729
+ return e.__ref;
2730
+ }`);
2731
+ }
2732
+
2733
+ if (needs.time) {
2734
+ out.push(`function ${P}timeout(r) {
2735
+ if (r == null) return null;
2736
+ const ms = __w.__time_unix_milli ? __w.__time_unix_milli(r) : 0;
2737
+ const off = __w.__time_offset ? __w.__time_offset(r) : 0;
2738
+ return { _d: new Date(ms), _offset: off };
2739
+ }
2740
+ function ${P}timeout_ptr(r) {
2741
+ return ${P}timeout(r);
2742
+ }
2743
+ function ${P}timein(v) {
2744
+ if (v == null) return null;
2745
+ const d = v._d ? v._d : (v instanceof Date ? v : new Date(v));
2746
+ const ms = d.getTime();
2747
+ const off = typeof v?._offset === "number" ? v._offset : -d.getTimezoneOffset() * 60;
2748
+ return __w.__time_make ? __w.__time_make(ms, off) : null;
2749
+ }
2750
+ function ${P}timein_ptr(v) {
2751
+ return ${P}timein(v);
2752
+ }`);
2753
+ }
2754
+
2755
+ if (needs.shared) {
2756
+ // Zero-copy TypedArray views over the exported memory. Memory never
2757
+ // grows after the start function (allocation is startup-only), but the
2758
+ // cached view is still re-validated against the live buffer.
2759
+ out.push(`const ${P}shc = new WeakMap();
2760
+ function ${P}shout(r, Ctor) {
2761
+ if (r == null) return null;
2762
+ let v = ${P}shc.get(r);
2763
+ if (!v || v.buffer !== __w.memory.buffer) {
2764
+ v = new Ctor(__w.memory.buffer, __w.__shared_base(r), __w.__shared_len(r));
2765
+ ${P}shc.set(r, v);
2766
+ }
2767
+ return v;
2768
+ }
2769
+ function ${P}shin(v, Ctor) {
2770
+ if (v == null) return null;
2771
+ if (!(v instanceof Ctor) || v.buffer !== __w.memory.buffer) throw new TypeError("GoFront: expected a " + Ctor.name + " view over the wasm shared memory");
2772
+ return __w.__shared_make(v.byteOffset, v.length);
1211
2773
  }`);
1212
2774
  }
1213
2775
 
1214
- for (const c of meta.consts) out.push(`const ${c.name} = ${c.value};`);
2776
+ for (const c of meta.consts) {
2777
+ if (c.dynamic) {
2778
+ out.push(
2779
+ `const ${c.name} = ${outExpr(c.desc, `__w.__const_${c.name}()`)};`,
2780
+ );
2781
+ } else {
2782
+ out.push(`const ${c.name} = ${c.value};`);
2783
+ }
2784
+ }
2785
+ for (const v of meta.vars ?? [])
2786
+ out.push(
2787
+ `const ${v.name} = ${P}shout(__w.__var_${v.name}(), ${v.desc.ctor});`,
2788
+ );
1215
2789
 
1216
2790
  for (const f of meta.funcs) {
1217
2791
  const { names, body } = genFuncBody(
@@ -1298,21 +2872,66 @@ ${backLines.join("\n")}
1298
2872
  }`);
1299
2873
  }
1300
2874
 
2875
+ function genIfaceFacade(out, key, desc) {
2876
+ const methods = desc.methods.map((m) => {
2877
+ const { names, body } = genFuncBody(m.callName, m.params, m.returns, [
2878
+ "this.__ref",
2879
+ ]);
2880
+ return `\t${m.name}(${names.join(", ")}) {\n${indent(body, "\t")}\n\t}`;
2881
+ });
2882
+ const cases = desc.concrete.map((c, i) => {
2883
+ const wrap = c.both ? `${P}view_${c.name}(r)` : `${c.name}.__wrap(r)`;
2884
+ return `\t\tcase ${i + 1}: return ${P}ptag(${wrap});`;
2885
+ });
2886
+ const tag =
2887
+ cases.length > 0
2888
+ ? `\tswitch (__w.__itag$${key}(r)) {\n${cases.join("\n")}\n\t}\n`
2889
+ : "";
2890
+ const msg = `GoFront: a JS-implemented value cannot cross into wasm as interface '${desc.name}'; implement it in a wasm package or pass a func callback`;
2891
+ const implMsg = `GoFront: wasm value does not implement interface '${desc.name}'`;
2892
+ out.push(`class ${P}I_${key} {
2893
+ constructor(r) { this.__ref = r; }
2894
+ ${methods.join("\n")}
2895
+ }
2896
+ const ${P}ic_${key} = new WeakMap();
2897
+ function ${P}ifout_${key}(r) {
2898
+ if (r == null) return null;
2899
+ ${tag} let o = ${P}ic_${key}.get(r);
2900
+ if (!o) { o = new ${P}I_${key}(r); ${P}ic_${key}.set(r, o); }
2901
+ return o;
2902
+ }
2903
+ function ${P}ifin_${key}(v) {
2904
+ if (v == null) return null;
2905
+ if (v.__ref === undefined) throw new TypeError(${JSON.stringify(msg)});
2906
+ if (!__w.__impl$${key}(v.__ref)) throw new TypeError(${JSON.stringify(implMsg)});
2907
+ return v.__ref;
2908
+ }`);
2909
+ }
2910
+
1301
2911
  function genHandleStruct(out, name, fields, methods) {
1302
2912
  // `$`-suffixed params keep the mangler from renaming the matching getters.
1303
2913
  const ctorParams = fields.map((f) => `${f.name}$ = ${zeroExpr(f.desc)}`);
1304
2914
  const ctorArgs = fields.map((f) => inExpr(f.desc, `${f.name}$`));
2915
+ const fromArgs = fields.map((f) => `o.${f.name}`);
2916
+ const ptrMsg = `GoFront: expected a ${name} handle for a *${name} parameter (use ${name}.from(obj) to create one)`;
1305
2917
  const lines = [
1306
2918
  `const ${P}h_${name} = new WeakMap();
1307
2919
  function ${P}hval_${name}(h) {
1308
2920
  if (h == null) return __w.__zero_${name}();
1309
- return __w.__clone_${name}(h.__ref);
2921
+ if (h.__ref !== undefined) return __w.__clone_${name}(h.__ref);
2922
+ return ${name}.from(h).__ref;
2923
+ }
2924
+ function ${P}href_${name}(h) {
2925
+ if (h == null) return null;
2926
+ if (h.__ref !== undefined) return h.__ref;
2927
+ throw new TypeError(${JSON.stringify(ptrMsg)});
1310
2928
  }
1311
2929
  class ${name} {
1312
2930
  constructor(${ctorParams.join(", ")}) {
1313
2931
  this.__ref = __w.__new_${name}(${ctorArgs.join(", ")});
1314
2932
  ${P}h_${name}.set(this.__ref, this);
1315
2933
  }
2934
+ static from(o) { return new ${name}(${fromArgs.join(", ")}); }
1316
2935
  static __wrap(ref) {
1317
2936
  if (ref == null) return null;
1318
2937
  let h = ${P}h_${name}.get(ref);