@scriptc/compiler 0.0.36 → 0.1.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 (113) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +7 -5
  2. package/ambient/scriptc-overrides.d.ts +0 -7
  3. package/ambient/scriptc.d.ts +1 -1
  4. package/dist/backend/c/exprs.js +69 -45
  5. package/dist/backend/c/exprs.js.map +1 -1
  6. package/dist/backend/c/may-throw.js +3 -0
  7. package/dist/backend/c/may-throw.js.map +1 -1
  8. package/dist/backend/c/stmts.js +18 -0
  9. package/dist/backend/c/stmts.js.map +1 -1
  10. package/dist/backend/c/walkers.js +5 -7
  11. package/dist/backend/c/walkers.js.map +1 -1
  12. package/dist/backend/external-c.d.ts +33 -0
  13. package/dist/backend/external-c.js +51 -0
  14. package/dist/backend/external-c.js.map +1 -0
  15. package/dist/backend/link-plan.d.ts +37 -0
  16. package/dist/backend/link-plan.js +26 -0
  17. package/dist/backend/link-plan.js.map +1 -0
  18. package/dist/backend/linker.d.ts +22 -0
  19. package/dist/backend/linker.js +236 -0
  20. package/dist/backend/linker.js.map +1 -0
  21. package/dist/backend/llvm/dyn.js +7 -0
  22. package/dist/backend/llvm/dyn.js.map +1 -1
  23. package/dist/backend/llvm/emitter.js +34 -7
  24. package/dist/backend/llvm/emitter.js.map +1 -1
  25. package/dist/backend/llvm/expr-containers.js +68 -54
  26. package/dist/backend/llvm/expr-containers.js.map +1 -1
  27. package/dist/backend/llvm/expr-context.d.ts +1 -1
  28. package/dist/backend/llvm/expr-dispatch.js +2 -0
  29. package/dist/backend/llvm/expr-dispatch.js.map +1 -1
  30. package/dist/backend/llvm/expr-primitives.d.ts +1 -1
  31. package/dist/backend/llvm/expr-primitives.js +20 -0
  32. package/dist/backend/llvm/expr-primitives.js.map +1 -1
  33. package/dist/backend/llvm/expr-stream-bridges.js +25 -1
  34. package/dist/backend/llvm/expr-stream-bridges.js.map +1 -1
  35. package/dist/backend/llvm/walkers.js +31 -4
  36. package/dist/backend/llvm/walkers.js.map +1 -1
  37. package/dist/backend/native-codegen.d.ts +8 -1
  38. package/dist/backend/native-codegen.js +39 -25
  39. package/dist/backend/native-codegen.js.map +1 -1
  40. package/dist/backend/native-codegen.test.js +42 -4
  41. package/dist/backend/native-codegen.test.js.map +1 -1
  42. package/dist/backend/native-link-info.d.ts +1 -1
  43. package/dist/backend/native-link-info.js +42 -18
  44. package/dist/backend/native-link-info.js.map +1 -1
  45. package/dist/backend/native-toolchain.test.js +5 -1
  46. package/dist/backend/native-toolchain.test.js.map +1 -1
  47. package/dist/backend/runtime-pack-targets.test.d.ts +1 -0
  48. package/dist/backend/runtime-pack-targets.test.js +54 -0
  49. package/dist/backend/runtime-pack-targets.test.js.map +1 -0
  50. package/dist/backend/runtime-pack.d.ts +5 -30
  51. package/dist/backend/runtime-pack.js +46 -141
  52. package/dist/backend/runtime-pack.js.map +1 -1
  53. package/dist/backend/runtime-pack.test.js +49 -14
  54. package/dist/backend/runtime-pack.test.js.map +1 -1
  55. package/dist/backend/targets.d.ts +63 -17
  56. package/dist/backend/targets.js +251 -35
  57. package/dist/backend/targets.js.map +1 -1
  58. package/dist/backend/targets.test.js +27 -5
  59. package/dist/backend/targets.test.js.map +1 -1
  60. package/dist/coverage/surface-manifest.js +10 -1
  61. package/dist/coverage/surface-manifest.js.map +1 -1
  62. package/dist/frontend/lowering/array-values.d.ts +8 -0
  63. package/dist/frontend/lowering/array-values.js +60 -0
  64. package/dist/frontend/lowering/array-values.js.map +1 -0
  65. package/dist/frontend/lowering/lower-array-sort.d.ts +22 -0
  66. package/dist/frontend/lowering/lower-array-sort.js +619 -0
  67. package/dist/frontend/lowering/lower-array-sort.js.map +1 -0
  68. package/dist/frontend/lowering/lower-builtins.js +164 -4
  69. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  70. package/dist/frontend/lowering/lower-calls.d.ts +3 -4
  71. package/dist/frontend/lowering/lower-calls.js +260 -27
  72. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  73. package/dist/frontend/lowering/lower-containers.js +728 -655
  74. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  75. package/dist/frontend/lowering/lower-exprs.d.ts +4 -0
  76. package/dist/frontend/lowering/lower-exprs.js +769 -101
  77. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  78. package/dist/frontend/lowering/lower-inspect.js +47 -33
  79. package/dist/frontend/lowering/lower-inspect.js.map +1 -1
  80. package/dist/frontend/lowering/lower-island.d.ts +5 -4
  81. package/dist/frontend/lowering/lower-island.js +10 -5
  82. package/dist/frontend/lowering/lower-island.js.map +1 -1
  83. package/dist/frontend/lowering/lower-server.js +10 -0
  84. package/dist/frontend/lowering/lower-server.js.map +1 -1
  85. package/dist/frontend/lowering/lower-stmts.js +302 -71
  86. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  87. package/dist/frontend/lowering/lowerer.d.ts +74 -2
  88. package/dist/frontend/lowering/lowerer.js +1087 -10
  89. package/dist/frontend/lowering/lowerer.js.map +1 -1
  90. package/dist/frontend/lowering/surfaces.d.ts +10 -2
  91. package/dist/frontend/lowering/surfaces.js +15 -3
  92. package/dist/frontend/lowering/surfaces.js.map +1 -1
  93. package/dist/index.d.ts +3 -3
  94. package/dist/index.js +45 -23
  95. package/dist/index.js.map +1 -1
  96. package/dist/ir/ir.d.ts +65 -11
  97. package/dist/ir/ir.js +25 -25
  98. package/dist/ir/ir.js.map +1 -1
  99. package/dist/ir/serialize.d.ts +1 -1
  100. package/dist/ir/serialize.js +1 -1
  101. package/dist/ir/validate.js +84 -37
  102. package/dist/ir/validate.js.map +1 -1
  103. package/dist/library/fence-eval.js +3 -1
  104. package/dist/library/fence-eval.js.map +1 -1
  105. package/dist/library/int-infer.js +12 -1
  106. package/dist/library/int-infer.js.map +1 -1
  107. package/dist/library/int-infer.test.js +4 -4
  108. package/dist/library/library-cache.test.js +5 -5
  109. package/dist/startup-cache.d.ts +9 -1
  110. package/dist/startup-cache.js +16 -1
  111. package/dist/startup-cache.js.map +1 -1
  112. package/package.json +17 -4
  113. package/surface-manifest.json +5 -5
@@ -10,6 +10,8 @@ import { droppableStatic, isRequireMainFilename, lowerDynObjectLiteral, probeLow
10
10
  import { forOfVarTarget, lowerDestructuringAssign } from "./lower-stmts.js";
11
11
  import { isJsSourceFile, locOf } from "../program.js";
12
12
  import { islandPrimitiveExit, lowerDynDispatchMethodCall } from "./lower-calls.js";
13
+ import { buildArraySortFn, buildBytesSortFn } from "./lower-array-sort.js";
14
+ import { arrayValueRead, arrayValueStore, arrayValueType } from "./array-values.js";
13
15
  import { typeKey } from "../type-mapper.js";
14
16
  import { dynUndefinedExpr, own } from "./lowerer.js";
15
17
  import { boolLit, countedFor, numLit, varRef } from "../../ir/build.js";
@@ -89,10 +91,49 @@ function lowerOptionalDefaultArg(lowerer, node, expected, defaultValue) {
89
91
  }
90
92
  return lowerer.coerceInto(node, value, expected);
91
93
  }
94
+ /** Build the argument array for a mutating/copying operation while keeping
95
+ * the receiver's scalar payload ABI. A missing read is represented as the
96
+ * present UNDEFINED state in this temporary, so push/unshift and toSpliced
97
+ * materialize it through their ordinary indexed/iterator copies. */
98
+ function lowerArrayValueItems(lowerer, values, elem, arrType, loc) {
99
+ const out = lowerer.declareHiddenLocal("%arrayItems", arrType);
100
+ const outRef = varRef(out.id, arrType, loc);
101
+ const length = () => ({
102
+ kind: "arrIntrinsic",
103
+ method: "length",
104
+ receiver: outRef,
105
+ args: [],
106
+ type: F64,
107
+ loc,
108
+ });
109
+ const body = [
110
+ { kind: "varDecl", localId: out.id, init: { kind: "arrayLit", elems: [], type: arrType, loc }, loc },
111
+ ];
112
+ for (const value of values) {
113
+ const stored = lowerer.runtimeOptionalWidening(value.type, elem) !== null
114
+ ? value
115
+ : lowerer.coerceToExpected(value, elem);
116
+ body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc));
117
+ }
118
+ return { kind: "seqExpr", stmts: body, result: outRef, type: arrType, loc };
119
+ }
92
120
  function lowerSplitLimitArg(lowerer, node, loc) {
93
121
  const defaultValue = { kind: "numLit", value: 4294967295, type: F64, loc };
94
122
  return node ? lowerOptionalDefaultArg(lowerer, node, F64, defaultValue) : defaultValue;
95
123
  }
