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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gofront",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "author": "Luuk van Venrooij",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -11,12 +11,15 @@
11
11
  "url": "https://github.com/seriva/gofront/issues"
12
12
  },
13
13
  "homepage": "https://github.com/seriva/gofront#readme",
14
- "description": "A Go-inspired language that transpiles to JavaScript",
14
+ "description": "A Go-inspired language that compiles to JavaScript and WebAssembly",
15
15
  "keywords": [
16
16
  "go",
17
17
  "compiler",
18
18
  "transpiler",
19
19
  "javascript",
20
+ "webassembly",
21
+ "wasm",
22
+ "wasmgc",
20
23
  "frontend",
21
24
  "type-safe"
22
25
  ],
@@ -31,31 +34,24 @@
31
34
  "gofront": "./src/index.js"
32
35
  },
33
36
  "scripts": {
34
- "build:simple": "node src/index.js example/simple/src -o example/simple/app.js",
35
- "build:reactive": "node src/index.js example/reactive/src -o example/reactive/app.js",
36
- "build:gom": "node src/index.js example/gom/src -o example/gom/app.js",
37
- "build:templ": "node src/index.js example/templ/src -o example/templ/app.js",
38
- "build:webgl": "node src/index.js example/webgl/src -o example/webgl/app.js",
39
- "build:all": "npm run build:simple && npm run build:reactive && npm run build:gom && npm run build:templ && npm run build:webgl",
40
- "test": "npm run test:unit",
41
- "test:all": "npm run test:unit && npm run test:perf && npm run test:examples && npm run test:examples:dom && npm run test:e2e",
42
- "test:unit": "node test/unit/run.js",
43
- "test:perf": "node test/e2e/perf/zero-alloc.js",
44
- "bench:raycast": "node --expose-gc test/e2e/perf/raycast-bench.js",
45
- "test:examples": "node src/index.js test example/simple/src && node src/index.js test example/simple/src/utils && node src/index.js test example/reactive/src && node src/index.js test example/reactive/src/utils && node src/index.js test example/gom/src && node src/index.js test example/gom/src/utils && node src/index.js test example/templ/src && node src/index.js test example/templ/src/utils",
46
- "test:examples:dom": "node src/index.js test --dom example/simple/src && node src/index.js test --dom example/gom/src && node src/index.js test --dom example/templ/src",
37
+ "build": "node src/index.js example/simple/src -o example/simple/app.js && node src/index.js example/reactive/src -o example/reactive/app.js && node src/index.js example/gom/src -o example/gom/app.js && node src/index.js example/templ/src -o example/templ/app.js && node src/index.js example/webgl/src -o example/webgl/app.js && node src/index.js example/hybrid/src -o example/hybrid/app.js",
38
+ "test": "node test/unit/run.js && node test/e2e/perf/zero-alloc.js && node src/index.js test --dom example/...",
39
+ "test:e2e": "playwright test",
47
40
  "test:coverage": "c8 --include='src/**' --reporter=text --reporter=lcov node test/unit/run.js",
48
- "test:e2e": "npm run build:all && playwright test",
41
+ "bench": "node --expose-gc test/e2e/perf/raycast-bench.js",
49
42
  "format": "biome format --write .",
50
- "check": "biome check . && sentrux check && sentrux gate && node src/index.js example/simple/src --check && node src/index.js example/reactive/src --check && node src/index.js example/gom/src --check && node src/index.js example/templ/src --check && node src/index.js example/webgl/src --check",
43
+ "check": "biome check . && sentrux check && sentrux gate && node src/index.js check example/...",
51
44
  "prepare": "lefthook install"
52
45
  },
53
46
  "devDependencies": {
54
- "@biomejs/biome": "^2.5.14",
47
+ "@biomejs/biome": "^2.5.15",
55
48
  "@playwright/test": "^1.63.0",
56
49
  "c8": "^12.0.0",
57
- "jsdom": "^30.1.0",
58
- "lefthook": "^2.1.14",
50
+ "jsdom": "^30.1.2",
51
+ "lefthook": "^2.1.17",
59
52
  "serve": "^14.2.6"
53
+ },
54
+ "optionalDependencies": {
55
+ "binaryen": "^132.0.0"
60
56
  }
61
57
  }
