gofront 1.9.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -2
  3. package/src/backend/js/expressions.js +329 -58
  4. package/src/backend/js/index.js +90 -10
  5. package/src/backend/js/runtime.js +51 -9
  6. package/src/backend/js/statements.js +193 -84
  7. package/src/backend/js/type-info.js +123 -8
  8. package/src/backend/shared/fmt-types.js +30 -1
  9. package/src/backend/wasm/emit-builtins.js +250 -9
  10. package/src/backend/wasm/emit-exprs.js +63 -22
  11. package/src/backend/wasm/emit-shared.js +4 -1
  12. package/src/backend/wasm/emit-stmts.js +239 -205
  13. package/src/backend/wasm/emit.js +73 -26
  14. package/src/backend/wasm/globals.js +1 -0
  15. package/src/backend/wasm/index.js +8 -0
  16. package/src/backend/wasm/module-emitter.js +211 -57
  17. package/src/backend/wasm/monomorph.js +36 -2
  18. package/src/backend/wasm/natives.js +4 -0
  19. package/src/backend/wasm/types.js +32 -3
  20. package/src/backend/wasm/variadic.js +6 -1
  21. package/src/compiler.js +6 -1
  22. package/src/lexer/lexer.js +1 -0
  23. package/src/link/stdlib.js +1 -0
  24. package/src/link/type-names.js +72 -16
  25. package/src/lower/embedding.js +43 -15
  26. package/src/lower/fmt-formatters.js +38 -14
  27. package/src/lower/functions.js +223 -7
  28. package/src/lower/gen-types.js +21 -2
  29. package/src/lower/index.js +7 -1
  30. package/src/lower/json-codecs.js +114 -6
  31. package/src/parser/declarations.js +4 -2
  32. package/src/parser/expressions.js +25 -41
  33. package/src/parser/statements.js +107 -0
  34. package/src/parser/types.js +11 -0
  35. package/src/typechecker/assignability.js +94 -12
  36. package/src/typechecker/constants.js +124 -2
  37. package/src/typechecker/expressions.js +171 -17
  38. package/src/typechecker/index.js +158 -29
  39. package/src/typechecker/operand-text.js +1 -0
  40. package/src/typechecker/resolve.js +308 -39
  41. package/src/typechecker/statements.js +171 -21
  42. package/src/typechecker/types.js +84 -2
@@ -637,6 +637,7 @@ export class Lexer {
637
637
  }
638
638
  if (ch === "'") {
639
639
  this.push(T.INT, this.readRuneLiteral(), l, c);
640
+ this.tokens[this.tokens.length - 1].rune = true;
640
641
  continue;
641
642
  }
642
643
 
@@ -20,6 +20,7 @@ import { lowerStdlibRefs, registerWasmUnit } from "./wasm.js";
20
20
 
21
21
  export {
22
22
  mangleCollidingTypes,
23
+ registerLocalTypeOwners,
23
24
  renameLocalTypes,
24
25
  withOriginalTypeNames,
25
26
  } from "./type-names.js";