124
+ /** Extract the checker-proven receiver arm from a runtime-optional array
125
+ * read. The receiver is stabilized once before its tag is tested, and a
126
+ * missing value throws the same property-read TypeError Node throws before
127
+ * evaluating method arguments. */
128
+ function lowerMethodReceiver(lowerer, node, expected, member) {
129
+ const lowered = lowerer.lowerExpr(node);
130
+ const optional = lowerer.runtimeOptionalPropertyReceiver(node, lowered, expected, member);
131
+ if (optional)
132
+ return optional;
133
+ if (typeEquals(lowered.type, expected))
134
+ return lowered;
135
+ return lowerer.coerceInto(node, lowered, expected);
136
+ }
96
137
  /** Callback-driven array producers bypass mapType's ordinary T[] mapping:
97
138
  * the callback is lowered first, then the helper's result array is built
98
139
  * directly from its IR return type. Fence element kinds ScrArr cannot hold
@@ -227,12 +268,62 @@ export function lowerArrayMethodCall(lowerer, call, access) {
227
268
  }
228
269
  const receiver = lowerer.lowerExpr(access.expression);
229
270
  const index = lowerer.lowerExprExpecting(call.arguments[0], F64);
230
- const value = lowerer.coerceInto(call.arguments[1], lowerer.lowerExpr(call.arguments[1]), elem);
271
+ const value = lowerer.lowerExpr(call.arguments[1]);
272
+ if (value.type.kind === "union" && lowerer.runtimeOptionalWidening(value.type, elem) !== null) {
273
+ // `.with()` validates its index and returns a fresh copy. Stabilize
274
+ // all three operands, then select the scalar replacement or the
275
+ // present-undefined copy without evaluating a read twice.
276
+ const receiverLocal = lowerer.declareHiddenLocal("%withRecv", receiverIr);
277
+ const indexLocal = lowerer.declareHiddenLocal("%withIndex", F64);
278
+ const valueLocal = lowerer.declareHiddenLocal("%withValue", value.type);
279
+ const unionId = value.type.unionId;
280
+ const receiverRef = varRef(receiverLocal.id, receiverIr, loc);
281
+ const indexRef = varRef(indexLocal.id, F64, loc);
282
+ const valueRef = varRef(valueLocal.id, value.type, loc);
283
+ const undefTag = lowerer.armTag(unionId, UNDEFINED_T);
284
+ const condition = {
285
+ kind: "unionIsTag",
286
+ unionId,
287
+ tag: undefTag,
288
+ negated: false,
289
+ value: valueRef,
290
+ type: BOOL,
291
+ loc,
292
+ };
293
+ const missing = {
294
+ kind: "arrIntrinsic",
295
+ method: "withUndefined",
296
+ receiver: receiverRef,
297
+ args: [indexRef],
298
+ type: receiverIr,
299
+ loc,
300
+ };
301
+ const present = {
302
+ kind: "arrIntrinsic",
303
+ method: "with",
304
+ receiver: receiverRef,
305
+ args: [indexRef, lowerer.coerceToExpected(valueRef, elem)],
306
+ type: receiverIr,
307
+ loc,
308
+ };
309
+ return {
310
+ kind: "seqExpr",
311
+ stmts: [
312
+ { kind: "varDecl", localId: receiverLocal.id, init: receiver, loc },
313
+ { kind: "varDecl", localId: indexLocal.id, init: index, loc },
314
+ { kind: "varDecl", localId: valueLocal.id, init: value, loc },
315
+ ],
316
+ result: { kind: "ternary", cond: condition, then: missing, else_: present, type: receiverIr, loc },
317
+ type: receiverIr,
318
+ loc,
319
+ };
320
+ }
321
+ const coercedValue = lowerer.coerceInto(call.arguments[1], value, elem);
231
322
  return {
232
323
  kind: "arrIntrinsic",
233
324
  method: "with",
234
325
  receiver,
235
- args: [index, value],
326
+ args: [index, coercedValue],
236
327
  type: receiverIr,
237
328
  loc,
238
329
  };
@@ -256,12 +347,17 @@ export function lowerArrayMethodCall(lowerer, call, access) {
256
347
  : call.arguments[1]
257
348
  ? lowerOptionalDefaultArg(lowerer, call.arguments[1], F64, deleteCountDefault)
258
349
  : { kind: "numLit", value: Infinity, type: F64, loc };
259
- const items = {
260
- kind: "arrayLit",
261
- elems: call.arguments.slice(2).map((arg) => lowerer.coerceInto(arg, lowerer.lowerExpr(arg), elem)),
262
- type: receiverIr,
263
- loc,
264
- };
350
+ const itemNodes = call.arguments.slice(2);
351
+ const itemProbes = itemNodes.map((arg) => probeLower(lowerer, arg));
352
+ const statefulItems = itemProbes.some((probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null);
353
+ const items = statefulItems
354
+ ? lowerArrayValueItems(lowerer, itemNodes.map((arg) => lowerer.lowerExpr(arg)), elem, receiverIr, loc)
355
+ : {
356
+ kind: "arrayLit",
357
+ elems: itemNodes.map((arg) => lowerer.coerceInto(arg, lowerer.lowerExpr(arg), elem)),
358
+ type: receiverIr,
359
+ loc,
360
+ };
265
361
  return {
266
362
  kind: "arrIntrinsic",
267
363
  method: "toSpliced",
@@ -299,8 +395,17 @@ export function lowerArrayMethodCall(lowerer, call, access) {
299
395
  : undefined;
300
396
  lowerer.noLowering(`.${name} with ${call.arguments.length} argument${call.arguments.length === 1 ? "" : "s"}`, call, hint);
301
397
  }
302
- if (name === "push" || name === "unshift" || name === "pop") {
303
- const receiver = lowerer.lowerExpr(access.expression);
398
+ if (name === "push" || name === "unshift") {
399
+ let receiver = lowerer.lowerExpr(access.expression);
400
+ if (receiver.type.kind === "union" && lowerer.armTag(receiver.type.unionId, UNDEFINED_T) >= 0) {
401
+ const present = lowerer.stripUndefinedArm(receiver.type);
402
+ const helper = present.kind === "array"
403
+ ? lowerer.narrowedArmHelper(receiver.type.unionId, present, loc)
404
+ : null;
405
+ receiver = helper
406
+ ? { kind: "call", callee: helper, args: [receiver], type: present, loc }
407
+ : lowerer.maybeNarrow(receiver, access.expression);
408
+ }
304
409
  // `a.push(...src)` / `a.unshift(...src)`: copy src's elements in
305
410
  // order (count snapshotted first, so the self-spread forms duplicate
306
411
  // like JS) and return the new length.
@@ -326,6 +431,18 @@ export function lowerArrayMethodCall(lowerer, call, access) {
326
431
  loc,
327
432
  };
328
433
  }
434
+ const valueProbes = call.arguments.map((arg) => probeLower(lowerer, arg));
435
+ if (valueProbes.some((probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null)) {
436
+ const items = lowerArrayValueItems(lowerer, call.arguments.map((arg) => lowerer.lowerExpr(arg)), elem, receiverIr, loc);
437
+ return {
438
+ kind: "arrIntrinsic",
439
+ method: name === "push" ? "pushSpread" : "unshiftSpread",
440
+ receiver,
441
+ args: [items],
442
+ type: F64,
443
+ loc,
444
+ };
445
+ }
329
446
  // Inserted values flow into the element slot like an assignment would:
330
447
  // union-element arrays wrap plain arm values (coerceInto is inert
331
448
  // when the types already agree).
@@ -335,39 +452,31 @@ export function lowerArrayMethodCall(lowerer, call, access) {
335
452
  method: name,
336
453
  receiver,
337
454
  args,
338
- type: name === "push" || name === "unshift" ? F64 : elem,
455
+ type: F64,
339
456
  loc,
340
457
  };
341
458
  }
342
- if (name === "indexOf" || name === "includes") {
343
- // Union elements are fenced: the runtime compares ref elements by
344
- // POINTER identity (JS-exact for records/objects/arrays), but a
345
- // union BOX is a compiler artifact — two boxes of the same arm value
346
- // are distinct pointers where JS would say ===. Honest answer: none.
347
- if (elem.kind === "union") {
348
- lowerer.unsupported("SC1090", call, `'.${name}()' on union-element arrays (union values have no stable identity — ` +
349
- "loop and compare the narrowed values instead)");
459
+ if (name === "pop") {
460
+ // Like shift(), pop() returns undefined for an empty array. Union
461
+ // element arrays remain fenced here because their existing boxed
462
+ // element already carries the result union and a second undefined
463
+ // wrapper would lose the element identity contract.
464
+ if (elem.kind === "union" && lowerer.armTag(elem.unionId, UNDEFINED_T) < 0) {
465
+ lowerer.unsupported("SC1090", call, "'.pop()' on union-element arrays without an undefined arm (read the last element and splice it after narrowing instead)");
350
466
  }
351
467
  const receiver = lowerer.lowerExpr(access.expression);
352
- let needle = lowerer.lowerExpr(call.arguments[0]);
353
- // A derived CLASS VALUE against a base-classval element widens (the
354
- // same pointer — identity search is exact); the coercion path owns
355
- // the ABI gate and its pointed fences.
356
- if (needle.type.kind === "classval" && elem.kind === "classval" && !typeEquals(needle.type, elem)) {
357
- needle = lowerer.coerceInto(call.arguments[0], needle, elem);
358
- }
359
- if (!typeEquals(needle.type, elem)) {
360
- lowerer.badType(call.arguments[0], lowerer.typeOf(call.arguments[0]));
361
- }
362
468
  return {
363
469
  kind: "arrIntrinsic",
364
- method: name,
470
+ method: "pop",
365
471
  receiver,
366
- args: [needle],
367
- type: name === "indexOf" ? F64 : BOOL,
472
+ args: [],
473
+ type: arrayValueType(lowerer, elem),
368
474
  loc,
369
475
  };
370
476
  }
477
+ if (name === "indexOf" || name === "includes") {
478
+ return lowerArraySearchCall(lowerer, call, access, name, elem);
479
+ }
371
480
  if (name === "concat") {
372
481
  // `a.concat(x, ys, ...)` — a fresh array of a's elements followed by
373
482
  // each argument in order: JS spreads array arguments one level
@@ -455,11 +564,11 @@ export function lowerArrayMethodCall(lowerer, call, access) {
455
564
  // arrays are fenced: their shift result would collapse arms
456
565
  // (`(string | undefined)[]`'s shift is `string | undefined` too, and
457
566
  // the box can't say which world the undefined came from).
458
- if (elem.kind === "union") {
459
- lowerer.unsupported("SC1090", call, "'.shift()' on union-element arrays (read [0] and splice(0, 1) with the narrowed value instead)");
567
+ if (elem.kind === "union" && lowerer.armTag(elem.unionId, UNDEFINED_T) < 0) {
568
+ lowerer.unsupported("SC1090", call, "'.shift()' on union-element arrays without an undefined arm (read [0] and splice(0, 1) with the narrowed value instead)");
460
569
  }
461
570
  const receiver = lowerer.lowerExpr(access.expression);
462
- return { kind: "arrIntrinsic", method: "shift", receiver, args: [], type: lowerer.withUndefinedArm(elem), loc };
571
+ return { kind: "arrIntrinsic", method: "shift", receiver, args: [], type: arrayValueType(lowerer, elem), loc };
463
572
  }
464
573
  if (name === "join") {
465
574
  // The ambient declares join on every Array<T> (a per-element-type
@@ -496,6 +605,217 @@ export function lowerArrayMethodCall(lowerer, call, access) {
496
605
  // reduce / reduceRight
497
606
  return lowerArrayReduceCall(lowerer, call, access, name, elem);
498
607
  }
608
+ /** `indexOf` and `includes` normally use the runtime's typed search helpers,
609
+ * but an indexed array read is a `T | undefined` value. Keep that union
610
+ * through a small lowered loop so the needle's missing state is observable:
611
+ * indexOf skips holes, while includes treats holes as undefined. The same
612
+ * loop also gives union-element arrays their value equality rather than
613
+ * comparing the compiler's union boxes by pointer. */
614
+ function lowerArraySearchCall(lowerer, call, access, method, elem) {
615
+ const loc = locOf(call);
616
+ const receiver = lowerer.lowerExpr(access.expression);
617
+ const argNode = call.arguments[0];
618
+ let needle = lowerer.lowerExpr(argNode);
619
+ // A derived CLASS VALUE against a base-classval element widens (the same
620
+ // pointer — identity search is exact); the coercion path owns the ABI gate.
621
+ if (needle.type.kind === "classval" && elem.kind === "classval" && !typeEquals(needle.type, elem)) {
622
+ needle = lowerer.coerceInto(argNode, needle, elem);
623
+ }
624
+ // Preserve the existing fast path for an ordinary, non-union needle. Its
625
+ // runtime implementation already has exact primitive/reference equality.
626
+ if (elem.kind !== "union" && typeEquals(needle.type, elem)) {
627
+ return {
628
+ kind: "arrIntrinsic",
629
+ method,
630
+ receiver,
631
+ args: [needle],
632
+ type: method === "indexOf" ? F64 : BOOL,
633
+ loc,
634
+ };
635
+ }
636
+ const valueT = arrayValueType(lowerer, elem);
637
+ // jsval/dyn arrays have a single engine value representation rather than a
638
+ // static undefined arm. Keep their existing runtime path and its explicit
639
+ // type fence; typed static arrays take the state-aware helper below.
640
+ if (valueT.kind !== "union") {
641
+ if (!typeEquals(needle.type, elem))
642
+ lowerer.badType(argNode, lowerer.typeOf(argNode));
643
+ return {
644
+ kind: "arrIntrinsic",
645
+ method,
646
+ receiver,
647
+ args: [needle],
648
+ type: method === "indexOf" ? F64 : BOOL,
649
+ loc,
650
+ };
651
+ }
652
+ const searchNeedle = lowerer.coerceInto(argNode, needle, valueT);
653
+ if (!typeEquals(searchNeedle.type, valueT))
654
+ lowerer.badType(argNode, lowerer.typeOf(argNode));
655
+ const helper = arraySearchHelper(lowerer, method, elem, valueT, loc);
656
+ return { kind: "call", callee: helper, args: [receiver, searchNeedle], type: method === "indexOf" ? F64 : BOOL, loc };
657
+ }
658
+ /** SameValueZero over a static value union. `unionEq` supplies strict
659
+ * equality for every arm; the extra f64-arm test is the only difference for
660
+ * includes, where NaN equals NaN while +0 and -0 remain equal. */
661
+ function arraySearchEquality(lowerer, left, right, sameValueZero, loc) {
662
+ if (left.type.kind !== "union" || right.type.kind !== "union" || left.type.unionId !== right.type.unionId) {
663
+ return { kind: "bin", op: "===", left, right, type: BOOL, loc };
664
+ }
665
+ const unionId = left.type.unionId;
666
+ const strict = { kind: "unionEq", unionId, negated: false, sameValue: false, left, right, type: BOOL, loc };
667
+ if (!sameValueZero)
668
+ return strict;
669
+ const numberTag = lowerer.unions.get(unionId)?.arms.findIndex((arm) => arm.kind === "f64") ?? -1;
670
+ if (numberTag < 0)
671
+ return strict;
672
+ const leftIsNumber = { kind: "unionIsTag", unionId, tag: numberTag, negated: false, value: left, type: BOOL, loc };
673
+ const rightIsNumber = { kind: "unionIsTag", unionId, tag: numberTag, negated: false, value: right, type: BOOL, loc };
674
+ const leftNumber = { kind: "unionNarrow", unionId, tag: numberTag, value: left, type: F64, loc };
675
+ const rightNumber = { kind: "unionNarrow", unionId, tag: numberTag, value: right, type: F64, loc };
676
+ const bothNaN = {
677
+ kind: "logical",
678
+ op: "&&",
679
+ left: {
680
+ kind: "logical",
681
+ op: "&&",
682
+ left: leftIsNumber,
683
+ right: rightIsNumber,
684
+ type: BOOL,
685
+ loc,
686
+ },
687
+ right: {
688
+ kind: "logical",
689
+ op: "&&",
690
+ left: { kind: "libCall", fn: "num.isNaN", args: [leftNumber], type: BOOL, loc },
691
+ right: { kind: "libCall", fn: "num.isNaN", args: [rightNumber], type: BOOL, loc },
692
+ type: BOOL,
693
+ loc,
694
+ },
695
+ type: BOOL,
696
+ loc,
697
+ };
698
+ return { kind: "logical", op: "||", left: strict, right: bothNaN, type: BOOL, loc };
699
+ }
700
+ function arraySearchHelper(lowerer, method, elem, valueT, loc) {
701
+ const key = `${method}:${typeKey(elem)}:${typeKey(valueT)}`;
702
+ const existing = lowerer.arrHofHelpers.get(key);
703
+ if (existing)
704
+ return existing;
705
+ const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
706
+ lowerer.arrHofHelpers.set(key, name);
707
+ const arrT = arrayOf(elem);
708
+ const resultT = method === "indexOf" ? F64 : BOOL;
709
+ const arrRef = varRef("a.0", arrT, loc);
710
+ const indexRef = varRef("i.0", F64, loc);
711
+ const needleRef = varRef("needle.0", valueT, loc);
712
+ const stateRef = varRef("state.0", F64, loc);
713
+ const nextRef = varRef("next.0", F64, loc);
714
+ const rawRead = { kind: "arrayGet", arr: arrRef, index: indexRef, type: elem, loc };
715
+ const missing = lowerer.wrappedUndefined(valueT, loc);
716
+ if (!missing)
717
+ throw new InternalCompilerError("array search helper needs an undefined arm");
718
+ const value = {
719
+ kind: "ternary",
720
+ cond: { kind: "bin", op: "===", left: stateRef, right: numLit(1, loc), type: BOOL, loc },
721
+ then: typeEquals(elem, valueT) ? rawRead : lowerer.coerceToExpected(rawRead, valueT),
722
+ else_: missing,
723
+ type: valueT,
724
+ loc,
725
+ };
726
+ const equal = arraySearchEquality(lowerer, varRef("value.0", valueT, loc), needleRef, method === "includes", loc);
727
+ const match = method === "indexOf"
728
+ ? {
729
+ kind: "logical",
730
+ op: "&&",
731
+ left: { kind: "bin", op: "!==", left: stateRef, right: numLit(0, loc), type: BOOL, loc },
732
+ right: equal,
733
+ type: BOOL,
734
+ loc,
735
+ }
736
+ : equal;
737
+ const undefinedTag = lowerer.armTag(valueT.unionId, UNDEFINED_T);
738
+ if (undefinedTag < 0)
739
+ throw new InternalCompilerError("array search helper needs an undefined tag");
740
+ const needleIsUndefined = {
741
+ kind: "unionIsTag",
742
+ unionId: valueT.unionId,
743
+ tag: undefinedTag,
744
+ negated: false,
745
+ value: needleRef,
746
+ type: BOOL,
747
+ loc,
748
+ };
749
+ const advance = {
750
+ kind: "assign",
751
+ localId: "i.0",
752
+ value: { kind: "bin", op: "+", left: indexRef, right: numLit(1, loc), type: F64, loc },
753
+ loc,
754
+ };
755
+ const visitPresent = [
756
+ { kind: "varDecl", localId: "value.0", init: value, loc },
757
+ {
758
+ kind: "if",
759
+ cond: match,
760
+ then: [{ kind: "return", value: method === "indexOf" ? indexRef : boolLit(true, loc), loc }],
761
+ else_: null,
762
+ loc,
763
+ },
764
+ advance,
765
+ ];
766
+ const skipHole = [
767
+ {
768
+ kind: "varDecl",
769
+ localId: "next.0",
770
+ init: { kind: "arrIntrinsic", method: "nextPresent", receiver: arrRef, args: [indexRef], type: F64, loc },
771
+ loc,
772
+ },
773
+ ...(method === "includes"
774
+ ? [{ kind: "if", cond: needleIsUndefined, then: [{ kind: "return", value: boolLit(true, loc), loc }], else_: null, loc }]
775
+ : []),
776
+ { kind: "assign", localId: "i.0", value: nextRef, loc },
777
+ ];
778
+ const loopBody = [
779
+ { kind: "varDecl", localId: "state.0", init: { kind: "arrayState", arr: arrRef, index: indexRef, type: F64, loc }, loc },
780
+ {
781
+ kind: "if",
782
+ cond: { kind: "bin", op: "===", left: stateRef, right: numLit(0, loc), type: BOOL, loc },
783
+ then: skipHole,
784
+ else_: visitPresent,
785
+ loc,
786
+ },
787
+ ];
788
+ lowerer.liftedFns.push({
789
+ name,
790
+ params: [
791
+ { localId: "a.0", name: "a", type: arrT },
792
+ { localId: "needle.0", name: "needle", type: valueT },
793
+ ],
794
+ returnType: resultT,
795
+ locals: [
796
+ { id: "a.0", name: "a", type: arrT, mutable: true },
797
+ { id: "needle.0", name: "needle", type: valueT, mutable: true },
798
+ { id: "n.0", name: "n", type: F64, mutable: false },
799
+ { id: "i.0", name: "i", type: F64, mutable: true },
800
+ { id: "state.0", name: "state", type: F64, mutable: false },
801
+ { id: "next.0", name: "next", type: F64, mutable: false },
802
+ { id: "value.0", name: "value", type: valueT, mutable: false },
803
+ ],
804
+ body: [
805
+ readLenStmt(arrT, loc),
806
+ { kind: "varDecl", localId: "i.0", init: numLit(0, loc), loc },
807
+ {
808
+ kind: "while",
809
+ cond: { kind: "bin", op: "<", left: indexRef, right: varRef("n.0", F64, loc), type: BOOL, loc },
810
+ body: loopBody,
811
+ loc,
812
+ },
813
+ { kind: "return", value: method === "indexOf" ? numLit(-1, loc) : boolLit(false, loc), loc },
814
+ ],
815
+ loc,
816
+ });
817
+ return name;
818
+ }
499
819
  /** Lowers and validates a HOF callback argument. The lib declares optional
500
820
  * trailing (index, array) parameters after `lead` (the element for the
501
821
  * map family, accumulator + element for reduce); a callback may declare
@@ -572,7 +892,22 @@ function lowerArrayHofCall(lowerer, call, access, method, elem) {
572
892
  const argNode = call.arguments[0];
573
893
  if (!argNode)
574
894
  lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded
575
- const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [elem], arrayOf(elem));
895
+ const booleanFilter = method === "filter" &&
896
+ ts.isIdentifier(argNode) &&
897
+ argNode.text === "Boolean" &&
898
+ lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(argNode) ?? undefined);
899
+ const valueT = arrayValueType(lowerer, elem);
900
+ if (booleanFilter) {
901
+ if (valueT.kind === "union")
902
+ lowerer.requireTruthyUnion(valueT.unionId, argNode);
903
+ if (valueT.kind === "dyn" || valueT.kind === "jsval" || valueT.kind === "void" || isUnitType(valueT)) {
904
+ lowerer.badType(argNode, lowerer.typeOf(argNode));
905
+ }
906
+ }
907
+ const callback = booleanFilter
908
+ ? { fnArg: truthyFilterCallback(lowerer, valueT, loc), arity: 1 }
909
+ : hofCallbackArg(lowerer, argNode, [valueT], arrayOf(elem));
910
+ const { fnArg, arity } = callback;
576
911
  const fnRet = fnArg.type.ret;
577
912
  if (method === "map" && (fnRet.kind === "void" || fnRet.kind === "func")) {
578
913
  // The result array U[] is unrepresentable here (no void elements;
@@ -595,10 +930,98 @@ function lowerArrayHofCall(lowerer, call, access, method, elem) {
595
930
  }
596
931
  lowerer.badType(argNode, lowerer.typeOf(argNode));
597
932
  }
598
- const helper = arrayHofHelper(lowerer, method, elem, fnRet, arity, loc);
599
- const resultType = method === "map" ? arrayOf(fnRet) : method === "filter" ? arrayOf(elem) : VOID;
933
+ const outElem = method === "filter"
934
+ ? filterResultElem(lowerer, call, argNode, elem, valueT, booleanFilter)
935
+ : callbackArrayElem(lowerer, call, fnRet);
936
+ const helper = arrayHofHelper(lowerer, method, elem, fnRet, arity, loc, outElem);
937
+ const resultType = method === "map" || method === "filter" ? arrayOf(outElem) : VOID;
600
938
  return { kind: "call", callee: helper, args: [receiver, fnArg], type: resultType, loc };
601
939
  }
940
+ /** Preserve the checker-selected payload ABI when a callback's runtime
941
+ * return gained only undefined. The array state byte carries that value. */
942
+ function callbackArrayElem(lowerer, call, fnRet) {
943
+ const result = lowerer.mapTypeOf(lowerer.typeOf(call));
944
+ return result?.kind === "array" && lowerer.runtimeOptionalWidening(fnRet, result.elem)
945
+ ? result.elem : fnRet;
946
+ }
947
+ /** The checker-selected element of a filter result, with the same trust
948
+ * boundary as the historical narrowing filter: an inferred inline type
949
+ * predicate may prove one source arm; a written predicate annotation may
950
+ * not. Boolean is safe when every discarded arm is a nullish unit. */
951
+ function filterResultElem(lowerer, call, argNode, elem, valueT, booleanFilter) {
952
+ const callT = lowerer.typeOf(call);
953
+ const result = lowerer.mapTypeOf(callT);
954
+ let outElem = result?.kind === "array" ? result.elem : elem;
955
+ const access = ts.isPropertyAccessExpression(call.expression) ? call.expression : null;
956
+ const checkerReceiver = access
957
+ ? lowerer.mapTypeOf(lowerer.typeOf(access.expression))
958
+ : null;
959
+ // Evolving-any arrays of island handles retain their jsval element ABI,
960
+ // while the checker later describes both the receiver and unchanged
961
+ // filter result by the evolved callable type. Equal checker elements mean
962
+ // no predicate narrowing occurred; preserve the runtime element home.
963
+ if (elem.kind === "jsval" && valueT.kind === "jsval" &&
964
+ result?.kind === "array" && checkerReceiver?.kind === "array" &&
965
+ typeEquals(result.elem, checkerReceiver.elem)) {
966
+ return elem;
967
+ }
968
+ if (!typeEquals(outElem, elem)) {
969
+ const contextual = lowerer.mapTypeOf(lowerer.checker.getContextualType(call) ?? callT);
970
+ if (contextual?.kind === "array" && typeEquals(contextual.elem, elem))
971
+ outElem = elem;
972
+ }
973
+ if (typeEquals(outElem, elem))
974
+ return elem;
975
+ const annotateEscape = "keep the receiver's element type instead — annotate the callback's return ': boolean' " +
976
+ "(the checker then skips the predicate) or annotate the result with the receiver's own " +
977
+ "element type — and narrow the elements after";
978
+ if (!booleanFilter) {
979
+ if (!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) {
980
+ lowerer.unsupported("SC1090", argNode, `narrowing '.filter' through a callback VALUE (only an inline callback whose predicate ` +
981
+ `the checker inferred can re-tag — ${annotateEscape})`);
982
+ }
983
+ if (argNode.type) {
984
+ lowerer.unsupported("SC1090", argNode, `narrowing '.filter' with a hand-written type predicate (a written 'x is T' is an ` +
985
+ `unchecked assertion nothing validates at runtime — ${annotateEscape})`);
986
+ }
987
+ }
988
+ if (valueT.kind !== "union" || lowerer.armTag(valueT.unionId, outElem) < 0) {
989
+ const multiArm = outElem.kind === "union";
990
+ lowerer.unsupported("SC1090", call, `'.filter' narrowing '${lowerer.fmt(elem)}' elements to ` +
991
+ `${multiArm ? `the multi-arm '${lowerer.fmt(outElem)}'` : `'${lowerer.fmt(outElem)}'`} ` +
992
+ `(${multiArm ? "only a SINGLE arm re-tags" : "only a single runtime arm re-tags"} — ${annotateEscape})`);
993
+ }
994
+ if (booleanFilter) {
995
+ const arms = lowerer.unions.get(valueT.unionId)?.arms ?? [];
996
+ if (arms.some((arm) => !typeEquals(arm, outElem) && !isUnitType(arm))) {
997
+ lowerer.unsupported("SC1090", call, `'.filter(Boolean)' narrowing '${lowerer.fmt(elem)}' to '${lowerer.fmt(outElem)}' ` +
998
+ "when another truthy arm could survive");
999
+ }
1000
+ }
1001
+ return outElem;
1002
+ }
1003
+ /** A native closure for the ambient Boolean callback. Reifying the
1004
+ * JavaScript constructor itself is unnecessary; Array#filter only observes
1005
+ * its one-argument ToBoolean result. */
1006
+ function truthyFilterCallback(lowerer, valueT, loc) {
1007
+ const fnT = { kind: "func", params: [valueT], ret: BOOL };
1008
+ const key = `filterBoolean:${typeKey(valueT)}`;
1009
+ let name = lowerer.arrHofHelpers.get(key);
1010
+ if (!name) {
1011
+ name = `%arr.filterBoolean.${lowerer.arrHofHelpers.size}`;
1012
+ lowerer.arrHofHelpers.set(key, name);
1013
+ const value = varRef("v.0", valueT, loc);
1014
+ lowerer.liftedFns.push({
1015
+ name,
1016
+ params: [{ localId: "v.0", name: "v", type: valueT }],
1017
+ returnType: BOOL,
1018
+ locals: [{ id: "v.0", name: "v", type: valueT, mutable: false }],
1019
+ body: [{ kind: "return", value: { kind: "toBool", operand: value, type: BOOL, loc }, loc }],
1020
+ loc,
1021
+ });
1022
+ }
1023
+ return { kind: "closure", fnName: name, captures: [], type: fnT, loc };
1024
+ }
602
1025
  /** The predicate result kinds `.filter()` accepts. JS applies ToBoolean to
603
1026
  * whatever the callback answers, so a bool is not required: the scalars and
604
1027
  * the reference kinds have constant or by-value answers, and a union is fine
@@ -644,14 +1067,14 @@ function filterCond(call, fnRet, loc) {
644
1067
  * combo. Named `%arr.<method>.<n>` ('%' keeps it out of the user
645
1068
  * namespace); rides `liftedFns` into the module like a lifted lambda (it
646
1069
  * is a plain function — no captures). */
