gofront 1.7.0 → 1.9.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 (194) hide show
  1. package/README.md +14 -14
  2. package/package.json +10 -38
  3. package/src/backend/js/expressions.js +2060 -389
  4. package/src/backend/js/index.js +586 -174
  5. package/src/backend/js/natives.js +383 -0
  6. package/src/backend/js/runtime.js +936 -1644
  7. package/src/backend/js/statements.js +1349 -132
  8. package/src/backend/js/stdlib/index.js +51 -94
  9. package/src/backend/js/templ.js +67 -118
  10. package/src/backend/js/type-info.js +436 -0
  11. package/src/backend/shared/fixed-float.js +20 -0
  12. package/src/backend/shared/fmt-types.js +80 -0
  13. package/src/backend/shared/host-unicode.js +36 -0
  14. package/src/backend/wasm/boundary.js +753 -872
  15. package/src/backend/wasm/emit-builtins.js +914 -237
  16. package/src/backend/wasm/emit-exprs.js +1456 -195
  17. package/src/backend/wasm/emit-maps.js +78 -380
  18. package/src/backend/wasm/emit-stmts.js +1659 -214
  19. package/src/backend/wasm/emit.js +614 -88
  20. package/src/backend/wasm/encode.js +43 -13
  21. package/src/backend/wasm/globals.js +491 -0
  22. package/src/backend/wasm/glue.js +2 -4
  23. package/src/backend/wasm/index.js +406 -3195
  24. package/src/backend/wasm/method-expr.js +392 -0
  25. package/src/backend/wasm/module-emitter.js +1615 -0
  26. package/src/backend/wasm/monomorph.js +164 -9
  27. package/src/backend/wasm/natives.js +596 -0
  28. package/src/backend/wasm/prescan.js +249 -0
  29. package/src/backend/wasm/prune-funcs.js +89 -0
  30. package/src/backend/wasm/stdlib/index.js +2 -37
  31. package/src/backend/wasm/stdlib/simd.js +293 -28
  32. package/src/backend/wasm/types.js +260 -27
  33. package/src/backend/wasm/variadic.js +93 -0
  34. package/src/backend/wasm/wat.js +47 -20
  35. package/src/{cli-core.js → cli/cli-core.js} +241 -11
  36. package/src/{test-runner.js → cli/test-runner.js} +188 -122
  37. package/src/{vendor.js → cli/vendor.js} +3 -4
  38. package/src/compiler.js +202 -386
  39. package/src/index.js +15 -169
  40. package/src/{lexer.js → lexer/lexer.js} +203 -62
  41. package/src/{templ-lexer.js → lexer/templ-lexer.js} +61 -18
  42. package/src/{tokens.js → lexer/tokens.js} +2 -0
  43. package/src/link/sourcemap.js +97 -0
  44. package/src/link/stdlib.js +737 -0
  45. package/src/link/type-names.js +148 -0
  46. package/src/link/wasm.js +318 -0
  47. package/src/lower/bounds.js +547 -0
  48. package/src/lower/boxing.js +1 -1
  49. package/src/lower/captures.js +267 -64
  50. package/src/lower/embedding.js +32 -3
  51. package/src/lower/errors-as.js +196 -0
  52. package/src/lower/fmt-formatters.js +905 -0
  53. package/src/lower/functions.js +2 -0
  54. package/src/lower/gen-types.js +274 -0
  55. package/src/lower/index.js +33 -19
  56. package/src/lower/json-codecs.js +1379 -0
  57. package/src/lower/ownership.js +47 -8
  58. package/src/lower/range.js +86 -1
  59. package/src/lower/reachability.js +150 -0
  60. package/src/lower/walk.js +12 -0
  61. package/src/parser/declarations.js +83 -15
  62. package/src/parser/expressions.js +110 -18
  63. package/src/parser/index.js +2 -1
  64. package/src/parser/statements.js +38 -22
  65. package/src/{templ-parser.js → parser/templ.js} +179 -17
  66. package/src/parser/types.js +70 -9
  67. package/src/resolver.js +21 -1
  68. package/src/typechecker/assignability.js +291 -19
  69. package/src/typechecker/const-fold.js +399 -0
  70. package/src/typechecker/constants.js +282 -0
  71. package/src/typechecker/expressions.js +1220 -183
  72. package/src/typechecker/index.js +659 -92
  73. package/src/typechecker/init-order.js +426 -0
  74. package/src/typechecker/operand-text.js +40 -0
  75. package/src/typechecker/resolve.js +613 -62
  76. package/src/typechecker/statements.js +212 -20
  77. package/src/typechecker/stdlib/excluded.js +902 -0
  78. package/src/typechecker/stdlib/index.js +38 -0
  79. package/src/typechecker/stdlib/simd.js +65 -15
  80. package/src/typechecker/termination.js +11 -2
  81. package/src/typechecker/types.js +268 -514
  82. package/stdlib/bytes/buffer.go +479 -0
  83. package/stdlib/bytes/bytes.go +1010 -0
  84. package/stdlib/bytes/reader.go +161 -0
  85. package/stdlib/cmp/cmp.go +65 -0
  86. package/stdlib/context/context.go +115 -0
  87. package/stdlib/crypto/rand/rand.go +34 -0
  88. package/stdlib/crypto/sha256/sha256.go +249 -0
  89. package/stdlib/encoding/base64/base64.go +331 -0
  90. package/stdlib/encoding/binary/binary.go +332 -0
  91. package/stdlib/encoding/hex/hex.go +140 -0
  92. package/stdlib/encoding/json/errors.go +31 -0
  93. package/stdlib/encoding/json/indent.go +71 -0
  94. package/stdlib/encoding/json/json.go +62 -0
  95. package/stdlib/encoding/json/number.go +12 -0
  96. package/stdlib/encoding/json/stream.go +173 -0
  97. package/stdlib/errors/errors.go +174 -0
  98. package/stdlib/fmt/errors.go +110 -0
  99. package/stdlib/fmt/format.go +732 -0
  100. package/stdlib/fmt/print.go +1232 -0
  101. package/stdlib/gom/gom.go +293 -0
  102. package/stdlib/hash/hash.go +53 -0
  103. package/stdlib/html/html.go +251 -0
  104. package/stdlib/internal/fmtrt/fmtrt.go +163 -0
  105. package/stdlib/internal/jsonrt/dec.go +871 -0
  106. package/stdlib/internal/jsonrt/enc.go +516 -0
  107. package/stdlib/internal/jsonrt/indent.go +135 -0
  108. package/stdlib/internal/jsonrt/number.go +27 -0
  109. package/stdlib/internal/jsonrt/reader.go +73 -0
  110. package/stdlib/internal/jsonrt/scanner.go +658 -0
  111. package/stdlib/io/io.go +502 -0
  112. package/stdlib/iter/iter.go +18 -0
  113. package/stdlib/maps/maps.go +131 -0
  114. package/stdlib/math/bits/bits.go +507 -0
  115. package/stdlib/math/extra.go +272 -0
  116. package/stdlib/math/math.go +439 -0
  117. package/stdlib/math/rand/rand.go +297 -0
  118. package/stdlib/os/os.go +93 -0
  119. package/stdlib/path/filepath/filepath.go +47 -0
  120. package/stdlib/path/path.go +394 -0
  121. package/stdlib/regexp/regexp.go +231 -0
  122. package/stdlib/slices/slices.go +526 -0
  123. package/stdlib/slices/sort.go +209 -0
  124. package/stdlib/slices/zsort_cmp.go +480 -0
  125. package/stdlib/slices/zsort_ordered.go +482 -0
  126. package/stdlib/sort/sort.go +500 -0
  127. package/stdlib/sort/zsort_func.go +480 -0
  128. package/stdlib/sort/zsort_iface.go +480 -0
  129. package/stdlib/strconv/float.go +1699 -0
  130. package/stdlib/strconv/isprint.go +752 -0
  131. package/stdlib/strconv/quote.go +526 -0
  132. package/stdlib/strconv/strconv.go +616 -0
  133. package/stdlib/strings/builder.go +95 -0
  134. package/stdlib/strings/reader.go +163 -0
  135. package/stdlib/strings/replacer.go +102 -0
  136. package/stdlib/strings/strings.go +807 -0
  137. package/stdlib/sync/sync.go +268 -0
  138. package/stdlib/testing/async_js.go +61 -0
  139. package/stdlib/testing/benchmark.go +297 -0
  140. package/stdlib/testing/testing.go +523 -0
  141. package/stdlib/time/duration.go +233 -0
  142. package/stdlib/time/format.go +745 -0
  143. package/stdlib/time/parse.go +790 -0
  144. package/stdlib/time/sleep_js.go +63 -0
  145. package/stdlib/time/time.go +834 -0
  146. package/stdlib/unicode/unicode.go +250 -0
  147. package/stdlib/unicode/utf8/utf8.go +464 -0
  148. package/src/backend/js/stdlib/base64.js +0 -12
  149. package/src/backend/js/stdlib/binary.js +0 -102
  150. package/src/backend/js/stdlib/bits.js +0 -46
  151. package/src/backend/js/stdlib/builder.js +0 -63
  152. package/src/backend/js/stdlib/bytes.js +0 -122
  153. package/src/backend/js/stdlib/errors.js +0 -44
  154. package/src/backend/js/stdlib/fmt.js +0 -178
  155. package/src/backend/js/stdlib/gom.js +0 -155
  156. package/src/backend/js/stdlib/hex.js +0 -17
  157. package/src/backend/js/stdlib/html.js +0 -20
  158. package/src/backend/js/stdlib/io.js +0 -137
  159. package/src/backend/js/stdlib/json.js +0 -972
  160. package/src/backend/js/stdlib/maps.js +0 -45
  161. package/src/backend/js/stdlib/math.js +0 -69
  162. package/src/backend/js/stdlib/os.js +0 -13
  163. package/src/backend/js/stdlib/path.js +0 -48
  164. package/src/backend/js/stdlib/rand.js +0 -32
  165. package/src/backend/js/stdlib/regexp.js +0 -54
  166. package/src/backend/js/stdlib/sha256.js +0 -12
  167. package/src/backend/js/stdlib/shared.js +0 -17
  168. package/src/backend/js/stdlib/simd.js +0 -24
  169. package/src/backend/js/stdlib/slices.js +0 -77
  170. package/src/backend/js/stdlib/sort.js +0 -30
  171. package/src/backend/js/stdlib/strconv.js +0 -74
  172. package/src/backend/js/stdlib/strings.js +0 -222
  173. package/src/backend/js/stdlib/sync.js +0 -13
  174. package/src/backend/js/stdlib/testing.js +0 -34
  175. package/src/backend/js/stdlib/time.js +0 -128
  176. package/src/backend/js/stdlib/unicode.js +0 -25
  177. package/src/backend/js/stdlib/utf8.js +0 -26
  178. package/src/backend/wasm/const-fold.js +0 -205
  179. package/src/backend/wasm/emit-stdlib.js +0 -13
  180. package/src/backend/wasm/stdlib/bytes.js +0 -5039
  181. package/src/backend/wasm/stdlib/core.js +0 -8903
  182. package/src/backend/wasm/stdlib/io.js +0 -2161
  183. package/src/backend/wasm/stdlib/packages.js +0 -2400
  184. package/src/backend/wasm/stdlib/pure.js +0 -720
  185. package/src/backend/wasm/stdlib/time.js +0 -1200
  186. package/src/typechecker/stdlib/core.js +0 -801
  187. package/src/typechecker/stdlib/extended.js +0 -914
  188. package/src/typechecker/stdlib.js +0 -22
  189. /package/src/{asset-manager.js → cli/asset-manager.js} +0 -0
  190. /package/src/{colors.js → cli/colors.js} +0 -0
  191. /package/src/{dev-server.js → cli/dev-server.js} +0 -0
  192. /package/src/{minifier.js → cli/minifier.js} +0 -0
  193. /package/src/{project-config.js → cli/project-config.js} +0 -0
  194. /package/src/{pwa.js → cli/pwa.js} +0 -0
@@ -1,16 +1,147 @@
1
1
  import {
2
2
  bodyRedeclares,
3
+ forEachChild,
4
+ hasCallOrReceive,
3
5
  isIntRangeType,
4
6
  isMapRangeType,
5
7
  isRangeFor,
6
8
  isSimpleIntRange,
7
9
  isStringRangeType,
8
- nodeAssigns,
10
+ loopVarsCapturedInLoop,
11
+ someChild,
9
12
  } from "../../lower/index.js";
10
- import { isComplex } from "../../typechecker/types.js";
13
+ import {
14
+ is64BitInt,
15
+ isComplex,
16
+ isTypedArraySlice,
17
+ } from "../../typechecker/types.js";
11
18
 
12
19
  /** @typedef {import('./index.js').CodeGen} CodeGen */
13
20
 