@@ -37,10 +37,12 @@ export function mangleCollidingTypes(programs, pkgKey, pkgName, owners) {
37
37
  // backends key types by bare name. A local type whose name another type of the
38
38
  // package also uses (a package-level type or a local of another function)
39
39
  // is renamed to `Name__L<n>` for its scope; `%T` and diagnostics drop the
40
- // suffix (see `displayTypeName`). Returns whether anything was renamed.
40
+ // suffix (see `displayTypeName`). `reserved` holds type names other packages
41
+ // own: a local type spelled like one is renamed too, so it cannot clash with
42
+ // the imported type in the flat module. Returns whether anything was renamed.
41
43
  let localTypeCounter = 0;
42
44
 
43
- export function renameLocalTypes(programs) {
45
+ export function renameLocalTypes(programs, reserved = new Set()) {
44
46
  const counts = new Map();
45
47
  const count = (node) => {
46
48
  if (!node || typeof node !== "object") return;
@@ -50,34 +52,88 @@ export function renameLocalTypes(programs) {
50
52
  forEachChild(node, count);
51
53
  };
52
54
  for (const p of programs) count(p.decls);
53
- const dup = new Set([...counts].filter(([, n]) => n > 1).map(([k]) => k));
54
- if (dup.size === 0) return false;
55
+ const dup = new Set(
56
+ [...counts].filter(([k, n]) => n > 1 || reserved.has(k)).map(([k]) => k),
57
+ );
55
58
  let changed = false;
56
59
  // Inner scopes first, so an outer rename never touches a nested
57
- // declaration that already has its own name.
58
- const scan = (node) => {
60
+ // declaration that already has its own name. `visible` holds the value
61
+ // names declared so far in the enclosing scopes.
62
+ const scan = (node, visible) => {
59
63
  if (!node || typeof node !== "object") return;
60
64
  if (Array.isArray(node)) {
61
- node.forEach(scan);
65
+ const seen = new Set(visible);
62
66
  for (let i = 0; i < node.length; i++) {
63
67
  const d = node[i];
64
- if (d?.kind !== "TypeDecl" || !dup.has(d.name)) continue;
65
- const renamed = new Map([
66
- [d.name, `${d.name}__L${++localTypeCounter}`],
67
- ]);
68
- for (let j = i; j < node.length; j++)
69
- renameTypeRefs(node[j], renamed, new Set(), new Set());
70
- changed = true;
68
+ scan(d, seen);
69
+ if (d?.kind === "TypeDecl") {
70
+ // A value of the same name would clash with the type in JS.
71
+ const stop = node.findIndex(
72
+ (s, j) => j > i && declaresValue(s, d.name),
73
+ );
74
+ if (dup.has(d.name) || seen.has(d.name) || stop >= 0) {
75
+ const renamed = new Map([
76
+ [d.name, `${d.name}__L${++localTypeCounter}`],
77
+ ]);
78
+ const end = stop >= 0 ? stop : node.length - 1;
79
+ for (let j = i; j <= end; j++) {
80
+ // The shadowing declaration's own right-hand side and type
81
+ // are evaluated before its new value is in scope.
82
+ const target =
83
+ j === stop && node[j].kind === "DefineStmt"
84
+ ? node[j].rhs
85
+ : node[j];
86
+ renameTypeRefs(target, renamed, new Set(), new Set());
87
+ }
88
+ changed = true;
89
+ }
90
+ } else if (d) for (const n of valueNames(d)) seen.add(n);
71
91
  }
72
92
  return;
73
93
  }
74
- forEachChild(node, scan);
94
+ const here = FUNC_KINDS.has(node.kind)
95
+ ? new Set([...visible, ...declaredParams(node)])
96
+ : visible;
97
+ forEachChild(node, (c) => scan(c, here));
75
98
  };
76
99
  for (const p of programs)
77
- for (const d of p.decls ?? []) if (d.kind !== "TypeDecl") scan(d);
100
+ for (const d of p.decls ?? [])
101
+ if (d.kind !== "TypeDecl") scan(d, new Set());
78
102
  return changed;
79
103
  }
80
104
 
105
+ const VALUE_DECL_KINDS = new Set(["DefineStmt", "VarDecl", "ConstDecl"]);
106
+
107
+ // Names a statement declares as values in its own scope.
108
+ function valueNames(stmt) {
109
+ return VALUE_DECL_KINDS.has(stmt.kind) ? declaredNames(stmt) : [];
110
+ }
111
+
112
+ function declaresValue(stmt, name) {
113
+ return valueNames(stmt).includes(name);
114
+ }
115
+
116
+ function declaredParams(fn) {
117
+ const names = (fn.params ?? []).map((p) => p.name).filter(Boolean);
118
+ if (fn.recvName) names.push(fn.recvName);
119
+ return names;
120
+ }
121
+
122
+ // A package compiled later that declares a type this package only declares
123
+ // inside a function is renamed by `mangleCollidingTypes`, so `Pt` of one package
124
+ // and a local `Pt` of another never meet in the flat module.
125
+ export function registerLocalTypeOwners(programs, pkgKey, owners) {
126
+ if (!owners) return;
127
+ const visit = (node) => {
128
+ if (!node || typeof node !== "object") return;
129
+ if (Array.isArray(node)) return node.forEach(visit);
130
+ if (node.kind === "TypeDecl" && !owners.has(node.name))
131
+ owners.set(node.name, pkgKey);
132
+ forEachChild(node, visit);
133
+ };
134
+ for (const p of programs) visit(p.decls);
135
+ }
136
+
81
137
  // `tparams` are type parameters in scope; `values` are names a function
82
138
  // declares as parameters or locals, which shadow a same-spelled bare `Ident`.
83
139
  function renameTypeRefs(node, renamed, tparams, values) {
@@ -1,7 +1,10 @@
1
1
  // src/lower/embedding.js
2
2
  // Computes promoted methods and delegation stubs for embedded structs.
3
3
 
4
- import { embeddedFieldName } from "../typechecker/types.js";
4
+ import {
5
+ ambiguousPromotions,
6
+ embeddedFieldName,
7
+ } from "../typechecker/types.js";
5
8
 
6
9
  // A method promoted from an embedded struct value but declared on its
7
10
  // pointer receiver is outside the method set of the outer struct's value.
@@ -10,36 +13,60 @@ function isPtrOnlyPromotion(rawEmbed, embed, mName, mType) {
10
13
  return Boolean((mType ?? embed.methods?.get(mName))?._ptrRecv);
11
14
  }
12
15
 
16
+ // A pointer-receiver method reached through an embedded pointer, which may
17
+ // be nil.
18
+ function isNilablePtrRecv(rawEmbed, mType) {
19
+ return rawEmbed.kind === "pointer" && Boolean(mType?._ptrRecv);
20
+ }
21
+
22
+ // An embedded interface (named, literal or `error`) forwards each method of
23
+ // its method set to the interface value in the field.
24
+ function pushInterfaceStubs(stubs, declared, iface, fieldName) {
25
+ for (const mName of iface.methods?.keys() ?? []) {
26
+ if (declared.has(mName)) continue;
27
+ stubs.push({ methodName: mName, fieldName, ifaceEmbed: true });
28
+ declared.add(mName);
29
+ }
30
+ }
31
+
32
+ function pushValueEmbedStubs(stubs, declared, embed, embedName, fieldName) {
33
+ for (const [mName, mType] of embed.methods?.entries() ?? []) {
34
+ if (declared.has(mName) || mType?._ptrRecv) continue;
35
+ stubs.push({ methodName: mName, embedName, fieldName, valueEmbed: true });
36
+ declared.add(mName);
37
+ }
38
+ }
39
+
13
40
  export function computeEmbeddedStubs(name, methodDecls, checker) {
14
41
  if (!checker) return [];
15
42
  const resolvedType = checker.types.get(name)?.underlying;
16
43
  if (resolvedType?.kind !== "struct" || !resolvedType._embeds) return [];
17
44
 
18
- const declared = new Set(methodDecls.map((m) => m.name));
45
+ // A method two embeds declare at the same depth is in neither method set:
46
+ // no stub, so the value does not satisfy an interface that needs it.
47
+ const declared = new Set([
48
+ ...methodDecls.map((m) => m.name),
49
+ ...ambiguousPromotions(resolvedType),
50
+ ]);
19
51
  const stubs = [];
20
52
 
21
53
  const embedded = (resolvedType._structFields ?? []).filter((f) => f.embedded);
22
54
  for (const [i, rawEmbed] of resolvedType._embeds.entries()) {
23
55
  const fieldName = embedded[i] ? embeddedFieldName(embedded[i]) : null;
24
56
  const embed = rawEmbed.kind === "pointer" ? rawEmbed.base : rawEmbed;
57
+ const embedBase = embed.kind === "named" ? embed.underlying : embed;
58
+ if (embedBase?.kind === "interface") {
59
+ if (rawEmbed.kind !== "pointer" && fieldName)
60
+ pushInterfaceStubs(stubs, declared, embedBase, fieldName);
61
+ continue;
62
+ }
25
63
  const embedName = embed.kind === "named" ? embed.name : null;
26
64
  if (!embedName) continue;
27
- const embedBase = embed.kind === "named" ? embed.underlying : embed;
28
65
  if (embedBase?.kind !== "struct" || !embedBase.methods) {
29
66
  // A named non-struct type (`time.Duration`) promotes its value
30
67
  // methods; the stub passes the embedded value as the receiver.
31
- if (rawEmbed.kind === "pointer" || embedBase?.kind === "interface")
32
- continue;
33
- for (const [mName, mType] of embed.methods?.entries() ?? []) {
34
- if (declared.has(mName) || mType?._ptrRecv) continue;
35
- stubs.push({
36
- methodName: mName,
37
- embedName,
38
- fieldName,
39
- valueEmbed: true,
40
- });
41
- declared.add(mName);
42
- }
68
+ if (rawEmbed.kind !== "pointer")
69
+ pushValueEmbedStubs(stubs, declared, embed, embedName, fieldName);
43
70
  continue;
44
71
  }
45
72
 
@@ -50,6 +77,7 @@ export function computeEmbeddedStubs(name, methodDecls, checker) {
50
77
  embedName,
51
78
  fieldName,
52
79
  ptrOnly: isPtrOnlyPromotion(rawEmbed, embed, mName, mType),
80
+ nilPtrRecv: isNilablePtrRecv(rawEmbed, mType),
53
81
  });
54
82
  declared.add(mName); // Prevent duplicate stubs for diamond embeds
55
83
  }
@@ -34,7 +34,7 @@ import {
34
34
  under,
35
35
  walkNodes,
36
36
  } from "./gen-types.js";
37
- import { holdsFunc, isCompositeType } from "./json-codecs.js";
37
+ import { holdsFunc } from "./json-codecs.js";
38
38
 
39
39
  const FMT_PATH = "fmt";
40
40
  const RT_PATH = "internal/fmtrt";
@@ -137,6 +137,12 @@ const returnsString = (m) => {
137
137
  );
138
138
  };
139
139
 
140
+ // A map, slice or array type: one that generic code boxes into an interface
141
+ // is not spelled in the package, so the formatter lookup must describe it.
142
+ function isMapSliceOrArray(t) {
143
+ return t?.kind === "map" || t?.kind === "slice" || t?.kind === "array";
144
+ }
145
+
140
146
  class FmtGen extends TypeSpeller {
141
147
  constructor(checker, pkgName, qualifiers, programs, formatted = false) {
142
148
  super(pkgName, qualifiers, programs);
@@ -559,12 +565,17 @@ class FmtGen extends TypeSpeller {
559
565
  if (via === "declared" && !this.topLevel.has(type.name)) continue;
560
566
  add(type);
561
567
  }
562
- // Only instances of generic types (and pointers to them): they are the
563
- // types a package never spells, and any other type here would take the
564
- // place of the dispatch a type parameter value already has.
565
- for (const type of this.genericBodyTypes()) {
568
+ // Only instances of generic types (and pointers to them), and maps, slices and
569
+ // arrays a generic body boxes into an interface: they are the types a package
570
+ // never spells, and any other type here would take the place of the
571
+ // dispatch a type parameter value already has.
572
+ for (const { type, boxed } of this.genericBodyTypes()) {
566
573
  const base = type.kind === "pointer" ? type.base : type;
567
- if (base.kind === "named" && base.typeArgs?.length > 0) add(type);
574
+ if (
575
+ (boxed && isMapSliceOrArray(type)) ||
576
+ (base.kind === "named" && base.typeArgs?.length > 0)
577
+ )
578
+ add(type);
568
579
  }
569
580
  return [...found.values()];
570
581
  }
@@ -593,16 +604,28 @@ class FmtGen extends TypeSpeller {
593
604
  while (queue.length > 0) {
594
605
  const { decl, map } = queue.shift();
595
606
  walkNodes(decl.body, (node) => {
596
- // A type without a type parameter is found by the walk of the
597
- // package already.
598
- if (node._type && mentionsTypeParam(node._type))
599
- out.push(defaultType(this.checker.substituteType(node._type, map)));
607
+ out.push(...this.bodyNodeTypes(node, map));
600
608
  request(node, map);
601
609
  });
602
610
  }
603
611
  return out;
604
612
  }
605
613
 
614
+ // The types a node of an instantiated generic body adds: the node's own
615
+ // type once substituted (a type without a type parameter is found by the
616
+ // walk of the package already), and a map it boxes into an interface,
617
+ // which is a type of its own (a type parameter's value keeps its dispatch).
618
+ bodyNodeTypes(node, map) {
619
+ const sub = (t) => defaultType(this.checker.substituteType(t, map));
620
+ const out = [];
621
+ if (node._type && mentionsTypeParam(node._type))
622
+ out.push({ type: sub(node._type) });
623
+ const src = node._ifaceSrc;
624
+ if (isMapSliceOrArray(src) && mentionsTypeParam(src))
625
+ out.push({ type: sub(src), boxed: true });
626
+ return out;
627
+ }
628
+
606
629
  // The instantiation of a generic function that `call` makes, with its
607
630
  // type arguments substituted through `map` (the instantiation of the
608
631
  // function that contains the call), or null for any other node or one
@@ -626,11 +649,12 @@ class FmtGen extends TypeSpeller {
626
649
  if (isInterface(t) || under(t)?.kind === "interface") return false;
627
650
  if (t.kind === "basic") return false;
628
651
  if (t.kind === "func") return true;
629
- if (holdsFunc(t)) return false;
630
- // An unnamed array or map of small integers would shadow its look-alikes
631
- // in the type switch (see SHARED_REPR in json-codecs).
652
+ // An array, map or slice is boxed by its Go type, so one with small
653
+ // integer or boolean keys or elements no longer shadows its look-alikes in
654
+ // the type switch. One holding funcs is described too, so `%T` names it.
632
655
  if (t.kind === "array" || t.kind === "map" || t.kind === "slice")
633
- return isCompositeType(t);
656
+ return true;
657
+ if (holdsFunc(t)) return false;
634
658
  return true;
635
659
  }
636
660
 
@@ -1,7 +1,7 @@
1
1
  // src/lower/functions.js
2
2
  // Analyzes function signatures, named returns and defer structures.
3
3
 
4
- import { forEachChild, someChild } from "./walk.js";
4
+ import { forEachChild, hasCallOrReceive, someChild } from "./walk.js";
5
5
 
6
6
  export function extractNamedReturns(decl) {
7
7
  const named = decl?.returnType?._namedReturns;
@@ -146,6 +146,80 @@ function deferCalleeMayRecover(funcExpr, ctx) {
146
146
  return true;
147
147
  }
148
148
 
149
+ // A constant argument (`2.5`, `"a"`, `nil`, `k` for a constant `k`, `-1`)
150
+ // has no value to capture at the `defer` statement, so it stays in the
151
+ // deferred call, where it takes the callee's parameter type exactly as in a
152
+ // direct call. Hoisting it into a temporary would default its type (`float64`,
153
+ // `int`) and reject a defined-type, interface, pointer or generic parameter.
154
+ function isDeferConstant(arg) {
155
+ if (arg?.kind === "BasicLit" && arg.litKind === "NIL") return true;
156
+ if (arg?._type?.kind !== "untyped") return false;
157
+ if (arg._const) return true;
158
+ switch (arg.kind) {
159
+ case "BasicLit":
160
+ case "ImagLit":
161
+ return true;
162
+ case "ParenExpr":
163
+ return isDeferConstant(arg.expr);
164
+ case "UnaryExpr":
165
+ return isDeferConstant(arg.operand);
166
+ case "BinaryExpr":
167
+ return isDeferConstant(arg.left) && isDeferConstant(arg.right);
168
+ default:
169
+ return false;
170
+ }
171
+ }
172
+
173
+ // A package-level basic or defined type a temporary can be declared with by
174
+ // its name; other parameter types (qualified, generic, composite) are not
175
+ // hoisted with a declared type.
176
+ function isNamedScalarParam(t) {
177
+ if (t?.kind === "basic")
178
+ return t.name !== "any" && /^[A-Za-z_]\w*$/.test(t.name ?? "");
179
+ return (
180
+ t?.kind === "named" &&
181
+ t.underlying?.kind === "basic" &&
182
+ !t.typeArgs?.length &&
183
+ /^[A-Za-z_]\w*$/.test(t.name ?? "")
184
+ );
185
+ }
186
+
187
+ // A non-constant untyped expression (`1 << n`) the typechecker already gave
188
+ // its parameter's type: it is still untyped when the lowered code is checked
189
+ // again, so its temporary needs that type declared.
190
+ function isRetypedUntyped(e) {
191
+ if (!e || e._const) return false;
192
+ switch (e.kind) {
193
+ case "ParenExpr":
194
+ return isRetypedUntyped(e.expr);
195
+ case "UnaryExpr":
196
+ return isRetypedUntyped(e.operand);
197
+ case "BinaryExpr":
198
+ if (e.op === "<<" || e.op === ">>") return Boolean(e.left?._const);
199
+ return isRetypedUntyped(e.left) || isRetypedUntyped(e.right);
200
+ default:
201
+ return false;
202
+ }
203
+ }
204
+
205
+ // The type of the temporary that holds deferred argument `a`: an untyped
206
+ // non-constant value (a shift, a comparison) takes its basic or defined
207
+ // scalar parameter's type, as it does in a direct call. A variadic call's
208
+ // trailing arguments take the element type.
209
+ function deferTempType(funcType, a, arg) {
210
+ const argType = arg._type;
211
+ if (argType?.kind !== "untyped" && !isRetypedUntyped(arg)) return argType;
212
+ const params = funcType?.params;
213
+ let param = params?.[a];
214
+ if (funcType?.variadic && a >= params.length - 1) {
215
+ // The last parameter's type is the element type (a slice only when
216
+ // a table spells it that way).
217
+ param = params[params.length - 1];
218
+ if (!isNamedScalarParam(param)) param = param?.elem;
219
+ }
220
+ return isNamedScalarParam(param) ? param : argType;
221
+ }
222
+
149
223
  function normalizeDeferStmt(stmt, parentList = null, index = -1, ctx = null) {
150
224
  if (stmt.call?.kind !== "CallExpr") return 0;
151
225
  const call = stmt.call;
@@ -245,19 +319,40 @@ function normalizeDeferStmt(stmt, parentList = null, index = -1, ctx = null) {
245
319
  lhs: tupleLhs,
246
320
  rhs: [arg],
247
321
  });
322
+ } else if (isDeferConstant(arg)) {
323
+ tempIdents.push(arg);
248
324
  } else {
249
325
  defargCounter++;
250
326
  const tempName = `__defarg$${defargCounter}`;
327
+ const tempType = deferTempType(call.func?._type, a, arg);
251
328
  tempIdents.push({
252
329
  kind: "Ident",
253
330
  name: tempName,
254
- _type: arg._type,
255
- });
256
- tempStmts.push({
257
- kind: "DefineStmt",
258
- lhs: [{ kind: "Ident", name: tempName, _type: arg._type }],
259
- rhs: [arg],
331
+ _type: tempType,
332
+ // A spread argument stays one: the call gets the slice itself.
333
+ ...(arg._spread && { _spread: true }),
260
334
  });
335
+ tempStmts.push(
336
+ tempType === arg._type &&
337
+ !(isRetypedUntyped(arg) && isNamedScalarParam(tempType))
338
+ ? {
339
+ kind: "DefineStmt",
340
+ lhs: [{ kind: "Ident", name: tempName, _type: tempType }],
341
+ rhs: [arg],
342
+ }
343
+ : {
344
+ // Declared with the parameter's type so the lowered code
345
+ // type-checks again with the constant converted.
346
+ kind: "VarDecl",
347
+ decls: [
348
+ {
349
+ names: [tempName],
350
+ type: { kind: "TypeName", name: tempType.name },
351
+ value: [arg],
352
+ },
353
+ ],
354
+ },
355
+ );
261
356
  }
262
357
  }
263
358
  }
@@ -302,3 +397,124 @@ function normalizeDeferStmt(stmt, parentList = null, index = -1, ctx = null) {
302
397
 
303
398
  return tempStmts.length;
304
399
  }
400
+
401
+ let fwdCounter = 0;
402
+
403
+ /**
404
+ * Splits a multi-value call forwarded into interfaces into temps:
405
+ * `return f()` where some result is an interface becomes
406
+ * `__fwd$1, __fwd$2 := f(); return __fwd$1, __fwd$2`, and `g(f())` where
407
+ * some parameter is an interface becomes `…; g(__fwd$1, __fwd$2)`. The
408
+ * typechecker records per-element interface-box marks in `call._fwdIface`;
409
+ * each temp takes its element's marks, so both backends box it through their
410
+ * ordinary per-value path. Forwards with no interface element are left alone.
411
+ *
412
+ * `g(f())` is split only as the operand of a statement (expression
413
+ * statement, single-value `:=`/`=`/`var`/`return`) whose callee evaluates
414
+ * no call, so hoisting `f()` keeps Go's evaluation order.
415
+ */
416
+ export function splitForwardedTuples(programOrPrograms) {
417
+ fwdCounter = 0;
418
+ const programs = Array.isArray(programOrPrograms)
419
+ ? programOrPrograms
420
+ : [programOrPrograms];
421
+ for (const program of programs)
422
+ for (const d of program?.decls ?? []) splitForwardsIn(d);
423
+ }
424
+
425
+ function splitForwardsIn(node) {
426
+ if (!node || typeof node !== "object") return;
427
+ if (Array.isArray(node)) {
428
+ for (let i = 0; i < node.length; i++) {
429
+ const item = node[i];
430
+ splitForwardsIn(item);
431
+ const defines = splitForwardsInStmt(item);
432
+ node.splice(i, 0, ...defines);
433
+ i += defines.length;
434
+ }
435
+ return;
436
+ }
437
+ forEachChild(node, splitForwardsIn);
438
+ }
439
+
440
+ // The `:=` statements to insert before `stmt` (in order) after rewriting its
441
+ // forwarded calls to read the temps.
442
+ function splitForwardsInStmt(stmt) {
443
+ const defines = [];
444
+ const operand = statementOperand(stmt);
445
+ if (
446
+ operand?.kind === "CallExpr" &&
447
+ operand._multiForward &&
448
+ operand.args?.length === 1 &&
449
+ !hasCallOrReceive(operand.func)
450
+ ) {
451
+ const split = forwardTemps(operand.args[0]);
452
+ if (split) {
453
+ operand.args = split.operands;
454
+ operand._multiForward = false;
455
+ defines.push(split.define);
456
+ }
457
+ }
458
+ if (stmt?.kind === "ReturnStmt") {
459
+ const define = splitForwardedReturn(stmt);
460
+ if (define) defines.push(define);
461
+ }
462
+ return defines;
463
+ }
464
+
465
+ // The single expression a statement evaluates last, or null.
466
+ function statementOperand(stmt) {
467
+ switch (stmt?.kind) {
468
+ case "ExprStmt":
469
+ return stmt.expr;
470
+ case "DefineStmt":
471
+ case "AssignStmt":
472
+ return stmt.rhs?.length === 1 &&
473
+ stmt.lhs?.every((e) => e.kind === "Ident")
474
+ ? stmt.rhs[0]
475
+ : null;
476
+ case "ReturnStmt":
477
+ return stmt.values?.length === 1 ? stmt.values[0] : null;
478
+ case "VarDecl":
479
+ return stmt.decls?.length === 1 && stmt.decls[0].value?.length === 1
480
+ ? stmt.decls[0].value[0]
481
+ : null;
482
+ default:
483
+ return null;
484
+ }
485
+ }
486
+
487
+ // Splits one forwarded call into temps: returns the `:=` statement defining
488
+ // them and the operand idents (carrying each element's interface-box marks),
489
+ // or null when the call needs no split.
490
+ function forwardTemps(call) {
491
+ const holders = call?._fwdIface;
492
+ const types = call?._type?.types;
493
+ if (!holders || !Array.isArray(types) || types.length !== holders.length)
494
+ return null;
495
+ const names = types.map(() => `__fwd$${++fwdCounter}`);
496
+ const define = {
497
+ kind: "DefineStmt",
498
+ lhs: names.map((name, i) => ({ kind: "Ident", name, _type: types[i] })),
499
+ rhs: [call],
500
+ };
501
+ // The call is no longer a forward; a cached AST must not split it again.
502
+ call._fwdIface = undefined;
503
+ const operands = names.map((name, i) => ({
504
+ kind: "Ident",
505
+ name,
506
+ _type: types[i],
507
+ ...holders[i],
508
+ }));
509
+ return { define, operands };
510
+ }
511
+
512
+ function splitForwardedReturn(stmt) {
513
+ if (stmt.values?.length !== 1) return null;
514
+ const split = forwardTemps(stmt.values[0]);
515
+ if (!split) return null;
516
+ stmt.values = split.operands;
517
+ if (Array.isArray(stmt._types))
518
+ stmt._types = split.operands.map((o) => o._type);
519
+ return split.define;
520
+ }
@@ -59,8 +59,16 @@ export function walkNodes(root, fn) {
59
59
  }
60
60
  fn(root);
61
61
  forEachChild(root, (c) => walkNodes(c, fn));
62
+ // templ bodies keep their Go expressions in non-enumerable properties
63
+ // (see parser/templ.js), which forEachChild does not visit.
64
+ for (const name of TEMPL_EXPR_PROPS) {
65
+ const d = Object.getOwnPropertyDescriptor(root, name);
66
+ if (d && !d.enumerable) walkNodes(d.value, fn);
67
+ }
62
68
  }
63
69
 
70
+ const TEMPL_EXPR_PROPS = ["expr", "exprs", "forStmt"];
71
+
64
72
  // True when `t` mentions a type parameter (a generic declaration, or a type
65
73
  // built from one inside a generic function).
66
74
  export function mentionsTypeParam(t, seen = new Set()) {
@@ -126,8 +134,9 @@ export function importsFor(qualifiers) {
126
134
 
127
135
  // Every type a closed-world generator may need to describe: the entries
128
136
  // { type, node, via } for the type arguments of generic calls (`typeArg`),
129
- // the type of every expression (`expr`) and the package's declared named
130
- // types (`declared`, node null).
137
+ // the type of every expression (`expr`), the static type of every value
138
+ // converted to an interface (`boxed`) and the package's declared named types
139
+ // (`declared`, node null).
131
140
  export function collectProgramTypes(programs, checker) {
132
141
  const found = [];
133
142
  for (const p of programs)
@@ -136,6 +145,14 @@ export function collectProgramTypes(programs, checker) {
136
145
  found.push({ type: defaultType(a), node, via: "typeArg" });
137
146
  if (node._type)
138
147
  found.push({ type: defaultType(node._type), node, via: "expr" });
148
+ if (node._ifaceSrc)
149
+ found.push({ type: defaultType(node._ifaceSrc), node, via: "boxed" });
150
+ // A multi-value call forwarded into interfaces (`return f()`,
151
+ // `g(f())`) keeps one mark holder per element until lowering
152
+ // splits it.
153
+ for (const h of node._fwdIface ?? [])
154
+ if (h?._ifaceSrc)
155
+ found.push({ type: defaultType(h._ifaceSrc), node, via: "boxed" });
139
156
  });
140
157
  for (const [name, t] of checker.types) {
141
158
  if (t?.kind !== "named" || t.name !== name) continue;
@@ -253,6 +270,8 @@ export class TypeSpeller {
253
270
  const byKind = {
254
271
  basic: () => t.name !== "void" && t.name !== "nil",
255
272
  named: () => this.spellableNamed(t, ok, seen),
273
+ // A JS-interop (.d.ts) type has no Go source to write.
274
+ namespace: () => false,
256
275
  pointer: () => ok(t.base),
257
276
  slice: () => ok(t.elem),
258
277
  array: () => ok(t.elem),
@@ -16,7 +16,12 @@ import { scanAddressTaken } from "./boxing.js";
16
16
  import { analyzeCaptures } from "./captures.js";
17
17
  import { computeEmbeddedStubs } from "./embedding.js";
18
18
  import { paramEscapes } from "./escape.js";
19
- import { extractNamedReturns, hasDefer, normalizeDefers } from "./functions.js";
19
+ import {
20
+ extractNamedReturns,
21
+ hasDefer,
22
+ normalizeDefers,
23
+ splitForwardedTuples,
24
+ } from "./functions.js";
20
25
  import { computeGlobalAnalysis } from "./globals.js";
21
26
  import { OwnershipContext, takesElementAddress } from "./ownership.js";
22
27
 
@@ -83,6 +88,7 @@ export function lower(programOrPrograms, checker = null) {
83
88
  ? programOrPrograms
84
89
  : [programOrPrograms];
85
90
 
91
+ splitForwardedTuples(programs);
86
92
  normalizeDefers(programs);
87
93
 
88
94
  res.elemAddrTaken = programs.some((p) => takesElementAddress(p?.decls));