gofront 1.6.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +45 -49
  2. package/package.json +12 -8
  3. package/src/backend/js/expressions.js +1627 -148
  4. package/src/backend/js/fmt-runtime.js +573 -0
  5. package/src/backend/js/index.js +508 -102
  6. package/src/backend/js/runtime.js +1997 -106
  7. package/src/backend/js/statements.js +931 -87
  8. package/src/backend/js/stdlib/bits.js +87 -14
  9. package/src/backend/js/stdlib/builder.js +41 -6
  10. package/src/backend/js/stdlib/bytes.js +253 -7
  11. package/src/backend/js/stdlib/errors.js +7 -1
  12. package/src/backend/js/stdlib/fmt.js +349 -10
  13. package/src/backend/js/stdlib/index.js +19 -2
  14. package/src/backend/js/stdlib/json.js +1167 -0
  15. package/src/backend/js/stdlib/maps.js +0 -8
  16. package/src/backend/js/stdlib/math.js +17 -1
  17. package/src/backend/js/stdlib/simd.js +34 -0
  18. package/src/backend/js/stdlib/slices.js +36 -6
  19. package/src/backend/js/stdlib/strconv.js +10 -5
  20. package/src/backend/js/stdlib/strings.js +83 -4
  21. package/src/backend/js/stdlib/testing.js +7 -0
  22. package/src/backend/js/stdlib/time.js +79 -55
  23. package/src/backend/js/stdlib/unicode.js +1 -1
  24. package/src/backend/js/stdlib/utf8.js +1 -1
  25. package/src/backend/js/templ.js +38 -76
  26. package/src/backend/js/type-info.js +272 -0
  27. package/src/backend/shared/fmt-types.js +188 -0
  28. package/src/backend/wasm/boundary.js +446 -72
  29. package/src/backend/wasm/emit-builtins.js +320 -13
  30. package/src/backend/wasm/emit-exprs.js +1404 -199
  31. package/src/backend/wasm/emit-maps.js +61 -13
  32. package/src/backend/wasm/emit-stmts.js +1359 -164
  33. package/src/backend/wasm/emit.js +518 -74
  34. package/src/backend/wasm/encode.js +128 -13
  35. package/src/backend/wasm/globals.js +452 -0
  36. package/src/backend/wasm/index.js +194 -2489
  37. package/src/backend/wasm/method-expr.js +381 -0
  38. package/src/backend/wasm/module-emitter.js +2240 -0
  39. package/src/backend/wasm/monomorph.js +62 -4
  40. package/src/backend/wasm/optimize.js +4 -2
  41. package/src/backend/wasm/prescan.js +397 -0
  42. package/src/backend/wasm/stdlib/bytes.js +1804 -127
  43. package/src/backend/wasm/stdlib/core.js +1574 -199
  44. package/src/backend/wasm/stdlib/index.js +3 -5
  45. package/src/backend/wasm/stdlib/io.js +1567 -150
  46. package/src/backend/wasm/stdlib/packages.js +2 -0
  47. package/src/backend/wasm/stdlib/simd.js +499 -0
  48. package/src/backend/wasm/stdlib/time.js +1041 -12
  49. package/src/backend/wasm/types.js +221 -15
  50. package/src/backend/wasm/variadic.js +93 -0
  51. package/src/backend/wasm/wat.js +84 -20
  52. package/src/{cli-core.js → cli/cli-core.js} +236 -10
  53. package/src/{test-runner.js → cli/test-runner.js} +332 -39
  54. package/src/{vendor.js → cli/vendor.js} +89 -15
  55. package/src/compiler.js +107 -269
  56. package/src/dts-parser.js +71 -14
  57. package/src/index.js +15 -169
  58. package/src/{lexer.js → lexer/lexer.js} +102 -55
  59. package/src/{tokens.js → lexer/tokens.js} +1 -0
  60. package/src/link/sourcemap.js +97 -0
  61. package/src/link/stdlib.js +515 -0
  62. package/src/link/wasm.js +294 -0
  63. package/src/lower/bounds.js +394 -0
  64. package/src/lower/boxing.js +1 -6
  65. package/src/lower/captures.js +176 -32
  66. package/src/lower/embedding.js +2 -1
  67. package/src/lower/index.js +26 -18
  68. package/src/lower/json-codecs.js +1398 -0
  69. package/src/lower/ownership.js +22 -1
  70. package/src/lower/range.js +86 -1
  71. package/src/lower/reachability.js +132 -0
  72. package/src/parser/declarations.js +58 -13
  73. package/src/parser/expressions.js +29 -7
  74. package/src/parser/index.js +2 -1
  75. package/src/parser/statements.js +37 -22
  76. package/src/parser/struct-tag.js +144 -0
  77. package/src/{templ-parser.js → parser/templ.js} +138 -15
  78. package/src/parser/types.js +130 -13
  79. package/src/resolver.js +22 -1
  80. package/src/typechecker/assignability.js +215 -11
  81. package/src/typechecker/const-fold.js +356 -0
  82. package/src/typechecker/constants.js +256 -0
  83. package/src/typechecker/expressions.js +960 -99
  84. package/src/typechecker/index.js +491 -72
  85. package/src/typechecker/init-order.js +330 -0
  86. package/src/typechecker/resolve.js +596 -52
  87. package/src/typechecker/statements.js +157 -17
  88. package/src/typechecker/stdlib/core.js +333 -55
  89. package/src/typechecker/stdlib/excluded.js +898 -0
  90. package/src/typechecker/stdlib/extended.js +359 -26
  91. package/src/typechecker/{stdlib.js → stdlib/index.js} +6 -4
  92. package/src/typechecker/stdlib/simd.js +123 -0
  93. package/src/typechecker/termination.js +1 -1
  94. package/src/typechecker/types.js +165 -26
  95. package/stdlib/bytes/iter.go +192 -0
  96. package/stdlib/bytes/search.go +240 -0
  97. package/stdlib/bytes/split.go +218 -0
  98. package/stdlib/cmp/cmp.go +65 -0
  99. package/stdlib/encoding/json/errors.go +31 -0
  100. package/stdlib/encoding/json/indent.go +71 -0
  101. package/stdlib/encoding/json/number.go +12 -0
  102. package/stdlib/encoding/json/stream.go +173 -0
  103. package/stdlib/errors/overlay.go +12 -0
  104. package/stdlib/hash/hash.go +53 -0
  105. package/stdlib/internal/jsonrt/dec.go +738 -0
  106. package/stdlib/internal/jsonrt/enc.go +366 -0
  107. package/stdlib/internal/jsonrt/indent.go +135 -0
  108. package/stdlib/internal/jsonrt/number.go +27 -0
  109. package/stdlib/internal/jsonrt/reader.go +73 -0
  110. package/stdlib/internal/jsonrt/scanner.go +649 -0
  111. package/stdlib/io/io.go +346 -0
  112. package/stdlib/iter/iter.go +18 -0
  113. package/stdlib/maps/iter.go +65 -0
  114. package/stdlib/math/extra.go +270 -0
  115. package/stdlib/slices/extra.go +73 -0
  116. package/stdlib/slices/iter.go +113 -0
  117. package/stdlib/sort/sort.go +561 -0
  118. package/stdlib/strconv/quote.go +341 -0
  119. package/stdlib/strings/extra.go +35 -0
  120. package/stdlib/strings/iter.go +122 -0
  121. package/stdlib/sync/extra.go +105 -0
  122. package/stdlib/unicode/unicode.go +112 -0
  123. package/stdlib/unicode/utf8/bytes.go +239 -0
  124. package/src/backend/wasm/const-fold.js +0 -205
  125. package/src/backend/wasm/emit-stdlib.js +0 -13
  126. /package/src/{asset-manager.js → cli/asset-manager.js} +0 -0
  127. /package/src/{colors.js → cli/colors.js} +0 -0
  128. /package/src/{dev-server.js → cli/dev-server.js} +0 -0
  129. /package/src/{minifier.js → cli/minifier.js} +0 -0
  130. /package/src/{project-config.js → cli/project-config.js} +0 -0
  131. /package/src/{pwa.js → cli/pwa.js} +0 -0
  132. /package/src/{templ-lexer.js → lexer/templ-lexer.js} +0 -0
@@ -1,16 +1,129 @@
1
1
  import {
2
2
  bodyRedeclares,
3
+ forEachChild,
3
4
  isIntRangeType,
4
5
  isMapRangeType,
5
6
  isRangeFor,
6
7
  isSimpleIntRange,
7
8
  isStringRangeType,
9
+ loopVarsCapturedInLoop,
8
10
  nodeAssigns,
11
+ someChild,
9
12
  } from "../../lower/index.js";
10
- import { isComplex } from "../../typechecker/types.js";
13
+ import { isComplex, isTypedArraySlice } from "../../typechecker/types.js";
11
14
 
12
15
  /** @typedef {import('./index.js').CodeGen} CodeGen */
13
16
 
