gofront 1.8.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +3 -3
  2. package/package.json +6 -5
  3. package/src/backend/js/expressions.js +1157 -408
  4. package/src/backend/js/index.js +327 -153
  5. package/src/backend/js/natives.js +383 -0
  6. package/src/backend/js/runtime.js +559 -2552
  7. package/src/backend/js/statements.js +670 -158
  8. package/src/backend/js/stdlib/index.js +45 -93
  9. package/src/backend/js/templ.js +32 -45
  10. package/src/backend/js/type-info.js +298 -19
  11. package/src/backend/shared/fixed-float.js +20 -0
  12. package/src/backend/shared/fmt-types.js +91 -170
  13. package/src/backend/shared/host-unicode.js +36 -0
  14. package/src/backend/wasm/boundary.js +490 -965
  15. package/src/backend/wasm/emit-builtins.js +884 -256
  16. package/src/backend/wasm/emit-exprs.js +185 -77
  17. package/src/backend/wasm/emit-maps.js +17 -367
  18. package/src/backend/wasm/emit-shared.js +4 -1
  19. package/src/backend/wasm/emit-stmts.js +524 -240
  20. package/src/backend/wasm/emit.js +176 -38
  21. package/src/backend/wasm/globals.js +48 -8
  22. package/src/backend/wasm/glue.js +2 -4
  23. package/src/backend/wasm/index.js +44 -530
  24. package/src/backend/wasm/method-expr.js +12 -1
  25. package/src/backend/wasm/module-emitter.js +299 -770
  26. package/src/backend/wasm/monomorph.js +139 -8
  27. package/src/backend/wasm/natives.js +600 -0
  28. package/src/backend/wasm/prescan.js +5 -153
  29. package/src/backend/wasm/prune-funcs.js +89 -0
  30. package/src/backend/wasm/stdlib/index.js +2 -32
  31. package/src/backend/wasm/types.js +97 -20
  32. package/src/backend/wasm/variadic.js +9 -4
  33. package/src/cli/cli-core.js +5 -1
  34. package/src/cli/test-runner.js +115 -342
  35. package/src/compiler.js +113 -130
  36. package/src/lexer/lexer.js +106 -11
  37. package/src/lexer/templ-lexer.js +61 -18
  38. package/src/lexer/tokens.js +1 -0
  39. package/src/link/stdlib.js +355 -132
  40. package/src/link/type-names.js +204 -0
  41. package/src/link/wasm.js +30 -6
  42. package/src/lower/bounds.js +153 -0
  43. package/src/lower/captures.js +92 -33
  44. package/src/lower/embedding.js +62 -5
  45. package/src/lower/errors-as.js +196 -0
  46. package/src/lower/fmt-formatters.js +929 -0
  47. package/src/lower/functions.js +225 -7
  48. package/src/lower/gen-types.js +293 -0
  49. package/src/lower/index.js +14 -2
  50. package/src/lower/json-codecs.js +334 -245
  51. package/src/lower/ownership.js +25 -7
  52. package/src/lower/reachability.js +69 -51
  53. package/src/lower/walk.js +12 -0
  54. package/src/parser/declarations.js +30 -5
  55. package/src/parser/expressions.js +111 -57
  56. package/src/parser/statements.js +108 -0
  57. package/src/parser/templ.js +44 -5
  58. package/src/parser/types.js +12 -0
  59. package/src/resolver.js +1 -2
  60. package/src/typechecker/assignability.js +214 -19
  61. package/src/typechecker/const-fold.js +70 -27
  62. package/src/typechecker/constants.js +156 -8
  63. package/src/typechecker/expressions.js +490 -108
  64. package/src/typechecker/index.js +346 -42
  65. package/src/typechecker/init-order.js +136 -40
  66. package/src/typechecker/operand-text.js +41 -0
  67. package/src/typechecker/resolve.js +490 -65
  68. package/src/typechecker/statements.js +288 -27
  69. package/src/typechecker/stdlib/excluded.js +9 -5
  70. package/src/typechecker/stdlib/index.js +25 -9
  71. package/src/typechecker/termination.js +10 -1
  72. package/src/typechecker/types.js +287 -575
  73. package/stdlib/bytes/buffer.go +479 -0
  74. package/stdlib/bytes/bytes.go +1010 -0
  75. package/stdlib/bytes/reader.go +161 -0
  76. package/stdlib/context/context.go +115 -0
  77. package/stdlib/crypto/rand/rand.go +34 -0
  78. package/stdlib/crypto/sha256/sha256.go +249 -0
  79. package/stdlib/encoding/base64/base64.go +331 -0
  80. package/stdlib/encoding/binary/binary.go +332 -0
  81. package/stdlib/encoding/hex/hex.go +140 -0
  82. package/stdlib/encoding/json/json.go +62 -0
  83. package/stdlib/errors/errors.go +174 -0
  84. package/stdlib/fmt/errors.go +110 -0
  85. package/stdlib/fmt/format.go +732 -0
  86. package/stdlib/fmt/print.go +1232 -0
  87. package/stdlib/gom/gom.go +293 -0
  88. package/stdlib/html/html.go +251 -0
  89. package/stdlib/internal/fmtrt/fmtrt.go +163 -0
  90. package/stdlib/internal/jsonrt/dec.go +171 -38
  91. package/stdlib/internal/jsonrt/enc.go +186 -36
  92. package/stdlib/internal/jsonrt/scanner.go +17 -8
  93. package/stdlib/io/io.go +157 -1
  94. package/stdlib/maps/maps.go +131 -0
  95. package/stdlib/math/bits/bits.go +507 -0
  96. package/stdlib/math/extra.go +16 -14
  97. package/stdlib/math/math.go +439 -0
  98. package/stdlib/math/rand/rand.go +297 -0
  99. package/stdlib/os/os.go +93 -0
  100. package/stdlib/path/filepath/filepath.go +47 -0
  101. package/stdlib/path/path.go +394 -0
  102. package/stdlib/regexp/regexp.go +231 -0
  103. package/stdlib/slices/slices.go +526 -0
  104. package/stdlib/slices/sort.go +209 -0
  105. package/stdlib/slices/zsort_cmp.go +480 -0
  106. package/stdlib/slices/zsort_ordered.go +482 -0
  107. package/stdlib/sort/sort.go +391 -452
  108. package/stdlib/sort/zsort_func.go +480 -0
  109. package/stdlib/sort/zsort_iface.go +480 -0
  110. package/stdlib/strconv/float.go +1699 -0
  111. package/stdlib/strconv/isprint.go +752 -0
  112. package/stdlib/strconv/quote.go +235 -50
  113. package/stdlib/strconv/strconv.go +616 -0
  114. package/stdlib/strings/builder.go +95 -0
  115. package/stdlib/strings/reader.go +163 -0
  116. package/stdlib/strings/replacer.go +102 -0
  117. package/stdlib/strings/strings.go +807 -0
  118. package/stdlib/sync/sync.go +268 -0
  119. package/stdlib/testing/async_js.go +61 -0
  120. package/stdlib/testing/benchmark.go +297 -0
  121. package/stdlib/testing/testing.go +523 -0
  122. package/stdlib/time/duration.go +233 -0
  123. package/stdlib/time/format.go +745 -0
  124. package/stdlib/time/parse.go +790 -0
  125. package/stdlib/time/sleep_js.go +63 -0
  126. package/stdlib/time/time.go +834 -0
  127. package/stdlib/unicode/unicode.go +141 -3
  128. package/stdlib/unicode/utf8/utf8.go +464 -0
  129. package/src/backend/js/fmt-runtime.js +0 -573
  130. package/src/backend/js/stdlib/base64.js +0 -12
  131. package/src/backend/js/stdlib/binary.js +0 -102
  132. package/src/backend/js/stdlib/bits.js +0 -119
  133. package/src/backend/js/stdlib/builder.js +0 -98
  134. package/src/backend/js/stdlib/bytes.js +0 -368
  135. package/src/backend/js/stdlib/errors.js +0 -45
  136. package/src/backend/js/stdlib/fmt.js +0 -517
  137. package/src/backend/js/stdlib/gom.js +0 -155
  138. package/src/backend/js/stdlib/hex.js +0 -17
  139. package/src/backend/js/stdlib/html.js +0 -20
  140. package/src/backend/js/stdlib/io.js +0 -137
  141. package/src/backend/js/stdlib/json.js +0 -1167
  142. package/src/backend/js/stdlib/maps.js +0 -37
  143. package/src/backend/js/stdlib/math.js +0 -85
  144. package/src/backend/js/stdlib/os.js +0 -13
  145. package/src/backend/js/stdlib/path.js +0 -48
  146. package/src/backend/js/stdlib/rand.js +0 -32
  147. package/src/backend/js/stdlib/regexp.js +0 -54
  148. package/src/backend/js/stdlib/sha256.js +0 -12
  149. package/src/backend/js/stdlib/shared.js +0 -17
  150. package/src/backend/js/stdlib/simd.js +0 -34
  151. package/src/backend/js/stdlib/slices.js +0 -107
  152. package/src/backend/js/stdlib/sort.js +0 -30
  153. package/src/backend/js/stdlib/strconv.js +0 -79
  154. package/src/backend/js/stdlib/strings.js +0 -301
  155. package/src/backend/js/stdlib/sync.js +0 -13
  156. package/src/backend/js/stdlib/testing.js +0 -41
  157. package/src/backend/js/stdlib/time.js +0 -152
  158. package/src/backend/js/stdlib/unicode.js +0 -25
  159. package/src/backend/js/stdlib/utf8.js +0 -26
  160. package/src/backend/wasm/stdlib/bytes.js +0 -6716
  161. package/src/backend/wasm/stdlib/core.js +0 -10278
  162. package/src/backend/wasm/stdlib/io.js +0 -3578
  163. package/src/backend/wasm/stdlib/packages.js +0 -2402
  164. package/src/backend/wasm/stdlib/pure.js +0 -720
  165. package/src/backend/wasm/stdlib/time.js +0 -2229
  166. package/src/typechecker/stdlib/core.js +0 -1079
  167. package/src/typechecker/stdlib/extended.js +0 -1210
  168. package/stdlib/bytes/iter.go +0 -192
  169. package/stdlib/bytes/search.go +0 -240
  170. package/stdlib/bytes/split.go +0 -218
  171. package/stdlib/errors/overlay.go +0 -12
  172. package/stdlib/maps/iter.go +0 -65
  173. package/stdlib/slices/extra.go +0 -73
  174. package/stdlib/slices/iter.go +0 -113
  175. package/stdlib/strings/extra.go +0 -35
  176. package/stdlib/strings/iter.go +0 -122
  177. package/stdlib/sync/extra.go +0 -105
  178. package/stdlib/unicode/utf8/bytes.go +0 -239
