gofront 1.7.0 → 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 (128) hide show
  1. package/README.md +13 -13
  2. package/package.json +8 -37
  3. package/src/backend/js/expressions.js +1341 -148
  4. package/src/backend/js/fmt-runtime.js +573 -0
  5. package/src/backend/js/index.js +375 -57
  6. package/src/backend/js/runtime.js +1460 -133
  7. package/src/backend/js/statements.js +900 -86
  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 +2 -1
  12. package/src/backend/js/stdlib/fmt.js +349 -10
  13. package/src/backend/js/stdlib/index.js +8 -3
  14. package/src/backend/js/stdlib/json.js +247 -52
  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 +10 -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 +436 -80
  29. package/src/backend/wasm/emit-builtins.js +303 -13
  30. package/src/backend/wasm/emit-exprs.js +1397 -203
  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 +512 -77
  34. package/src/backend/wasm/encode.js +43 -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/prescan.js +397 -0
  41. package/src/backend/wasm/stdlib/bytes.js +1804 -127
  42. package/src/backend/wasm/stdlib/core.js +1574 -199
  43. package/src/backend/wasm/stdlib/index.js +0 -5
  44. package/src/backend/wasm/stdlib/io.js +1567 -150
  45. package/src/backend/wasm/stdlib/packages.js +2 -0
  46. package/src/backend/wasm/stdlib/simd.js +293 -28
  47. package/src/backend/wasm/stdlib/time.js +1041 -12
  48. package/src/backend/wasm/types.js +200 -15
  49. package/src/backend/wasm/variadic.js +93 -0
  50. package/src/backend/wasm/wat.js +47 -20
  51. package/src/{cli-core.js → cli/cli-core.js} +236 -10
  52. package/src/{test-runner.js → cli/test-runner.js} +332 -39
  53. package/src/{vendor.js → cli/vendor.js} +3 -4
  54. package/src/compiler.js +107 -269
  55. package/src/index.js +15 -169
  56. package/src/{lexer.js → lexer/lexer.js} +102 -55
  57. package/src/{tokens.js → lexer/tokens.js} +1 -0
  58. package/src/link/sourcemap.js +97 -0
  59. package/src/link/stdlib.js +515 -0
  60. package/src/link/wasm.js +294 -0
  61. package/src/lower/bounds.js +394 -0
  62. package/src/lower/boxing.js +1 -1
  63. package/src/lower/captures.js +176 -32
  64. package/src/lower/index.js +26 -18
  65. package/src/lower/json-codecs.js +1398 -0
  66. package/src/lower/ownership.js +22 -1
  67. package/src/lower/range.js +86 -1
  68. package/src/lower/reachability.js +132 -0
  69. package/src/parser/declarations.js +58 -13
  70. package/src/parser/expressions.js +29 -7
  71. package/src/parser/index.js +2 -1
  72. package/src/parser/statements.js +37 -22
  73. package/src/{templ-parser.js → parser/templ.js} +138 -15
  74. package/src/parser/types.js +69 -9
  75. package/src/resolver.js +22 -1
  76. package/src/typechecker/assignability.js +176 -17
  77. package/src/typechecker/const-fold.js +356 -0
  78. package/src/typechecker/constants.js +256 -0
  79. package/src/typechecker/expressions.js +931 -122
  80. package/src/typechecker/index.js +459 -67
  81. package/src/typechecker/init-order.js +330 -0
  82. package/src/typechecker/resolve.js +451 -56
  83. package/src/typechecker/statements.js +90 -9
  84. package/src/typechecker/stdlib/core.js +333 -55
  85. package/src/typechecker/stdlib/excluded.js +898 -0
  86. package/src/typechecker/stdlib/extended.js +322 -26
  87. package/src/typechecker/{stdlib.js → stdlib/index.js} +5 -5
  88. package/src/typechecker/stdlib/simd.js +65 -15
  89. package/src/typechecker/termination.js +1 -1
  90. package/src/typechecker/types.js +149 -25
  91. package/stdlib/bytes/iter.go +192 -0
  92. package/stdlib/bytes/search.go +240 -0
  93. package/stdlib/bytes/split.go +218 -0
  94. package/stdlib/cmp/cmp.go +65 -0
  95. package/stdlib/encoding/json/errors.go +31 -0
  96. package/stdlib/encoding/json/indent.go +71 -0
  97. package/stdlib/encoding/json/number.go +12 -0
  98. package/stdlib/encoding/json/stream.go +173 -0
  99. package/stdlib/errors/overlay.go +12 -0
  100. package/stdlib/hash/hash.go +53 -0
  101. package/stdlib/internal/jsonrt/dec.go +738 -0
  102. package/stdlib/internal/jsonrt/enc.go +366 -0
  103. package/stdlib/internal/jsonrt/indent.go +135 -0
  104. package/stdlib/internal/jsonrt/number.go +27 -0
  105. package/stdlib/internal/jsonrt/reader.go +73 -0
  106. package/stdlib/internal/jsonrt/scanner.go +649 -0
  107. package/stdlib/io/io.go +346 -0
  108. package/stdlib/iter/iter.go +18 -0
  109. package/stdlib/maps/iter.go +65 -0
  110. package/stdlib/math/extra.go +270 -0
  111. package/stdlib/slices/extra.go +73 -0
  112. package/stdlib/slices/iter.go +113 -0
  113. package/stdlib/sort/sort.go +561 -0
  114. package/stdlib/strconv/quote.go +341 -0
  115. package/stdlib/strings/extra.go +35 -0
  116. package/stdlib/strings/iter.go +122 -0
  117. package/stdlib/sync/extra.go +105 -0
  118. package/stdlib/unicode/unicode.go +112 -0
  119. package/stdlib/unicode/utf8/bytes.go +239 -0
  120. package/src/backend/wasm/const-fold.js +0 -205
  121. package/src/backend/wasm/emit-stdlib.js +0 -13
  122. /package/src/{asset-manager.js → cli/asset-manager.js} +0 -0
  123. /package/src/{colors.js → cli/colors.js} +0 -0
  124. /package/src/{dev-server.js → cli/dev-server.js} +0 -0
  125. /package/src/{minifier.js → cli/minifier.js} +0 -0
  126. /package/src/{project-config.js → cli/project-config.js} +0 -0
  127. /package/src/{pwa.js → cli/pwa.js} +0 -0
  128. /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,34 +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,