21
+ // The same variable or field chain (`x`, `n.Children`) on both sides.
22
+ const sameChain = (a, b) =>
23
+ a.kind === b.kind &&
24
+ (a.kind === "Ident"
25
+ ? a.name === b.name && a.name !== "_"
26
+ : a.kind === "SelectorExpr" &&
27
+ a.field === b.field &&
28
+ sameChain(a.expr, b.expr));
29
+
30
+ // `x = x[lo:hi]` (no max) with the same chain on both sides.
31
+ const isSelfResliceShape = (stmt) =>
32
+ stmt.op === "=" &&
33
+ stmt.lhs.length === 1 &&
34
+ stmt.rhs.length === 1 &&
35
+ stmt.rhs[0].kind === "SliceExpr" &&
36
+ !stmt.rhs[0].max &&
37
+ sameChain(stmt.lhs[0], stmt.rhs[0].expr);
38
+
39
+ // Pure expressions (the slice chain of a `__rcs` sync) that read the same
40
+ // value after the write as before it: literals, variables and fields not
41
+ // assigned by the statement, `len` and `cap`, and arithmetic over those.
42
+ const isStableIndex = (e, assigned) => {
43
+ switch (e?.kind) {
44
+ case "BasicLit":
45
+ return true;
46
+ case "Ident":
47
+ return !assigned.has(e.name);
48
+ case "SelectorExpr":
49
+ return !e._isMethodValue && isStableIndex(e.expr, assigned);
50
+ case "ParenExpr":
51
+ return isStableIndex(e.expr, assigned);
52
+ case "UnaryExpr":
53
+ return e.op !== "<-" && isStableIndex(e.operand, assigned);
54
+ case "BinaryExpr":
55
+ return (
56
+ isStableIndex(e.left, assigned) && isStableIndex(e.right, assigned)
57
+ );
58
+ case "CallExpr":
59
+ return (
60
+ e.func?.kind === "Ident" &&
61
+ (e.func.name === "len" || e.func.name === "cap") &&
62
+ e.args.every((a) => isStableIndex(a, assigned))
63
+ );
64
+ default:
65
+ return false;
66
+ }
67
+ };
68
+
69
+ const isLenCap = (e) =>
70
+ e.kind === "CallExpr" &&
71
+ e.func?.kind === "Ident" &&
72
+ (e.func.name === "len" || e.func.name === "cap");
73
+
74
+ // A call (other than `len`/`cap`), receive or await in `n` (not inside a
75
+ // function literal): it may change variables and fields.
76
+ const hasCall = (n) => {
77
+ if (Array.isArray(n)) return n.some(hasCall);
78
+ if (!n || typeof n !== "object" || typeof n.kind !== "string") return false;
79
+ if (n.kind === "FuncLit") return false;
80
+ if (n.kind === "AwaitExpr" || (n.kind === "CallExpr" && !isLenCap(n)))
81
+ return true;
82
+ if (n.kind === "UnaryExpr" && n.op === "<-") return true;
83
+ return someChild(n, hasCall);
84
+ };
85
+
86
+ // A call in a left-hand index or on the right-hand side of the statement.
87
+ const stmtHasCall = (lhs, stmt) =>
88
+ lhs.some((e) => hasCall(e.index)) ||
89
+ (stmt.kind === "AssignStmt" && hasCall(stmt.rhs));
90
+
91
+ // A multi-assign with a field or `*p` left-hand side: it may reassign the
92
+ // slice chain of an index write, whose sync must then use the old slice.
93
+ const reassignsChains = (lhs, stmt) =>
94
+ stmt.kind === "AssignStmt" &&
95
+ lhs.length > 1 &&
96
+ lhs.some(
97
+ (e) =>
98
+ e.kind === "SelectorExpr" || (e.kind === "UnaryExpr" && e.op === "*"),
99
+ );
100
+
101
+ // Index of a `__rcs` sync that can be evaluated again after the write:
102
+ // constants, and, when `vars` (a single-LHS statement without calls), unboxed
103
+ // variables, with `len`/`cap` and arithmetic over those. Any other index
104
+ // is bound to a temporary first, so the write and the sync use one value.
105
+ const isReusableIndex = (e, vars, boxed) => {
106
+ if (e?._const || e?.kind === "BasicLit") return true;
107
+ switch (e?.kind) {
108
+ case "Ident":
109
+ return vars && !boxed.has(e.name);
110
+ case "ParenExpr":
111
+ return isReusableIndex(e.expr, vars, boxed);
112
+ case "UnaryExpr":
113
+ return (
114
+ (e.op === "-" || e.op === "+" || e.op === "^") &&
115
+ isReusableIndex(e.operand, vars, boxed)
116
+ );
117
+ case "BinaryExpr":
118
+ return (
119
+ isReusableIndex(e.left, vars, boxed) &&
120
+ isReusableIndex(e.right, vars, boxed)
121
+ );
122
+ case "CallExpr":
123
+ return (
124
+ isLenCap(e) && e.args.every((a) => isReusableIndex(a, vars, boxed))
125
+ );
126
+ default:
127
+ return false;
128
+ }
129
+ };
130
+
131
+ // Adds the name of every identifier under `n` to `into`.
132
+ const collectIdentNames = (n, into) => {
133
+ if (Array.isArray(n)) {
134
+ for (const c of n) collectIdentNames(c, into);
135
+ return;
136
+ }
137
+ if (!n || typeof n !== "object") return;
138
+ if (n.kind === "Ident") into.add(n.name);
139
+ someChild(n, (c) => {
140
+ collectIdentNames(c, into);
141
+ return false;
142
+ });
143
+ };
144
+
14
145
  // Method-name dispatch for genStmt — string values add no static call edges.