@@ -14,7 +14,7 @@ import {
14
14
 
15
15
  /** @typedef {import('./index.js').CodeGen} CodeGen */
16
16
 
17
- export const STRICT_NUMERIC_TYPES = new Set([
17
+ const STRICT_NUMERIC_TYPES = new Set([
18
18
  "float32",
19
19
  "int8",
20
20
  "int16",
@@ -26,7 +26,7 @@ export const STRICT_NUMERIC_TYPES = new Set([
26
26
  "uint32",
27
27
  ]);
28
28
 
29
- export const STRICT_OPS = new Set([
29
+ const STRICT_OPS = new Set([
30
30
  "+",
31
31
  "-",
32
32
  "*",
@@ -233,14 +233,19 @@ const BUILTIN_GEN = {
233
233
  }
234
234
  return `{ value: ${s.zeroValueForExpr(arg)} }`;
235
235
  },
236
- print: (s, e) => `console.log(${e.args.map((a) => s.genExpr(a)).join(", ")})`,
236
+ // `print` writes without a trailing newline (Go semantics); the wasm glue
237
+ // buffers the same way, so output parity holds across backends.
238
+ print: (s, e) =>
239
+ `((__s) => (typeof process !== "undefined" && process?.stdout?.write ? process.stdout.write(__s) : console.log(__s)))([${e.args.map((a) => s.genExpr(a)).join(", ")}].map(String).join(" "))`,
237
240
  println: (s, e) =>
238
241
  `console.log(${e.args.map((a) => s.genExpr(a)).join(", ")})`,
239
242
  panic: (s, e) => `(() => { throw new Error(${s.genExpr(e.args[0])}); })()`,
240
- recover: () =>
241
- `(typeof __panic !== "undefined" && __panic !== null ? (() => { const __r = __panic.message ?? String(__panic); __panic = null; return __r; })() : null)`,
243
+ recover: (s) => {
244
+ s.useHelper("panicRt");
245
+ return `__recover(${s._recoverOkInScope ? "__recoverOk" : "false"})`;
246
+ },
242
247
  error: (s, e) => {
243
- s._usesError = true;
248
+ s.useHelper("error");
244
249
  return `__error(${s.genExpr(e.args[0])})`;
245
250
  },
246
251
  min: (s, e) => `Math.min(${e.args.map((a) => s.genExpr(a)).join(", ")})`,
@@ -272,7 +277,7 @@ export const expressionGenMethods = {
272
277
  if (expr._ifaceBox && !expr._ifaceBoxing) {
273
278
  expr._ifaceBoxing = true;
274
279
  try {
275
- this._usesIfaceBox = true;
280
+ this.useHelper("ifaceBox");
276
281
  const fn = expr._ifaceBox === "ptr" ? "__ifp" : "__ifv";
277
282
  return `${fn}(${this.genExpr(expr)})`;
278
283
  } finally {
@@ -365,7 +370,7 @@ export const expressionGenMethods = {
365
370
  if (base.kind === "struct") {
366
371
  if (this._structClassName(t) || this._structClassName(base))
367
372
  return `${js}.__clone()`;
368
- this._usesSClone = true;
373
+ this.useHelper("sclone");
369
374
  return `__sclone(${js})`;
370
375
  }
371
376
  const elemClone = this._cloneJs(base.elem, "__e");
@@ -613,7 +618,7 @@ export const expressionGenMethods = {
613
618
  this._withFnCtx(expr.body, () =>
614
619
  this.indented(() => {
615
620
  this._emitParamCopies(expr.params);
616
- this._genBody(expr.body);
621
+ this._genBody(expr.body, expr);
617
622
  }),
618
623
  );
619
624
  const body = this.out.join("\n");
@@ -702,12 +707,20 @@ export const expressionGenMethods = {
702
707
  return this._genTimeMethodCall(expr.func.field, expr);
703
708
  if (recvName === "testing.T")
704
709
  return this._genTestingMethodCall(expr.func.field, expr);
710
+ if (recvName?.startsWith("shared.") && expr.func.field === "Subarray") {
711
+ const args = expr.args.map((a) => this.genExpr(a)).join(", ");
712
+ return `${this.genExpr(expr.func.expr)}.subarray(${args})`;
713
+ }
705
714
  return undefined;
706
715
  },
707
716
 
708
717
  _genSelectorCall(expr) {
709
- if (this._isErrorMethodCall(expr))
710
- return `${this.genExpr(expr.func.expr)}.Error()`;
718
+ if (this._isErrorMethodCall(expr)) {
719
+ const recv = this.genExpr(expr.func.expr);
720
+ if (!this.strict) return `${recv}.Error()`;
721
+ this.useHelper("nilCall");
722
+ return `__nilcall(${recv}).Error()`;
723
+ }
711
724
  if (expr.func.expr.kind === "Ident") {
712
725
  const ns = expr.func.expr.name;
713
726
  const fn = expr.func.field;
@@ -716,7 +729,25 @@ export const expressionGenMethods = {
716
729
  }
717
730
  const recvType = expr.func.expr._type;
718
731
  const recvName = this._resolveRecvName(recvType);
719
- return this._genReceiverTypeCall(recvName, expr);
732
+ const typed = this._genReceiverTypeCall(recvName, expr);
733
+ if (typed !== undefined) return typed;
734
+ if (this.strict && this._isInterfaceType(recvType)) {
735
+ // Go panics with a nil-dereference runtime error; JS would throw a
736
+ // TypeError with a different message.
737
+ this.useHelper("nilCall");
738
+ const recv = this.genExpr(expr.func.expr);
739
+ const args = expr.args
740
+ .map((a) => (a._spread ? `...${this.genExpr(a)}` : this.genExpr(a)))
741
+ .join(", ");
742
+ return `__nilcall(${recv}).${expr.func.field}(${args})`;
743
+ }
744
+ return undefined;
745
+ },
746
+
747
+ _isInterfaceType(t) {
748
+ if (!t) return false;
749
+ if (t.kind === "interface") return true;
750
+ return t.kind === "named" && t.underlying?.kind === "interface";
720
751
  },
721
752
 
722
753
  _genBuiltinLen(expr) {
@@ -727,7 +758,7 @@ export const expressionGenMethods = {
727
758
  if (wrapField) return `${this.genExpr(arg)}.${wrapField}.length`;
728
759
  const js = this.genExpr(arg);
729
760
  if (t?.kind === "map") return `Object.keys(${js}).length`;
730
- this._usesLen = true;
761
+ this.useHelper("len");
731
762
  return `__len(${js})`;
732
763
  },
733
764
 
@@ -760,7 +791,7 @@ export const expressionGenMethods = {
760
791
  a._spread ? `...${this.genExpr(a)}` : this.genValueExpr(a),
761
792
  );
762
793
  if (elems.length === 0) return this.genExpr(expr.args[0]);
763
- this._usesAppend = true;
794
+ this.useHelper("append");
764
795
  return `new ${retType.name}(__append(${sliceJS}, ${elems.join(", ")}))`;
765
796
  }
766
797
  const slice = this.genExpr(expr.args[0]);
@@ -768,7 +799,7 @@ export const expressionGenMethods = {
768
799
  .slice(1)
769
800
  .map((a) => (a._spread ? `...${this.genExpr(a)}` : this.genValueExpr(a)));
770
801
  if (elems.length === 0) return slice;
771
- this._usesAppend = true;
802
+ this.useHelper("append");
772
803
  const sliceType = expr._type ?? expr.args[0]._type;
773
804
  const typedCtor = isTypedArraySlice(sliceType)
774
805
  ? typedArrayConstructorForElem(
@@ -1240,10 +1271,10 @@ export const expressionGenMethods = {
1240
1271
  case "-":
1241
1272
  return `{ re: ${l}.re - ${r}.re, im: ${l}.im - ${r}.im }`;
1242
1273
  case "*":
1243
- this._usesCmul = true;
1274
+ this.useHelper("cmul");
1244
1275
  return `__cmul(${l}, ${r})`;
1245
1276
  case "/":
1246
- this._usesCdiv = true;
1277
+ this.useHelper("cdiv");
1247
1278
  return `__cdiv(${l}, ${r})`;
1248
1279
  case "==":
1249
1280
  return `(${l}.re === ${r}.re && ${l}.im === ${r}.im)`;
@@ -1261,7 +1292,7 @@ export const expressionGenMethods = {
1261
1292
  return this._genComplexBinary(expr);
1262
1293
 
1263
1294
  if (this._isStructEquality(expr)) {
1264
- this._usesEqual = true;
1295
+ this.useHelper("equal");
1265
1296
  const cmp = `__equal(${this.genExpr(expr.left)}, ${this.genExpr(expr.right)})`;
1266
1297
  return expr.op === "==" ? cmp : `!${cmp}`;
1267
1298
  }
@@ -1346,7 +1377,7 @@ export const expressionGenMethods = {
1346
1377
  return this._genSliceTypeConversion(expr, inner, t);
1347
1378
  const target = t?.name;
1348
1379
  if (target === "error") {
1349
- this._usesError = true;
1380
+ this.useHelper("error");
1350
1381
  return `__error(${inner})`;
1351
1382
  }
1352
1383
  if (target && this.namedWrapperNames.has(target))
@@ -19,7 +19,9 @@
19
19
 
20
20
  import {
21
21
  computeEmbeddedStubs,
22
+ hasDirectRecover,
22
23
  isReferenceType,
24
+ normalizeDefers,
23
25
  scanAddressTaken,
24
26
  } from "../../lower/index.js";
25
27
  import { isComplex, isNumeric } from "../../typechecker/types.js";
@@ -34,21 +36,24 @@ import {
34
36
  HELPER_IFACE_BOX,
35
37
  HELPER_INJECT_STYLES,
36
38
  HELPER_LEN,
39
+ HELPER_NIL_CALL,
40
+ HELPER_PANIC,
37
41
  HELPER_PATH_CLEAN,
38
42
  HELPER_S,
39
43
  HELPER_SCLONE,
40
44
  HELPER_SORT_SLICE,
41
45
  HELPER_SPRINTF,
46
+ HELPER_STRCONV,
42
47
  HELPER_TESTING,
43
48
  HELPER_TIME_FMT,
44
49
  HELPER_TIME_PARSE,
45
50
  } from "./runtime.js";
46
- import { buildSourceMap } from "./source-map.js";
51
+ import { buildSourceMap, buildWasmSourceMap } from "./source-map.js";
47
52
  import { statementGenMethods } from "./statements.js";
48
53
  import { stdlibGenMethods } from "./stdlib/index.js";
49
54
  import { templGenMethods } from "./templ.js";
50
55
 
51
- export { buildSourceMap };
56
+ export { buildSourceMap, buildWasmSourceMap };
52
57
 
53
58
  // Valid Go identifiers that cannot be used as JS bindings; emitted with a `$` suffix.
54
59
  const JS_RESERVED = new Set([
@@ -90,6 +95,30 @@ const JS_RESERVED = new Set([
90
95
  const jsSafeName = (n) =>
91
96
  typeof n === "string" && JS_RESERVED.has(n) ? `${n}$` : n;
92
97
 
98
+ // Runtime helpers by registry name, in output order. Mark one as needed with
99
+ // `this.useHelper(name)`; `_prependHelpers` emits every marked entry.
100
+ const HELPER_MAP = [
101
+ ["len", HELPER_LEN],
102
+ ["append", HELPER_APPEND],
103
+ ["sliceGuard", HELPER_S],
104
+ ["sclone", HELPER_SCLONE],
105
+ ["ifaceBox", HELPER_IFACE_BOX],
106
+ ["equal", HELPER_EQUAL],
107
+ ["cmul", HELPER_CMUL],
108
+ ["cdiv", HELPER_CDIV],
109
+ ["panicRt", HELPER_PANIC],
110
+ ["nilCall", HELPER_NIL_CALL],
111
+ ["sprintf", HELPER_SPRINTF],
112
+ ["error", HELPER_ERROR],
113
+ ["errorIs", HELPER_ERROR_IS],
114
+ ["strconv", HELPER_STRCONV],
115
+ ["pathClean", HELPER_PATH_CLEAN],
116
+ ["sortSlice", HELPER_SORT_SLICE],
117
+ ["timeFmt", HELPER_TIME_FMT],
118
+ ["timeParse", HELPER_TIME_PARSE],
119
+ ["testing", HELPER_TESTING],
120
+ ];
121
+
93
122
  export class CodeGen {
94
123
  // jsImports: Map<importPath, string[]> — npm package imports to emit at top of file
95
124
  // bundledPackages: Set<string> — GoFront package names bundled inline;
@@ -116,22 +145,7 @@ export class CodeGen {
116
145
  this._currentSrcFileIdx = 0; // updated as each top-level decl is generated
117
146
  this._boxedVars = new Set(); // address-taken scalar variables that need boxing
118
147
  // Runtime helper usage tracking — only emit helpers that are actually used
119
- this._usesLen = false;
120
- this._usesAppend = false;
121
- this._usesSliceGuard = false;
122
- this._usesSprintf = false;
123
- this._usesEqual = false;
124
- this._usesCmul = false;
125
- this._usesCdiv = false;
126
- this._usesError = false;
127
- this._usesErrorIs = false;
128
- this._usesPathClean = false;
129
- this._usesSortSlice = false;
130
- this._usesTimeFmt = false;
131
- this._usesTimeParse = false;
132
- this._usesTesting = false;
133
- this._usesSClone = false;
134
- this._usesIfaceBox = false;
148
+ this._helpers = new Set();
135
149
  // Per-function context for Go value semantics (see _withFnCtx)
136
150
  this._fnCtx = null;
137
151
  // Iterator (range-over-func) context
@@ -217,6 +231,10 @@ export class CodeGen {
217
231
 
218
232
  generate(program, options = {}) {
219
233
  const isTest = options.isTest ?? false;
234
+ // Defer lowering (argument/receiver hoisting, recover-target marking) is
235
+ // idempotent and owned by the backends: the wasm path runs it via
236
+ // `lower()`, the JS path here.
237
+ normalizeDefers(program);
220
238
  // Ensure _pkgName is set on all decls (generateAll sets it for
221
239
  // multi-file; for single-file compiles we derive it from the AST).
222
240
  const pkgName = program.pkg?.name;
@@ -407,28 +425,21 @@ export class CodeGen {
407
425
  return methods;
408
426
  }
409
427
 
428
+ /** Marks a runtime helper (key of HELPER_MAP) as needed by the output. */
429
+ useHelper(name) {
430
+ this._helpers.add(name);
431
+ }
432
+
410
433
  _prependHelpers(isTest = false) {
411
- const needsTesting = isTest || this._usesTesting;
412
- const HELPER_MAP = [
413
- [this.collectedCss.length > 0, HELPER_INJECT_STYLES],
414
- [this._usesLen, HELPER_LEN],
415
- [this._usesAppend, HELPER_APPEND],
416
- [this._usesSliceGuard, HELPER_S],
417
- [this._usesSClone, HELPER_SCLONE],
418
- [this._usesIfaceBox, HELPER_IFACE_BOX],
419
- [this._usesEqual, HELPER_EQUAL],
420
- [this._usesCmul, HELPER_CMUL],
421
- [this._usesCdiv, HELPER_CDIV],
422
- [this._usesSprintf || needsTesting, HELPER_SPRINTF],
423
- [this._usesError, HELPER_ERROR],
424
- [this._usesErrorIs, HELPER_ERROR_IS],
425
- [this._usesPathClean, HELPER_PATH_CLEAN],
426
- [this._usesSortSlice, HELPER_SORT_SLICE],
427
- [this._usesTimeFmt, HELPER_TIME_FMT],
428
- [this._usesTimeParse, HELPER_TIME_PARSE],
429
- [needsTesting, HELPER_TESTING],
430
- ];
431
- const helpers = HELPER_MAP.filter(([flag]) => flag).map(([, h]) => h);
434
+ if (isTest || this._helpers.has("testing")) {
435
+ this.useHelper("sprintf");
436
+ this.useHelper("testing");
437
+ }
438
+ const helpers = [];
439
+ if (this.collectedCss.length > 0) helpers.push(HELPER_INJECT_STYLES);
440
+ for (const [name, src] of HELPER_MAP) {
441
+ if (this._helpers.has(name)) helpers.push(src);
442
+ }
432
443
  if (helpers.length > 0) {
433
444
  const helperLines = helpers.flatMap((h) => h.split("\n"));
434
445
  helperLines.push("");
@@ -661,7 +672,7 @@ export class CodeGen {
661
672
  }
662
673
  }
663
674
  this._emitParamCopies(decl.params);
664
- this._withNamedReturns(decl, () => this._genBody(decl.body));
675
+ this._withNamedReturns(decl, () => this._genBody(decl.body, decl));
665
676
  }),
666
677
  );
667
678
  this._unwrappedRecv = prevUnwrapped;
@@ -718,7 +729,7 @@ export class CodeGen {
718
729
  this._withFnCtx(decl.body, () =>
719
730
  this.indented(() => {
720
731
  this._emitParamCopies(decl.params);
721
- this._withNamedReturns(decl, () => this._genBody(decl.body));
732
+ this._withNamedReturns(decl, () => this._genBody(decl.body, decl));
722
733
  }),
723
734
  );
724
735
  this._boxedVars = prevBoxed;
@@ -726,45 +737,74 @@ export class CodeGen {
726
737
  }
727
738
 
728
739
  _withNamedReturns(decl, fn) {
740
+ const prevDecl = this._currentDecl;
741
+ this._currentDecl = decl;
729
742
  const named = decl.returnType?._namedReturns;
730
743
  const prev = this.namedReturnVars;
731
744
  if (named) {
732
745
  // Emit zero-value declarations for named return vars
733
- for (const { name, type } of named) {
734
- if (name) {
735
- this._markOwnership(name, true);
736
- this.line(`let ${name} = ${this.zeroValueForTypeNode(type)};`);
737
- }
746
+ for (let i = 0; i < named.length; i++) {
747
+ const { name, type } = named[i];
748
+ const varName = !name || name === "_" ? `__ret_blank$${i}` : name;
749
+ this._markOwnership(varName, true);
750
+ this.line(`let ${varName} = ${this.zeroValueForTypeNode(type)};`);
738
751
  }
739
- this.namedReturnVars = named.map((r) => r.name).filter(Boolean);
752
+ this.namedReturnVars = named.map((r, i) =>
753
+ !r.name || r.name === "_" ? `__ret_blank$${i}` : r.name,
754
+ );
740
755
  } else {
741
756
  this.namedReturnVars = null;
742
757
  }
743
758
  fn();
744
759
  this.namedReturnVars = prev;
760
+ this._currentDecl = prevDecl;
745
761
  }
746
762
 
747
763
  // Emit a function body, wrapping in try/catch/finally for defer if needed.
748
- _genBody(body) {
764
+ // `fnNode` is the enclosing FuncDecl/MethodDecl/FuncLit; it decides whether
765
+ // the recover-arming prologue is needed (see HELPER_PANIC).
766
+ _genBody(body, fnNode = null) {
767
+ const prevRecoverOk = this._recoverOkInScope;
768
+ this._recoverOkInScope = false;
769
+ const isTarget = Boolean(fnNode?._isDeferTarget);
770
+ const forwards = isTarget && Boolean(fnNode._deferForward);
771
+ if (!forwards && (isTarget || hasDirectRecover(body))) {
772
+ this.useHelper("panicRt");
773
+ this.line(
774
+ "const __recoverOk = __gopanic.armed; __gopanic.armed = false;",
775
+ );
776
+ this._recoverOkInScope = true;
777
+ }
749
778
  if (!body._hasDefer) {
750
779
  this.genBlock(body);
780
+ this._recoverOkInScope = prevRecoverOk;
751
781
  return;
752
782
  }
783
+ this.useHelper("panicRt");
753
784
  this.line("const __defers = [];");
754
- this.line("let __panic = null;");
785
+ this.line("const __frame = { pn: null };");
755
786
  this.line("try {");
756
787
  this.indented(() => this.genBlock(body));
757
788
  this.line("} catch (__err) {");
758
- this.indented(() => this.line("__panic = __err;"));
789
+ this.indented(() => {
790
+ this.line("__frame.pn = { err: __err, recovered: false };");
791
+ this.line("__gopanic.stack.push(__frame.pn);");
792
+ });
759
793
  this.line("} finally {");
760
794
  this.indented(() => {
761
- this.line(
762
- "for (let __i = __defers.length - 1; __i >= 0; __i--) __defers[__i]();",
763
- );
764
- this.line("if (__panic !== null) throw __panic;");
795
+ // Runs defers LIFO and rethrows an unrecovered panic.
796
+ this.line("__runDefers(__defers, __frame);");
797
+ if (this.namedReturnVars?.length > 0) {
798
+ const vars = this.namedReturnVars;
799
+ this.line(
800
+ vars.length === 1
801
+ ? `return ${vars[0]};`
802
+ : `return [${vars.join(", ")}];`,
803
+ );
804
+ }
765
805
  });
766
806
  this.line("}");
767
- // If a recover() cleared __panic, execution reaches here.
807
+ // If a recover() cleared the panic, execution reaches here.
768
808
  // Return named return vars so deferred mutations are visible to the caller.
769
809
  if (this.namedReturnVars?.length > 0) {
770
810
  const vars = this.namedReturnVars;
@@ -773,7 +813,12 @@ export class CodeGen {
773
813
  ? `return ${vars[0]};`
774
814
  : `return [${vars.join(", ")}];`,
775
815
  );
816
+ } else if (this._currentDecl?.returnType) {
817
+ this.line(
818
+ `return ${this.zeroValueForTypeNode(this._currentDecl.returnType)};`,
819
+ );
776
820
  }
821
+ this._recoverOkInScope = prevRecoverOk;
777
822
  }
778
823
 
779
824
  // Scan AST node for _addressTaken idents on scalars and populate _boxedVars.
@@ -55,6 +55,13 @@ var __ifp = __ifp || function(x) {
55
55
  return x;
56
56
  };`;
57
57
 
58
+ // Nil-interface method call guard (strict mode only): matches Go's panic
59
+ // message instead of a JS TypeError.
60
+ export const HELPER_NIL_CALL = `var __nilcall = __nilcall || function(x) {
61
+ if (x == null) throw new Error("runtime error: invalid memory address or nil pointer dereference");
62
+ return x;
63
+ };`;
64
+
58
65
  export const HELPER_EQUAL = `var __equal = __equal || function __equal(a, b) {
59
66
  if (a === b) return true;
60
67
  if (a === null || b === null) return false;
@@ -86,6 +93,35 @@ export const HELPER_CDIV = `var __cdiv = __cdiv || function(a, b) {
86
93
  return { re: (a.re * b.re + a.im * b.im) / d, im: (a.im * b.re - a.re * b.im) / d };
87
94
  };`;
88
95
 
96
+ // Panic/defer runtime. \`stack\` holds the in-flight panic of every frame
97
+ // currently running its defers; \`armed\` is set right before a deferred
98
+ // function is invoked and consumed by that function's prologue, so recover()
99
+ // only takes effect when called directly by the deferred function (Go spec).
100
+ export const HELPER_PANIC = `var __gopanic = __gopanic || { stack: [], armed: false };
101
+ var __runDefers = __runDefers || function(defers, frame) {
102
+ for (let i = defers.length - 1; i >= 0; i--) {
103
+ __gopanic.armed = true;
104
+ try { defers[i](); }
105
+ catch (e) {
106
+ if (frame.pn !== null) __gopanic.stack.pop();
107
+ frame.pn = { err: e, recovered: false };
108
+ __gopanic.stack.push(frame.pn);
109
+ } finally { __gopanic.armed = false; }
110
+ }
111
+ if (frame.pn !== null) {
112
+ __gopanic.stack.pop();
113
+ if (!frame.pn.recovered) throw frame.pn.err;
114
+ }
115
+ };
116
+ var __recover = __recover || function(ok) {
117
+ if (!ok) return null;
118
+ const s = __gopanic.stack;
119
+ const top = s.length > 0 ? s[s.length - 1] : null;
120
+ if (top === null || top.recovered) return null;
121
+ top.recovered = true;
122
+ return top.err?.message ?? String(top.err);
123
+ };`;
124
+
89
125
  export const HELPER_SPRINTF = `var __sprintf = __sprintf || function(f, ...a) {
90
126
  let i = 0;
91
127
  return f.replace(/%([#+\\- 0]*)([0-9]*)\\.?([0-9]*)[sdvftxXqobeEgGw%]/g, (m) => {
@@ -150,6 +186,72 @@ export const HELPER_ERROR_IS = `var __errorIs = __errorIs || function(err, targe
150
186
  return false;
151
187
  };`;
152
188
 
189
+ // Go-semantics strconv parsers shared by the JS backend and the wasm runtime
190
+ // imports (boundary.js), so both targets agree. Each parser returns
191
+ // [value, errorMessage | null]; integer values are BigInt (the wasm side
192
+ // needs i64, the JS side converts with Number()).
193
+ export const HELPER_STRCONV = `var __strconv_atoi = __strconv_atoi || function(s) {
194
+ if (!/^[+-]?[0-9]+$/.test(s)) return [0n, 'strconv.Atoi: parsing "' + s + '": invalid syntax'];
195
+ const v = BigInt(s);
196
+ if (v > 9223372036854775807n || v < -9223372036854775808n) {
197
+ return [v > 0n ? 9223372036854775807n : -9223372036854775808n, 'strconv.Atoi: parsing "' + s + '": value out of range'];
198
+ }
199
+ return [v, null];
200
+ };
201
+ var __strconv_parse_int = __strconv_parse_int || function(s, base, bits) {
202
+ let str = s, neg = false;
203
+ if (str[0] === "-" || str[0] === "+") { neg = str[0] === "-"; str = str.slice(1); }
204
+ if (base === 0) {
205
+ if (/^0[xX]/.test(str)) { base = 16; str = str.slice(2); }
206
+ else if (/^0[bB]/.test(str)) { base = 2; str = str.slice(2); }
207
+ else if (/^0[oO]/.test(str)) { base = 8; str = str.slice(2); }
208
+ else if (/^0[0-9]/.test(str)) { base = 8; str = str.slice(1); }
209
+ else base = 10;
210
+ str = str.replaceAll("_", "");
211
+ }
212
+ if (base < 2 || base > 36) return [0n, 'strconv.ParseInt: parsing "' + s + '": invalid base ' + base];
213
+ const digits = "0123456789abcdefghijklmnopqrstuvwxyz".slice(0, base);
214
+ if (str === "" || [...str.toLowerCase()].some((c) => !digits.includes(c))) {
215
+ return [0n, 'strconv.ParseInt: parsing "' + s + '": invalid syntax'];
216
+ }
217
+ let v = 0n; const B = BigInt(base);
218
+ for (const c of str.toLowerCase()) v = v * B + BigInt(digits.indexOf(c));
219
+ if (neg) v = -v;
220
+ const w = bits === 0 ? 64 : bits;
221
+ const max = (1n << BigInt(w - 1)) - 1n, min = -(1n << BigInt(w - 1));
222
+ if (v > max || v < min) return [v > max ? max : min, 'strconv.ParseInt: parsing "' + s + '": value out of range'];
223
+ return [v, null];
224
+ };
225
+ var __strconv_parse_float = __strconv_parse_float || function(s) {
226
+ const str = s.replaceAll("_", "");
227
+ if (/^[+-]?(inf|infinity)$/i.test(str)) return [str[0] === "-" ? -Infinity : Infinity, null];
228
+ if (/^[+-]?nan$/i.test(str)) return [NaN, null];
229
+ if (str === "" || !/^[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?$|^[+-]?0[xX][0-9a-fA-F]+$/.test(str)) {
230
+ return [0, 'strconv.ParseFloat: parsing "' + s + '": invalid syntax'];
231
+ }
232
+ return [Number(str), null];
233
+ };
234
+ var __strconv_parse_bool = __strconv_parse_bool || function(s) {
235
+ if (["1", "t", "T", "TRUE", "true", "True"].includes(s)) return [true, null];
236
+ if (["0", "f", "F", "FALSE", "false", "False"].includes(s)) return [false, null];
237
+ return [false, 'strconv.ParseBool: parsing "' + s + '": invalid syntax'];
238
+ };
239
+ var __strconv_format_float = __strconv_format_float || function(f, fmtc, prec) {
240
+ const c = String.fromCharCode(fmtc);
241
+ if (!Number.isFinite(f)) return f !== f ? "NaN" : (f > 0 ? "+Inf" : "-Inf");
242
+ if (c === "f") return prec < 0 ? String(f) : f.toFixed(prec);
243
+ if (c === "e" || c === "E") {
244
+ const r = prec < 0 ? f.toExponential() : f.toExponential(prec);
245
+ const o = r.replace(/e([+-])(\\d)$/, "e$10$2");
246
+ return c === "E" ? o.toUpperCase() : o;
247
+ }
248
+ if (c === "g" || c === "G") {
249
+ const r = prec < 0 ? String(f) : f.toPrecision(prec);
250
+ return c === "G" ? r.toUpperCase() : r;
251
+ }
252
+ return String(f);
253
+ };`;
254
+
153
255
  export const HELPER_PATH_CLEAN = `var __pathClean = __pathClean || function(p) {
154
256
  if (!p) return ".";
155
257
  const abs = p.startsWith("/");
@@ -57,3 +57,35 @@ export function buildSourceMap(sources, mappings, sourcesContent) {
57
57
  if (sourcesContent) map.sourcesContent = sourcesContent;
58
58
  return JSON.stringify(map);
59
59
  }
60
+
61
+ // WASM source maps use the convention "generated line 0, generated column =
62
+ // byte offset into the module". `mappings` is Array<{ offset, srcFileIdx,
63
+ // srcLine }> (0-based line), one segment per function body (function-level
64
+ // granularity); DevTools shows the Go file/line for each wasm function.
65
+ export function buildWasmSourceMap(sources, mappings, sourcesContent) {
66
+ const sorted = [...mappings].sort((a, b) => a.offset - b.offset);
67
+ const segments = [];
68
+ let prevOffset = 0;
69
+ let prevSrcFile = 0;
70
+ let prevSrcLine = 0;
71
+ for (const m of sorted) {
72
+ const fileIdx = m.srcFileIdx ?? 0;
73
+ segments.push(
74
+ vlqEncode(m.offset - prevOffset) +
75
+ vlqEncode(fileIdx - prevSrcFile) +
76
+ vlqEncode(m.srcLine - prevSrcLine) +
77
+ vlqEncode(0),
78
+ );
79
+ prevOffset = m.offset;
80
+ prevSrcFile = fileIdx;
81
+ prevSrcLine = m.srcLine;
82
+ }
83
+ const map = {
84
+ version: 3,
85
+ sources: sources.map((s) => s.replace(/\\/g, "/")),
86
+ names: [],
87
+ mappings: segments.join(","),
88
+ };
89
+ if (sourcesContent) map.sourcesContent = sourcesContent;
90
+ return JSON.stringify(map);
91
+ }