gofront 1.5.1 → 1.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 (60) hide show
  1. package/README.md +174 -466
  2. package/package.json +16 -20
  3. package/src/backend/js/expressions.js +50 -19
  4. package/src/backend/js/index.js +100 -55
  5. package/src/backend/js/runtime.js +102 -0
  6. package/src/backend/js/source-map.js +32 -0
  7. package/src/backend/js/statements.js +96 -16
  8. package/src/backend/js/stdlib/errors.js +2 -2
  9. package/src/backend/js/stdlib/fmt.js +26 -8
  10. package/src/backend/js/stdlib/index.js +3 -0
  11. package/src/backend/js/stdlib/maps.js +1 -1
  12. package/src/backend/js/stdlib/path.js +2 -2
  13. package/src/backend/js/stdlib/shared.js +17 -0
  14. package/src/backend/js/stdlib/slices.js +1 -1
  15. package/src/backend/js/stdlib/sort.js +1 -1
  16. package/src/backend/js/stdlib/strconv.js +34 -9
  17. package/src/backend/js/stdlib/strings.js +4 -1
  18. package/src/backend/js/stdlib/testing.js +2 -2
  19. package/src/backend/js/stdlib/time.js +3 -3
  20. package/src/backend/js/templ.js +1 -1
  21. package/src/backend/wasm/boundary.js +486 -33
  22. package/src/backend/wasm/emit-builtins.js +1103 -0
  23. package/src/backend/wasm/emit-exprs.js +1908 -0
  24. package/src/backend/wasm/emit-maps.js +1863 -0
  25. package/src/backend/wasm/emit-shared.js +519 -0
  26. package/src/backend/wasm/emit-stdlib.js +3006 -0
  27. package/src/backend/wasm/emit-stmts.js +2472 -0
  28. package/src/backend/wasm/emit.js +606 -5009
  29. package/src/backend/wasm/encode.js +212 -64
  30. package/src/backend/wasm/glue.js +19 -5
  31. package/src/backend/wasm/index.js +1426 -49
  32. package/src/backend/wasm/monomorph.js +711 -0
  33. package/src/backend/wasm/optimize.js +227 -0
  34. package/src/backend/wasm/types.js +159 -35
  35. package/src/backend/wasm/wat.js +49 -2
  36. package/src/cli-core.js +275 -72
  37. package/src/compiler.js +144 -21
  38. package/src/index.js +86 -115
  39. package/src/lower/boxing.js +3 -4
  40. package/src/lower/captures.js +10 -17
  41. package/src/lower/escape.js +4 -9
  42. package/src/lower/functions.js +277 -5
  43. package/src/lower/globals.js +7 -15
  44. package/src/lower/index.js +5 -2
  45. package/src/lower/ownership.js +5 -10
  46. package/src/lower/range.js +2 -5
  47. package/src/lower/walk.js +25 -0
  48. package/src/resolver.js +2 -1
  49. package/src/test-runner.js +20 -0
  50. package/src/typechecker/assignability.js +42 -1
  51. package/src/typechecker/expressions.js +142 -2
  52. package/src/typechecker/index.js +27 -1
  53. package/src/typechecker/resolve.js +35 -6
  54. package/src/typechecker/statements.js +0 -7
  55. package/src/typechecker/stdlib/core.js +17 -19
  56. package/src/typechecker/stdlib/extended.js +18 -3
  57. package/src/typechecker/stdlib/shared.js +53 -0
  58. package/src/typechecker/stdlib.js +2 -0
  59. package/src/typechecker/types.js +206 -7
  60. package/src/vendor.js +2 -2
@@ -20,12 +20,22 @@
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 { HELPER_SPRINTF, HELPER_STRCONV } from "../js/runtime.js";
38
+ import { getSharedInfo, isTestingT } from "./types.js";
29
39
 
30
40
  // ── Type classification ──────────────────────────────────────
31
41
 