647
- function arrayHofHelper(lowerer, method, elem, fnRet, arity, loc) {
648
- const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}`;
1070
+ function arrayHofHelper(lowerer, method, elem, fnRet, arity, loc, outElem = fnRet) {
1071
+ const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}:${typeKey(outElem)}`;
649
1072
  const existing = lowerer.arrHofHelpers.get(key);
650
1073
  if (existing)
651
1074
  return existing;
652
1075
  const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
653
1076
  lowerer.arrHofHelpers.set(key, name);
654
- lowerer.liftedFns.push(buildArrayHofFn(lowerer, name, method, elem, fnRet, arity, loc));
1077
+ lowerer.liftedFns.push(buildArrayHofFn(lowerer, name, method, elem, fnRet, arity, loc, outElem));
655
1078
  return name;
656
1079
  }
657
1080
  /** READ-ONLY array methods on TUPLE receivers — `t.slice(...)` and
@@ -743,13 +1166,14 @@ export function lowerTupleReadMethodCall(lowerer, call, access) {
743
1166
  lowerer.noLowering(`.map with ${call.arguments.length} arguments`, call, "the thisArg parameter has no lowering — use an arrow function");
744
1167
  }
745
1168
  const argNode = call.arguments[0];
746
- const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [snapshotElem], arrayOf(snapshotElem));
1169
+ const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [arrayValueType(lowerer, snapshotElem)], arrayOf(snapshotElem));
747
1170
  const fnRet = fnArg.type.ret;
748
1171
  if (fnRet.kind === "void" || fnRet.kind === "func")
749
1172
  lowerer.badType(call, lowerer.typeOf(call));
750
1173
  fenceProducedArrayElem(lowerer, call, "'.map()'", fnRet);
751
- const helper = arrayHofHelper(lowerer, "map", snapshotElem, fnRet, arity, loc);
752
- return { kind: "call", callee: helper, args: [snapshot, fnArg], type: arrayOf(fnRet), loc };
1174
+ const outElem = callbackArrayElem(lowerer, call, fnRet);
1175
+ const helper = arrayHofHelper(lowerer, "map", snapshotElem, fnRet, arity, loc, outElem);
1176
+ return { kind: "call", callee: helper, args: [snapshot, fnArg], type: arrayOf(outElem), loc };
753
1177
  }
754
1178
  /** An OOB-SAFE indexed read: `xs[i]`
755
1179
  * answers the interned `elem | undefined` union — the element when `i`
@@ -764,54 +1188,13 @@ export function lowerSafeIndexRead(lowerer, arr, index, loc) {
764
1188
  const elem = arr.type.elem;
765
1189
  if (elem.kind === "void" || elem.kind === "dyn")
766
1190
  return null;
767
- const resultT = elem.kind === "union" ? lowerer.withUndefinedArmOf(elem) : lowerer.withUndefinedArm(elem);
768
- if (!resultT || resultT.kind !== "union")
769
- return null;
770
- const undefTag = lowerer.armTag(resultT.unionId, UNDEFINED_T);
771
- const foundTag = elem.kind === "union" ? -1 : lowerer.armTag(resultT.unionId, elem);
772
- if (undefTag < 0 || (elem.kind !== "union" && foundTag < 0))
773
- return null;
774
- const arrT = arr.type;
1191
+ const resultT = arrayValueType(lowerer, elem);
775
1192
  const key = `idxOr:${typeKey(elem)}`;
776
1193
  let name = lowerer.arrHofHelpers.get(key);
777
1194
  if (!name) {
778
1195
  name = `%arr.idxOr.${lowerer.arrHofHelpers.size}`;
779
1196
  lowerer.arrHofHelpers.set(key, name);
780
- const i = varRef("i.0", F64, loc);
781
- const n = varRef("n.0", F64, loc);
782
- const v = varRef("v.0", elem, loc);
783
- const lt = (l, r) => ({ kind: "bin", op: "<", left: l, right: r, type: BOOL, loc });
784
- const miss = {
785
- kind: "unionWrap",
786
- unionId: resultT.unionId,
787
- tag: undefTag,
788
- value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
789
- type: resultT,
790
- loc,
791
- };
792
- const body = [
793
- readLenStmt(arrT, loc),
794
- { kind: "if", cond: { kind: "bin", op: "!==", left: i, right: i, type: BOOL, loc }, then: [{ kind: "return", value: miss, loc }], else_: null, loc },
795
- { kind: "if", cond: lt(i, numLit(0, loc)), then: [{ kind: "return", value: miss, loc }], else_: null, loc },
796
- { kind: "if", cond: { kind: "bin", op: ">=", left: i, right: n, type: BOOL, loc }, then: [{ kind: "return", value: miss, loc }], else_: null, loc },
797
- // A fractional index is a property miss too (floor(i) < i exactly
798
- // when i has a fraction; negatives already returned above).
799
- {
800
- kind: "if",
801
- cond: lt({ kind: "libCall", fn: "math.floor", args: [i], type: F64, loc }, i),
802
- then: [{ kind: "return", value: miss, loc }],
803
- else_: null,
804
- loc,
805
- },
806
- { kind: "varDecl", localId: "v.0", init: { kind: "arrayGet", arr: varRef("a.0", arrT, loc), index: i, type: elem, loc }, loc },
807
- {
808
- kind: "return",
809
- value: elem.kind === "union"
810
- ? lowerer.coerceToExpected(v, resultT)
811
- : { kind: "unionWrap", unionId: resultT.unionId, tag: foundTag, value: v, type: resultT, loc },
812
- loc,
813
- },
814
- ];
1197
+ const arrT = arr.type;
815
1198
  lowerer.liftedFns.push({
816
1199
  name,
817
1200
  params: [
@@ -820,12 +1203,10 @@ export function lowerSafeIndexRead(lowerer, arr, index, loc) {
820
1203
  ],
821
1204
  returnType: resultT,
822
1205
  locals: [
823
- { id: "a.0", name: "a", type: arrT, mutable: true },
824
- { id: "i.0", name: "i", type: F64, mutable: true },
825
- { id: "n.0", name: "n", type: F64, mutable: false },
826
- { id: "v.0", name: "v", type: elem, mutable: false },
1206
+ { id: "a.0", name: "a", type: arrT, mutable: false },
1207
+ { id: "i.0", name: "i", type: F64, mutable: false },
827
1208
  ],
828
- body,
1209
+ body: [{ kind: "return", value: arrayValueRead(lowerer, varRef("a.0", arrT, loc), varRef("i.0", F64, loc), elem, loc), loc }],
829
1210
  loc,
830
1211
  });
831
1212
  }
@@ -859,7 +1240,7 @@ function arrayConcatHelper(lowerer, elem, shape, loc) {
859
1240
  kind: "exprStmt",
860
1241
  expr: {
861
1242
  kind: "arrIntrinsic",
862
- method: s === "a" ? "pushSpread" : "push",
1243
+ method: s === "a" ? "concatSpread" : "push",
863
1244
  receiver: varRef("out.0", arrT, loc),
864
1245
  args: [src],
865
1246
  type: F64,
@@ -887,9 +1268,10 @@ function arrayConcatHelper(lowerer, elem, shape, loc) {
887
1268
  * `arity` is the callback's declared parameter count (1–3): the loop
888
1269
  * passes the index and the receiver itself after the element when the
889
1270
  * callback names them, exactly the arguments JS supplies. */