44
54
  HELPER_IFEQ,
45
55
  HELPER_INJECT_STYLES,
46
56
  HELPER_IO_EOF,
47
57
  HELPER_JSON,
48
58
  HELPER_LEN,
49
59
  HELPER_NIL_CALL,
60
+ HELPER_NIL_MAP,
61
+ HELPER_NM,
62
+ HELPER_OS,
50
63
  HELPER_PANIC,
51
64
  HELPER_PATH_CLEAN,
52
65
  HELPER_PATH_ERROR,
66
+ HELPER_PTREQ,
53
67
  HELPER_S,
54
68
  HELPER_SCLONE,
55
69
  HELPER_SHA256,
70
+ HELPER_SHIFT,
56
71
  HELPER_SIMD,
72
+ HELPER_SLICE,
57
73
  HELPER_SORT_SLICE,
74
+ HELPER_SPRINT,
58
75
  HELPER_SPRINTF,
59
76
  HELPER_STRCONV,
60
77
  HELPER_STRCONV_ERROR,
78
+ HELPER_STRUCT_KEY,
79
+ HELPER_SV,
61
80
  HELPER_SYNC,
62
81
  HELPER_TESTING,
63
82
  HELPER_TIME_FMT,
64
83
  HELPER_TIME_PARSE,
84
+ HELPER_VA,
85
+ HELPER_VCLONE,
65
86
  } from "./runtime.js";
66
87
  import { buildSourceMap, buildWasmSourceMap } from "./source-map.js";
67
88
  import { statementGenMethods } from "./statements.js";
68
89
  import { stdlibGenMethods } from "./stdlib/index.js";