15
146
  const STMT_GEN_DELEGATE = {
16
147
  VarDecl: "genVarDecl",
@@ -22,26 +153,141 @@ const STMT_GEN_DELEGATE = {
22
153
  SwitchStmt: "genSwitch",
23
154
  TypeSwitchStmt: "genTypeSwitch",
24
155
  BranchStmt: "_genBranchStmt",
156
+ TemplMount: "_genTemplMountStmt",
25
157
  };
26
158
 
27
159
  /** @type {ThisType<CodeGen>} */
28
160
  export const statementGenMethods = {
29
161
  genBlock(block) {
30
- for (const stmt of block.stmts) this.genStmt(stmt);
162
+ let nested = 0;
163
+ const earlier = new Set();
164
+ for (const stmt of block.stmts) {
165
+ if (this._definesShadowedRead(stmt)) {
166
+ this._genShadowingDefine(stmt);
167
+ nested++;
168
+ } else if (this._definesAfterOuterUse(stmt, earlier)) {
169
+ this.line("{");
170
+ this.indent++;
171
+ nested++;
172
+ this.genStmt(stmt);
173
+ } else {
174
+ this.genStmt(stmt);
175
+ }
176
+ collectIdentNames(stmt, earlier);
177
+ }
178
+ for (; nested > 0; nested--) {
179
+ this.indent--;
180
+ this.line("}");
181
+ }
182
+ },
183
+
184
+ // `err = nil; _, err := g()` where `err` is an outer variable used earlier
185
+ // in this block: JS `let` would put those uses in its temporal dead zone,
186
+ // so the rest of the block moves into a nested block.
187
+ _definesAfterOuterUse(stmt, earlier) {
188
+ if (stmt.kind !== "DefineStmt" || earlier.size === 0) return false;
189
+ return stmt.lhs.some(
190
+ (l) => !l._redecl && l.name !== "_" && earlier.has(l.name),
191
+ );
192
+ },
193
+
194
+ // `x := f(x)` declaring a new `x` that its own right-hand side reads (the
195
+ // outer `x`): JS `let` would put the read in its temporal dead zone.
196
+ _definesShadowedRead(stmt) {
197
+ if (stmt.kind !== "DefineStmt") return false;
198
+ const names = new Set(
199
+ stmt.lhs.filter((l) => !l._redecl && l.name !== "_").map((l) => l.name),
200
+ );
201
+ if (names.size === 0) return false;
202
+ const reads = (n) => {
203
+ if (Array.isArray(n)) return n.some(reads);
204
+ if (!n || typeof n !== "object") return false;
205
+ if (n.kind === "Ident" && names.has(n.name)) return true;
206
+ return someChild(n, reads);
207
+ };
208
+ return stmt.rhs.some(reads);
209
+ },
210
+
211
+ // Emits an `if`/`switch` init statement, then `body`; an init that reads the
212
+ // name it declares goes through `_genShadowingDefine`.
213
+ _genScopedInit(init, body) {
214
+ const shadowing = this._definesShadowedRead(init);
215
+ if (shadowing) this._genShadowingDefine(init);
216
+ else this.genStmt(init);
217
+ body();
218
+ if (shadowing) {
219
+ this.indent--;
220
+ this.line("}");
221
+ }
222
+ },
223
+
224
+ // Evaluates the right-hand side in the enclosing scope, then opens a nested
225
+ // block (closed at the end of the enclosing block) that declares the names.
226
+ _genShadowingDefine(stmt) {
227
+ if (this._isCommaOkMapExpr(stmt, stmt.rhs[0])) {
228
+ const node = stmt.rhs[0];
229
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
230
+ const m = `__sm${this._tmpCounter}`;
231
+ const k = `__sk${this._tmpCounter}`;
232
+ this.line(
233
+ `const ${m} = ${this.genExpr(node.expr)}, ${k} = ${this.genExpr(node.index)};`,
234
+ );
235
+ this.line("{");
236
+ this.indent++;
237
+ this._genCommaOkMapIndex(stmt, node, true, { map: m, key: k });
238
+ return;
239
+ }
240
+ const rhs = this._defineRhs(stmt);
241
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
242
+ const tmp = `__t${this._tmpCounter}`;
243
+ const all = rhs.length === 1 ? rhs[0] : `[${rhs.join(", ")}]`;
244
+ this.line(`const ${tmp} = ${all};`);
245
+ this.line("{");
246
+ this.indent++;
247
+ this._genDefineStmt(
248
+ stmt,
249
+ rhs.length === 1 ? [tmp] : rhs.map((_, i) => `${tmp}[${i}]`),
250
+ );
251
+ },
252
+
253
+ // `m[k]` of a non-strict comma-ok: a missing key reads as the scalar zero
254
+ // value, not `undefined`.
255
+ _nonStrictMapRead(mapExpr, keyExpr, valType, copyVal) {
256
+ const zero = valType ? this.zeroValueForType(valType) : "";
257
+ const read = `${mapExpr}[${keyExpr}]`;
258
+ if (/^(0n?|""|false)$/.test(zero)) return `(${read} ?? ${zero})`;
259
+ return copyVal(
260
+ this._aggregateBase(valType) ? `(${read} ?? ${zero})` : read,
261
+ );
31
262
  },
32
263
 
33
264
  // Shared helper for comma-ok map index in both DefineStmt and AssignStmt.
34
- _genCommaOkMapIndex(stmt, rhsNode, isDefine) {
35
- const mapExpr = this.genExpr(rhsNode.expr);
36
- const keyExpr = this.genExpr(rhsNode.index);
265
+ // `pre` carries the map and key already evaluated (a shadowing declaration
266
+ // reads them before its names exist).
267
+ _genCommaOkMapIndex(stmt, rhsNode, isDefine, pre = null) {
268
+ const mapExpr = pre?.map ?? this.genExpr(rhsNode.expr);
269
+ const keyExpr = pre?.key ?? this.genExpr(rhsNode.index);
37
270
  const [vName, okName] = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
38
- const decl = isDefine ? "let " : "";
271
+ // A name `:=` reuses is assigned, not declared again; an address-taken
272
+ // variable lives in a `{ value }` box.
273
+ const bind = (l, name, js) => {
274
+ const boxed = this._boxedVars.has(name);
275
+ if (isDefine && !l._redecl)
276
+ return `let ${name} = ${boxed ? `{ value: ${js} }` : js};`;
277
+ return `${name}${boxed ? ".value" : ""} = ${js};`;
278
+ };
279
+ const [vL, okL] = stmt.lhs;
280
+ // A struct or array map value is a copy, not the stored value.
281
+ const valType = rhsNode._mapValueType;
282
+ const copyVal = (js) =>
283
+ this._aggregateBase(valType) ? this._cloneJs(valType, js) : js;
39
284
  if (isDefine && !this.strict) {
40
285
  if (vName !== "_") {
41
- this.line(`let ${vName} = ${mapExpr}[${keyExpr}];`);
286
+ const val = this._nonStrictMapRead(mapExpr, keyExpr, valType, copyVal);
287
+ this.line(bind(vL, vName, val));
42
288
  }
43
289
  if (okName !== "_") {
44
- this.line(`let ${okName} = (${keyExpr}) in ${mapExpr};`);
290
+ this.line(bind(okL, okName, `(${keyExpr}) in ${mapExpr}`));
45
291
  }
46
292
  return;
47
293
  }
@@ -54,11 +300,15 @@ export const statementGenMethods = {
54
300
  this.line(`const ${mTmp} = ${mapExpr}, ${kTmp} = ${keyExpr};`);
55
301
  if (vName !== "_") {
56
302
  this.line(
57
- `${decl}${vName} = (${mTmp} && (${kTmp} in ${mTmp})) ? ${mTmp}[${kTmp}] : ${zero};`,
303
+ bind(
304
+ vL,
305
+ vName,
306
+ `(${mTmp} && (${kTmp} in ${mTmp})) ? ${copyVal(`${mTmp}[${kTmp}]`)} : ${zero}`,
307
+ ),
58
308
  );
59
309
  }
60
310
  if (okName !== "_") {
61
- this.line(`${decl}${okName} = Boolean(${mTmp} && (${kTmp} in ${mTmp}));`);
311
+ this.line(bind(okL, okName, `Boolean(${mTmp} && (${kTmp} in ${mTmp}))`));
62
312
  }
63
313
  },
64
314
 
@@ -66,11 +316,24 @@ export const statementGenMethods = {
66
316
  // Record source mapping for the first line this statement produces
67
317
  const srcLine = stmt._line ?? null;
68
318
  const _line0 = this.out.length;
319
+ const restore = [];
320
+ const syncs =
321
+ stmt.kind === "AssignStmt"
322
+ ? this._resliceSyncs(stmt.lhs, stmt, false, restore)
323
+ : stmt.kind === "IncDecStmt"
324
+ ? this._resliceSyncs([stmt.expr], stmt, false, restore)
325
+ : null;
69
326
  switch (stmt.kind) {
70
327
  case "TypeDecl":
71
- this.genTypeDeclWithMethods(stmt, []);
328
+ {
329
+ const start = this.out.length;
330
+ this.genTypeDeclWithMethods(stmt, []);
331
+ this._hoistLocalClass(stmt, start);
332
+ }
72
333
  break;
73
334
  case "IncDecStmt": {
335
+ this._markIndexLvalues([stmt.expr]);
336
+ this._markDerefOpTarget(stmt.expr);
74
337
  if (stmt.expr.kind === "IndexExpr" && stmt.expr._mapValueType) {
75
338
  const zero = this.zeroValueForType(stmt.expr._mapValueType);
76
339
  const wrapField = this._namedWrapperField(
@@ -82,28 +345,47 @@ export const statementGenMethods = {
82
345
  : this.genExpr(stmt.expr.expr);
83
346
  const idx = this.genExpr(stmt.expr.index);
84
347
  const op = stmt.op === "++" ? "+" : "-";
348
+ const is64Val = is64BitInt(stmt.expr._mapValueType);
349
+ const delta = is64Val ? "1n" : "1";
85
350
  if (
86
351
  this._hasCallExpr(stmt.expr.expr) ||
87
352
  this._hasCallExpr(stmt.expr.index)
88
353
  ) {
354
+ const rhs = is64Val
355
+ ? this._gen64BitBinary(
356
+ op,
357
+ `(__m[__k] ?? ${zero})`,
358
+ delta,
359
+ stmt.expr._mapValueType,
360
+ )
361
+ : `(__m[__k] ?? ${zero}) ${op} 1`;
89
362
  this.line(
90
- `((__m, __k) => { __m[__k] = (__m[__k] ?? ${zero}) ${op} 1; })(${base}, ${idx});`,
363
+ `((__m, __k) => { ${this._genMapWriteBase("__m")}[__k] = ${rhs}; })(${base}, ${idx});`,
91
364
  srcLine,
92
365
  );
93
366
  } else {
367
+ const rhs = is64Val
368
+ ? this._gen64BitBinary(
369
+ op,
370
+ `(${base}[${idx}] ?? ${zero})`,
371
+ delta,
372
+ stmt.expr._mapValueType,
373
+ )
374
+ : `(${base}[${idx}] ?? ${zero}) ${op} 1`;
94
375
  this.line(
95
- `${base}[${idx}] = (${base}[${idx}] ?? ${zero}) ${op} 1;`,
376
+ `${this._genMapWriteBase(base)}[${idx}] = ${rhs};`,
96
377
  srcLine,
97
378
  );
98
379
  }
99
380
  break;
100
381
  }
382
+ const is64 = is64BitInt(stmt.expr._type);
101
383
  if (
102
- this.strict &&
384
+ (this.strict || is64 || this._isFloat32(stmt.expr._type)) &&
103
385
  this._genStrictCompound(
104
386
  stmt.expr,
105
387
  stmt.op === "++" ? "+" : "-",
106
- "1",
388
+ is64 ? "1n" : "1",
107
389
  srcLine,
108
390
  )
109
391
  )
@@ -118,8 +400,7 @@ export const statementGenMethods = {
118
400
  if (stmt.init) {
119
401
  this.line("{");
120
402
  this.indented(() => {
121
- this.genStmt(stmt.init);
122
- this._genIf(stmt);
403
+ this._genScopedInit(stmt.init, () => this._genIf(stmt));
123
404
  });
124
405
  this.line("}");
125
406
  } else {
@@ -127,12 +408,26 @@ export const statementGenMethods = {
127
408
  }
128
409
  break;
129
410
  case "DeferStmt":
130
- this.line(`__defers.push(() => { ${this.genExpr(stmt.call)}; });`);
411
+ // A deferred native call runs the Go body.
412
+ this._withoutNatives(() =>
413
+ this.line(`__defers.push(() => { ${this.genExpr(stmt.call)}; });`),
414
+ );
131
415
  break;
132
- case "LabeledStmt":
133
- this.line(`${stmt.label}:`);
134
- this.genStmt(stmt.body);
416
+ case "LabeledStmt": {
417
+ // Stacked labels (`a: b: for ...`) all stay attached to the loop.
418
+ const labels = [stmt.label];
419
+ let body = stmt.body;
420
+ while (body?.kind === "LabeledStmt") {
421
+ labels.push(body.label);
422
+ body = body.body;
423
+ }
424
+ if (body?.kind === "ForStmt") body.label = labels[labels.length - 1];
425
+ const hoisted = this._hoistForInit(body);
426
+ for (const label of labels) this.line(`${label}:`);
427
+ this.genStmt(body);
428
+ if (hoisted) this._closeHoistedForInit();
135
429
  break;
430
+ }
136
431
  case "Block":
137
432
  this.line("{");
138
433
  this.indented(() => this.genBlock(stmt));
@@ -147,6 +442,9 @@ export const statementGenMethods = {
147
442
  throw new Error(`CodeGen: unhandled statement kind '${stmt.kind}'`);
148
443
  }
149
444
  }
445
+ this._emitResliceSyncs(syncs);
446
+ // The AST is shared with the WASM backend: undo the temporaries.
447
+ for (const [node, key, v] of restore) node[key] = v;
150
448
  // Record source mapping: first output line this statement produced
151
449
  if (srcLine != null && this.out.length > _line0) {
152
450
  this._srcMappings.push({
@@ -158,11 +456,14 @@ export const statementGenMethods = {
158
456
  },
159
457
 
160
458
  _isCommaOkMapExpr(stmt, rhsNode) {
459
+ if (stmt._commaOkMap) return true;
460
+ const t = rhsNode?.expr?._type;
461
+ const underlying = t?.kind === "named" ? t.underlying : t;
161
462
  return (
162
463
  stmt.rhs.length === 1 &&
163
464
  stmt.lhs.length === 2 &&
164
465
  rhsNode?.kind === "IndexExpr" &&
165
- rhsNode.expr?._type?.kind === "map"
466
+ underlying?.kind === "map"
166
467
  );
167
468
  },
168
469
 
@@ -178,22 +479,14 @@ export const statementGenMethods = {
178
479
  },
179
480
 
180
481
  _genDefineMixedRedecl(lhs, rhs, redecls) {
181
- if (rhs.length === 1 && lhs.length > 1) {
182
- this._tmpCounter = (this._tmpCounter ?? 0) + 1;
183
- const tmp = `__t${this._tmpCounter}`;
184
- this.line(`const ${tmp} = ${rhs[0]};`);
185
- for (let i = 0; i < lhs.length; i++) {
186
- if (lhs[i] === "_") continue;
187
- this._genDefineSingleVar([lhs[i]], [`${tmp}[${i}]`], [redecls[i]]);
188
- }
189
- } else {
190
- for (let i = 0; i < lhs.length; i++) {
191
- if (lhs[i] === "_") {
192
- this.line(`${rhs[i]};`);
193
- continue;
194
- }
195
- this._genDefineSingleVar([lhs[i]], [rhs[i]], [redecls[i]]);
196
- }
482
+ // `x, y := 2, x` reads the old `x`: evaluate every value first.
483
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
484
+ const tmp = `__t${this._tmpCounter}`;
485
+ const all = rhs.length === 1 ? rhs[0] : `[${rhs.join(", ")}]`;
486
+ this.line(`const ${tmp} = ${all};`);
487
+ for (let i = 0; i < lhs.length; i++) {
488
+ if (lhs[i] === "_") continue;
489
+ this._genDefineSingleVar([lhs[i]], [`${tmp}[${i}]`], [redecls[i]]);
197
490
  }
198
491
  },
199
492
 
@@ -206,19 +499,23 @@ export const statementGenMethods = {
206
499
  }
207
500
  },
208
501
 
209
- _genDefineStmt(stmt) {
210
- const rhsNode = stmt.rhs[0];
211
- if (this._isCommaOkMapExpr(stmt, rhsNode)) {
212
- this._genCommaOkMapIndex(stmt, rhsNode, true);
213
- return;
214
- }
502
+ _defineRhs(stmt) {
215
503
  const paired = stmt.rhs.length === stmt.lhs.length;
216
- const rhs = stmt.rhs.map((e, i) => {
504
+ return stmt.rhs.map((e, i) => {
217
505
  const l = paired ? stmt.lhs[i] : null;
218
506
  if (!l || l.name === "_") return this.genExpr(e);
219
507
  if (l._redecl) return this.genValueExpr(e);
220
508
  return this._genDeclValue(l.name, e);
221
509
  });
510
+ },
511
+
512
+ _genDefineStmt(stmt, preEvaluated = null) {
513
+ const rhsNode = stmt.rhs[0];
514
+ if (this._isCommaOkMapExpr(stmt, rhsNode)) {
515
+ this._genCommaOkMapIndex(stmt, rhsNode, true);
516
+ return;
517
+ }
518
+ const rhs = preEvaluated ?? this._defineRhs(stmt);
222
519
  const lhs = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
223
520
  const redecls = stmt.lhs.map((e) => !!e._redecl);
224
521
  const anyRedecl = redecls.some((r) => r);
@@ -249,6 +546,68 @@ export const statementGenMethods = {
249
546
  );
250
547
  },
251
548
 
549
+ // `*p = v` where `p` points at a type parameter: erased, so the pointer is
550
+ // either a struct (copy into it) or a get/set wrapper.
551
+ _isTypeParamDerefAssign(stmt) {
552
+ if (stmt.lhs.length !== 1 || stmt.rhs.length !== 1 || stmt.op !== "=")
553
+ return false;
554
+ const l = stmt.lhs[0];
555
+ return (
556
+ l.kind === "UnaryExpr" &&
557
+ l.op === "*" &&
558
+ l.operand?._type?.base?.kind === "typeParam"
559
+ );
560
+ },
561
+
562
+ _isTypeParamDerefTarget(l) {
563
+ return (
564
+ l.kind === "UnaryExpr" &&
565
+ l.op === "*" &&
566
+ l.operand?._type?.base?.kind === "typeParam"
567
+ );
568
+ },
569
+
570
+ // `*a, *b = *b, *a` with type-parameter pointers: `*p` is not a settable
571
+ // JS destructuring target, so evaluate the right-hand side into temporaries
572
+ // and assign each target on its own.
573
+ _hasTypeParamDerefTarget(stmt) {
574
+ return (
575
+ stmt.op === "=" &&
576
+ stmt.lhs.length > 1 &&
577
+ stmt.lhs.some((l) => this._isTypeParamDerefTarget(l))
578
+ );
579
+ },
580
+
581
+ _genTypeParamDerefMultiAssign(stmt) {
582
+ const n = stmt.lhs.length;
583
+ const names = stmt.lhs.map((_, i) => `__t${i}`);
584
+ this.line("{");
585
+ this.indented(() => {
586
+ if (stmt.rhs.length === n) {
587
+ const vals = stmt.rhs.map((e, i) =>
588
+ stmt.lhs[i].name !== "_" ? this.genValueExpr(e) : this.genExpr(e),
589
+ );
590
+ this.line(`const [${names.join(", ")}] = [${vals.join(", ")}];`);
591
+ } else {
592
+ this.line(
593
+ `const [${names.join(", ")}] = ${this.genExpr(stmt.rhs[0])};`,
594
+ );
595
+ }
596
+ stmt.lhs.forEach((l, i) => {
597
+ if (l.name === "_") return;
598
+ if (this._isTypeParamDerefTarget(l)) {
599
+ const isStruct = this._typeParamIsStructJs(l.operand._type.base.name);
600
+ this.line(
601
+ `((__p, __v) => { if (${isStruct}) Object.assign(__p, __v); else __p.value = __v; })(${this.genExpr(l.operand)}, ${names[i]});`,
602
+ );
603
+ } else {
604
+ this.line(`${this._genAssignLhsExpr(l)} = ${names[i]};`);
605
+ }
606
+ });
607
+ });
608
+ this.line("}");
609
+ },
610
+
252
611
  _isStructDerefAssign(stmt) {
253
612
  if (stmt.lhs.length !== 1 || stmt.rhs.length !== 1 || stmt.op !== "=")
254
613
  return false;
@@ -270,6 +629,13 @@ export const statementGenMethods = {
270
629
  for (const e of exprs) if (e.kind === "IndexExpr") e._lvalue = true;
271
630
  },
272
631
 
632
+ // Op-assign / inc-dec through a type-parameter pointer needs a settable
633
+ // target: the erased pointer is a get/set wrapper (struct instantiations
634
+ // cannot be op-assigned), so target `.value` directly.
635
+ _markDerefOpTarget(e) {
636
+ if (this._isTypeParamDerefTarget(e)) e._derefOpTarget = true;
637
+ },
638
+
273
639
  _genAssignMulti(stmt, lhs, rhs, active, pairs) {
274
640
  const rhsStr = rhs.length === 1 ? rhs[0] : `[${rhs.join(", ")}]`;
275
641
  if (active.length === pairs.length) {
@@ -279,7 +645,132 @@ export const statementGenMethods = {
279
645
  }
280
646
  },
281
647
 
648
+ // `x = x[lo:hi]` with the same variable or field chain on both sides:
649
+ // the old value of `x` is replaced, so `_genSliceExpr` may copy instead
650
+ // of building a slice view (`__reslice`).
651
+ _markSelfReslice(stmt) {
652
+ if (stmt.op !== "=" || stmt.lhs.length !== 1 || stmt.rhs.length !== 1)
653
+ return;
654
+ const r = stmt.rhs[0];
655
+ if (r.kind !== "SliceExpr" || r.max) return;
656
+ // A parameter or named result may alias the caller's slice: keep the
657
+ // view. Field chains (`n.Children = n.Children[:k]`) still copy.
658
+ const lhs = stmt.lhs[0];
659
+ if (lhs.kind === "Ident" && this._fnCtx?.params.has(lhs.name)) return;
660
+ if (sameChain(stmt.lhs[0], r.expr)) r._selfReslice = true;
661
+ },
662
+
663
+ // Names of the variables and fields (`.F`) that some `x = x[lo:hi]` in the
664
+ // program may turn into a `__reslice` copy (`_resliceSyncs`).
665
+ _collectResliceNames(program) {
666
+ const names = new Set();
667
+ const visit = (n) => {
668
+ if (Array.isArray(n)) {
669
+ for (const c of n) visit(c);
670
+ return;
671
+ }
672
+ if (!n || typeof n !== "object" || typeof n.kind !== "string") return;
673
+ if (n.kind === "AssignStmt" && isSelfResliceShape(n)) {
674
+ const l = n.lhs[0];
675
+ names.add(l.kind === "Ident" ? l.name : `.${l.field}`);
676
+ }
677
+ forEachChild(n, visit);
678
+ };
679
+ visit(program.decls);
680
+ this._resliceNames = names;
681
+ },
682
+
683
+ // Index writes in `lhs` whose slice may be a `__reslice` copy: each is
684
+ // followed by `__rcs(x, i)` (`_emitResliceSyncs`), which writes the element
685
+ // through to the copy's old array, so writes through the copy and through
686
+ // other slices of that array keep Go's order. An index that the statement
687
+ // could change (`isReusableIndex`) is first bound to a temporary (not in
688
+ // `inline` for-clause statements, which skip the sync: the write-back on
689
+ // extension still applies). Not covered: writes through other references to the copy.
690
+ // Once one index is bound, every left-hand index with a call is bound too,
691
+ // in source order and each after its operand, and so is the slice operand
692
+ // of a synced write in a statement with a call or one that may reassign
693
+ // the chain through a field or `*p` (`reassignsChains`; inline statements
694
+ // skip that sync) — Go evaluates both before the right-hand side.
695
+ // `restore` collects the replaced nodes for the caller to put back.
696
+ _resliceSyncs(lhs, stmt, inline = false, restore = []) {
697
+ if (!this._resliceNames?.size) return null;
698
+ const targets = this._resliceTargets(lhs, stmt);
699
+ if (targets.size === 0) return null;
700
+ const called = stmtHasCall(lhs, stmt);
701
+ const moved = reassignsChains(lhs, stmt);
702
+ const vars = lhs.length === 1 && !called;
703
+ const reusable = (e) => isReusableIndex(e.index, vars, this._boxedVars);
704
+ const out = [...targets].filter((e) => !inline || (!moved && reusable(e)));
705
+ const operands = called || moved;
706
+ if (!inline && (operands || out.some((e) => !reusable(e))))
707
+ this._bindResliceOperands(lhs, targets, operands, reusable, restore);
708
+ if (out.length === 0) return null;
709
+ this.useHelper("slice");
710
+ return out.map((e) => {
711
+ const wrapField = this._namedWrapperField(e.expr._type, e.expr);
712
+ const base = wrapField
713
+ ? `${this.genExpr(e.expr)}.${wrapField}`
714
+ : this.genExpr(e.expr);
715
+ return `__rcs(${base}, ${this.genExpr(e.index)})`;
716
+ });
717
+ },
718
+
719
+ // The `_isResliceSyncTarget` writes in `lhs`.
720
+ _resliceTargets(lhs, stmt) {
721
+ const assigned = new Set();
722
+ if (stmt.kind === "AssignStmt" && stmt.lhs.length > 1)
723
+ for (const e of stmt.lhs) if (e.kind === "Ident") assigned.add(e.name);
724
+ return new Set(lhs.filter((e) => this._isResliceSyncTarget(e, assigned)));
725
+ },
726
+
727
+ // Binds the left-hand operands that `_resliceSyncs` must not evaluate
728
+ // twice to temporaries, in source order: an index after its operand, and
729
+ // the operand of every synced write when `operands`.
730
+ _bindResliceOperands(lhs, targets, operands, reusable, restore) {
731
+ const bind = (e, key) => {
732
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
733
+ const tmp = `__rci${this._tmpCounter}`;
734
+ this.line(`const ${tmp} = ${this.genExpr(e[key])};`);
735
+ restore.push([e, key, e[key]]);
736
+ e[key] = { kind: "Ident", name: tmp, _type: e[key]._type };
737
+ };
738
+ for (const e of lhs) {
739
+ if (e.kind !== "IndexExpr") continue;
740
+ const t = targets.has(e);
741
+ const index = (t && !reusable(e)) || hasCall(e.index);
742
+ if ((t && operands) || index) bind(e, "expr");
743
+ if (index) bind(e, "index");
744
+ }
745
+ },
746
+
747
+ // `x[i]` on a plain slice held by a chain in `_resliceNames` whose root
748
+ // variable the statement does not reassign.
749
+ _isResliceSyncTarget(e, assigned) {
750
+ if (e.kind !== "IndexExpr" || e._mapValueType) return false;
751
+ const c = e.expr;
752
+ if (c.kind !== "Ident" && c.kind !== "SelectorExpr") return false;
753
+ if (!this._resliceNames.has(c.kind === "Ident" ? c.name : `.${c.field}`))
754
+ return false;
755
+ const u = c._type?.kind === "named" ? c._type.underlying : c._type;
756
+ if (u?.kind !== "slice" || isTypedArraySlice(c._type)) return false;
757
+ let root = c;
758
+ while (root.kind === "SelectorExpr") root = root.expr;
759
+ return (
760
+ root.kind === "Ident" &&
761
+ !assigned.has(root.name) &&
762
+ isStableIndex(c, assigned)
763
+ );
764
+ },
765
+
766
+ _emitResliceSyncs(syncs) {
767
+ if (syncs) for (const s of syncs) this.line(`${s};`);
768
+ },
769
+
282
770
  _genAssignStmt(stmt) {
771
+ this._markIndexLvalues(stmt.lhs);
772
+ if (stmt.op !== "=") for (const l of stmt.lhs) this._markDerefOpTarget(l);
773
+ this._markSelfReslice(stmt);
283
774
  if (this._isComplexCompoundAssign(stmt)) {
284
775
  this._genComplexCompoundAssign(stmt);
285
776
  return;
@@ -292,18 +783,42 @@ export const statementGenMethods = {
292
783
  this._genCommaOkTypeAssert(stmt);
293
784
  return;
294
785
  }
786
+ if (this._isTypeParamDerefAssign(stmt)) {
787
+ const ptr = this.genExpr(stmt.lhs[0].operand);
788
+ const val = this.genValueExpr(stmt.rhs[0]);
789
+ const isStruct = this._typeParamIsStructJs(
790
+ stmt.lhs[0].operand._type.base.name,
791
+ );
792
+ this.line(
793
+ `((__p, __v) => { if (${isStruct}) Object.assign(__p, __v); else __p.value = __v; })(${ptr}, ${val});`,
794
+ );
795
+ return;
796
+ }
797
+ if (this._hasTypeParamDerefTarget(stmt)) {
798
+ this._genTypeParamDerefMultiAssign(stmt);
799
+ return;
800
+ }
295
801
  if (this._isStructDerefAssign(stmt)) {
296
802
  this._genStructDerefAssign(stmt);
297
803
  return;
298
804
  }
805
+ if (this._isStableWholeAssign(stmt)) {
806
+ this._genStableWholeAssign(stmt);
807
+ return;
808
+ }
809
+ if (this._isStableTupleAssign(stmt)) {
810
+ this._genStableTupleAssign(stmt);
811
+ return;
812
+ }
299
813
  const paired = stmt.rhs.length === stmt.lhs.length && stmt.op === "=";
300
814
  const rhs = stmt.rhs.map((e, i) =>
301
815
  paired && stmt.lhs[i].name !== "_"
302
816
  ? this.genValueExpr(e)
303
817
  : this.genExpr(e),
304
818
  );
305
- this._markIndexLvalues(stmt.lhs);
819
+ const hoisted = this._hoistLhsOperands(stmt);
306
820
  const lhs = stmt.lhs.map((e) => this._genAssignLhsExpr(e));
821
+ hoisted?.restore();
307
822
  const pairs = lhs.map((l, i) => ({ l, r: rhs[i] ?? rhs[0] }));
308
823
  const active = pairs.filter((p) => p.l !== "_");
309
824
  if (active.length === 0) {
@@ -324,42 +839,139 @@ export const statementGenMethods = {
324
839
  : this.genExpr(stmt.lhs[0].expr);
325
840
  const idx = this.genExpr(stmt.lhs[0].index);
326
841
  const binOp = stmt.op.slice(0, -1);
842
+ const is64Val = is64BitInt(stmt.lhs[0]._mapValueType);
843
+ let rhsVal = active[0].r;
844
+ if (
845
+ is64Val &&
846
+ stmt.rhs[0]?._type?.kind === "untyped" &&
847
+ !rhsVal.endsWith("n") &&
848
+ /^-?\d+$/.test(rhsVal)
849
+ ) {
850
+ rhsVal = `${rhsVal}n`;
851
+ }
852
+ const rhsExpr = is64Val
853
+ ? this._gen64BitBinary(
854
+ binOp,
855
+ `(__m[__k] ?? ${zero})`,
856
+ rhsVal,
857
+ stmt.lhs[0]._mapValueType,
858
+ )
859
+ : `(__m[__k] ?? ${zero}) ${binOp === "&^" ? "& ~" : binOp} (${rhsVal})`;
860
+ const rhsExprDirect = is64Val
861
+ ? this._gen64BitBinary(
862
+ binOp,
863
+ `(${base}[${idx}] ?? ${zero})`,
864
+ rhsVal,
865
+ stmt.lhs[0]._mapValueType,
866
+ )
867
+ : `(${base}[${idx}] ?? ${zero}) ${binOp === "&^" ? "& ~" : binOp} (${rhsVal})`;
327
868
  if (
328
869
  this._hasCallExpr(stmt.lhs[0].expr) ||
329
870
  this._hasCallExpr(stmt.lhs[0].index)
330
871
  ) {
331
872
  this.line(
332
- `((__m, __k) => { __m[__k] = (__m[__k] ?? ${zero}) ${binOp} (${active[0].r}); })(${base}, ${idx});`,
873
+ `((__m, __k) => { ${this._genMapWriteBase("__m")}[__k] = ${rhsExpr}; })(${base}, ${idx});`,
333
874
  );
334
875
  } else {
335
876
  this.line(
336
- `${base}[${idx}] = (${base}[${idx}] ?? ${zero}) ${binOp} (${active[0].r});`,
877
+ `${this._genMapWriteBase(base)}[${idx}] = ${rhsExprDirect};`,
337
878
  );
338
879
  }
339
880
  return;
340
881
  }
882
+ const is64 = is64BitInt(stmt.lhs[0]._type);
883
+ let rhsVal = active[0].r;
341
884
  if (
342
- this.strict &&
343
- stmt.op !== "=" &&
885
+ is64 &&
886
+ stmt.rhs[0]?._type?.kind === "untyped" &&
887
+ !rhsVal.endsWith("n") &&
888
+ /^-?\d+$/.test(rhsVal)
889
+ ) {
890
+ rhsVal = `${rhsVal}n`;
891
+ }
892
+ if (
893
+ this._needsStrictCompoundStmt(stmt, is64) &&
344
894
  this._genStrictCompound(
345
895
  stmt.lhs[0],
346
896
  stmt.op.slice(0, -1),
347
- active[0].r,
897
+ rhsVal,
348
898
  null,
349
899
  active[0].l,
900
+ stmt.rhs[0]?._type,
350
901
  )
351
902
  ) {
352
903
  return;
353
904
  }
354
- this.line(`${active[0].l} ${stmt.op} ${active[0].r};`);
905
+ const targetL =
906
+ active[0].l.startsWith("{") && !active[0].l.startsWith("({")
907
+ ? `(${active[0].l})`
908
+ : active[0].l;
909
+ if (stmt.op === "&^=") this.line(`${targetL} &= ~(${rhsVal});`);
910
+ else this.line(`${targetL} ${stmt.op} ${rhsVal};`);
911
+ } else if (hoisted) {
912
+ this.line("{");
913
+ this.indented(() => {
914
+ for (const l of hoisted.lines) this.line(l);
915
+ this._genAssignMulti(stmt, lhs, rhs, active, pairs);
916
+ });
917
+ this.line("}");
355
918
  } else {
356
919
  this._genAssignMulti(stmt, lhs, rhs, active, pairs);
357
920
  }
358
921
  },
359
922
 
923
+ // `k, xs[k] = 1, 5`: Go evaluates the operands of every index and field
924
+ // target before any assignment, but a JS destructuring target reads its
925
+ // operands after the earlier ones are stored. Operands that mention a
926
+ // variable assigned by the same statement are bound to temporaries first.
927
+ _hoistLhsOperands(stmt) {
928
+ if (stmt.op !== "=" || stmt.lhs.length < 2) return null;
929
+ const assigned = new Set(
930
+ stmt.lhs
931
+ .filter((l) => l.kind === "Ident" && l.name !== "_")
932
+ .map((l) => l.name),
933
+ );
934
+ if (assigned.size === 0) return null;
935
+ const mentions = (n) =>
936
+ !!n &&
937
+ typeof n === "object" &&
938
+ ((n.kind === "Ident" && assigned.has(n.name)) || someChild(n, mentions));
939
+ const lines = [];
940
+ const undo = [];
941
+ const visit = (l) => {
942
+ let keys;
943
+ if (l.kind === "IndexExpr") keys = ["expr", "index"];
944
+ else if (l.kind === "SelectorExpr" && !l._isMethodExpr) keys = ["expr"];
945
+ else return;
946
+ for (const key of keys) {
947
+ const operand = l[key];
948
+ if (!mentions(operand)) continue;
949
+ // A struct or array value names the variable's storage in Go: hoist
950
+ // the operands inside it (`ps[n].A`) instead of the value.
951
+ if (key === "expr" && this._aggregateBase(operand._type)) {
952
+ visit(operand);
953
+ continue;
954
+ }
955
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
956
+ const name = `__h${this._tmpCounter}`;
957
+ lines.push(`const ${name} = ${this.genExpr(operand)};`);
958
+ undo.push([l, key, operand]);
959
+ l[key] = { kind: "Ident", name, _type: operand._type };
960
+ }
961
+ };
962
+ for (const l of stmt.lhs) visit(l);
963
+ if (lines.length === 0) return null;
964
+ return {
965
+ lines,
966
+ restore: () => {
967
+ for (const [node, key, v] of undo) node[key] = v;
968
+ },
969
+ };
970
+ },
971
+
360
972
  _genCommaOkTypeAssert(stmt) {
361
973
  const val = this.genExpr(stmt.rhs[0]);
362
- const [vName, okName] = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
974
+ const [vName, okName] = stmt.lhs.map((e) => this._genAssignLhsExpr(e));
363
975
  this._tmpCounter = (this._tmpCounter ?? 0) + 1;
364
976
  const tmp = `__ta${this._tmpCounter}`;
365
977
  this.line(`const ${tmp} = ${val};`);
@@ -367,6 +979,118 @@ export const statementGenMethods = {
367
979
  if (okName !== "_") this.line(`${okName} = ${tmp}[1];`);
368
980
  },
369
981
 
982
+ // `a = [3]int{...}` / `s = S{...}` on a variable whose address (or an
983
+ // element's or field's address) is taken must write into the existing
984
+ // storage so those pointers see the new value (Task 0.33).
985
+ _isStableWholeAssign(stmt) {
986
+ if (stmt.op !== "=" || stmt.lhs.length !== 1 || stmt.rhs.length !== 1)
987
+ return false;
988
+ return this._isStableTarget(stmt.lhs[0], stmt.rhs[0]);
989
+ },
990
+
991
+ // `l = rhs` writes into the existing storage of `l` (see above). When the
992
+ // program takes `&xs[i]` anywhere, an element of a slice does too: any
993
+ // callee may hold the pointer.
994
+ _isStableTarget(l, rhs) {
995
+ if (l.kind === "IndexExpr" && l._mapValueType) return false;
996
+ if (
997
+ l.kind !== "Ident" &&
998
+ l.kind !== "SelectorExpr" &&
999
+ l.kind !== "IndexExpr"
1000
+ )
1001
+ return false;
1002
+ if (l.kind === "Ident" && l.name === "_") return false;
1003
+ if (!this._aggregateBase(l._type) || !this._aggregateBase(rhs._type))
1004
+ return false;
1005
+ if (this._elemAddrTaken && this._isSliceElemTarget(l)) return true;
1006
+ const root = this._rootIdentName(l);
1007
+ return root !== null && this._fnMutates(root, true);
1008
+ },
1009
+
1010
+ // `xs[i]` on a plain slice (not a typed array, array or map).
1011
+ _isSliceElemTarget(l) {
1012
+ if (l.kind !== "IndexExpr" || l._mapValueType) return false;
1013
+ const u =
1014
+ l.expr._type?.kind === "named" ? l.expr._type.underlying : l.expr._type;
1015
+ return u?.kind === "slice" && !isTypedArraySlice(l.expr._type);
1016
+ },
1017
+
1018
+ // `a, b = x, y` where a target is stored in place: every right-hand value
1019
+ // is evaluated (and copied) first, then each target is assigned in order.
1020
+ _isStableTupleAssign(stmt) {
1021
+ return (
1022
+ stmt.op === "=" &&
1023
+ stmt.lhs.length > 1 &&
1024
+ stmt.rhs.length === stmt.lhs.length &&
1025
+ stmt.lhs.some((l, i) => this._isStableTarget(l, stmt.rhs[i]))
1026
+ );
1027
+ },
1028
+
1029
+ _genStableTupleAssign(stmt) {
1030
+ const hoisted = this._hoistLhsOperands(stmt);
1031
+ const stable = stmt.lhs.map((l, i) => this._isStableTarget(l, stmt.rhs[i]));
1032
+ this.line("{");
1033
+ this.indented(() => {
1034
+ for (const l of hoisted?.lines ?? []) this.line(l);
1035
+ const base = (this._tmpCounter ?? 0) + 1;
1036
+ this._tmpCounter = base + stmt.lhs.length;
1037
+ const val = (i) => `__s${base + i}`;
1038
+ const dst = (i) => `__d${base + i}`;
1039
+ stmt.rhs.forEach((r, i) => {
1040
+ this.line(`const ${val(i)} = ${this.genValueExpr(r)};`);
1041
+ });
1042
+ stmt.lhs.forEach((l, i) => {
1043
+ if (stable[i])
1044
+ this.line(`const ${dst(i)} = ${this._genAssignLhsExpr(l)};`);
1045
+ });
1046
+ stmt.lhs.forEach((l, i) => {
1047
+ if (stable[i]) this._emitCopyInto(dst(i), val(i), l._type);
1048
+ else if (!(l.kind === "Ident" && l.name === "_"))
1049
+ this.line(`${this._genAssignLhsExpr(l)} = ${val(i)};`);
1050
+ });
1051
+ });
1052
+ this.line("}");
1053
+ hoisted?.restore();
1054
+ },
1055
+
1056
+ _genStableWholeAssign(stmt) {
1057
+ let target = this._genAssignLhsExpr(stmt.lhs[0]);
1058
+ const type = stmt.lhs[0]._type;
1059
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
1060
+ const src = `__s${this._tmpCounter}`;
1061
+ this.line(`{ const ${src} = ${this.genExpr(stmt.rhs[0])};`);
1062
+ if (stmt.lhs[0].kind === "IndexExpr") {
1063
+ // Evaluate the element reference once.
1064
+ target = `__d${this._tmpCounter}`;
1065
+ this.line(`const ${target} = ${this._genAssignLhsExpr(stmt.lhs[0])};`);
1066
+ }
1067
+ this._emitCopyInto(target, src, type);
1068
+ this.line("}");
1069
+ },
1070
+
1071
+ // Copies aggregate `src` into the storage `dst` element by element, going
1072
+ // into nested arrays and structs so pointers to inner parts stay valid.
1073
+ _emitCopyInto(dst, src, type) {
1074
+ const base = this._aggregateBase(type);
1075
+ if (base.kind === "struct") {
1076
+ for (const [name, ft] of base.fields) {
1077
+ if (name === "_") continue;
1078
+ const f = `.${name}`;
1079
+ if (this._aggregateBase(ft))
1080
+ this._emitCopyInto(`${dst}${f}`, `${src}${f}`, ft);
1081
+ else this.line(`${dst}${f} = ${src}${f};`);
1082
+ }
1083
+ return;
1084
+ }
1085
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
1086
+ const k = `__k${this._tmpCounter}`;
1087
+ this.line(`for (let ${k} = 0; ${k} < ${src}.length; ${k}++) {`);
1088
+ if (this._aggregateBase(base.elem))
1089
+ this._emitCopyInto(`${dst}[${k}]`, `${src}[${k}]`, base.elem);
1090
+ else this.line(`${dst}[${k}] = ${src}[${k}];`);
1091
+ this.line("}");
1092
+ },
1093
+
370
1094
  _genStructDerefAssign(stmt) {
371
1095
  const target = this.genExpr(stmt.lhs[0].operand);
372
1096
  const rhsNode = stmt.rhs[0];
@@ -393,31 +1117,45 @@ export const statementGenMethods = {
393
1117
  },
394
1118
 
395
1119
  _genComplexCompoundAssign(stmt) {
396
- const lhsStr = stmt.lhs[0].name ?? this.genExpr(stmt.lhs[0]);
1120
+ const l = stmt.lhs[0];
1121
+ let lhsTarget;
1122
+ let lhsRead;
1123
+ if (l.kind === "IndexExpr" && l._mapValueType) {
1124
+ const wrapField = this._namedWrapperField(l.expr._type, l.expr);
1125
+ const base = wrapField
1126
+ ? `${this.genExpr(l.expr)}.${wrapField}`
1127
+ : this.genExpr(l.expr);
1128
+ const idx = this.genExpr(l.index);
1129
+ lhsTarget = `${this._genMapWriteBase(base)}[${idx}]`;
1130
+ lhsRead = `(${base}[${idx}] ?? { re: 0, im: 0 })`;
1131
+ } else {
1132
+ lhsTarget = l.name ?? this.genExpr(l);
1133
+ lhsRead = lhsTarget;
1134
+ }
397
1135
  const rhsExpr = this._genComplexOperand(stmt.rhs[0]);
398
1136
  const baseOp = stmt.op.slice(0, -1); // "+=" → "+"
399
1137
  let result;
400
1138
  switch (baseOp) {
401
1139
  case "+":
402
- result = `{ re: ${lhsStr}.re + ${rhsExpr}.re, im: ${lhsStr}.im + ${rhsExpr}.im }`;
1140
+ result = `{ re: ${lhsRead}.re + ${rhsExpr}.re, im: ${lhsRead}.im + ${rhsExpr}.im }`;
403
1141
  break;
404
1142
  case "-":
405
- result = `{ re: ${lhsStr}.re - ${rhsExpr}.re, im: ${lhsStr}.im - ${rhsExpr}.im }`;
1143
+ result = `{ re: ${lhsRead}.re - ${rhsExpr}.re, im: ${lhsRead}.im - ${rhsExpr}.im }`;
406
1144
  break;
407
1145
  case "*":
408
1146
  this.useHelper("cmul");
409
- result = `__cmul(${lhsStr}, ${rhsExpr})`;
1147
+ result = `__cmul(${lhsRead}, ${rhsExpr})`;
410
1148
  break;
411
1149
  case "/":
412
1150
  this.useHelper("cdiv");
413
- result = `__cdiv(${lhsStr}, ${rhsExpr})`;
1151
+ result = `__cdiv(${lhsRead}, ${rhsExpr})`;
414
1152
  break;
415
1153
  default:
416
1154
  throw new Error(
417
1155
  `unsupported complex compound assignment operator: ${stmt.op}`,
418
1156
  );
419
1157
  }
420
- this.line(`${lhsStr} = ${result};`);
1158
+ this.line(`${lhsTarget} = ${result};`);
421
1159
  },
422
1160
 
423
1161
  _genIteratorReturn(stmt) {
@@ -476,8 +1214,12 @@ export const statementGenMethods = {
476
1214
  }
477
1215
  },
478
1216
 
479
- _genBranchStmt(stmt) {
480
- if (this._inIteratorBody && !stmt.label) {
1217
+ _genIteratorBranchStmt(stmt) {
1218
+ if (!stmt.label) {
1219
+ if (this._iterLoopDepth > 0) {
1220
+ if (stmt.keyword !== "fallthrough") this.line(`${stmt.keyword};`);
1221
+ return;
1222
+ }
481
1223
  if (stmt.keyword === "break") {
482
1224
  this.line(`${this._iterBreakFlag} = true; return false;`);
483
1225
  return;
@@ -486,6 +1228,29 @@ export const statementGenMethods = {
486
1228
  this.line("return true;");
487
1229
  return;
488
1230
  }
1231
+ return;
1232
+ }
1233
+ if (this._innerLoopLabels?.has(stmt.label)) {
1234
+ if (stmt.keyword !== "fallthrough")
1235
+ this.line(`${stmt.keyword} ${stmt.label};`);
1236
+ return;
1237
+ }
1238
+ if (
1239
+ stmt.keyword === "continue" &&
1240
+ stmt.label === this._currentIterLoopLabel
1241
+ ) {
1242
+ this.line("return true;");
1243
+ return;
1244
+ }
1245
+ this.line(
1246
+ `${this._iterTargetFlag} = "${stmt.keyword}$${stmt.label}"; return false;`,
1247
+ );
1248
+ },
1249
+
1250
+ _genBranchStmt(stmt) {
1251
+ if (this._inIteratorBody) {
1252
+ this._genIteratorBranchStmt(stmt);
1253
+ return;
489
1254
  }
490
1255
  if (stmt.keyword !== "fallthrough")
491
1256
  this.line(
@@ -505,7 +1270,12 @@ export const statementGenMethods = {
505
1270
  this.line("}");
506
1271
  return;
507
1272
  }
508
- if (elseBody.kind === "IfStmt") {
1273
+ if (elseBody.kind === "IfStmt" && elseBody.init) {
1274
+ // `else if init; cond`: the init runs only when this branch is taken.
1275
+ this.line("} else {");
1276
+ this.indented(() => this.genStmt(elseBody));
1277
+ this.line("}");
1278
+ } else if (elseBody.kind === "IfStmt") {
509
1279
  this.line(`} else if (${this.genExpr(elseBody.cond)}) {`);
510
1280
  this.indented(() => this.genBlock(elseBody.body));
511
1281
  this._genElse(elseBody.elseBody);
@@ -518,7 +1288,8 @@ export const statementGenMethods = {
518
1288
 
519
1289
  _rangeExprLimit(iterType, iteree) {
520
1290
  if (this._isIntRangeType(iterType)) return iteree;
521
- if (this._isMapRangeType(iterType)) return `Object.keys(${iteree}).length`;
1291
+ if (this._isMapRangeType(iterType))
1292
+ return `Object.keys(${iteree} ?? {}).length`;
522
1293
  return `(${iteree} ? ${iteree}.length : 0)`;
523
1294
  },
524
1295
 
@@ -527,7 +1298,7 @@ export const statementGenMethods = {
527
1298
  this.genIteratorForCond(stmt);
528
1299
  return;
529
1300
  }
530
- const iterType = stmt.cond.expr._type;
1301
+ const iterType = stmt.cond.expr._coreType ?? stmt.cond.expr._type;
531
1302
  const iteree = this.genExpr(stmt.cond.expr);
532
1303
  const limit = this._rangeExprLimit(iterType, iteree);
533
1304
  this.line(`for (let _$ = 0; _$ < ${limit}; _$++) {`);
@@ -545,32 +1316,114 @@ export const statementGenMethods = {
545
1316
  this.line("}");
546
1317
  },
547
1318
 
1319
+ // `for n := n; ...` declares a loop variable its own initialiser reads: the
1320
+ // initialiser values are evaluated into temporaries ahead of the loop.
1321
+ _hoistForInit(stmt) {
1322
+ const init = stmt.kind === "ForStmt" && stmt.init;
1323
+ if (
1324
+ !init ||
1325
+ stmt._initPre ||
1326
+ init.kind !== "DefineStmt" ||
1327
+ this.isRangeFor(stmt) ||
1328
+ stmt.cond?.kind === "RangeExpr" ||
1329
+ !this._definesShadowedRead(init)
1330
+ )
1331
+ return false;
1332
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
1333
+ const values = this._defineRhs(init);
1334
+ const names = values.map((_, i) => `__fi${this._tmpCounter}_${i}`);
1335
+ this.line("{");
1336
+ this.indent++;
1337
+ this.line(
1338
+ `const ${values.map((v, i) => `${names[i]} = ${v}`).join(", ")};`,
1339
+ );
1340
+ stmt._initPre = names;
1341
+ return true;
1342
+ },
1343
+
1344
+ _closeHoistedForInit() {
1345
+ this.indent--;
1346
+ this.line("}");
1347
+ },
1348
+
548
1349
  genFor(stmt) {
549
- if (this.isRangeFor(stmt)) {
550
- if (stmt.init.rhs[0]._isIterator) this.genIteratorFor(stmt);
551
- else this.genRangeFor(stmt);
552
- return;
1350
+ const hoisted = this._hoistForInit(stmt);
1351
+ try {
1352
+ this._genForLoop(stmt);
1353
+ } finally {
1354
+ delete stmt._initPre;
553
1355
  }
554
- if (stmt.cond?.kind === "RangeExpr") {
555
- this._genRangeExprFor(stmt);
556
- return;
1356
+ if (hoisted) this._closeHoistedForInit();
1357
+ },
1358
+
1359
+ _genForLoop(stmt) {
1360
+ const isInner = this._inIteratorBody;
1361
+ if (isInner) {
1362
+ this._iterLoopDepth = (this._iterLoopDepth ?? 0) + 1;
1363
+ if (stmt.label) {
1364
+ this._innerLoopLabels = this._innerLoopLabels ?? new Set();
1365
+ this._innerLoopLabels.add(stmt.label);
1366
+ }
557
1367
  }
558
- if (!stmt.init && !stmt.post) {
559
- this._genSimpleWhileFor(stmt);
560
- return;
1368
+ try {
1369
+ if (this.isRangeFor(stmt)) {
1370
+ if (stmt.init.rhs[0]._isIterator) this.genIteratorFor(stmt);
1371
+ else this.genRangeFor(stmt);
1372
+ return;
1373
+ }
1374
+ if (stmt.cond?.kind === "RangeExpr") {
1375
+ this._genRangeExprFor(stmt);
1376
+ return;
1377
+ }
1378
+ if (!stmt.init && !stmt.post) {
1379
+ this._genSimpleWhileFor(stmt);
1380
+ return;
1381
+ }
1382
+ const init = stmt.init ? this.stmtInline(stmt.init, stmt._initPre) : "";
1383
+ const cond = stmt.cond ? this.genExpr(stmt.cond) : "";
1384
+ const post = this._perIterationClones(
1385
+ stmt,
1386
+ stmt.post ? this.stmtInline(stmt.post) : "",
1387
+ );
1388
+ this.line(`for (${init}; ${cond}; ${post}) {`);
1389
+ this.indented(() => this.genBlock(stmt.body));
1390
+ this.line("}");
1391
+ } finally {
1392
+ if (isInner) {
1393
+ this._iterLoopDepth--;
1394
+ if (stmt.label) this._innerLoopLabels.delete(stmt.label);
1395
+ }
561
1396
  }
562
- const init = stmt.init ? this.stmtInline(stmt.init) : "";
563
- const cond = stmt.cond ? this.genExpr(stmt.cond) : "";
564
- const post = stmt.post ? this.stmtInline(stmt.post) : "";
565
- this.line(`for (${init}; ${cond}; ${post}) {`);
566
- this.indented(() => this.genBlock(stmt.body));
567
- this.line("}");
568
1397
  },
569
1398
 
570
1399
  isRangeFor(stmt) {
571
1400
  return isRangeFor(stmt);
572
1401
  },
573
1402
 
1403
+ // Go 1.22 per-iteration loop variables. `let` gives each iteration its
1404
+ // own binding, but a struct or array variable is a reference and an
1405
+ // address-taken scalar is a `{ value }` box: replace it at the start of
1406
+ // the post step (which runs in the next iteration's binding), so closures
1407
+ // and pointers from the finished iteration keep their own variable.
1408
+ _perIterationClones(stmt, post) {
1409
+ const clones = [];
1410
+ for (const name of loopVarsCapturedInLoop(stmt)) {
1411
+ const i = stmt.init.lhs.findIndex((e) => e.name === name);
1412
+ const t = stmt.init.lhs[i]?._type ?? stmt.init.rhs?.[i]?._type;
1413
+ if (this._boxedVars?.has(name)) {
1414
+ const v = `${name}.value`;
1415
+ const copy = this._aggregateBase(t) ? this._cloneJs(t, v) : v;
1416
+ clones.push(`${name} = { value: ${copy} }`);
1417
+ continue;
1418
+ }
1419
+ if (!this._aggregateBase(t)) continue;
1420
+ clones.push(`${name} = ${this._cloneJs(t, name)}`);
1421
+ }
1422
+ if (clones.length === 0) return post;
1423
+ const tail = post.replace(/;\s*$/, "");
1424
+ return tail ? `${clones.join(", ")}, ${tail}` : clones.join(", ");
1425
+ },
1426
+
574
1427
  _isIntRangeType(iterType) {
575
1428
  return isIntRangeType(iterType);
576
1429
  },
@@ -617,10 +1470,14 @@ export const statementGenMethods = {
617
1470
  const iterType = iterNode._type;
618
1471
  const base = iterType?.kind === "named" ? iterType.underlying : iterType;
619
1472
  const elemType = base?.elem;
620
- if (!valNode || !this._aggregateBase(elemType)) return js;
621
- if (isAssign || valNode.kind !== "Ident")
1473
+ if (
1474
+ !valNode ||
1475
+ (!this._aggregateBase(elemType) && !this._typeParamClone(elemType, js))
1476
+ )
1477
+ return js;
1478
+ if (valNode.kind === "Ident" && valNode.name === "_") return js;
1479
+ if (isAssign || valNode.kind !== "Ident" || elemType?.kind === "typeParam")
622
1480
  return this._cloneJs(elemType, js);
623
- if (valNode.name === "_") return js;
624
1481
  const root = this._rootIdentName(iterNode);
625
1482
  const mutated =
626
1483
  this._fnMutates(valNode.name) ||
@@ -629,21 +1486,33 @@ export const statementGenMethods = {
629
1486
  return mutated ? this._cloneJs(elemType, js) : js;
630
1487
  },
631
1488
 
632
- _genSliceRangeFor(stmt, iteree, isAssign) {
1489
+ _genSliceRangeFor(stmt, iteree, isAssign, unwrapView = false) {
633
1490
  this._loopDepth = this._loopDepth ?? 0;
634
1491
  const d = this._loopDepth++;
635
1492
  const [idxNode, valNode] = stmt.init.lhs;
636
1493
  const i = `__i${d}`;
637
1494
  const arr = `__arr${d}`;
638
1495
  const len = `__len${d}`;
639
- this.line(
640
- `for (let ${i} = 0, ${arr} = ${iteree}, ${len} = ${arr} ? ${arr}.length : 0; ${i} < ${len}; ${i}++) {`,
641
- );
1496
+ let elem = `${arr}[${i}]`;
1497
+ if (unwrapView) {
1498
+ // A partial view of a plain array is a Proxy (`__slice`): read the
1499
+ // elements from its backing array `b` at offset `o` instead.
1500
+ this.useHelper("sv");
1501
+ const [v, b, o] = [`__v${d}`, `__b${d}`, `__o${d}`];
1502
+ this.line(
1503
+ `for (let ${i} = 0, ${arr} = ${iteree}, ${v} = ${arr}?.[__sv], ${b} = ${v} === undefined ? ${arr} : ${v}.b, ${o} = ${v} === undefined ? 0 : ${v}.o, ${len} = ${v} !== undefined ? ${v}.n : ${arr} ? ${arr}.length : 0; ${i} < ${len}; ${i}++) {`,
1504
+ );
1505
+ elem = `${b}[${o} + ${i}]`;
1506
+ } else {
1507
+ this.line(
1508
+ `for (let ${i} = 0, ${arr} = ${iteree}, ${len} = ${arr} ? ${arr}.length : 0; ${i} < ${len}; ${i}++) {`,
1509
+ );
1510
+ }
642
1511
  this.indented(() => {
643
1512
  this._emitRangeVar(idxNode, i, isAssign);
644
1513
  this._emitRangeVar(
645
1514
  valNode,
646
- this._rangeElemValue(stmt, valNode, `${arr}[${i}]`, isAssign),
1515
+ this._rangeElemValue(stmt, valNode, elem, isAssign),
647
1516
  isAssign,
648
1517
  );
649
1518
  const declared = isAssign
@@ -699,11 +1568,6 @@ export const statementGenMethods = {
699
1568
  this._loopDepth--;
700
1569
  },
701
1570
 
702
- // True when `node` contains an assignment or ++/-- targeting identifier `name`.
703
- _nodeAssigns(node, name) {
704
- return nodeAssigns(node, name);
705
- },
706
-
707
1571
  _genStringRangeFor(stmt, iteree, isAssign) {
708
1572
  this._loopDepth = this._loopDepth ?? 0;
709
1573
  const d = this._loopDepth++;
@@ -711,13 +1575,17 @@ export const statementGenMethods = {
711
1575
  const s = `__s${d}`;
712
1576
  const i = `__i${d}`;
713
1577
  const cp = `__cp${d}`;
1578
+ const w = `__w${d}`;
1579
+ // The index is a UTF-16 offset (an astral rune is two units) so `s[i:]`
1580
+ // agrees with the WebAssembly string layout.
714
1581
  this.line(
715
- `for (let ${i} = 0, ${s} = Array.from(${iteree} ?? ""); ${i} < ${s}.length; ${i}++) {`,
1582
+ `for (let ${i} = 0, ${s} = ${iteree} ?? "", ${w} = 0; ${i} < ${s}.length; ${i} += ${w}) {`,
716
1583
  );
717
1584
  this.indented(() => {
1585
+ this.line(`const ${cp} = ${s}.codePointAt(${i});`);
1586
+ this.line(`${w} = ${cp} > 0xffff ? 2 : 1;`);
718
1587
  this._emitRangeVar(idxNode, i, isAssign);
719
1588
  if (valNode && valNode.name !== "_") {
720
- this.line(`const ${cp} = ${s}[${i}].codePointAt(0);`);
721
1589
  this._emitRangeVar(valNode, cp, isAssign);
722
1590
  }
723
1591
  const declared = isAssign
@@ -739,23 +1607,108 @@ export const statementGenMethods = {
739
1607
 
740
1608
  _genIterFor(init, iterType, iteree, lhs, body) {
741
1609
  const iterExpr = this._genRangeIterExpr(iterType, iteree, lhs);
1610
+ const keyDecode = this._structKeyRange(iterType);
1611
+ const paramKey = this._typeParamKeyRange(iterType);
1612
+ const is64Key = this._is64BitMapRange(iterType);
1613
+ const scalarKey = this._scalarKeyRange(iterType);
1614
+ const isAssign = init.kind === "AssignStmt";
1615
+ // `for _, v = range m` assigns existing variables, where a blank is a
1616
+ // hole in the destructuring pattern; a lone blank still needs a binding.
1617
+ const blank = isAssign && lhs.length > 1 ? "" : "_$";
742
1618
  const binding =
743
1619
  lhs.length === 1
744
1620
  ? lhs[0] === "_"
745
1621
  ? "_$"
746
1622
  : lhs[0]
747
- : `[${lhs.map((n) => (n === "_" ? "_$" : n)).join(", ")}]`;
748
- const kw = init.kind === "AssignStmt" ? "" : "let ";
1623
+ : `[${lhs.map((n) => (n === "_" ? blank : n)).join(", ")}]`;
1624
+ const kw = !isAssign || binding === "_$" ? "let " : "";
749
1625
  this.line(`for (${kw}${binding} of ${iterExpr}) {`);
750
- this.indented(() => this.genBlock(body));
1626
+ this.indented(() => {
1627
+ if (paramKey && lhs[0] !== "_")
1628
+ this.line(`${lhs[0]} = __kdt(${paramKey}, ${lhs[0]});`);
1629
+ else if (keyDecode && lhs[0] !== "_")
1630
+ this.line(`${lhs[0]} = __kd(${lhs[0]}, true);`);
1631
+ else if (is64Key && lhs[0] !== "_")
1632
+ this.line(`${lhs[0]} = BigInt(${lhs[0]});`);
1633
+ else if (scalarKey && lhs[0] !== "_")
1634
+ this.line(
1635
+ scalarKey === "bool"
1636
+ ? `${lhs[0]} = ${lhs[0]} === "true";`
1637
+ : `${lhs[0]} = Number(${lhs[0]});`,
1638
+ );
1639
+ this._cloneMapRangeValue(init, iterType, lhs[1]);
1640
+ this.genBlock(body);
1641
+ });
751
1642
  this.line("}");
752
1643
  },
753
1644
 
1645
+ _is64BitMapRange(iterType) {
1646
+ if (!this._isMapRangeType(iterType)) return false;
1647
+ const base = iterType?.kind === "named" ? iterType.underlying : iterType;
1648
+ const key = base?.kind === "map" ? base.key : null;
1649
+ return is64BitInt(key);
1650
+ },
1651
+
1652
+ // Ranging over a map with plain int, float or bool keys: object property
1653
+ // names are strings, so the key variable is converted back.
1654
+ _scalarKeyRange(iterType) {
1655
+ if (!this._isMapRangeType(iterType)) return null;
1656
+ const base = iterType?.kind === "named" ? iterType.underlying : iterType;
1657
+ const key = base?.kind === "map" ? base.key : null;
1658
+ if (!key || key.kind === "typeParam") return null;
1659
+ const u = this._underlyingOf(key);
1660
+ if (u?.kind !== "basic" || this._is64BitInt(key)) return null;
1661
+ if (u.name === "bool") return "bool";
1662
+ return /^(u?int\d*|uintptr|float(32|64))$/.test(u.name) ? "num" : null;
1663
+ },
1664
+
1665
+ // Ranging over a map whose key type is a type parameter: the key is decoded
1666
+ // from its property string by the instantiated key type's zero factory.
1667
+ _typeParamKeyRange(iterType) {
1668
+ if (!this._isMapRangeType(iterType)) return null;
1669
+ const base = iterType?.kind === "named" ? iterType.underlying : iterType;
1670
+ const key = base?.kind === "map" ? base.key : null;
1671
+ if (key?.kind !== "typeParam") return null;
1672
+ const factory = this._typeParamZeroFactory(key.name);
1673
+ if (!factory) return null;
1674
+ this.useHelper("structKey");
1675
+ return factory;
1676
+ },
1677
+
1678
+ // Ranging over a map whose keys are structs: the key variable is decoded
1679
+ // from its encoded string (see HELPER_STRUCT_KEY).
1680
+ _structKeyRange(iterType) {
1681
+ if (!this._isMapRangeType(iterType)) return false;
1682
+ const base = iterType?.kind === "named" ? iterType.underlying : iterType;
1683
+ const key = base?.kind === "map" ? base.key : null;
1684
+ const keyBase = key?.kind === "named" ? key.underlying : key;
1685
+ if (
1686
+ keyBase?.kind !== "struct" &&
1687
+ keyBase?.kind !== "array" &&
1688
+ keyBase?.kind !== "pointer" &&
1689
+ !this._isIfaceType(key)
1690
+ )
1691
+ return false;
1692
+ this.useHelper("structKey");
1693
+ return true;
1694
+ },
1695
+
1696
+ // A map range value of array or struct type is a copy: clone it when the
1697
+ // body may mutate it (it otherwise aliases the stored value).
1698
+ _cloneMapRangeValue(init, iterType, name) {
1699
+ if (!name || name === "_" || !this._isMapRangeType(iterType)) return;
1700
+ const base = iterType?.kind === "named" ? iterType.underlying : iterType;
1701
+ const valType = base?.value ?? base?.elem;
1702
+ if (!this._aggregateBase(valType)) return;
1703
+ if (init.kind !== "AssignStmt" && !this._fnMutates(name)) return;
1704
+ this.line(`${name} = ${this._cloneJs(valType, name)};`);
1705
+ },
1706
+
754
1707
  genRangeFor(stmt) {
755
1708
  const init = stmt.init;
756
1709
  const range = init.rhs[0];
757
1710
  const lhs = init.lhs.map((e) => e.name ?? this.genExpr(e));
758
- const iterType = range.expr._type;
1711
+ const iterType = range.expr._coreType ?? range.expr._type;
759
1712
  const wrapField = this._namedWrapperField(iterType, range.expr);
760
1713
  const iteree = wrapField
761
1714
  ? `${this.genExpr(range.expr)}.${wrapField}`
@@ -776,20 +1729,74 @@ export const statementGenMethods = {
776
1729
  return;
777
1730
  }
778
1731
 
779
- this._genSliceRangeFor(stmt, iteree, init.kind === "AssignStmt");
1732
+ // Go ranges over a copy of an array: the range expression is evaluated
1733
+ // once, so writes to the array inside the body do not reach `v`.
1734
+ const valNode = init.lhs[1];
1735
+ const rangesArrayValue =
1736
+ valNode &&
1737
+ valNode.name !== "_" &&
1738
+ this._aggregateBase(iterType)?.kind === "array";
1739
+ const iterBase =
1740
+ iterType?.kind === "named" ? iterType.underlying : iterType;
1741
+ const unwrapView =
1742
+ valNode?.name !== "_" &&
1743
+ !!valNode &&
1744
+ iterBase?.kind === "slice" &&
1745
+ !isTypedArraySlice(iterType);
1746
+ // Ranging over an array for its indexes only does not evaluate the
1747
+ // array (`range p` on a nil array pointer is valid): the length is the
1748
+ // type's.
1749
+ const arrayLen = this._aggregateBase(iterType)?.size;
1750
+ const indexOnlyArray =
1751
+ (!valNode || valNode.name === "_") &&
1752
+ typeof arrayLen === "number" &&
1753
+ this._aggregateBase(iterType)?.kind === "array" &&
1754
+ !hasCallOrReceive(range.expr);
1755
+ this._genSliceRangeFor(
1756
+ stmt,
1757
+ indexOnlyArray
1758
+ ? `({ length: ${arrayLen} })`
1759
+ : rangesArrayValue
1760
+ ? this._cloneJs(iterType, iteree)
1761
+ : iteree,
1762
+ init.kind === "AssignStmt",
1763
+ unwrapView,
1764
+ );
780
1765
  },
781
1766
 
782
1767
  _genRangeIterExpr(iterType, iteree, lhs) {
783
1768
  if (this._isMapRangeType(iterType)) {
784
- if (lhs.length === 1) return `Object.keys(${iteree})`;
785
- return `Object.entries(${iteree})`;
1769
+ // A nil map is `null`; ranging over it runs zero iterations.
1770
+ if (lhs.length === 1) return `Object.keys(${iteree} ?? {})`;
1771
+ return `Object.entries(${iteree} ?? {})`;
786
1772
  }
787
1773
  this.useHelper("sliceGuard");
788
1774
  if (lhs.length === 1) return `__s(${iteree}).keys()`;
789
1775
  return `__s(${iteree}).entries()`;
790
1776
  },
791
1777
 
792
- _genIteratorLoop(itereeExpr, paramsStr, body) {
1778
+ _emitIterTargetDispatch(targetFlag) {
1779
+ const labels = this._functionLabels ?? [];
1780
+ this.line(`if (${targetFlag}) {`);
1781
+ this.indented(() => {
1782
+ for (const lbl of labels) {
1783
+ this.line(`if (${targetFlag} === "break$${lbl}") break ${lbl};`);
1784
+ this.line(`if (${targetFlag} === "continue$${lbl}") continue ${lbl};`);
1785
+ }
1786
+ if (this._inIteratorBody) {
1787
+ this.line(`${this._iterTargetFlag} = ${targetFlag}; return false;`);
1788
+ }
1789
+ });
1790
+ this.line("}");
1791
+ },
1792
+
1793
+ _genIteratorLoop(
1794
+ itereeExpr,
1795
+ paramsStr,
1796
+ body,
1797
+ assignPreamble = null,
1798
+ loopLabel = null,
1799
+ ) {
793
1800
  let iteree = this.genExpr(itereeExpr);
794
1801
  if (itereeExpr.kind === "FuncLit") {
795
1802
  iteree = `(${iteree})`;
@@ -799,38 +1806,61 @@ export const statementGenMethods = {
799
1806
  const breakFlag = `__broke${d}`;
800
1807
  const retFlag = `__returned${d}`;
801
1808
  const retVar = `__retVal${d}`;
1809
+ const targetFlag = `__target${d}`;
802
1810
 
803
1811
  this.line("{");
804
1812
  this.indented(() => {
805
1813
  this.line(`let ${breakFlag} = false;`);
806
1814
  this.line(`let ${retFlag} = false;`);
807
1815
  this.line(`let ${retVar};`);
1816
+ this.line(`let ${targetFlag} = null;`);
808
1817
 
809
1818
  this.line(`${iteree}(function(${paramsStr}) {`);
810
1819
  this.indented(() => {
811
- this.line(`if (${breakFlag}) return false;`);
1820
+ this.line(`if (${breakFlag} || ${retFlag} || ${targetFlag}) {`);
1821
+ this.indented(() => {
1822
+ if (this.strict) {
1823
+ this.line(
1824
+ `throw new Error("range function continued iteration after function for loop body returned false");`,
1825
+ );
1826
+ } else {
1827
+ this.line("return false;");
1828
+ }
1829
+ });
1830
+ this.line("}");
812
1831
  const prev = {
813
1832
  in: this._inIteratorBody,
814
1833
  break: this._iterBreakFlag,
815
1834
  ret: this._iterReturnFlag,
816
1835
  retVar: this._iterReturnVar,
1836
+ target: this._iterTargetFlag,
1837
+ loopLabel: this._currentIterLoopLabel,
1838
+ loopDepth: this._iterLoopDepth,
817
1839
  };
818
1840
  this._inIteratorBody = true;
819
1841
  this._iterBreakFlag = breakFlag;
820
1842
  this._iterReturnFlag = retFlag;
821
1843
  this._iterReturnVar = retVar;
1844
+ this._iterTargetFlag = targetFlag;
1845
+ this._currentIterLoopLabel = loopLabel;
1846
+ this._iterLoopDepth = 0;
822
1847
 
1848
+ if (assignPreamble) assignPreamble();
823
1849
  this.genBlock(body);
824
1850
 
825
1851
  this._inIteratorBody = prev.in;
826
1852
  this._iterBreakFlag = prev.break;
827
1853
  this._iterReturnFlag = prev.ret;
828
1854
  this._iterReturnVar = prev.retVar;
1855
+ this._iterTargetFlag = prev.target;
1856
+ this._currentIterLoopLabel = prev.loopLabel;
1857
+ this._iterLoopDepth = prev.loopDepth;
829
1858
 
830
1859
  this.line("return true;");
831
1860
  });
832
1861
  this.line("});");
833
1862
  this.line(`if (${retFlag}) return ${retVar};`);
1863
+ this._emitIterTargetDispatch(targetFlag);
834
1864
  });
835
1865
  this.line("}");
836
1866
  this._iterDepth--;
@@ -838,32 +1868,100 @@ export const statementGenMethods = {
838
1868
 
839
1869
  genIteratorFor(stmt) {
840
1870
  const range = stmt.init.rhs[0];
1871
+ const isAssign = stmt.init.kind === "AssignStmt";
841
1872
  const lhs = stmt.init.lhs.map((e) => e.name ?? this.genExpr(e));
1873
+ const yieldTypes = range._yieldParams ?? [];
1874
+ if (isAssign) {
1875
+ const cbParams = lhs.map((_, i) => `__p${i}`);
1876
+ const params = yieldTypes.length === 0 ? "" : cbParams.join(", ");
1877
+ const assignPreamble = () => {
1878
+ for (let i = 0; i < lhs.length; i++) {
1879
+ if (lhs[i] !== "_") {
1880
+ const yType = yieldTypes[i];
1881
+ const isStruct =
1882
+ this._isStructType(yType) ||
1883
+ this._isStructType(stmt.init.lhs[i]?._type);
1884
+ if (isStruct) {
1885
+ this.useHelper("sclone");
1886
+ this.line(`${lhs[i]} = __sclone(${cbParams[i]});`);
1887
+ } else {
1888
+ this.line(`${lhs[i]} = ${cbParams[i]};`);
1889
+ }
1890
+ }
1891
+ }
1892
+ };
1893
+ this._genIteratorLoop(
1894
+ range.expr,
1895
+ params,
1896
+ stmt.body,
1897
+ assignPreamble,
1898
+ stmt.label ?? null,
1899
+ );
1900
+ return;
1901
+ }
842
1902
  const cbParams = lhs.map((n, i) => (n === "_" ? `_$${i}` : n));
843
- const params = range._yieldParams.length === 0 ? "" : cbParams.join(", ");
844
- this._genIteratorLoop(range.expr, params, stmt.body);
1903
+ const params = yieldTypes.length === 0 ? "" : cbParams.join(", ");
1904
+ const clonePreamble = () => {
1905
+ for (let i = 0; i < lhs.length; i++) {
1906
+ if (lhs[i] !== "_") {
1907
+ const yType = yieldTypes[i];
1908
+ const isStruct =
1909
+ this._isStructType(yType) ||
1910
+ this._isStructType(stmt.init.lhs[i]?._type);
1911
+ if (isStruct) {
1912
+ this.useHelper("sclone");
1913
+ this.line(`${cbParams[i]} = __sclone(${cbParams[i]});`);
1914
+ }
1915
+ }
1916
+ }
1917
+ };
1918
+ this._genIteratorLoop(
1919
+ range.expr,
1920
+ params,
1921
+ stmt.body,
1922
+ clonePreamble,
1923
+ stmt.label ?? null,
1924
+ );
845
1925
  },
846
1926
 
847
1927
  // 0-param iterator: for range iter { body }
848
1928
  genIteratorForCond(stmt) {
849
- this._genIteratorLoop(stmt.cond.expr, "", stmt.body);
1929
+ this._genIteratorLoop(
1930
+ stmt.cond.expr,
1931
+ "",
1932
+ stmt.body,
1933
+ null,
1934
+ stmt.label ?? null,
1935
+ );
850
1936
  },
851
1937
 
852
1938
  // Inline a statement as a for-init/post string (no semicolon)
853
- stmtInline(stmt) {
1939
+ stmtInline(stmt, pre = null) {
854
1940
  switch (stmt.kind) {
855
- case "DefineStmt": {
856
- const rhs = stmt.rhs.map((e) => this.genExpr(e)).join(", ");
857
- const lhs = stmt.lhs.map((e) => e.name ?? this.genExpr(e)).join(", ");
858
- return `let ${lhs} = ${rhs}`;
859
- }
860
- case "AssignStmt": {
861
- const rhs = stmt.rhs.map((e) => this.genExpr(e)).join(", ");
862
- const lhs = stmt.lhs.map((e) => this.genExpr(e)).join(", ");
863
- return `${lhs} ${stmt.op} ${rhs}`;
864
- }
865
- case "IncDecStmt":
866
- return `${this.genExpr(stmt.expr)}${stmt.op}`;
1941
+ case "DefineStmt":
1942
+ return this._inlineDefine(stmt, pre);
1943
+ case "AssignStmt":
1944
+ return this._withInlineSyncs(
1945
+ this._resliceSyncs(stmt.lhs, stmt, true),
1946
+ this._inlineAssign(stmt),
1947
+ );
1948
+ case "IncDecStmt": {
1949
+ const syncs = this._resliceSyncs([stmt.expr], stmt, true);
1950
+ this._markIndexLvalues([stmt.expr]);
1951
+ this._markDerefOpTarget(stmt.expr);
1952
+ if (is64BitInt(stmt.expr._type)) {
1953
+ const l = this.genExpr(stmt.expr);
1954
+ const binOp = stmt.op === "++" ? "+" : "-";
1955
+ return this._withInlineSyncs(
1956
+ syncs,
1957
+ `${l} = ${this._gen64BitBinary(binOp, l, "1n", stmt.expr._type)}`,
1958
+ );
1959
+ }
1960
+ return this._withInlineSyncs(
1961
+ syncs,
1962
+ `${this.genExpr(stmt.expr)}${stmt.op}`,
1963
+ );
1964
+ }
867
1965
  case "ExprStmt":
868
1966
  return this.genExpr(stmt.expr);
869
1967
  default:
@@ -871,9 +1969,110 @@ export const statementGenMethods = {
871
1969
  }
872
1970
  },
873
1971
 
1972
+ _withInlineSyncs(syncs, js) {
1973
+ return syncs ? `${js}, ${syncs.join(", ")}` : js;
1974
+ },
1975
+
1976
+ _inlineDefine(stmt, pre = null) {
1977
+ const boxed =
1978
+ stmt.lhs.length === 1 &&
1979
+ stmt.rhs.length === 1 &&
1980
+ this._boxedVars.has(stmt.lhs[0].name);
1981
+ if (boxed)
1982
+ return `let ${stmt.lhs[0].name} = { value: ${pre ? pre[0] : this.genValueExpr(stmt.rhs[0])} }`;
1983
+ const rhs = pre ?? stmt.rhs.map((e) => this.genExpr(e));
1984
+ const lhs = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
1985
+ if (lhs.length === 1) return `let ${lhs[0]} = ${rhs[0]}`;
1986
+ // `for i, j := 0, 10`: one declarator per variable.
1987
+ if (rhs.length === lhs.length) {
1988
+ const decls = lhs.map((n, i) =>
1989
+ this._boxedVars.has(n)
1990
+ ? `${n} = { value: ${rhs[i]} }`
1991
+ : `${n} = ${rhs[i]}`,
1992
+ );
1993
+ return `let ${decls.join(", ")}`;
1994
+ }
1995
+ return `let [${lhs.map((n) => (n === "_" ? "" : n)).join(", ")}] = ${rhs[0]}`;
1996
+ },
1997
+
1998
+ // Compound assignments that need more than the JS operator: strict
1999
+ // numeric wrapping, 64-bit and float32 arithmetic, and integer `/=`.
2000
+ _needsStrictCompoundStmt(stmt, is64) {
2001
+ if (stmt.op === "=") return false;
2002
+ const t = stmt.lhs[0]._type;
2003
+ return (
2004
+ this.strict ||
2005
+ is64 ||
2006
+ this._isFloat32(t) ||
2007
+ (stmt.op === "/=" && this.isIntType(t))
2008
+ );
2009
+ },
2010
+
2011
+ // The single-expression form of `_genStrictCompound` for `for` post
2012
+ // statements: the emitted statement is lifted back out of the output.
2013
+ _inlineStrictCompound(stmt, rhsJs) {
2014
+ const mark = this.out.length;
2015
+ const handled = this._genStrictCompound(
2016
+ stmt.lhs[0],
2017
+ stmt.op.slice(0, -1),
2018
+ rhsJs,
2019
+ null,
2020
+ null,
2021
+ stmt.rhs[0]?._type,
2022
+ );
2023
+ const emitted = this.out.splice(mark).map((l) => l.trim());
2024
+ if (!handled) return null;
2025
+ const js = emitted.join(" ").replace(/;$/, "");
2026
+ return js.startsWith("{") ? `(() => ${js})()` : js;
2027
+ },
2028
+
2029
+ _inlineAssign(stmt) {
2030
+ this._markIndexLvalues(stmt.lhs);
2031
+ const rhs = stmt.rhs.map((e) => this.genExpr(e));
2032
+ if (stmt.lhs.length === 1) {
2033
+ if (stmt.op !== "=" && is64BitInt(stmt.lhs[0]._type)) {
2034
+ const binOp = stmt.op.slice(0, -1);
2035
+ let r = rhs[0];
2036
+ if (
2037
+ stmt.rhs[0]?._type?.kind === "untyped" &&
2038
+ !r.endsWith("n") &&
2039
+ /^-?\d+$/.test(r)
2040
+ ) {
2041
+ r = `${r}n`;
2042
+ }
2043
+ const l = this.genExpr(stmt.lhs[0]);
2044
+ return `${l} = ${this._gen64BitBinary(binOp, l, r, stmt.lhs[0]._type)}`;
2045
+ }
2046
+ if (this._needsStrictCompoundStmt(stmt, is64BitInt(stmt.lhs[0]._type))) {
2047
+ const strict = this._inlineStrictCompound(stmt, rhs[0]);
2048
+ if (strict) return strict;
2049
+ }
2050
+ if (stmt.op === "&^=")
2051
+ return `${this.genExpr(stmt.lhs[0])} &= ~(${rhs[0]})`;
2052
+ return `${this.genExpr(stmt.lhs[0])} ${stmt.op} ${rhs[0]}`;
2053
+ }
2054
+ // `i, j = i+1, j-1`: every right-hand side is evaluated first.
2055
+ const lhs = stmt.lhs.map((e) =>
2056
+ e.name === "_" ? "" : this._genAssignLhsExpr(e),
2057
+ );
2058
+ const r = rhs.length === 1 ? rhs[0] : `[${rhs.join(", ")}]`;
2059
+ return `[${lhs.join(", ")}] = ${r}`;
2060
+ },
2061
+
874
2062
  // Interface-typed tags may hold boxed named values; cases compare with __ifeq.
875
2063
  _switchTagAndCases(stmt) {
876
2064
  const tag = stmt.tag ? this.genExpr(stmt.tag) : "true";
2065
+ if (stmt.tag && this._isStructOrArrayType(stmt.tag._type)) {
2066
+ // Struct and array tags compare by value, not by reference.
2067
+ this.useHelper("equal");
2068
+ this._swCounter = (this._swCounter ?? 0) + 1;
2069
+ const tmp = `__sw${this._swCounter}`;
2070
+ this.line(`const ${tmp} = ${tag};`);
2071
+ return {
2072
+ tag: "true",
2073
+ caseExpr: (e) => `__equal(${tmp}, ${this.genExpr(e)})`,
2074
+ };
2075
+ }
877
2076
  if (!stmt.tag || !this._isIfaceType(stmt.tag._type))
878
2077
  return { tag, caseExpr: (e) => this.genExpr(e) };
879
2078
  this.useHelper("ifeq");
@@ -901,7 +2100,7 @@ export const statementGenMethods = {
901
2100
  // across sibling cases (JS switch shares one block scope).
902
2101
  this.line("{");
903
2102
  this.indented(() => {
904
- for (const s of c.stmts) this.genStmt(s);
2103
+ this.genBlock(c);
905
2104
  // Go doesn't fall through by default — add break unless last stmt is return/break
906
2105
  const last = c.stmts[c.stmts.length - 1];
907
2106
  if (
@@ -920,10 +2119,18 @@ export const statementGenMethods = {
920
2119
  if (stmt.init) {
921
2120
  this.line("{");
922
2121
  this.indented(() => {
923
- this.genStmt(stmt.init);
924
- genBody();
2122
+ this._genScopedInit(stmt.init, genBody);
925
2123
  });
926
2124
  this.line("}");
2125
+ } else if (
2126
+ stmt.tag &&
2127
+ (this._isStructOrArrayType(stmt.tag._type) ||
2128
+ this._isIfaceType(stmt.tag._type))
2129
+ ) {
2130
+ // The tag temporary is a `const`, which cannot directly follow a label.
2131
+ this.line("{");
2132
+ this.indented(genBody);
2133
+ this.line("}");
927
2134
  } else {
928
2135
  genBody();
929
2136
  }
@@ -934,7 +2141,13 @@ export const statementGenMethods = {
934
2141
  this.indented(() => {
935
2142
  const val = this.genExpr(stmt.expr);
936
2143
  this.line(`const __tsw = ${val};`);
937
- if (stmt.assign) this.line(`let ${stmt.assign} = __tsw;`);
2144
+ // The binding is declared per clause, after the switch expression
2145
+ // was evaluated (`switch x := any(x).(type)` reads the outer `x`).
2146
+ const boxedBind = stmt.assign && this._boxedVars.has(stmt.assign);
2147
+ const bind = (valueJs) =>
2148
+ this.line(
2149
+ `let ${stmt.assign} = ${boxedBind ? `{ value: ${valueJs} }` : valueJs};`,
2150
+ );
938
2151
 
939
2152
  let first = true;
940
2153
  let hasDefault = false;
@@ -949,13 +2162,16 @@ export const statementGenMethods = {
949
2162
  .join(" || ");
950
2163
  this.line(`${first ? "if" : "else if"} (${cond}) {`);
951
2164
  this.indented(() => {
952
- // A single named non-struct case binds the unboxed value.
953
- if (
954
- stmt.assign &&
955
- c.types.length === 1 &&
956
- this._isNamedBoxTypeNode(c.types[0])
957
- )
958
- this.line(`const ${stmt.assign} = __tsw.__gfUnbox();`);
2165
+ if (stmt.assign) {
2166
+ const only = c.types.length === 1 ? c.types[0] : null;
2167
+ // A single named non-struct case binds the unboxed value.
2168
+ if (only && this._assertedValueJs(only, "__tsw") !== "__tsw")
2169
+ bind(this._assertedValueJs(only, "__tsw"));
2170
+ // An array case binds a copy of the boxed value.
2171
+ else if (only && this._typeFromNode(only)?.kind === "array")
2172
+ bind(this._cloneJs(this._typeFromNode(only), "__tsw"));
2173
+ else bind("__tsw");
2174
+ }
959
2175
  for (const s of c.stmts) this.genStmt(s);
960
2176
  });
961
2177
  this.line("}");
@@ -964,6 +2180,7 @@ export const statementGenMethods = {
964
2180
  if (hasDefault) {
965
2181
  this.line(first ? "{" : "else {");
966
2182
  this.indented(() => {
2183
+ if (stmt.assign) bind("__tsw");
967
2184
  for (const s of hasDefault.stmts) this.genStmt(s);
968
2185
  });
969
2186
  this.line("}");