gofront 1.6.1 → 1.8.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 (132) hide show
  1. package/README.md +45 -49
  2. package/package.json +12 -8
  3. package/src/backend/js/expressions.js +1627 -148
  4. package/src/backend/js/fmt-runtime.js +573 -0
  5. package/src/backend/js/index.js +508 -102
  6. package/src/backend/js/runtime.js +1997 -106
  7. package/src/backend/js/statements.js +931 -87
  8. package/src/backend/js/stdlib/bits.js +87 -14
  9. package/src/backend/js/stdlib/builder.js +41 -6
  10. package/src/backend/js/stdlib/bytes.js +253 -7
  11. package/src/backend/js/stdlib/errors.js +7 -1
  12. package/src/backend/js/stdlib/fmt.js +349 -10
  13. package/src/backend/js/stdlib/index.js +19 -2
  14. package/src/backend/js/stdlib/json.js +1167 -0
  15. package/src/backend/js/stdlib/maps.js +0 -8
  16. package/src/backend/js/stdlib/math.js +17 -1
  17. package/src/backend/js/stdlib/simd.js +34 -0
  18. package/src/backend/js/stdlib/slices.js +36 -6
  19. package/src/backend/js/stdlib/strconv.js +10 -5
  20. package/src/backend/js/stdlib/strings.js +83 -4
  21. package/src/backend/js/stdlib/testing.js +7 -0
  22. package/src/backend/js/stdlib/time.js +79 -55
  23. package/src/backend/js/stdlib/unicode.js +1 -1
  24. package/src/backend/js/stdlib/utf8.js +1 -1
  25. package/src/backend/js/templ.js +38 -76
  26. package/src/backend/js/type-info.js +272 -0
  27. package/src/backend/shared/fmt-types.js +188 -0
  28. package/src/backend/wasm/boundary.js +446 -72
  29. package/src/backend/wasm/emit-builtins.js +320 -13
  30. package/src/backend/wasm/emit-exprs.js +1404 -199
  31. package/src/backend/wasm/emit-maps.js +61 -13
  32. package/src/backend/wasm/emit-stmts.js +1359 -164
  33. package/src/backend/wasm/emit.js +518 -74
  34. package/src/backend/wasm/encode.js +128 -13
  35. package/src/backend/wasm/globals.js +452 -0
  36. package/src/backend/wasm/index.js +194 -2489
  37. package/src/backend/wasm/method-expr.js +381 -0
  38. package/src/backend/wasm/module-emitter.js +2240 -0
  39. package/src/backend/wasm/monomorph.js +62 -4
  40. package/src/backend/wasm/optimize.js +4 -2
  41. package/src/backend/wasm/prescan.js +397 -0
  42. package/src/backend/wasm/stdlib/bytes.js +1804 -127
  43. package/src/backend/wasm/stdlib/core.js +1574 -199
  44. package/src/backend/wasm/stdlib/index.js +3 -5
  45. package/src/backend/wasm/stdlib/io.js +1567 -150
  46. package/src/backend/wasm/stdlib/packages.js +2 -0
  47. package/src/backend/wasm/stdlib/simd.js +499 -0
  48. package/src/backend/wasm/stdlib/time.js +1041 -12
  49. package/src/backend/wasm/types.js +221 -15
  50. package/src/backend/wasm/variadic.js +93 -0
  51. package/src/backend/wasm/wat.js +84 -20
  52. package/src/{cli-core.js → cli/cli-core.js} +236 -10
  53. package/src/{test-runner.js → cli/test-runner.js} +332 -39
  54. package/src/{vendor.js → cli/vendor.js} +89 -15
  55. package/src/compiler.js +107 -269
  56. package/src/dts-parser.js +71 -14
  57. package/src/index.js +15 -169
  58. package/src/{lexer.js → lexer/lexer.js} +102 -55
  59. package/src/{tokens.js → lexer/tokens.js} +1 -0
  60. package/src/link/sourcemap.js +97 -0
  61. package/src/link/stdlib.js +515 -0
  62. package/src/link/wasm.js +294 -0
  63. package/src/lower/bounds.js +394 -0
  64. package/src/lower/boxing.js +1 -6
  65. package/src/lower/captures.js +176 -32
  66. package/src/lower/embedding.js +2 -1
  67. package/src/lower/index.js +26 -18
  68. package/src/lower/json-codecs.js +1398 -0
  69. package/src/lower/ownership.js +22 -1
  70. package/src/lower/range.js +86 -1
  71. package/src/lower/reachability.js +132 -0
  72. package/src/parser/declarations.js +58 -13
  73. package/src/parser/expressions.js +29 -7
  74. package/src/parser/index.js +2 -1
  75. package/src/parser/statements.js +37 -22
  76. package/src/parser/struct-tag.js +144 -0
  77. package/src/{templ-parser.js → parser/templ.js} +138 -15
  78. package/src/parser/types.js +130 -13
  79. package/src/resolver.js +22 -1
  80. package/src/typechecker/assignability.js +215 -11
  81. package/src/typechecker/const-fold.js +356 -0
  82. package/src/typechecker/constants.js +256 -0
  83. package/src/typechecker/expressions.js +960 -99
  84. package/src/typechecker/index.js +491 -72
  85. package/src/typechecker/init-order.js +330 -0
  86. package/src/typechecker/resolve.js +596 -52
  87. package/src/typechecker/statements.js +157 -17
  88. package/src/typechecker/stdlib/core.js +333 -55
  89. package/src/typechecker/stdlib/excluded.js +898 -0
  90. package/src/typechecker/stdlib/extended.js +359 -26
  91. package/src/typechecker/{stdlib.js → stdlib/index.js} +6 -4
  92. package/src/typechecker/stdlib/simd.js +123 -0
  93. package/src/typechecker/termination.js +1 -1
  94. package/src/typechecker/types.js +165 -26
  95. package/stdlib/bytes/iter.go +192 -0
  96. package/stdlib/bytes/search.go +240 -0
  97. package/stdlib/bytes/split.go +218 -0
  98. package/stdlib/cmp/cmp.go +65 -0
  99. package/stdlib/encoding/json/errors.go +31 -0
  100. package/stdlib/encoding/json/indent.go +71 -0
  101. package/stdlib/encoding/json/number.go +12 -0
  102. package/stdlib/encoding/json/stream.go +173 -0
  103. package/stdlib/errors/overlay.go +12 -0
  104. package/stdlib/hash/hash.go +53 -0
  105. package/stdlib/internal/jsonrt/dec.go +738 -0
  106. package/stdlib/internal/jsonrt/enc.go +366 -0
  107. package/stdlib/internal/jsonrt/indent.go +135 -0
  108. package/stdlib/internal/jsonrt/number.go +27 -0
  109. package/stdlib/internal/jsonrt/reader.go +73 -0
  110. package/stdlib/internal/jsonrt/scanner.go +649 -0
  111. package/stdlib/io/io.go +346 -0
  112. package/stdlib/iter/iter.go +18 -0
  113. package/stdlib/maps/iter.go +65 -0
  114. package/stdlib/math/extra.go +270 -0
  115. package/stdlib/slices/extra.go +73 -0
  116. package/stdlib/slices/iter.go +113 -0
  117. package/stdlib/sort/sort.go +561 -0
  118. package/stdlib/strconv/quote.go +341 -0
  119. package/stdlib/strings/extra.go +35 -0
  120. package/stdlib/strings/iter.go +122 -0
  121. package/stdlib/sync/extra.go +105 -0
  122. package/stdlib/unicode/unicode.go +112 -0
  123. package/stdlib/unicode/utf8/bytes.go +239 -0
  124. package/src/backend/wasm/const-fold.js +0 -205
  125. package/src/backend/wasm/emit-stdlib.js +0 -13
  126. /package/src/{asset-manager.js → cli/asset-manager.js} +0 -0
  127. /package/src/{colors.js → cli/colors.js} +0 -0
  128. /package/src/{dev-server.js → cli/dev-server.js} +0 -0
  129. /package/src/{minifier.js → cli/minifier.js} +0 -0
  130. /package/src/{project-config.js → cli/project-config.js} +0 -0
  131. /package/src/{pwa.js → cli/pwa.js} +0 -0
  132. /package/src/{templ-lexer.js → lexer/templ-lexer.js} +0 -0