@@ -1,16 +1,21 @@
1
1
  import {
2
2
  bodyRedeclares,
3
3
  forEachChild,
4
+ hasCallOrReceive,
4
5
  isIntRangeType,
5
6
  isMapRangeType,
6
7
  isRangeFor,
7
8
  isSimpleIntRange,
8
9
  isStringRangeType,
9
10
  loopVarsCapturedInLoop,
10
- nodeAssigns,
11
11
  someChild,
12
12
  } from "../../lower/index.js";
13
- import { isComplex, isTypedArraySlice } from "../../typechecker/types.js";
13
+ import {
14
+ is64BitInt,
15
+ isComplex,
16
+ isComplex64,
17
+ isTypedArraySlice,
18
+ } from "../../typechecker/types.js";
14
19
 
15
20
  /** @typedef {import('./index.js').CodeGen} CodeGen */
16
21
 
@@ -124,6 +129,20 @@ const isReusableIndex = (e, vars, boxed) => {
124
129
  }
125
130
  };
126
131
 
132
+ // Adds the name of every identifier under `n` to `into`.
133
+ const collectIdentNames = (n, into) => {
134
+ if (Array.isArray(n)) {
135
+ for (const c of n) collectIdentNames(c, into);
136
+ return;
137
+ }
138
+ if (!n || typeof n !== "object") return;
139
+ if (n.kind === "Ident") into.add(n.name);
140
+ someChild(n, (c) => {
141
+ collectIdentNames(c, into);
142
+ return false;
143
+ });
144
+ };
145
+
127
146
  // Method-name dispatch for genStmt — string values add no static call edges.