69
90
  import { templGenMethods } from "./templ.js";
91
+ import { typeInfoGenMethods } from "./type-info.js";
70
92
 
71
93
  export { buildSourceMap, buildWasmSourceMap };
72
94
 
@@ -110,21 +132,41 @@ const JS_RESERVED = new Set([
110
132
  const jsSafeName = (n) =>
111
133
  typeof n === "string" && JS_RESERVED.has(n) ? `${n}$` : n;
112
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
+
113
141
  // Runtime helpers by registry name, in output order. Mark one as needed with
114
142
  // `this.useHelper(name)`; `_prependHelpers` emits every marked entry.
115
143
  const HELPER_MAP = [
116
144
  ["len", HELPER_LEN],
145
+ ["sv", HELPER_SV],
146
+ ["slice", HELPER_SLICE],
117
147
  ["append", HELPER_APPEND],
118
148
  ["sliceGuard", HELPER_S],
119
149
  ["sclone", HELPER_SCLONE],
150
+ ["va", HELPER_VA],
151
+ ["vclone", HELPER_VCLONE],
120
152
  ["ifaceBox", HELPER_IFACE_BOX],
153
+ ["ifd", HELPER_IFD],
121
154
  ["equal", HELPER_EQUAL],
155
+ ["geq", HELPER_GEQ],
156
+ ["ptreq", HELPER_PTREQ],
122
157
  ["ifeq", HELPER_IFEQ],
123
158
  ["cmul", HELPER_CMUL],
124
159
  ["cdiv", HELPER_CDIV],
125
160
  ["panicRt", HELPER_PANIC],
161
+ ["shift", HELPER_SHIFT],
126
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],
127
168
  ["sprintf", HELPER_SPRINTF],
169
+ ["sprint", HELPER_SPRINT],
128
170
  ["error", HELPER_ERROR],
129
171
  ["errorIs", HELPER_ERROR_IS],
130
172
  ["errorAs", HELPER_ERROR_AS],
@@ -132,6 +174,7 @@ const HELPER_MAP = [
132
174
  ["strconvError", HELPER_STRCONV_ERROR],
133
175
  ["pathError", HELPER_PATH_ERROR],
134
176
  ["ioEOF", HELPER_IO_EOF],
177
+ ["os", HELPER_OS],
135
178
  ["strconv", HELPER_STRCONV],
136
179
  ["pathClean", HELPER_PATH_CLEAN],
137
180
  ["sortSlice", HELPER_SORT_SLICE],
@@ -149,6 +192,10 @@ const HELPER_MAP = [
149
192
  ["simd", HELPER_SIMD],
150
193
  ];
151
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
+
152
199
  export class CodeGen {
153
200
  // jsImports: Map<importPath, string[]> — npm package imports to emit at top of file
154
201
  // bundledPackages: Set<string> — GoFront package names bundled inline;
@@ -168,6 +215,9 @@ export class CodeGen {
168
215
  this.structFields = new Map();
169
216
  this.namedWrapperNames = new Set();
170
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
171
221
  this.jsImports = jsImports;
172
222
  this.bundledPackages = bundledPackages;
173
223
  this.namedReturnVars = null; // names of current function's named return vars
@@ -175,6 +225,8 @@ export class CodeGen {
175
225
  this._currentSrcFileIdx = 0; // updated as each top-level decl is generated
176
226
  this._boxedVars = new Set(); // address-taken scalar variables that need boxing
177
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
178
230
  // Runtime helper usage tracking — only emit helpers that are actually used
179
231
  this._helpers = new Set();
180
232
  // Per-function context for Go value semantics (see _withFnCtx)
@@ -221,6 +273,9 @@ export class CodeGen {
221
273
  }
222
274
 
223
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);
224
279
  for (const d of program.decls) {
225
280
  if (d.kind === "TypeDecl") {
226
281
  this._currentSrcFileIdx = d._srcFileIdx ?? 0;
@@ -231,20 +286,28 @@ export class CodeGen {
231
286
  }
232
287
 
233
288
  _emitVarConstDecls(program) {
234
- for (const d of program.decls) {
235
- if (d.kind === "ConstDecl") {
236
- this._currentSrcFileIdx = d._srcFileIdx ?? 0;
237
- this.genConstDecl(d);
238
- 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;
239
300
  }
240
- }
241
- for (const d of program.decls) {
242
- if (d.kind === "VarDecl") {
243
- this._currentSrcFileIdx = d._srcFileIdx ?? 0;
244
- 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] });
245
305
  this.blank();
246
306
  }
247
- }
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);
248
311
  }
249
312
 
250
313
  _callInitAndMain(initNames, program, isTest = false) {
@@ -262,19 +325,28 @@ export class CodeGen {
262
325
 
263
326
  generate(program, options = {}) {
264
327
  const isTest = options.isTest ?? false;
328
+ this._precisionWarned = new Set();
265
329
  // Defer lowering (argument/receiver hoisting, recover-target marking) is
266
330
  // idempotent and owned by the backends: the wasm path runs it via
267
331
  // `lower()`, the JS path here.
268
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];
269
336
  // Ensure _pkgName is set on all decls (generateAll sets it for
270
337
  // multi-file; for single-file compiles we derive it from the AST).
271
338
  const pkgName = program.pkg?.name;
272
339
  if (pkgName) {
340
+ this.pkgName = pkgName;
273
341
  for (const d of program.decls) {
274
342
  if (!d._pkgName) d._pkgName = pkgName;
275
343
  }
276
344
  }
277
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);
278
350
  const methods = this._collectDecls(program);
279
351
  this._emitJsImports();
280
352
  this._emitTypeDecls(program, methods);
@@ -331,12 +403,12 @@ export class CodeGen {
331
403
  }
332
404
 
333
405
  _renameParams(params) {
334
- for (const p of params ?? []) p.name = jsSafeName(p.name);
406
+ for (const p of params ?? []) p.name = jsBindingName(p.name);
335
407
  }
336
408
 
337
409
  _renameNamedReturns(node) {
338
410
  for (const r of node.returnType?._namedReturns ?? [])
339
- r.name = jsSafeName(r.name);
411
+ r.name = jsBindingName(r.name);
340
412
  }
341
413
 
342
414
  _renameSignature(node) {
@@ -345,27 +417,37 @@ export class CodeGen {
345
417
  }
346
418
 
347
419
  _renameIdentNode(node) {
348
- if (!node._isStructKey && node._type?.kind !== "builtin")
349
- 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);
350
424
  }
351
425
 
352
426
  _renameFuncDeclNode(node) {
353
- node.name = jsSafeName(node.name);
427
+ node.name = jsBindingName(node.name);
354
428
  this._renameSignature(node);
355
429
  }
356
430
 
357
431
  _renameMethodDeclNode(node) {
358
- node.recvName = jsSafeName(node.recvName);
432
+ node.recvName = jsBindingName(node.recvName);
359
433
  this._renameSignature(node);
360
434
  }
361
435
 
362
436
  _renameDeclSpecs(node) {
363
437
  for (const spec of node.decls ?? [])
364
- 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);
365
447
  }
366
448
 
367
449
  _renameTypeSwitchNode(node) {
368
- node.assign = jsSafeName(node.assign);
450
+ node.assign = jsBindingName(node.assign);
369
451
  }
370
452
 
371
453
  _markStructKeys(node) {
@@ -384,7 +466,15 @@ export class CodeGen {
384
466
  MethodDecl: "_renameMethodDeclNode",
385
467
  FuncLit: "_renameSignature",
386
468
  TemplDecl: "_renameSignature",
469
+ TemplExpr: "_renameTemplExprNode",
470
+ TemplComponent: "_renameTemplExprNode",
471
+ TemplIf: "_renameTemplExprNode",
472
+ TemplSwitch: "_renameTemplExprNode",
473
+ TemplCase: "_renameTemplExprNode",
474
+ TemplFor: "_renameTemplForNode",
475
+ TemplElement: "_renameTemplElementNode",
387
476
  CssDecl: "_renameSignature",
477
+ DefineStmt: "_renameDefineNode",
388
478
  VarDecl: "_renameDeclSpecs",
389
479
  ConstDecl: "_renameDeclSpecs",
390
480
  TypeSwitchStmt: "_renameTypeSwitchNode",
@@ -410,6 +500,18 @@ export class CodeGen {
410
500
  // Collects struct names, method map, and named wrapper names from program decls.
411
501
  // Returns the method map (typeName → MethodDecl[]).
412
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
+ }
413
515
  for (const d of program.decls) {
414
516
  if (d.kind === "TypeDecl" && d.type.kind === "StructType")
415
517
  this.structNames.add(d.name);
@@ -427,11 +529,14 @@ export class CodeGen {
427
529
 
428
530
  _collectMethodMap(program) {
429
531
  const methods = new Map();
532
+ this.nilSafeMethods = new Set();
430
533
  for (const d of program.decls) {
431
534
  if (d.kind === "MethodDecl") {
432
535
  const name = d.recvType.name;
433
536
  if (!methods.has(name)) methods.set(name, []);
434
537
  methods.get(name).push(d);
538
+ if (d.recvPointer && comparesToNil(d.body, d.recvName))
539
+ this.nilSafeMethods.add(`${name}.${d.name}`);
435
540
  }
436
541
  }
437
542
  return methods;
@@ -460,6 +565,8 @@ export class CodeGen {
460
565
  /** Marks a runtime helper (key of HELPER_MAP) as needed by the output. */
461
566
  useHelper(name) {
462
567
  this._helpers.add(name);
568
+ if (name === "ifeq" || name === "geq") this._helpers.add("equal");
569
+ if (name === "geq") this._helpers.add("ifeq");
463
570
  if (
464
571
  name === "strconvError" ||
465
572
  name === "pathError" ||
@@ -473,6 +580,13 @@ export class CodeGen {
473
580
  if (name === "json") {
474
581
  this.useHelper("base64");
475
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");
476
590
  if (name === "binary") {
477
591
  this.useHelper("ioEOF");
478
592
  this._helpers.add("error");
@@ -501,16 +615,29 @@ export class CodeGen {
501
615
  // Generate a single bundle from multiple programs (same-package multi-file).
502
616
  // Annotates each decl with its source file index before merging.
503
617
  generateAll(programs, options = {}) {
618
+ this._srcFileNames = programs.map((p) => p._filename ?? null);
504
619
  for (let i = 0; i < programs.length; i++) {
505
620
  for (const decl of programs[i].decls) {
506
621
  decl._srcFileIdx = i;
507
622
  decl._pkgName = programs[i].pkg?.name;
508
623
  }
509
624
  }
510
- const merged = { decls: programs.flatMap((p) => p.decls) };
625
+ const merged = {
626
+ pkg: programs[0]?.pkg,
627
+ decls: programs.flatMap((p) => p.decls),
628
+ };
511
629
  return this.generate(merged, options);
512
630
  }
513
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
+
514
641
  getCss() {
515
642
  return [...new Set(this.collectedCss)].join("\n\n");
516
643
  }
@@ -529,14 +656,40 @@ export class CodeGen {
529
656
 
530
657
  // ── Type declarations ────────────────────────────────────────
531
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
+
532
676
  genTypeDeclWithMethods(decl, methodDecls) {
677
+ const definedStruct =
678
+ decl.type.kind === "StructType" ? null : this._definedStructAst(decl);
533
679
  if (decl.type.kind === "StructType") {
534
680
  this.genStruct(decl.name, decl.type, methodDecls);
681
+ } else if (definedStruct) {
682
+ this.genStruct(decl.name, definedStruct, methodDecls);
535
683
  } else if (decl.type.kind === "InterfaceType") {
536
684
  // Interfaces are compile-time only — no JS output needed.
537
685
  this.line(`// interface ${decl.name} (compile-time only)`);
538
- } else if (methodDecls.length > 0) {
686
+ } else if (
687
+ methodDecls.length > 0 ||
688
+ this._isPlainNamedBox(this.checker?.types.get(decl.name))
689
+ ) {
539
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)}`);
540
693
  this.genNamedTypeClass(decl.name, methodDecls);
541
694
  } else {
542
695
  this.line(`// type ${decl.name} = ${this.typeComment(decl.type)}`);
@@ -589,12 +742,28 @@ export class CodeGen {
589
742
  return "_items";
590
743
  }
591
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
+
592
755
  // Box/dispatch info for a named non-struct type with methods, or null.
593
756
  _namedBoxInfo(type) {
594
757
  if (type?.kind !== "named") return null;
595
758
  const u = type.underlying;
596
759
  if (!u || u.kind === "struct" || u.kind === "interface") return null;
597
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
+ };
598
767
  if (!methods || methods.size === 0) return null;
599
768
  if (![...methods.values()].some((m) => Object.hasOwn(m ?? {}, "_ptrRecv")))
600
769
  return null;
@@ -626,9 +795,11 @@ export class CodeGen {
626
795
  const resolved = this.checker.types.get(typeName)?.underlying;
627
796
  if (resolved?.kind === "struct") {
628
797
  const fields = [];
629
- for (const [name, type] of resolved.fields.entries()) {
630
- fields.push({ name, zero: this.zeroValueForType(type) });
631
- }
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
+ });
632
803
  this.structFields.set(typeName, fields);
633
804
  return fields;
634
805
  }
@@ -661,17 +832,69 @@ export class CodeGen {
661
832
  this.line("}");
662
833
  }
663
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
+
664
876
  _genStructClone(name, structTypeAst, fields) {
665
877
  const resolved = this.checker?.types.get(name)?.underlying;
666
878
  const astTypes = new Map();
667
879
  for (const f of structTypeAst?.fields ?? [])
668
880
  for (const n of f.names ?? []) astTypes.set(n, f.type);
669
- const args = fields.map((f) => {
670
- const t =
671
- resolved?.fields?.get(f.name) ??
672
- this._typeFromNode(astTypes.get(f.name));
673
- return this._cloneJs(t, `this.${f.name}`);
674
- });
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]");
675
898
  this.blank();
676
899
  this.line(`__clone() { return new ${name}(${args.join(", ")}); }`);
677
900
  }
@@ -685,18 +908,38 @@ export class CodeGen {
685
908
  );
686
909
  }
687
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
+
688
918
  genStruct(name, structTypeAst, methodDecls) {
689
- const fields = this._collectStructFields(name, structTypeAst);
690
- 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
+ );
691
932
  this.line(`class ${name} {`);
692
933
  this.indented(() => {
693
- this._genStructConstructor(fields);
934
+ if (tpNames) this._genGenericStructConstructor(fields, tpNames);
935
+ else this._genStructConstructor(fields);
694
936
  for (const m of methodDecls) {
695
937
  this.blank();
696
938
  this.genMethod(m);
697
939
  }
698
940
  this._genEmbeddedMethodStubs(name, methodDecls);
699
941
  this._genStructClone(name, structTypeAst, fields);
942
+ this._genStructKeyString(name, fields);
700
943
  });
701
944
  this.line("}");
702
945
  // Lets generic `p.value` dereferences resolve to the struct itself.
@@ -705,9 +948,11 @@ export class CodeGen {
705
948
  `Object.defineProperty(${name}.prototype, "value", { get() { return this; }, set(v) { Object.assign(this, v); }, configurable: true });`,
706
949
  );
707
950
  }
951
+ this._genFmtClassMeta(name, methodDecls);
708
952
  }
709
953
 
710
954
  _emitJsonCodecs() {
955
+ this._emitJsonAnyHooks();
711
956
  if (!this._jsonCodecs || this._jsonCodecs.size === 0) return;
712
957
  for (const [, code] of this._jsonCodecs) {
713
958
  this.line(code);
@@ -715,6 +960,16 @@ export class CodeGen {
715
960
  }
716
961
  }
717
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}; }`;
971
+ }
972
+
718
973
  _genSingleEmbedStubs(embed, declared) {
719
974
  const embedName = embed.kind === "named" ? embed.name : null;
720
975
  if (!embedName) return;
@@ -723,9 +978,7 @@ export class CodeGen {
723
978
  for (const [mName] of embedBase.methods.entries()) {
724
979
  if (!declared.has(mName)) {
725
980
  this.blank();
726
- this.line(
727
- `${mName}(...__a) { return ${embedName}.prototype.${mName}.call(this, ...__a); }`,
728
- );
981
+ this.line(this._embedStub(mName, embedName));
729
982
  }
730
983
  }
731
984
  }
@@ -734,9 +987,7 @@ export class CodeGen {
734
987
  const stubs = computeEmbeddedStubs(name, methodDecls, this.checker);
735
988
  for (const stub of stubs) {
736
989
  this.blank();
737
- this.line(
738
- `${stub.methodName}(...__a) { return ${stub.embedName}.prototype.${stub.methodName}.call(this, ...__a); }`,
739
- );
990
+ this.line(this._embedStub(stub.methodName, stub.embedName));
740
991
  }
741
992
  }
742
993
 
@@ -751,12 +1002,35 @@ export class CodeGen {
751
1002
  decl._line ?? null,
752
1003
  );
753
1004
  const prevBoxed = this._boxedVars;
754
- this._boxedVars = new Set();
1005
+ this._boxedVars = new Set(this._boxedGlobals);
755
1006
  this._scanAddressTaken(decl.body);
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) {
756
1028
  this._withFnCtx(decl.body, () =>
757
1029
  this.indented(() => {
1030
+ if (tpNames) this._genTypeParamBindings(tpNames, "this?.[__tz]");
1031
+ if (decl.recvName) this._fnCtx.params.add(decl.recvName);
758
1032
  if (!staticNamed && decl.recvName && decl.recvName !== "_") {
759
- this.line(`const ${decl.recvName} = ${this._receiverValue(decl)};`);
1033
+ this.line(`let ${decl.recvName} = ${this._receiverValue(decl)};`);
760
1034
  }
761
1035
  if (staticNamed && this._boxedVars.has(recvParam)) {
762
1036
  this.line(`${recvParam} = { value: ${recvParam} };`);
@@ -765,14 +1039,18 @@ export class CodeGen {
765
1039
  this._withNamedReturns(decl, () => this._genBody(decl.body, decl));
766
1040
  }),
767
1041
  );
768
- this._boxedVars = prevBoxed;
769
- this.line("}");
770
- 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
+ );
771
1049
  }
772
1050
 
773
1051
  // Static methods of named non-struct types take the receiver as first parameter.
774
1052
  _methodSignature(decl, staticNamed, recvParam) {
775
- const params = decl.params.map((p) => p.name);
1053
+ const params = this._paramNames(decl.params);
776
1054
  const asyncPrefix = decl.async ? "async " : "";
777
1055
  if (!staticNamed)
778
1056
  return `${asyncPrefix}${decl.name}(${params.join(", ")}) {`;
@@ -803,17 +1081,33 @@ export class CodeGen {
803
1081
  // ── Function declarations ────────────────────────────────────
804
1082
 
805
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) {
806
1095
  const name = nameOverride ?? decl.name;
807
- const params = decl.params
808
- .map((p, i) =>
809
- p.variadic && i === decl.params.length - 1 ? `...${p.name}` : p.name,
810
- )
811
- .join(", ");
1096
+ const params = [
1097
+ ...(tpNames ?? []).map((n) => `$z_${n}`),
1098
+ ...this._paramNames(decl.params),
1099
+ ].join(", ");
812
1100
  const asyncPrefix = decl.async ? "async " : "";
813
1101
  const srcLine = decl._line ?? null;
814
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
+ }
815
1109
  const prevBoxed = this._boxedVars;
816
- this._boxedVars = new Set();
1110
+ this._boxedVars = new Set(this._boxedGlobals);
817
1111
  this._scanAddressTaken(decl.body);
818
1112
  this._withFnCtx(decl.body, () =>
819
1113
  this.indented(() => {
@@ -836,6 +1130,7 @@ export class CodeGen {
836
1130
  const { name, type } = named[i];
837
1131
  const varName = !name || name === "_" ? `__ret_blank$${i}` : name;
838
1132
  this._markOwnership(varName, true);
1133
+ this._fnCtx?.params.add(varName);
839
1134
  this.line(`let ${varName} = ${this.zeroValueForTypeNode(type)};`);
840
1135
  }
841
1136
  this.namedReturnVars = named.map((r, i) =>
@@ -910,11 +1205,25 @@ export class CodeGen {
910
1205
  this._recoverOkInScope = prevRecoverOk;
911
1206
  }
912
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
+
913
1222
  // Scan AST node for _addressTaken idents on scalars and populate _boxedVars.
914
1223
  // A pointer-receiver method call on a named non-struct local (`l.Set(5)`)
915
1224
  // takes the variable's address too, so the variable must be boxed.
916
- _scanAddressTaken(node) {
917
- scanAddressTaken(node, this._boxedVars);
1225
+ _scanAddressTaken(node, into = this._boxedVars) {
1226
+ scanAddressTaken(node, into);
918
1227
  const visit = (n) => {
919
1228
  if (!n || typeof n !== "object") return;
920
1229
  if (Array.isArray(n)) {
@@ -927,7 +1236,7 @@ export class CodeGen {
927
1236
  recv?.kind === "Ident" &&
928
1237
  this._namedPtrMethodOn(recv._type, n.func.field)
929
1238
  )
930
- this._boxedVars.add(recv.name);
1239
+ into.add(recv.name);
931
1240
  }
932
1241
  for (const key of Object.keys(n)) {
933
1242
  if (key.startsWith("_") || key === "kind") continue;
@@ -935,6 +1244,7 @@ export class CodeGen {
935
1244
  }
936
1245
  };
937
1246
  visit(node);
1247
+ return into;
938
1248
  }
939
1249
 
940
1250
  _namedPtrMethodOn(type, method) {
@@ -974,6 +1284,7 @@ export class CodeGen {
974
1284
  } else {
975
1285
  const zero = spec.type ? this.zeroValueForTypeNode(spec.type) : "null";
976
1286
  for (const name of spec.names) {
1287
+ if (name === "_") continue;
977
1288
  this._markOwnership(name, true);
978
1289
  const val = this._boxedVars.has(name) ? `{ value: ${zero} }` : zero;
979
1290
  this.line(`let ${name} = ${val};`);
@@ -984,6 +1295,11 @@ export class CodeGen {
984
1295
 
985
1296
  // `var a = v` / `var a, b = v1, v2` / `var a, b = f()`, boxing address-taken names.
986
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
+ }
987
1303
  if (names.length === 1) {
988
1304
  const val = this._boxedVars.has(names[0])
989
1305
  ? `{ value: ${vals[0]} }`
@@ -992,7 +1308,8 @@ export class CodeGen {
992
1308
  return;
993
1309
  }
994
1310
  const rhs = paired ? `[${vals.join(", ")}]` : vals[0];
995
- this.line(`let [${names.join(", ")}] = ${rhs};`);
1311
+ const targets = names.map((n) => (n === "_" ? "" : n));
1312
+ this.line(`let [${targets.join(", ")}] = ${rhs};`);
996
1313
  this._boxDeclaredNames(names);
997
1314
  }
998
1315
 
@@ -1012,5 +1329,6 @@ export class CodeGen {
1012
1329
 
1013
1330
  Object.assign(CodeGen.prototype, statementGenMethods);
1014
1331
  Object.assign(CodeGen.prototype, expressionGenMethods);
1332
+ Object.assign(CodeGen.prototype, typeInfoGenMethods);
1015
1333
  Object.assign(CodeGen.prototype, stdlibGenMethods);
1016
1334
  Object.assign(CodeGen.prototype, templGenMethods);