17
+ // The same variable or field chain (`x`, `n.Children`) on both sides.
18
+ const sameChain = (a, b) =>
19
+ a.kind === b.kind &&
20
+ (a.kind === "Ident"
21
+ ? a.name === b.name && a.name !== "_"
22
+ : a.kind === "SelectorExpr" &&
23
+ a.field === b.field &&
24
+ sameChain(a.expr, b.expr));
25
+
26
+ // `x = x[lo:hi]` (no max) with the same chain on both sides.
27
+ const isSelfResliceShape = (stmt) =>
28
+ stmt.op === "=" &&
29
+ stmt.lhs.length === 1 &&
30
+ stmt.rhs.length === 1 &&
31
+ stmt.rhs[0].kind === "SliceExpr" &&
32
+ !stmt.rhs[0].max &&
33
+ sameChain(stmt.lhs[0], stmt.rhs[0].expr);
34
+
35
+ // Pure expressions (the slice chain of a `__rcs` sync) that read the same
36
+ // value after the write as before it: literals, variables and fields not
37
+ // assigned by the statement, `len` and `cap`, and arithmetic over those.
38
+ const isStableIndex = (e, assigned) => {
39
+ switch (e?.kind) {
40
+ case "BasicLit":
41
+ return true;
42
+ case "Ident":
43
+ return !assigned.has(e.name);
44
+ case "SelectorExpr":
45
+ return !e._isMethodValue && isStableIndex(e.expr, assigned);
46
+ case "ParenExpr":
47
+ return isStableIndex(e.expr, assigned);
48
+ case "UnaryExpr":
49
+ return e.op !== "<-" && isStableIndex(e.operand, assigned);
50
+ case "BinaryExpr":
51
+ return (
52
+ isStableIndex(e.left, assigned) && isStableIndex(e.right, assigned)
53
+ );
54
+ case "CallExpr":
55
+ return (
56
+ e.func?.kind === "Ident" &&
57
+ (e.func.name === "len" || e.func.name === "cap") &&
58
+ e.args.every((a) => isStableIndex(a, assigned))
59
+ );
60
+ default:
61
+ return false;
62
+ }
63
+ };
64
+
65
+ const isLenCap = (e) =>
66
+ e.kind === "CallExpr" &&
67
+ e.func?.kind === "Ident" &&
68
+ (e.func.name === "len" || e.func.name === "cap");
69
+
70
+ // A call (other than `len`/`cap`), receive or await in `n` (not inside a
71
+ // function literal): it may change variables and fields.
72
+ const hasCall = (n) => {
73
+ if (Array.isArray(n)) return n.some(hasCall);
74
+ if (!n || typeof n !== "object" || typeof n.kind !== "string") return false;
75
+ if (n.kind === "FuncLit") return false;
76
+ if (n.kind === "AwaitExpr" || (n.kind === "CallExpr" && !isLenCap(n)))
77
+ return true;
78
+ if (n.kind === "UnaryExpr" && n.op === "<-") return true;
79
+ return someChild(n, hasCall);
80
+ };
81
+
82
+ // A call in a left-hand index or on the right-hand side of the statement.
83
+ const stmtHasCall = (lhs, stmt) =>
84
+ lhs.some((e) => hasCall(e.index)) ||
85
+ (stmt.kind === "AssignStmt" && hasCall(stmt.rhs));
86
+
87
+ // A multi-assign with a field or `*p` left-hand side: it may reassign the
88
+ // slice chain of an index write, whose sync must then use the old slice.
89
+ const reassignsChains = (lhs, stmt) =>
90
+ stmt.kind === "AssignStmt" &&
91
+ lhs.length > 1 &&
92
+ lhs.some(
93
+ (e) =>
94
+ e.kind === "SelectorExpr" || (e.kind === "UnaryExpr" && e.op === "*"),
95
+ );
96
+
97
+ // Index of a `__rcs` sync that can be evaluated again after the write:
98
+ // constants, and, when `vars` (a single-LHS statement without calls), unboxed
99
+ // variables, with `len`/`cap` and arithmetic over those. Any other index
100
+ // is bound to a temporary first, so the write and the sync use one value.
101
+ const isReusableIndex = (e, vars, boxed) => {
102
+ if (e?._const || e?.kind === "BasicLit") return true;
103
+ switch (e?.kind) {
104
+ case "Ident":
105
+ return vars && !boxed.has(e.name);
106
+ case "ParenExpr":
107
+ return isReusableIndex(e.expr, vars, boxed);
108
+ case "UnaryExpr":
109
+ return (
110
+ (e.op === "-" || e.op === "+" || e.op === "^") &&
111
+ isReusableIndex(e.operand, vars, boxed)
112
+ );
113
+ case "BinaryExpr":
114
+ return (
115
+ isReusableIndex(e.left, vars, boxed) &&
116
+ isReusableIndex(e.right, vars, boxed)
117
+ );
118
+ case "CallExpr":
119
+ return (
120
+ isLenCap(e) && e.args.every((a) => isReusableIndex(a, vars, boxed))
121
+ );
122
+ default:
123
+ return false;
124
+ }
125
+ };
126
+
14
127
  // Method-name dispatch for genStmt — string values add no static call edges.