@@ -79,7 +89,7 @@ function funcSigFromAst(t) {
79
89
 
80
90
  // Classifies a Go type (AST node or checker type) for boundary marshalling.
81
91
  // Returns a descriptor `{ k, ... }`; `k === "unsupported"` carries `what`.
82
- export function classifyType(goType, mod) {
92
+ function classifyType(goType, mod) {
83
93
  if (!goType) return { k: "void" };
84
94
  if (isTestingT(goType)) return { k: "extern", wType: "externref" };
85
95
 
@@ -95,6 +105,8 @@ export function classifyType(goType, mod) {
95
105
  switch (t.kind) {
96
106
  case "named":
97
107
  return classifyNamed(t, mod);
108
+ case "shared":
109
+ return classifyShared(t, mod);
98
110
  case "untyped":
99
111
  return classifyBasic(t.base);
100
112
  case "basic":
@@ -108,7 +120,7 @@ export function classifyType(goType, mod) {
108
120
  case "func":
109
121
  return classifyFunc(t, mod);
110
122
  case "interface":
111
- return classifyInterface(t);
123
+ return classifyInterface(t, mod);
112
124
  default:
113
125
  return {
114
126
  k: "unsupported",
@@ -183,6 +195,17 @@ function classifyPointer(t, mod) {
183
195
  return { k: "unsupported", what: `pointer to ${what}` };
184
196
  }
185
197
 
198
+ function classifyShared(t, mod) {
199
+ const info = getSharedInfo(t, mod.checker);
200
+ return {
201
+ k: "shared",
202
+ key: info.key,
203
+ ctor: info.ctor,
204
+ bytes: info.bytes,
205
+ wType: mod.getSharedType().wType,
206
+ };
207
+ }
208
+
186
209
  function classifySlice(t, mod) {
187
210
  const elem = classifyType(t.elem, mod);
188
211
  if (elem.k === "unsupported") return elem;
@@ -216,31 +239,66 @@ function classifyArray(t, mod) {
216
239
  };
217
240
  }
218
241
 
219
- function classifyFunc(t, mod) {
242
+ function classifySig(t, mod) {
220
243
  const params = (t.params ?? []).map((p) => classifyType(p, mod));
221
244
  const returns = (t.returns ?? [])
222
245
  .map((r) => classifyType(r, mod))
223
246
  .filter((d) => d.k !== "void");
224
247
  const bad = [...params, ...returns].find((d) => d.k === "unsupported");
225
- if (bad) return bad;
248
+ return bad ?? { params, returns };
249
+ }
250
+
251
+ function classifyFunc(t, mod) {
252
+ const sig = classifySig(t, mod);
253
+ if (sig.k === "unsupported") return sig;
226
254
  const closureInfo = mod.getClosureType(t);
227
255
  return {
228
256
  k: "func",
229
- params,
230
- returns,
257
+ params: sig.params,
258
+ returns: sig.returns,
231
259
  closureInfo,
232
260
  key: `c${closureInfo.typeIndex}`,
233
261
  wType: { kind: "ref", nullable: true, typeIndex: closureInfo.typeIndex },
234
262
  };
235
263
  }
236
264
 
237
- function classifyInterface(t) {
265
+ function classifyInterface(t, mod) {
238
266
  if (t.name === "error") return { k: "unsupported", what: "error" };
239
267
  if (!t.methods || t.methods.size === 0) return { k: "any", wType: "anyref" };
240
- return {
241
- k: "unsupported",
242
- what: `interface${t.name ? ` '${t.name}'` : ""}`,
268
+ if (!t.name || !(t.methods instanceof Map))
269
+ return { k: "unsupported", what: "anonymous interface" };
270
+ mod._boundaryIfaces ??= new Map();
271
+ return mod._boundaryIfaces.get(t.name) ?? classifyNamedInterface(t, mod);
272
+ }
273
+
274
+ function classifyNamedInterface(t, mod) {
275
+ const desc = {
276
+ k: "iface",
277
+ name: t.name,
278
+ key: t.name,
279
+ wType: "anyref",
280
+ methods: [],
281
+ concrete: [],
243
282
  };
283
+ // Cached before the walk so self-referencing method signatures terminate.
284
+ mod._boundaryIfaces.set(t.name, desc);
285
+ for (const [mName, sigType] of t.methods) {
286
+ const sig = classifySig(sigType, mod);
287
+ if (sig.k === "unsupported") {
288
+ const err = {
289
+ k: "unsupported",
290
+ what: `${sig.what} (in method ${t.name}.${mName})`,
291
+ };
292
+ mod._boundaryIfaces.set(t.name, err);
293
+ return err;
294
+ }
295
+ desc.methods.push({
296
+ name: mName,
297
+ dispatch: `__dispatch_${t.name}_${mName}`,
298
+ ...sig,
299
+ });
300
+ }
301
+ return desc;
244
302
  }
245
303
 
246
304
  // ── Export metadata ──────────────────────────────────────────
@@ -291,6 +349,7 @@ export function collectBoundaryMeta(progs, funcDecls, mod) {
291
349
  structs: [],
292
350
  funcs: [],
293
351
  consts: [],
352
+ vars: [], // exported package-level shared buffers of wasm packages
294
353
  structByName: new Map(),
295
354
  _mod: mod,
296
355
  };
@@ -304,6 +363,8 @@ export function collectBoundaryMeta(progs, funcDecls, mod) {
304
363
  collectStructDecl(d, pkgName, pkgTarget, meta, mod);
305
364
  } else if (d.kind === "ConstDecl" && pkgTarget === "wasm") {
306
365
  collectConstDecl(d, pkgName, meta, errors);
366
+ } else if (d.kind === "VarDecl" && pkgTarget === "wasm") {
367
+ collectSharedVarDecl(d, meta, mod);
307
368
  }
308
369
  }
309
370
  }
@@ -350,6 +411,22 @@ function checkDesc(desc, where, errors) {
350
411
  return desc;
351
412
  }
352
413
 
414
+ // Exported `var Buf = shared.NewFloat32(n)` globals are exposed to JS as
415
+ // TypedArray views (read once after instantiation; sizing is fixed).
416
+ function collectSharedVarDecl(d, meta, mod) {
417
+ for (const spec of d.decls ?? d.specs ?? [d]) {
418
+ const names = spec.names ?? (spec.name ? [spec.name] : []);
419
+ const values = spec.value ?? (spec.init ? [spec.init] : []);
420
+ for (let i = 0; i < names.length; i++) {
421
+ const name = names[i];
422
+ if (!isExported(name)) continue;
423
+ const goType = spec.type ?? values[i]?._type;
424
+ if (!getSharedInfo(goType, mod.checker)) continue;
425
+ meta.vars.push({ name, desc: classifyType(goType, mod) });
426
+ }
427
+ }
428
+ }
429
+
353
430
  function collectMethodMeta(fn, meta, errors, mod) {
354
431
  if (!isExported(fn._methodName)) return;
355
432
  const s = meta.structByName.get(fn._recvTypeName);
@@ -358,6 +435,7 @@ function collectMethodMeta(fn, meta, errors, mod) {
358
435
  s.methods.push({
359
436
  name: fn._methodName,
360
437
  exportName: fn._exportName,
438
+ fnName: fn.name,
361
439
  ptrRecv: fn.params[0].type?.kind === "pointer",
362
440
  params: fn.params
363
441
  .slice(1)
@@ -418,11 +496,37 @@ function visitNeed(desc, needs, errors, mod) {
418
496
  needs.funcs.set(desc.key, desc);
419
497
  visitSignatureNeeds(desc, needs, errors, mod);
420
498
  return;
499
+ case "iface":
500
+ if (needs.ifaces.has(desc.key)) return;
501
+ needs.ifaces.set(desc.key, desc);
502
+ for (const m of desc.methods) visitSignatureNeeds(m, needs, errors, mod);
503
+ return;
504
+ case "shared":
505
+ needs.shared = true;
506
+ return;
421
507
  default:
422
508
  return;
423
509
  }
424
510
  }
425
511
 
512
+ // Exported `*T` types (already on the boundary surface) whose JS class exposes
513
+ // every interface method: such values reach JS as the handle / live view
514
+ // itself, so type switches and identity still work. `__itag` matches by
515
+ // `ref.test`, so a struct is only chosen for values that really hold it.
516
+ function concreteCandidates(desc, needs, meta, mod) {
517
+ const out = [];
518
+ for (const [name, { info }] of needs.structs) {
519
+ const both = info.pkgTarget === "both";
520
+ const names = both
521
+ ? [...(mod.checker?.types?.get(name)?.underlying?.methods?.keys() ?? [])]
522
+ : (meta.structByName.get(name)?.methods ?? []).map((m) => m.name);
523
+ const have = new Set(names);
524
+ if (!desc.methods.every((m) => have.has(m.name))) continue;
525
+ out.push({ name, typeIndex: info.typeIndex, both });
526
+ }
527
+ return out;
528
+ }
529
+
426
530
  function visitSignatureNeeds(sig, needs, errors, mod) {
427
531
  for (const p of sig.params) visitNeed(p, needs, errors, mod);
428
532
  for (const r of sig.returns) visitNeed(r, needs, errors, mod);
@@ -437,6 +541,8 @@ function collectNeeds(meta) {
437
541
  slices: new Map(), // key -> desc
438
542
  arrays: new Map(), // key -> desc
439
543
  funcs: new Map(), // key -> desc
544
+ ifaces: new Map(), // interface name -> desc
545
+ shared: false, // any shared buffer on the surface
440
546
  };
441
547
  const errors = [];
442
548
  for (const s of meta.structs) {
@@ -445,7 +551,10 @@ function collectNeeds(meta) {
445
551
  for (const m of s.methods) visitSignatureNeeds(m, needs, errors, mod);
446
552
  }
447
553
  for (const f of meta.funcs) visitSignatureNeeds(f, needs, errors, mod);
554
+ for (const v of meta.vars ?? []) visitNeed(v.desc, needs, errors, mod);
448
555
  if (errors.length > 0) throw new Error([...new Set(errors)].join("\n"));
556
+ for (const desc of needs.ifaces.values())
557
+ desc.concrete = concreteCandidates(desc, needs, meta, mod);
449
558
  return needs;
450
559
  }
451
560
 
@@ -800,7 +909,8 @@ export function addBoundaryHelpers(mod, meta) {
800
909
  // get a trampoline (`__x_<export>`) with the boundary signature; the facade
801
910
  // calls that instead of the raw export.
802
911
  const addTrampoline = (entry) => {
803
- const fnIdx = mod.funcMap.get(entry.exportName);
912
+ // Methods are keyed `T.M` in funcMap but exported as `T_M`.
913
+ const fnIdx = mod.funcMap.get(entry.fnName ?? entry.exportName);
804
914
  if (fnIdx === undefined) return;
805
915
  const sig = typeAt(mod, mod.funcs[fnIdx - importFuncCount]?.typeIndex);
806
916
  if (sig?.form !== "func") return;
@@ -823,6 +933,101 @@ export function addBoundaryHelpers(mod, meta) {
823
933
  };
824
934
  for (const s of meta.structs) for (const m of s.methods) addTrampoline(m);
825
935
  for (const f of meta.funcs) addTrampoline(f);
936
+ for (const desc of needs.ifaces.values())
937
+ addIfaceHelpers(desc, mod, addFunc, addTrampoline);
938
+
939
+ if (needs.shared) {
940
+ const { wType, typeIndex } = mod.getSharedType();
941
+ addFunc(
942
+ "__shared_base",
943
+ [EXT],
944
+ ["i32"],
945
+ [...getIn(0, wType), { op: "struct.get", typeIndex, fieldIndex: 0 }],
946
+ );
947
+ addFunc(
948
+ "__shared_len",
949
+ [EXT],
950
+ ["i32"],
951
+ [...getIn(0, wType), { op: "struct.get", typeIndex, fieldIndex: 1 }],
952
+ );
953
+ // JS-side views (e.g. a `subarray`) go back in as a fresh (base, len).
954
+ addFunc(
955
+ "__shared_make",
956
+ ["i32", "i32"],
957
+ [EXT],
958
+ [get(0), get(1), { op: "struct.new", typeIndex }, ...toExt(wType)],
959
+ );
960
+ for (const v of meta.vars ?? []) {
961
+ const g = mod.resolveGlobal(v.name);
962
+ if (!g) continue;
963
+ addFunc(
964
+ `__var_${v.name}`,
965
+ [],
966
+ [EXT],
967
+ [{ op: "global.get", index: g.index }, ...toExt(wType)],
968
+ );
969
+ }
970
+ }
971
+ }
972
+
973
+ // `(externref) -> i32`: `ref.test`s param 0 against each type index and
974
+ // returns `resultFor(i)` for the first match, 0 otherwise.
975
+ function refTestChain(typeIndices, resultFor) {
976
+ const body = typeIndices.flatMap((ti, i) => [
977
+ { op: "local.get", index: 0 },
978
+ { op: "any.convert_extern" },
979
+ { op: "ref.test", typeIndex: ti },
980
+ { op: "if", blockType: "void" },
981
+ { op: "i32.const", value: resultFor(i) },
982
+ { op: "return" },
983
+ { op: "end" },
984
+ ]);
985
+ body.push({ op: "i32.const", value: 0 });
986
+ return body;
987
+ }
988
+
989
+ // Type indices (incl. unexported and boxed-value types) whose method set
990
+ // covers the interface, from the same candidate search the dispatchers use.
991
+ function implementingTypeIndices(desc, mod) {
992
+ let acc = null;
993
+ for (const m of desc.methods) {
994
+ const have = new Set(
995
+ mod.findInterfaceCandidates(null, m.name).map((c) => c.testTypeIndex),
996
+ );
997
+ acc = acc === null ? have : new Set([...acc].filter((t) => have.has(t)));
998
+ }
999
+ return [...(acc ?? [])];
1000
+ }
1001
+
1002
+ // Interfaces: method calls go through the existing `ref.test` dispatchers;
1003
+ // `__impl$<I>` tells whether a wasm value implements I (guards JS inputs);
1004
+ // `__itag$<I>` reports which exported `*T` (1-based) a value holds, 0 if none.
1005
+ function addIfaceHelpers(desc, mod, addFunc, addTrampoline) {
1006
+ for (const m of desc.methods) {
1007
+ const entry = { exportName: m.dispatch };
1008
+ addTrampoline(entry);
1009
+ if (!entry.callName)
1010
+ throw new Error(
1011
+ `internal: no wasm dispatcher for interface method ${desc.name}.${m.name}`,
1012
+ );
1013
+ m.callName = entry.callName;
1014
+ }
1015
+ addFunc(
1016
+ `__impl$${desc.key}`,
1017
+ ["externref"],
1018
+ ["i32"],
1019
+ refTestChain(implementingTypeIndices(desc, mod), () => 1),
1020
+ );
1021
+ if (desc.concrete.length === 0) return;
1022
+ addFunc(
1023
+ `__itag$${desc.key}`,
1024
+ ["externref"],
1025
+ ["i32"],
1026
+ refTestChain(
1027
+ desc.concrete.map((c) => c.typeIndex),
1028
+ (i) => i + 1,
1029
+ ),
1030
+ );
826
1031
  }
827
1032
 
828
1033
  // ── JS facade generation ─────────────────────────────────────
@@ -848,13 +1053,19 @@ function inExpr(desc, v) {
848
1053
  case "struct":
849
1054
  if (desc.info.pkgTarget === "both")
850
1055
  return `${P}to_${desc.name}(${v}, ${desc.ptr ? "false" : "true"})`;
851
- return desc.ptr ? `${P}href(${v})` : `${P}hval_${desc.name}(${v})`;
1056
+ return desc.ptr
1057
+ ? `${P}href_${desc.name}(${v})`
1058
+ : `${P}hval_${desc.name}(${v})`;
852
1059
  case "slice":
853
1060
  return `${P}slin_${desc.key}(${v})`;
854
1061
  case "array":
855
1062
  return `${P}arrin_${desc.key}(${v})`;
856
1063
  case "func":
857
1064
  return `${P}fnin_${desc.key}(${v})`;
1065
+ case "iface":
1066
+ return `${P}ifin_${desc.key}(${v})`;
1067
+ case "shared":
1068
+ return `${P}shin(${v}, ${desc.ctor})`;
858
1069
  default:
859
1070
  return v;
860
1071
  }
@@ -881,6 +1092,10 @@ function outExpr(desc, v) {
881
1092
  return `${P}arrout_${desc.key}(${v})`;
882
1093
  case "func":
883
1094
  return `${P}fnout_${desc.key}(${v})`;
1095
+ case "iface":
1096
+ return `${P}ifout_${desc.key}(${v})`;
1097
+ case "shared":
1098
+ return `${P}shout(${v}, ${desc.ctor})`;
884
1099
  default:
885
1100
  return v;
886
1101
  }
@@ -989,15 +1204,37 @@ function indent(text, prefix) {
989
1204
  // quoted, helpers prefixed with `__`.
990
1205
  export const WASM_IMPORTS_JS = `const ${P}MAX = 9007199254740991n;
991
1206
  function ${P}imports(stringTable, extraEnv, tag, write) {
992
- let buf = [];
1207
+ let lineBuf = "";
993
1208
  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(); };
1209
+ const late = { exports: null };
1210
+ const flush = () => { write(lineBuf); lineBuf = ""; };
1211
+ const print = (v) => {
1212
+ const s = String(v);
1213
+ if (lineBuf.length > 0 && !lineBuf.endsWith(" ") && !s.startsWith(" ")) {
1214
+ lineBuf += " ";
1215
+ }
1216
+ lineBuf += s;
1217
+ };
1218
+ const println = (v) => {
1219
+ const s = String(v);
1220
+ if (lineBuf.length > 0 && !lineBuf.endsWith(" ") && !s.startsWith(" ")) {
1221
+ lineBuf += " ";
1222
+ }
1223
+ lineBuf += s;
1224
+ flush();
1225
+ };
997
1226
  const targ = (v) => { targs.push(v); };
998
1227
  const env = {
999
1228
  "panicTag": tag,
1000
- "panic": (msg) => { throw new Error(String(msg)); },
1229
+ "panic": (msg) => {
1230
+ let str;
1231
+ try {
1232
+ str = String(msg);
1233
+ } catch {
1234
+ str = "[panic object]";
1235
+ }
1236
+ throw new Error(str);
1237
+ },
1001
1238
  "str": (i) => stringTable[i] ?? "",
1002
1239
  "str_len": (s) => (s ? s.length : 0),
1003
1240
  "str_concat": (a, b) => (a ?? "") + (b ?? ""),
@@ -1011,6 +1248,33 @@ function ${P}imports(stringTable, extraEnv, tag, write) {
1011
1248
  "str_slice": (s, a, b) => (s ? s.slice(a, b) : ""),
1012
1249
  "str_from_code_point": (c) => String.fromCodePoint(c),
1013
1250
  "str_code_point_at": (s, i) => (s ? s.codePointAt(i) : 0),
1251
+ "str_hash": (s) => {
1252
+ if (!s) return 0;
1253
+ let h = 0;
1254
+ for (let i = 0; i < s.length; i++) {
1255
+ h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
1256
+ }
1257
+ return h;
1258
+ },
1259
+ "str_to_upper": (s) => (s ? s.toUpperCase() : ""),
1260
+ "str_to_lower": (s) => (s ? s.toLowerCase() : ""),
1261
+ "str_trim_space": (s) => (s ? s.trim() : ""),
1262
+ "str_contains": (s, sub) => (s && sub !== undefined ? (s.includes(sub) ? 1 : 0) : 0),
1263
+ "str_has_prefix": (s, pre) => (s && pre !== undefined ? (s.startsWith(pre) ? 1 : 0) : 0),
1264
+ "str_has_suffix": (s, suf) => (s && suf !== undefined ? (s.endsWith(suf) ? 1 : 0) : 0),
1265
+ "str_index": (s, sub) => (s ? s.indexOf(sub) : -1),
1266
+ "str_last_index": (s, sub) => (s ? s.lastIndexOf(sub) : -1),
1267
+ "str_repeat": (s, n) => (s && n > 0 ? s.repeat(Number(n)) : ""),
1268
+ "str_replace_all": (s, o, n) => (s ? s.replaceAll(o, n) : ""),
1269
+ "str_equal_fold": (a, b) => ((a ?? "").toLowerCase() === (b ?? "").toLowerCase() ? 1 : 0),
1270
+ "str_count": (s, sep) => {
1271
+ if (!s) return sep === "" ? 1 : 0;
1272
+ if (sep === "") return s.length + 1;
1273
+ return s.split(sep).length - 1;
1274
+ },
1275
+ "str_from_i64": (n) => String(n),
1276
+ "str_from_i32": (n) => String(n),
1277
+ "str_from_f64": (f) => String(f),
1014
1278
  "is_string": (v) => (typeof v === "string" ? 1 : 0),
1015
1279
  "print_i32": print, "print_i64": print, "print_f32": print, "print_f64": print,
1016
1280
  "print_str": print, "print_any": print,
@@ -1027,14 +1291,107 @@ function ${P}imports(stringTable, extraEnv, tag, write) {
1027
1291
  "testing_name": (t) => t.Name(),
1028
1292
  "testing_flag": (t, i) => (t[stringTable[i]]() ? 1 : 0),
1029
1293
  };
1294
+ // Stdlib imports (strings/strconv/utf8/fmt/t.Run) are a separate block so
1295
+ // modules that never call them do not ship it (see WASM_STDLIB_JS).
1296
+ if (typeof ${P}stdlib === "function") {
1297
+ Object.assign(env, ${P}stdlib(stringTable, {
1298
+ "take": () => { const a = targs; targs = []; return a; },
1299
+ "printf": (s) => {
1300
+ lineBuf += s;
1301
+ const nl = lineBuf.lastIndexOf("\\n");
1302
+ if (nl >= 0) { write(lineBuf.slice(0, nl)); lineBuf = lineBuf.slice(nl + 1); }
1303
+ },
1304
+ "late": late,
1305
+ }));
1306
+ }
1030
1307
  Object.assign(env, extraEnv);
1031
1308
  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 };
1309
+ for (const k of ["sin", "cos", "tan", "asin", "acos", "atan", "atan2", "pow", "exp", "log", "log2", "log10", "cbrt", "hypot"]) m[k] = Math[k];
1310
+ m.mod = (x, y) => x % y;
1311
+ // Go rounds half away from zero; Math.round rounds half toward +Infinity.
1312
+ m.round = (x) => Math.sign(x) * Math.round(Math.abs(x));
1313
+ // Exports are needed by imports that call back into the module (t.Run).
1314
+ return { "env": env, "Math": m, "__bind": (ex) => { late.exports = ex; } };
1315
+ }`;
1316
+
1317
+ // JS side of the STDLIB_IMPORTS table (index.js); emitted only when the
1318
+ // module imports one of the packages that use it.
1319
+ export const WASM_STDLIB_JS = `${HELPER_SPRINTF}
1320
+ ${HELPER_STRCONV}
1321
+ function ${P}stdlib(stringTable, io) {
1322
+ let parts = [];
1323
+ let strconvErr = null;
1324
+ let decodeSize = 0;
1325
+ return {
1326
+ "testing_run": (t, name, fn) => (t.Run(name, (sub) => { io.late.exports.__testing_run_cb(fn, sub); }) ? 1 : 0),
1327
+ "fmt_sprintf": (f) => __sprintf(f ?? "", ...io.take()),
1328
+ "fmt_printf": (f) => { io.printf(__sprintf(f ?? "", ...io.take())); },
1329
+ "str_trim": (s, c) => ${P}trim(s ?? "", c ?? "", true, true),
1330
+ "str_trim_left": (s, c) => ${P}trim(s ?? "", c ?? "", true, false),
1331
+ "str_trim_right": (s, c) => ${P}trim(s ?? "", c ?? "", false, true),
1332
+ "str_trim_prefix": (s, p) => (s && p && s.startsWith(p) ? s.slice(p.length) : (s ?? "")),
1333
+ "str_trim_suffix": (s, p) => (s && p && s.endsWith(p) ? s.slice(0, s.length - p.length) : (s ?? "")),
1334
+ "str_replace": (s, o, n, cnt) => {
1335
+ s = s ?? ""; o = o ?? ""; n = n ?? "";
1336
+ if (cnt < 0) return s.replaceAll(o, n);
1337
+ let out = "", from = 0;
1338
+ for (let k = 0; k < cnt; k++) {
1339
+ const at = s.indexOf(o, from);
1340
+ if (at < 0) break;
1341
+ out += s.slice(from, at) + n;
1342
+ from = at + o.length;
1343
+ if (o === "") { if (from >= s.length) break; out += s[from]; from++; }
1344
+ }
1345
+ return out + s.slice(from);
1346
+ },
1347
+ "str_contains_rune": (s, r) => ((s ?? "").includes(String.fromCodePoint(r)) ? 1 : 0),
1348
+ "str_contains_any": (s, cs) => { for (const c of (cs ?? "")) if ((s ?? "").includes(c)) return 1; return 0; },
1349
+ "str_index_byte": (s, b) => (s ?? "").indexOf(String.fromCharCode(b)),
1350
+ "str_index_rune": (s, r) => (s ?? "").indexOf(String.fromCodePoint(r)),
1351
+ "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; },
1352
+ "str_last_index_byte": (s, b) => (s ?? "").lastIndexOf(String.fromCharCode(b)),
1353
+ "str_title": (s) => (s ?? "").replace(/(^|[^\\p{L}\\p{N}_'])(\\p{Ll})/gu, (m, a, b) => a + b.toUpperCase()),
1354
+ "str_split": (s, sep) => { parts = (s ?? "").split(sep ?? ""); return parts.length; },
1355
+ "str_fields": (s) => { parts = (s ?? "").split(/\\s+/).filter((p) => p !== ""); return parts.length; },
1356
+ "str_part": (i) => parts[i] ?? "",
1357
+ "strconv_atoi": (s) => { const r = __strconv_atoi(s ?? ""); strconvErr = r[1]; return r[0]; },
1358
+ "strconv_parse_int": (s, base, bits) => { const r = __strconv_parse_int(s ?? "", base, bits); strconvErr = r[1]; return r[0]; },
1359
+ "strconv_parse_float": (s) => { const r = __strconv_parse_float(s ?? ""); strconvErr = r[1]; return r[0]; },
1360
+ "strconv_parse_bool": (s) => { const r = __strconv_parse_bool(s ?? ""); strconvErr = r[1]; return r[0] ? 1 : 0; },
1361
+ "strconv_err": () => strconvErr,
1362
+ "strconv_format_float": (f, fmtc, prec, _bits) => __strconv_format_float(f, fmtc, prec),
1363
+ "strconv_format_int": (n, base) => n.toString(base),
1364
+ "strconv_quote": (s) => JSON.stringify(s ?? ""),
1365
+ "utf8_rune_count": (s) => { let n = 0; for (const _ of (s ?? "")) n++; return n; },
1366
+ "utf8_rune_len": (r) => (r < 0 || r > 0x10ffff || (r >= 0xd800 && r <= 0xdfff) ? -1 : r < 0x80 ? 1 : r < 0x800 ? 2 : r < 0x10000 ? 3 : 4),
1367
+ "utf8_valid_string": (s) => (s ?? "").isWellFormed ? ((s ?? "").isWellFormed() ? 1 : 0) : (/[\\uD800-\\uDFFF]/.test((s ?? "").replace(/[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]/g, "")) ? 0 : 1),
1368
+ "utf8_decode_rune": (s, last) => {
1369
+ s = s ?? "";
1370
+ if (s.length === 0) { decodeSize = 0; return 0xfffd; }
1371
+ let cp;
1372
+ if (last) { const tail = s.codePointAt(s.length - 1); cp = (tail >= 0xdc00 && tail <= 0xdfff && s.length >= 2) ? s.codePointAt(s.length - 2) : tail; }
1373
+ else cp = s.codePointAt(0);
1374
+ if (cp >= 0xd800 && cp <= 0xdfff) { decodeSize = 1; return 0xfffd; }
1375
+ // Size is in UTF-16 code units, matching the JS backend (strings are JS strings).
1376
+ decodeSize = cp > 0xffff ? 2 : 1;
1377
+ return cp;
1378
+ },
1379
+ "utf8_decode_size": () => decodeSize,
1380
+ "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; },
1381
+ };
1382
+ }
1383
+ function ${P}trim(s, cutset, left, right) {
1384
+ const cs = Array.from(cutset);
1385
+ const chars = Array.from(s);
1386
+ let a = 0, b = chars.length;
1387
+ if (left) while (a < b && cs.includes(chars[a])) a++;
1388
+ if (right) while (b > a && cs.includes(chars[b - 1])) b--;
1389
+ return chars.slice(a, b).join("");
1034
1390
  }`;
1035
1391
 
1036
1392
  // Runtime loader emitted at the top of the facade.
1037
- export const WASM_LOADER_JS = `${WASM_IMPORTS_JS}
1393
+ const wasmLoaderJs = (stdlibEnv) => `${WASM_IMPORTS_JS}
1394
+ ${stdlibEnv ? WASM_STDLIB_JS : ""}
1038
1395
  async function ${P}fetch(url) {
1039
1396
  const res = await fetch(url);
1040
1397
  if (!res.ok) throw new Error("GoFront: failed to fetch " + url + " (" + res.status + ")");
@@ -1043,7 +1400,8 @@ async function ${P}fetch(url) {
1043
1400
  async function ${P}instantiate(imports) {
1044
1401
  const bytes = globalThis.__GOFRONT_WASM_BYTES;
1045
1402
  if (bytes) return WebAssembly.instantiate(bytes, imports);
1046
- const url = globalThis.__GOFRONT_WASM_URL ?? "app.wasm";
1403
+ // Resolve relative to the bundle so the app works from any page path.
1404
+ const url = globalThis.__GOFRONT_WASM_URL ?? new URL("app.wasm", import.meta.url).href;
1047
1405
  if (typeof WebAssembly.instantiateStreaming === "function") {
1048
1406
  try { return await WebAssembly.instantiateStreaming(fetch(url), imports); }
1049
1407
  catch { /* fall through: wrong MIME type or no streaming support */ }
@@ -1060,6 +1418,7 @@ async function ${P}load(stringTable, extraEnv) {
1060
1418
  // start function while package-level initializers run), so exports are
1061
1419
  // returned raw: no try/catch wrapper, which keeps JS→wasm calls inlinable.
1062
1420
  const result = await ${P}instantiate(imports);
1421
+ imports.__bind(result.instance.exports);
1063
1422
  return result.instance.exports;
1064
1423
  }
1065
1424
  // Go int/int64 cross the boundary as f64 (exact within the safe-integer range).
@@ -1074,6 +1433,11 @@ const ${P}u64out = (v) => {
1074
1433
  };
1075
1434
  const ${P}strin = (v) => (v == null ? "" : String(v));
1076
1435
  const ${P}href = (h) => (h == null ? null : h.__ref);
1436
+ // Tags a struct pointer held in an interface (same marker as the JS backend's __ifp).
1437
+ const ${P}ptag = (h) => {
1438
+ if (h !== null && h.__p !== true && Object.isExtensible(h)) Object.defineProperty(h, "__p", { "value": true, "configurable": true });
1439
+ return h;
1440
+ };
1077
1441
  const ${P}NIL_DEREF_PATTERNS = [
1078
1442
  "dereferencing a null pointer", // V8
1079
1443
  "dereferencing null pointer", // SpiderMonkey
@@ -1110,16 +1474,19 @@ function ${P}idxview(n, getAt, setAt) {
1110
1474
  // Generates the JS facade for the linked wasm module.
1111
1475
  export function generateFacade(
1112
1476
  meta,
1113
- { stringTable = [], callMain = false } = {},
1477
+ { stringTable = [], callMain = false, stdlibEnv = true } = {},
1114
1478
  ) {
1115
1479
  const needs = meta._needs ?? collectNeeds(meta);
1116
- const out = [WASM_LOADER_JS];
1480
+ const out = [wasmLoaderJs(stdlibEnv)];
1117
1481
 
1118
1482
  // JS callback imports (registered before instantiation; bodies run after).
1119
1483
  out.push(`const ${P}env = {};`);
1120
1484
  for (const [key, desc] of needs.funcs) {
1121
1485
  const names = desc.params.map((_, i) => `a${i}`);
1122
- const call = `fn(${desc.params.map((d, i) => outExpr(d, names[i])).join(", ")})`;
1486
+ const args = desc.params.map((d, i) =>
1487
+ isPtrBoth(d) ? `${P}view_${d.name}(${names[i]})` : outExpr(d, names[i]),
1488
+ );
1489
+ const call = `fn(${args.join(", ")})`;
1123
1490
  let body;
1124
1491
  if (desc.returns.length === 0) body = `${call};`;
1125
1492
  else if (desc.returns.length === 1)
@@ -1129,7 +1496,9 @@ export function generateFacade(
1129
1496
  .map((d, i) => inExpr(d, `__r[${i}]`))
1130
1497
  .join(", ")}];`;
1131
1498
  const sig = ["fn", ...names].join(", ");
1132
- out.push(`${P}env["__invoke$${key}"] = (${sig}) => { ${body} };`);
1499
+ out.push(
1500
+ `${P}env["__invoke$${key}"] = (${sig}) => { try { ${body} } catch (__e) { if (__w && __w.__push_panic) __w.__push_panic(__e.message ?? String(__e)); throw __e; } };`,
1501
+ );
1133
1502
  }
1134
1503
  out.push(
1135
1504
  `const __w = await ${P}load(${JSON.stringify(stringTable)}, ${P}env);`,
@@ -1196,22 +1565,61 @@ function ${P}arrview_${key}(a) {
1196
1565
  desc.returns,
1197
1566
  ["c"],
1198
1567
  );
1199
- out.push(`const ${P}fnmap_${key} = new WeakMap();
1568
+ out.push(`const ${P}fn_j2w_${key} = new WeakMap();
1569
+ const ${P}fn_w2j_${key} = new WeakMap();
1200
1570
  function ${P}fnin_${key}(fn) {
1201
1571
  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); }
1572
+ let c = ${P}fn_j2w_${key}.get(fn);
1573
+ if (!c) {
1574
+ c = __w.__wrap_fn$${key}(fn);
1575
+ ${P}fn_j2w_${key}.set(fn, c);
1576
+ ${P}fn_w2j_${key}.set(c, fn);
1577
+ }
1204
1578
  return c;
1205
1579
  }
1206
1580
  function ${P}fnout_${key}(c) {
1207
1581
  if (c == null) return null;
1208
- return (${names.join(", ")}) => {
1582
+ let fn = ${P}fn_w2j_${key}.get(c);
1583
+ if (!fn) {
1584
+ fn = (${names.join(", ")}) => {
1209
1585
  ${indent(body, "\t")}
1210
- };
1586
+ };
1587
+ fn.__ref = c;
1588
+ ${P}fn_w2j_${key}.set(c, fn);
1589
+ ${P}fn_j2w_${key}.set(fn, c);
1590
+ }
1591
+ return fn;
1592
+ }`);
1593
+ }
1594
+
1595
+ for (const [key, desc] of needs.ifaces) genIfaceFacade(out, key, desc);
1596
+
1597
+ if (needs.shared) {
1598
+ // Zero-copy TypedArray views over the exported memory. Memory never
1599
+ // grows after the start function (allocation is startup-only), but the
1600
+ // cached view is still re-validated against the live buffer.
1601
+ out.push(`const ${P}shc = new WeakMap();
1602
+ function ${P}shout(r, Ctor) {
1603
+ if (r == null) return null;
1604
+ let v = ${P}shc.get(r);
1605
+ if (!v || v.buffer !== __w.memory.buffer) {
1606
+ v = new Ctor(__w.memory.buffer, __w.__shared_base(r), __w.__shared_len(r));
1607
+ ${P}shc.set(r, v);
1608
+ }
1609
+ return v;
1610
+ }
1611
+ function ${P}shin(v, Ctor) {
1612
+ if (v == null) return null;
1613
+ if (!(v instanceof Ctor) || v.buffer !== __w.memory.buffer) throw new TypeError("GoFront: expected a " + Ctor.name + " view over the wasm shared memory");
1614
+ return __w.__shared_make(v.byteOffset, v.length);
1211
1615
  }`);
1212
1616
  }
1213
1617
 
1214
1618
  for (const c of meta.consts) out.push(`const ${c.name} = ${c.value};`);
1619
+ for (const v of meta.vars ?? [])
1620
+ out.push(
1621
+ `const ${v.name} = ${P}shout(__w.__var_${v.name}(), ${v.desc.ctor});`,
1622
+ );
1215
1623
 
1216
1624
  for (const f of meta.funcs) {
1217
1625
  const { names, body } = genFuncBody(
@@ -1298,21 +1706,66 @@ ${backLines.join("\n")}
1298
1706
  }`);
1299
1707
  }
1300
1708
 
1709
+ function genIfaceFacade(out, key, desc) {
1710
+ const methods = desc.methods.map((m) => {
1711
+ const { names, body } = genFuncBody(m.callName, m.params, m.returns, [
1712
+ "this.__ref",
1713
+ ]);
1714
+ return `\t${m.name}(${names.join(", ")}) {\n${indent(body, "\t")}\n\t}`;
1715
+ });
1716
+ const cases = desc.concrete.map((c, i) => {
1717
+ const wrap = c.both ? `${P}view_${c.name}(r)` : `${c.name}.__wrap(r)`;
1718
+ return `\t\tcase ${i + 1}: return ${P}ptag(${wrap});`;
1719
+ });
1720
+ const tag =
1721
+ cases.length > 0
1722
+ ? `\tswitch (__w.__itag$${key}(r)) {\n${cases.join("\n")}\n\t}\n`
1723
+ : "";
1724
+ 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`;
1725
+ const implMsg = `GoFront: wasm value does not implement interface '${desc.name}'`;
1726
+ out.push(`class ${P}I_${key} {
1727
+ constructor(r) { this.__ref = r; }
1728
+ ${methods.join("\n")}
1729
+ }
1730
+ const ${P}ic_${key} = new WeakMap();
1731
+ function ${P}ifout_${key}(r) {
1732
+ if (r == null) return null;
1733
+ ${tag} let o = ${P}ic_${key}.get(r);
1734
+ if (!o) { o = new ${P}I_${key}(r); ${P}ic_${key}.set(r, o); }
1735
+ return o;
1736
+ }
1737
+ function ${P}ifin_${key}(v) {
1738
+ if (v == null) return null;
1739
+ if (v.__ref === undefined) throw new TypeError(${JSON.stringify(msg)});
1740
+ if (!__w.__impl$${key}(v.__ref)) throw new TypeError(${JSON.stringify(implMsg)});
1741
+ return v.__ref;
1742
+ }`);
1743
+ }
1744
+
1301
1745
  function genHandleStruct(out, name, fields, methods) {
1302
1746
  // `$`-suffixed params keep the mangler from renaming the matching getters.
1303
1747
  const ctorParams = fields.map((f) => `${f.name}$ = ${zeroExpr(f.desc)}`);
1304
1748
  const ctorArgs = fields.map((f) => inExpr(f.desc, `${f.name}$`));
1749
+ const fromArgs = fields.map((f) => `o.${f.name}`);
1750
+ const ptrMsg = `GoFront: expected a ${name} handle for a *${name} parameter (use ${name}.from(obj) to create one)`;
1305
1751
  const lines = [
1306
1752
  `const ${P}h_${name} = new WeakMap();
1307
1753
  function ${P}hval_${name}(h) {
1308
1754
  if (h == null) return __w.__zero_${name}();
1309
- return __w.__clone_${name}(h.__ref);
1755
+ if (h.__ref !== undefined) return __w.__clone_${name}(h.__ref);
1756
+ return ${name}.from(h).__ref;
1757
+ }
1758
+ function ${P}href_${name}(h) {
1759
+ if (h == null) return null;
1760
+ if (h.__ref !== undefined) return h.__ref;
1761
+ throw new TypeError(${JSON.stringify(ptrMsg)});
1310
1762
  }
1311
1763
  class ${name} {
1312
1764
  constructor(${ctorParams.join(", ")}) {
1313
1765
  this.__ref = __w.__new_${name}(${ctorArgs.join(", ")});
1314
1766
  ${P}h_${name}.set(this.__ref, this);
1315
1767
  }
1768
+ static from(o) { return new ${name}(${fromArgs.join(", ")}); }
1316
1769
  static __wrap(ref) {
1317
1770
  if (ref == null) return null;
1318
1771
  let h = ${P}h_${name}.get(ref);