890
- function buildArrayHofFn(lowerer, name, method, elem, fnRet, arity, loc) {
1271
+ function buildArrayHofFn(lowerer, name, method, elem, fnRet, arity, loc, outElem) {
891
1272
  const arrT = arrayOf(elem);
892
- const fnT = funcOf([elem, F64, arrT].slice(0, arity), fnRet);
1273
+ const valueT = arrayValueType(lowerer, elem);
1274
+ const fnT = funcOf([arrayValueType(lowerer, elem), F64, arrT].slice(0, arity), fnRet);
893
1275
  const locals = [
894
1276
  { id: "a.0", name: "a", type: arrT, mutable: true },
895
1277
  { id: "f.0", name: "f", type: fnT, mutable: true },
@@ -907,25 +1289,14 @@ function buildArrayHofFn(lowerer, name, method, elem, fnRet, arity, loc) {
907
1289
  type: fnRet,
908
1290
  loc,
909
1291
  });
910
- const getElem = {
911
- kind: "arrayGet",
1292
+ const getElem = arrayValueRead(lowerer, varRef("a.0", arrT, loc), varRef("i.0", F64, loc), elem, loc);
1293
+ const hasElem = {
1294
+ kind: "arrayHas",
912
1295
  arr: varRef("a.0", arrT, loc),
913
1296
  index: varRef("i.0", F64, loc),
914
- type: elem,
1297
+ type: BOOL,
915
1298
  loc,
916
1299
  };
917
- const push = (outT, value) => ({
918
- kind: "exprStmt",
919
- expr: {
920
- kind: "arrIntrinsic",
921
- method: "push",
922
- receiver: varRef("out.0", outT, loc),
923
- args: [value],
924
- type: F64,
925
- loc,
926
- },
927
- loc,
928
- });
929
1300
  const readLen = {
930
1301
  kind: "varDecl",
931
1302
  localId: "n.0",
@@ -935,41 +1306,85 @@ function buildArrayHofFn(lowerer, name, method, elem, fnRet, arity, loc) {
935
1306
  const forLoop = (body) => countedFor(loc, varRef("n.0", F64, loc), () => body);
936
1307
  let returnType;
937
1308
  let body;
938
- if (method === "map") {
939
- const outT = arrayOf(fnRet);
940
- locals.push({ id: "out.0", name: "out", type: outT, mutable: false });
1309
+ if (method === "map" || method === "flatMap") {
1310
+ const outT = arrayOf(outElem);
1311
+ locals.push({ id: "out.0", name: "out", type: outT, mutable: false }, { id: "mapped.0", name: "mapped", type: fnRet, mutable: false });
941
1312
  returnType = outT;
942
1313
  body = [
943
1314
  { kind: "varDecl", localId: "out.0", init: { kind: "arrayLit", elems: [], type: outT, loc }, loc },
944
1315
  readLen,
945
- forLoop([push(outT, callF(getElem))]),
1316
+ ...(method === "map" ? [{ kind: "arraySetLength", arr: varRef("out.0", outT, loc), length: varRef("n.0", F64, loc), loc }] : []),
1317
+ forLoop([
1318
+ {
1319
+ kind: "if",
1320
+ cond: hasElem,
1321
+ then: [
1322
+ { kind: "varDecl", localId: "mapped.0", init: callF(getElem), loc },
1323
+ arrayValueStore(lowerer, varRef("out.0", outT, loc), method === "map" ? varRef("i.0", F64, loc) : { kind: "arrIntrinsic", method: "length", receiver: varRef("out.0", outT, loc), args: [], type: F64, loc }, varRef("mapped.0", fnRet, loc), outElem, loc),
1324
+ ],
1325
+ else_: null,
1326
+ loc,
1327
+ },
1328
+ ]),
946
1329
  { kind: "return", value: varRef("out.0", outT, loc), loc },
947
1330
  ];
948
1331
  }
949
1332
  else if (method === "filter") {
950
- locals.push({ id: "out.0", name: "out", type: arrT, mutable: false }, { id: "v.0", name: "v", type: elem, mutable: false });
951
- returnType = arrT;
1333
+ const outT = arrayOf(outElem);
1334
+ locals.push({ id: "out.0", name: "out", type: outT, mutable: false }, { id: "v.0", name: "v", type: valueT, mutable: false });
1335
+ returnType = outT;
1336
+ const retained = typeEquals(outElem, elem)
1337
+ ? varRef("v.0", valueT, loc)
1338
+ : valueT.kind === "union"
1339
+ ? {
1340
+ kind: "unionNarrow",
1341
+ unionId: valueT.unionId,
1342
+ tag: lowerer.armTag(valueT.unionId, outElem),
1343
+ value: varRef("v.0", valueT, loc),
1344
+ type: outElem,
1345
+ loc,
1346
+ }
1347
+ : varRef("v.0", valueT, loc);
952
1348
  body = [
953
- { kind: "varDecl", localId: "out.0", init: { kind: "arrayLit", elems: [], type: arrT, loc }, loc },
1349
+ { kind: "varDecl", localId: "out.0", init: { kind: "arrayLit", elems: [], type: outT, loc }, loc },
954
1350
  readLen,
955
1351
  forLoop([
956
- { kind: "varDecl", localId: "v.0", init: getElem, loc },
957
1352
  {
958
1353
  kind: "if",
959
- // ToBoolean over the predicate's answer — inert when it already
960
- // returned bool, the per-union helper when it returned a union.
961
- cond: filterCond(callF(varRef("v.0", elem, loc)), fnRet, loc),
962
- then: [push(arrT, varRef("v.0", elem, loc))],
1354
+ cond: hasElem,
1355
+ then: [
1356
+ { kind: "varDecl", localId: "v.0", init: getElem, loc },
1357
+ {
1358
+ kind: "if",
1359
+ // ToBoolean over the predicate's answer — inert when it already
1360
+ // returned bool, the per-union helper when it returned a union.
1361
+ cond: filterCond(callF(varRef("v.0", valueT, loc)), fnRet, loc),
1362
+ then: [arrayValueStore(lowerer, varRef("out.0", outT, loc), { kind: "arrIntrinsic", method: "length", receiver: varRef("out.0", outT, loc), args: [], type: F64, loc }, retained, outElem, loc)],
1363
+ else_: null,
1364
+ loc,
1365
+ },
1366
+ ],
963
1367
  else_: null,
964
1368
  loc,
965
1369
  },
966
1370
  ]),
967
- { kind: "return", value: varRef("out.0", arrT, loc), loc },
1371
+ { kind: "return", value: varRef("out.0", outT, loc), loc },
968
1372
  ];
969
1373
  }
970
1374
  else {
971
1375
  returnType = VOID;
972
- body = [readLen, forLoop([{ kind: "exprStmt", expr: callF(getElem), loc }])];
1376
+ body = [
1377
+ readLen,
1378
+ forLoop([
1379
+ {
1380
+ kind: "if",
1381
+ cond: hasElem,
1382
+ then: [{ kind: "exprStmt", expr: callF(getElem), loc }],
1383
+ else_: null,
1384
+ loc,
1385
+ },
1386
+ ]),
1387
+ ];
973
1388
  }
974
1389
  return { name, params, returnType, locals, body, loc };
975
1390
  }
@@ -992,7 +1407,7 @@ function lowerArrayFindLikeCall(lowerer, call, access, method, elem) {
992
1407
  const argNode = call.arguments[0];
993
1408
  if (!argNode)
994
1409
  lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded
995
- const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [elem], arrT);
1410
+ const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [arrayValueType(lowerer, elem)], arrT);
996
1411
  // JS takes the ToBoolean of the predicate's result — allowed wherever
997
1412
  // that ToBoolean has a static answer (bool passes through; f64/string
998
1413
  // by value; a truthy-answerable union by its arm — the
@@ -1028,15 +1443,7 @@ function lowerArrayFindLikeCall(lowerer, call, access, method, elem) {
1028
1443
  const undefTag = lowerer.armTag(resultT.unionId, UNDEFINED_T);
1029
1444
  if (undefTag < 0)
1030
1445
  lowerer.badType(call, lowerer.typeOf(call));
1031
- let foundTag = null;
1032
- if (!typeEquals(resultT, elem)) {
1033
- foundTag = lowerer.armTag(resultT.unionId, elem);
1034
- if (foundTag < 0) {
1035
- lowerer.unsupported("SC1090", call, `'.${method}' on '${lowerer.fmt(elem)}'-element arrays (the found element would need a union ` +
1036
- "re-tag that is not supported yet — loop and test instead)");
1037
- }
1038
- }
1039
- const helper = findHelper(lowerer, elem, resultT, foundTag, undefTag, fnRet, arity, last, loc);
1446
+ const helper = findHelper(lowerer, elem, resultT, undefTag, fnRet, arity, last, loc);
1040
1447
  return { kind: "call", callee: helper, args: [receiver, fnArg], type: resultT, loc };
1041
1448
  }
1042
1449
  /** ToBoolean of a predicate result inside a synthesized HOF helper — the
@@ -1053,7 +1460,7 @@ function predToBool(e) {
1053
1460
  * for (i = 0; i < n; i++) { v = a[i]; if (f(v)) return <v as result arm>; }
1054
1461
  * return <undefined arm>;
1055
1462
  */
1056
- function findHelper(lowerer, elem, resultT, foundTag, undefTag, fnRet, arity, last, loc) {
1463
+ function findHelper(lowerer, elem, resultT, undefTag, fnRet, arity, last, loc) {
1057
1464
  const method = last ? "findLast" : "find";
1058
1465
  const key = `${method}:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}`;
1059
1466
  const existing = lowerer.arrHofHelpers.get(key);
@@ -1062,11 +1469,10 @@ function findHelper(lowerer, elem, resultT, foundTag, undefTag, fnRet, arity, la
1062
1469
  const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
1063
1470
  lowerer.arrHofHelpers.set(key, name);
1064
1471
  const arrT = arrayOf(elem);
1065
- const fnT = funcOf([elem, F64, arrT].slice(0, arity), fnRet);
1066
- const v = varRef("v.0", elem, loc);
1067
- const found = foundTag === null
1068
- ? v
1069
- : { kind: "unionWrap", unionId: resultT.unionId, tag: foundTag, value: v, type: resultT, loc };
1472
+ const fnT = funcOf([arrayValueType(lowerer, elem), F64, arrT].slice(0, arity), fnRet);
1473
+ const valueT = arrayValueType(lowerer, elem);
1474
+ const v = varRef("v.0", valueT, loc);
1475
+ const found = lowerer.coerceToExpected(v, resultT);
1070
1476
  const miss = {
1071
1477
  kind: "unionWrap",
1072
1478
  unionId: resultT.unionId,
@@ -1075,8 +1481,8 @@ function findHelper(lowerer, elem, resultT, foundTag, undefTag, fnRet, arity, la
1075
1481
  type: resultT,
1076
1482
  loc,
1077
1483
  };
1078
- const visit = [
1079
- { kind: "varDecl", localId: "v.0", init: getElemExpr(arrT, elem, loc), loc },
1484
+ const visitBody = [
1485
+ { kind: "varDecl", localId: "v.0", init: arrayValueRead(lowerer, varRef("a.0", arrT, loc), varRef("i.0", F64, loc), elem, loc), loc },
1080
1486
  {
1081
1487
  kind: "if",
1082
1488
  cond: predToBool({
@@ -1091,6 +1497,7 @@ function findHelper(lowerer, elem, resultT, foundTag, undefTag, fnRet, arity, la
1091
1497
  loc,
1092
1498
  },
1093
1499
  ];
1500
+ const visit = visitBody;
1094
1501
  const loop = last
1095
1502
  ? reverseCountedForLoop(loc, visit)
1096
1503
  : countedFor(loc, varRef("n.0", F64, loc), () => visit);
@@ -1111,7 +1518,7 @@ function findHelper(lowerer, elem, resultT, foundTag, undefTag, fnRet, arity, la
1111
1518
  { id: "f.0", name: "f", type: fnT, mutable: true },
1112
1519
  { id: "n.0", name: "n", type: F64, mutable: false },
1113
1520
  { id: "i.0", name: "i", type: F64, mutable: true },
1114
- { id: "v.0", name: "v", type: elem, mutable: false },
1521
+ { id: "v.0", name: "v", type: valueT, mutable: false },
1115
1522
  ],
1116
1523
  body,
1117
1524
  loc,
@@ -1132,13 +1539,13 @@ function findIndexHelper(lowerer, elem, fnRet, arity, last, loc) {
1132
1539
  const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
1133
1540
  lowerer.arrHofHelpers.set(key, name);
1134
1541
  const arrT = arrayOf(elem);
1135
- const fnT = funcOf([elem, F64, arrT].slice(0, arity), fnRet);
1542
+ const fnT = funcOf([arrayValueType(lowerer, elem), F64, arrT].slice(0, arity), fnRet);
1136
1543
  const visit = [{
1137
1544
  kind: "if",
1138
1545
  cond: predToBool({
1139
1546
  kind: "callValue",
1140
1547
  callee: varRef("f.0", fnT, loc),
1141
- args: [getElemExpr(arrT, elem, loc), varRef("i.0", F64, loc), varRef("a.0", arrT, loc)].slice(0, arity),
1548
+ args: [arrayValueRead(lowerer, varRef("a.0", arrT, loc), varRef("i.0", F64, loc), elem, loc), varRef("i.0", F64, loc), varRef("a.0", arrT, loc)].slice(0, arity),
1142
1549
  type: fnRet,
1143
1550
  loc,
1144
1551
  }),
@@ -1185,11 +1592,11 @@ function someEveryHelper(lowerer, method, elem, fnRet, arity, loc) {
1185
1592
  const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
1186
1593
  lowerer.arrHofHelpers.set(key, name);
1187
1594
  const arrT = arrayOf(elem);
1188
- const fnT = funcOf([elem, F64, arrT].slice(0, arity), fnRet);
1595
+ const fnT = funcOf([arrayValueType(lowerer, elem), F64, arrT].slice(0, arity), fnRet);
1189
1596
  const callF = predToBool({
1190
1597
  kind: "callValue",
1191
1598
  callee: varRef("f.0", fnT, loc),
1192
- args: [getElemExpr(arrT, elem, loc), varRef("i.0", F64, loc), varRef("a.0", arrT, loc)].slice(0, arity),
1599
+ args: [arrayValueRead(lowerer, varRef("a.0", arrT, loc), varRef("i.0", F64, loc), elem, loc), varRef("i.0", F64, loc), varRef("a.0", arrT, loc)].slice(0, arity),
1193
1600
  type: fnRet,
1194
1601
  loc,
1195
1602
  });
@@ -1198,8 +1605,14 @@ function someEveryHelper(lowerer, method, elem, fnRet, arity, loc) {
1198
1605
  countedFor(loc, varRef("n.0", F64, loc), () => [
1199
1606
  {
1200
1607
  kind: "if",
1201
- cond: method === "some" ? callF : { kind: "unary", op: "!", operand: callF, type: BOOL, loc },
1202
- then: [{ kind: "return", value: boolLit(method === "some", loc), loc }],
1608
+ cond: getHasExpr(arrT, loc),
1609
+ then: [{
1610
+ kind: "if",
1611
+ cond: method === "some" ? callF : { kind: "unary", op: "!", operand: callF, type: BOOL, loc },
1612
+ then: [{ kind: "return", value: boolLit(method === "some", loc), loc }],
1613
+ else_: null,
1614
+ loc,
1615
+ }],
1203
1616
  else_: null,
1204
1617
  loc,
1205
1618
  },
@@ -1237,7 +1650,7 @@ function lowerArrayFlatMapCall(lowerer, call, access, elem) {
1237
1650
  const argNode = call.arguments[0];
1238
1651
  if (!argNode)
1239
1652
  lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded
1240
- const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [elem], arrT);
1653
+ const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [arrayValueType(lowerer, elem)], arrT);
1241
1654
  const fnRet = fnArg.type.ret;
1242
1655
  if (fnRet.kind === "union") {
1243
1656
  const def = lowerer.unions.get(fnRet.unionId);
@@ -1247,12 +1660,13 @@ function lowerArrayFlatMapCall(lowerer, call, access, elem) {
1247
1660
  }
1248
1661
  }
1249
1662
  if (fnRet.kind !== "array") {
1250
- // No flatten happens: exactly map's loop, map's helper.
1663
+ // Scalar results append densely; source holes do not produce output slots.
1251
1664
  if (fnRet.kind === "void" || fnRet.kind === "func")
1252
1665
  lowerer.badType(call, lowerer.typeOf(call));
1253
1666
  fenceProducedArrayElem(lowerer, call, "'.flatMap()'", fnRet);
1254
- const helper = arrayHofHelper(lowerer, "map", elem, fnRet, arity, loc);
1255
- return { kind: "call", callee: helper, args: [receiver, fnArg], type: arrayOf(fnRet), loc };
1667
+ const outElem = callbackArrayElem(lowerer, call, fnRet);
1668
+ const helper = arrayHofHelper(lowerer, "flatMap", elem, fnRet, arity, loc, outElem);
1669
+ return { kind: "call", callee: helper, args: [receiver, fnArg], type: arrayOf(outElem), loc };
1256
1670
  }
1257
1671
  const helper = flatMapHelper(lowerer, elem, fnRet, arity, loc);
1258
1672
  return { kind: "call", callee: helper, args: [receiver, fnArg], type: fnRet, loc };
@@ -1275,7 +1689,7 @@ function flatMapHelper(lowerer, elem, fnRet, arity, loc) {
1275
1689
  lowerer.arrHofHelpers.set(key, name);
1276
1690
  const arrT = arrayOf(elem);
1277
1691
  const inner = fnRet.elem;
1278
- const fnT = funcOf([elem, F64, arrT].slice(0, arity), fnRet);
1692
+ const fnT = funcOf([arrayValueType(lowerer, elem), F64, arrT].slice(0, arity), fnRet);
1279
1693
  const innerLoop = {
1280
1694
  kind: "for",
1281
1695
  init: { kind: "varDecl", localId: "j.0", init: numLit(0, loc), loc },
@@ -1288,15 +1702,13 @@ function flatMapHelper(lowerer, elem, fnRet, arity, loc) {
1288
1702
  },
1289
1703
  body: [
1290
1704
  {
1291
- kind: "exprStmt",
1292
- expr: {
1293
- kind: "arrIntrinsic",
1294
- method: "push",
1295
- receiver: varRef("out.0", fnRet, loc),
1296
- args: [{ kind: "arrayGet", arr: varRef("r.0", fnRet, loc), index: varRef("j.0", F64, loc), type: inner, loc }],
1297
- type: F64,
1298
- loc,
1299
- },
1705
+ kind: "if",
1706
+ cond: arrayHasExpr(varRef("r.0", fnRet, loc), varRef("j.0", F64, loc), loc),
1707
+ then: [
1708
+ { kind: "varDecl", localId: "inner.0", init: arrayValueRead(lowerer, varRef("r.0", fnRet, loc), varRef("j.0", F64, loc), inner, loc), loc },
1709
+ arrayValueStore(lowerer, varRef("out.0", fnRet, loc), { kind: "arrIntrinsic", method: "length", receiver: varRef("out.0", fnRet, loc), args: [], type: F64, loc }, varRef("inner.0", arrayValueType(lowerer, inner), loc), inner, loc),
1710
+ ],
1711
+ else_: null,
1300
1712
  loc,
1301
1713
  },
1302
1714
  ],
@@ -1307,24 +1719,32 @@ function flatMapHelper(lowerer, elem, fnRet, arity, loc) {
1307
1719
  readLenStmt(arrT, loc),
1308
1720
  countedFor(loc, varRef("n.0", F64, loc), () => [
1309
1721
  {
1310
- kind: "varDecl",
1311
- localId: "r.0",
1312
- init: {
1313
- kind: "callValue",
1314
- callee: varRef("f.0", fnT, loc),
1315
- args: [getElemExpr(arrT, elem, loc), varRef("i.0", F64, loc), varRef("a.0", arrT, loc)].slice(0, arity),
1316
- type: fnRet,
1317
- loc,
1318
- },
1319
- loc,
1320
- },
1321
- {
1322
- kind: "varDecl",
1323
- localId: "m.0",
1324
- init: { kind: "arrIntrinsic", method: "length", receiver: varRef("r.0", fnRet, loc), args: [], type: F64, loc },
1722
+ kind: "if",
1723
+ cond: getHasExpr(arrT, loc),
1724
+ then: [
1725
+ {
1726
+ kind: "varDecl",
1727
+ localId: "r.0",
1728
+ init: {
1729
+ kind: "callValue",
1730
+ callee: varRef("f.0", fnT, loc),
1731
+ args: [arrayValueRead(lowerer, varRef("a.0", arrT, loc), varRef("i.0", F64, loc), elem, loc), varRef("i.0", F64, loc), varRef("a.0", arrT, loc)].slice(0, arity),
1732
+ type: fnRet,
1733
+ loc,
1734
+ },
1735
+ loc,
1736
+ },
1737
+ {
1738
+ kind: "varDecl",
1739
+ localId: "m.0",
1740
+ init: { kind: "arrIntrinsic", method: "length", receiver: varRef("r.0", fnRet, loc), args: [], type: F64, loc },
1741
+ loc,
1742
+ },
1743
+ innerLoop,
1744
+ ],
1745
+ else_: null,
1325
1746
  loc,
1326
1747
  },
1327
- innerLoop,
1328
1748
  ]),
1329
1749
  { kind: "return", value: varRef("out.0", fnRet, loc), loc },
1330
1750
  ];
@@ -1344,6 +1764,7 @@ function flatMapHelper(lowerer, elem, fnRet, arity, loc) {
1344
1764
  { id: "r.0", name: "r", type: fnRet, mutable: false },
1345
1765
  { id: "m.0", name: "m", type: F64, mutable: false },
1346
1766
  { id: "j.0", name: "j", type: F64, mutable: true },
1767
+ { id: "inner.0", name: "inner", type: arrayValueType(lowerer, inner), mutable: false },
1347
1768
  ],
1348
1769
  body,
1349
1770
  loc,
@@ -1363,13 +1784,12 @@ function lowerArrayReduceCall(lowerer, call, access, method, elem) {
1363
1784
  if (!argNode)
1364
1785
  lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded
1365
1786
  const hasInit = call.arguments.length === 2;
1366
- const accT = hasInit ? lowerer.irTypeOf(call) : elem;
1787
+ const accT = lowerer.runtimeOptionalReduceTypes.get(call) ??
1788
+ (hasInit ? lowerer.irTypeOf(call) : arrayValueType(lowerer, elem));
1367
1789
  if (accT.kind === "void" || accT.kind === "func")
1368
1790
  lowerer.badType(call, lowerer.typeOf(call));
1369
- const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [accT, elem], arrT);
1370
- if (!typeEquals(fnArg.type.ret, accT))
1371
- lowerer.badType(argNode, lowerer.typeOf(argNode));
1372
- const helper = reduceHelper(lowerer, method, elem, accT, arity, hasInit, loc);
1791
+ const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [accT, arrayValueType(lowerer, elem)], arrT);
1792
+ const helper = reduceHelper(lowerer, method, elem, accT, fnArg.type.ret, arity, hasInit, loc);
1373
1793
  const args = [receiver, fnArg];
1374
1794
  if (hasInit)
1375
1795
  args.push(lowerer.lowerExprExpecting(call.arguments[1], accT));
@@ -1385,76 +1805,53 @@ function lowerArrayReduceCall(lowerer, call, access, method, elem) {
1385
1805
  * reduce walks 0→n-1, reduceRight n-1→0; the seed without an initial
1386
1806
  * value is a[0] / a[n-1] and the loop starts one past it. The empty-array
1387
1807
  * TypeError message is Node's, byte for byte. */
1388
- function reduceHelper(lowerer, method, elem, accT, arity, hasInit, loc) {
1389
- const key = `${method}:${typeKey(elem)}:${typeKey(accT)}:${arity}:${hasInit ? "init" : "seed"}`;
1808
+ function reduceHelper(lowerer, method, elem, accT, fnRet, arity, hasInit, loc) {
1809
+ const key = `${method}:${typeKey(elem)}:${typeKey(accT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : "seed"}`;
1390
1810
  const existing = lowerer.arrHofHelpers.get(key);
1391
1811
  if (existing)
1392
1812
  return existing;
1393
1813
  const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
1394
1814
  lowerer.arrHofHelpers.set(key, name);
1395
1815
  const arrT = arrayOf(elem);
1396
- const fnT = funcOf([accT, elem, F64, arrT].slice(0, arity), accT);
1816
+ const fnT = funcOf([accT, arrayValueType(lowerer, elem), F64, arrT].slice(0, arity), fnRet);
1397
1817
  const n = varRef("n.0", F64, loc);
1398
1818
  const i = varRef("i.0", F64, loc);
1399
1819
  const right = method === "reduceRight";
1400
- const at = (index) => ({ kind: "arrayGet", arr: varRef("a.0", arrT, loc), index, type: elem, loc });
1820
+ const at = (index) => arrayValueRead(lowerer, varRef("a.0", arrT, loc), index, elem, loc);
1401
1821
  const locals = [
1402
1822
  { id: "a.0", name: "a", type: arrT, mutable: true },
1403
1823
  { id: "f.0", name: "f", type: fnT, mutable: true },
1404
1824
  ...(hasInit ? [{ id: "z.0", name: "z", type: accT, mutable: true }] : []),
1405
1825
  { id: "acc.0", name: "acc", type: accT, mutable: true },
1406
1826
  { id: "n.0", name: "n", type: F64, mutable: false },
1407
- { id: "i.0", name: "i", type: F64, mutable: true },
1408
- ];
1409
- const params = [
1410
- { localId: "a.0", name: "a", type: arrT },
1411
- { localId: "f.0", name: "f", type: fnT },
1412
- ...(hasInit ? [{ localId: "z.0", name: "z", type: accT }] : []),
1413
- ];
1414
- const seedStmts = hasInit
1415
- ? [{ kind: "varDecl", localId: "acc.0", init: varRef("z.0", accT, loc), loc }]
1416
- : [
1417
- {
1418
- kind: "if",
1419
- cond: { kind: "bin", op: "===", left: n, right: numLit(0, loc), type: BOOL, loc },
1420
- then: [
1421
- {
1422
- kind: "throw",
1423
- value: {
1424
- kind: "libCall",
1425
- fn: "error.new",
1426
- args: [
1427
- {
1428
- kind: "strLit",
1429
- value: "Reduce of empty array with no initial value",
1430
- type: STRING,
1431
- loc,
1432
- },
1433
- ],
1434
- type: { kind: "object", className: "%TypeError" },
1435
- loc,
1436
- },
1437
- loc,
1438
- },
1439
- ],
1440
- else_: null,
1441
- loc,
1442
- },
1443
- {
1444
- kind: "varDecl",
1445
- localId: "acc.0",
1446
- init: at(right ? { kind: "bin", op: "-", left: n, right: numLit(1, loc), type: F64, loc } : numLit(0, loc)),
1447
- loc,
1448
- },
1449
- ];
1450
- // Loop bounds: with init the walk covers every index; the seeded form
1451
- // starts one past the seed. reduce ascends, reduceRight descends.
1827
+ { id: "i.0", name: "i", type: F64, mutable: true },
1828
+ ];
1829
+ const params = [
1830
+ { localId: "a.0", name: "a", type: arrT },
1831
+ { localId: "f.0", name: "f", type: fnT },
1832
+ ...(hasInit ? [{ localId: "z.0", name: "z", type: accT }] : []),
1833
+ ];
1452
1834
  const start = right
1453
- ? { kind: "bin", op: "-", left: n, right: numLit(hasInit ? 1 : 2, loc), type: F64, loc }
1454
- : numLit(hasInit ? 0 : 1, loc);
1835
+ ? { kind: "bin", op: "-", left: n, right: numLit(1, loc), type: F64, loc }
1836
+ : numLit(0, loc);
1837
+ const inRange = right
1838
+ ? { kind: "bin", op: ">=", left: i, right: numLit(0, loc), type: BOOL, loc }
1839
+ : { kind: "bin", op: "<", left: i, right: n, type: BOOL, loc };
1840
+ const advance = { kind: "assign", localId: "i.0", value: { kind: "bin", op: right ? "-" : "+", left: i, right: numLit(1, loc), type: F64, loc }, loc };
1841
+ const seedStmts = [
1842
+ { kind: "varDecl", localId: "i.0", init: start, loc },
1843
+ ...(hasInit
1844
+ ? [{ kind: "varDecl", localId: "acc.0", init: varRef("z.0", accT, loc), loc }]
1845
+ : [
1846
+ { kind: "while", cond: { kind: "ternary", cond: inRange, then: { kind: "unary", op: "!", operand: getHasExpr(arrT, loc), type: BOOL, loc }, else_: boolLit(false, loc), type: BOOL, loc }, body: [advance], loc },
1847
+ { kind: "if", cond: { kind: "unary", op: "!", operand: inRange, type: BOOL, loc }, then: [{ kind: "throw", value: { kind: "libCall", fn: "error.new", args: [{ kind: "strLit", value: "Reduce of empty array with no initial value", type: STRING, loc }], type: { kind: "object", className: "%TypeError" }, loc }, loc }], else_: null, loc },
1848
+ { kind: "varDecl", localId: "acc.0", init: lowerer.coerceToExpected(at(i), accT), loc },
1849
+ advance,
1850
+ ]),
1851
+ ];
1455
1852
  const loop = {
1456
1853
  kind: "for",
1457
- init: { kind: "varDecl", localId: "i.0", init: start, loc },
1854
+ init: null,
1458
1855
  cond: right
1459
1856
  ? { kind: "bin", op: ">=", left: i, right: numLit(0, loc), type: BOOL, loc }
1460
1857
  : { kind: "bin", op: "<", left: i, right: n, type: BOOL, loc },
@@ -1464,20 +1861,20 @@ function reduceHelper(lowerer, method, elem, accT, arity, hasInit, loc) {
1464
1861
  value: { kind: "bin", op: right ? "-" : "+", left: i, right: numLit(1, loc), type: F64, loc },
1465
1862
  loc,
1466
1863
  },
1467
- body: [
1468
- {
1469
- kind: "assign",
1470
- localId: "acc.0",
1471
- value: {
1472
- kind: "callValue",
1473
- callee: varRef("f.0", fnT, loc),
1474
- args: [varRef("acc.0", accT, loc), at(i), i, varRef("a.0", arrT, loc)].slice(0, arity),
1475
- type: accT,
1476
- loc,
1477
- },
1478
- loc,
1479
- },
1480
- ],
1864
+ body: [{ kind: "if", cond: getHasExpr(arrT, loc), then: [
1865
+ {
1866
+ kind: "assign",
1867
+ localId: "acc.0",
1868
+ value: lowerer.coerceToExpected({
1869
+ kind: "callValue",
1870
+ callee: varRef("f.0", fnT, loc),
1871
+ args: [varRef("acc.0", accT, loc), at(i), i, varRef("a.0", arrT, loc)].slice(0, arity),
1872
+ type: fnRet,
1873
+ loc,
1874
+ }, accT),
1875
+ loc,
1876
+ },
1877
+ ], else_: null, loc }],
1481
1878
  loc,
1482
1879
  };
1483
1880
  const body = [
@@ -1493,19 +1890,20 @@ function reduceHelper(lowerer, method, elem, accT, arity, hasInit, loc) {
1493
1890
  * function like the other array HOFs. sort mutates and returns the receiver;
1494
1891
  * toSorted takes its shallow snapshot INSIDE the helper, after the receiver
1495
1892
  * and comparator expressions have both been evaluated, then sorts and
1496
- * returns that copy without touching the receiver. The loop is a
1497
- * binary-free INSERTION sort: stable (equal-comparing elements keep their
1498
- * source order, which is what Node's stable TimSort produces for any
1499
- * consistent comparator) and JS-faithful on the comparator contract — an
1500
- * element moves left only while cmp(left, v) > 0, so a NaN or 0 result holds
1501
- * position exactly like the spec's "treat as equal". The SEQUENCE of
1502
- * comparator calls differs from V8's TimSort (SEMANTICS.md); results are
1503
- * identical for consistent comparators. The comparator-less form lowers for
1504
- * STRING elements only — JS's default converts every element to string and
1505
- * compares UTF-16 units, so the interned synthesized comparator selects the
1506
- * runtime's code-unit ordering rather than scriptc's documented code-point
1507
- * relational operators. For numbers that default is the notorious string
1508
- * sort ([10, 9, 1] → [1, 10, 9]), deliberately fenced toward an explicit
1893
+ * returns that copy without touching the receiver. The helper uses a stable
1894
+ * bottom-up merge sort with an ordered-boundary check: ordered inputs use a
1895
+ * linear number of comparator calls, but buffer movement remains O(n log n)
1896
+ * even when every boundary is already ordered. During a merge, an element
1897
+ * moves right only while cmp(left, right) > 0, so a NaN or 0 result keeps
1898
+ * the left element first. Undefined values sink without reaching the user
1899
+ * comparator. The callback sequence differs from V8's TimSort, so exact
1900
+ * order and count parity are not claimed; results agree for consistent
1901
+ * comparators. The comparator-less form lowers for STRING elements only —
1902
+ * JS's default converts every element to string and compares UTF-16 units,
1903
+ * so the interned synthesized comparator selects the runtime's code-unit
1904
+ * ordering rather than scriptc's documented code-point relational
1905
+ * operators. For numbers that default is the notorious string sort
1906
+ * ([10, 9, 1] → [1, 10, 9]), deliberately fenced toward an explicit
1509
1907
  * comparator. */
1510
1908
  function lowerArraySortCall(lowerer, call, access, elem, arrT) {
1511
1909
  const loc = locOf(call);
@@ -1599,169 +1997,10 @@ function defaultStringCmpHelper(lowerer, loc) {
1599
1997
  });
1600
1998
  return name;
1601
1999
  }
1602
- /** The insertion-sort loop, from existing IR nodes:
1603
- *
1604
- * n = a.length;
1605
- * for (i = 1; i < n; i++) {
1606
- * v = a[i]; j = i - 1;
1607
- * while (j >= 0) {
1608
- * if (CompareArrayElements(a[j], v, f) > 0) {
1609
- * a[j + 1] = a[j]; j = j - 1;
1610
- * } else break;
1611
- * }
1612
- * a[j + 1] = v;
1613
- * }
1614
- * return a;
1615
- */
1616
- function buildArraySortFn(name, elem, arity, copyFirst, undefinedTag, loc) {
1617
- const arrT = arrayOf(elem);
1618
- const fnT = funcOf([elem, elem].slice(0, arity), F64);
1619
- const j = varRef("j.0", F64, loc);
1620
- const at = (index) => ({ kind: "arrayGet", arr: varRef("a.0", arrT, loc), index, type: elem, loc });
1621
- const jPlus1 = { kind: "bin", op: "+", left: j, right: numLit(1, loc), type: F64, loc };
1622
- const isUndefined = (value) => {
1623
- if (elem.kind === "union" && undefinedTag !== null) {
1624
- return {
1625
- kind: "unionIsTag",
1626
- unionId: elem.unionId,
1627
- tag: undefinedTag,
1628
- negated: false,
1629
- value,
1630
- type: BOOL,
1631
- loc,
1632
- };
1633
- }
1634
- if (elem.kind === "jsval") {
1635
- return {
1636
- kind: "jsOp",
1637
- op: "eq",
1638
- args: [
1639
- value,
1640
- { kind: "jsOp", op: "undefLit", args: [], type: JSVAL, loc },
1641
- ],
1642
- type: BOOL,
1643
- loc,
1644
- };
1645
- }
1646
- return null;
1647
- };
1648
- const compareGreater = {
1649
- kind: "bin",
1650
- op: ">",
1651
- left: {
1652
- kind: "callValue",
1653
- callee: varRef("f.0", fnT, loc),
1654
- args: [at(j), varRef("v.0", elem, loc)].slice(0, arity),
1655
- type: F64,
1656
- loc,
1657
- },
1658
- right: numLit(0, loc),
1659
- type: BOOL,
1660
- loc,
1661
- };
1662
- const leftUndefined = isUndefined(at(j));
1663
- const valueUndefined = isUndefined(varRef("v.0", elem, loc));
1664
- // CompareArrayElements: undefined always sinks and never reaches the
1665
- // user comparator. Ternaries preserve that callback suppression.
1666
- const shouldShift = leftUndefined !== null && valueUndefined !== null
1667
- ? {
1668
- kind: "ternary",
1669
- cond: leftUndefined,
1670
- then: {
1671
- kind: "unary",
1672
- op: "!",
1673
- operand: valueUndefined,
1674
- type: BOOL,
1675
- loc,
1676
- },
1677
- else_: {
1678
- kind: "ternary",
1679
- cond: valueUndefined,
1680
- then: { kind: "boolLit", value: false, type: BOOL, loc },
1681
- else_: compareGreater,
1682
- type: BOOL,
1683
- loc,
1684
- },
1685
- type: BOOL,
1686
- loc,
1687
- }
1688
- : compareGreater;
1689
- const shiftLoop = {
1690
- kind: "while",
1691
- cond: { kind: "bin", op: ">=", left: j, right: numLit(0, loc), type: BOOL, loc },
1692
- body: [
1693
- {
1694
- kind: "if",
1695
- cond: shouldShift,
1696
- then: [
1697
- { kind: "arraySet", arr: varRef("a.0", arrT, loc), index: jPlus1, value: at(j), loc },
1698
- { kind: "assign", localId: "j.0", value: { kind: "bin", op: "-", left: j, right: numLit(1, loc), type: F64, loc }, loc },
1699
- ],
1700
- else_: [{ kind: "break", loc }],
1701
- loc,
1702
- },
1703
- ],
1704
- loc,
1705
- };
1706
- const body = [
1707
- ...(copyFirst
1708
- ? [{
1709
- kind: "assign",
1710
- localId: "a.0",
1711
- value: {
1712
- kind: "arrIntrinsic",
1713
- method: "slice",
1714
- receiver: varRef("a.0", arrT, loc),
1715
- args: [],
1716
- type: arrT,
1717
- loc,
1718
- },
1719
- loc,
1720
- }]
1721
- : []),
1722
- readLenStmt(arrT, loc),
1723
- countedFor(loc, { kind: "bin", op: "-", left: varRef("n.0", F64, loc), right: numLit(1, loc), type: F64, loc }, (index) => [
1724
- {
1725
- kind: "varDecl",
1726
- localId: "v.0",
1727
- init: {
1728
- kind: "arrayGet",
1729
- arr: varRef("a.0", arrT, loc),
1730
- index: { kind: "bin", op: "+", left: index, right: numLit(1, loc), type: F64, loc },
1731
- type: elem,
1732
- loc,
1733
- },
1734
- loc,
1735
- },
1736
- { kind: "varDecl", localId: "j.0", init: index, loc },
1737
- shiftLoop,
1738
- { kind: "arraySet", arr: varRef("a.0", arrT, loc), index: jPlus1, value: varRef("v.0", elem, loc), loc },
1739
- ]),
1740
- { kind: "return", value: varRef("a.0", arrT, loc), loc },
1741
- ];
1742
- return {
1743
- name,
1744
- params: [
1745
- { localId: "a.0", name: "a", type: arrT },
1746
- { localId: "f.0", name: "f", type: fnT },
1747
- ],
1748
- returnType: arrT,
1749
- locals: [
1750
- { id: "a.0", name: "a", type: arrT, mutable: true },
1751
- { id: "f.0", name: "f", type: fnT, mutable: true },
1752
- { id: "n.0", name: "n", type: F64, mutable: false },
1753
- { id: "i.0", name: "i", type: F64, mutable: true },
1754
- { id: "v.0", name: "v", type: elem, mutable: false },
1755
- { id: "j.0", name: "j", type: F64, mutable: true },
1756
- ],
1757
- body,
1758
- loc,
1759
- };
1760
- }
1761
2000
  /** Uint8Array.prototype.toSorted. The receiver/comparator expressions are
1762
2001
  * evaluated before entering the helper; the helper snapshots with
1763
2002
  * TypedArray.prototype.slice before its first comparison, then performs
1764
- * the same stable insertion walk as Array.toSorted. Uint8Array's default
2003
+ * the same stable merge walk as Array.toSorted. Uint8Array's default
1765
2004
  * comparator is numeric ascending, so its comparator-less form needs no
1766
2005
  * string-conversion machinery. */
1767
2006
  function lowerBytesToSortedCall(lowerer, call, access, bytesT) {
@@ -1835,205 +2074,6 @@ function lowerBytesToSortedCall(lowerer, call, access, bytesT) {
1835
2074
  loc,
1836
2075
  };
1837
2076
  }
1838
- function buildBytesSortFn(name, arity, hasComparator, loc) {
1839
- const bytesT = BYTES_U8;
1840
- const fnT = funcOf([F64, F64].slice(0, arity), F64);
1841
- const j = varRef("j.0", F64, loc);
1842
- const at = (index) => ({
1843
- kind: "bytesIntrinsic",
1844
- method: "get",
1845
- receiver: varRef("a.0", bytesT, loc),
1846
- args: [index],
1847
- type: F64,
1848
- loc,
1849
- });
1850
- const jPlus1 = {
1851
- kind: "bin",
1852
- op: "+",
1853
- left: j,
1854
- right: numLit(1, loc),
1855
- type: F64,
1856
- loc,
1857
- };
1858
- const compare = hasComparator
1859
- ? {
1860
- kind: "callValue",
1861
- callee: varRef("f.0", fnT, loc),
1862
- args: [at(j), varRef("v.0", F64, loc)].slice(0, arity),
1863
- type: F64,
1864
- loc,
1865
- }
1866
- : {
1867
- kind: "bin",
1868
- op: "-",
1869
- left: at(j),
1870
- right: varRef("v.0", F64, loc),
1871
- type: F64,
1872
- loc,
1873
- };
1874
- const shiftLoop = {
1875
- kind: "while",
1876
- cond: {
1877
- kind: "bin",
1878
- op: ">=",
1879
- left: j,
1880
- right: numLit(0, loc),
1881
- type: BOOL,
1882
- loc,
1883
- },
1884
- body: [
1885
- {
1886
- kind: "if",
1887
- cond: {
1888
- kind: "bin",
1889
- op: ">",
1890
- left: compare,
1891
- right: numLit(0, loc),
1892
- type: BOOL,
1893
- loc,
1894
- },
1895
- then: [
1896
- {
1897
- kind: "bytesSet",
1898
- arr: varRef("a.0", bytesT, loc),
1899
- index: jPlus1,
1900
- value: at(j),
1901
- loc,
1902
- },
1903
- {
1904
- kind: "assign",
1905
- localId: "j.0",
1906
- value: {
1907
- kind: "bin",
1908
- op: "-",
1909
- left: j,
1910
- right: numLit(1, loc),
1911
- type: F64,
1912
- loc,
1913
- },
1914
- loc,
1915
- },
1916
- ],
1917
- else_: [{ kind: "break", loc }],
1918
- loc,
1919
- },
1920
- ],
1921
- loc,
1922
- };
1923
- const params = [
1924
- { localId: "a.0", name: "a", type: bytesT },
1925
- ...(hasComparator
1926
- ? [{ localId: "f.0", name: "f", type: fnT }]
1927
- : []),
1928
- ];
1929
- const locals = [
1930
- { id: "a.0", name: "a", type: bytesT, mutable: true },
1931
- ...(hasComparator
1932
- ? [{ id: "f.0", name: "f", type: fnT, mutable: true }]
1933
- : []),
1934
- { id: "n.0", name: "n", type: F64, mutable: false },
1935
- { id: "i.0", name: "i", type: F64, mutable: true },
1936
- { id: "v.0", name: "v", type: F64, mutable: false },
1937
- { id: "j.0", name: "j", type: F64, mutable: true },
1938
- ];
1939
- const body = [
1940
- {
1941
- kind: "assign",
1942
- localId: "a.0",
1943
- value: {
1944
- kind: "bytesIntrinsic",
1945
- method: "slice",
1946
- receiver: varRef("a.0", bytesT, loc),
1947
- args: [],
1948
- type: bytesT,
1949
- loc,
1950
- },
1951
- loc,
1952
- },
1953
- {
1954
- kind: "varDecl",
1955
- localId: "n.0",
1956
- init: {
1957
- kind: "bytesIntrinsic",
1958
- method: "length",
1959
- receiver: varRef("a.0", bytesT, loc),
1960
- args: [],
1961
- type: F64,
1962
- loc,
1963
- },
1964
- loc,
1965
- },
1966
- {
1967
- kind: "for",
1968
- init: {
1969
- kind: "varDecl",
1970
- localId: "i.0",
1971
- init: numLit(1, loc),
1972
- loc,
1973
- },
1974
- cond: {
1975
- kind: "bin",
1976
- op: "<",
1977
- left: varRef("i.0", F64, loc),
1978
- right: varRef("n.0", F64, loc),
1979
- type: BOOL,
1980
- loc,
1981
- },
1982
- update: {
1983
- kind: "assign",
1984
- localId: "i.0",
1985
- value: {
1986
- kind: "bin",
1987
- op: "+",
1988
- left: varRef("i.0", F64, loc),
1989
- right: numLit(1, loc),
1990
- type: F64,
1991
- loc,
1992
- },
1993
- loc,
1994
- },
1995
- body: [
1996
- {
1997
- kind: "varDecl",
1998
- localId: "v.0",
1999
- init: at(varRef("i.0", F64, loc)),
2000
- loc,
2001
- },
2002
- {
2003
- kind: "varDecl",
2004
- localId: "j.0",
2005
- init: {
2006
- kind: "bin",
2007
- op: "-",
2008
- left: varRef("i.0", F64, loc),
2009
- right: numLit(1, loc),
2010
- type: F64,
2011
- loc,
2012
- },
2013
- loc,
2014
- },
2015
- shiftLoop,
2016
- {
2017
- kind: "bytesSet",
2018
- arr: varRef("a.0", bytesT, loc),
2019
- index: jPlus1,
2020
- value: varRef("v.0", F64, loc),
2021
- loc,
2022
- },
2023
- ],
2024
- loc,
2025
- },
2026
- { kind: "return", value: varRef("a.0", bytesT, loc), loc },
2027
- ];
2028
- return {
2029
- name,
2030
- params,
2031
- returnType: bytesT,
2032
- locals,
2033
- body,
2034
- loc,
2035
- };
2036
- }
2037
2077
  /** `n = a.length` — the once-up-front length read every array HOF loop
2038
2078
  * starts with (locals a.0/n.0 by convention). */
2039
2079
  function readLenStmt(arrT, loc) {
@@ -2111,14 +2151,6 @@ function lowerArrayAtCall(lowerer, call, access, elem) {
2111
2151
  const undefTag = lowerer.armTag(resultT.unionId, UNDEFINED_T);
2112
2152
  if (undefTag < 0)
2113
2153
  lowerer.badType(call, lowerer.typeOf(call));
2114
- let foundTag = null;
2115
- if (!typeEquals(resultT, elem)) {
2116
- foundTag = lowerer.armTag(resultT.unionId, elem);
2117
- if (foundTag < 0) {
2118
- lowerer.unsupported("SC1090", call, `'.at' on '${lowerer.fmt(elem)}'-element arrays (the element would need a union ` +
2119
- "re-tag that is not supported yet — index and test instead)");
2120
- }
2121
- }
2122
2154
  const key = `at:${typeKey(elem)}:${typeKey(resultT)}`;
2123
2155
  let name = lowerer.arrHofHelpers.get(key);
2124
2156
  if (!name) {
@@ -2128,10 +2160,7 @@ function lowerArrayAtCall(lowerer, call, access, elem) {
2128
2160
  const t = varRef("t.0", F64, loc);
2129
2161
  const i = varRef("i.0", F64, loc);
2130
2162
  const n = varRef("n.0", F64, loc);
2131
- const v = varRef("v.0", elem, loc);
2132
- const found = foundTag === null
2133
- ? v
2134
- : { kind: "unionWrap", unionId: resultT.unionId, tag: foundTag, value: v, type: resultT, loc };
2163
+ const found = lowerer.coerceToExpected(arrayValueRead(lowerer, varRef("a.0", arrT, loc), t, elem, loc), resultT);
2135
2164
  const miss = {
2136
2165
  kind: "unionWrap",
2137
2166
  unionId: resultT.unionId,
@@ -2183,12 +2212,6 @@ function lowerArrayAtCall(lowerer, call, access, elem) {
2183
2212
  else_: null,
2184
2213
  loc,
2185
2214
  },
2186
- {
2187
- kind: "varDecl",
2188
- localId: "v.0",
2189
- init: { kind: "arrayGet", arr: varRef("a.0", arrT, loc), index: t, type: elem, loc },
2190
- loc,
2191
- },
2192
2215
  { kind: "return", value: found, loc },
2193
2216
  ];
2194
2217
  lowerer.liftedFns.push({
@@ -2203,7 +2226,6 @@ function lowerArrayAtCall(lowerer, call, access, elem) {
2203
2226
  { id: "i.0", name: "i", type: F64, mutable: true },
2204
2227
  { id: "n.0", name: "n", type: F64, mutable: false },
2205
2228
  { id: "t.0", name: "t", type: F64, mutable: true },
2206
- { id: "v.0", name: "v", type: elem, mutable: false },
2207
2229
  ],
2208
2230
  body,
2209
2231
  loc,
@@ -3906,7 +3928,7 @@ export function lowerForOfSearchParams(lowerer, stmt, iterable, proj) {
3906
3928
  export function lowerForOfArrayIter(lowerer, stmt, iterable, proj) {
3907
3929
  const loc = locOf(stmt);
3908
3930
  const arrT = iterable.type;
3909
- const elemT = arrT.kind === "array" ? arrT.elem : F64;
3931
+ const elemT = arrT.kind === "array" ? arrayValueType(lowerer, arrT.elem) : F64;
3910
3932
  const yieldsPair = proj === "entries";
3911
3933
  if (!ts.isVariableDeclarationList(stmt.initializer)) {
3912
3934
  lowerer.unsupported("SC1090", stmt.initializer, "for-of over a pre-declared variable (declare the loop variable in the loop: for (const x of ...))");
@@ -3925,13 +3947,7 @@ export function lowerForOfArrayIter(lowerer, stmt, iterable, proj) {
3925
3947
  i.mutable = true;
3926
3948
  const keyRead = () => varRef(i.id, F64, loc);
3927
3949
  const valRead = () => arrT.kind === "array"
3928
- ? {
3929
- kind: "arrayGet",
3930
- arr: varRef(arr.id, arrT, loc),
3931
- index: varRef(i.id, F64, loc),
3932
- type: elemT,
3933
- loc,
3934
- }
3950
+ ? arrayValueRead(lowerer, varRef(arr.id, arrT, loc), varRef(i.id, F64, loc), arrT.elem, loc)
3935
3951
  : {
3936
3952
  kind: "bytesIntrinsic",
3937
3953
  method: "get",
@@ -3954,6 +3970,11 @@ export function lowerForOfArrayIter(lowerer, stmt, iterable, proj) {
3954
3970
  binds.push({ kind: "varDecl", localId: kLocal.id, init: keyRead(), loc });
3955
3971
  if (els[1]) {
3956
3972
  const vLocal = lowerer.declareLocal(els[1].name, els[1].name.text, elemT, isLet);
3973
+ if (arrT.kind === "array" && !typeEquals(elemT, arrT.elem)) {
3974
+ const root = lowerer.runtimeOptionalRootOf(vLocal);
3975
+ lowerer.runtimeOptionalLocals.add(root);
3976
+ lowerer.runtimeOptionalStorageLocals.add(root);
3977
+ }
3957
3978
  binds.push({ kind: "varDecl", localId: vLocal.id, init: valRead(), loc });
3958
3979
  }
3959
3980
  }
@@ -3964,6 +3985,13 @@ export function lowerForOfArrayIter(lowerer, stmt, iterable, proj) {
3964
3985
  const declT = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(decl.name));
3965
3986
  if (yieldsPair) {
3966
3987
  const shape = declT?.kind === "record" ? lowerer.shapes.get(declT.shapeId) : null;
3988
+ if (declT?.kind === "record" && shape?.tuple && arrT.kind === "array") {
3989
+ const valueField = shape.fields.find((field) => field.name === "1");
3990
+ if (valueField && typeEquals(valueField.type, arrT.elem) && !typeEquals(elemT, arrT.elem)) {
3991
+ valueField.type = elemT;
3992
+ lowerer.runtimeOptionalFields.add(lowerer.runtimeOptionalFieldKey(declT.shapeId, "1"));
3993
+ }
3994
+ }
3967
3995
  if (declT?.kind !== "record" || !shape?.tuple || shape.fields.length !== 2 ||
3968
3996
  shape.fields.find((f) => f.name === "0")?.type.kind !== "f64" ||
3969
3997
  !typeEquals(shape.fields.find((f) => f.name === "1")?.type ?? F64, elemT)) {
@@ -4391,7 +4419,15 @@ export function lowerRegexMethodCall(lowerer, call, access, dynReceiver) {
4391
4419
  return null;
4392
4420
  const name = access.name.text;
4393
4421
  const receiverKind = dynReceiver ? "string" : lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind;
4394
- const lowerReceiver = () => (dynReceiver ? dynReceiver() : lowerer.lowerExpr(access.expression));
4422
+ const lowerReceiver = () => {
4423
+ if (dynReceiver)
4424
+ return dynReceiver();
4425
+ if (receiverKind === "string")
4426
+ return lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text);
4427
+ if (receiverKind === "regex")
4428
+ return lowerMethodReceiver(lowerer, access.expression, { kind: "regex" }, access.name.text);
4429
+ return lowerer.lowerExpr(access.expression);
4430
+ };
4395
4431
  const loc = locOf(call);
4396
4432
  if (receiverKind === "regex" && name === "test") {
4397
4433
  // The statefulness fence, at compile time where the flags are
@@ -4406,7 +4442,7 @@ export function lowerRegexMethodCall(lowerer, call, access, dynReceiver) {
4406
4442
  lowerer.unsupported("SC1121", call);
4407
4443
  }
4408
4444
  }
4409
- const receiver = lowerer.lowerExpr(access.expression);
4445
+ const receiver = lowerReceiver();
4410
4446
  const args = call.arguments.map((a) => lowerer.lowerExpr(a));
4411
4447
  return { kind: "regexIntrinsic", method: "test", receiver, args, type: BOOL, loc };
4412
4448
  }
@@ -4429,7 +4465,7 @@ export function lowerRegexMethodCall(lowerer, call, access, dynReceiver) {
4429
4465
  lowerer.unsupported("SC1121", call);
4430
4466
  }
4431
4467
  }
4432
- const re = lowerer.lowerExpr(access.expression);
4468
+ const re = lowerReceiver();
4433
4469
  const subject = lowerer.lowerExprExpecting(call.arguments[0], STRING);
4434
4470
  const resultT = { kind: "union", unionId: lowerer.unions.intern([arrayOf(STRING), { kind: "nullT" }]) };
4435
4471
  return { kind: "regexIntrinsic", method: "match", receiver: subject, args: [re], type: resultT, loc };
@@ -4473,7 +4509,9 @@ export function lowerRegexMethodCall(lowerer, call, access, dynReceiver) {
4473
4509
  lowerer.unsupported("SC1120", call, "'.match()' with the 'g' or 'y' flag (an every-match array is a different shape — use replaceAll/split, or test() per position)");
4474
4510
  }
4475
4511
  }
4476
- const receiver = nullableStringRecv ? lowerer.lowerExprExpecting(access.expression, STRING) : lowerReceiver();
4512
+ const receiver = nullableStringRecv
4513
+ ? lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text)
4514
+ : lowerReceiver();
4477
4515
  const re = lowerer.lowerExpr(arg0);
4478
4516
  // RegExpMatchArray | null maps to the string[] | null union by
4479
4517
  // itself; intern it directly when the checker's spelling doesn't —
@@ -4587,7 +4625,9 @@ export function lowerStringMethodCall(lowerer, call, access, dynReceiver) {
4587
4625
  if (call.arguments.length < entry.minArgs || call.arguments.length > entry.maxArgs) {
4588
4626
  lowerer.noLowering(`.${access.name.text} with ${call.arguments.length} argument${call.arguments.length === 1 ? "" : "s"} on strings`, call);
4589
4627
  }
4590
- const receiver = dynReceiver ? dynReceiver() : lowerer.lowerExpr(access.expression);
4628
+ const receiver = dynReceiver
4629
+ ? dynReceiver()
4630
+ : lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text);
4591
4631
  const args = entry.method === "split"
4592
4632
  ? [lowerer.lowerExpr(call.arguments[0]), lowerSplitLimitArg(lowerer, call.arguments[1], locOf(call))]
4593
4633
  : call.arguments.map((a) => lowerer.lowerExpr(a));
@@ -4630,7 +4670,7 @@ function lowerLocaleCompareCall(lowerer, call, access) {
4630
4670
  lowerer.noLowering(`.localeCompare with ${call.arguments.length} arguments`, call, "the locales/options parameters select ICU collations that have no lowering — " +
4631
4671
  "pass exactly the comparison string");
4632
4672
  }
4633
- const receiver = lowerer.lowerExpr(access.expression);
4673
+ const receiver = lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text);
4634
4674
  const arg = lowerer.lowerExpr(call.arguments[0]);
4635
4675
  if (arg.type.kind !== "string")
4636
4676
  lowerer.badType(call.arguments[0], lowerer.typeOf(call.arguments[0]));
@@ -4752,6 +4792,25 @@ export function lowerBytesNew(lowerer, expr, symbol) {
4752
4792
  return { kind: "bytesNew", source: count, type, loc };
4753
4793
  }
4754
4794
  const src = lowerer.lowerExpr(argNode);
4795
+ if (src.type.kind === "union" && lowerer.armTag(src.type.unionId, UNDEFINED_T) >= 0) {
4796
+ const present = lowerer.stripUndefinedArm(src.type);
4797
+ if (present.kind === "array" && present.elem.kind === "f64") {
4798
+ const undefTag = lowerer.armTag(src.type.unionId, UNDEFINED_T);
4799
+ const presentTag = lowerer.armTag(src.type.unionId, present);
4800
+ if (undefTag >= 0 && presentTag >= 0) {
4801
+ const empty = { kind: "arrayLit", elems: [], type: present, loc };
4802
+ const source = {
4803
+ kind: "ternary",
4804
+ cond: { kind: "unionIsTag", unionId: src.type.unionId, tag: undefTag, negated: false, value: src, type: BOOL, loc },
4805
+ then: empty,
4806
+ else_: { kind: "unionNarrow", unionId: src.type.unionId, tag: presentTag, value: src, type: present, loc },
4807
+ type: present,
4808
+ loc,
4809
+ };
4810
+ return { kind: "bytesNew", source, type, loc };
4811
+ }
4812
+ }
4813
+ }
4755
4814
  if (src.type.kind === "f64" ||
4756
4815
  typeEquals(src.type, type) ||
4757
4816
  (src.type.kind === "array" && src.type.elem.kind === "f64")) {
@@ -5132,6 +5191,20 @@ const BUF_ENCODINGS = {
5132
5191
  export function knownBufEncoding(name) {
5133
5192
  return own(BUF_ENCODINGS, name);
5134
5193
  }
5194
+ /** Presence guard for a synthetic native-array loop. HOFs skip holes, while
5195
+ * the ordinary `arrayGet` remains a proven-presence operation. */
5196
+ function arrayHasExpr(arr, index, loc) {
5197
+ return {
5198
+ kind: "arrayHas",
5199
+ arr,
5200
+ index,
5201
+ type: BOOL,
5202
+ loc,
5203
+ };
5204
+ }
5205
+ function getHasExpr(arrT, loc) {
5206
+ return arrayHasExpr({ kind: "varRef", localId: "a.0", type: arrT, loc }, { kind: "varRef", localId: "i.0", type: F64, loc }, loc);
5207
+ }
5135
5208
  /** The literal encoding argument of a Buffer surface, normalized — or a
5136
5209
  * fence when it isn't a literal alias Node knows. */
5137
5210
  export function bufEncoding(lowerer, what, encNode) {