15
128
  const STMT_GEN_DELEGATE = {
16
129
  VarDecl: "genVarDecl",
@@ -27,7 +140,53 @@ const STMT_GEN_DELEGATE = {
27
140
  /** @type {ThisType<CodeGen>} */
28
141
  export const statementGenMethods = {
29
142
  genBlock(block) {
30
- for (const stmt of block.stmts) this.genStmt(stmt);
143
+ let nested = 0;
144
+ for (const stmt of block.stmts) {
145
+ if (this._definesShadowedRead(stmt)) {
146
+ this._genShadowingDefine(stmt);
147
+ nested++;
148
+ } else {
149
+ this.genStmt(stmt);
150
+ }
151
+ }
152
+ for (; nested > 0; nested--) {
153
+ this.indent--;
154
+ this.line("}");
155
+ }
156
+ },
157
+
158
+ // `x := f(x)` declaring a new `x` that its own right-hand side reads (the
159
+ // outer `x`): JS `let` would put the read in its temporal dead zone.
160
+ _definesShadowedRead(stmt) {
161
+ if (stmt.kind !== "DefineStmt" || this._isCommaOkMapExpr(stmt, stmt.rhs[0]))
162
+ return false;
163
+ const names = new Set(
164
+ stmt.lhs.filter((l) => !l._redecl && l.name !== "_").map((l) => l.name),
165
+ );
166
+ if (names.size === 0) return false;
167
+ const reads = (n) => {
168
+ if (Array.isArray(n)) return n.some(reads);
169
+ if (!n || typeof n !== "object") return false;
170
+ if (n.kind === "Ident" && names.has(n.name)) return true;
171
+ return someChild(n, reads);
172
+ };
173
+ return stmt.rhs.some(reads);
174
+ },
175
+
176
+ // Evaluates the right-hand side in the enclosing scope, then opens a nested
177
+ // block (closed at the end of the enclosing block) that declares the names.
178
+ _genShadowingDefine(stmt) {
179
+ const rhs = this._defineRhs(stmt);
180
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
181
+ const tmp = `__t${this._tmpCounter}`;
182
+ const all = rhs.length === 1 ? rhs[0] : `[${rhs.join(", ")}]`;
183
+ this.line(`const ${tmp} = ${all};`);
184
+ this.line("{");
185
+ this.indent++;
186
+ this._genDefineStmt(
187
+ stmt,
188
+ rhs.length === 1 ? [tmp] : rhs.map((_, i) => `${tmp}[${i}]`),
189
+ );
31
190
  },
32
191
 
33
192
  // Shared helper for comma-ok map index in both DefineStmt and AssignStmt.
@@ -36,9 +195,15 @@ export const statementGenMethods = {
36
195
  const keyExpr = this.genExpr(rhsNode.index);
37
196
  const [vName, okName] = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
38
197
  const decl = isDefine ? "let " : "";
198
+ // An array map value is a copy, not the stored array.
199
+ const valType = rhsNode._mapValueType;
200
+ const copyVal = (js) =>
201
+ this._aggregateBase(valType)?.kind === "array"
202
+ ? this._cloneJs(valType, js)
203
+ : js;
39
204
  if (isDefine && !this.strict) {
40
205
  if (vName !== "_") {
41
- this.line(`let ${vName} = ${mapExpr}[${keyExpr}];`);
206
+ this.line(`let ${vName} = ${copyVal(`${mapExpr}[${keyExpr}]`)};`);
42
207
  }
43
208
  if (okName !== "_") {
44
209
  this.line(`let ${okName} = (${keyExpr}) in ${mapExpr};`);
@@ -54,7 +219,7 @@ export const statementGenMethods = {
54
219
  this.line(`const ${mTmp} = ${mapExpr}, ${kTmp} = ${keyExpr};`);
55
220
  if (vName !== "_") {
56
221
  this.line(
57
- `${decl}${vName} = (${mTmp} && (${kTmp} in ${mTmp})) ? ${mTmp}[${kTmp}] : ${zero};`,
222
+ `${decl}${vName} = (${mTmp} && (${kTmp} in ${mTmp})) ? ${copyVal(`${mTmp}[${kTmp}]`)} : ${zero};`,
58
223
  );
59
224
  }
60
225
  if (okName !== "_") {
@@ -66,11 +231,21 @@ export const statementGenMethods = {
66
231
  // Record source mapping for the first line this statement produces
67
232
  const srcLine = stmt._line ?? null;
68
233
  const _line0 = this.out.length;
234
+ const restore = [];
235
+ const syncs =
236
+ stmt.kind === "AssignStmt"
237
+ ? this._resliceSyncs(stmt.lhs, stmt, false, restore)
238
+ : stmt.kind === "IncDecStmt"
239
+ ? this._resliceSyncs([stmt.expr], stmt, false, restore)
240
+ : null;
69
241
  switch (stmt.kind) {
70
242
  case "TypeDecl":
71
243
  this.genTypeDeclWithMethods(stmt, []);
244
+ this._noteLocalTypeDecl(stmt);
72
245
  break;
73
246
  case "IncDecStmt": {
247
+ this._markIndexLvalues([stmt.expr]);
248
+ this._markDerefOpTarget(stmt.expr);
74
249
  if (stmt.expr.kind === "IndexExpr" && stmt.expr._mapValueType) {
75
250
  const zero = this.zeroValueForType(stmt.expr._mapValueType);
76
251
  const wrapField = this._namedWrapperField(
@@ -87,12 +262,12 @@ export const statementGenMethods = {
87
262
  this._hasCallExpr(stmt.expr.index)
88
263
  ) {
89
264
  this.line(
90
- `((__m, __k) => { __m[__k] = (__m[__k] ?? ${zero}) ${op} 1; })(${base}, ${idx});`,
265
+ `((__m, __k) => { ${this._genMapWriteBase("__m")}[__k] = (__m[__k] ?? ${zero}) ${op} 1; })(${base}, ${idx});`,
91
266
  srcLine,
92
267
  );
93
268
  } else {
94
269
  this.line(
95
- `${base}[${idx}] = (${base}[${idx}] ?? ${zero}) ${op} 1;`,
270
+ `${this._genMapWriteBase(base)}[${idx}] = (${base}[${idx}] ?? ${zero}) ${op} 1;`,
96
271
  srcLine,
97
272
  );
98
273
  }
@@ -118,8 +293,14 @@ export const statementGenMethods = {
118
293
  if (stmt.init) {
119
294
  this.line("{");
120
295
  this.indented(() => {
121
- this.genStmt(stmt.init);
296
+ const shadowing = this._definesShadowedRead(stmt.init);
297
+ if (shadowing) this._genShadowingDefine(stmt.init);
298
+ else this.genStmt(stmt.init);
122
299
  this._genIf(stmt);
300
+ if (shadowing) {
301
+ this.indent--;
302
+ this.line("}");
303
+ }
123
304
  });
124
305
  this.line("}");
125
306
  } else {
@@ -130,6 +311,9 @@ export const statementGenMethods = {
130
311
  this.line(`__defers.push(() => { ${this.genExpr(stmt.call)}; });`);
131
312
  break;
132
313
  case "LabeledStmt":
314
+ if (stmt.body && stmt.body.kind === "ForStmt") {
315
+ stmt.body.label = stmt.label;
316
+ }
133
317
  this.line(`${stmt.label}:`);
134
318
  this.genStmt(stmt.body);
135
319
  break;
@@ -147,6 +331,9 @@ export const statementGenMethods = {
147
331
  throw new Error(`CodeGen: unhandled statement kind '${stmt.kind}'`);
148
332
  }
149
333
  }
334
+ this._emitResliceSyncs(syncs);
335
+ // The AST is shared with the WASM backend: undo the temporaries.
336
+ for (const [node, key, v] of restore) node[key] = v;
150
337
  // Record source mapping: first output line this statement produced
151
338
  if (srcLine != null && this.out.length > _line0) {
152
339
  this._srcMappings.push({
@@ -158,11 +345,14 @@ export const statementGenMethods = {
158
345
  },
159
346
 
160
347
  _isCommaOkMapExpr(stmt, rhsNode) {
348
+ if (stmt._commaOkMap) return true;
349
+ const t = rhsNode?.expr?._type;
350
+ const underlying = t?.kind === "named" ? t.underlying : t;
161
351
  return (
162
352
  stmt.rhs.length === 1 &&
163
353
  stmt.lhs.length === 2 &&
164
354
  rhsNode?.kind === "IndexExpr" &&
165
- rhsNode.expr?._type?.kind === "map"
355
+ underlying?.kind === "map"
166
356
  );
167
357
  },
168
358
 
@@ -178,40 +368,43 @@ export const statementGenMethods = {
178
368
  },
179
369
 
180
370
  _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
- const prefix = redecls[i] ? "" : "let ";
188
- this.line(`${prefix}${lhs[i]} = ${tmp}[${i}];`);
189
- }
190
- } else {
191
- for (let i = 0; i < lhs.length; i++) {
192
- if (lhs[i] === "_") {
193
- this.line(`${rhs[i]};`);
194
- continue;
195
- }
196
- const prefix = redecls[i] ? "" : "let ";
197
- this.line(`${prefix}${lhs[i]} = ${rhs[i]};`);
198
- }
371
+ // `x, y := 2, x` reads the old `x`: evaluate every value first.
372
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
373
+ const tmp = `__t${this._tmpCounter}`;
374
+ const all = rhs.length === 1 ? rhs[0] : `[${rhs.join(", ")}]`;
375
+ this.line(`const ${tmp} = ${all};`);
376
+ for (let i = 0; i < lhs.length; i++) {
377
+ if (lhs[i] === "_") continue;
378
+ this._genDefineSingleVar([lhs[i]], [`${tmp}[${i}]`], [redecls[i]]);
199
379
  }
200
380
  },
201
381
 
202
- _genDefineStmt(stmt) {
203
- const rhsNode = stmt.rhs[0];
204
- if (this._isCommaOkMapExpr(stmt, rhsNode)) {
205
- this._genCommaOkMapIndex(stmt, rhsNode, true);
206
- return;
382
+ // Address-taken variables introduced by a destructuring declaration are
383
+ // re-bound to their { value } box.
384
+ _boxDeclaredNames(names) {
385
+ for (const n of names) {
386
+ if (n && n !== "_" && this._boxedVars.has(n))
387
+ this.line(`${n} = { value: ${n} };`);
207
388
  }
389
+ },
390
+
391
+ _defineRhs(stmt) {
208
392
  const paired = stmt.rhs.length === stmt.lhs.length;
209
- const rhs = stmt.rhs.map((e, i) => {
393
+ return stmt.rhs.map((e, i) => {
210
394
  const l = paired ? stmt.lhs[i] : null;
211
395
  if (!l || l.name === "_") return this.genExpr(e);
212
396
  if (l._redecl) return this.genValueExpr(e);
213
397
  return this._genDeclValue(l.name, e);
214
398
  });
399
+ },
400
+
401
+ _genDefineStmt(stmt, preEvaluated = null) {
402
+ const rhsNode = stmt.rhs[0];
403
+ if (this._isCommaOkMapExpr(stmt, rhsNode)) {
404
+ this._genCommaOkMapIndex(stmt, rhsNode, true);
405
+ return;
406
+ }
407
+ const rhs = preEvaluated ?? this._defineRhs(stmt);
215
408
  const lhs = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
216
409
  const redecls = stmt.lhs.map((e) => !!e._redecl);
217
410
  const anyRedecl = redecls.some((r) => r);
@@ -221,6 +414,7 @@ export const statementGenMethods = {
221
414
  const rhsStr = rhs.length === 1 ? rhs[0] : `[${rhs.join(", ")}]`;
222
415
  const names = lhs.map((n) => (n === "_" ? "" : n));
223
416
  this.line(`let [${names.join(", ")}] = ${rhsStr};`);
417
+ this._boxDeclaredNames(names);
224
418
  } else {
225
419
  this._genDefineMixedRedecl(lhs, rhs, redecls);
226
420
  }
@@ -241,6 +435,68 @@ export const statementGenMethods = {
241
435
  );
242
436
  },
243
437
 
438
+ // `*p = v` where `p` points at a type parameter: erased, so the pointer is
439
+ // either a struct (copy into it) or a get/set wrapper.
440
+ _isTypeParamDerefAssign(stmt) {
441
+ if (stmt.lhs.length !== 1 || stmt.rhs.length !== 1 || stmt.op !== "=")
442
+ return false;
443
+ const l = stmt.lhs[0];
444
+ return (
445
+ l.kind === "UnaryExpr" &&
446
+ l.op === "*" &&
447
+ l.operand?._type?.base?.kind === "typeParam"
448
+ );
449
+ },
450
+
451
+ _isTypeParamDerefTarget(l) {
452
+ return (
453
+ l.kind === "UnaryExpr" &&
454
+ l.op === "*" &&
455
+ l.operand?._type?.base?.kind === "typeParam"
456
+ );
457
+ },
458
+
459
+ // `*a, *b = *b, *a` with type-parameter pointers: `*p` is not a settable
460
+ // JS destructuring target, so evaluate the right-hand side into temporaries
461
+ // and assign each target on its own.
462
+ _hasTypeParamDerefTarget(stmt) {
463
+ return (
464
+ stmt.op === "=" &&
465
+ stmt.lhs.length > 1 &&
466
+ stmt.lhs.some((l) => this._isTypeParamDerefTarget(l))
467
+ );
468
+ },
469
+
470
+ _genTypeParamDerefMultiAssign(stmt) {
471
+ const n = stmt.lhs.length;
472
+ const names = stmt.lhs.map((_, i) => `__t${i}`);
473
+ this.line("{");
474
+ this.indented(() => {
475
+ if (stmt.rhs.length === n) {
476
+ const vals = stmt.rhs.map((e, i) =>
477
+ stmt.lhs[i].name !== "_" ? this.genValueExpr(e) : this.genExpr(e),
478
+ );
479
+ this.line(`const [${names.join(", ")}] = [${vals.join(", ")}];`);
480
+ } else {
481
+ this.line(
482
+ `const [${names.join(", ")}] = ${this.genExpr(stmt.rhs[0])};`,
483
+ );
484
+ }
485
+ stmt.lhs.forEach((l, i) => {
486
+ if (l.name === "_") return;
487
+ if (this._isTypeParamDerefTarget(l)) {
488
+ const isStruct = this._typeParamIsStructJs(l.operand._type.base.name);
489
+ this.line(
490
+ `((__p, __v) => { if (${isStruct}) Object.assign(__p, __v); else __p.value = __v; })(${this.genExpr(l.operand)}, ${names[i]});`,
491
+ );
492
+ } else {
493
+ this.line(`${this._genAssignLhsExpr(l)} = ${names[i]};`);
494
+ }
495
+ });
496
+ });
497
+ this.line("}");
498
+ },
499
+
244
500
  _isStructDerefAssign(stmt) {
245
501
  if (stmt.lhs.length !== 1 || stmt.rhs.length !== 1 || stmt.op !== "=")
246
502
  return false;
@@ -262,6 +518,13 @@ export const statementGenMethods = {
262
518
  for (const e of exprs) if (e.kind === "IndexExpr") e._lvalue = true;
263
519
  },
264
520
 
521
+ // Op-assign / inc-dec through a type-parameter pointer needs a settable
522
+ // target: the erased pointer is a get/set wrapper (struct instantiations
523
+ // cannot be op-assigned), so target `.value` directly.
524
+ _markDerefOpTarget(e) {
525
+ if (this._isTypeParamDerefTarget(e)) e._derefOpTarget = true;
526
+ },
527
+
265
528
  _genAssignMulti(stmt, lhs, rhs, active, pairs) {
266
529
  const rhsStr = rhs.length === 1 ? rhs[0] : `[${rhs.join(", ")}]`;
267
530
  if (active.length === pairs.length) {
@@ -271,7 +534,132 @@ export const statementGenMethods = {
271
534
  }
272
535
  },
273
536
 
537
+ // `x = x[lo:hi]` with the same variable or field chain on both sides:
538
+ // the old value of `x` is replaced, so `_genSliceExpr` may copy instead
539
+ // of building a slice view (`__reslice`).
540
+ _markSelfReslice(stmt) {
541
+ if (stmt.op !== "=" || stmt.lhs.length !== 1 || stmt.rhs.length !== 1)
542
+ return;
543
+ const r = stmt.rhs[0];
544
+ if (r.kind !== "SliceExpr" || r.max) return;
545
+ // A parameter or named result may alias the caller's slice: keep the
546
+ // view. Field chains (`n.Children = n.Children[:k]`) still copy.
547
+ const lhs = stmt.lhs[0];
548
+ if (lhs.kind === "Ident" && this._fnCtx?.params.has(lhs.name)) return;
549
+ if (sameChain(stmt.lhs[0], r.expr)) r._selfReslice = true;
550
+ },
551
+
552
+ // Names of the variables and fields (`.F`) that some `x = x[lo:hi]` in the
553
+ // program may turn into a `__reslice` copy (`_resliceSyncs`).
554
+ _collectResliceNames(program) {
555
+ const names = new Set();
556
+ const visit = (n) => {
557
+ if (Array.isArray(n)) {
558
+ for (const c of n) visit(c);
559
+ return;
560
+ }
561
+ if (!n || typeof n !== "object" || typeof n.kind !== "string") return;
562
+ if (n.kind === "AssignStmt" && isSelfResliceShape(n)) {
563
+ const l = n.lhs[0];
564
+ names.add(l.kind === "Ident" ? l.name : `.${l.field}`);
565
+ }
566
+ forEachChild(n, visit);
567
+ };
568
+ visit(program.decls);
569
+ this._resliceNames = names;
570
+ },
571
+
572
+ // Index writes in `lhs` whose slice may be a `__reslice` copy: each is
573
+ // followed by `__rcs(x, i)` (`_emitResliceSyncs`), which writes the element
574
+ // through to the copy's old array, so writes through the copy and through
575
+ // other slices of that array keep Go's order. An index that the statement
576
+ // could change (`isReusableIndex`) is first bound to a temporary (not in
577
+ // `inline` for-clause statements, which skip the sync: the write-back on
578
+ // extension still applies). Not covered: writes through other references to the copy.
579
+ // Once one index is bound, every left-hand index with a call is bound too,
580
+ // in source order and each after its operand, and so is the slice operand
581
+ // of a synced write in a statement with a call or one that may reassign
582
+ // the chain through a field or `*p` (`reassignsChains`; inline statements
583
+ // skip that sync) — Go evaluates both before the right-hand side.
584
+ // `restore` collects the replaced nodes for the caller to put back.
585
+ _resliceSyncs(lhs, stmt, inline = false, restore = []) {
586
+ if (!this._resliceNames?.size) return null;
587
+ const targets = this._resliceTargets(lhs, stmt);
588
+ if (targets.size === 0) return null;
589
+ const called = stmtHasCall(lhs, stmt);
590
+ const moved = reassignsChains(lhs, stmt);
591
+ const vars = lhs.length === 1 && !called;
592
+ const reusable = (e) => isReusableIndex(e.index, vars, this._boxedVars);
593
+ const out = [...targets].filter((e) => !inline || (!moved && reusable(e)));
594
+ const operands = called || moved;
595
+ if (!inline && (operands || out.some((e) => !reusable(e))))
596
+ this._bindResliceOperands(lhs, targets, operands, reusable, restore);
597
+ if (out.length === 0) return null;
598
+ this.useHelper("slice");
599
+ return out.map((e) => {
600
+ const wrapField = this._namedWrapperField(e.expr._type, e.expr);
601
+ const base = wrapField
602
+ ? `${this.genExpr(e.expr)}.${wrapField}`
603
+ : this.genExpr(e.expr);
604
+ return `__rcs(${base}, ${this.genExpr(e.index)})`;
605
+ });
606
+ },
607
+
608
+ // The `_isResliceSyncTarget` writes in `lhs`.
609
+ _resliceTargets(lhs, stmt) {
610
+ const assigned = new Set();
611
+ if (stmt.kind === "AssignStmt" && stmt.lhs.length > 1)
612
+ for (const e of stmt.lhs) if (e.kind === "Ident") assigned.add(e.name);
613
+ return new Set(lhs.filter((e) => this._isResliceSyncTarget(e, assigned)));
614
+ },
615
+
616
+ // Binds the left-hand operands that `_resliceSyncs` must not evaluate
617
+ // twice to temporaries, in source order: an index after its operand, and
618
+ // the operand of every synced write when `operands`.
619
+ _bindResliceOperands(lhs, targets, operands, reusable, restore) {
620
+ const bind = (e, key) => {
621
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
622
+ const tmp = `__rci${this._tmpCounter}`;
623
+ this.line(`const ${tmp} = ${this.genExpr(e[key])};`);
624
+ restore.push([e, key, e[key]]);
625
+ e[key] = { kind: "Ident", name: tmp, _type: e[key]._type };
626
+ };
627
+ for (const e of lhs) {
628
+ if (e.kind !== "IndexExpr") continue;
629
+ const t = targets.has(e);
630
+ const index = (t && !reusable(e)) || hasCall(e.index);
631
+ if ((t && operands) || index) bind(e, "expr");
632
+ if (index) bind(e, "index");
633
+ }
634
+ },
635
+
636
+ // `x[i]` on a plain slice held by a chain in `_resliceNames` whose root
637
+ // variable the statement does not reassign.
638
+ _isResliceSyncTarget(e, assigned) {
639
+ if (e.kind !== "IndexExpr" || e._mapValueType) return false;
640
+ const c = e.expr;
641
+ if (c.kind !== "Ident" && c.kind !== "SelectorExpr") return false;
642
+ if (!this._resliceNames.has(c.kind === "Ident" ? c.name : `.${c.field}`))
643
+ return false;
644
+ const u = c._type?.kind === "named" ? c._type.underlying : c._type;
645
+ if (u?.kind !== "slice" || isTypedArraySlice(c._type)) return false;
646
+ let root = c;
647
+ while (root.kind === "SelectorExpr") root = root.expr;
648
+ return (
649
+ root.kind === "Ident" &&
650
+ !assigned.has(root.name) &&
651
+ isStableIndex(c, assigned)
652
+ );
653
+ },
654
+
655
+ _emitResliceSyncs(syncs) {
656
+ if (syncs) for (const s of syncs) this.line(`${s};`);
657
+ },
658
+
274
659
  _genAssignStmt(stmt) {
660
+ this._markIndexLvalues(stmt.lhs);
661
+ if (stmt.op !== "=") for (const l of stmt.lhs) this._markDerefOpTarget(l);
662
+ this._markSelfReslice(stmt);
275
663
  if (this._isComplexCompoundAssign(stmt)) {
276
664
  this._genComplexCompoundAssign(stmt);
277
665
  return;
@@ -284,18 +672,38 @@ export const statementGenMethods = {
284
672
  this._genCommaOkTypeAssert(stmt);
285
673
  return;
286
674
  }
675
+ if (this._isTypeParamDerefAssign(stmt)) {
676
+ const ptr = this.genExpr(stmt.lhs[0].operand);
677
+ const val = this.genValueExpr(stmt.rhs[0]);
678
+ const isStruct = this._typeParamIsStructJs(
679
+ stmt.lhs[0].operand._type.base.name,
680
+ );
681
+ this.line(
682
+ `((__p, __v) => { if (${isStruct}) Object.assign(__p, __v); else __p.value = __v; })(${ptr}, ${val});`,
683
+ );
684
+ return;
685
+ }
686
+ if (this._hasTypeParamDerefTarget(stmt)) {
687
+ this._genTypeParamDerefMultiAssign(stmt);
688
+ return;
689
+ }
287
690
  if (this._isStructDerefAssign(stmt)) {
288
691
  this._genStructDerefAssign(stmt);
289
692
  return;
290
693
  }
694
+ if (this._isStableWholeAssign(stmt)) {
695
+ this._genStableWholeAssign(stmt);
696
+ return;
697
+ }
291
698
  const paired = stmt.rhs.length === stmt.lhs.length && stmt.op === "=";
292
699
  const rhs = stmt.rhs.map((e, i) =>
293
700
  paired && stmt.lhs[i].name !== "_"
294
701
  ? this.genValueExpr(e)
295
702
  : this.genExpr(e),
296
703
  );
297
- this._markIndexLvalues(stmt.lhs);
704
+ const hoisted = this._hoistLhsOperands(stmt);
298
705
  const lhs = stmt.lhs.map((e) => this._genAssignLhsExpr(e));
706
+ hoisted?.restore();
299
707
  const pairs = lhs.map((l, i) => ({ l, r: rhs[i] ?? rhs[0] }));
300
708
  const active = pairs.filter((p) => p.l !== "_");
301
709
  if (active.length === 0) {
@@ -321,11 +729,11 @@ export const statementGenMethods = {
321
729
  this._hasCallExpr(stmt.lhs[0].index)
322
730
  ) {
323
731
  this.line(
324
- `((__m, __k) => { __m[__k] = (__m[__k] ?? ${zero}) ${binOp} (${active[0].r}); })(${base}, ${idx});`,
732
+ `((__m, __k) => { ${this._genMapWriteBase("__m")}[__k] = (__m[__k] ?? ${zero}) ${binOp} (${active[0].r}); })(${base}, ${idx});`,
325
733
  );
326
734
  } else {
327
735
  this.line(
328
- `${base}[${idx}] = (${base}[${idx}] ?? ${zero}) ${binOp} (${active[0].r});`,
736
+ `${this._genMapWriteBase(base)}[${idx}] = (${base}[${idx}] ?? ${zero}) ${binOp} (${active[0].r});`,
329
737
  );
330
738
  }
331
739
  return;
@@ -343,12 +751,67 @@ export const statementGenMethods = {
343
751
  ) {
344
752
  return;
345
753
  }
346
- this.line(`${active[0].l} ${stmt.op} ${active[0].r};`);
754
+ const targetL =
755
+ active[0].l.startsWith("{") && !active[0].l.startsWith("({")
756
+ ? `(${active[0].l})`
757
+ : active[0].l;
758
+ this.line(`${targetL} ${stmt.op} ${active[0].r};`);
759
+ } else if (hoisted) {
760
+ this.line("{");
761
+ this.indented(() => {
762
+ for (const l of hoisted.lines) this.line(l);
763
+ this._genAssignMulti(stmt, lhs, rhs, active, pairs);
764
+ });
765
+ this.line("}");
347
766
  } else {
348
767
  this._genAssignMulti(stmt, lhs, rhs, active, pairs);
349
768
  }
350
769
  },
351
770
 
771
+ // `k, xs[k] = 1, 5`: Go evaluates the operands of every index and field
772
+ // target before any assignment, but a JS destructuring target reads its
773
+ // operands after the earlier ones are stored. Operands that mention a
774
+ // variable assigned by the same statement are bound to temporaries first.
775
+ _hoistLhsOperands(stmt) {
776
+ if (stmt.op !== "=" || stmt.lhs.length < 2) return null;
777
+ const assigned = new Set(
778
+ stmt.lhs
779
+ .filter((l) => l.kind === "Ident" && l.name !== "_")
780
+ .map((l) => l.name),
781
+ );
782
+ if (assigned.size === 0) return null;
783
+ const mentions = (n) =>
784
+ !!n &&
785
+ typeof n === "object" &&
786
+ ((n.kind === "Ident" && assigned.has(n.name)) || someChild(n, mentions));
787
+ const lines = [];
788
+ const undo = [];
789
+ for (const l of stmt.lhs) {
790
+ let keys;
791
+ if (l.kind === "IndexExpr") keys = ["expr", "index"];
792
+ else if (l.kind === "SelectorExpr" && !l._isMethodExpr) keys = ["expr"];
793
+ else continue;
794
+ for (const key of keys) {
795
+ const operand = l[key];
796
+ if (!mentions(operand)) continue;
797
+ // A struct or array value names the variable's storage in Go.
798
+ if (key === "expr" && this._aggregateBase(operand._type)) continue;
799
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
800
+ const name = `__h${this._tmpCounter}`;
801
+ lines.push(`const ${name} = ${this.genExpr(operand)};`);
802
+ undo.push([l, key, operand]);
803
+ l[key] = { kind: "Ident", name, _type: operand._type };
804
+ }
805
+ }
806
+ if (lines.length === 0) return null;
807
+ return {
808
+ lines,
809
+ restore: () => {
810
+ for (const [node, key, v] of undo) node[key] = v;
811
+ },
812
+ };
813
+ },
814
+
352
815
  _genCommaOkTypeAssert(stmt) {
353
816
  const val = this.genExpr(stmt.rhs[0]);
354
817
  const [vName, okName] = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
@@ -359,6 +822,68 @@ export const statementGenMethods = {
359
822
  if (okName !== "_") this.line(`${okName} = ${tmp}[1];`);
360
823
  },
361
824
 
825
+ // `a = [3]int{...}` / `s = S{...}` on a variable whose address (or an
826
+ // element's or field's address) is taken must write into the existing
827
+ // storage so those pointers see the new value (Task 0.33).
828
+ _isStableWholeAssign(stmt) {
829
+ if (stmt.op !== "=" || stmt.lhs.length !== 1 || stmt.rhs.length !== 1)
830
+ return false;
831
+ const l = stmt.lhs[0];
832
+ if (l.kind === "IndexExpr" && l._mapValueType) return false;
833
+ if (
834
+ l.kind !== "Ident" &&
835
+ l.kind !== "SelectorExpr" &&
836
+ l.kind !== "IndexExpr"
837
+ )
838
+ return false;
839
+ if (l.kind === "Ident" && l.name === "_") return false;
840
+ if (
841
+ !this._aggregateBase(l._type) ||
842
+ !this._aggregateBase(stmt.rhs[0]._type)
843
+ )
844
+ return false;
845
+ const root = this._rootIdentName(l);
846
+ return root !== null && this._fnMutates(root, true);
847
+ },
848
+
849
+ _genStableWholeAssign(stmt) {
850
+ let target = this._genAssignLhsExpr(stmt.lhs[0]);
851
+ const type = stmt.lhs[0]._type;
852
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
853
+ const src = `__s${this._tmpCounter}`;
854
+ this.line(`{ const ${src} = ${this.genExpr(stmt.rhs[0])};`);
855
+ if (stmt.lhs[0].kind === "IndexExpr") {
856
+ // Evaluate the element reference once.
857
+ target = `__d${this._tmpCounter}`;
858
+ this.line(`const ${target} = ${this._genAssignLhsExpr(stmt.lhs[0])};`);
859
+ }
860
+ this._emitCopyInto(target, src, type);
861
+ this.line("}");
862
+ },
863
+
864
+ // Copies aggregate `src` into the storage `dst` element by element, going
865
+ // into nested arrays and structs so pointers to inner parts stay valid.
866
+ _emitCopyInto(dst, src, type) {
867
+ const base = this._aggregateBase(type);
868
+ if (base.kind === "struct") {
869
+ for (const [name, ft] of base.fields) {
870
+ if (name === "_") continue;
871
+ const f = `.${name}`;
872
+ if (this._aggregateBase(ft))
873
+ this._emitCopyInto(`${dst}${f}`, `${src}${f}`, ft);
874
+ else this.line(`${dst}${f} = ${src}${f};`);
875
+ }
876
+ return;
877
+ }
878
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
879
+ const k = `__k${this._tmpCounter}`;
880
+ this.line(`for (let ${k} = 0; ${k} < ${src}.length; ${k}++) {`);
881
+ if (this._aggregateBase(base.elem))
882
+ this._emitCopyInto(`${dst}[${k}]`, `${src}[${k}]`, base.elem);
883
+ else this.line(`${dst}[${k}] = ${src}[${k}];`);
884
+ this.line("}");
885
+ },
886
+
362
887
  _genStructDerefAssign(stmt) {
363
888
  const target = this.genExpr(stmt.lhs[0].operand);
364
889
  const rhsNode = stmt.rhs[0];
@@ -385,31 +910,45 @@ export const statementGenMethods = {
385
910
  },
386
911
 
387
912
  _genComplexCompoundAssign(stmt) {
388
- const lhsStr = stmt.lhs[0].name ?? this.genExpr(stmt.lhs[0]);
913
+ const l = stmt.lhs[0];
914
+ let lhsTarget;
915
+ let lhsRead;
916
+ if (l.kind === "IndexExpr" && l._mapValueType) {
917
+ const wrapField = this._namedWrapperField(l.expr._type, l.expr);
918
+ const base = wrapField
919
+ ? `${this.genExpr(l.expr)}.${wrapField}`
920
+ : this.genExpr(l.expr);
921
+ const idx = this.genExpr(l.index);
922
+ lhsTarget = `${this._genMapWriteBase(base)}[${idx}]`;
923
+ lhsRead = `(${base}[${idx}] ?? { re: 0, im: 0 })`;
924
+ } else {
925
+ lhsTarget = l.name ?? this.genExpr(l);
926
+ lhsRead = lhsTarget;
927
+ }
389
928
  const rhsExpr = this._genComplexOperand(stmt.rhs[0]);
390
929
  const baseOp = stmt.op.slice(0, -1); // "+=" → "+"
391
930
  let result;
392
931
  switch (baseOp) {
393
932
  case "+":
394
- result = `{ re: ${lhsStr}.re + ${rhsExpr}.re, im: ${lhsStr}.im + ${rhsExpr}.im }`;
933
+ result = `{ re: ${lhsRead}.re + ${rhsExpr}.re, im: ${lhsRead}.im + ${rhsExpr}.im }`;
395
934
  break;
396
935
  case "-":
397
- result = `{ re: ${lhsStr}.re - ${rhsExpr}.re, im: ${lhsStr}.im - ${rhsExpr}.im }`;
936
+ result = `{ re: ${lhsRead}.re - ${rhsExpr}.re, im: ${lhsRead}.im - ${rhsExpr}.im }`;
398
937
  break;
399
938
  case "*":
400
939
  this.useHelper("cmul");
401
- result = `__cmul(${lhsStr}, ${rhsExpr})`;
940
+ result = `__cmul(${lhsRead}, ${rhsExpr})`;
402
941
  break;
403
942
  case "/":
404
943
  this.useHelper("cdiv");
405
- result = `__cdiv(${lhsStr}, ${rhsExpr})`;
944
+ result = `__cdiv(${lhsRead}, ${rhsExpr})`;
406
945
  break;
407
946
  default:
408
947
  throw new Error(
409
948
  `unsupported complex compound assignment operator: ${stmt.op}`,
410
949
  );
411
950
  }
412
- this.line(`${lhsStr} = ${result};`);
951
+ this.line(`${lhsTarget} = ${result};`);
413
952
  },
414
953
 
415
954
  _genIteratorReturn(stmt) {
@@ -468,8 +1007,12 @@ export const statementGenMethods = {
468
1007
  }
469
1008
  },
470
1009
 
471
- _genBranchStmt(stmt) {
472
- if (this._inIteratorBody && !stmt.label) {
1010
+ _genIteratorBranchStmt(stmt) {
1011
+ if (!stmt.label) {
1012
+ if (this._iterLoopDepth > 0) {
1013
+ if (stmt.keyword !== "fallthrough") this.line(`${stmt.keyword};`);
1014
+ return;
1015
+ }
473
1016
  if (stmt.keyword === "break") {
474
1017
  this.line(`${this._iterBreakFlag} = true; return false;`);
475
1018
  return;
@@ -478,6 +1021,29 @@ export const statementGenMethods = {
478
1021
  this.line("return true;");
479
1022
  return;
480
1023
  }
1024
+ return;
1025
+ }
1026
+ if (this._innerLoopLabels?.has(stmt.label)) {
1027
+ if (stmt.keyword !== "fallthrough")
1028
+ this.line(`${stmt.keyword} ${stmt.label};`);
1029
+ return;
1030
+ }
1031
+ if (
1032
+ stmt.keyword === "continue" &&
1033
+ stmt.label === this._currentIterLoopLabel
1034
+ ) {
1035
+ this.line("return true;");
1036
+ return;
1037
+ }
1038
+ this.line(
1039
+ `${this._iterTargetFlag} = "${stmt.keyword}$${stmt.label}"; return false;`,
1040
+ );
1041
+ },
1042
+
1043
+ _genBranchStmt(stmt) {
1044
+ if (this._inIteratorBody) {
1045
+ this._genIteratorBranchStmt(stmt);
1046
+ return;
481
1047
  }
482
1048
  if (stmt.keyword !== "fallthrough")
483
1049
  this.line(
@@ -538,31 +1104,73 @@ export const statementGenMethods = {
538
1104
  },
539
1105
 
540
1106
  genFor(stmt) {
541
- if (this.isRangeFor(stmt)) {
542
- if (stmt.init.rhs[0]._isIterator) this.genIteratorFor(stmt);
543
- else this.genRangeFor(stmt);
544
- return;
545
- }
546
- if (stmt.cond?.kind === "RangeExpr") {
547
- this._genRangeExprFor(stmt);
548
- return;
1107
+ const isInner = this._inIteratorBody;
1108
+ if (isInner) {
1109
+ this._iterLoopDepth = (this._iterLoopDepth ?? 0) + 1;
1110
+ if (stmt.label) {
1111
+ this._innerLoopLabels = this._innerLoopLabels ?? new Set();
1112
+ this._innerLoopLabels.add(stmt.label);
1113
+ }
549
1114
  }
550
- if (!stmt.init && !stmt.post) {
551
- this._genSimpleWhileFor(stmt);
552
- return;
1115
+ try {
1116
+ if (this.isRangeFor(stmt)) {
1117
+ if (stmt.init.rhs[0]._isIterator) this.genIteratorFor(stmt);
1118
+ else this.genRangeFor(stmt);
1119
+ return;
1120
+ }
1121
+ if (stmt.cond?.kind === "RangeExpr") {
1122
+ this._genRangeExprFor(stmt);
1123
+ return;
1124
+ }
1125
+ if (!stmt.init && !stmt.post) {
1126
+ this._genSimpleWhileFor(stmt);
1127
+ return;
1128
+ }
1129
+ const init = stmt.init ? this.stmtInline(stmt.init) : "";
1130
+ const cond = stmt.cond ? this.genExpr(stmt.cond) : "";
1131
+ const post = this._perIterationClones(
1132
+ stmt,
1133
+ stmt.post ? this.stmtInline(stmt.post) : "",
1134
+ );
1135
+ this.line(`for (${init}; ${cond}; ${post}) {`);
1136
+ this.indented(() => this.genBlock(stmt.body));
1137
+ this.line("}");
1138
+ } finally {
1139
+ if (isInner) {
1140
+ this._iterLoopDepth--;
1141
+ if (stmt.label) this._innerLoopLabels.delete(stmt.label);
1142
+ }
553
1143
  }
554
- const init = stmt.init ? this.stmtInline(stmt.init) : "";
555
- const cond = stmt.cond ? this.genExpr(stmt.cond) : "";
556
- const post = stmt.post ? this.stmtInline(stmt.post) : "";
557
- this.line(`for (${init}; ${cond}; ${post}) {`);
558
- this.indented(() => this.genBlock(stmt.body));
559
- this.line("}");
560
1144
  },
561
1145
 
562
1146
  isRangeFor(stmt) {
563
1147
  return isRangeFor(stmt);
564
1148
  },
565
1149
 
1150
+ // Go 1.22 per-iteration loop variables. `let` gives each iteration its
1151
+ // own binding, but a struct or array variable is a reference and an
1152
+ // address-taken scalar is a `{ value }` box: replace it at the start of
1153
+ // the post step (which runs in the next iteration's binding), so closures
1154
+ // and pointers from the finished iteration keep their own variable.
1155
+ _perIterationClones(stmt, post) {
1156
+ const clones = [];
1157
+ for (const name of loopVarsCapturedInLoop(stmt)) {
1158
+ const i = stmt.init.lhs.findIndex((e) => e.name === name);
1159
+ const t = stmt.init.lhs[i]?._type ?? stmt.init.rhs?.[i]?._type;
1160
+ if (this._boxedVars?.has(name)) {
1161
+ const v = `${name}.value`;
1162
+ const copy = this._aggregateBase(t) ? this._cloneJs(t, v) : v;
1163
+ clones.push(`${name} = { value: ${copy} }`);
1164
+ continue;
1165
+ }
1166
+ if (!this._aggregateBase(t)) continue;
1167
+ clones.push(`${name} = ${this._cloneJs(t, name)}`);
1168
+ }
1169
+ if (clones.length === 0) return post;
1170
+ const tail = post.replace(/;\s*$/, "");
1171
+ return tail ? `${clones.join(", ")}, ${tail}` : clones.join(", ");
1172
+ },
1173
+
566
1174
  _isIntRangeType(iterType) {
567
1175
  return isIntRangeType(iterType);
568
1176
  },
@@ -621,21 +1229,33 @@ export const statementGenMethods = {
621
1229
  return mutated ? this._cloneJs(elemType, js) : js;
622
1230
  },
623
1231
 
624
- _genSliceRangeFor(stmt, iteree, isAssign) {
1232
+ _genSliceRangeFor(stmt, iteree, isAssign, unwrapView = false) {
625
1233
  this._loopDepth = this._loopDepth ?? 0;
626
1234
  const d = this._loopDepth++;
627
1235
  const [idxNode, valNode] = stmt.init.lhs;
628
1236
  const i = `__i${d}`;
629
1237
  const arr = `__arr${d}`;
630
1238
  const len = `__len${d}`;
631
- this.line(
632
- `for (let ${i} = 0, ${arr} = ${iteree}, ${len} = ${arr} ? ${arr}.length : 0; ${i} < ${len}; ${i}++) {`,
633
- );
1239
+ let elem = `${arr}[${i}]`;
1240
+ if (unwrapView) {
1241
+ // A partial view of a plain array is a Proxy (`__slice`): read the
1242
+ // elements from its backing array `b` at offset `o` instead.
1243
+ this.useHelper("sv");
1244
+ const [v, b, o] = [`__v${d}`, `__b${d}`, `__o${d}`];
1245
+ this.line(
1246
+ `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}++) {`,
1247
+ );
1248
+ elem = `${b}[${o} + ${i}]`;
1249
+ } else {
1250
+ this.line(
1251
+ `for (let ${i} = 0, ${arr} = ${iteree}, ${len} = ${arr} ? ${arr}.length : 0; ${i} < ${len}; ${i}++) {`,
1252
+ );
1253
+ }
634
1254
  this.indented(() => {
635
1255
  this._emitRangeVar(idxNode, i, isAssign);
636
1256
  this._emitRangeVar(
637
1257
  valNode,
638
- this._rangeElemValue(stmt, valNode, `${arr}[${i}]`, isAssign),
1258
+ this._rangeElemValue(stmt, valNode, elem, isAssign),
639
1259
  isAssign,
640
1260
  );
641
1261
  const declared = isAssign
@@ -731,6 +1351,7 @@ export const statementGenMethods = {
731
1351
 
732
1352
  _genIterFor(init, iterType, iteree, lhs, body) {
733
1353
  const iterExpr = this._genRangeIterExpr(iterType, iteree, lhs);
1354
+ const keyDecode = this._structKeyRange(iterType);
734
1355
  const binding =
735
1356
  lhs.length === 1
736
1357
  ? lhs[0] === "_"
@@ -739,10 +1360,38 @@ export const statementGenMethods = {
739
1360
  : `[${lhs.map((n) => (n === "_" ? "_$" : n)).join(", ")}]`;
740
1361
  const kw = init.kind === "AssignStmt" ? "" : "let ";
741
1362
  this.line(`for (${kw}${binding} of ${iterExpr}) {`);
742
- this.indented(() => this.genBlock(body));
1363
+ this.indented(() => {
1364
+ if (keyDecode && lhs[0] !== "_")
1365
+ this.line(`${lhs[0]} = __kd(${lhs[0]}, true);`);
1366
+ this._cloneMapRangeValue(init, iterType, lhs[1]);
1367
+ this.genBlock(body);
1368
+ });
743
1369
  this.line("}");
744
1370
  },
745
1371
 
1372
+ // Ranging over a map whose keys are structs: the key variable is decoded
1373
+ // from its encoded string (see HELPER_STRUCT_KEY).
1374
+ _structKeyRange(iterType) {
1375
+ if (!this._isMapRangeType(iterType)) return false;
1376
+ const base = iterType?.kind === "named" ? iterType.underlying : iterType;
1377
+ const key = base?.kind === "map" ? base.key : null;
1378
+ const keyBase = key?.kind === "named" ? key.underlying : key;
1379
+ if (keyBase?.kind !== "struct" && !this._isIfaceType(key)) return false;
1380
+ this.useHelper("structKey");
1381
+ return true;
1382
+ },
1383
+
1384
+ // A map range value of array or struct type is a copy: clone it when the
1385
+ // body may mutate it (it otherwise aliases the stored value).
1386
+ _cloneMapRangeValue(init, iterType, name) {
1387
+ if (!name || name === "_" || !this._isMapRangeType(iterType)) return;
1388
+ const base = iterType?.kind === "named" ? iterType.underlying : iterType;
1389
+ const valType = base?.value ?? base?.elem;
1390
+ if (!this._aggregateBase(valType)) return;
1391
+ if (init.kind !== "AssignStmt" && !this._fnMutates(name)) return;
1392
+ this.line(`${name} = ${this._cloneJs(valType, name)};`);
1393
+ },
1394
+
746
1395
  genRangeFor(stmt) {
747
1396
  const init = stmt.init;
748
1397
  const range = init.rhs[0];
@@ -768,7 +1417,26 @@ export const statementGenMethods = {
768
1417
  return;
769
1418
  }
770
1419
 
771
- this._genSliceRangeFor(stmt, iteree, init.kind === "AssignStmt");
1420
+ // Go ranges over a copy of an array: the range expression is evaluated
1421
+ // once, so writes to the array inside the body do not reach `v`.
1422
+ const valNode = init.lhs[1];
1423
+ const rangesArrayValue =
1424
+ valNode &&
1425
+ valNode.name !== "_" &&
1426
+ this._aggregateBase(iterType)?.kind === "array";
1427
+ const iterBase =
1428
+ iterType?.kind === "named" ? iterType.underlying : iterType;
1429
+ const unwrapView =
1430
+ valNode?.name !== "_" &&
1431
+ !!valNode &&
1432
+ iterBase?.kind === "slice" &&
1433
+ !isTypedArraySlice(iterType);
1434
+ this._genSliceRangeFor(
1435
+ stmt,
1436
+ rangesArrayValue ? this._cloneJs(iterType, iteree) : iteree,
1437
+ init.kind === "AssignStmt",
1438
+ unwrapView,
1439
+ );
772
1440
  },
773
1441
 
774
1442
  _genRangeIterExpr(iterType, iteree, lhs) {
@@ -781,7 +1449,28 @@ export const statementGenMethods = {
781
1449
  return `__s(${iteree}).entries()`;
782
1450
  },
783
1451
 
784
- _genIteratorLoop(itereeExpr, paramsStr, body) {
1452
+ _emitIterTargetDispatch(targetFlag) {
1453
+ const labels = this._functionLabels ?? [];
1454
+ this.line(`if (${targetFlag}) {`);
1455
+ this.indented(() => {
1456
+ for (const lbl of labels) {
1457
+ this.line(`if (${targetFlag} === "break$${lbl}") break ${lbl};`);
1458
+ this.line(`if (${targetFlag} === "continue$${lbl}") continue ${lbl};`);
1459
+ }
1460
+ if (this._inIteratorBody) {
1461
+ this.line(`${this._iterTargetFlag} = ${targetFlag}; return false;`);
1462
+ }
1463
+ });
1464
+ this.line("}");
1465
+ },
1466
+
1467
+ _genIteratorLoop(
1468
+ itereeExpr,
1469
+ paramsStr,
1470
+ body,
1471
+ assignPreamble = null,
1472
+ loopLabel = null,
1473
+ ) {
785
1474
  let iteree = this.genExpr(itereeExpr);
786
1475
  if (itereeExpr.kind === "FuncLit") {
787
1476
  iteree = `(${iteree})`;
@@ -791,38 +1480,61 @@ export const statementGenMethods = {
791
1480
  const breakFlag = `__broke${d}`;
792
1481
  const retFlag = `__returned${d}`;
793
1482
  const retVar = `__retVal${d}`;
1483
+ const targetFlag = `__target${d}`;
794
1484
 
795
1485
  this.line("{");
796
1486
  this.indented(() => {
797
1487
  this.line(`let ${breakFlag} = false;`);
798
1488
  this.line(`let ${retFlag} = false;`);
799
1489
  this.line(`let ${retVar};`);
1490
+ this.line(`let ${targetFlag} = null;`);
800
1491
 
801
1492
  this.line(`${iteree}(function(${paramsStr}) {`);
802
1493
  this.indented(() => {
803
- this.line(`if (${breakFlag}) return false;`);
1494
+ this.line(`if (${breakFlag} || ${retFlag} || ${targetFlag}) {`);
1495
+ this.indented(() => {
1496
+ if (this.strict) {
1497
+ this.line(
1498
+ `throw new Error("range function continued iteration after function for loop body returned false");`,
1499
+ );
1500
+ } else {
1501
+ this.line("return false;");
1502
+ }
1503
+ });
1504
+ this.line("}");
804
1505
  const prev = {
805
1506
  in: this._inIteratorBody,
806
1507
  break: this._iterBreakFlag,
807
1508
  ret: this._iterReturnFlag,
808
1509
  retVar: this._iterReturnVar,
1510
+ target: this._iterTargetFlag,
1511
+ loopLabel: this._currentIterLoopLabel,
1512
+ loopDepth: this._iterLoopDepth,
809
1513
  };
810
1514
  this._inIteratorBody = true;
811
1515
  this._iterBreakFlag = breakFlag;
812
1516
  this._iterReturnFlag = retFlag;
813
1517
  this._iterReturnVar = retVar;
1518
+ this._iterTargetFlag = targetFlag;
1519
+ this._currentIterLoopLabel = loopLabel;
1520
+ this._iterLoopDepth = 0;
814
1521
 
1522
+ if (assignPreamble) assignPreamble();
815
1523
  this.genBlock(body);
816
1524
 
817
1525
  this._inIteratorBody = prev.in;
818
1526
  this._iterBreakFlag = prev.break;
819
1527
  this._iterReturnFlag = prev.ret;
820
1528
  this._iterReturnVar = prev.retVar;
1529
+ this._iterTargetFlag = prev.target;
1530
+ this._currentIterLoopLabel = prev.loopLabel;
1531
+ this._iterLoopDepth = prev.loopDepth;
821
1532
 
822
1533
  this.line("return true;");
823
1534
  });
824
1535
  this.line("});");
825
1536
  this.line(`if (${retFlag}) return ${retVar};`);
1537
+ this._emitIterTargetDispatch(targetFlag);
826
1538
  });
827
1539
  this.line("}");
828
1540
  this._iterDepth--;
@@ -830,32 +1542,92 @@ export const statementGenMethods = {
830
1542
 
831
1543
  genIteratorFor(stmt) {
832
1544
  const range = stmt.init.rhs[0];
1545
+ const isAssign = stmt.init.kind === "AssignStmt";
833
1546
  const lhs = stmt.init.lhs.map((e) => e.name ?? this.genExpr(e));
1547
+ const yieldTypes = range._yieldParams ?? [];
1548
+ if (isAssign) {
1549
+ const cbParams = lhs.map((_, i) => `__p${i}`);
1550
+ const params = yieldTypes.length === 0 ? "" : cbParams.join(", ");
1551
+ const assignPreamble = () => {
1552
+ for (let i = 0; i < lhs.length; i++) {
1553
+ if (lhs[i] !== "_") {
1554
+ const yType = yieldTypes[i];
1555
+ const isStruct =
1556
+ this._isStructType(yType) ||
1557
+ this._isStructType(stmt.init.lhs[i]?._type);
1558
+ if (isStruct) {
1559
+ this.useHelper("sclone");
1560
+ this.line(`${lhs[i]} = __sclone(${cbParams[i]});`);
1561
+ } else {
1562
+ this.line(`${lhs[i]} = ${cbParams[i]};`);
1563
+ }
1564
+ }
1565
+ }
1566
+ };
1567
+ this._genIteratorLoop(
1568
+ range.expr,
1569
+ params,
1570
+ stmt.body,
1571
+ assignPreamble,
1572
+ stmt.label ?? null,
1573
+ );
1574
+ return;
1575
+ }
834
1576
  const cbParams = lhs.map((n, i) => (n === "_" ? `_$${i}` : n));
835
- const params = range._yieldParams.length === 0 ? "" : cbParams.join(", ");
836
- this._genIteratorLoop(range.expr, params, stmt.body);
1577
+ const params = yieldTypes.length === 0 ? "" : cbParams.join(", ");
1578
+ const clonePreamble = () => {
1579
+ for (let i = 0; i < lhs.length; i++) {
1580
+ if (lhs[i] !== "_") {
1581
+ const yType = yieldTypes[i];
1582
+ const isStruct =
1583
+ this._isStructType(yType) ||
1584
+ this._isStructType(stmt.init.lhs[i]?._type);
1585
+ if (isStruct) {
1586
+ this.useHelper("sclone");
1587
+ this.line(`${cbParams[i]} = __sclone(${cbParams[i]});`);
1588
+ }
1589
+ }
1590
+ }
1591
+ };
1592
+ this._genIteratorLoop(
1593
+ range.expr,
1594
+ params,
1595
+ stmt.body,
1596
+ clonePreamble,
1597
+ stmt.label ?? null,
1598
+ );
837
1599
  },
838
1600
 
839
1601
  // 0-param iterator: for range iter { body }
840
1602
  genIteratorForCond(stmt) {
841
- this._genIteratorLoop(stmt.cond.expr, "", stmt.body);
1603
+ this._genIteratorLoop(
1604
+ stmt.cond.expr,
1605
+ "",
1606
+ stmt.body,
1607
+ null,
1608
+ stmt.label ?? null,
1609
+ );
842
1610
  },
843
1611
 
844
1612
  // Inline a statement as a for-init/post string (no semicolon)
845
1613
  stmtInline(stmt) {
846
1614
  switch (stmt.kind) {
847
- case "DefineStmt": {
848
- const rhs = stmt.rhs.map((e) => this.genExpr(e)).join(", ");
849
- const lhs = stmt.lhs.map((e) => e.name ?? this.genExpr(e)).join(", ");
850
- return `let ${lhs} = ${rhs}`;
851
- }
852
- case "AssignStmt": {
853
- const rhs = stmt.rhs.map((e) => this.genExpr(e)).join(", ");
854
- const lhs = stmt.lhs.map((e) => this.genExpr(e)).join(", ");
855
- return `${lhs} ${stmt.op} ${rhs}`;
856
- }
857
- case "IncDecStmt":
858
- return `${this.genExpr(stmt.expr)}${stmt.op}`;
1615
+ case "DefineStmt":
1616
+ return this._inlineDefine(stmt);
1617
+ case "AssignStmt":
1618
+ return this._withInlineSyncs(
1619
+ this._resliceSyncs(stmt.lhs, stmt, true),
1620
+ this._inlineAssign(stmt),
1621
+ );
1622
+ case "IncDecStmt": {
1623
+ const syncs = this._resliceSyncs([stmt.expr], stmt, true);
1624
+ this._markIndexLvalues([stmt.expr]);
1625
+ this._markDerefOpTarget(stmt.expr);
1626
+ return this._withInlineSyncs(
1627
+ syncs,
1628
+ `${this.genExpr(stmt.expr)}${stmt.op}`,
1629
+ );
1630
+ }
859
1631
  case "ExprStmt":
860
1632
  return this.genExpr(stmt.expr);
861
1633
  default:
@@ -863,14 +1635,68 @@ export const statementGenMethods = {
863
1635
  }
864
1636
  },
865
1637
 
1638
+ _withInlineSyncs(syncs, js) {
1639
+ return syncs ? `${js}, ${syncs.join(", ")}` : js;
1640
+ },
1641
+
1642
+ _inlineDefine(stmt) {
1643
+ const boxed =
1644
+ stmt.lhs.length === 1 &&
1645
+ stmt.rhs.length === 1 &&
1646
+ this._boxedVars.has(stmt.lhs[0].name);
1647
+ if (boxed)
1648
+ return `let ${stmt.lhs[0].name} = { value: ${this.genValueExpr(stmt.rhs[0])} }`;
1649
+ const rhs = stmt.rhs.map((e) => this.genExpr(e));
1650
+ const lhs = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
1651
+ if (lhs.length === 1) return `let ${lhs[0]} = ${rhs[0]}`;
1652
+ // `for i, j := 0, 10`: one declarator per variable.
1653
+ if (rhs.length === lhs.length) {
1654
+ const decls = lhs.map((n, i) =>
1655
+ this._boxedVars.has(n)
1656
+ ? `${n} = { value: ${rhs[i]} }`
1657
+ : `${n} = ${rhs[i]}`,
1658
+ );
1659
+ return `let ${decls.join(", ")}`;
1660
+ }
1661
+ return `let [${lhs.map((n) => (n === "_" ? "" : n)).join(", ")}] = ${rhs[0]}`;
1662
+ },
1663
+
1664
+ _inlineAssign(stmt) {
1665
+ this._markIndexLvalues(stmt.lhs);
1666
+ const rhs = stmt.rhs.map((e) => this.genExpr(e));
1667
+ if (stmt.lhs.length === 1)
1668
+ return `${this.genExpr(stmt.lhs[0])} ${stmt.op} ${rhs[0]}`;
1669
+ // `i, j = i+1, j-1`: every right-hand side is evaluated first.
1670
+ const lhs = stmt.lhs.map((e) =>
1671
+ e.name === "_" ? "" : this._genAssignLhsExpr(e),
1672
+ );
1673
+ const r = rhs.length === 1 ? rhs[0] : `[${rhs.join(", ")}]`;
1674
+ return `[${lhs.join(", ")}] = ${r}`;
1675
+ },
1676
+
1677
+ // Interface-typed tags may hold boxed named values; cases compare with __ifeq.
1678
+ _switchTagAndCases(stmt) {
1679
+ const tag = stmt.tag ? this.genExpr(stmt.tag) : "true";
1680
+ if (!stmt.tag || !this._isIfaceType(stmt.tag._type))
1681
+ return { tag, caseExpr: (e) => this.genExpr(e) };
1682
+ this.useHelper("ifeq");
1683
+ this._swCounter = (this._swCounter ?? 0) + 1;
1684
+ const tmp = `__sw${this._swCounter}`;
1685
+ this.line(`const ${tmp} = ${tag};`);
1686
+ return {
1687
+ tag: "true",
1688
+ caseExpr: (e) => `__ifeq(${tmp}, ${this._genIfaceOperand(e)})`,
1689
+ };
1690
+ },
1691
+
866
1692
  genSwitch(stmt) {
867
1693
  const genBody = () => {
868
- const tag = stmt.tag ? this.genExpr(stmt.tag) : "true";
1694
+ const { tag, caseExpr } = this._switchTagAndCases(stmt);
869
1695
  this.line(`switch (${tag}) {`);
870
1696
  this.indented(() => {
871
1697
  for (const c of stmt.cases) {
872
1698
  if (c.list) {
873
- for (const e of c.list) this.line(`case ${this.genExpr(e)}:`);
1699
+ for (const e of c.list) this.line(`case ${caseExpr(e)}:`);
874
1700
  } else {
875
1701
  this.line("default:");
876
1702
  }
@@ -926,6 +1752,24 @@ export const statementGenMethods = {
926
1752
  .join(" || ");
927
1753
  this.line(`${first ? "if" : "else if"} (${cond}) {`);
928
1754
  this.indented(() => {
1755
+ // A single named non-struct case binds the unboxed value.
1756
+ if (
1757
+ stmt.assign &&
1758
+ c.types.length === 1 &&
1759
+ this._assertedValueJs(c.types[0], "__tsw") !== "__tsw"
1760
+ )
1761
+ this.line(
1762
+ `const ${stmt.assign} = ${this._assertedValueJs(c.types[0], "__tsw")};`,
1763
+ );
1764
+ // An array case binds a copy of the boxed value.
1765
+ else if (
1766
+ stmt.assign &&
1767
+ c.types.length === 1 &&
1768
+ this._typeFromNode(c.types[0])?.kind === "array"
1769
+ )
1770
+ this.line(
1771
+ `let ${stmt.assign} = ${this._cloneJs(this._typeFromNode(c.types[0]), "__tsw")};`,
1772
+ );
929
1773
  for (const s of c.stmts) this.genStmt(s);
930
1774
  });
931
1775
  this.line("}");