@@ -21,15 +21,22 @@ import {
21
21
  computeEmbeddedStubs,
22
22
  hasDirectRecover,
23
23
  isReferenceType,
24
+ markInBoundsIndexes,
24
25
  normalizeDefers,
25
26
  scanAddressTaken,
26
27
  } from "../../lower/index.js";
27
- import { isComplex, isNumeric } from "../../typechecker/types.js";
28
- import { expressionGenMethods } from "./expressions.js";
28
+ import { orderPackageInits } from "../../typechecker/init-order.js";
29
+ import {
30
+ BROWSER_GLOBALS,
31
+ isComplex,
32
+ isNumeric,
33
+ } from "../../typechecker/types.js";
34
+ import { comparesToNil, expressionGenMethods } from "./expressions.js";
29
35
  import {
30
36
  HELPER_APPEND,
31
37
  HELPER_BASE64,
32
38
  HELPER_BINARY,
39
+ HELPER_BOUNDS,
33
40
  HELPER_CDIV,
34
41
  HELPER_CMUL,
35
42
  HELPER_CRYPTO_RAND,
@@ -39,31 +46,49 @@ import {
39
46
  HELPER_ERROR_AS,
40
47
  HELPER_ERROR_IS,
41
48
  HELPER_ERROR_JOIN,
49
+ HELPER_GENERICS,
50
+ HELPER_GEQ,
42
51
  HELPER_HEX,
43
52
  HELPER_IFACE_BOX,
53
+ HELPER_IFD,
54
+ HELPER_IFEQ,
44
55
  HELPER_INJECT_STYLES,
45
56
  HELPER_IO_EOF,
57
+ HELPER_JSON,
46
58
  HELPER_LEN,
47
59
  HELPER_NIL_CALL,
60
+ HELPER_NIL_MAP,
61
+ HELPER_NM,
62
+ HELPER_OS,
48
63
  HELPER_PANIC,
49
64
  HELPER_PATH_CLEAN,
50
65
  HELPER_PATH_ERROR,
66
+ HELPER_PTREQ,
51
67
  HELPER_S,
52
68
  HELPER_SCLONE,
53
69
  HELPER_SHA256,
70
+ HELPER_SHIFT,
71
+ HELPER_SIMD,
72
+ HELPER_SLICE,
54
73
  HELPER_SORT_SLICE,
74
+ HELPER_SPRINT,
55
75
  HELPER_SPRINTF,
56
76
  HELPER_STRCONV,
57
77
  HELPER_STRCONV_ERROR,
78
+ HELPER_STRUCT_KEY,
79
+ HELPER_SV,
58
80
  HELPER_SYNC,
59
81
  HELPER_TESTING,
60
82
  HELPER_TIME_FMT,
61
83
  HELPER_TIME_PARSE,
84
+ HELPER_VA,
85
+ HELPER_VCLONE,
62
86
  } from "./runtime.js";
63
87
  import { buildSourceMap, buildWasmSourceMap } from "./source-map.js";
64
88
  import { statementGenMethods } from "./statements.js";
65
89
  import { stdlibGenMethods } from "./stdlib/index.js";
66
90
  import { templGenMethods } from "./templ.js";
91
+ import { typeInfoGenMethods } from "./type-info.js";
67
92
 
68
93
  export { buildSourceMap, buildWasmSourceMap };
69
94
 
@@ -107,20 +132,41 @@ const JS_RESERVED = new Set([
107
132
  const jsSafeName = (n) =>
108
133
  typeof n === "string" && JS_RESERVED.has(n) ? `${n}$` : n;
109
134
 
135
+ // Go declarations may shadow browser globals (Task 0.9). The JS bundle is one
136
+ // flat module scope, so such a binding gets a `$` suffix: generated code and
137
+ // other packages keep reaching the real global (`Array.from`, `Object.keys`).
138
+ const jsBindingName = (n) =>
139
+ typeof n === "string" && BROWSER_GLOBALS.has(n) ? `${n}$` : jsSafeName(n);
140
+
110
141
  // Runtime helpers by registry name, in output order. Mark one as needed with
111
142
  // `this.useHelper(name)`; `_prependHelpers` emits every marked entry.
112
143
  const HELPER_MAP = [
113
144
  ["len", HELPER_LEN],
145
+ ["sv", HELPER_SV],
146
+ ["slice", HELPER_SLICE],
114
147
  ["append", HELPER_APPEND],
115
148
  ["sliceGuard", HELPER_S],
116
149
  ["sclone", HELPER_SCLONE],
150
+ ["va", HELPER_VA],
151
+ ["vclone", HELPER_VCLONE],
117
152
  ["ifaceBox", HELPER_IFACE_BOX],
153
+ ["ifd", HELPER_IFD],
118
154
  ["equal", HELPER_EQUAL],
155
+ ["geq", HELPER_GEQ],
156
+ ["ptreq", HELPER_PTREQ],
157
+ ["ifeq", HELPER_IFEQ],
119
158
  ["cmul", HELPER_CMUL],
120
159
  ["cdiv", HELPER_CDIV],
121
160
  ["panicRt", HELPER_PANIC],
161
+ ["shift", HELPER_SHIFT],
122
162
  ["nilCall", HELPER_NIL_CALL],
163
+ ["bounds", HELPER_BOUNDS],
164
+ ["nilMap", HELPER_NIL_MAP],
165
+ ["nm", HELPER_NM],
166
+ ["structKey", HELPER_STRUCT_KEY],
167
+ ["generics", HELPER_GENERICS],
123
168
  ["sprintf", HELPER_SPRINTF],
169
+ ["sprint", HELPER_SPRINT],
124
170
  ["error", HELPER_ERROR],
125
171
  ["errorIs", HELPER_ERROR_IS],
126
172
  ["errorAs", HELPER_ERROR_AS],
@@ -128,6 +174,7 @@ const HELPER_MAP = [
128
174
  ["strconvError", HELPER_STRCONV_ERROR],
129
175
  ["pathError", HELPER_PATH_ERROR],
130
176
  ["ioEOF", HELPER_IO_EOF],
177
+ ["os", HELPER_OS],
131
178
  ["strconv", HELPER_STRCONV],
132
179
  ["pathClean", HELPER_PATH_CLEAN],
133
180
  ["sortSlice", HELPER_SORT_SLICE],
@@ -141,8 +188,14 @@ const HELPER_MAP = [
141
188
  ["binary", HELPER_BINARY],
142
189
  ["hex", HELPER_HEX],
143
190
  ["base64", HELPER_BASE64],
191
+ ["json", HELPER_JSON],
192
+ ["simd", HELPER_SIMD],
144
193
  ];
145
194
 
195
+ // JS bodies of `//gofront:intrinsic` functions (names: INTRINSICS in
196
+ // typechecker/types.js). The WASM backend keeps their empty body.
197
+ const JS_INTRINSICS = {};
198
+
146
199
  export class CodeGen {
147
200
  // jsImports: Map<importPath, string[]> — npm package imports to emit at top of file
148
201
  // bundledPackages: Set<string> — GoFront package names bundled inline;
@@ -162,12 +215,18 @@ export class CodeGen {
162
215
  this.structFields = new Map();
163
216
  this.namedWrapperNames = new Set();
164
217
  this.collectedCss = [];
218
+ this.warnings = []; // non-fatal diagnostics; the compiler prints them
219
+ this._precisionWarned = new Set(); // nodes already given a precision warning
220
+ this._srcFileNames = []; // source file name per _srcFileIdx
165
221
  this.jsImports = jsImports;
166
222
  this.bundledPackages = bundledPackages;
167
223
  this.namedReturnVars = null; // names of current function's named return vars
168
224
  this._srcMappings = []; // { genLine, srcLine, srcFileIdx } for source map
169
225
  this._currentSrcFileIdx = 0; // updated as each top-level decl is generated
170
226
  this._boxedVars = new Set(); // address-taken scalar variables that need boxing
227
+ this._jsonCodecs = new Map(); // memoized JSON encoders and decoders
228
+ this._localTypeNames = new Set(); // function-local type declarations
229
+ this._jsonAnyUsed = false; // program calls json.Marshal / Encoder.Encode
171
230
  // Runtime helper usage tracking — only emit helpers that are actually used
172
231
  this._helpers = new Set();
173
232
  // Per-function context for Go value semantics (see _withFnCtx)
@@ -214,6 +273,9 @@ export class CodeGen {
214
273
  }
215
274
 
216
275
  _emitTypeDecls(program, methods) {
276
+ this._typeDeclsByName = new Map();
277
+ for (const d of program.decls)
278
+ if (d.kind === "TypeDecl") this._typeDeclsByName.set(d.name, d);
217
279
  for (const d of program.decls) {
218
280
  if (d.kind === "TypeDecl") {
219
281
  this._currentSrcFileIdx = d._srcFileIdx ?? 0;
@@ -224,20 +286,28 @@ export class CodeGen {
224
286
  }
225
287
 
226
288
  _emitVarConstDecls(program) {
227
- for (const d of program.decls) {
228
- if (d.kind === "ConstDecl") {
229
- this._currentSrcFileIdx = d._srcFileIdx ?? 0;
230
- this.genConstDecl(d);
231
- this.blank();
289
+ const { consts, vars } = orderPackageInits([program]);
290
+ const emit = (decls, gen, ordered) => {
291
+ const natural = decls.flatMap((d) => d.decls ?? []);
292
+ const inOrder = ordered.every((e, i) => e.spec === natural[i]);
293
+ if (inOrder) {
294
+ for (const d of decls) {
295
+ this._currentSrcFileIdx = d._srcFileIdx ?? 0;
296
+ gen.call(this, d);
297
+ this.blank();
298
+ }
299
+ return;
232
300
  }
233
- }
234
- for (const d of program.decls) {
235
- if (d.kind === "VarDecl") {
236
- this._currentSrcFileIdx = d._srcFileIdx ?? 0;
237
- this.genVarDecl(d);
301
+ // Initialisers depend on declarations that come later (Go allows it).
302
+ for (const e of ordered) {
303
+ this._currentSrcFileIdx = e.decl._srcFileIdx ?? 0;
304
+ gen.call(this, { ...e.decl, decls: [e.spec] });
238
305
  this.blank();
239
306
  }
240
- }
307
+ };
308
+ const declsOf = (kind) => program.decls.filter((d) => d.kind === kind);
309
+ emit(declsOf("ConstDecl"), this.genConstDecl, consts);
310
+ emit(declsOf("VarDecl"), this.genVarDecl, vars);
241
311
  }
242
312
 
243
313
  _callInitAndMain(initNames, program, isTest = false) {
@@ -255,24 +325,34 @@ export class CodeGen {
255
325
 
256
326
  generate(program, options = {}) {
257
327
  const isTest = options.isTest ?? false;
328
+ this._precisionWarned = new Set();
258
329
  // Defer lowering (argument/receiver hoisting, recover-target marking) is
259
330
  // idempotent and owned by the backends: the wasm path runs it via
260
331
  // `lower()`, the JS path here.
261
332
  normalizeDefers(program);
333
+ // Strict mode: index checks the loop shape proves unnecessary are elided.
334
+ if (this.strict) markInBoundsIndexes(program);
335
+ if (program._filename) this._srcFileNames = [program._filename];
262
336
  // Ensure _pkgName is set on all decls (generateAll sets it for
263
337
  // multi-file; for single-file compiles we derive it from the AST).
264
338
  const pkgName = program.pkg?.name;
265
339
  if (pkgName) {
340
+ this.pkgName = pkgName;
266
341
  for (const d of program.decls) {
267
342
  if (!d._pkgName) d._pkgName = pkgName;
268
343
  }
269
344
  }
270
345
  this._renameReservedIdents(program.decls);
346
+ this._boxedGlobals = this._collectBoxedGlobals(program);
347
+ this._boxedVars = new Set(this._boxedGlobals);
348
+ this._collectResliceNames(program);
349
+ this._jsonAnyUsed = this._usesJsonAnyMarshal(program.decls);
271
350
  const methods = this._collectDecls(program);
272
351
  this._emitJsImports();
273
352
  this._emitTypeDecls(program, methods);
274
353
  this._emitVarConstDecls(program);
275
354
  const initNames = this._emitFuncDecls(program);
355
+ this._emitJsonCodecs();
276
356
  this._callInitAndMain(initNames, program, isTest);
277
357
  const prependedLines = this._prependHelpers(isTest);
278
358
  return this._finalizeOutput(prependedLines);
@@ -323,12 +403,12 @@ export class CodeGen {
323
403
  }
324
404
 
325
405
  _renameParams(params) {
326
- for (const p of params ?? []) p.name = jsSafeName(p.name);
406
+ for (const p of params ?? []) p.name = jsBindingName(p.name);
327
407
  }
328
408
 
329
409
  _renameNamedReturns(node) {
330
410
  for (const r of node.returnType?._namedReturns ?? [])
331
- r.name = jsSafeName(r.name);
411
+ r.name = jsBindingName(r.name);
332
412
  }
333
413
 
334
414
  _renameSignature(node) {
@@ -337,27 +417,37 @@ export class CodeGen {
337
417
  }
338
418
 
339
419
  _renameIdentNode(node) {
340
- if (!node._isStructKey && node._type?.kind !== "builtin")
341
- node.name = jsSafeName(node.name);
420
+ if (node._isStructKey || node._type?.kind === "builtin") return;
421
+ node.name = node._shadowsGlobal
422
+ ? jsBindingName(node.name)
423
+ : jsSafeName(node.name);
342
424
  }
343
425
 
344
426
  _renameFuncDeclNode(node) {
345
- node.name = jsSafeName(node.name);
427
+ node.name = jsBindingName(node.name);
346
428
  this._renameSignature(node);
347
429
  }
348
430
 
349
431
  _renameMethodDeclNode(node) {
350
- node.recvName = jsSafeName(node.recvName);
432
+ node.recvName = jsBindingName(node.recvName);
351
433
  this._renameSignature(node);
352
434
  }
353
435
 
354
436
  _renameDeclSpecs(node) {
355
437
  for (const spec of node.decls ?? [])
356
- spec.names = spec.names.map(jsSafeName);
438
+ spec.names = spec.names.map(jsBindingName);
439
+ }
440
+
441
+ // `document := 1` declares a binding that shadows the browser global. A
442
+ // `:=` redeclaration (`x, document := 2, 3`) always targets a local of the
443
+ // same scope, which carries the same suffix.
444
+ _renameDefineNode(node) {
445
+ for (const l of node.lhs ?? [])
446
+ if (l.kind === "Ident") l.name = jsBindingName(l.name);
357
447
  }
358
448
 
359
449
  _renameTypeSwitchNode(node) {
360
- node.assign = jsSafeName(node.assign);
450
+ node.assign = jsBindingName(node.assign);
361
451
  }
362
452
 
363
453
  _markStructKeys(node) {
@@ -376,7 +466,15 @@ export class CodeGen {
376
466
  MethodDecl: "_renameMethodDeclNode",
377
467
  FuncLit: "_renameSignature",
378
468
  TemplDecl: "_renameSignature",
469
+ TemplExpr: "_renameTemplExprNode",
470
+ TemplComponent: "_renameTemplExprNode",
471
+ TemplIf: "_renameTemplExprNode",
472
+ TemplSwitch: "_renameTemplExprNode",
473
+ TemplCase: "_renameTemplExprNode",
474
+ TemplFor: "_renameTemplForNode",
475
+ TemplElement: "_renameTemplElementNode",
379
476
  CssDecl: "_renameSignature",
477
+ DefineStmt: "_renameDefineNode",
380
478
  VarDecl: "_renameDeclSpecs",
381
479
  ConstDecl: "_renameDeclSpecs",
382
480
  TypeSwitchStmt: "_renameTypeSwitchNode",
@@ -402,6 +500,18 @@ export class CodeGen {
402
500
  // Collects struct names, method map, and named wrapper names from program decls.
403
501
  // Returns the method map (typeName → MethodDecl[]).
404
502
  _collectStructNames(program) {
503
+ // Go-compiled variadic functions, methods and templ components: a
504
+ // `f(xs...)` call to one passes the slice itself (see `_genCallArgs`,
505
+ // `_emitParamCopies`).
506
+ this.variadicFns = new Set();
507
+ for (const d of program.decls) {
508
+ const last = d.params?.[d.params.length - 1];
509
+ if (!last?.variadic) continue;
510
+ if (d.kind === "FuncDecl" || d.kind === "TemplDecl")
511
+ this.variadicFns.add(d.name);
512
+ else if (d.kind === "MethodDecl")
513
+ this.variadicFns.add(`${d.recvType.name}.${d.name}`);
514
+ }
405
515
  for (const d of program.decls) {
406
516
  if (d.kind === "TypeDecl" && d.type.kind === "StructType")
407
517
  this.structNames.add(d.name);
@@ -419,11 +529,14 @@ export class CodeGen {
419
529
 
420
530
  _collectMethodMap(program) {
421
531
  const methods = new Map();
532
+ this.nilSafeMethods = new Set();
422
533
  for (const d of program.decls) {
423
534
  if (d.kind === "MethodDecl") {
424
535
  const name = d.recvType.name;
425
536
  if (!methods.has(name)) methods.set(name, []);
426
537
  methods.get(name).push(d);
538
+ if (d.recvPointer && comparesToNil(d.body, d.recvName))
539
+ this.nilSafeMethods.add(`${name}.${d.name}`);
427
540
  }
428
541
  }
429
542
  return methods;
@@ -452,15 +565,28 @@ export class CodeGen {
452
565
  /** Marks a runtime helper (key of HELPER_MAP) as needed by the output. */
453
566
  useHelper(name) {
454
567
  this._helpers.add(name);
568
+ if (name === "ifeq" || name === "geq") this._helpers.add("equal");
569
+ if (name === "geq") this._helpers.add("ifeq");
455
570
  if (
456
571
  name === "strconvError" ||
457
572
  name === "pathError" ||
458
573
  name === "ioEOF" ||
459
574
  name === "base64" ||
460
- name === "hex"
575
+ name === "hex" ||
576
+ name === "json"
461
577
  ) {
462
578
  this._helpers.add("error");
463
579
  }
580
+ if (name === "json") {
581
+ this.useHelper("base64");
582
+ }
583
+ if (name === "os") {
584
+ this.useHelper("ioEOF");
585
+ this._helpers.add("error");
586
+ }
587
+ if (name === "append") this.useHelper("slice");
588
+ if (name === "sprint") this._helpers.add("sprintf");
589
+ if (name === "slice") this._helpers.add("sv");
464
590
  if (name === "binary") {
465
591
  this.useHelper("ioEOF");
466
592
  this._helpers.add("error");
@@ -489,16 +615,29 @@ export class CodeGen {
489
615
  // Generate a single bundle from multiple programs (same-package multi-file).
490
616
  // Annotates each decl with its source file index before merging.
491
617
  generateAll(programs, options = {}) {
618
+ this._srcFileNames = programs.map((p) => p._filename ?? null);
492
619
  for (let i = 0; i < programs.length; i++) {
493
620
  for (const decl of programs[i].decls) {
494
621
  decl._srcFileIdx = i;
495
622
  decl._pkgName = programs[i].pkg?.name;
496
623
  }
497
624
  }
498
- const merged = { decls: programs.flatMap((p) => p.decls) };
625
+ const merged = {
626
+ pkg: programs[0]?.pkg,
627
+ decls: programs.flatMap((p) => p.decls),
628
+ };
499
629
  return this.generate(merged, options);
500
630
  }
501
631
 
632
+ // Records a non-fatal diagnostic at `node` ("main.go at line 3:5: msg").
633
+ _warn(msg, node) {
634
+ const file = this._srcFileNames[this._currentSrcFileIdx ?? 0];
635
+ const line = node?.line ?? node?._line;
636
+ const at = line ? `at line ${line}:${node.col ?? node._col ?? 1}` : "";
637
+ const loc = [file, at].filter(Boolean).join(" ");
638
+ this.warnings.push(loc ? `${loc}: ${msg}` : msg);
639
+ }
640
+
502
641
  getCss() {
503
642
  return [...new Set(this.collectedCss)].join("\n\n");
504
643
  }
@@ -517,14 +656,40 @@ export class CodeGen {
517
656
 
518
657
  // ── Type declarations ────────────────────────────────────────
519
658
 
659
+ // `type S Base` with Base a struct declared in this package: the struct
660
+ // AST S inherits its fields from, or null.
661
+ _definedStructAst(decl) {
662
+ if (decl.isAlias || decl.typeParams) return null;
663
+ if (this.checker?.types.get(decl.name)?.underlying?.kind !== "struct")
664
+ return null;
665
+ let t = decl.type;
666
+ for (let depth = 0; depth < 16 && t; depth++) {
667
+ if (t.kind === "StructType") return t;
668
+ if (t.kind !== "TypeName" && t.kind !== "Ident") return null;
669
+ const next = this._typeDeclsByName?.get(t.name);
670
+ if (!next || next.isAlias) return null;
671
+ t = next.type;
672
+ }
673
+ return null;
674
+ }
675
+
520
676
  genTypeDeclWithMethods(decl, methodDecls) {
677
+ const definedStruct =
678
+ decl.type.kind === "StructType" ? null : this._definedStructAst(decl);
521
679
  if (decl.type.kind === "StructType") {
522
680
  this.genStruct(decl.name, decl.type, methodDecls);
681
+ } else if (definedStruct) {
682
+ this.genStruct(decl.name, definedStruct, methodDecls);
523
683
  } else if (decl.type.kind === "InterfaceType") {
524
684
  // Interfaces are compile-time only — no JS output needed.
525
685
  this.line(`// interface ${decl.name} (compile-time only)`);
526
- } else if (methodDecls.length > 0) {
686
+ } else if (
687
+ methodDecls.length > 0 ||
688
+ this._isPlainNamedBox(this.checker?.types.get(decl.name))
689
+ ) {
527
690
  // Named non-struct type with methods — emit an ES6 wrapper class.
691
+ if (methodDecls.length === 0)
692
+ this.line(`// type ${decl.name} = ${this.typeComment(decl.type)}`);
528
693
  this.genNamedTypeClass(decl.name, methodDecls);
529
694
  } else {
530
695
  this.line(`// type ${decl.name} = ${this.typeComment(decl.type)}`);
@@ -533,31 +698,79 @@ export class CodeGen {
533
698
 
534
699
  genNamedTypeClass(name, methodDecls) {
535
700
  const namedType = this.checker?.types.get(name);
536
- const underlying = namedType?.underlying;
537
- let field, ctorDefault;
538
- if (underlying?.kind === "func") {
539
- field = "_fn";
540
- ctorDefault = "null";
541
- } else if (underlying?.kind === "map") {
542
- field = "_map";
543
- ctorDefault = "{}";
544
- } else {
545
- field = "_items";
546
- ctorDefault = "[]";
547
- }
701
+ const field = this._namedBoxField(namedType?.underlying);
702
+ // Values of the type stay raw (number, string, array, map, function); the
703
+ // class is only the box used when a value is stored in an interface.
704
+ // Methods are static (`T.$M(recv, ...)`) so raw values can call them.
548
705
  this.line(`class ${name} {`);
549
706
  this.indented(() => {
550
- this.line(
551
- `constructor(${field} = ${ctorDefault}) { this.${field} = ${field}; }`,
552
- );
707
+ this.line(`constructor(${field}) { this.${field} = ${field}; }`);
708
+ this.line(`__gfUnbox() { return this.${field}; }`);
553
709
  for (const m of methodDecls) {
554
710
  this.blank();
555
- this.genMethod(m, field);
711
+ this.genMethod(m, { staticNamed: true });
712
+ if (!m.recvPointer) {
713
+ this.line(
714
+ `${m.name}(...__a) { return ${name}.$${m.name}(this.${field}, ...__a); }`,
715
+ );
716
+ }
717
+ }
718
+ });
719
+ this.line("}");
720
+ // Box for *T in an interface. It also proxies `.value` to the pointer, so
721
+ // helpers that write through a pointer argument (errors.As, json.Unmarshal)
722
+ // keep working. Pointer methods get the { value } box, value methods its value.
723
+ this.line(`class ${name}$Ptr {`);
724
+ this.indented(() => {
725
+ this.line("constructor(_p) { this._p = _p; }");
726
+ this.line("__gfUnbox() { return this._p; }");
727
+ this.line("get value() { return this._p.value; }");
728
+ this.line("set value(v) { this._p.value = v; }");
729
+ for (const m of methodDecls) {
730
+ const recv = m.recvPointer ? "this._p" : "this._p.value";
731
+ this.line(
732
+ `${m.name}(...__a) { return ${name}.$${m.name}(${recv}, ...__a); }`,
733
+ );
556
734
  }
557
735
  });
558
736
  this.line("}");
559
737
  }
560
738
 
739
+ _namedBoxField(underlying) {
740
+ if (underlying?.kind === "func") return "_fn";
741
+ if (underlying?.kind === "map") return "_map";
742
+ return "_items";
743
+ }
744
+
745
+ // A user-declared named basic type without methods: still boxed in an
746
+ // interface so `any(L("x")).(string)` fails like Go.
747
+ _isPlainNamedBox(type) {
748
+ if (type?.kind !== "named" || type._generic) return false;
749
+ if (type._target === undefined || type._target === "wasm") return false;
750
+ if (type.underlying?.kind !== "basic" || type.methods?.size > 0)
751
+ return false;
752
+ return type.methods instanceof Map;
753
+ }
754
+
755
+ // Box/dispatch info for a named non-struct type with methods, or null.
756
+ _namedBoxInfo(type) {
757
+ if (type?.kind !== "named") return null;
758
+ const u = type.underlying;
759
+ if (!u || u.kind === "struct" || u.kind === "interface") return null;
760
+ const methods = type.methods ?? type._generic?.methods;
761
+ if (this._isPlainNamedBox(type))
762
+ return {
763
+ cls: this._dequalify(type.name),
764
+ field: this._namedBoxField(u),
765
+ methods: new Map(),
766
+ };
767
+ if (!methods || methods.size === 0) return null;
768
+ if (![...methods.values()].some((m) => Object.hasOwn(m ?? {}, "_ptrRecv")))
769
+ return null;
770
+ const cls = this._dequalify(type.name).replace(/\[.*$/, "");
771
+ return { cls, field: this._namedBoxField(u), methods };
772
+ }
773
+
561
774
  _collectStructFields(name, structTypeAst) {
562
775
  const fields = [];
563
776
  if (this.checker) {
@@ -582,9 +795,11 @@ export class CodeGen {
582
795
  const resolved = this.checker.types.get(typeName)?.underlying;
583
796
  if (resolved?.kind === "struct") {
584
797
  const fields = [];
585
- for (const [name, type] of resolved.fields.entries()) {
586
- fields.push({ name, zero: this.zeroValueForType(type) });
587
- }
798
+ // Cached per type name, so never bound to the current generic body.
799
+ this._withTypeParamZeros(null, () => {
800
+ for (const [name, type] of resolved.fields.entries())
801
+ fields.push({ name, zero: this.zeroValueForType(type) });
802
+ });
588
803
  this.structFields.set(typeName, fields);
589
804
  return fields;
590
805
  }
@@ -617,17 +832,69 @@ export class CodeGen {
617
832
  this.line("}");
618
833
  }
619
834
 
835
+ // A generic struct's constructor takes the instance's zero-value factories
836
+ // after the fields (see HELPER_GENERICS). A field whose zero value depends
837
+ // on a type parameter starts at it when its argument is undefined.
838
+ _genTypeParamBindings(tpNames, tz) {
839
+ tpNames.forEach((n, i) => {
840
+ this.line(`const $z_${n} = ${tz}?.[${i}] ?? __zn;`);
841
+ });
842
+ }
843
+
844
+ _genGenericStructConstructor(fields, tpNames) {
845
+ const usesTz = (f) => f.zero.includes("$z_");
846
+ const params = fields.map((f) =>
847
+ usesTz(f) ? `${f.name}$` : `${f.name}$ = ${f.zero}`,
848
+ );
849
+ this.line(`constructor(${[...params, "$tz"].join(", ")}) {`);
850
+ this.indented(() => {
851
+ this.line("this[__tz] = $tz;");
852
+ if (fields.some(usesTz)) this._genTypeParamBindings(tpNames, "$tz");
853
+ for (const f of fields) {
854
+ const v = usesTz(f)
855
+ ? `${f.name}$ === undefined ? ${f.zero} : ${f.name}$`
856
+ : `${f.name}$`;
857
+ this.line(`this.${f.name} = ${v};`);
858
+ }
859
+ });
860
+ this.line("}");
861
+ }
862
+
863
+ // JS object keys are strings: a struct used as a map key becomes an
864
+ // injective encoding of its fields (see HELPER_STRUCT_KEY), so equal values
865
+ // share a key and distinct values never collide.
866
+ _genStructKeyString(_name, fields) {
867
+ if (fields.some((f) => f.name === "toString")) return;
868
+ if (fields.length === 0) {
869
+ this.line('toString() { return "{}"; }');
870
+ return;
871
+ }
872
+ this.useHelper("structKey");
873
+ this.line("toString() { return __sk(this); }");
874
+ }
875
+
620
876
  _genStructClone(name, structTypeAst, fields) {
621
877
  const resolved = this.checker?.types.get(name)?.underlying;
622
878
  const astTypes = new Map();
623
879
  for (const f of structTypeAst?.fields ?? [])
624
880
  for (const n of f.names ?? []) astTypes.set(n, f.type);
625
- const args = fields.map((f) => {
626
- const t =
627
- resolved?.fields?.get(f.name) ??
628
- this._typeFromNode(astTypes.get(f.name));
629
- return this._cloneJs(t, `this.${f.name}`);
630
- });
881
+ const tpNames = this._genericTypeParamNames(name);
882
+ const prevZeros = this._typeParamZeros;
883
+ this._typeParamZeros = tpNames
884
+ ? new Map(tpNames.map((n, i) => [n, `this[__tz]?.[${i}]`]))
885
+ : null;
886
+ let args;
887
+ try {
888
+ args = fields.map((f) => {
889
+ const t =
890
+ resolved?.fields?.get(f.name) ??
891
+ this._typeFromNode(astTypes.get(f.name));
892
+ return this._cloneJs(t, `this.${f.name}`);
893
+ });
894
+ } finally {
895
+ this._typeParamZeros = prevZeros;
896
+ }
897
+ if (tpNames) args.push("this[__tz]");
631
898
  this.blank();
632
899
  this.line(`__clone() { return new ${name}(${args.join(", ")}); }`);
633
900
  }
@@ -641,18 +908,38 @@ export class CodeGen {
641
908
  );
642
909
  }
643
910
 
911
+ // Type parameter names of generic type `name`, or null.
912
+ _genericTypeParamNames(name) {
913
+ const generic = this.checker?.types.get(name)?._generic;
914
+ if (!generic?.typeParams?.length || generic.isAlias) return null;
915
+ return generic.typeParams.map((tp) => tp.name);
916
+ }
917
+
644
918
  genStruct(name, structTypeAst, methodDecls) {
645
- const fields = this._collectStructFields(name, structTypeAst);
646
- this.structFields.set(name, fields);
919
+ const tpNames = this._genericTypeParamNames(name);
920
+ if (tpNames) this.useHelper("generics");
921
+ const fields = this._withTypeParamZeros(tpNames, () =>
922
+ this._collectStructFields(name, structTypeAst),
923
+ );
924
+ this.structFields.set(
925
+ name,
926
+ tpNames
927
+ ? this._withTypeParamZeros(null, () =>
928
+ this._collectStructFields(name, structTypeAst),
929
+ )
930
+ : fields,
931
+ );
647
932
  this.line(`class ${name} {`);
648
933
  this.indented(() => {
649
- this._genStructConstructor(fields);
934
+ if (tpNames) this._genGenericStructConstructor(fields, tpNames);
935
+ else this._genStructConstructor(fields);
650
936
  for (const m of methodDecls) {
651
937
  this.blank();
652
938
  this.genMethod(m);
653
939
  }
654
940
  this._genEmbeddedMethodStubs(name, methodDecls);
655
941
  this._genStructClone(name, structTypeAst, fields);
942
+ this._genStructKeyString(name, fields);
656
943
  });
657
944
  this.line("}");
658
945
  // Lets generic `p.value` dereferences resolve to the struct itself.
@@ -661,6 +948,26 @@ export class CodeGen {
661
948
  `Object.defineProperty(${name}.prototype, "value", { get() { return this; }, set(v) { Object.assign(this, v); }, configurable: true });`,
662
949
  );
663
950
  }
951
+ this._genFmtClassMeta(name, methodDecls);
952
+ }
953
+
954
+ _emitJsonCodecs() {
955
+ this._emitJsonAnyHooks();
956
+ if (!this._jsonCodecs || this._jsonCodecs.size === 0) return;
957
+ for (const [, code] of this._jsonCodecs) {
958
+ this.line(code);
959
+ this.blank();
960
+ }
961
+ }
962
+
963
+ // A promoted method runs on the embedded value (`this.In`), so it and the
964
+ // outer struct's promoted-field accesses see the same storage. Embedded
965
+ // types that are not compiled Go structs keep the borrowed-`this` form.
966
+ _embedStub(method, embedName) {
967
+ const call = this.structNames.has(embedName)
968
+ ? `this.${embedName}.${method}(...__a)`
969
+ : `${embedName}.prototype.${method}.call(this, ...__a)`;
970
+ return `${method}(...__a) { return ${call}; }`;
664
971
  }
665
972
 
666
973
  _genSingleEmbedStubs(embed, declared) {
@@ -671,9 +978,7 @@ export class CodeGen {
671
978
  for (const [mName] of embedBase.methods.entries()) {
672
979
  if (!declared.has(mName)) {
673
980
  this.blank();
674
- this.line(
675
- `${mName}(...__a) { return ${embedName}.prototype.${mName}.call(this, ...__a); }`,
676
- );
981
+ this.line(this._embedStub(mName, embedName));
677
982
  }
678
983
  }
679
984
  }
@@ -682,40 +987,74 @@ export class CodeGen {
682
987
  const stubs = computeEmbeddedStubs(name, methodDecls, this.checker);
683
988
  for (const stub of stubs) {
684
989
  this.blank();
685
- this.line(
686
- `${stub.methodName}(...__a) { return ${stub.embedName}.prototype.${stub.methodName}.call(this, ...__a); }`,
687
- );
990
+ this.line(this._embedStub(stub.methodName, stub.embedName));
688
991
  }
689
992
  }
690
993
 
691
- genMethod(decl, recvField = null) {
994
+ genMethod(decl, opts = null) {
692
995
  const prevFileIdx = this._currentSrcFileIdx;
693
996
  if (decl._srcFileIdx != null) this._currentSrcFileIdx = decl._srcFileIdx;
694
- const params = decl.params.map((p) => p.name).join(", ");
695
- const asyncPrefix = decl.async ? "async " : "";
696
- this.line(`${asyncPrefix}${decl.name}(${params}) {`, decl._line ?? null);
997
+ const staticNamed = Boolean(opts?.staticNamed);
998
+ const recvParam =
999
+ decl.recvName && decl.recvName !== "_" ? decl.recvName : "__recv";
1000
+ this.line(
1001
+ this._methodSignature(decl, staticNamed, recvParam),
1002
+ decl._line ?? null,
1003
+ );
697
1004
  const prevBoxed = this._boxedVars;
698
- this._boxedVars = new Set();
1005
+ this._boxedVars = new Set(this._boxedGlobals);
699
1006
  this._scanAddressTaken(decl.body);
700
- const prevUnwrapped = this._unwrappedRecv;
1007
+ // Methods of a generic struct read its type arguments from the
1008
+ // instance; named non-struct types have no instance to carry them.
1009
+ const tpNames = staticNamed
1010
+ ? null
1011
+ : this._genericTypeParamNames(decl.recvType?.name);
1012
+ this._withTypeParamZeros(tpNames, () =>
1013
+ this._genMethodBody(decl, staticNamed, recvParam, tpNames),
1014
+ );
1015
+ this._boxedVars = prevBoxed;
1016
+ this.line("}");
1017
+ // `Unwrap() []error` is told apart from `Unwrap() error` in type tests.
1018
+ if (
1019
+ !staticNamed &&
1020
+ decl.name === "Unwrap" &&
1021
+ decl.returnType?.kind === "SliceType"
1022
+ )
1023
+ this.line("static { this.prototype.Unwrap.__multi = true; }");
1024
+ this._currentSrcFileIdx = prevFileIdx;
1025
+ }
1026
+
1027
+ _genMethodBody(decl, staticNamed, recvParam, tpNames) {
701
1028
  this._withFnCtx(decl.body, () =>
702
1029
  this.indented(() => {
703
- if (decl.recvName && decl.recvName !== "_") {
704
- if (recvField) {
705
- this.line(`const ${decl.recvName} = this.${recvField};`);
706
- this._unwrappedRecv = decl.recvName;
707
- } else {
708
- this.line(`const ${decl.recvName} = ${this._receiverValue(decl)};`);
709
- }
1030
+ if (tpNames) this._genTypeParamBindings(tpNames, "this?.[__tz]");
1031
+ if (decl.recvName) this._fnCtx.params.add(decl.recvName);
1032
+ if (!staticNamed && decl.recvName && decl.recvName !== "_") {
1033
+ this.line(`let ${decl.recvName} = ${this._receiverValue(decl)};`);
1034
+ }
1035
+ if (staticNamed && this._boxedVars.has(recvParam)) {
1036
+ this.line(`${recvParam} = { value: ${recvParam} };`);
710
1037
  }
711
1038
  this._emitParamCopies(decl.params);
712
1039
  this._withNamedReturns(decl, () => this._genBody(decl.body, decl));
713
1040
  }),
714
1041
  );
715
- this._unwrappedRecv = prevUnwrapped;
716
- this._boxedVars = prevBoxed;
717
- this.line("}");
718
- this._currentSrcFileIdx = prevFileIdx;
1042
+ }
1043
+
1044
+ // JS parameter names; a variadic Go parameter is a rest parameter.
1045
+ _paramNames(params) {
1046
+ return params.map((p, i) =>
1047
+ p.variadic && i === params.length - 1 ? `...${p.name}` : p.name,
1048
+ );
1049
+ }
1050
+
1051
+ // Static methods of named non-struct types take the receiver as first parameter.
1052
+ _methodSignature(decl, staticNamed, recvParam) {
1053
+ const params = this._paramNames(decl.params);
1054
+ const asyncPrefix = decl.async ? "async " : "";
1055
+ if (!staticNamed)
1056
+ return `${asyncPrefix}${decl.name}(${params.join(", ")}) {`;
1057
+ return `static ${asyncPrefix}$${decl.name}(${[recvParam, ...params].join(", ")}) {`;
719
1058
  }
720
1059
 
721
1060
  // Value receivers are copies in Go; only materialize the copy when the body mutates it.
@@ -733,35 +1072,42 @@ export class CodeGen {
733
1072
  return "this";
734
1073
  }
735
1074
 
736
- // Returns the wrapper field name ("_fn", "_items", "_map") if `type` is a named
737
- // non-struct type emitted as a wrapper class, or null otherwise.
738
- // Pass `expr` so we can skip unwrapping when the expression is the method receiver
739
- // (which was already unwrapped to `this.<field>` at the top of the method body).
740
- _namedWrapperField(type, expr = null) {
741
- if (type?.kind !== "named") return null;
742
- if (!this.namedWrapperNames.has(type.name)) return null;
743
- if (expr?.kind === "Ident" && expr.name === this._unwrappedRecv)
744
- return null;
745
- const u = type.underlying;
746
- if (u?.kind === "func") return "_fn";
747
- if (u?.kind === "map") return "_map";
748
- return "_items";
1075
+ // Named non-struct values are kept raw in the JS backend, so there is no
1076
+ // wrapper field to unwrap any more; kept for call-site compatibility.
1077
+ _namedWrapperField(_type, _expr = null) {
1078
+ return null;
749
1079
  }
750
1080
 
751
1081
  // ── Function declarations ────────────────────────────────────
752
1082
 
753
1083
  genFuncDecl(decl, nameOverride) {
1084
+ const tpNames = decl.typeParams?.length
1085
+ ? decl.typeParams.map((tp) => tp.name)
1086
+ : null;
1087
+ this._withTypeParamZeros(tpNames, () =>
1088
+ this._genFuncDecl(decl, nameOverride, tpNames),
1089
+ );
1090
+ }
1091
+
1092
+ // A generic function takes one zero-value factory per type parameter
1093
+ // before its parameters (see HELPER_GENERICS).
1094
+ _genFuncDecl(decl, nameOverride, tpNames) {
754
1095
  const name = nameOverride ?? decl.name;
755
- const params = decl.params
756
- .map((p, i) =>
757
- p.variadic && i === decl.params.length - 1 ? `...${p.name}` : p.name,
758
- )
759
- .join(", ");
1096
+ const params = [
1097
+ ...(tpNames ?? []).map((n) => `$z_${n}`),
1098
+ ...this._paramNames(decl.params),
1099
+ ].join(", ");
760
1100
  const asyncPrefix = decl.async ? "async " : "";
761
1101
  const srcLine = decl._line ?? null;
762
1102
  this.line(`${asyncPrefix}function ${name}(${params}) {`, srcLine);
1103
+ if (decl.intrinsic) {
1104
+ const args = decl.params.map((p) => p.name);
1105
+ this.indented(() => this.line(JS_INTRINSICS[decl.intrinsic](args)));
1106
+ this.line("}");
1107
+ return;
1108
+ }
763
1109
  const prevBoxed = this._boxedVars;
764
- this._boxedVars = new Set();
1110
+ this._boxedVars = new Set(this._boxedGlobals);
765
1111
  this._scanAddressTaken(decl.body);
766
1112
  this._withFnCtx(decl.body, () =>
767
1113
  this.indented(() => {
@@ -784,6 +1130,7 @@ export class CodeGen {
784
1130
  const { name, type } = named[i];
785
1131
  const varName = !name || name === "_" ? `__ret_blank$${i}` : name;
786
1132
  this._markOwnership(varName, true);
1133
+ this._fnCtx?.params.add(varName);
787
1134
  this.line(`let ${varName} = ${this.zeroValueForTypeNode(type)};`);
788
1135
  }
789
1136
  this.namedReturnVars = named.map((r, i) =>
@@ -858,9 +1205,51 @@ export class CodeGen {
858
1205
  this._recoverOkInScope = prevRecoverOk;
859
1206
  }
860
1207
 
1208
+ // Package-level variables whose address is taken anywhere (`&g`, `g.Inc()`
1209
+ // on a pointer-receiver method) are boxed like address-taken locals.
1210
+ _collectBoxedGlobals(program) {
1211
+ const names = new Set();
1212
+ for (const d of program.decls)
1213
+ if (d.kind === "VarDecl")
1214
+ for (const spec of d.decls) for (const n of spec.names) names.add(n);
1215
+ const boxed = new Set();
1216
+ if (names.size === 0) return boxed;
1217
+ for (const n of this._scanAddressTaken(program.decls, new Set()))
1218
+ if (names.has(n)) boxed.add(n);
1219
+ return boxed;
1220
+ }
1221
+
861
1222
  // Scan AST node for _addressTaken idents on scalars and populate _boxedVars.
862
- _scanAddressTaken(node) {
863
- scanAddressTaken(node, this._boxedVars);
1223
+ // A pointer-receiver method call on a named non-struct local (`l.Set(5)`)
1224
+ // takes the variable's address too, so the variable must be boxed.
1225
+ _scanAddressTaken(node, into = this._boxedVars) {
1226
+ scanAddressTaken(node, into);
1227
+ const visit = (n) => {
1228
+ if (!n || typeof n !== "object") return;
1229
+ if (Array.isArray(n)) {
1230
+ for (const item of n) visit(item);
1231
+ return;
1232
+ }
1233
+ if (n.kind === "CallExpr" && n.func?.kind === "SelectorExpr") {
1234
+ const recv = n.func.expr;
1235
+ if (
1236
+ recv?.kind === "Ident" &&
1237
+ this._namedPtrMethodOn(recv._type, n.func.field)
1238
+ )
1239
+ into.add(recv.name);
1240
+ }
1241
+ for (const key of Object.keys(n)) {
1242
+ if (key.startsWith("_") || key === "kind") continue;
1243
+ visit(n[key]);
1244
+ }
1245
+ };
1246
+ visit(node);
1247
+ return into;
1248
+ }
1249
+
1250
+ _namedPtrMethodOn(type, method) {
1251
+ const info = this._namedBoxInfo(type);
1252
+ return Boolean(info?.methods.get(method)?._ptrRecv);
864
1253
  }
865
1254
 
866
1255
  _isReferenceType(t) {
@@ -891,15 +1280,11 @@ export class CodeGen {
891
1280
  }
892
1281
  return js;
893
1282
  });
894
- if (spec.names.length === 1) {
895
- this.line(`let ${spec.names[0]} = ${vals[0]};`);
896
- } else {
897
- // let [a, b] = [v1, v2]
898
- this.line(`let [${spec.names.join(", ")}] = [${vals.join(", ")}];`);
899
- }
1283
+ this._emitVarBindings(spec.names, vals, paired);
900
1284
  } else {
901
1285
  const zero = spec.type ? this.zeroValueForTypeNode(spec.type) : "null";
902
1286
  for (const name of spec.names) {
1287
+ if (name === "_") continue;
903
1288
  this._markOwnership(name, true);
904
1289
  const val = this._boxedVars.has(name) ? `{ value: ${zero} }` : zero;
905
1290
  this.line(`let ${name} = ${val};`);
@@ -908,6 +1293,26 @@ export class CodeGen {
908
1293
  }
909
1294
  }
910
1295
 
1296
+ // `var a = v` / `var a, b = v1, v2` / `var a, b = f()`, boxing address-taken names.
1297
+ _emitVarBindings(names, vals, paired) {
1298
+ if (names.length === 1 && names[0] === "_") {
1299
+ // A blank binding only runs its initialiser (`var _ = f()` may repeat).
1300
+ this.line(`void (${vals[0]});`);
1301
+ return;
1302
+ }
1303
+ if (names.length === 1) {
1304
+ const val = this._boxedVars.has(names[0])
1305
+ ? `{ value: ${vals[0]} }`
1306
+ : vals[0];
1307
+ this.line(`let ${names[0]} = ${val};`);
1308
+ return;
1309
+ }
1310
+ const rhs = paired ? `[${vals.join(", ")}]` : vals[0];
1311
+ const targets = names.map((n) => (n === "_" ? "" : n));
1312
+ this.line(`let [${targets.join(", ")}] = ${rhs};`);
1313
+ this._boxDeclaredNames(names);
1314
+ }
1315
+
911
1316
  genConstDecl(decl) {
912
1317
  for (const spec of decl.decls) {
913
1318
  const vals = spec.value.map((v) => this.genExpr(v));
@@ -924,5 +1329,6 @@ export class CodeGen {
924
1329
 
925
1330
  Object.assign(CodeGen.prototype, statementGenMethods);
926
1331
  Object.assign(CodeGen.prototype, expressionGenMethods);
1332
+ Object.assign(CodeGen.prototype, typeInfoGenMethods);
927
1333
  Object.assign(CodeGen.prototype, stdlibGenMethods);
928
1334
  Object.assign(CodeGen.prototype, templGenMethods);