128
147
  const STMT_GEN_DELEGATE = {
129
148
  VarDecl: "genVarDecl",
@@ -135,19 +154,27 @@ const STMT_GEN_DELEGATE = {
135
154
  SwitchStmt: "genSwitch",
136
155
  TypeSwitchStmt: "genTypeSwitch",
137
156
  BranchStmt: "_genBranchStmt",
157
+ TemplMount: "_genTemplMountStmt",
138
158
  };
139
159
 
140
160
  /** @type {ThisType<CodeGen>} */
141
161
  export const statementGenMethods = {
142
162
  genBlock(block) {
143
163
  let nested = 0;
164
+ const earlier = new Set();
144
165
  for (const stmt of block.stmts) {
145
166
  if (this._definesShadowedRead(stmt)) {
146
167
  this._genShadowingDefine(stmt);
147
168
  nested++;
169
+ } else if (this._definesAfterOuterUse(stmt, earlier)) {
170
+ this.line("{");
171
+ this.indent++;
172
+ nested++;
173
+ this.genStmt(stmt);
148
174
  } else {
149
175
  this.genStmt(stmt);
150
176
  }
177
+ collectIdentNames(stmt, earlier);
151
178
  }
152
179
  for (; nested > 0; nested--) {
153
180
  this.indent--;
@@ -155,11 +182,20 @@ export const statementGenMethods = {
155
182
  }
156
183
  },
157
184
 
185
+ // `err = nil; _, err := g()` where `err` is an outer variable used earlier
186
+ // in this block: JS `let` would put those uses in its temporal dead zone,
187
+ // so the rest of the block moves into a nested block.
188
+ _definesAfterOuterUse(stmt, earlier) {
189
+ if (stmt.kind !== "DefineStmt" || earlier.size === 0) return false;
190
+ return stmt.lhs.some(
191
+ (l) => !l._redecl && l.name !== "_" && earlier.has(l.name),
192
+ );
193
+ },
194
+
158
195
  // `x := f(x)` declaring a new `x` that its own right-hand side reads (the
159
196
  // outer `x`): JS `let` would put the read in its temporal dead zone.
160
197
  _definesShadowedRead(stmt) {
161
- if (stmt.kind !== "DefineStmt" || this._isCommaOkMapExpr(stmt, stmt.rhs[0]))
162
- return false;
198
+ if (stmt.kind !== "DefineStmt") return false;
163
199
  const names = new Set(
164
200
  stmt.lhs.filter((l) => !l._redecl && l.name !== "_").map((l) => l.name),
165
201
  );
@@ -173,9 +209,35 @@ export const statementGenMethods = {
173
209
  return stmt.rhs.some(reads);
174
210
  },
175
211
 
212
+ // Emits an `if`/`switch` init statement, then `body`; an init that reads the
213
+ // name it declares goes through `_genShadowingDefine`.
214
+ _genScopedInit(init, body) {
215
+ const shadowing = this._definesShadowedRead(init);
216
+ if (shadowing) this._genShadowingDefine(init);
217
+ else this.genStmt(init);
218
+ body();
219
+ if (shadowing) {
220
+ this.indent--;
221
+ this.line("}");
222
+ }
223
+ },
224
+
176
225
  // Evaluates the right-hand side in the enclosing scope, then opens a nested
177
226
  // block (closed at the end of the enclosing block) that declares the names.
178
227
  _genShadowingDefine(stmt) {
228
+ if (this._isCommaOkMapExpr(stmt, stmt.rhs[0])) {
229
+ const node = stmt.rhs[0];
230
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
231
+ const m = `__sm${this._tmpCounter}`;
232
+ const k = `__sk${this._tmpCounter}`;
233
+ this.line(
234
+ `const ${m} = ${this.genExpr(node.expr)}, ${k} = ${this.genExpr(node.index)};`,
235
+ );
236
+ this.line("{");
237
+ this.indent++;
238
+ this._genCommaOkMapIndex(stmt, node, true, { map: m, key: k });
239
+ return;
240
+ }
179
241
  const rhs = this._defineRhs(stmt);
180
242
  this._tmpCounter = (this._tmpCounter ?? 0) + 1;
181
243
  const tmp = `__t${this._tmpCounter}`;
@@ -189,24 +251,44 @@ export const statementGenMethods = {
189
251
  );
190
252
  },
191
253
 
254
+ // `m[k]` of a non-strict comma-ok: a missing key reads as the scalar zero
255
+ // value, not `undefined`.
256
+ _nonStrictMapRead(mapExpr, keyExpr, valType, copyVal) {
257
+ const zero = valType ? this.zeroValueForType(valType) : "";
258
+ const read = `${mapExpr}[${keyExpr}]`;
259
+ if (/^(0n?|""|false)$/.test(zero)) return `(${read} ?? ${zero})`;
260
+ return copyVal(
261
+ this._aggregateBase(valType) ? `(${read} ?? ${zero})` : read,
262
+ );
263
+ },
264
+
192
265
  // Shared helper for comma-ok map index in both DefineStmt and AssignStmt.
193
- _genCommaOkMapIndex(stmt, rhsNode, isDefine) {
194
- const mapExpr = this.genExpr(rhsNode.expr);
195
- const keyExpr = this.genExpr(rhsNode.index);
266
+ // `pre` carries the map and key already evaluated (a shadowing declaration
267
+ // reads them before its names exist).
268
+ _genCommaOkMapIndex(stmt, rhsNode, isDefine, pre = null) {
269
+ const mapExpr = pre?.map ?? this.genExpr(rhsNode.expr);
270
+ const keyExpr = pre?.key ?? this.genExpr(rhsNode.index);
196
271
  const [vName, okName] = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
197
- const decl = isDefine ? "let " : "";
198
- // An array map value is a copy, not the stored array.
272
+ // A name `:=` reuses is assigned, not declared again; an address-taken
273
+ // variable lives in a `{ value }` box.
274
+ const bind = (l, name, js) => {
275
+ const boxed = this._boxedVars.has(name);
276
+ if (isDefine && !l._redecl)
277
+ return `let ${name} = ${boxed ? `{ value: ${js} }` : js};`;
278
+ return `${name}${boxed ? ".value" : ""} = ${js};`;
279
+ };
280
+ const [vL, okL] = stmt.lhs;
281
+ // A struct or array map value is a copy, not the stored value.
199
282
  const valType = rhsNode._mapValueType;
200
283
  const copyVal = (js) =>
201
- this._aggregateBase(valType)?.kind === "array"
202
- ? this._cloneJs(valType, js)
203
- : js;
284
+ this._aggregateBase(valType) ? this._cloneJs(valType, js) : js;
204
285
  if (isDefine && !this.strict) {
205
286
  if (vName !== "_") {
206
- this.line(`let ${vName} = ${copyVal(`${mapExpr}[${keyExpr}]`)};`);
287
+ const val = this._nonStrictMapRead(mapExpr, keyExpr, valType, copyVal);
288
+ this.line(bind(vL, vName, val));
207
289
  }
208
290
  if (okName !== "_") {
209
- this.line(`let ${okName} = (${keyExpr}) in ${mapExpr};`);
291
+ this.line(bind(okL, okName, `(${keyExpr}) in ${mapExpr}`));
210
292
  }
211
293
  return;
212
294
  }
@@ -219,11 +301,15 @@ export const statementGenMethods = {
219
301
  this.line(`const ${mTmp} = ${mapExpr}, ${kTmp} = ${keyExpr};`);
220
302
  if (vName !== "_") {
221
303
  this.line(
222
- `${decl}${vName} = (${mTmp} && (${kTmp} in ${mTmp})) ? ${copyVal(`${mTmp}[${kTmp}]`)} : ${zero};`,
304
+ bind(
305
+ vL,
306
+ vName,
307
+ `(${mTmp} && (${kTmp} in ${mTmp})) ? ${copyVal(`${mTmp}[${kTmp}]`)} : ${zero}`,
308
+ ),
223
309
  );
224
310
  }
225
311
  if (okName !== "_") {
226
- this.line(`${decl}${okName} = Boolean(${mTmp} && (${kTmp} in ${mTmp}));`);
312
+ this.line(bind(okL, okName, `Boolean(${mTmp} && (${kTmp} in ${mTmp}))`));
227
313
  }
228
314
  },
229
315
 
@@ -240,8 +326,11 @@ export const statementGenMethods = {
240
326
  : null;
241
327
  switch (stmt.kind) {
242
328
  case "TypeDecl":
243
- this.genTypeDeclWithMethods(stmt, []);
244
- this._noteLocalTypeDecl(stmt);
329
+ {
330
+ const start = this.out.length;
331
+ this.genTypeDeclWithMethods(stmt, []);
332
+ this._hoistLocalClass(stmt, start);
333
+ }
245
334
  break;
246
335
  case "IncDecStmt": {
247
336
  this._markIndexLvalues([stmt.expr]);
@@ -257,28 +346,47 @@ export const statementGenMethods = {
257
346
  : this.genExpr(stmt.expr.expr);
258
347
  const idx = this.genExpr(stmt.expr.index);
259
348
  const op = stmt.op === "++" ? "+" : "-";
349
+ const is64Val = is64BitInt(stmt.expr._mapValueType);
350
+ const delta = is64Val ? "1n" : "1";
260
351
  if (
261
352
  this._hasCallExpr(stmt.expr.expr) ||
262
353
  this._hasCallExpr(stmt.expr.index)
263
354
  ) {
355
+ const rhs = is64Val
356
+ ? this._gen64BitBinary(
357
+ op,
358
+ `(__m[__k] ?? ${zero})`,
359
+ delta,
360
+ stmt.expr._mapValueType,
361
+ )
362
+ : `(__m[__k] ?? ${zero}) ${op} 1`;
264
363
  this.line(
265
- `((__m, __k) => { ${this._genMapWriteBase("__m")}[__k] = (__m[__k] ?? ${zero}) ${op} 1; })(${base}, ${idx});`,
364
+ `((__m, __k) => { ${this._genMapWriteBase("__m")}[__k] = ${rhs}; })(${base}, ${idx});`,
266
365
  srcLine,
267
366
  );
268
367
  } else {
368
+ const rhs = is64Val
369
+ ? this._gen64BitBinary(
370
+ op,
371
+ `(${base}[${idx}] ?? ${zero})`,
372
+ delta,
373
+ stmt.expr._mapValueType,
374
+ )
375
+ : `(${base}[${idx}] ?? ${zero}) ${op} 1`;
269
376
  this.line(
270
- `${this._genMapWriteBase(base)}[${idx}] = (${base}[${idx}] ?? ${zero}) ${op} 1;`,
377
+ `${this._genMapWriteBase(base)}[${idx}] = ${rhs};`,
271
378
  srcLine,
272
379
  );
273
380
  }
274
381
  break;
275
382
  }
383
+ const is64 = is64BitInt(stmt.expr._type);
276
384
  if (
277
- this.strict &&
385
+ (this.strict || is64 || this._isFloat32(stmt.expr._type)) &&
278
386
  this._genStrictCompound(
279
387
  stmt.expr,
280
388
  stmt.op === "++" ? "+" : "-",
281
- "1",
389
+ is64 ? "1n" : "1",
282
390
  srcLine,
283
391
  )
284
392
  )
@@ -293,14 +401,7 @@ export const statementGenMethods = {
293
401
  if (stmt.init) {
294
402
  this.line("{");
295
403
  this.indented(() => {
296
- const shadowing = this._definesShadowedRead(stmt.init);
297
- if (shadowing) this._genShadowingDefine(stmt.init);
298
- else this.genStmt(stmt.init);
299
- this._genIf(stmt);
300
- if (shadowing) {
301
- this.indent--;
302
- this.line("}");
303
- }
404
+ this._genScopedInit(stmt.init, () => this._genIf(stmt));
304
405
  });
305
406
  this.line("}");
306
407
  } else {
@@ -308,15 +409,41 @@ export const statementGenMethods = {
308
409
  }
309
410
  break;
310
411
  case "DeferStmt":
311
- this.line(`__defers.push(() => { ${this.genExpr(stmt.call)}; });`);
412
+ // A deferred native call runs the Go body.
413
+ this._withoutNatives(() =>
414
+ this.line(`__defers.push(() => { ${this.genExpr(stmt.call)}; });`),
415
+ );
312
416
  break;
313
- case "LabeledStmt":
314
- if (stmt.body && stmt.body.kind === "ForStmt") {
315
- stmt.body.label = stmt.label;
417
+ case "LabeledStmt": {
418
+ // Stacked labels (`a: b: for ...`) all stay attached to the loop.
419
+ const labels = [stmt.label];
420
+ let body = stmt.body;
421
+ while (body?.kind === "LabeledStmt") {
422
+ labels.push(body.label);
423
+ body = body.body;
316
424
  }
317
- this.line(`${stmt.label}:`);
318
- this.genStmt(stmt.body);
425
+ if (body?.kind === "ForStmt") {
426
+ body.label = labels[labels.length - 1];
427
+ body.labels = labels;
428
+ }
429
+ const hoisted = this._hoistForInit(body);
430
+ for (const label of labels) this.line(`${label}:`);
431
+ // Labels in scope of the JS function being emitted (see
432
+ // `_emitIterTargetDispatch`).
433
+ const prevLabels = this._jsLabels ?? [];
434
+ const loop = body?.kind === "ForStmt";
435
+ this._jsLabels = [
436
+ ...prevLabels,
437
+ ...labels.map((name) => ({ name, loop })),
438
+ ];
439
+ try {
440
+ this.genStmt(body);
441
+ } finally {
442
+ this._jsLabels = prevLabels;
443
+ }
444
+ if (hoisted) this._closeHoistedForInit();
319
445
  break;
446
+ }
320
447
  case "Block":
321
448
  this.line("{");
322
449
  this.indented(() => this.genBlock(stmt));
@@ -656,6 +783,14 @@ export const statementGenMethods = {
656
783
  if (syncs) for (const s of syncs) this.line(`${s};`);
657
784
  },
658
785
 
786
+ // An untyped constant operand of a 64-bit compound assignment is a Number
787
+ // in the output; the operator needs it as a BigInt.
788
+ _untypedTo64(rhsVal, rhsNode) {
789
+ if (rhsNode?._type?.kind !== "untyped" || rhsVal.endsWith("n"))
790
+ return rhsVal;
791
+ return /^-?\d+$/.test(rhsVal) ? `${rhsVal}n` : `BigInt(${rhsVal})`;
792
+ },
793
+
659
794
  _genAssignStmt(stmt) {
660
795
  this._markIndexLvalues(stmt.lhs);
661
796
  if (stmt.op !== "=") for (const l of stmt.lhs) this._markDerefOpTarget(l);
@@ -695,6 +830,10 @@ export const statementGenMethods = {
695
830
  this._genStableWholeAssign(stmt);
696
831
  return;
697
832
  }
833
+ if (this._isStableTupleAssign(stmt)) {
834
+ this._genStableTupleAssign(stmt);
835
+ return;
836
+ }
698
837
  const paired = stmt.rhs.length === stmt.lhs.length && stmt.op === "=";
699
838
  const rhs = stmt.rhs.map((e, i) =>
700
839
  paired && stmt.lhs[i].name !== "_"
@@ -724,29 +863,51 @@ export const statementGenMethods = {
724
863
  : this.genExpr(stmt.lhs[0].expr);
725
864
  const idx = this.genExpr(stmt.lhs[0].index);
726
865
  const binOp = stmt.op.slice(0, -1);
866
+ const is64Val = is64BitInt(stmt.lhs[0]._mapValueType);
867
+ let rhsVal = active[0].r;
868
+ if (is64Val) rhsVal = this._untypedTo64(rhsVal, stmt.rhs[0]);
869
+ const rhsExpr = is64Val
870
+ ? this._gen64BitBinary(
871
+ binOp,
872
+ `(__m[__k] ?? ${zero})`,
873
+ rhsVal,
874
+ stmt.lhs[0]._mapValueType,
875
+ )
876
+ : `(__m[__k] ?? ${zero}) ${binOp === "&^" ? "& ~" : binOp} (${rhsVal})`;
877
+ const rhsExprDirect = is64Val
878
+ ? this._gen64BitBinary(
879
+ binOp,
880
+ `(${base}[${idx}] ?? ${zero})`,
881
+ rhsVal,
882
+ stmt.lhs[0]._mapValueType,
883
+ )
884
+ : `(${base}[${idx}] ?? ${zero}) ${binOp === "&^" ? "& ~" : binOp} (${rhsVal})`;
727
885
  if (
728
886
  this._hasCallExpr(stmt.lhs[0].expr) ||
729
887
  this._hasCallExpr(stmt.lhs[0].index)
730
888
  ) {
731
889
  this.line(
732
- `((__m, __k) => { ${this._genMapWriteBase("__m")}[__k] = (__m[__k] ?? ${zero}) ${binOp} (${active[0].r}); })(${base}, ${idx});`,
890
+ `((__m, __k) => { ${this._genMapWriteBase("__m")}[__k] = ${rhsExpr}; })(${base}, ${idx});`,
733
891
  );
734
892
  } else {
735
893
  this.line(
736
- `${this._genMapWriteBase(base)}[${idx}] = (${base}[${idx}] ?? ${zero}) ${binOp} (${active[0].r});`,
894
+ `${this._genMapWriteBase(base)}[${idx}] = ${rhsExprDirect};`,
737
895
  );
738
896
  }
739
897
  return;
740
898
  }
899
+ const is64 = is64BitInt(stmt.lhs[0]._type);
900
+ let rhsVal = active[0].r;
901
+ if (is64) rhsVal = this._untypedTo64(rhsVal, stmt.rhs[0]);
741
902
  if (
742
- this.strict &&
743
- stmt.op !== "=" &&
903
+ this._needsStrictCompoundStmt(stmt, is64) &&
744
904
  this._genStrictCompound(
745
905
  stmt.lhs[0],
746
906
  stmt.op.slice(0, -1),
747
- active[0].r,
907
+ rhsVal,
748
908
  null,
749
909
  active[0].l,
910
+ stmt.rhs[0]?._type,
750
911
  )
751
912
  ) {
752
913
  return;
@@ -755,7 +916,8 @@ export const statementGenMethods = {
755
916
  active[0].l.startsWith("{") && !active[0].l.startsWith("({")
756
917
  ? `(${active[0].l})`
757
918
  : active[0].l;
758
- this.line(`${targetL} ${stmt.op} ${active[0].r};`);
919
+ if (stmt.op === "&^=") this.line(`${targetL} &= ~(${rhsVal});`);
920
+ else this.line(`${targetL} ${stmt.op} ${rhsVal};`);
759
921
  } else if (hoisted) {
760
922
  this.line("{");
761
923
  this.indented(() => {
@@ -786,23 +948,28 @@ export const statementGenMethods = {
786
948
  ((n.kind === "Ident" && assigned.has(n.name)) || someChild(n, mentions));
787
949
  const lines = [];
788
950
  const undo = [];
789
- for (const l of stmt.lhs) {
951
+ const visit = (l) => {
790
952
  let keys;
791
953
  if (l.kind === "IndexExpr") keys = ["expr", "index"];
792
954
  else if (l.kind === "SelectorExpr" && !l._isMethodExpr) keys = ["expr"];
793
- else continue;
955
+ else return;
794
956
  for (const key of keys) {
795
957
  const operand = l[key];
796
958
  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;
959
+ // A struct or array value names the variable's storage in Go: hoist
960
+ // the operands inside it (`ps[n].A`) instead of the value.
961
+ if (key === "expr" && this._aggregateBase(operand._type)) {
962
+ visit(operand);
963
+ continue;
964
+ }
799
965
  this._tmpCounter = (this._tmpCounter ?? 0) + 1;
800
966
  const name = `__h${this._tmpCounter}`;
801
967
  lines.push(`const ${name} = ${this.genExpr(operand)};`);
802
968
  undo.push([l, key, operand]);
803
969
  l[key] = { kind: "Ident", name, _type: operand._type };
804
970
  }
805
- }
971
+ };
972
+ for (const l of stmt.lhs) visit(l);
806
973
  if (lines.length === 0) return null;
807
974
  return {
808
975
  lines,
@@ -814,7 +981,7 @@ export const statementGenMethods = {
814
981
 
815
982
  _genCommaOkTypeAssert(stmt) {
816
983
  const val = this.genExpr(stmt.rhs[0]);
817
- const [vName, okName] = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
984
+ const [vName, okName] = stmt.lhs.map((e) => this._genAssignLhsExpr(e));
818
985
  this._tmpCounter = (this._tmpCounter ?? 0) + 1;
819
986
  const tmp = `__ta${this._tmpCounter}`;
820
987
  this.line(`const ${tmp} = ${val};`);
@@ -828,7 +995,13 @@ export const statementGenMethods = {
828
995
  _isStableWholeAssign(stmt) {
829
996
  if (stmt.op !== "=" || stmt.lhs.length !== 1 || stmt.rhs.length !== 1)
830
997
  return false;
831
- const l = stmt.lhs[0];
998
+ return this._isStableTarget(stmt.lhs[0], stmt.rhs[0]);
999
+ },
1000
+
1001
+ // `l = rhs` writes into the existing storage of `l` (see above). When the
1002
+ // program takes `&xs[i]` anywhere, an element of a slice does too: any
1003
+ // callee may hold the pointer.
1004
+ _isStableTarget(l, rhs) {
832
1005
  if (l.kind === "IndexExpr" && l._mapValueType) return false;
833
1006
  if (
834
1007
  l.kind !== "Ident" &&
@@ -837,15 +1010,59 @@ export const statementGenMethods = {
837
1010
  )
838
1011
  return false;
839
1012
  if (l.kind === "Ident" && l.name === "_") return false;
840
- if (
841
- !this._aggregateBase(l._type) ||
842
- !this._aggregateBase(stmt.rhs[0]._type)
843
- )
1013
+ if (!this._aggregateBase(l._type) || !this._aggregateBase(rhs._type))
844
1014
  return false;
1015
+ if (this._elemAddrTaken && this._isSliceElemTarget(l)) return true;
845
1016
  const root = this._rootIdentName(l);
846
1017
  return root !== null && this._fnMutates(root, true);
847
1018
  },
848
1019
 
1020
+ // `xs[i]` on a plain slice (not a typed array, array or map).
1021
+ _isSliceElemTarget(l) {
1022
+ if (l.kind !== "IndexExpr" || l._mapValueType) return false;
1023
+ const u =
1024
+ l.expr._type?.kind === "named" ? l.expr._type.underlying : l.expr._type;
1025
+ return u?.kind === "slice" && !isTypedArraySlice(l.expr._type);
1026
+ },
1027
+
1028
+ // `a, b = x, y` where a target is stored in place: every right-hand value
1029
+ // is evaluated (and copied) first, then each target is assigned in order.
1030
+ _isStableTupleAssign(stmt) {
1031
+ return (
1032
+ stmt.op === "=" &&
1033
+ stmt.lhs.length > 1 &&
1034
+ stmt.rhs.length === stmt.lhs.length &&
1035
+ stmt.lhs.some((l, i) => this._isStableTarget(l, stmt.rhs[i]))
1036
+ );
1037
+ },
1038
+
1039
+ _genStableTupleAssign(stmt) {
1040
+ const hoisted = this._hoistLhsOperands(stmt);
1041
+ const stable = stmt.lhs.map((l, i) => this._isStableTarget(l, stmt.rhs[i]));
1042
+ this.line("{");
1043
+ this.indented(() => {
1044
+ for (const l of hoisted?.lines ?? []) this.line(l);
1045
+ const base = (this._tmpCounter ?? 0) + 1;
1046
+ this._tmpCounter = base + stmt.lhs.length;
1047
+ const val = (i) => `__s${base + i}`;
1048
+ const dst = (i) => `__d${base + i}`;
1049
+ stmt.rhs.forEach((r, i) => {
1050
+ this.line(`const ${val(i)} = ${this.genValueExpr(r)};`);
1051
+ });
1052
+ stmt.lhs.forEach((l, i) => {
1053
+ if (stable[i])
1054
+ this.line(`const ${dst(i)} = ${this._genAssignLhsExpr(l)};`);
1055
+ });
1056
+ stmt.lhs.forEach((l, i) => {
1057
+ if (stable[i]) this._emitCopyInto(dst(i), val(i), l._type);
1058
+ else if (!(l.kind === "Ident" && l.name === "_"))
1059
+ this.line(`${this._genAssignLhsExpr(l)} = ${val(i)};`);
1060
+ });
1061
+ });
1062
+ this.line("}");
1063
+ hoisted?.restore();
1064
+ },
1065
+
849
1066
  _genStableWholeAssign(stmt) {
850
1067
  let target = this._genAssignLhsExpr(stmt.lhs[0]);
851
1068
  const type = stmt.lhs[0]._type;
@@ -927,27 +1144,16 @@ export const statementGenMethods = {
927
1144
  }
928
1145
  const rhsExpr = this._genComplexOperand(stmt.rhs[0]);
929
1146
  const baseOp = stmt.op.slice(0, -1); // "+=" → "+"
930
- let result;
931
- switch (baseOp) {
932
- case "+":
933
- result = `{ re: ${lhsRead}.re + ${rhsExpr}.re, im: ${lhsRead}.im + ${rhsExpr}.im }`;
934
- break;
935
- case "-":
936
- result = `{ re: ${lhsRead}.re - ${rhsExpr}.re, im: ${lhsRead}.im - ${rhsExpr}.im }`;
937
- break;
938
- case "*":
939
- this.useHelper("cmul");
940
- result = `__cmul(${lhsRead}, ${rhsExpr})`;
941
- break;
942
- case "/":
943
- this.useHelper("cdiv");
944
- result = `__cdiv(${lhsRead}, ${rhsExpr})`;
945
- break;
946
- default:
947
- throw new Error(
948
- `unsupported complex compound assignment operator: ${stmt.op}`,
949
- );
950
- }
1147
+ if (!["+", "-", "*", "/"].includes(baseOp))
1148
+ throw new Error(
1149
+ `unsupported complex compound assignment operator: ${stmt.op}`,
1150
+ );
1151
+ const result = this._genComplexArith(
1152
+ baseOp,
1153
+ lhsRead,
1154
+ rhsExpr,
1155
+ isComplex64(l._type),
1156
+ );
951
1157
  this.line(`${lhsTarget} = ${result};`);
952
1158
  },
953
1159
 
@@ -956,10 +1162,21 @@ export const statementGenMethods = {
956
1162
  this.line(`${this._iterReturnFlag} = true; return false;`);
957
1163
  return;
958
1164
  }
959
- const vals =
1165
+ let vals =
960
1166
  stmt.values.length > 0
961
1167
  ? stmt.values.map((v) => this._genReturnValue(v))
962
1168
  : (this.namedReturnVars ?? []);
1169
+ const named = this.namedReturnVars ?? [];
1170
+ if (stmt.values.length > 0 && named.length > 0) {
1171
+ // Deferred calls read the named results after the loop returns.
1172
+ const rhs =
1173
+ vals.length === named.length && named.length > 1
1174
+ ? `[${vals.join(", ")}]`
1175
+ : vals[0];
1176
+ const lhs = named.length === 1 ? named[0] : `[${named.join(", ")}]`;
1177
+ this.line(`${lhs} = ${rhs};`);
1178
+ vals = named;
1179
+ }
963
1180
  const stored = vals.length === 1 ? vals[0] : `[${vals.join(", ")}]`;
964
1181
  this.line(
965
1182
  `${this._iterReturnFlag} = true; ${this._iterReturnVar} = ${stored}; return false;`,
@@ -1028,11 +1245,10 @@ export const statementGenMethods = {
1028
1245
  this.line(`${stmt.keyword} ${stmt.label};`);
1029
1246
  return;
1030
1247
  }
1031
- if (
1032
- stmt.keyword === "continue" &&
1033
- stmt.label === this._currentIterLoopLabel
1034
- ) {
1035
- this.line("return true;");
1248
+ if (this._currentIterLoopLabels?.includes(stmt.label)) {
1249
+ if (stmt.keyword === "continue") this.line("return true;");
1250
+ else if (stmt.keyword === "break")
1251
+ this.line(`${this._iterBreakFlag} = true; return false;`);
1036
1252
  return;
1037
1253
  }
1038
1254
  this.line(
@@ -1063,7 +1279,12 @@ export const statementGenMethods = {
1063
1279
  this.line("}");
1064
1280
  return;
1065
1281
  }
1066
- if (elseBody.kind === "IfStmt") {
1282
+ if (elseBody.kind === "IfStmt" && elseBody.init) {
1283
+ // `else if init; cond`: the init runs only when this branch is taken.
1284
+ this.line("} else {");
1285
+ this.indented(() => this.genStmt(elseBody));
1286
+ this.line("}");
1287
+ } else if (elseBody.kind === "IfStmt") {
1067
1288
  this.line(`} else if (${this.genExpr(elseBody.cond)}) {`);
1068
1289
  this.indented(() => this.genBlock(elseBody.body));
1069
1290
  this._genElse(elseBody.elseBody);
@@ -1076,7 +1297,8 @@ export const statementGenMethods = {
1076
1297
 
1077
1298
  _rangeExprLimit(iterType, iteree) {
1078
1299
  if (this._isIntRangeType(iterType)) return iteree;
1079
- if (this._isMapRangeType(iterType)) return `Object.keys(${iteree}).length`;
1300
+ if (this._isMapRangeType(iterType))
1301
+ return `Object.keys(${iteree} ?? {}).length`;
1080
1302
  return `(${iteree} ? ${iteree}.length : 0)`;
1081
1303
  },
1082
1304
 
@@ -1085,7 +1307,7 @@ export const statementGenMethods = {
1085
1307
  this.genIteratorForCond(stmt);
1086
1308
  return;
1087
1309
  }
1088
- const iterType = stmt.cond.expr._type;
1310
+ const iterType = stmt.cond.expr._coreType ?? stmt.cond.expr._type;
1089
1311
  const iteree = this.genExpr(stmt.cond.expr);
1090
1312
  const limit = this._rangeExprLimit(iterType, iteree);
1091
1313
  this.line(`for (let _$ = 0; _$ < ${limit}; _$++) {`);
@@ -1103,14 +1325,52 @@ export const statementGenMethods = {
1103
1325
  this.line("}");
1104
1326
  },
1105
1327
 
1328
+ // `for n := n; ...` declares a loop variable its own initialiser reads: the
1329
+ // initialiser values are evaluated into temporaries ahead of the loop.
1330
+ _hoistForInit(stmt) {
1331
+ const init = stmt.kind === "ForStmt" && stmt.init;
1332
+ if (
1333
+ !init ||
1334
+ stmt._initPre ||
1335
+ init.kind !== "DefineStmt" ||
1336
+ this.isRangeFor(stmt) ||
1337
+ stmt.cond?.kind === "RangeExpr" ||
1338
+ !this._definesShadowedRead(init)
1339
+ )
1340
+ return false;
1341
+ this._tmpCounter = (this._tmpCounter ?? 0) + 1;
1342
+ const values = this._defineRhs(init);
1343
+ const names = values.map((_, i) => `__fi${this._tmpCounter}_${i}`);
1344
+ this.line("{");
1345
+ this.indent++;
1346
+ this.line(
1347
+ `const ${values.map((v, i) => `${names[i]} = ${v}`).join(", ")};`,
1348
+ );
1349
+ stmt._initPre = names;
1350
+ return true;
1351
+ },
1352
+
1353
+ _closeHoistedForInit() {
1354
+ this.indent--;
1355
+ this.line("}");
1356
+ },
1357
+
1106
1358
  genFor(stmt) {
1359
+ const hoisted = this._hoistForInit(stmt);
1360
+ try {
1361
+ this._genForLoop(stmt);
1362
+ } finally {
1363
+ delete stmt._initPre;
1364
+ }
1365
+ if (hoisted) this._closeHoistedForInit();
1366
+ },
1367
+
1368
+ _genForLoop(stmt) {
1107
1369
  const isInner = this._inIteratorBody;
1108
1370
  if (isInner) {
1109
1371
  this._iterLoopDepth = (this._iterLoopDepth ?? 0) + 1;
1110
- if (stmt.label) {
1111
- this._innerLoopLabels = this._innerLoopLabels ?? new Set();
1112
- this._innerLoopLabels.add(stmt.label);
1113
- }
1372
+ this._innerLoopLabels = this._innerLoopLabels ?? new Set();
1373
+ for (const l of this._loopLabels(stmt)) this._innerLoopLabels.add(l);
1114
1374
  }
1115
1375
  try {
1116
1376
  if (this.isRangeFor(stmt)) {
@@ -1126,7 +1386,7 @@ export const statementGenMethods = {
1126
1386
  this._genSimpleWhileFor(stmt);
1127
1387
  return;
1128
1388
  }
1129
- const init = stmt.init ? this.stmtInline(stmt.init) : "";
1389
+ const init = stmt.init ? this.stmtInline(stmt.init, stmt._initPre) : "";
1130
1390
  const cond = stmt.cond ? this.genExpr(stmt.cond) : "";
1131
1391
  const post = this._perIterationClones(
1132
1392
  stmt,
@@ -1138,7 +1398,7 @@ export const statementGenMethods = {
1138
1398
  } finally {
1139
1399
  if (isInner) {
1140
1400
  this._iterLoopDepth--;
1141
- if (stmt.label) this._innerLoopLabels.delete(stmt.label);
1401
+ for (const l of this._loopLabels(stmt)) this._innerLoopLabels.delete(l);
1142
1402
  }
1143
1403
  }
1144
1404
  },
@@ -1194,10 +1454,26 @@ export const statementGenMethods = {
1194
1454
  return `${this._genAssignLhsExpr(e)} = `;
1195
1455
  },
1196
1456
 
1457
+ // The value a range assignment stores into an interface-typed variable,
1458
+ // boxed as in any conversion to an interface.
1459
+ _boxRangeAssigned(e, valueJs) {
1460
+ const box = e._rangeBox;
1461
+ if (!box) return valueJs;
1462
+ if (this._isNamedBoxKind(box))
1463
+ return this._genNamedIfaceBox(
1464
+ valueJs,
1465
+ box._ifaceNamedType,
1466
+ box._ifaceBox === "named_ptr",
1467
+ );
1468
+ this.useHelper("ifaceBox");
1469
+ return this._ifaceBoxWrap(valueJs, box._ifaceBox, box._ifacePtrFields);
1470
+ },
1471
+
1197
1472
  _emitRangeVar(e, valueJs, isAssign) {
1198
1473
  if (!e) return;
1199
1474
  const target = this._rangeVarTarget(e, isAssign);
1200
1475
  if (target === null) return;
1476
+ if (isAssign) valueJs = this._boxRangeAssigned(e, valueJs);
1201
1477
  if (typeof target === "string") {
1202
1478
  this.line(`${target}${valueJs};`);
1203
1479
  return;
@@ -1217,10 +1493,14 @@ export const statementGenMethods = {
1217
1493
  const iterType = iterNode._type;
1218
1494
  const base = iterType?.kind === "named" ? iterType.underlying : iterType;
1219
1495
  const elemType = base?.elem;
1220
- if (!valNode || !this._aggregateBase(elemType)) return js;
1221
- if (isAssign || valNode.kind !== "Ident")
1496
+ if (
1497
+ !valNode ||
1498
+ (!this._aggregateBase(elemType) && !this._typeParamClone(elemType, js))
1499
+ )
1500
+ return js;
1501
+ if (valNode.kind === "Ident" && valNode.name === "_") return js;
1502
+ if (isAssign || valNode.kind !== "Ident" || elemType?.kind === "typeParam")
1222
1503
  return this._cloneJs(elemType, js);
1223
- if (valNode.name === "_") return js;
1224
1504
  const root = this._rootIdentName(iterNode);
1225
1505
  const mutated =
1226
1506
  this._fnMutates(valNode.name) ||
@@ -1311,11 +1591,6 @@ export const statementGenMethods = {
1311
1591
  this._loopDepth--;
1312
1592
  },
1313
1593
 
1314
- // True when `node` contains an assignment or ++/-- targeting identifier `name`.
1315
- _nodeAssigns(node, name) {
1316
- return nodeAssigns(node, name);
1317
- },
1318
-
1319
1594
  _genStringRangeFor(stmt, iteree, isAssign) {
1320
1595
  this._loopDepth = this._loopDepth ?? 0;
1321
1596
  const d = this._loopDepth++;
@@ -1323,13 +1598,17 @@ export const statementGenMethods = {
1323
1598
  const s = `__s${d}`;
1324
1599
  const i = `__i${d}`;
1325
1600
  const cp = `__cp${d}`;
1601
+ const w = `__w${d}`;
1602
+ // The index is a UTF-16 offset (an astral rune is two units) so `s[i:]`
1603
+ // agrees with the WebAssembly string layout.
1326
1604
  this.line(
1327
- `for (let ${i} = 0, ${s} = Array.from(${iteree} ?? ""); ${i} < ${s}.length; ${i}++) {`,
1605
+ `for (let ${i} = 0, ${s} = ${iteree} ?? "", ${w} = 0; ${i} < ${s}.length; ${i} += ${w}) {`,
1328
1606
  );
1329
1607
  this.indented(() => {
1608
+ this.line(`const ${cp} = ${s}.codePointAt(${i});`);
1609
+ this.line(`${w} = ${cp} > 0xffff ? 2 : 1;`);
1330
1610
  this._emitRangeVar(idxNode, i, isAssign);
1331
1611
  if (valNode && valNode.name !== "_") {
1332
- this.line(`const ${cp} = ${s}[${i}].codePointAt(0);`);
1333
1612
  this._emitRangeVar(valNode, cp, isAssign);
1334
1613
  }
1335
1614
  const declared = isAssign
@@ -1349,26 +1628,102 @@ export const statementGenMethods = {
1349
1628
  this._loopDepth--;
1350
1629
  },
1351
1630
 
1352
- _genIterFor(init, iterType, iteree, lhs, body) {
1631
+ // `for _, v = range m` into an address-taken variable (or an element or
1632
+ // field) cannot destructure into the target: bind temporaries and store
1633
+ // through the variable's box after decoding. Fills `stores` with the
1634
+ // [target, temporary] pairs and returns the names to bind.
1635
+ _rangeAssignTemps(init, names, stores) {
1636
+ if (init.kind !== "AssignStmt") return names;
1637
+ // `let [..]` declares every name, so once one target needs a temporary
1638
+ // all the others go through temporaries too.
1639
+ const needs = (n, i) => {
1640
+ const e = init.lhs[i];
1641
+ return (
1642
+ n !== "_" &&
1643
+ (e._rangeBox || !(e.kind === "Ident" && !this._boxedVars.has(e.name)))
1644
+ );
1645
+ };
1646
+ const any = names.some(needs);
1647
+ return names.map((n, i) => {
1648
+ if (n === "_" || !any) return n;
1649
+ stores.push([init.lhs[i], `__r${i}$`]);
1650
+ return `__r${i}$`;
1651
+ });
1652
+ },
1653
+
1654
+ _genIterFor(init, iterType, iteree, lhsNames, body) {
1655
+ const isAssign = init.kind === "AssignStmt";
1656
+ const stores = [];
1657
+ const lhs = this._rangeAssignTemps(init, lhsNames, stores);
1353
1658
  const iterExpr = this._genRangeIterExpr(iterType, iteree, lhs);
1354
1659
  const keyDecode = this._structKeyRange(iterType);
1660
+ const paramKey = this._typeParamKeyRange(iterType);
1661
+ const is64Key = this._is64BitMapRange(iterType);
1662
+ const scalarKey = this._scalarKeyRange(iterType);
1663
+ // `for _, v = range m` assigns existing variables, where a blank is a
1664
+ // hole in the destructuring pattern; a lone blank still needs a binding.
1665
+ const blank = isAssign && lhs.length > 1 ? "" : "_$";
1355
1666
  const binding =
1356
1667
  lhs.length === 1
1357
1668
  ? lhs[0] === "_"
1358
1669
  ? "_$"
1359
1670
  : lhs[0]
1360
- : `[${lhs.map((n) => (n === "_" ? "_$" : n)).join(", ")}]`;
1361
- const kw = init.kind === "AssignStmt" ? "" : "let ";
1671
+ : `[${lhs.map((n) => (n === "_" ? blank : n)).join(", ")}]`;
1672
+ const kw = !isAssign || binding === "_$" || stores.length > 0 ? "let " : "";
1362
1673
  this.line(`for (${kw}${binding} of ${iterExpr}) {`);
1363
1674
  this.indented(() => {
1364
- if (keyDecode && lhs[0] !== "_")
1675
+ if (paramKey && lhs[0] !== "_")
1676
+ this.line(`${lhs[0]} = __kdt(${paramKey}, ${lhs[0]});`);
1677
+ else if (keyDecode && lhs[0] !== "_")
1365
1678
  this.line(`${lhs[0]} = __kd(${lhs[0]}, true);`);
1679
+ else if (is64Key && lhs[0] !== "_")
1680
+ this.line(`${lhs[0]} = BigInt(${lhs[0]});`);
1681
+ else if (scalarKey && lhs[0] !== "_")
1682
+ this.line(
1683
+ scalarKey === "bool"
1684
+ ? `${lhs[0]} = ${lhs[0]} === "true";`
1685
+ : `${lhs[0]} = Number(${lhs[0]});`,
1686
+ );
1366
1687
  this._cloneMapRangeValue(init, iterType, lhs[1]);
1688
+ for (const [e, tmp] of stores) this._emitRangeVar(e, tmp, true);
1367
1689
  this.genBlock(body);
1368
1690
  });
1369
1691
  this.line("}");
1370
1692
  },
1371
1693
 
1694
+ _is64BitMapRange(iterType) {
1695
+ if (!this._isMapRangeType(iterType)) return false;
1696
+ const base = iterType?.kind === "named" ? iterType.underlying : iterType;
1697
+ const key = base?.kind === "map" ? base.key : null;
1698
+ return is64BitInt(key);
1699
+ },
1700
+
1701
+ // Ranging over a map with plain int, float or bool keys: object property
1702
+ // names are strings, so the key variable is converted back.
1703
+ _scalarKeyRange(iterType) {
1704
+ if (!this._isMapRangeType(iterType)) return null;
1705
+ const base = iterType?.kind === "named" ? iterType.underlying : iterType;
1706
+ const key = base?.kind === "map" ? base.key : null;
1707
+ if (!key || key.kind === "typeParam") return null;
1708
+ const u = this._underlyingOf(key);
1709
+ if (u?.kind !== "basic" || this._is64BitInt(key)) return null;
1710
+ if (u.name === "bool") return "bool";
1711
+ return /^(u?int\d*|uintptr|float(32|64))$/.test(u.name) ? "num" : null;
1712
+ },
1713
+
1714
+ // Ranging over a map whose key type is a type parameter: the key is decoded
1715
+ // from its property string by the instantiated key type's zero factory.
1716
+ _typeParamKeyRange(iterType) {
1717
+ if (!this._isMapRangeType(iterType)) return null;
1718
+ const base = iterType?.kind === "named" ? iterType.underlying : iterType;
1719
+ const key = base?.kind === "map" ? base.key : null;
1720
+ if (key?.kind !== "typeParam") return null;
1721
+ const factory = this._typeParamZeroFactory(key.name);
1722
+ if (!factory) return null;
1723
+ this.useHelper("structKey");
1724
+ return factory;
1725
+ },
1726
+
1372
1727
  // Ranging over a map whose keys are structs: the key variable is decoded
1373
1728
  // from its encoded string (see HELPER_STRUCT_KEY).
1374
1729
  _structKeyRange(iterType) {
@@ -1376,7 +1731,13 @@ export const statementGenMethods = {
1376
1731
  const base = iterType?.kind === "named" ? iterType.underlying : iterType;
1377
1732
  const key = base?.kind === "map" ? base.key : null;
1378
1733
  const keyBase = key?.kind === "named" ? key.underlying : key;
1379
- if (keyBase?.kind !== "struct" && !this._isIfaceType(key)) return false;
1734
+ if (
1735
+ keyBase?.kind !== "struct" &&
1736
+ keyBase?.kind !== "array" &&
1737
+ keyBase?.kind !== "pointer" &&
1738
+ !this._isIfaceType(key)
1739
+ )
1740
+ return false;
1380
1741
  this.useHelper("structKey");
1381
1742
  return true;
1382
1743
  },
@@ -1396,7 +1757,7 @@ export const statementGenMethods = {
1396
1757
  const init = stmt.init;
1397
1758
  const range = init.rhs[0];
1398
1759
  const lhs = init.lhs.map((e) => e.name ?? this.genExpr(e));
1399
- const iterType = range.expr._type;
1760
+ const iterType = range.expr._coreType ?? range.expr._type;
1400
1761
  const wrapField = this._namedWrapperField(iterType, range.expr);
1401
1762
  const iteree = wrapField
1402
1763
  ? `${this.genExpr(range.expr)}.${wrapField}`
@@ -1431,9 +1792,22 @@ export const statementGenMethods = {
1431
1792
  !!valNode &&
1432
1793
  iterBase?.kind === "slice" &&
1433
1794
  !isTypedArraySlice(iterType);
1795
+ // Ranging over an array for its indexes only does not evaluate the
1796
+ // array (`range p` on a nil array pointer is valid): the length is the
1797
+ // type's.
1798
+ const arrayLen = this._aggregateBase(iterType)?.size;
1799
+ const indexOnlyArray =
1800
+ (!valNode || valNode.name === "_") &&
1801
+ typeof arrayLen === "number" &&
1802
+ this._aggregateBase(iterType)?.kind === "array" &&
1803
+ !hasCallOrReceive(range.expr);
1434
1804
  this._genSliceRangeFor(
1435
1805
  stmt,
1436
- rangesArrayValue ? this._cloneJs(iterType, iteree) : iteree,
1806
+ indexOnlyArray
1807
+ ? `({ length: ${arrayLen} })`
1808
+ : rangesArrayValue
1809
+ ? this._cloneJs(iterType, iteree)
1810
+ : iteree,
1437
1811
  init.kind === "AssignStmt",
1438
1812
  unwrapView,
1439
1813
  );
@@ -1441,25 +1815,49 @@ export const statementGenMethods = {
1441
1815
 
1442
1816
  _genRangeIterExpr(iterType, iteree, lhs) {
1443
1817
  if (this._isMapRangeType(iterType)) {
1444
- if (lhs.length === 1) return `Object.keys(${iteree})`;
1445
- return `Object.entries(${iteree})`;
1818
+ // A nil map is `null`; ranging over it runs zero iterations.
1819
+ if (lhs.length === 1) return `Object.keys(${iteree} ?? {})`;
1820
+ return `Object.entries(${iteree} ?? {})`;
1446
1821
  }
1447
1822
  this.useHelper("sliceGuard");
1448
1823
  if (lhs.length === 1) return `__s(${iteree}).keys()`;
1449
1824
  return `__s(${iteree}).entries()`;
1450
1825
  },
1451
1826
 
1452
- _emitIterTargetDispatch(targetFlag) {
1453
- const labels = this._functionLabels ?? [];
1827
+ // Every label of a loop: stacked labels (`a: b: for ...`) all name it.
1828
+ _loopLabels(stmt) {
1829
+ return stmt.labels ?? (stmt.label ? [stmt.label] : []);
1830
+ },
1831
+
1832
+ _emitIterTargetDispatch(targetFlag, ownLabels = []) {
1833
+ // The loop's own label is handled inside the callback (`break` and
1834
+ // `continue` of the range-over-func loop), and a JS `continue` cannot
1835
+ // name the labeled block that wraps it. Only the labels of statements
1836
+ // enclosing this loop in the same JS function are in scope (`_jsLabels`):
1837
+ // inside another iterator's callback, the enclosing iterator loop's
1838
+ // label is handled like a branch in its body, and any other label is
1839
+ // passed on to the next dispatcher.
1840
+ const inIter = this._inIteratorBody;
1841
+ const labels = (this._jsLabels ?? []).filter(
1842
+ (l) => !ownLabels.includes(l.name),
1843
+ );
1454
1844
  this.line(`if (${targetFlag}) {`);
1455
1845
  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};`);
1846
+ for (const { name, loop } of labels) {
1847
+ this.line(`if (${targetFlag} === "break$${name}") break ${name};`);
1848
+ if (loop)
1849
+ this.line(
1850
+ `if (${targetFlag} === "continue$${name}") continue ${name};`,
1851
+ );
1459
1852
  }
1460
- if (this._inIteratorBody) {
1461
- this.line(`${this._iterTargetFlag} = ${targetFlag}; return false;`);
1853
+ if (!inIter) return;
1854
+ for (const encl of this._currentIterLoopLabels ?? []) {
1855
+ this.line(`if (${targetFlag} === "continue$${encl}") return true;`);
1856
+ this.line(
1857
+ `if (${targetFlag} === "break$${encl}") { ${this._iterBreakFlag} = true; return false; }`,
1858
+ );
1462
1859
  }
1860
+ this.line(`${this._iterTargetFlag} = ${targetFlag}; return false;`);
1463
1861
  });
1464
1862
  this.line("}");
1465
1863
  },
@@ -1469,7 +1867,7 @@ export const statementGenMethods = {
1469
1867
  paramsStr,
1470
1868
  body,
1471
1869
  assignPreamble = null,
1472
- loopLabel = null,
1870
+ loopLabels = [],
1473
1871
  ) {
1474
1872
  let iteree = this.genExpr(itereeExpr);
1475
1873
  if (itereeExpr.kind === "FuncLit") {
@@ -1508,16 +1906,21 @@ export const statementGenMethods = {
1508
1906
  ret: this._iterReturnFlag,
1509
1907
  retVar: this._iterReturnVar,
1510
1908
  target: this._iterTargetFlag,
1511
- loopLabel: this._currentIterLoopLabel,
1909
+ loopLabels: this._currentIterLoopLabels,
1512
1910
  loopDepth: this._iterLoopDepth,
1911
+ innerLabels: this._innerLoopLabels,
1912
+ jsLabels: this._jsLabels,
1513
1913
  };
1514
1914
  this._inIteratorBody = true;
1515
1915
  this._iterBreakFlag = breakFlag;
1516
1916
  this._iterReturnFlag = retFlag;
1517
1917
  this._iterReturnVar = retVar;
1518
1918
  this._iterTargetFlag = targetFlag;
1519
- this._currentIterLoopLabel = loopLabel;
1919
+ this._currentIterLoopLabels = loopLabels;
1520
1920
  this._iterLoopDepth = 0;
1921
+ // Loops of an enclosing callback are not JS labels in this one.
1922
+ this._innerLoopLabels = new Set();
1923
+ this._jsLabels = [];
1521
1924
 
1522
1925
  if (assignPreamble) assignPreamble();
1523
1926
  this.genBlock(body);
@@ -1527,14 +1930,16 @@ export const statementGenMethods = {
1527
1930
  this._iterReturnFlag = prev.ret;
1528
1931
  this._iterReturnVar = prev.retVar;
1529
1932
  this._iterTargetFlag = prev.target;
1530
- this._currentIterLoopLabel = prev.loopLabel;
1933
+ this._currentIterLoopLabels = prev.loopLabels;
1531
1934
  this._iterLoopDepth = prev.loopDepth;
1935
+ this._innerLoopLabels = prev.innerLabels;
1936
+ this._jsLabels = prev.jsLabels;
1532
1937
 
1533
1938
  this.line("return true;");
1534
1939
  });
1535
1940
  this.line("});");
1536
1941
  this.line(`if (${retFlag}) return ${retVar};`);
1537
- this._emitIterTargetDispatch(targetFlag);
1942
+ this._emitIterTargetDispatch(targetFlag, loopLabels);
1538
1943
  });
1539
1944
  this.line("}");
1540
1945
  this._iterDepth--;
@@ -1543,7 +1948,8 @@ export const statementGenMethods = {
1543
1948
  genIteratorFor(stmt) {
1544
1949
  const range = stmt.init.rhs[0];
1545
1950
  const isAssign = stmt.init.kind === "AssignStmt";
1546
- const lhs = stmt.init.lhs.map((e) => e.name ?? this.genExpr(e));
1951
+ // A non-identifier target is stored through _emitRangeVar, not by name.
1952
+ const lhs = stmt.init.lhs.map((e) => e.name ?? "_$target");
1547
1953
  const yieldTypes = range._yieldParams ?? [];
1548
1954
  if (isAssign) {
1549
1955
  const cbParams = lhs.map((_, i) => `__p${i}`);
@@ -1555,12 +1961,9 @@ export const statementGenMethods = {
1555
1961
  const isStruct =
1556
1962
  this._isStructType(yType) ||
1557
1963
  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
- }
1964
+ if (isStruct) this.useHelper("sclone");
1965
+ const value = isStruct ? `__sclone(${cbParams[i]})` : cbParams[i];
1966
+ this._emitRangeVar(stmt.init.lhs[i], value, true);
1564
1967
  }
1565
1968
  }
1566
1969
  };
@@ -1569,7 +1972,7 @@ export const statementGenMethods = {
1569
1972
  params,
1570
1973
  stmt.body,
1571
1974
  assignPreamble,
1572
- stmt.label ?? null,
1975
+ this._loopLabels(stmt),
1573
1976
  );
1574
1977
  return;
1575
1978
  }
@@ -1594,7 +1997,7 @@ export const statementGenMethods = {
1594
1997
  params,
1595
1998
  stmt.body,
1596
1999
  clonePreamble,
1597
- stmt.label ?? null,
2000
+ this._loopLabels(stmt),
1598
2001
  );
1599
2002
  },
1600
2003
 
@@ -1605,15 +2008,15 @@ export const statementGenMethods = {
1605
2008
  "",
1606
2009
  stmt.body,
1607
2010
  null,
1608
- stmt.label ?? null,
2011
+ this._loopLabels(stmt),
1609
2012
  );
1610
2013
  },
1611
2014
 
1612
2015
  // Inline a statement as a for-init/post string (no semicolon)
1613
- stmtInline(stmt) {
2016
+ stmtInline(stmt, pre = null) {
1614
2017
  switch (stmt.kind) {
1615
2018
  case "DefineStmt":
1616
- return this._inlineDefine(stmt);
2019
+ return this._inlineDefine(stmt, pre);
1617
2020
  case "AssignStmt":
1618
2021
  return this._withInlineSyncs(
1619
2022
  this._resliceSyncs(stmt.lhs, stmt, true),
@@ -1623,6 +2026,14 @@ export const statementGenMethods = {
1623
2026
  const syncs = this._resliceSyncs([stmt.expr], stmt, true);
1624
2027
  this._markIndexLvalues([stmt.expr]);
1625
2028
  this._markDerefOpTarget(stmt.expr);
2029
+ if (is64BitInt(stmt.expr._type)) {
2030
+ const l = this.genExpr(stmt.expr);
2031
+ const binOp = stmt.op === "++" ? "+" : "-";
2032
+ return this._withInlineSyncs(
2033
+ syncs,
2034
+ `${l} = ${this._gen64BitBinary(binOp, l, "1n", stmt.expr._type)}`,
2035
+ );
2036
+ }
1626
2037
  return this._withInlineSyncs(
1627
2038
  syncs,
1628
2039
  `${this.genExpr(stmt.expr)}${stmt.op}`,
@@ -1639,14 +2050,14 @@ export const statementGenMethods = {
1639
2050
  return syncs ? `${js}, ${syncs.join(", ")}` : js;
1640
2051
  },
1641
2052
 
1642
- _inlineDefine(stmt) {
2053
+ _inlineDefine(stmt, pre = null) {
1643
2054
  const boxed =
1644
2055
  stmt.lhs.length === 1 &&
1645
2056
  stmt.rhs.length === 1 &&
1646
2057
  this._boxedVars.has(stmt.lhs[0].name);
1647
2058
  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));
2059
+ return `let ${stmt.lhs[0].name} = { value: ${pre ? pre[0] : this.genValueExpr(stmt.rhs[0])} }`;
2060
+ const rhs = pre ?? stmt.rhs.map((e) => this.genExpr(e));
1650
2061
  const lhs = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
1651
2062
  if (lhs.length === 1) return `let ${lhs[0]} = ${rhs[0]}`;
1652
2063
  // `for i, j := 0, 10`: one declarator per variable.
@@ -1661,11 +2072,56 @@ export const statementGenMethods = {
1661
2072
  return `let [${lhs.map((n) => (n === "_" ? "" : n)).join(", ")}] = ${rhs[0]}`;
1662
2073
  },
1663
2074
 
2075
+ // Compound assignments that need more than the JS operator: strict
2076
+ // numeric wrapping, 64-bit and float32 arithmetic, and integer `/=`.
2077
+ _needsStrictCompoundStmt(stmt, is64) {
2078
+ if (stmt.op === "=") return false;
2079
+ const t = stmt.lhs[0]._type;
2080
+ return (
2081
+ this.strict ||
2082
+ is64 ||
2083
+ this._isFloat32(t) ||
2084
+ (stmt.op === "/=" && this.isIntType(t))
2085
+ );
2086
+ },
2087
+
2088
+ // The single-expression form of `_genStrictCompound` for `for` post
2089
+ // statements: the emitted statement is lifted back out of the output.
2090
+ _inlineStrictCompound(stmt, rhsJs) {
2091
+ const mark = this.out.length;
2092
+ const handled = this._genStrictCompound(
2093
+ stmt.lhs[0],
2094
+ stmt.op.slice(0, -1),
2095
+ rhsJs,
2096
+ null,
2097
+ null,
2098
+ stmt.rhs[0]?._type,
2099
+ );
2100
+ const emitted = this.out.splice(mark).map((l) => l.trim());
2101
+ if (!handled) return null;
2102
+ const js = emitted.join(" ").replace(/;$/, "");
2103
+ return js.startsWith("{") ? `(() => ${js})()` : js;
2104
+ },
2105
+
1664
2106
  _inlineAssign(stmt) {
1665
2107
  this._markIndexLvalues(stmt.lhs);
1666
2108
  const rhs = stmt.rhs.map((e) => this.genExpr(e));
1667
- if (stmt.lhs.length === 1)
2109
+ if (stmt.lhs.length === 1) {
2110
+ if (stmt.op !== "=" && is64BitInt(stmt.lhs[0]._type)) {
2111
+ const binOp = stmt.op.slice(0, -1);
2112
+ // Same operand coercion as the statement path (`_genAssignStmt`).
2113
+ const r = this._untypedTo64(rhs[0], stmt.rhs[0]);
2114
+ const l = this.genExpr(stmt.lhs[0]);
2115
+ return `${l} = ${this._gen64BitBinary(binOp, l, r, stmt.lhs[0]._type)}`;
2116
+ }
2117
+ if (this._needsStrictCompoundStmt(stmt, is64BitInt(stmt.lhs[0]._type))) {
2118
+ const strict = this._inlineStrictCompound(stmt, rhs[0]);
2119
+ if (strict) return strict;
2120
+ }
2121
+ if (stmt.op === "&^=")
2122
+ return `${this.genExpr(stmt.lhs[0])} &= ~(${rhs[0]})`;
1668
2123
  return `${this.genExpr(stmt.lhs[0])} ${stmt.op} ${rhs[0]}`;
2124
+ }
1669
2125
  // `i, j = i+1, j-1`: every right-hand side is evaluated first.
1670
2126
  const lhs = stmt.lhs.map((e) =>
1671
2127
  e.name === "_" ? "" : this._genAssignLhsExpr(e),
@@ -1674,9 +2130,56 @@ export const statementGenMethods = {
1674
2130
  return `[${lhs.join(", ")}] = ${r}`;
1675
2131
  },
1676
2132
 
2133
+ _isMapTag(tag) {
2134
+ const t = tag._type?.kind === "named" ? tag._type.underlying : tag._type;
2135
+ return t?.kind === "map" || tag._coreType?.kind === "map";
2136
+ },
2137
+
1677
2138
  // Interface-typed tags may hold boxed named values; cases compare with __ifeq.
1678
2139
  _switchTagAndCases(stmt) {
1679
2140
  const tag = stmt.tag ? this.genExpr(stmt.tag) : "true";
2141
+ if (stmt.tag && this._isStructOrArrayType(stmt.tag._type)) {
2142
+ // Struct and array tags compare by value, not by reference.
2143
+ this.useHelper("equal");
2144
+ this._swCounter = (this._swCounter ?? 0) + 1;
2145
+ const tmp = `__sw${this._swCounter}`;
2146
+ this.line(`const ${tmp} = ${tag};`);
2147
+ return {
2148
+ tag: "true",
2149
+ caseExpr: (e) => `__equal(${tmp}, ${this.genExpr(e)})`,
2150
+ };
2151
+ }
2152
+ if (stmt.tag && isComplex(stmt.tag._type)) {
2153
+ // A complex tag is an object: cases compare its parts.
2154
+ this._swCounter = (this._swCounter ?? 0) + 1;
2155
+ const tmp = `__sw${this._swCounter}`;
2156
+ this.line(`const ${tmp} = ${tag};`);
2157
+ return {
2158
+ tag: "true",
2159
+ caseExpr: (e) =>
2160
+ this._genComplexEquality(
2161
+ "===",
2162
+ "&&",
2163
+ tmp,
2164
+ this._genComplexOperand(e),
2165
+ ),
2166
+ };
2167
+ }
2168
+ if (stmt.tag && this._isMapTag(stmt.tag)) {
2169
+ // A nil map is a sentinel object in JS, not `null`: `case nil`
2170
+ // goes through `__isnm`, as `m == nil` does.
2171
+ this.useHelper(this.strict ? "nilMap" : "nm");
2172
+ this._swCounter = (this._swCounter ?? 0) + 1;
2173
+ const tmp = `__sw${this._swCounter}`;
2174
+ this.line(`const ${tmp} = ${tag};`);
2175
+ return {
2176
+ tag: "true",
2177
+ caseExpr: (e) =>
2178
+ this._isNilLiteral(e)
2179
+ ? `__isnm(${tmp})`
2180
+ : `${tmp} === ${this.genExpr(e)}`,
2181
+ };
2182
+ }
1680
2183
  if (!stmt.tag || !this._isIfaceType(stmt.tag._type))
1681
2184
  return { tag, caseExpr: (e) => this.genExpr(e) };
1682
2185
  this.useHelper("ifeq");
@@ -1704,7 +2207,7 @@ export const statementGenMethods = {
1704
2207
  // across sibling cases (JS switch shares one block scope).
1705
2208
  this.line("{");
1706
2209
  this.indented(() => {
1707
- for (const s of c.stmts) this.genStmt(s);
2210
+ this.genBlock(c);
1708
2211
  // Go doesn't fall through by default — add break unless last stmt is return/break
1709
2212
  const last = c.stmts[c.stmts.length - 1];
1710
2213
  if (
@@ -1723,10 +2226,20 @@ export const statementGenMethods = {
1723
2226
  if (stmt.init) {
1724
2227
  this.line("{");
1725
2228
  this.indented(() => {
1726
- this.genStmt(stmt.init);
1727
- genBody();
2229
+ this._genScopedInit(stmt.init, genBody);
1728
2230
  });
1729
2231
  this.line("}");
2232
+ } else if (
2233
+ stmt.tag &&
2234
+ (this._isStructOrArrayType(stmt.tag._type) ||
2235
+ this._isIfaceType(stmt.tag._type) ||
2236
+ isComplex(stmt.tag._type) ||
2237
+ this._isMapTag(stmt.tag))
2238
+ ) {
2239
+ // The tag temporary is a `const`, which cannot directly follow a label.
2240
+ this.line("{");
2241
+ this.indented(genBody);
2242
+ this.line("}");
1730
2243
  } else {
1731
2244
  genBody();
1732
2245
  }
@@ -1737,7 +2250,13 @@ export const statementGenMethods = {
1737
2250
  this.indented(() => {
1738
2251
  const val = this.genExpr(stmt.expr);
1739
2252
  this.line(`const __tsw = ${val};`);
1740
- if (stmt.assign) this.line(`let ${stmt.assign} = __tsw;`);
2253
+ // The binding is declared per clause, after the switch expression
2254
+ // was evaluated (`switch x := any(x).(type)` reads the outer `x`).
2255
+ const boxedBind = stmt.assign && this._boxedVars.has(stmt.assign);
2256
+ const bind = (valueJs) =>
2257
+ this.line(
2258
+ `let ${stmt.assign} = ${boxedBind ? `{ value: ${valueJs} }` : valueJs};`,
2259
+ );
1741
2260
 
1742
2261
  let first = true;
1743
2262
  let hasDefault = false;
@@ -1752,24 +2271,16 @@ export const statementGenMethods = {
1752
2271
  .join(" || ");
1753
2272
  this.line(`${first ? "if" : "else if"} (${cond}) {`);
1754
2273
  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
- );
2274
+ if (stmt.assign) {
2275
+ const only = c.types.length === 1 ? c.types[0] : null;
2276
+ // A single named non-struct case binds the unboxed value.
2277
+ if (only && this._assertedValueJs(only, "__tsw") !== "__tsw")
2278
+ bind(this._assertedValueJs(only, "__tsw"));
2279
+ // An array case binds a copy of the boxed value.
2280
+ else if (only && this._typeFromNode(only)?.kind === "array")
2281
+ bind(this._cloneJs(this._typeFromNode(only), "__tsw"));
2282
+ else bind("__tsw");
2283
+ }
1773
2284
  for (const s of c.stmts) this.genStmt(s);
1774
2285
  });
1775
2286
  this.line("}");
@@ -1778,6 +2289,7 @@ export const statementGenMethods = {
1778
2289
  if (hasDefault) {
1779
2290
  this.line(first ? "{" : "else {");
1780
2291
  this.indented(() => {
2292
+ if (stmt.assign) bind("__tsw");
1781
2293
  for (const s of hasDefault.stmts) this.genStmt(s);
1782
2294
  });
1783
2295
  this.line("}");