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
package/README.md CHANGED
@@ -461,7 +461,7 @@ For detailed command flags, project configuration (`gofront.json`), multi-file p
461
461
  ## Tests
462
462
 
463
463
  ```sh
464
- npm test # unit, zero-alloc perf, and example tests (~3,800 tests, no browser required)
464
+ npm test # unit, zero-alloc perf, and example tests (~4,700 tests, no browser required)
465
465
  npm run test:e2e # E2E browser tests (Playwright, headless Chromium)
466
466
  npm run bench # Raycast, JSON, and SIMD particle benchmarks (pure JS vs hybrid WASM)
467
467
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gofront",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "author": "Luuk van Venrooij",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -47,7 +47,7 @@
47
47
  "prepare": "lefthook install"
48
48
  },
49
49
  "devDependencies": {
50
- "@biomejs/biome": "^2.5.15",
50
+ "@biomejs/biome": "^2.6.6",
51
51
  "@playwright/test": "^1.64.1",
52
52
  "c8": "^12.0.0",
53
53
  "esbuild": "^0.28.2",
@@ -16,6 +16,7 @@ import {
16
16
  is64BitUnsigned,
17
17
  isAny,
18
18
  isComplex,
19
+ isComplex64,
19
20
  isTypedArraySlice,
20
21
  typedArrayConstructorForElem,
21
22
  typeStr,
@@ -42,8 +43,92 @@ export function comparesToNil(node, name) {
42
43
  return someChild(node, (c) => comparesToNil(c, name));
43
44
  }
44
45
 
46
+ // True when evaluating `node` reads through the receiver `name` (`r.f`,
47
+ // `r[i]`, `*r`), which panics on a nil receiver anyway. Calling a method of
48
+ // `safe` on it is not a read: that method accepts a nil receiver. A func
49
+ // literal's body is not evaluated where it appears, so it is not searched.
50
+ export function derefsReceiver(node, name, safe) {
51
+ if (Array.isArray(node))
52
+ return node.some((n) => derefsReceiver(n, name, safe));
53
+ if (!node || typeof node !== "object" || node.kind === "FuncLit")
54
+ return false;
55
+ const isRecv = (e) => e?.kind === "Ident" && e.name === name;
56
+ if (node.kind === "SelectorExpr" && isRecv(node.expr))
57
+ return !safe.has(node.field);
58
+ if (
59
+ (node.kind === "IndexExpr" || node.kind === "SliceExpr") &&
60
+ isRecv(node.expr)
61
+ )
62
+ return true;
63
+ if (node.kind === "UnaryExpr" && node.op === "*" && isRecv(node.operand))
64
+ return true;
65
+ return someChild(node, (c) => derefsReceiver(c, name, safe));
66
+ }
67
+
68
+ // True when `stmts` (a method body) reads through the receiver `name` before
69
+ // anything could skip the read: only the statements that always run first
70
+ // count, up to the first branch, loop, return or rebinding of `name`. A
71
+ // method that does so panics on a nil receiver either way.
72
+ export function derefsReceiverFirst(stmts, name, safe) {
73
+ return receiverPrefix(stmts, name, safe) === "deref";
74
+ }
75
+
76
+ // What one statement of a method body's unconditional prefix does with the
77
+ // receiver: "deref" reads through it, "stop" may skip the rest (a branch,
78
+ // loop, return, panic or rebinding of the name), "next" runs on.
79
+ const binds = (lhs, name) =>
80
+ lhs.some((e) => e?.kind === "Ident" && e.name === name);
81
+ const branches = (reads, ...parts) => (parts.some(reads) ? "deref" : "stop");
82
+ const RECEIVER_STEP = {
83
+ Block: (s, _reads, name, safe) => receiverPrefix(s.stmts, name, safe),
84
+ IfStmt: (s, reads) => branches(reads, s.init, s.cond),
85
+ ForStmt: (s, reads) => branches(reads, s.init, s.cond),
86
+ SwitchStmt: (s, reads) => branches(reads, s.init, s.tag),
87
+ TypeSwitchStmt: (s, reads) => branches(reads, s.expr),
88
+ ReturnStmt: (s, reads) => branches(reads, s.values),
89
+ ExprStmt: (s, reads) => {
90
+ if (reads(s.expr)) return "deref";
91
+ const panics = s.expr?.kind === "CallExpr" && s.expr.func?.name === "panic";
92
+ return panics ? "stop" : "next";
93
+ },
94
+ DefineStmt: (s, reads, name) => {
95
+ if (reads(s.rhs) || reads(s.lhs.filter((e) => e?.kind !== "Ident")))
96
+ return "deref";
97
+ return binds(s.lhs, name) ? "stop" : "next";
98
+ },
99
+ IncDecStmt: (s, reads) => (reads(s.expr) ? "deref" : "next"),
100
+ VarDecl: (s, reads, name) => {
101
+ if (s.decls.some((d) => reads(d.value))) return "deref";
102
+ const named = (d) => d.names.some((n) => (n?.name ?? n) === name);
103
+ return s.decls.some(named) ? "stop" : "next";
104
+ },
105
+ };
106
+ RECEIVER_STEP.AssignStmt = RECEIVER_STEP.DefineStmt;
107
+
108
+ // "deref" when the unconditional prefix of `stmts` reads through the
109
+ // receiver, "stop" when something may skip the rest, else "next".
110
+ function receiverPrefix(stmts, name, safe) {
111
+ const reads = (n) => derefsReceiver(n, name, safe);
112
+ for (const s of stmts ?? []) {
113
+ const step = RECEIVER_STEP[s?.kind];
114
+ const r = step ? step(s, reads, name, safe) : "stop";
115
+ if (r !== "next") return r;
116
+ }
117
+ return "next";
118
+ }
119
+
45
120
  /** @typedef {import('./index.js').CodeGen} CodeGen */
46
121
 
122
+ // True for a map, slice or array type that mentions a type parameter: one
123
+ // that generic code boxes into an interface is tagged by its instantiated
124
+ // type.
125
+ export function isGenericComposite(t) {
126
+ return (
127
+ (t?.kind === "map" || t?.kind === "slice" || t?.kind === "array") &&
128
+ mentionsTypeParam(t)
129
+ );
130
+ }
131
+
47
132
  // True when type `t` mentions a type parameter anywhere.
48
133
  export function mentionsTypeParam(t, depth = 0) {
49
134
  if (!t || depth > 16) return false;
@@ -130,21 +215,6 @@ const STRICT_OPS = new Set([
130
215
  ">>",
131
216
  ]);
132
217
 
133
- function collectFunctionLabels(node, labels = new Set()) {
134
- if (!node || typeof node !== "object") return labels;
135
- if (Array.isArray(node)) {
136
- for (const item of node) collectFunctionLabels(item, labels);
137
- return labels;
138
- }
139
- if (node.kind === "LabeledStmt" && node.label) labels.add(node.label);
140
- if (node.kind === "ForStmt" && node.label) labels.add(node.label);
141
- for (const k of Object.keys(node)) {
142
- if (k.startsWith("_")) continue;
143
- collectFunctionLabels(node[k], labels);
144
- }
145
- return labels;
146
- }
147
-
148
218
  const NAMED_ZERO_TABLE = {
149
219
  "simd.F32x4": [null, "Object.freeze([0, 0, 0, 0])"],
150
220
  F32x4: [null, "Object.freeze([0, 0, 0, 0])"],
@@ -522,6 +592,41 @@ function hasTypeParamType(t) {
522
592
  ).includes('"typeParam"');
523
593
  }
524
594
 
595
+ // Complex constant arithmetic on [re, im] pairs.
596
+ const COMPLEX_CONST_OPS = {
597
+ "+": (a, b) => [a[0] + b[0], a[1] + b[1]],
598
+ "-": (a, b) => [a[0] - b[0], a[1] - b[1]],
599
+ "*": (a, b) => [a[0] * b[0] - a[1] * b[1], a[0] * b[1] + a[1] * b[0]],
600
+ "/": (a, b) => {
601
+ const d = b[0] * b[0] + b[1] * b[1];
602
+ if (d === 0) return null;
603
+ return [(a[0] * b[0] + a[1] * b[1]) / d, (a[1] * b[0] - a[0] * b[1]) / d];
604
+ },
605
+ };
606
+
607
+ // The [re, im] value of an untyped complex constant expression built from
608
+ // literals and named constants, or null.
609
+ function complexConstParts(e, depth = 0) {
610
+ if (!e || typeof e !== "object" || depth > 32) return null;
611
+ if (e.kind === "ParenExpr") return complexConstParts(e.expr, depth + 1);
612
+ // A named complex constant has no folded value: fold its initializer.
613
+ if (e._complexInit) return complexConstParts(e._complexInit, depth + 1);
614
+ if (e.kind === "ImagLit") return [0, Number(e.value)];
615
+ const val = e._const;
616
+ if (val && (val.k === "FLOAT" || val.k === "INT")) return [Number(val.v), 0];
617
+ if (e.kind === "UnaryExpr") return complexConstNeg(e, depth);
618
+ const op = e.kind === "BinaryExpr" ? COMPLEX_CONST_OPS[e.op] : null;
619
+ const a = op && complexConstParts(e.left, depth + 1);
620
+ const b = a && complexConstParts(e.right, depth + 1);
621
+ return b ? op(a, b) : null;
622
+ }
623
+
624
+ function complexConstNeg(e, depth) {
625
+ if (e.op !== "-" && e.op !== "+") return null;
626
+ const c = complexConstParts(e.operand, depth + 1);
627
+ return c && e.op === "-" ? [-c[0], -c[1]] : c;
628
+ }
629
+
525
630
  export const expressionGenMethods = {
526
631
  genExpr(expr) {
527
632
  if (expr._asComplex && !expr._complexWrapping) {
@@ -605,14 +710,44 @@ export const expressionGenMethods = {
605
710
  }
606
711
  }
607
712
  if (this._isNamedIfaceBox(expr)) return this._genNamedIfaceBoxExpr(expr);
608
- if (expr._ifaceSrc && !expr._ityTagging && !this._isNamedBoxKind(expr)) {
609
- const tag = this._dynTypeTag(expr._ifaceSrc);
713
+ if (
714
+ expr._ifaceSrc &&
715
+ !expr._tfnTagging &&
716
+ this._isTaggedFuncType(expr._ifaceSrc)
717
+ ) {
718
+ // A value of a defined func type keeps that type in an interface.
719
+ expr._tfnTagging = true;
720
+ try {
721
+ this.useHelper("ifaceBox");
722
+ return `__tfn(${this.genExpr(expr)}, ${JSON.stringify(expr._ifaceSrc.name)})`;
723
+ } finally {
724
+ expr._tfnTagging = false;
725
+ }
726
+ }
727
+ if (isGenericComposite(expr._ifaceSrc) && !expr._gmapTagging) {
728
+ const tag = this._genericMapTag(expr._ifaceSrc);
729
+ if (tag) {
730
+ this.useHelper("ifaceBox");
731
+ expr._gmapTagging = true;
732
+ try {
733
+ // An array is copied before it is tagged: a copy made after
734
+ // `__ity` (`.map`) would drop the tag.
735
+ return `__ity(${this.genValueExpr(expr)}, ${tag})`;
736
+ } finally {
737
+ expr._gmapTagging = false;
738
+ }
739
+ }
740
+ }
741
+ {
742
+ const tag = this._ityTag(expr);
610
743
  if (tag) {
611
744
  this.useHelper("ifaceBox");
612
745
  expr._ityTagging = true;
613
746
  try {
614
- const ptr = expr._ifaceSrc.kind === "pointer" ? ", true" : "";
615
- return `__ity(${this.genExpr(expr)}, ${JSON.stringify(tag)}${ptr})`;
747
+ const ptr = this._dynTagFlags(expr._ifaceSrc);
748
+ // An array is copied before it is tagged: the copy is the value
749
+ // the interface holds, and it must keep the tag.
750
+ return `__ity(${this.genValueExpr(expr)}, ${JSON.stringify(tag)}${ptr})`;
616
751
  } finally {
617
752
  expr._ityTagging = false;
618
753
  }
@@ -633,6 +768,10 @@ export const expressionGenMethods = {
633
768
  expr._ifaceBoxing = false;
634
769
  }
635
770
  }
771
+ const f32 = this._genFloat32Const(expr);
772
+ if (f32 !== null) return f32;
773
+ const c64 = this._genComplex64Const(expr);
774
+ if (c64 !== null) return c64;
636
775
  const folded = this._genFoldedConst(expr);
637
776
  if (folded !== null) return folded;
638
777
  switch (expr.kind) {
@@ -687,6 +826,31 @@ export const expressionGenMethods = {
687
826
  return null;
688
827
  },
689
828
 
829
+ // An untyped constant converted to float32 (`var c float32 = 0.01`) is
830
+ // emitted as its float32 value, or null when that changes nothing.
831
+ _genFloat32Const(expr) {
832
+ const val = expr._const;
833
+ if (!expr._f32Const || !val || (val.k !== "FLOAT" && val.k !== "INT"))
834
+ return null;
835
+ const n = Number(val.v);
836
+ if (!Number.isFinite(n)) return null;
837
+ const r = Math.fround(n);
838
+ if (r === n) return null;
839
+ return CONST_LITS.FLOAT(r);
840
+ },
841
+
842
+ // An untyped complex constant that takes type complex64
843
+ // (`var c complex64 = 1 + 0.01i`) is emitted as its folded value with
844
+ // float32 components, or null when it cannot be folded here.
845
+ _genComplex64Const(expr) {
846
+ if (!expr._c64Const) return null;
847
+ const c = complexConstParts(expr);
848
+ if (!c) return null;
849
+ const re = CONST_LITS.FLOAT(Math.fround(c[0]));
850
+ const im = CONST_LITS.FLOAT(Math.fround(c[1]));
851
+ return re === null || im === null ? null : `{ re: ${re}, im: ${im} }`;
852
+ },
853
+
690
854
  // The literal for a folded constant expression (`1 << 40` →
691
855
  // `1099511627776`), or null. Integers outside ±2⁵³ emit the nearest
692
856
  // Number and a precision warning.
@@ -906,10 +1070,22 @@ export const expressionGenMethods = {
906
1070
  }
907
1071
  },
908
1072
 
1073
+ // The type tag `expr` takes on entering an interface, or null.
1074
+ _ityTag(expr) {
1075
+ if (!expr._ifaceSrc || expr._ityTagging || this._isNamedBoxKind(expr))
1076
+ return null;
1077
+ return this._dynTypeTag(expr._ifaceSrc);
1078
+ },
1079
+
909
1080
  // Emits `expr` as a Go value: struct/array operands that alias existing storage are copied.
910
1081
  genValueExpr(expr) {
1082
+ const tagged =
1083
+ Boolean(this._ityTag(expr)) ||
1084
+ (isGenericComposite(expr._ifaceSrc) &&
1085
+ !expr._gmapTagging &&
1086
+ Boolean(this._genericMapTag(expr._ifaceSrc)));
911
1087
  const js = this.genExpr(expr);
912
- if (expr._ifaceBox) return js;
1088
+ if (expr._ifaceBox || tagged) return js;
913
1089
  if (
914
1090
  !(
915
1091
  this._aggregateBase(expr._type) || this._typeParamClone(expr._type, js)
@@ -935,12 +1111,10 @@ export const expressionGenMethods = {
935
1111
 
936
1112
  _withFnCtx(body, fn) {
937
1113
  const prev = this._fnCtx;
938
- const prevLabels = this._functionLabels;
939
- const labels = collectFunctionLabels(
940
- body,
941
- new Set(this._functionLabels ?? []),
942
- );
943
- this._functionLabels = Array.from(labels);
1114
+ // A function starts with no JS labels in scope (see
1115
+ // `_emitIterTargetDispatch`).
1116
+ const prevLabels = this._jsLabels;
1117
+ this._jsLabels = [];
944
1118
  this._fnCtx = {
945
1119
  body,
946
1120
  mut: new Map(),
@@ -954,7 +1128,7 @@ export const expressionGenMethods = {
954
1128
  fn();
955
1129
  } finally {
956
1130
  this._fnCtx = prev;
957
- this._functionLabels = prevLabels;
1131
+ this._jsLabels = prevLabels;
958
1132
  }
959
1133
  },
960
1134
 
@@ -1121,9 +1295,24 @@ export const expressionGenMethods = {
1121
1295
  const namedExpr = this._namedMethodExprInfo(expr);
1122
1296
  if (namedExpr) return `${namedExpr.cls}.$${expr.field}`;
1123
1297
  const rt = expr._methodExprRecv;
1298
+ // `(*MyInt).M` for a pointer-receiver method of a named non-struct
1299
+ // type: the static method takes the pointer, nil included.
1300
+ if (rt?.kind === "pointer") {
1301
+ const info = this._namedBoxInfo(rt.base);
1302
+ const decl = rt.base?.methods?.get(expr.field)?._methodDecl;
1303
+ if (info?.methods.has(expr.field) && decl?.recvPointer)
1304
+ return `${info.cls}.$${expr.field}`;
1305
+ }
1124
1306
  const protoCls = this._ptrMethodProtoClass(rt, expr.field);
1125
- if (protoCls)
1126
- return `((recv, ...args) => ${protoCls}.prototype.${expr.field}.call(recv, ...args))`;
1307
+ if (protoCls) {
1308
+ // A value-receiver method reads `*recv`, which panics on nil.
1309
+ let recv = "recv";
1310
+ if (rt.base.methods.get(expr.field)?._ptrRecv === false) {
1311
+ this.useHelper("nilCall");
1312
+ recv = "__nilcall(recv)";
1313
+ }
1314
+ return `((recv, ...args) => ${protoCls}.prototype.${expr.field}.call(${recv}, ...args))`;
1315
+ }
1127
1316
  return `((recv, ...args) => recv.${expr.field}(...args))`;
1128
1317
  }
1129
1318
  const namedValue = this._genNamedMethodValue(expr);
@@ -1466,7 +1655,7 @@ export const expressionGenMethods = {
1466
1655
  if (!expr._lvalue) return this._genMapIndexExpr(expr, base, idx);
1467
1656
  return `${this._genMapWriteBase(base)}[${idx}]`;
1468
1657
  }
1469
- if (this._is64BitInt(expr.index?._type)) idx = `Number(${idx})`;
1658
+ idx = this._asIndexNumber(idx, expr.index?._type);
1470
1659
  const checked = this._genStrictIndex(expr, base, idx);
1471
1660
  if (checked) return checked;
1472
1661
  if (this._isStringExprType(exprType) && !expr._lvalue)
@@ -1523,14 +1712,10 @@ export const expressionGenMethods = {
1523
1712
  _genSliceExpr(expr) {
1524
1713
  const base = this.genExpr(expr.expr);
1525
1714
  const lo = expr.low
1526
- ? this._is64BitInt(expr.low._type)
1527
- ? `Number(${this.genExpr(expr.low)})`
1528
- : this.genExpr(expr.low)
1715
+ ? this._asIndexNumber(this.genExpr(expr.low), expr.low._type)
1529
1716
  : "0";
1530
1717
  const hi = expr.high
1531
- ? this._is64BitInt(expr.high._type)
1532
- ? `Number(${this.genExpr(expr.high)})`
1533
- : this.genExpr(expr.high)
1718
+ ? this._asIndexNumber(this.genExpr(expr.high), expr.high._type)
1534
1719
  : "undefined";
1535
1720
  if (this._isStringExprType(expr.expr._type))
1536
1721
  return expr.high ? `${base}.slice(${lo}, ${hi})` : `${base}.slice(${lo})`;
@@ -1545,7 +1730,7 @@ export const expressionGenMethods = {
1545
1730
  if (expr._selfReslice && !isTypedArraySlice(expr.expr._type))
1546
1731
  return `__reslice(${base}, ${lo}, ${hi})`;
1547
1732
  const max = expr.max
1548
- ? `, ${this._is64BitInt(expr.max._type) ? `Number(${this.genExpr(expr.max)})` : this.genExpr(expr.max)}`
1733
+ ? `, ${this._asIndexNumber(this.genExpr(expr.max), expr.max._type)}`
1549
1734
  : "";
1550
1735
  return `__slice(${base}, ${lo}, ${hi}${max})`;
1551
1736
  },
@@ -1667,6 +1852,10 @@ export const expressionGenMethods = {
1667
1852
  [...named.underlying.methods.keys()],
1668
1853
  this._unwrapIsMulti(named.underlying.methods.get("Unwrap")),
1669
1854
  );
1855
+ if (this._isTaggedFuncType(named)) {
1856
+ this.useHelper("ifaceBox");
1857
+ return `__isty(${val}, ${JSON.stringify(name)}, "namedfunc")`;
1858
+ }
1670
1859
  if (this._isNamedComposite(named)) {
1671
1860
  this.useHelper("ifaceBox");
1672
1861
  return `__isty(${val}, ${JSON.stringify(name)}, "named")`;
@@ -1727,7 +1916,10 @@ export const expressionGenMethods = {
1727
1916
  this.useHelper("ifaceBox");
1728
1917
  return `__unnil(${val})`;
1729
1918
  }
1730
- if (typeNode?._assertType?.kind === "slice") {
1919
+ if (
1920
+ typeNode?._assertType?.kind === "slice" ||
1921
+ this._isNamedSliceTypeNode(typeNode)
1922
+ ) {
1731
1923
  // A nil slice in an interface is a tagged empty array.
1732
1924
  this.useHelper("ifaceBox");
1733
1925
  return `__unnil(${val})`;
@@ -1739,6 +1931,16 @@ export const expressionGenMethods = {
1739
1931
  return factory ? `(${factory}?.u ? ${val}.__gfUnbox() : ${val})` : val;
1740
1932
  },
1741
1933
 
1934
+ // A type name that denotes a defined slice type (`type S []int`).
1935
+ _isNamedSliceTypeNode(typeNode) {
1936
+ if (typeNode?.kind !== "TypeName") return false;
1937
+ const named = this.checker?.types.get(typeNode.name);
1938
+ return (
1939
+ this._isNamedComposite(named) &&
1940
+ this._underlyingOf(named)?.kind === "slice"
1941
+ );
1942
+ },
1943
+
1742
1944
  _isStructPtrTypeNode(typeNode) {
1743
1945
  const base = typeNode?.kind === "PointerType" ? typeNode.base : null;
1744
1946
  const name =
@@ -1793,6 +1995,10 @@ export const expressionGenMethods = {
1793
1995
  targetType?.kind === "named" ? targetType.underlying : targetType;
1794
1996
  if (base?.kind === "map" && this._isNilLiteral(expr.args[0]))
1795
1997
  return this._nilMapZero();
1998
+ if (this._isTaggedFuncType(targetType)) {
1999
+ this.useHelper("ifaceBox");
2000
+ return `__tfn(${arg}, ${JSON.stringify(targetType.name)})`;
2001
+ }
1796
2002
  if (
1797
2003
  targetType?.kind === "named" &&
1798
2004
  base?.kind === "basic" &&
@@ -1856,7 +2062,13 @@ export const expressionGenMethods = {
1856
2062
  // prototype so a nil `this` reaches it instead of a TypeError.
1857
2063
  const recv = this.genExpr(expr.func.expr);
1858
2064
  const rest = this._genCallArgs(expr);
1859
- return `${nilSafe}.prototype.${expr.func.field}.call(${rest ? `${recv}, ${rest}` : recv})`;
2065
+ const m = expr.func.field;
2066
+ const viaProto = (r) =>
2067
+ `${nilSafe}.prototype.${m}.call(${rest ? `${r}, ${rest}` : r})`;
2068
+ // A non-nil variable keeps the direct (faster) call.
2069
+ if (/^[\w$]+$/.test(recv))
2070
+ return `(${recv} != null ? ${recv}.${m}(${rest}) : ${viaProto(recv)})`;
2071
+ return viaProto(recv);
1860
2072
  }
1861
2073
 
1862
2074
  expr.func._callee = true;
@@ -1875,11 +2087,27 @@ export const expressionGenMethods = {
1875
2087
  const factory = this._typeParamZeroFactory(expr.func.expr._type.name);
1876
2088
  if (!factory) return null;
1877
2089
  this.useHelper("generics");
1878
- const recv = `__tbox(${factory}, ${this.genExpr(expr.func.expr)})`;
2090
+ let recv = `__tbox(${factory}, ${this.genExpr(expr.func.expr)})`;
2091
+ // `P(&v).M()` with `P interface{ *T; M() }`: a pointer to a boxed
2092
+ // type-parameter variable is the box, so reach the value it holds.
2093
+ if (this._hasPointerTypeSet(expr.func.expr._type))
2094
+ recv = `((__r) => typeof __r?.${expr.func.field} === "function" ? __r : __r?.value)(${recv})`;
1879
2095
  const args = this._genCallArgs(expr);
1880
2096
  return `${recv}.${expr.func.field}(${args})`;
1881
2097
  },
1882
2098
 
2099
+ // Whether type parameter `tp` is constrained to a single `*T` type-set term.
2100
+ _hasPointerTypeSet(tp) {
2101
+ const c = tp.constraint;
2102
+ const iface = c?.kind === "named" ? c.underlying : c;
2103
+ const terms = iface?.unionConstraint?.terms;
2104
+ return (
2105
+ terms?.length === 1 &&
2106
+ (terms[0].resolved?.kind === "pointer" ||
2107
+ terms[0].type?.kind === "PointerType")
2108
+ );
2109
+ },
2110
+
1883
2111
  // `(*S)(p).M()` where `p` points at another struct type: the JS object keeps
1884
2112
  // its own class, so call S's prototype method on it.
1885
2113
  _convertedPointerMethodCall(expr) {
@@ -1918,9 +2146,17 @@ export const expressionGenMethods = {
1918
2146
  if (func?.kind !== "SelectorExpr" || func._isMethodExpr) return null;
1919
2147
  const t = func.expr?._type;
1920
2148
  if (t?.kind !== "pointer" || t.base?.kind !== "named") return null;
1921
- const cls = t.base.name;
1922
- if (t.base.typeArgs?.length) return null;
1923
- if (this.nilSafeMethods?.has(`${cls}.${func.field}`)) return cls;
2149
+ // An instance of a generic struct (`Gen[int]`) shares its class.
2150
+ const cls = t.base.typeArgs?.length
2151
+ ? this._structClassName(t.base)
2152
+ : t.base.name;
2153
+ if (!cls) return null;
2154
+ if (
2155
+ t.base.underlying?.kind === "struct" &&
2156
+ !this._promotedPath(func) &&
2157
+ this.nilSafeMethods?.has(`${cls}.${func.field}`)
2158
+ )
2159
+ return cls;
1924
2160
  // A Go-source stdlib type (`Buffer$$bytes`) is declared outside this
1925
2161
  // package: its method decl tells whether it tests the receiver for nil.
1926
2162
  const decl = cls.includes("$$")
@@ -2066,9 +2302,7 @@ export const expressionGenMethods = {
2066
2302
  // TypeError with a different message.
2067
2303
  this.useHelper("nilCall");
2068
2304
  const recv = this.genExpr(expr.func.expr);
2069
- const args = expr.args
2070
- .map((a) => (a._spread ? `...${this.genExpr(a)}` : this.genExpr(a)))
2071
- .join(", ");
2305
+ const args = this._genCallArgs(expr);
2072
2306
  return `__nilcall(${recv}).${expr.func.field}(${args})`;
2073
2307
  }
2074
2308
  return undefined;
@@ -2223,8 +2457,7 @@ export const expressionGenMethods = {
2223
2457
  const resolvedKind = madeType?.kind;
2224
2458
  if (typeNode.kind === "SliceType" || resolvedKind === "slice") {
2225
2459
  let n = expr.args[1] ? this.genExpr(expr.args[1]) : "0";
2226
- if (expr.args[1] && this._is64BitInt(expr.args[1]._type))
2227
- n = `Number(${n})`;
2460
+ if (expr.args[1]) n = this._asIndexNumber(n, expr.args[1]._type);
2228
2461
  // A negative length or capacity panics with Go's message.
2229
2462
  n = this._guardMakeSize(n, "__mkn");
2230
2463
  const elemNode = typeNode.kind === "SliceType" ? typeNode.elem : null;
@@ -2240,8 +2473,8 @@ export const expressionGenMethods = {
2240
2473
  // make([]T, n, cap) allocates cap elements and returns a view of
2241
2474
  // the first n (`__mkslice`), so append within cap shares them.
2242
2475
  let capArg = expr.args[2] ? this.genExpr(expr.args[2]) : null;
2243
- if (capArg && expr.args[2] && this._is64BitInt(expr.args[2]._type))
2244
- capArg = `Number(${capArg})`;
2476
+ if (capArg && expr.args[2])
2477
+ capArg = this._asIndexNumber(capArg, expr.args[2]._type);
2245
2478
  if (capArg) {
2246
2479
  this.useHelper("slice");
2247
2480
  capArg = this._guardMakeSize(capArg, "__mkc");
@@ -3025,22 +3258,47 @@ export const expressionGenMethods = {
3025
3258
  const r = this._genComplexOperand(expr.right);
3026
3259
  switch (expr.op) {
3027
3260
  case "+":
3028
- return `{ re: ${l}.re + ${r}.re, im: ${l}.im + ${r}.im }`;
3029
3261
  case "-":
3030
- return `{ re: ${l}.re - ${r}.re, im: ${l}.im - ${r}.im }`;
3031
3262
  case "*":
3032
- this.useHelper("cmul");
3033
- return `__cmul(${l}, ${r})`;
3034
3263
  case "/":
3035
- this.useHelper("cdiv");
3036
- return `__cdiv(${l}, ${r})`;
3264
+ return this._genComplexArith(expr.op, l, r, isComplex64(expr._type));
3037
3265
  case "==":
3038
- return `(${l}.re === ${r}.re && ${l}.im === ${r}.im)`;
3266
+ return this._genComplexEquality("===", "&&", l, r);
3039
3267
  case "!=":
3040
- return `(${l}.re !== ${r}.re || ${l}.im !== ${r}.im)`;
3268
+ return this._genComplexEquality("!==", "||", l, r);
3041
3269
  }
3042
3270
  },
3043
3271
 
3272
+ // `l == r` / `l != r` on complex operands: each component reads both
3273
+ // operands, so an operand that calls a function is bound once first.
3274
+ _genComplexEquality(cmp, join, l, r) {
3275
+ const pure = (js) => !js.includes("(");
3276
+ if (pure(l) && pure(r))
3277
+ return `(${l}.re ${cmp} ${r}.re ${join} ${l}.im ${cmp} ${r}.im)`;
3278
+ return `((__a, __b) => __a.re ${cmp} __b.re ${join} __a.im ${cmp} __b.im)(${l}, ${r})`;
3279
+ },
3280
+
3281
+ // `l op r` for complex operands. A complex64 result has float32
3282
+ // components, as in Go (where the arithmetic runs in float64 and the
3283
+ // result is narrowed).
3284
+ _genComplexArith(op, l, r, is64) {
3285
+ let re;
3286
+ let im;
3287
+ let call = null;
3288
+ if (op === "+" || op === "-") {
3289
+ re = `${l}.re ${op} ${r}.re`;
3290
+ im = `${l}.im ${op} ${r}.im`;
3291
+ } else {
3292
+ const helper = op === "*" ? "cmul" : "cdiv";
3293
+ this.useHelper(helper);
3294
+ call = `__${helper}(${l}, ${r})`;
3295
+ }
3296
+ if (!is64) return call ?? `{ re: ${re}, im: ${im} }`;
3297
+ if (call)
3298
+ return `((__z) => ({ re: Math.fround(__z.re), im: Math.fround(__z.im) }))(${call})`;
3299
+ return `{ re: Math.fround(${re}), im: Math.fround(${im}) }`;
3300
+ },
3301
+
3044
3302
  _genBinaryExpr(expr) {
3045
3303
  if (
3046
3304
  isComplex(expr._type) ||
@@ -3507,6 +3765,19 @@ export const expressionGenMethods = {
3507
3765
  return is64BitInt(t);
3508
3766
  },
3509
3767
 
3768
+ // Whether a value of type `t` can be a BigInt at run time: a 64-bit
3769
+ // integer, or a type parameter whose type set holds one (`~int | ~int64`).
3770
+ _mayBeBigInt(t) {
3771
+ if (t?.kind !== "typeParam") return is64BitInt(t);
3772
+ const terms = this.checker?._constraintTerms(t.constraint, new Set());
3773
+ return terms?.some(is64BitInt) === true;
3774
+ },
3775
+
3776
+ // `code` as a JS number for an index, slice bound or `make` size.
3777
+ _asIndexNumber(code, t) {
3778
+ return this._mayBeBigInt(t) ? `Number(${code})` : code;
3779
+ },
3780
+
3510
3781
  // float32 arithmetic rounds through Math.fround and arithmetic on a type
3511
3782
  // parameter wraps for the instantiated type (`__tar`) in every package kind.
3512
3783
  _isFloat32(t) {