@scriptc/compiler 0.0.36 → 0.1.2

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 (256) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +52 -22
  2. package/ambient/scriptc-overrides.d.ts +0 -7
  3. package/ambient/scriptc.d.ts +3 -2
  4. package/dist/backend/build-cache.js.map +1 -1
  5. package/dist/backend/c/async.js +24 -2
  6. package/dist/backend/c/async.js.map +1 -1
  7. package/dist/backend/c/c-emitter.d.ts +22 -10
  8. package/dist/backend/c/c-emitter.js +60 -15
  9. package/dist/backend/c/c-emitter.js.map +1 -1
  10. package/dist/backend/c/exprs.js +289 -55
  11. package/dist/backend/c/exprs.js.map +1 -1
  12. package/dist/backend/c/island.js.map +1 -1
  13. package/dist/backend/c/may-throw.js +3 -0
  14. package/dist/backend/c/may-throw.js.map +1 -1
  15. package/dist/backend/c/stmts.d.ts +3 -5
  16. package/dist/backend/c/stmts.js +76 -17
  17. package/dist/backend/c/stmts.js.map +1 -1
  18. package/dist/backend/c/types.js.map +1 -1
  19. package/dist/backend/c/walkers.js +36 -12
  20. package/dist/backend/c/walkers.js.map +1 -1
  21. package/dist/backend/external-c.d.ts +33 -0
  22. package/dist/backend/external-c.js +51 -0
  23. package/dist/backend/external-c.js.map +1 -0
  24. package/dist/backend/link-plan.d.ts +37 -0
  25. package/dist/backend/link-plan.js +26 -0
  26. package/dist/backend/link-plan.js.map +1 -0
  27. package/dist/backend/linker.d.ts +22 -0
  28. package/dist/backend/linker.js +236 -0
  29. package/dist/backend/linker.js.map +1 -0
  30. package/dist/backend/llvm/blocks.js.map +1 -1
  31. package/dist/backend/llvm/dyn.js +63 -3
  32. package/dist/backend/llvm/dyn.js.map +1 -1
  33. package/dist/backend/llvm/emitter.js +132 -26
  34. package/dist/backend/llvm/emitter.js.map +1 -1
  35. package/dist/backend/llvm/expr-async.js +85 -3
  36. package/dist/backend/llvm/expr-async.js.map +1 -1
  37. package/dist/backend/llvm/expr-bytes.js.map +1 -1
  38. package/dist/backend/llvm/expr-calls.js.map +1 -1
  39. package/dist/backend/llvm/expr-containers.js +68 -54
  40. package/dist/backend/llvm/expr-containers.js.map +1 -1
  41. package/dist/backend/llvm/expr-context.d.ts +2 -1
  42. package/dist/backend/llvm/expr-control.js.map +1 -1
  43. package/dist/backend/llvm/expr-dispatch.js +2 -0
  44. package/dist/backend/llvm/expr-dispatch.js.map +1 -1
  45. package/dist/backend/llvm/expr-dynamic.js +6 -3
  46. package/dist/backend/llvm/expr-dynamic.js.map +1 -1
  47. package/dist/backend/llvm/expr-island.js.map +1 -1
  48. package/dist/backend/llvm/expr-primitives.d.ts +1 -1
  49. package/dist/backend/llvm/expr-primitives.js +20 -0
  50. package/dist/backend/llvm/expr-primitives.js.map +1 -1
  51. package/dist/backend/llvm/expr-records.js.map +1 -1
  52. package/dist/backend/llvm/expr-stream-bridges.js +245 -10
  53. package/dist/backend/llvm/expr-stream-bridges.js.map +1 -1
  54. package/dist/backend/llvm/lib-dispatch.js +2 -0
  55. package/dist/backend/llvm/lib-dispatch.js.map +1 -1
  56. package/dist/backend/llvm/lib-shared.js +18 -0
  57. package/dist/backend/llvm/lib-shared.js.map +1 -1
  58. package/dist/backend/llvm/lib-stream.js +6 -3
  59. package/dist/backend/llvm/lib-stream.js.map +1 -1
  60. package/dist/backend/llvm/split.js.map +1 -1
  61. package/dist/backend/llvm/unsupported.js.map +1 -1
  62. package/dist/backend/llvm/walkers.js +31 -4
  63. package/dist/backend/llvm/walkers.js.map +1 -1
  64. package/dist/backend/native-codegen.d.ts +8 -1
  65. package/dist/backend/native-codegen.js +39 -25
  66. package/dist/backend/native-codegen.js.map +1 -1
  67. package/dist/backend/native-codegen.test.js +44 -4
  68. package/dist/backend/native-codegen.test.js.map +1 -1
  69. package/dist/backend/native-link-info.d.ts +1 -1
  70. package/dist/backend/native-link-info.js +42 -18
  71. package/dist/backend/native-link-info.js.map +1 -1
  72. package/dist/backend/native-toolchain.js.map +1 -1
  73. package/dist/backend/native-toolchain.test.js +5 -1
  74. package/dist/backend/native-toolchain.test.js.map +1 -1
  75. package/dist/backend/runtime-pack-targets.test.d.ts +1 -0
  76. package/dist/backend/runtime-pack-targets.test.js +54 -0
  77. package/dist/backend/runtime-pack-targets.test.js.map +1 -0
  78. package/dist/backend/runtime-pack.d.ts +5 -30
  79. package/dist/backend/runtime-pack.js +46 -141
  80. package/dist/backend/runtime-pack.js.map +1 -1
  81. package/dist/backend/runtime-pack.test.js +49 -14
  82. package/dist/backend/runtime-pack.test.js.map +1 -1
  83. package/dist/backend/targets.d.ts +63 -17
  84. package/dist/backend/targets.js +251 -35
  85. package/dist/backend/targets.js.map +1 -1
  86. package/dist/backend/targets.test.js +27 -5
  87. package/dist/backend/targets.test.js.map +1 -1
  88. package/dist/backend/vendor-archives.js.map +1 -1
  89. package/dist/compat/fetch-profile.js +4 -2
  90. package/dist/compat/fetch-profile.js.map +1 -1
  91. package/dist/coverage/report.js.map +1 -1
  92. package/dist/coverage/surface-manifest.js +22 -1
  93. package/dist/coverage/surface-manifest.js.map +1 -1
  94. package/dist/diagnostics/diagnostic.d.ts +6 -5
  95. package/dist/diagnostics/diagnostic.js +7 -6
  96. package/dist/diagnostics/diagnostic.js.map +1 -1
  97. package/dist/diagnostics/render.js.map +1 -1
  98. package/dist/ffi/ffi-manifest.js.map +1 -1
  99. package/dist/frontend/cjs-lexer.js.map +1 -1
  100. package/dist/frontend/dts-paths.js.map +1 -1
  101. package/dist/frontend/input-tracker.d.ts +10 -0
  102. package/dist/frontend/input-tracker.js +26 -3
  103. package/dist/frontend/input-tracker.js.map +1 -1
  104. package/dist/frontend/input-tracker.test.js +8 -1
  105. package/dist/frontend/input-tracker.test.js.map +1 -1
  106. package/dist/frontend/lib-contract.js.map +1 -1
  107. package/dist/frontend/lowering/array-values.d.ts +12 -0
  108. package/dist/frontend/lowering/array-values.js +76 -0
  109. package/dist/frontend/lowering/array-values.js.map +1 -0
  110. package/dist/frontend/lowering/callback-arg.js.map +1 -1
  111. package/dist/frontend/lowering/containers/bytes.d.ts +35 -0
  112. package/dist/frontend/lowering/containers/bytes.js +1021 -0
  113. package/dist/frontend/lowering/containers/bytes.js.map +1 -0
  114. package/dist/frontend/lowering/containers/string-and-regexp.d.ts +17 -0
  115. package/dist/frontend/lowering/containers/string-and-regexp.js +345 -0
  116. package/dist/frontend/lowering/containers/string-and-regexp.js.map +1 -0
  117. package/dist/frontend/lowering/expressions/evaluation-safety.d.ts +13 -0
  118. package/dist/frontend/lowering/expressions/evaluation-safety.js +76 -0
  119. package/dist/frontend/lowering/expressions/evaluation-safety.js.map +1 -0
  120. package/dist/frontend/lowering/expressions/object-literals.d.ts +44 -0
  121. package/dist/frontend/lowering/expressions/object-literals.js +1815 -0
  122. package/dist/frontend/lowering/expressions/object-literals.js.map +1 -0
  123. package/dist/frontend/lowering/expressions/optional-chains.d.ts +31 -0
  124. package/dist/frontend/lowering/expressions/optional-chains.js +382 -0
  125. package/dist/frontend/lowering/expressions/optional-chains.js.map +1 -0
  126. package/dist/frontend/lowering/expressions/try-lower-expression.d.ts +7 -0
  127. package/dist/frontend/lowering/expressions/try-lower-expression.js +26 -0
  128. package/dist/frontend/lowering/expressions/try-lower-expression.js.map +1 -0
  129. package/dist/frontend/lowering/lib-boundary.js +7 -2
  130. package/dist/frontend/lowering/lib-boundary.js.map +1 -1
  131. package/dist/frontend/lowering/lower-array-sort.d.ts +22 -0
  132. package/dist/frontend/lowering/lower-array-sort.js +619 -0
  133. package/dist/frontend/lowering/lower-array-sort.js.map +1 -0
  134. package/dist/frontend/lowering/lower-assert.js.map +1 -1
  135. package/dist/frontend/lowering/lower-async-iteration.d.ts +7 -0
  136. package/dist/frontend/lowering/lower-async-iteration.js +657 -0
  137. package/dist/frontend/lowering/lower-async-iteration.js.map +1 -0
  138. package/dist/frontend/lowering/lower-builtins.d.ts +18 -0
  139. package/dist/frontend/lowering/lower-builtins.js +379 -48
  140. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  141. package/dist/frontend/lowering/lower-calls.d.ts +16 -8
  142. package/dist/frontend/lowering/lower-calls.js +667 -91
  143. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  144. package/dist/frontend/lowering/lower-classes.d.ts +7 -13
  145. package/dist/frontend/lowering/lower-classes.js +128 -35
  146. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  147. package/dist/frontend/lowering/lower-comptime.js.map +1 -1
  148. package/dist/frontend/lowering/lower-containers.d.ts +1 -47
  149. package/dist/frontend/lowering/lower-containers.js +779 -2038
  150. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  151. package/dist/frontend/lowering/lower-event-emitter.js +51 -5
  152. package/dist/frontend/lowering/lower-event-emitter.js.map +1 -1
  153. package/dist/frontend/lowering/lower-exprs.d.ts +4 -78
  154. package/dist/frontend/lowering/lower-exprs.js +1128 -2409
  155. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  156. package/dist/frontend/lowering/lower-generators.d.ts +18 -3
  157. package/dist/frontend/lowering/lower-generators.js +145 -9
  158. package/dist/frontend/lowering/lower-generators.js.map +1 -1
  159. package/dist/frontend/lowering/lower-inspect.js +50 -36
  160. package/dist/frontend/lowering/lower-inspect.js.map +1 -1
  161. package/dist/frontend/lowering/lower-island.d.ts +11 -4
  162. package/dist/frontend/lowering/lower-island.js +47 -10
  163. package/dist/frontend/lowering/lower-island.js.map +1 -1
  164. package/dist/frontend/lowering/lower-modules.js +52 -15
  165. package/dist/frontend/lowering/lower-modules.js.map +1 -1
  166. package/dist/frontend/lowering/lower-node-module.d.ts +18 -0
  167. package/dist/frontend/lowering/lower-node-module.js +302 -0
  168. package/dist/frontend/lowering/lower-node-module.js.map +1 -0
  169. package/dist/frontend/lowering/lower-server.js +11 -1
  170. package/dist/frontend/lowering/lower-server.js.map +1 -1
  171. package/dist/frontend/lowering/lower-stmts.d.ts +5 -24
  172. package/dist/frontend/lowering/lower-stmts.js +811 -314
  173. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  174. package/dist/frontend/lowering/lower-stream.js +4 -4
  175. package/dist/frontend/lowering/lower-stream.js.map +1 -1
  176. package/dist/frontend/lowering/lowerer.d.ts +109 -34
  177. package/dist/frontend/lowering/lowerer.js +1225 -62
  178. package/dist/frontend/lowering/lowerer.js.map +1 -1
  179. package/dist/frontend/lowering/optional-arguments.d.ts +9 -0
  180. package/dist/frontend/lowering/optional-arguments.js +57 -0
  181. package/dist/frontend/lowering/optional-arguments.js.map +1 -0
  182. package/dist/frontend/lowering/surfaces.d.ts +10 -2
  183. package/dist/frontend/lowering/surfaces.js +15 -3
  184. package/dist/frontend/lowering/surfaces.js.map +1 -1
  185. package/dist/frontend/npm-static-declarations.d.ts +35 -0
  186. package/dist/frontend/npm-static-declarations.js +506 -0
  187. package/dist/frontend/npm-static-declarations.js.map +1 -0
  188. package/dist/frontend/npm-static-declarations.test.d.ts +1 -0
  189. package/dist/frontend/npm-static-declarations.test.js +122 -0
  190. package/dist/frontend/npm-static-declarations.test.js.map +1 -0
  191. package/dist/frontend/npm-static.d.ts +2 -0
  192. package/dist/frontend/npm-static.js +26 -10
  193. package/dist/frontend/npm-static.js.map +1 -1
  194. package/dist/frontend/npm.d.ts +60 -0
  195. package/dist/frontend/npm.js +38 -4
  196. package/dist/frontend/npm.js.map +1 -1
  197. package/dist/frontend/npm.test.d.ts +1 -0
  198. package/dist/frontend/npm.test.js +50 -0
  199. package/dist/frontend/npm.test.js.map +1 -0
  200. package/dist/frontend/program.d.ts +3 -2
  201. package/dist/frontend/program.js +495 -68
  202. package/dist/frontend/program.js.map +1 -1
  203. package/dist/frontend/resolve.d.ts +10 -2
  204. package/dist/frontend/resolve.js +90 -14
  205. package/dist/frontend/resolve.js.map +1 -1
  206. package/dist/frontend/resolve.test.js +4 -1
  207. package/dist/frontend/resolve.test.js.map +1 -1
  208. package/dist/frontend/runtime-resolve.d.ts +22 -0
  209. package/dist/frontend/runtime-resolve.js +101 -0
  210. package/dist/frontend/runtime-resolve.js.map +1 -0
  211. package/dist/frontend/runtime-resolve.test.d.ts +1 -0
  212. package/dist/frontend/runtime-resolve.test.js +18 -0
  213. package/dist/frontend/runtime-resolve.test.js.map +1 -0
  214. package/dist/frontend/ts7/adapter.js +12 -13
  215. package/dist/frontend/ts7/adapter.js.map +1 -1
  216. package/dist/frontend/ts7/ast.js.map +1 -1
  217. package/dist/frontend/ts7/census-check.js +3 -3
  218. package/dist/frontend/ts7/census-check.js.map +1 -1
  219. package/dist/frontend/ts7/checker.d.ts +3 -0
  220. package/dist/frontend/ts7/checker.js +12 -3
  221. package/dist/frontend/ts7/checker.js.map +1 -1
  222. package/dist/frontend/ts7/enums.js.map +1 -1
  223. package/dist/frontend/ts7/program-adapter.js +42 -3
  224. package/dist/frontend/ts7/program-adapter.js.map +1 -1
  225. package/dist/frontend/type-mapper.js +120 -7
  226. package/dist/frontend/type-mapper.js.map +1 -1
  227. package/dist/index.d.ts +3 -3
  228. package/dist/index.js +50 -28
  229. package/dist/index.js.map +1 -1
  230. package/dist/ir/analysis.js.map +1 -1
  231. package/dist/ir/ir.d.ts +125 -41
  232. package/dist/ir/ir.js +82 -31
  233. package/dist/ir/ir.js.map +1 -1
  234. package/dist/ir/serialize.d.ts +1 -1
  235. package/dist/ir/serialize.js +1 -1
  236. package/dist/ir/validate.js +127 -95
  237. package/dist/ir/validate.js.map +1 -1
  238. package/dist/library/cache-primitives.js.map +1 -1
  239. package/dist/library/cache-reset.test.js.map +1 -1
  240. package/dist/library/compiler-self-identity.js.map +1 -1
  241. package/dist/library/fence-eval.js +3 -1
  242. package/dist/library/fence-eval.js.map +1 -1
  243. package/dist/library/int-infer.js +12 -1
  244. package/dist/library/int-infer.js.map +1 -1
  245. package/dist/library/int-infer.test.js +4 -4
  246. package/dist/library/int-infer.test.js.map +1 -1
  247. package/dist/library/library-cache.test.js +5 -5
  248. package/dist/library/library-profile.js.map +1 -1
  249. package/dist/library/sidecar-validate.js.map +1 -1
  250. package/dist/library/sidecar.js.map +1 -1
  251. package/dist/startup-cache.d.ts +9 -1
  252. package/dist/startup-cache.js +16 -1
  253. package/dist/startup-cache.js.map +1 -1
  254. package/dist/wasi-paths.js.map +1 -1
  255. package/package.json +18 -5
  256. package/surface-manifest.json +64 -8
@@ -1,97 +1,46 @@
1
1
  import { InternalCompilerError } from "../../errors.js";
2
2
  /* Container-surface call lowering: array methods (including the HOF family
3
3
  * map/filter/forEach with their synthesized helper functions), Map/Set
4
- * methods with Node-exact forEach desugaring, and the string and regex
5
- * method surfaces. */
4
+ * methods with Node-exact forEach desugaring, iterator helpers, and indexed
5
+ * record operations. */
6
6
  import * as ts from "../ts7/adapter.js";
7
- import { BOOL, BYTES_U8, CAUGHT, DYN, F64, JSVAL, STRING, UNDEFINED_T, VOID, arrayOf, bytesOf, funcOf, isRefCounted, isSupportedArrayElem, isSupportedIndexValue, isUnitType, typeEquals } from "../../ir/ir.js";
8
- import { ARRAY_METHODS, MAP_METHODS, SET_COMBINE_METHODS, SET_METHODS, STR_METHODS } from "./surfaces.js";
9
- import { droppableStatic, isRequireMainFilename, lowerDynObjectLiteral, probeLower, pureReemittable } from "./lower-exprs.js";
7
+ import { BOOL, CAUGHT, DYN, F64, JSVAL, STRING, UNDEFINED_T, VOID, arrayOf, funcOf, isRefCounted, isSupportedArrayElem, isSupportedIndexValue, isUnitType, typeEquals } from "../../ir/ir.js";
8
+ import { ARRAY_METHODS, MAP_METHODS, SET_COMBINE_METHODS, SET_METHODS } from "./surfaces.js";
9
+ import { tryLowerExpression } from "./expressions/try-lower-expression.js";
10
+ import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
10
11
  import { forOfVarTarget, lowerDestructuringAssign } from "./lower-stmts.js";
11
12
  import { isJsSourceFile, locOf } from "../program.js";
12
13
  import { islandPrimitiveExit, lowerDynDispatchMethodCall } from "./lower-calls.js";
14
+ import { buildArraySortFn } from "./lower-array-sort.js";
15
+ import { arrayIndexPresent, arrayValueRead, arrayValueStore, arrayValueType, currentArrayIndexPresent } from "./array-values.js";
13
16
  import { typeKey } from "../type-mapper.js";
14
- import { dynUndefinedExpr, own } from "./lowerer.js";
15
17
  import { boolLit, countedFor, numLit, varRef } from "../../ir/build.js";
16
- /** Lower an expression whose checker type is statically `undefined`/`void`.
17
- * Optional builtin arguments use this before their ordinary expected-type
18
- * coercion so every equivalent spelling (`undefined`, `void 0`, a typed
19
- * binding, or an effectful call returning undefined) selects the default.
20
- * An effectful `void e` is exact in this context: evaluate e, then produce
21
- * undefined, instead of hitting value-position void's general fence. */
22
- function lowerStaticallyUndefinedArg(lowerer, node) {
23
- const peelErasableWrappers = (value) => {
24
- let expr = value;
25
- while (ts.isParenthesizedExpression(expr) ||
26
- ts.isAsExpression(expr) ||
27
- ts.isTypeAssertion(expr) ||
28
- ts.isSatisfiesExpression(expr)) {
29
- expr = expr.expression;
30
- }
31
- return expr;
32
- };
33
- let expr = node;
34
- while (ts.isParenthesizedExpression(expr))
35
- expr = expr.expression;
36
- if ((lowerer.typeOf(expr).flags &
37
- (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) ===
38
- 0) {
39
- return null;
40
- }
41
- // TypeScript erases `as`, angle-bracket assertions, and `satisfies`.
42
- // Peel them only for the void recognition: a non-void assertion still
43
- // lowers through the ordinary path below, preserving the checked-dynamic
44
- // cast discipline. Nested voids add no effects of their own, so the one
45
- // innermost operand carries the complete runtime evaluation.
46
- expr = peelErasableWrappers(expr);
47
- let sawVoid = false;
48
- while (ts.isVoidExpression(expr)) {
49
- sawVoid = true;
50
- expr = peelErasableWrappers(expr.expression);
51
- }
52
- if (sawVoid) {
53
- // The caller discards this token before producing the parameter
54
- // default, so the operand itself carries exactly the required effects;
55
- // no bare undefined value needs to enter the IR.
56
- return lowerer.lowerExpr(expr);
57
- }
58
- return lowerer.lowerExpr(node);
59
- }
60
- /** Preserve a statically-undefined argument's effects, then answer the
61
- * optional parameter's already-lowered default value. */
62
- function defaultAfterUndefined(value, defaultValue) {
63
- if (droppableStatic(value))
64
- return defaultValue;
65
- return {
66
- kind: "seqExpr",
67
- stmts: [{ kind: "exprStmt", expr: value, loc: value.loc }],
68
- result: defaultValue,
69
- type: defaultValue.type,
70
- loc: value.loc,
71
- };
72
- }
73
- function lowerOptionalDefaultArg(lowerer, node, expected, defaultValue) {
74
- const undefinedArg = lowerStaticallyUndefinedArg(lowerer, node);
75
- if (undefinedArg)
76
- return defaultAfterUndefined(undefinedArg, defaultValue);
77
- const value = lowerer.lowerExpr(node);
78
- if (value.type.kind === "union") {
79
- const def = lowerer.unions.get(value.type.unionId);
80
- if (def?.arms.length === 2 &&
81
- def.arms.some((arm) => arm.kind === "undefinedT") &&
82
- def.arms.some((arm) => typeEquals(arm, expected))) {
83
- // A runtime optional argument uses its API default only on the
84
- // undefined arm. The exact two-arm gate deliberately excludes null:
85
- // optional parameters do not admit it, and an asserted null must keep
86
- // the ordinary checked-narrow failure instead of defaulting.
87
- return { kind: "nullish", left: value, right: defaultValue, type: expected, loc: value.loc };
88
- }
18
+ import { lowerOptionalArgument } from "./optional-arguments.js";
19
+ /** Build the argument array for a mutating/copying operation while keeping
20
+ * the receiver's scalar payload ABI. A missing read is represented as the
21
+ * present UNDEFINED state in this temporary, so push/unshift and toSpliced
22
+ * materialize it through their ordinary indexed/iterator copies. */
23
+ function lowerArrayValueItems(lowerer, values, elem, arrType, loc) {
24
+ const out = lowerer.declareHiddenLocal("%arrayItems", arrType);
25
+ const outRef = varRef(out.id, arrType, loc);
26
+ const length = () => ({
27
+ kind: "arrIntrinsic",
28
+ method: "length",
29
+ receiver: outRef,
30
+ args: [],
31
+ type: F64,
32
+ loc,
33
+ });
34
+ const body = [
35
+ { kind: "varDecl", localId: out.id, init: { kind: "arrayLit", elems: [], type: arrType, loc }, loc },
36
+ ];
37
+ for (const value of values) {
38
+ const stored = lowerer.runtimeOptionalWidening(value.type, elem) !== null
39
+ ? value
40
+ : lowerer.coerceToExpected(value, elem);
41
+ body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc));
89
42
  }
90
- return lowerer.coerceInto(node, value, expected);
91
- }
92
- function lowerSplitLimitArg(lowerer, node, loc) {
93
- const defaultValue = { kind: "numLit", value: 4294967295, type: F64, loc };
94
- return node ? lowerOptionalDefaultArg(lowerer, node, F64, defaultValue) : defaultValue;
43
+ return { kind: "seqExpr", stmts: body, result: outRef, type: arrType, loc };
95
44
  }
96
45
  /** Callback-driven array producers bypass mapType's ordinary T[] mapping:
97
46
  * the callback is lowered first, then the helper's result array is built
@@ -119,6 +68,9 @@ export function lowerArrayMethodCall(lowerer, call, access) {
119
68
  if (!ARRAY_METHODS.has(name) && name !== "sort" && name !== "shift" && name !== "splice")
120
69
  return null;
121
70
  let receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
71
+ const checkerReceiver = lowerer.checker.getTypeAtLocation(access.expression);
72
+ const implicitArrayReceiver = receiverIr?.kind === "array" &&
73
+ (checkerReceiver.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown) || lowerer.checkerAnyArrayType(checkerReceiver));
122
74
  // A checker-`any[]` receiver (the readonly-array Array.isArray quirk)
123
75
  // whose VALUE lowers to a real static array (maybeNarrow's isArray
124
76
  // bridge): ride the ordinary tables on the lowered element type — the
@@ -127,7 +79,10 @@ export function lowerArrayMethodCall(lowerer, call, access) {
127
79
  // the isStdlibMember gate is skipped for probed receivers (nothing
128
80
  // user-declared can shadow a method on a value the checker calls any).
129
81
  let probedUntyped = false;
130
- if (!receiverIr &&
82
+ if (implicitArrayReceiver)
83
+ probedUntyped = true;
84
+ if ((receiverIr === null || receiverIr.kind === "dyn" ||
85
+ (receiverIr.kind === "array" && receiverIr.elem.kind === "dyn")) &&
131
86
  (lowerer.checkerAnyArray(access.expression) ||
132
87
  // A checker-`any` CHAIN whose value lowers to a real static array
133
88
  // (`context.stack.split('\n').slice(2)` — the dyn-receiver string
@@ -227,12 +182,62 @@ export function lowerArrayMethodCall(lowerer, call, access) {
227
182
  }
228
183
  const receiver = lowerer.lowerExpr(access.expression);
229
184
  const index = lowerer.lowerExprExpecting(call.arguments[0], F64);
230
- const value = lowerer.coerceInto(call.arguments[1], lowerer.lowerExpr(call.arguments[1]), elem);
185
+ const value = lowerer.lowerExpr(call.arguments[1]);
186
+ if (value.type.kind === "union" && lowerer.runtimeOptionalWidening(value.type, elem) !== null) {
187
+ // `.with()` validates its index and returns a fresh copy. Stabilize
188
+ // all three operands, then select the scalar replacement or the
189
+ // present-undefined copy without evaluating a read twice.
190
+ const receiverLocal = lowerer.declareHiddenLocal("%withRecv", receiverIr);
191
+ const indexLocal = lowerer.declareHiddenLocal("%withIndex", F64);
192
+ const valueLocal = lowerer.declareHiddenLocal("%withValue", value.type);
193
+ const unionId = value.type.unionId;
194
+ const receiverRef = varRef(receiverLocal.id, receiverIr, loc);
195
+ const indexRef = varRef(indexLocal.id, F64, loc);
196
+ const valueRef = varRef(valueLocal.id, value.type, loc);
197
+ const undefTag = lowerer.armTag(unionId, UNDEFINED_T);
198
+ const condition = {
199
+ kind: "unionIsTag",
200
+ unionId,
201
+ tag: undefTag,
202
+ negated: false,
203
+ value: valueRef,
204
+ type: BOOL,
205
+ loc,
206
+ };
207
+ const missing = {
208
+ kind: "arrIntrinsic",
209
+ method: "withUndefined",
210
+ receiver: receiverRef,
211
+ args: [indexRef],
212
+ type: receiverIr,
213
+ loc,
214
+ };
215
+ const present = {
216
+ kind: "arrIntrinsic",
217
+ method: "with",
218
+ receiver: receiverRef,
219
+ args: [indexRef, lowerer.coerceToExpected(valueRef, elem)],
220
+ type: receiverIr,
221
+ loc,
222
+ };
223
+ return {
224
+ kind: "seqExpr",
225
+ stmts: [
226
+ { kind: "varDecl", localId: receiverLocal.id, init: receiver, loc },
227
+ { kind: "varDecl", localId: indexLocal.id, init: index, loc },
228
+ { kind: "varDecl", localId: valueLocal.id, init: value, loc },
229
+ ],
230
+ result: { kind: "ternary", cond: condition, then: missing, else_: present, type: receiverIr, loc },
231
+ type: receiverIr,
232
+ loc,
233
+ };
234
+ }
235
+ const coercedValue = lowerer.coerceInto(call.arguments[1], value, elem);
231
236
  return {
232
237
  kind: "arrIntrinsic",
233
238
  method: "with",
234
239
  receiver,
235
- args: [index, value],
240
+ args: [index, coercedValue],
236
241
  type: receiverIr,
237
242
  loc,
238
243
  };
@@ -254,14 +259,19 @@ export function lowerArrayMethodCall(lowerer, call, access) {
254
259
  const deleteCount = call.arguments.length === 0
255
260
  ? { kind: "numLit", value: 0, type: F64, loc }
256
261
  : call.arguments[1]
257
- ? lowerOptionalDefaultArg(lowerer, call.arguments[1], F64, deleteCountDefault)
262
+ ? lowerOptionalArgument(lowerer, call.arguments[1], F64, deleteCountDefault)
258
263
  : { 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
- };
264
+ const itemNodes = call.arguments.slice(2);
265
+ const itemProbes = itemNodes.map((arg) => tryLowerExpression(lowerer, arg));
266
+ const statefulItems = itemProbes.some((probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null);
267
+ const items = statefulItems
268
+ ? lowerArrayValueItems(lowerer, itemNodes.map((arg) => lowerer.lowerExpr(arg)), elem, receiverIr, loc)
269
+ : {
270
+ kind: "arrayLit",
271
+ elems: itemNodes.map((arg) => lowerer.coerceInto(arg, lowerer.lowerExpr(arg), elem)),
272
+ type: receiverIr,
273
+ loc,
274
+ };
265
275
  return {
266
276
  kind: "arrIntrinsic",
267
277
  method: "toSpliced",
@@ -299,8 +309,17 @@ export function lowerArrayMethodCall(lowerer, call, access) {
299
309
  : undefined;
300
310
  lowerer.noLowering(`.${name} with ${call.arguments.length} argument${call.arguments.length === 1 ? "" : "s"}`, call, hint);
301
311
  }
302
- if (name === "push" || name === "unshift" || name === "pop") {
303
- const receiver = lowerer.lowerExpr(access.expression);
312
+ if (name === "push" || name === "unshift") {
313
+ let receiver = lowerer.lowerExpr(access.expression);
314
+ if (receiver.type.kind === "union" && lowerer.armTag(receiver.type.unionId, UNDEFINED_T) >= 0) {
315
+ const present = lowerer.stripUndefinedArm(receiver.type);
316
+ const helper = present.kind === "array"
317
+ ? lowerer.narrowedArmHelper(receiver.type.unionId, present, loc)
318
+ : null;
319
+ receiver = helper
320
+ ? { kind: "call", callee: helper, args: [receiver], type: present, loc }
321
+ : lowerer.maybeNarrow(receiver, access.expression);
322
+ }
304
323
  // `a.push(...src)` / `a.unshift(...src)`: copy src's elements in
305
324
  // order (count snapshotted first, so the self-spread forms duplicate
306
325
  // like JS) and return the new length.
@@ -326,6 +345,18 @@ export function lowerArrayMethodCall(lowerer, call, access) {
326
345
  loc,
327
346
  };
328
347
  }
348
+ const valueProbes = call.arguments.map((arg) => tryLowerExpression(lowerer, arg));
349
+ if (valueProbes.some((probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null)) {
350
+ const items = lowerArrayValueItems(lowerer, call.arguments.map((arg) => lowerer.lowerExpr(arg)), elem, receiverIr, loc);
351
+ return {
352
+ kind: "arrIntrinsic",
353
+ method: name === "push" ? "pushSpread" : "unshiftSpread",
354
+ receiver,
355
+ args: [items],
356
+ type: F64,
357
+ loc,
358
+ };
359
+ }
329
360
  // Inserted values flow into the element slot like an assignment would:
330
361
  // union-element arrays wrap plain arm values (coerceInto is inert
331
362
  // when the types already agree).
@@ -335,39 +366,31 @@ export function lowerArrayMethodCall(lowerer, call, access) {
335
366
  method: name,
336
367
  receiver,
337
368
  args,
338
- type: name === "push" || name === "unshift" ? F64 : elem,
369
+ type: F64,
339
370
  loc,
340
371
  };
341
372
  }
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)");
373
+ if (name === "pop") {
374
+ // Like shift(), pop() returns undefined for an empty array. Union
375
+ // element arrays remain fenced here because their existing boxed
376
+ // element already carries the result union and a second undefined
377
+ // wrapper would lose the element identity contract.
378
+ if (elem.kind === "union" && lowerer.armTag(elem.unionId, UNDEFINED_T) < 0) {
379
+ lowerer.unsupported("SC1090", call, "'.pop()' on union-element arrays without an undefined arm (read the last element and splice it after narrowing instead)");
350
380
  }
351
381
  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
382
  return {
363
383
  kind: "arrIntrinsic",
364
- method: name,
384
+ method: "pop",
365
385
  receiver,
366
- args: [needle],
367
- type: name === "indexOf" ? F64 : BOOL,
386
+ args: [],
387
+ type: arrayValueType(lowerer, elem),
368
388
  loc,
369
389
  };
370
390
  }
391
+ if (name === "indexOf" || name === "includes") {
392
+ return lowerArraySearchCall(lowerer, call, access, name, elem);
393
+ }
371
394
  if (name === "concat") {
372
395
  // `a.concat(x, ys, ...)` — a fresh array of a's elements followed by
373
396
  // each argument in order: JS spreads array arguments one level
@@ -385,8 +408,12 @@ export function lowerArrayMethodCall(lowerer, call, access) {
385
408
  lowerer.unsupported("SC1090", argNode, "spread arguments to concat (pass the array itself — concat already spreads array arguments)");
386
409
  }
387
410
  const argIr = lowerer.mapTypeOf(lowerer.typeOf(argNode));
388
- if (argIr !== null && argIr.kind === "array" && typeEquals(argIr.elem, elem)) {
389
- if (elem.kind === "array" && typeEquals(argIr, elem)) {
411
+ const probedArg = probedUntyped ? tryLowerExpression(lowerer, argNode) : null;
412
+ const argArrayType = probedArg?.type.kind === "array"
413
+ ? probedArg.type
414
+ : argIr?.kind === "array" ? argIr : null;
415
+ if (argArrayType !== null && typeEquals(argArrayType.elem, elem)) {
416
+ if (elem.kind === "array" && typeEquals(argArrayType, elem)) {
390
417
  // number[][].concat(inner: number[]) — TS says element, JS's
391
418
  // IsArray says spread; no honest static answer exists.
392
419
  lowerer.unsupported("SC1090", argNode, "concat of a bare inner array onto an array-of-arrays (JS would SPREAD it one level — wrap it: a.concat([inner]))");
@@ -455,11 +482,11 @@ export function lowerArrayMethodCall(lowerer, call, access) {
455
482
  // arrays are fenced: their shift result would collapse arms
456
483
  // (`(string | undefined)[]`'s shift is `string | undefined` too, and
457
484
  // 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)");
485
+ if (elem.kind === "union" && lowerer.armTag(elem.unionId, UNDEFINED_T) < 0) {
486
+ 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
487
  }
461
488
  const receiver = lowerer.lowerExpr(access.expression);
462
- return { kind: "arrIntrinsic", method: "shift", receiver, args: [], type: lowerer.withUndefinedArm(elem), loc };
489
+ return { kind: "arrIntrinsic", method: "shift", receiver, args: [], type: arrayValueType(lowerer, elem), loc };
463
490
  }
464
491
  if (name === "join") {
465
492
  // The ambient declares join on every Array<T> (a per-element-type
@@ -487,7 +514,7 @@ export function lowerArrayMethodCall(lowerer, call, access) {
487
514
  }
488
515
  if (name === "find" || name === "findIndex" || name === "findLast" ||
489
516
  name === "findLastIndex" || name === "some" || name === "every") {
490
- return lowerArrayFindLikeCall(lowerer, call, access, name, elem);
517
+ return lowerArrayFindLikeCall(lowerer, call, access, name, elem, probedUntyped);
491
518
  }
492
519
  if (name === "at")
493
520
  return lowerArrayAtCall(lowerer, call, access, elem);
@@ -496,6 +523,217 @@ export function lowerArrayMethodCall(lowerer, call, access) {
496
523
  // reduce / reduceRight
497
524
  return lowerArrayReduceCall(lowerer, call, access, name, elem);
498
525
  }
526
+ /** `indexOf` and `includes` normally use the runtime's typed search helpers,
527
+ * but an indexed array read is a `T | undefined` value. Keep that union
528
+ * through a small lowered loop so the needle's missing state is observable:
529
+ * indexOf skips holes, while includes treats holes as undefined. The same
530
+ * loop also gives union-element arrays their value equality rather than
531
+ * comparing the compiler's union boxes by pointer. */
532
+ function lowerArraySearchCall(lowerer, call, access, method, elem) {
533
+ const loc = locOf(call);
534
+ const receiver = lowerer.lowerExpr(access.expression);
535
+ const argNode = call.arguments[0];
536
+ let needle = lowerer.lowerExpr(argNode);
537
+ // A derived CLASS VALUE against a base-classval element widens (the same
538
+ // pointer — identity search is exact); the coercion path owns the ABI gate.
539
+ if (needle.type.kind === "classval" && elem.kind === "classval" && !typeEquals(needle.type, elem)) {
540
+ needle = lowerer.coerceInto(argNode, needle, elem);
541
+ }
542
+ // Preserve the existing fast path for an ordinary, non-union needle. Its
543
+ // runtime implementation already has exact primitive/reference equality.
544
+ if (elem.kind !== "union" && typeEquals(needle.type, elem)) {
545
+ return {
546
+ kind: "arrIntrinsic",
547
+ method,
548
+ receiver,
549
+ args: [needle],
550
+ type: method === "indexOf" ? F64 : BOOL,
551
+ loc,
552
+ };
553
+ }
554
+ const valueT = arrayValueType(lowerer, elem);
555
+ // jsval/dyn arrays have a single engine value representation rather than a
556
+ // static undefined arm. Keep their existing runtime path and its explicit
557
+ // type fence; typed static arrays take the state-aware helper below.
558
+ if (valueT.kind !== "union") {
559
+ if (!typeEquals(needle.type, elem))
560
+ lowerer.badType(argNode, lowerer.typeOf(argNode));
561
+ return {
562
+ kind: "arrIntrinsic",
563
+ method,
564
+ receiver,
565
+ args: [needle],
566
+ type: method === "indexOf" ? F64 : BOOL,
567
+ loc,
568
+ };
569
+ }
570
+ const searchNeedle = lowerer.coerceInto(argNode, needle, valueT);
571
+ if (!typeEquals(searchNeedle.type, valueT))
572
+ lowerer.badType(argNode, lowerer.typeOf(argNode));
573
+ const helper = arraySearchHelper(lowerer, method, elem, valueT, loc);
574
+ return { kind: "call", callee: helper, args: [receiver, searchNeedle], type: method === "indexOf" ? F64 : BOOL, loc };
575
+ }
576
+ /** SameValueZero over a static value union. `unionEq` supplies strict
577
+ * equality for every arm; the extra f64-arm test is the only difference for
578
+ * includes, where NaN equals NaN while +0 and -0 remain equal. */
579
+ function arraySearchEquality(lowerer, left, right, sameValueZero, loc) {
580
+ if (left.type.kind !== "union" || right.type.kind !== "union" || left.type.unionId !== right.type.unionId) {
581
+ return { kind: "bin", op: "===", left, right, type: BOOL, loc };
582
+ }
583
+ const unionId = left.type.unionId;
584
+ const strict = { kind: "unionEq", unionId, negated: false, sameValue: false, left, right, type: BOOL, loc };
585
+ if (!sameValueZero)
586
+ return strict;
587
+ const numberTag = lowerer.unions.get(unionId)?.arms.findIndex((arm) => arm.kind === "f64") ?? -1;
588
+ if (numberTag < 0)
589
+ return strict;
590
+ const leftIsNumber = { kind: "unionIsTag", unionId, tag: numberTag, negated: false, value: left, type: BOOL, loc };
591
+ const rightIsNumber = { kind: "unionIsTag", unionId, tag: numberTag, negated: false, value: right, type: BOOL, loc };
592
+ const leftNumber = { kind: "unionNarrow", unionId, tag: numberTag, value: left, type: F64, loc };
593
+ const rightNumber = { kind: "unionNarrow", unionId, tag: numberTag, value: right, type: F64, loc };
594
+ const bothNaN = {
595
+ kind: "logical",
596
+ op: "&&",
597
+ left: {
598
+ kind: "logical",
599
+ op: "&&",
600
+ left: leftIsNumber,
601
+ right: rightIsNumber,
602
+ type: BOOL,
603
+ loc,
604
+ },
605
+ right: {
606
+ kind: "logical",
607
+ op: "&&",
608
+ left: { kind: "libCall", fn: "num.isNaN", args: [leftNumber], type: BOOL, loc },
609
+ right: { kind: "libCall", fn: "num.isNaN", args: [rightNumber], type: BOOL, loc },
610
+ type: BOOL,
611
+ loc,
612
+ },
613
+ type: BOOL,
614
+ loc,
615
+ };
616
+ return { kind: "logical", op: "||", left: strict, right: bothNaN, type: BOOL, loc };
617
+ }
618
+ function arraySearchHelper(lowerer, method, elem, valueT, loc) {
619
+ const key = `${method}:${typeKey(elem)}:${typeKey(valueT)}`;
620
+ const existing = lowerer.arrHofHelpers.get(key);
621
+ if (existing)
622
+ return existing;
623
+ const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
624
+ lowerer.arrHofHelpers.set(key, name);
625
+ const arrT = arrayOf(elem);
626
+ const resultT = method === "indexOf" ? F64 : BOOL;
627
+ const arrRef = varRef("a.0", arrT, loc);
628
+ const indexRef = varRef("i.0", F64, loc);
629
+ const needleRef = varRef("needle.0", valueT, loc);
630
+ const stateRef = varRef("state.0", F64, loc);
631
+ const nextRef = varRef("next.0", F64, loc);
632
+ const rawRead = { kind: "arrayGet", arr: arrRef, index: indexRef, type: elem, loc };
633
+ const missing = lowerer.wrappedUndefined(valueT, loc);
634
+ if (!missing)
635
+ throw new InternalCompilerError("array search helper needs an undefined arm");
636
+ const value = {
637
+ kind: "ternary",
638
+ cond: { kind: "bin", op: "===", left: stateRef, right: numLit(1, loc), type: BOOL, loc },
639
+ then: typeEquals(elem, valueT) ? rawRead : lowerer.coerceToExpected(rawRead, valueT),
640
+ else_: missing,
641
+ type: valueT,
642
+ loc,
643
+ };
644
+ const equal = arraySearchEquality(lowerer, varRef("value.0", valueT, loc), needleRef, method === "includes", loc);
645
+ const match = method === "indexOf"
646
+ ? {
647
+ kind: "logical",
648
+ op: "&&",
649
+ left: { kind: "bin", op: "!==", left: stateRef, right: numLit(0, loc), type: BOOL, loc },
650
+ right: equal,
651
+ type: BOOL,
652
+ loc,
653
+ }
654
+ : equal;
655
+ const undefinedTag = lowerer.armTag(valueT.unionId, UNDEFINED_T);
656
+ if (undefinedTag < 0)
657
+ throw new InternalCompilerError("array search helper needs an undefined tag");
658
+ const needleIsUndefined = {
659
+ kind: "unionIsTag",
660
+ unionId: valueT.unionId,
661
+ tag: undefinedTag,
662
+ negated: false,
663
+ value: needleRef,
664
+ type: BOOL,
665
+ loc,
666
+ };
667
+ const advance = {
668
+ kind: "assign",
669
+ localId: "i.0",
670
+ value: { kind: "bin", op: "+", left: indexRef, right: numLit(1, loc), type: F64, loc },
671
+ loc,
672
+ };
673
+ const visitPresent = [
674
+ { kind: "varDecl", localId: "value.0", init: value, loc },
675
+ {
676
+ kind: "if",
677
+ cond: match,
678
+ then: [{ kind: "return", value: method === "indexOf" ? indexRef : boolLit(true, loc), loc }],
679
+ else_: null,
680
+ loc,
681
+ },
682
+ advance,
683
+ ];
684
+ const skipHole = [
685
+ {
686
+ kind: "varDecl",
687
+ localId: "next.0",
688
+ init: { kind: "arrIntrinsic", method: "nextPresent", receiver: arrRef, args: [indexRef], type: F64, loc },
689
+ loc,
690
+ },
691
+ ...(method === "includes"
692
+ ? [{ kind: "if", cond: needleIsUndefined, then: [{ kind: "return", value: boolLit(true, loc), loc }], else_: null, loc }]
693
+ : []),
694
+ { kind: "assign", localId: "i.0", value: nextRef, loc },
695
+ ];
696
+ const loopBody = [
697
+ { kind: "varDecl", localId: "state.0", init: { kind: "arrayState", arr: arrRef, index: indexRef, type: F64, loc }, loc },
698
+ {
699
+ kind: "if",
700
+ cond: { kind: "bin", op: "===", left: stateRef, right: numLit(0, loc), type: BOOL, loc },
701
+ then: skipHole,
702
+ else_: visitPresent,
703
+ loc,
704
+ },
705
+ ];
706
+ lowerer.liftedFns.push({
707
+ name,
708
+ params: [
709
+ { localId: "a.0", name: "a", type: arrT },
710
+ { localId: "needle.0", name: "needle", type: valueT },
711
+ ],
712
+ returnType: resultT,
713
+ locals: [
714
+ { id: "a.0", name: "a", type: arrT, mutable: true },
715
+ { id: "needle.0", name: "needle", type: valueT, mutable: true },
716
+ { id: "n.0", name: "n", type: F64, mutable: false },
717
+ { id: "i.0", name: "i", type: F64, mutable: true },
718
+ { id: "state.0", name: "state", type: F64, mutable: false },
719
+ { id: "next.0", name: "next", type: F64, mutable: false },
720
+ { id: "value.0", name: "value", type: valueT, mutable: false },
721
+ ],
722
+ body: [
723
+ readLenStmt(arrT, loc),
724
+ { kind: "varDecl", localId: "i.0", init: numLit(0, loc), loc },
725
+ {
726
+ kind: "while",
727
+ cond: { kind: "bin", op: "<", left: indexRef, right: varRef("n.0", F64, loc), type: BOOL, loc },
728
+ body: loopBody,
729
+ loc,
730
+ },
731
+ { kind: "return", value: method === "indexOf" ? numLit(-1, loc) : boolLit(false, loc), loc },
732
+ ],
733
+ loc,
734
+ });
735
+ return name;
736
+ }
499
737
  /** Lowers and validates a HOF callback argument. The lib declares optional
500
738
  * trailing (index, array) parameters after `lead` (the element for the
501
739
  * map family, accumulator + element for reduce); a callback may declare
@@ -503,7 +741,8 @@ export function lowerArrayMethodCall(lowerer, call, access) {
503
741
  * loop passes exactly what it declares (JS passes everything; a callback
504
742
  * only sees the parameters it names). Returns the lowered callback and
505
743
  * its declared arity. */
506
- function hofCallbackArg(lowerer, argNode, lead, arrT) {
744
+ function hofCallbackArg(lowerer, argNode, lead, arrT, bindUntyped = false) {
745
+ const full = [...lead, F64, arrT];
507
746
  // A DYN-receiver HOF's callback (`parsed.flatMap((value) => ...)`):
508
747
  // the contextual signature types the unannotated param `any` (the
509
748
  // receiver is checker-`any[]`), while the VALUE each call receives is
@@ -537,6 +776,43 @@ function hofCallbackArg(lowerer, argNode, lead, arrT) {
537
776
  lowerer.jsvalParamOverrides.add(p);
538
777
  });
539
778
  }
779
+ // A checker-untyped array expression whose lowered value supplies a
780
+ // concrete static element type still contextually types an inline HOF
781
+ // callback with any/unknown. Bind only unannotated parameters to the
782
+ // exact ABI the synthesized loop passes. The override is temporary:
783
+ // one callback AST may be lowered under several generic instances.
784
+ const contextual = [];
785
+ const previousImplicit = lowerer.implicitParamTypes;
786
+ let contextualTs = null;
787
+ if (bindUntyped && (ts.isArrowFunction(argNode) || ts.isFunctionExpression(argNode))) {
788
+ argNode.parameters.forEach((param, i) => {
789
+ const expected = full[i];
790
+ if (expected === undefined || expected.kind === "dyn" || expected.kind === "jsval" ||
791
+ !ts.isIdentifier(param.name) || param.type || param.initializer || param.dotDotDotToken) {
792
+ return;
793
+ }
794
+ const checkerType = lowerer.checker.getTypeAtLocation(param.name);
795
+ if ((checkerType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) === 0)
796
+ return;
797
+ const symbol = lowerer.checker.getSymbolAtLocation(param.name);
798
+ if (!symbol)
799
+ return;
800
+ contextual.push({ symbol, previous: lowerer.runtimeOptionalBindingTypes.get(symbol) });
801
+ lowerer.runtimeOptionalBindingTypes.set(symbol, expected);
802
+ const bodyExpected = expected.kind === "union" && lowerer.armTag(expected.unionId, UNDEFINED_T) >= 0
803
+ ? lowerer.stripUndefinedArm(expected)
804
+ : expected;
805
+ const expectedTs = bodyExpected.kind === "string" ? lowerer.checker.getStringType() :
806
+ bodyExpected.kind === "f64" ? lowerer.checker.getNumberType() :
807
+ bodyExpected.kind === "bool" ? lowerer.checker.getBooleanType() : null;
808
+ if (expectedTs !== null) {
809
+ contextualTs ??= new Map(previousImplicit ?? []);
810
+ contextualTs.set(symbol, expectedTs);
811
+ }
812
+ });
813
+ }
814
+ if (contextualTs !== null)
815
+ lowerer.implicitParamTypes = contextualTs;
540
816
  let fnArg;
541
817
  try {
542
818
  fnArg = lowerer.lowerExpr(argNode);
@@ -544,8 +820,14 @@ function hofCallbackArg(lowerer, argNode, lead, arrT) {
544
820
  finally {
545
821
  for (const n of overridden)
546
822
  lowerer.chainNarrowedType.delete(n);
823
+ for (const { symbol, previous } of contextual) {
824
+ if (previous === undefined)
825
+ lowerer.runtimeOptionalBindingTypes.delete(symbol);
826
+ else
827
+ lowerer.runtimeOptionalBindingTypes.set(symbol, previous);
828
+ }
829
+ lowerer.implicitParamTypes = previousImplicit;
547
830
  }
548
- const full = [...lead, F64, arrT];
549
831
  if (fnArg.type.kind !== "func" ||
550
832
  fnArg.type.params.length > full.length ||
551
833
  !fnArg.type.params.every((p, i) => typeEquals(p, full[i]))) {
@@ -572,7 +854,22 @@ function lowerArrayHofCall(lowerer, call, access, method, elem) {
572
854
  const argNode = call.arguments[0];
573
855
  if (!argNode)
574
856
  lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded
575
- const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [elem], arrayOf(elem));
857
+ const booleanFilter = method === "filter" &&
858
+ ts.isIdentifier(argNode) &&
859
+ argNode.text === "Boolean" &&
860
+ lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(argNode) ?? undefined);
861
+ const valueT = arrayValueType(lowerer, elem);
862
+ if (booleanFilter) {
863
+ if (valueT.kind === "union")
864
+ lowerer.requireTruthyUnion(valueT.unionId, argNode);
865
+ if (valueT.kind === "dyn" || valueT.kind === "jsval" || valueT.kind === "void" || isUnitType(valueT)) {
866
+ lowerer.badType(argNode, lowerer.typeOf(argNode));
867
+ }
868
+ }
869
+ const callback = booleanFilter
870
+ ? { fnArg: truthyFilterCallback(lowerer, valueT, loc), arity: 1 }
871
+ : hofCallbackArg(lowerer, argNode, [valueT], arrayOf(elem));
872
+ const { fnArg, arity } = callback;
576
873
  const fnRet = fnArg.type.ret;
577
874
  if (method === "map" && (fnRet.kind === "void" || fnRet.kind === "func")) {
578
875
  // The result array U[] is unrepresentable here (no void elements;
@@ -595,10 +892,98 @@ function lowerArrayHofCall(lowerer, call, access, method, elem) {
595
892
  }
596
893
  lowerer.badType(argNode, lowerer.typeOf(argNode));
597
894
  }
598
- const helper = arrayHofHelper(lowerer, method, elem, fnRet, arity, loc);
599
- const resultType = method === "map" ? arrayOf(fnRet) : method === "filter" ? arrayOf(elem) : VOID;
895
+ const outElem = method === "filter"
896
+ ? filterResultElem(lowerer, call, argNode, elem, valueT, booleanFilter)
897
+ : callbackArrayElem(lowerer, call, fnRet);
898
+ const helper = arrayHofHelper(lowerer, method, elem, fnRet, arity, loc, outElem);
899
+ const resultType = method === "map" || method === "filter" ? arrayOf(outElem) : VOID;
600
900
  return { kind: "call", callee: helper, args: [receiver, fnArg], type: resultType, loc };
601
901
  }
902
+ /** Preserve the checker-selected payload ABI when a callback's runtime
903
+ * return gained only undefined. The array state byte carries that value. */
904
+ function callbackArrayElem(lowerer, call, fnRet) {
905
+ const result = lowerer.mapTypeOf(lowerer.typeOf(call));
906
+ return result?.kind === "array" && lowerer.runtimeOptionalWidening(fnRet, result.elem)
907
+ ? result.elem : fnRet;
908
+ }
909
+ /** The checker-selected element of a filter result, with the same trust
910
+ * boundary as the historical narrowing filter: an inferred inline type
911
+ * predicate may prove one source arm; a written predicate annotation may
912
+ * not. Boolean is safe when every discarded arm is a nullish unit. */
913
+ function filterResultElem(lowerer, call, argNode, elem, valueT, booleanFilter) {
914
+ const callT = lowerer.typeOf(call);
915
+ const result = lowerer.mapTypeOf(callT);
916
+ let outElem = result?.kind === "array" ? result.elem : elem;
917
+ const access = ts.isPropertyAccessExpression(call.expression) ? call.expression : null;
918
+ const checkerReceiver = access
919
+ ? lowerer.mapTypeOf(lowerer.typeOf(access.expression))
920
+ : null;
921
+ // Evolving-any arrays of island handles retain their jsval element ABI,
922
+ // while the checker later describes both the receiver and unchanged
923
+ // filter result by the evolved callable type. Equal checker elements mean
924
+ // no predicate narrowing occurred; preserve the runtime element home.
925
+ if (elem.kind === "jsval" && valueT.kind === "jsval" &&
926
+ result?.kind === "array" && checkerReceiver?.kind === "array" &&
927
+ typeEquals(result.elem, checkerReceiver.elem)) {
928
+ return elem;
929
+ }
930
+ if (!typeEquals(outElem, elem)) {
931
+ const contextual = lowerer.mapTypeOf(lowerer.checker.getContextualType(call) ?? callT);
932
+ if (contextual?.kind === "array" && typeEquals(contextual.elem, elem))
933
+ outElem = elem;
934
+ }
935
+ if (typeEquals(outElem, elem))
936
+ return elem;
937
+ const annotateEscape = "keep the receiver's element type instead — annotate the callback's return ': boolean' " +
938
+ "(the checker then skips the predicate) or annotate the result with the receiver's own " +
939
+ "element type — and narrow the elements after";
940
+ if (!booleanFilter) {
941
+ if (!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) {
942
+ lowerer.unsupported("SC1090", argNode, `narrowing '.filter' through a callback VALUE (only an inline callback whose predicate ` +
943
+ `the checker inferred can re-tag — ${annotateEscape})`);
944
+ }
945
+ if (argNode.type) {
946
+ lowerer.unsupported("SC1090", argNode, `narrowing '.filter' with a hand-written type predicate (a written 'x is T' is an ` +
947
+ `unchecked assertion nothing validates at runtime — ${annotateEscape})`);
948
+ }
949
+ }
950
+ if (valueT.kind !== "union" || lowerer.armTag(valueT.unionId, outElem) < 0) {
951
+ const multiArm = outElem.kind === "union";
952
+ lowerer.unsupported("SC1090", call, `'.filter' narrowing '${lowerer.fmt(elem)}' elements to ` +
953
+ `${multiArm ? `the multi-arm '${lowerer.fmt(outElem)}'` : `'${lowerer.fmt(outElem)}'`} ` +
954
+ `(${multiArm ? "only a SINGLE arm re-tags" : "only a single runtime arm re-tags"} — ${annotateEscape})`);
955
+ }
956
+ if (booleanFilter) {
957
+ const arms = lowerer.unions.get(valueT.unionId)?.arms ?? [];
958
+ if (arms.some((arm) => !typeEquals(arm, outElem) && !isUnitType(arm))) {
959
+ lowerer.unsupported("SC1090", call, `'.filter(Boolean)' narrowing '${lowerer.fmt(elem)}' to '${lowerer.fmt(outElem)}' ` +
960
+ "when another truthy arm could survive");
961
+ }
962
+ }
963
+ return outElem;
964
+ }
965
+ /** A native closure for the ambient Boolean callback. Reifying the
966
+ * JavaScript constructor itself is unnecessary; Array#filter only observes
967
+ * its one-argument ToBoolean result. */
968
+ function truthyFilterCallback(lowerer, valueT, loc) {
969
+ const fnT = { kind: "func", params: [valueT], ret: BOOL };
970
+ const key = `filterBoolean:${typeKey(valueT)}`;
971
+ let name = lowerer.arrHofHelpers.get(key);
972
+ if (!name) {
973
+ name = `%arr.filterBoolean.${lowerer.arrHofHelpers.size}`;
974
+ lowerer.arrHofHelpers.set(key, name);
975
+ const value = varRef("v.0", valueT, loc);
976
+ lowerer.liftedFns.push({
977
+ name,
978
+ params: [{ localId: "v.0", name: "v", type: valueT }],
979
+ returnType: BOOL,
980
+ locals: [{ id: "v.0", name: "v", type: valueT, mutable: false }],
981
+ body: [{ kind: "return", value: { kind: "toBool", operand: value, type: BOOL, loc }, loc }],
982
+ loc,
983
+ });
984
+ }
985
+ return { kind: "closure", fnName: name, captures: [], type: fnT, loc };
986
+ }
602
987
  /** The predicate result kinds `.filter()` accepts. JS applies ToBoolean to
603
988
  * whatever the callback answers, so a bool is not required: the scalars and
604
989
  * the reference kinds have constant or by-value answers, and a union is fine
@@ -644,14 +1029,14 @@ function filterCond(call, fnRet, loc) {
644
1029
  * combo. Named `%arr.<method>.<n>` ('%' keeps it out of the user
645
1030
  * namespace); rides `liftedFns` into the module like a lifted lambda (it
646
1031
  * is a plain function — no captures). */
647
- function arrayHofHelper(lowerer, method, elem, fnRet, arity, loc) {
648
- const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}`;
1032
+ function arrayHofHelper(lowerer, method, elem, fnRet, arity, loc, outElem = fnRet) {
1033
+ const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}:${typeKey(outElem)}`;
649
1034
  const existing = lowerer.arrHofHelpers.get(key);
650
1035
  if (existing)
651
1036
  return existing;
652
1037
  const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
653
1038
  lowerer.arrHofHelpers.set(key, name);
654
- lowerer.liftedFns.push(buildArrayHofFn(lowerer, name, method, elem, fnRet, arity, loc));
1039
+ lowerer.liftedFns.push(buildArrayHofFn(lowerer, name, method, elem, fnRet, arity, loc, outElem));
655
1040
  return name;
656
1041
  }
657
1042
  /** READ-ONLY array methods on TUPLE receivers — `t.slice(...)` and
@@ -690,7 +1075,7 @@ export function lowerTupleReadMethodCall(lowerer, call, access) {
690
1075
  receiver ??= lowerer.lowerExpr(access.expression);
691
1076
  if (receiver.type.kind !== "record")
692
1077
  return null;
693
- if (!pureReemittable(receiver)) {
1078
+ if (!isSafeToRepeat(receiver)) {
694
1079
  lowerer.noLowering(`${lowerer.checker.typeToString(lowerer.typeOf(access.expression))}.${name}`, call, "tuple receivers snapshot per position, so only effect-free receivers lower — bind the tuple to a const first");
695
1080
  }
696
1081
  const loc = locOf(call);
@@ -743,13 +1128,14 @@ export function lowerTupleReadMethodCall(lowerer, call, access) {
743
1128
  lowerer.noLowering(`.map with ${call.arguments.length} arguments`, call, "the thisArg parameter has no lowering — use an arrow function");
744
1129
  }
745
1130
  const argNode = call.arguments[0];
746
- const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [snapshotElem], arrayOf(snapshotElem));
1131
+ const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [arrayValueType(lowerer, snapshotElem)], arrayOf(snapshotElem));
747
1132
  const fnRet = fnArg.type.ret;
748
1133
  if (fnRet.kind === "void" || fnRet.kind === "func")
749
1134
  lowerer.badType(call, lowerer.typeOf(call));
750
1135
  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 };
1136
+ const outElem = callbackArrayElem(lowerer, call, fnRet);
1137
+ const helper = arrayHofHelper(lowerer, "map", snapshotElem, fnRet, arity, loc, outElem);
1138
+ return { kind: "call", callee: helper, args: [snapshot, fnArg], type: arrayOf(outElem), loc };
753
1139
  }
754
1140
  /** An OOB-SAFE indexed read: `xs[i]`
755
1141
  * answers the interned `elem | undefined` union — the element when `i`
@@ -764,54 +1150,13 @@ export function lowerSafeIndexRead(lowerer, arr, index, loc) {
764
1150
  const elem = arr.type.elem;
765
1151
  if (elem.kind === "void" || elem.kind === "dyn")
766
1152
  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;
1153
+ const resultT = arrayValueType(lowerer, elem);
775
1154
  const key = `idxOr:${typeKey(elem)}`;
776
1155
  let name = lowerer.arrHofHelpers.get(key);
777
1156
  if (!name) {
778
1157
  name = `%arr.idxOr.${lowerer.arrHofHelpers.size}`;
779
1158
  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
- ];
1159
+ const arrT = arr.type;
815
1160
  lowerer.liftedFns.push({
816
1161
  name,
817
1162
  params: [
@@ -820,12 +1165,10 @@ export function lowerSafeIndexRead(lowerer, arr, index, loc) {
820
1165
  ],
821
1166
  returnType: resultT,
822
1167
  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 },
1168
+ { id: "a.0", name: "a", type: arrT, mutable: false },
1169
+ { id: "i.0", name: "i", type: F64, mutable: false },
827
1170
  ],
828
- body,
1171
+ body: [{ kind: "return", value: arrayValueRead(lowerer, varRef("a.0", arrT, loc), varRef("i.0", F64, loc), elem, loc), loc }],
829
1172
  loc,
830
1173
  });
831
1174
  }
@@ -859,7 +1202,7 @@ function arrayConcatHelper(lowerer, elem, shape, loc) {
859
1202
  kind: "exprStmt",
860
1203
  expr: {
861
1204
  kind: "arrIntrinsic",
862
- method: s === "a" ? "pushSpread" : "push",
1205
+ method: s === "a" ? "concatSpread" : "push",
863
1206
  receiver: varRef("out.0", arrT, loc),
864
1207
  args: [src],
865
1208
  type: F64,
@@ -887,9 +1230,10 @@ function arrayConcatHelper(lowerer, elem, shape, loc) {
887
1230
  * `arity` is the callback's declared parameter count (1–3): the loop
888
1231
  * passes the index and the receiver itself after the element when the
889
1232
  * callback names them, exactly the arguments JS supplies. */
890
- function buildArrayHofFn(lowerer, name, method, elem, fnRet, arity, loc) {
1233
+ function buildArrayHofFn(lowerer, name, method, elem, fnRet, arity, loc, outElem) {
891
1234
  const arrT = arrayOf(elem);
892
- const fnT = funcOf([elem, F64, arrT].slice(0, arity), fnRet);
1235
+ const valueT = arrayValueType(lowerer, elem);
1236
+ const fnT = funcOf([arrayValueType(lowerer, elem), F64, arrT].slice(0, arity), fnRet);
893
1237
  const locals = [
894
1238
  { id: "a.0", name: "a", type: arrT, mutable: true },
895
1239
  { id: "f.0", name: "f", type: fnT, mutable: true },
@@ -907,25 +1251,14 @@ function buildArrayHofFn(lowerer, name, method, elem, fnRet, arity, loc) {
907
1251
  type: fnRet,
908
1252
  loc,
909
1253
  });
910
- const getElem = {
911
- kind: "arrayGet",
1254
+ const getElem = arrayValueRead(lowerer, varRef("a.0", arrT, loc), varRef("i.0", F64, loc), elem, loc);
1255
+ const hasElem = {
1256
+ kind: "arrayHas",
912
1257
  arr: varRef("a.0", arrT, loc),
913
1258
  index: varRef("i.0", F64, loc),
914
- type: elem,
1259
+ type: BOOL,
915
1260
  loc,
916
1261
  };
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
1262
  const readLen = {
930
1263
  kind: "varDecl",
931
1264
  localId: "n.0",
@@ -935,41 +1268,85 @@ function buildArrayHofFn(lowerer, name, method, elem, fnRet, arity, loc) {
935
1268
  const forLoop = (body) => countedFor(loc, varRef("n.0", F64, loc), () => body);
936
1269
  let returnType;
937
1270
  let body;
938
- if (method === "map") {
939
- const outT = arrayOf(fnRet);
940
- locals.push({ id: "out.0", name: "out", type: outT, mutable: false });
1271
+ if (method === "map" || method === "flatMap") {
1272
+ const outT = arrayOf(outElem);
1273
+ locals.push({ id: "out.0", name: "out", type: outT, mutable: false }, { id: "mapped.0", name: "mapped", type: fnRet, mutable: false });
941
1274
  returnType = outT;
942
1275
  body = [
943
1276
  { kind: "varDecl", localId: "out.0", init: { kind: "arrayLit", elems: [], type: outT, loc }, loc },
944
1277
  readLen,
945
- forLoop([push(outT, callF(getElem))]),
1278
+ ...(method === "map" ? [{ kind: "arraySetLength", arr: varRef("out.0", outT, loc), length: varRef("n.0", F64, loc), loc }] : []),
1279
+ forLoop([
1280
+ {
1281
+ kind: "if",
1282
+ cond: hasElem,
1283
+ then: [
1284
+ { kind: "varDecl", localId: "mapped.0", init: callF(getElem), loc },
1285
+ 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),
1286
+ ],
1287
+ else_: null,
1288
+ loc,
1289
+ },
1290
+ ]),
946
1291
  { kind: "return", value: varRef("out.0", outT, loc), loc },
947
1292
  ];
948
1293
  }
949
1294
  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;
1295
+ const outT = arrayOf(outElem);
1296
+ locals.push({ id: "out.0", name: "out", type: outT, mutable: false }, { id: "v.0", name: "v", type: valueT, mutable: false });
1297
+ returnType = outT;
1298
+ const retained = typeEquals(outElem, elem)
1299
+ ? varRef("v.0", valueT, loc)
1300
+ : valueT.kind === "union"
1301
+ ? {
1302
+ kind: "unionNarrow",
1303
+ unionId: valueT.unionId,
1304
+ tag: lowerer.armTag(valueT.unionId, outElem),
1305
+ value: varRef("v.0", valueT, loc),
1306
+ type: outElem,
1307
+ loc,
1308
+ }
1309
+ : varRef("v.0", valueT, loc);
952
1310
  body = [
953
- { kind: "varDecl", localId: "out.0", init: { kind: "arrayLit", elems: [], type: arrT, loc }, loc },
1311
+ { kind: "varDecl", localId: "out.0", init: { kind: "arrayLit", elems: [], type: outT, loc }, loc },
954
1312
  readLen,
955
1313
  forLoop([
956
- { kind: "varDecl", localId: "v.0", init: getElem, loc },
957
1314
  {
958
1315
  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))],
1316
+ cond: hasElem,
1317
+ then: [
1318
+ { kind: "varDecl", localId: "v.0", init: getElem, loc },
1319
+ {
1320
+ kind: "if",
1321
+ // ToBoolean over the predicate's answer — inert when it already
1322
+ // returned bool, the per-union helper when it returned a union.
1323
+ cond: filterCond(callF(varRef("v.0", valueT, loc)), fnRet, loc),
1324
+ 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)],
1325
+ else_: null,
1326
+ loc,
1327
+ },
1328
+ ],
963
1329
  else_: null,
964
1330
  loc,
965
1331
  },
966
1332
  ]),
967
- { kind: "return", value: varRef("out.0", arrT, loc), loc },
1333
+ { kind: "return", value: varRef("out.0", outT, loc), loc },
968
1334
  ];
969
1335
  }
970
1336
  else {
971
1337
  returnType = VOID;
972
- body = [readLen, forLoop([{ kind: "exprStmt", expr: callF(getElem), loc }])];
1338
+ body = [
1339
+ readLen,
1340
+ forLoop([
1341
+ {
1342
+ kind: "if",
1343
+ cond: hasElem,
1344
+ then: [{ kind: "exprStmt", expr: callF(getElem), loc }],
1345
+ else_: null,
1346
+ loc,
1347
+ },
1348
+ ]),
1349
+ ];
973
1350
  }
974
1351
  return { name, params, returnType, locals, body, loc };
975
1352
  }
@@ -985,14 +1362,14 @@ function buildArrayHofFn(lowerer, name, method, elem, fnRet, arity, loc) {
985
1362
  * backwards (`i = n - 1; i >= 0; i--`), exactly the es2023 spec's
986
1363
  * descending index walk. All require a bool-returning callback
987
1364
  * (JS's ToBoolean of arbitrary predicate results has no lowering). */
988
- function lowerArrayFindLikeCall(lowerer, call, access, method, elem) {
1365
+ function lowerArrayFindLikeCall(lowerer, call, access, method, elem, bindUntyped = false) {
989
1366
  const loc = locOf(call);
990
1367
  const receiver = lowerer.lowerExpr(access.expression);
991
1368
  const arrT = arrayOf(elem);
992
1369
  const argNode = call.arguments[0];
993
1370
  if (!argNode)
994
1371
  lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded
995
- const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [elem], arrT);
1372
+ const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [arrayValueType(lowerer, elem)], arrT, bindUntyped);
996
1373
  // JS takes the ToBoolean of the predicate's result — allowed wherever
997
1374
  // that ToBoolean has a static answer (bool passes through; f64/string
998
1375
  // by value; a truthy-answerable union by its arm — the
@@ -1022,21 +1399,13 @@ function lowerArrayFindLikeCall(lowerer, call, access, method, elem) {
1022
1399
  // found element passes through untouched; otherwise it wraps into its
1023
1400
  // arm. A union element whose result union differs would need the
1024
1401
  // union-into-union re-tag that doesn't exist — fenced.
1025
- const resultT = lowerer.irTypeOf(call);
1402
+ const resultT = bindUntyped ? arrayValueType(lowerer, elem) : lowerer.irTypeOf(call);
1026
1403
  if (resultT.kind !== "union")
1027
1404
  lowerer.badType(call, lowerer.typeOf(call)); // defensive: T | undefined always maps to a union
1028
1405
  const undefTag = lowerer.armTag(resultT.unionId, UNDEFINED_T);
1029
1406
  if (undefTag < 0)
1030
1407
  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);
1408
+ const helper = findHelper(lowerer, elem, resultT, undefTag, fnRet, arity, last, loc);
1040
1409
  return { kind: "call", callee: helper, args: [receiver, fnArg], type: resultT, loc };
1041
1410
  }
1042
1411
  /** ToBoolean of a predicate result inside a synthesized HOF helper — the
@@ -1053,7 +1422,7 @@ function predToBool(e) {
1053
1422
  * for (i = 0; i < n; i++) { v = a[i]; if (f(v)) return <v as result arm>; }
1054
1423
  * return <undefined arm>;
1055
1424
  */
1056
- function findHelper(lowerer, elem, resultT, foundTag, undefTag, fnRet, arity, last, loc) {
1425
+ function findHelper(lowerer, elem, resultT, undefTag, fnRet, arity, last, loc) {
1057
1426
  const method = last ? "findLast" : "find";
1058
1427
  const key = `${method}:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}`;
1059
1428
  const existing = lowerer.arrHofHelpers.get(key);
@@ -1062,11 +1431,10 @@ function findHelper(lowerer, elem, resultT, foundTag, undefTag, fnRet, arity, la
1062
1431
  const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
1063
1432
  lowerer.arrHofHelpers.set(key, name);
1064
1433
  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 };
1434
+ const fnT = funcOf([arrayValueType(lowerer, elem), F64, arrT].slice(0, arity), fnRet);
1435
+ const valueT = arrayValueType(lowerer, elem);
1436
+ const v = varRef("v.0", valueT, loc);
1437
+ const found = lowerer.coerceToExpected(v, resultT);
1070
1438
  const miss = {
1071
1439
  kind: "unionWrap",
1072
1440
  unionId: resultT.unionId,
@@ -1075,8 +1443,8 @@ function findHelper(lowerer, elem, resultT, foundTag, undefTag, fnRet, arity, la
1075
1443
  type: resultT,
1076
1444
  loc,
1077
1445
  };
1078
- const visit = [
1079
- { kind: "varDecl", localId: "v.0", init: getElemExpr(arrT, elem, loc), loc },
1446
+ const visitBody = [
1447
+ { kind: "varDecl", localId: "v.0", init: arrayValueRead(lowerer, varRef("a.0", arrT, loc), varRef("i.0", F64, loc), elem, loc), loc },
1080
1448
  {
1081
1449
  kind: "if",
1082
1450
  cond: predToBool({
@@ -1091,6 +1459,7 @@ function findHelper(lowerer, elem, resultT, foundTag, undefTag, fnRet, arity, la
1091
1459
  loc,
1092
1460
  },
1093
1461
  ];
1462
+ const visit = visitBody;
1094
1463
  const loop = last
1095
1464
  ? reverseCountedForLoop(loc, visit)
1096
1465
  : countedFor(loc, varRef("n.0", F64, loc), () => visit);
@@ -1111,7 +1480,7 @@ function findHelper(lowerer, elem, resultT, foundTag, undefTag, fnRet, arity, la
1111
1480
  { id: "f.0", name: "f", type: fnT, mutable: true },
1112
1481
  { id: "n.0", name: "n", type: F64, mutable: false },
1113
1482
  { id: "i.0", name: "i", type: F64, mutable: true },
1114
- { id: "v.0", name: "v", type: elem, mutable: false },
1483
+ { id: "v.0", name: "v", type: valueT, mutable: false },
1115
1484
  ],
1116
1485
  body,
1117
1486
  loc,
@@ -1132,13 +1501,13 @@ function findIndexHelper(lowerer, elem, fnRet, arity, last, loc) {
1132
1501
  const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
1133
1502
  lowerer.arrHofHelpers.set(key, name);
1134
1503
  const arrT = arrayOf(elem);
1135
- const fnT = funcOf([elem, F64, arrT].slice(0, arity), fnRet);
1504
+ const fnT = funcOf([arrayValueType(lowerer, elem), F64, arrT].slice(0, arity), fnRet);
1136
1505
  const visit = [{
1137
1506
  kind: "if",
1138
1507
  cond: predToBool({
1139
1508
  kind: "callValue",
1140
1509
  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),
1510
+ 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
1511
  type: fnRet,
1143
1512
  loc,
1144
1513
  }),
@@ -1185,11 +1554,11 @@ function someEveryHelper(lowerer, method, elem, fnRet, arity, loc) {
1185
1554
  const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
1186
1555
  lowerer.arrHofHelpers.set(key, name);
1187
1556
  const arrT = arrayOf(elem);
1188
- const fnT = funcOf([elem, F64, arrT].slice(0, arity), fnRet);
1557
+ const fnT = funcOf([arrayValueType(lowerer, elem), F64, arrT].slice(0, arity), fnRet);
1189
1558
  const callF = predToBool({
1190
1559
  kind: "callValue",
1191
1560
  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),
1561
+ 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
1562
  type: fnRet,
1194
1563
  loc,
1195
1564
  });
@@ -1198,8 +1567,14 @@ function someEveryHelper(lowerer, method, elem, fnRet, arity, loc) {
1198
1567
  countedFor(loc, varRef("n.0", F64, loc), () => [
1199
1568
  {
1200
1569
  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 }],
1570
+ cond: currentArrayIndexPresent(arrT, loc),
1571
+ then: [{
1572
+ kind: "if",
1573
+ cond: method === "some" ? callF : { kind: "unary", op: "!", operand: callF, type: BOOL, loc },
1574
+ then: [{ kind: "return", value: boolLit(method === "some", loc), loc }],
1575
+ else_: null,
1576
+ loc,
1577
+ }],
1203
1578
  else_: null,
1204
1579
  loc,
1205
1580
  },
@@ -1237,7 +1612,7 @@ function lowerArrayFlatMapCall(lowerer, call, access, elem) {
1237
1612
  const argNode = call.arguments[0];
1238
1613
  if (!argNode)
1239
1614
  lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded
1240
- const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [elem], arrT);
1615
+ const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [arrayValueType(lowerer, elem)], arrT);
1241
1616
  const fnRet = fnArg.type.ret;
1242
1617
  if (fnRet.kind === "union") {
1243
1618
  const def = lowerer.unions.get(fnRet.unionId);
@@ -1247,12 +1622,13 @@ function lowerArrayFlatMapCall(lowerer, call, access, elem) {
1247
1622
  }
1248
1623
  }
1249
1624
  if (fnRet.kind !== "array") {
1250
- // No flatten happens: exactly map's loop, map's helper.
1625
+ // Scalar results append densely; source holes do not produce output slots.
1251
1626
  if (fnRet.kind === "void" || fnRet.kind === "func")
1252
1627
  lowerer.badType(call, lowerer.typeOf(call));
1253
1628
  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 };
1629
+ const outElem = callbackArrayElem(lowerer, call, fnRet);
1630
+ const helper = arrayHofHelper(lowerer, "flatMap", elem, fnRet, arity, loc, outElem);
1631
+ return { kind: "call", callee: helper, args: [receiver, fnArg], type: arrayOf(outElem), loc };
1256
1632
  }
1257
1633
  const helper = flatMapHelper(lowerer, elem, fnRet, arity, loc);
1258
1634
  return { kind: "call", callee: helper, args: [receiver, fnArg], type: fnRet, loc };
@@ -1275,7 +1651,7 @@ function flatMapHelper(lowerer, elem, fnRet, arity, loc) {
1275
1651
  lowerer.arrHofHelpers.set(key, name);
1276
1652
  const arrT = arrayOf(elem);
1277
1653
  const inner = fnRet.elem;
1278
- const fnT = funcOf([elem, F64, arrT].slice(0, arity), fnRet);
1654
+ const fnT = funcOf([arrayValueType(lowerer, elem), F64, arrT].slice(0, arity), fnRet);
1279
1655
  const innerLoop = {
1280
1656
  kind: "for",
1281
1657
  init: { kind: "varDecl", localId: "j.0", init: numLit(0, loc), loc },
@@ -1288,15 +1664,13 @@ function flatMapHelper(lowerer, elem, fnRet, arity, loc) {
1288
1664
  },
1289
1665
  body: [
1290
1666
  {
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
- },
1667
+ kind: "if",
1668
+ cond: arrayIndexPresent(varRef("r.0", fnRet, loc), varRef("j.0", F64, loc), loc),
1669
+ then: [
1670
+ { kind: "varDecl", localId: "inner.0", init: arrayValueRead(lowerer, varRef("r.0", fnRet, loc), varRef("j.0", F64, loc), inner, loc), loc },
1671
+ 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),
1672
+ ],
1673
+ else_: null,
1300
1674
  loc,
1301
1675
  },
1302
1676
  ],
@@ -1307,24 +1681,32 @@ function flatMapHelper(lowerer, elem, fnRet, arity, loc) {
1307
1681
  readLenStmt(arrT, loc),
1308
1682
  countedFor(loc, varRef("n.0", F64, loc), () => [
1309
1683
  {
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 },
1684
+ kind: "if",
1685
+ cond: currentArrayIndexPresent(arrT, loc),
1686
+ then: [
1687
+ {
1688
+ kind: "varDecl",
1689
+ localId: "r.0",
1690
+ init: {
1691
+ kind: "callValue",
1692
+ callee: varRef("f.0", fnT, loc),
1693
+ 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),
1694
+ type: fnRet,
1695
+ loc,
1696
+ },
1697
+ loc,
1698
+ },
1699
+ {
1700
+ kind: "varDecl",
1701
+ localId: "m.0",
1702
+ init: { kind: "arrIntrinsic", method: "length", receiver: varRef("r.0", fnRet, loc), args: [], type: F64, loc },
1703
+ loc,
1704
+ },
1705
+ innerLoop,
1706
+ ],
1707
+ else_: null,
1325
1708
  loc,
1326
1709
  },
1327
- innerLoop,
1328
1710
  ]),
1329
1711
  { kind: "return", value: varRef("out.0", fnRet, loc), loc },
1330
1712
  ];
@@ -1344,6 +1726,7 @@ function flatMapHelper(lowerer, elem, fnRet, arity, loc) {
1344
1726
  { id: "r.0", name: "r", type: fnRet, mutable: false },
1345
1727
  { id: "m.0", name: "m", type: F64, mutable: false },
1346
1728
  { id: "j.0", name: "j", type: F64, mutable: true },
1729
+ { id: "inner.0", name: "inner", type: arrayValueType(lowerer, inner), mutable: false },
1347
1730
  ],
1348
1731
  body,
1349
1732
  loc,
@@ -1363,13 +1746,12 @@ function lowerArrayReduceCall(lowerer, call, access, method, elem) {
1363
1746
  if (!argNode)
1364
1747
  lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded
1365
1748
  const hasInit = call.arguments.length === 2;
1366
- const accT = hasInit ? lowerer.irTypeOf(call) : elem;
1749
+ const accT = lowerer.runtimeOptionalReduceTypes.get(call) ??
1750
+ (hasInit ? lowerer.irTypeOf(call) : arrayValueType(lowerer, elem));
1367
1751
  if (accT.kind === "void" || accT.kind === "func")
1368
1752
  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);
1753
+ const { fnArg, arity } = hofCallbackArg(lowerer, argNode, [accT, arrayValueType(lowerer, elem)], arrT);
1754
+ const helper = reduceHelper(lowerer, method, elem, accT, fnArg.type.ret, arity, hasInit, loc);
1373
1755
  const args = [receiver, fnArg];
1374
1756
  if (hasInit)
1375
1757
  args.push(lowerer.lowerExprExpecting(call.arguments[1], accT));
@@ -1385,19 +1767,19 @@ function lowerArrayReduceCall(lowerer, call, access, method, elem) {
1385
1767
  * reduce walks 0→n-1, reduceRight n-1→0; the seed without an initial
1386
1768
  * value is a[0] / a[n-1] and the loop starts one past it. The empty-array
1387
1769
  * 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"}`;
1770
+ function reduceHelper(lowerer, method, elem, accT, fnRet, arity, hasInit, loc) {
1771
+ const key = `${method}:${typeKey(elem)}:${typeKey(accT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : "seed"}`;
1390
1772
  const existing = lowerer.arrHofHelpers.get(key);
1391
1773
  if (existing)
1392
1774
  return existing;
1393
1775
  const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
1394
1776
  lowerer.arrHofHelpers.set(key, name);
1395
1777
  const arrT = arrayOf(elem);
1396
- const fnT = funcOf([accT, elem, F64, arrT].slice(0, arity), accT);
1778
+ const fnT = funcOf([accT, arrayValueType(lowerer, elem), F64, arrT].slice(0, arity), fnRet);
1397
1779
  const n = varRef("n.0", F64, loc);
1398
1780
  const i = varRef("i.0", F64, loc);
1399
1781
  const right = method === "reduceRight";
1400
- const at = (index) => ({ kind: "arrayGet", arr: varRef("a.0", arrT, loc), index, type: elem, loc });
1782
+ const at = (index) => arrayValueRead(lowerer, varRef("a.0", arrT, loc), index, elem, loc);
1401
1783
  const locals = [
1402
1784
  { id: "a.0", name: "a", type: arrT, mutable: true },
1403
1785
  { id: "f.0", name: "f", type: fnT, mutable: true },
@@ -1411,50 +1793,27 @@ function reduceHelper(lowerer, method, elem, accT, arity, hasInit, loc) {
1411
1793
  { localId: "f.0", name: "f", type: fnT },
1412
1794
  ...(hasInit ? [{ localId: "z.0", name: "z", type: accT }] : []),
1413
1795
  ];
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.
1452
1796
  const start = right
1453
- ? { kind: "bin", op: "-", left: n, right: numLit(hasInit ? 1 : 2, loc), type: F64, loc }
1454
- : numLit(hasInit ? 0 : 1, loc);
1797
+ ? { kind: "bin", op: "-", left: n, right: numLit(1, loc), type: F64, loc }
1798
+ : numLit(0, loc);
1799
+ const inRange = right
1800
+ ? { kind: "bin", op: ">=", left: i, right: numLit(0, loc), type: BOOL, loc }
1801
+ : { kind: "bin", op: "<", left: i, right: n, type: BOOL, loc };
1802
+ const advance = { kind: "assign", localId: "i.0", value: { kind: "bin", op: right ? "-" : "+", left: i, right: numLit(1, loc), type: F64, loc }, loc };
1803
+ const seedStmts = [
1804
+ { kind: "varDecl", localId: "i.0", init: start, loc },
1805
+ ...(hasInit
1806
+ ? [{ kind: "varDecl", localId: "acc.0", init: varRef("z.0", accT, loc), loc }]
1807
+ : [
1808
+ { kind: "while", cond: { kind: "ternary", cond: inRange, then: { kind: "unary", op: "!", operand: currentArrayIndexPresent(arrT, loc), type: BOOL, loc }, else_: boolLit(false, loc), type: BOOL, loc }, body: [advance], loc },
1809
+ { 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 },
1810
+ { kind: "varDecl", localId: "acc.0", init: lowerer.coerceToExpected(at(i), accT), loc },
1811
+ advance,
1812
+ ]),
1813
+ ];
1455
1814
  const loop = {
1456
1815
  kind: "for",
1457
- init: { kind: "varDecl", localId: "i.0", init: start, loc },
1816
+ init: null,
1458
1817
  cond: right
1459
1818
  ? { kind: "bin", op: ">=", left: i, right: numLit(0, loc), type: BOOL, loc }
1460
1819
  : { kind: "bin", op: "<", left: i, right: n, type: BOOL, loc },
@@ -1464,20 +1823,20 @@ function reduceHelper(lowerer, method, elem, accT, arity, hasInit, loc) {
1464
1823
  value: { kind: "bin", op: right ? "-" : "+", left: i, right: numLit(1, loc), type: F64, loc },
1465
1824
  loc,
1466
1825
  },
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
- ],
1826
+ body: [{ kind: "if", cond: currentArrayIndexPresent(arrT, loc), then: [
1827
+ {
1828
+ kind: "assign",
1829
+ localId: "acc.0",
1830
+ value: lowerer.coerceToExpected({
1831
+ kind: "callValue",
1832
+ callee: varRef("f.0", fnT, loc),
1833
+ args: [varRef("acc.0", accT, loc), at(i), i, varRef("a.0", arrT, loc)].slice(0, arity),
1834
+ type: fnRet,
1835
+ loc,
1836
+ }, accT),
1837
+ loc,
1838
+ },
1839
+ ], else_: null, loc }],
1481
1840
  loc,
1482
1841
  };
1483
1842
  const body = [
@@ -1493,19 +1852,20 @@ function reduceHelper(lowerer, method, elem, accT, arity, hasInit, loc) {
1493
1852
  * function like the other array HOFs. sort mutates and returns the receiver;
1494
1853
  * toSorted takes its shallow snapshot INSIDE the helper, after the receiver
1495
1854
  * 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
1855
+ * returns that copy without touching the receiver. The helper uses a stable
1856
+ * bottom-up merge sort with an ordered-boundary check: ordered inputs use a
1857
+ * linear number of comparator calls, but buffer movement remains O(n log n)
1858
+ * even when every boundary is already ordered. During a merge, an element
1859
+ * moves right only while cmp(left, right) > 0, so a NaN or 0 result keeps
1860
+ * the left element first. Undefined values sink without reaching the user
1861
+ * comparator. The callback sequence differs from V8's TimSort, so exact
1862
+ * order and count parity are not claimed; results agree for consistent
1863
+ * comparators. The comparator-less form lowers for STRING elements only —
1864
+ * JS's default converts every element to string and compares UTF-16 units,
1865
+ * so the interned synthesized comparator selects the runtime's code-unit
1866
+ * ordering rather than scriptc's documented code-point relational
1867
+ * operators. For numbers that default is the notorious string sort
1868
+ * ([10, 9, 1] → [1, 10, 9]), deliberately fenced toward an explicit
1509
1869
  * comparator. */
1510
1870
  function lowerArraySortCall(lowerer, call, access, elem, arrT) {
1511
1871
  const loc = locOf(call);
@@ -1599,441 +1959,6 @@ function defaultStringCmpHelper(lowerer, loc) {
1599
1959
  });
1600
1960
  return name;
1601
1961
  }
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
- /** Uint8Array.prototype.toSorted. The receiver/comparator expressions are
1762
- * evaluated before entering the helper; the helper snapshots with
1763
- * TypedArray.prototype.slice before its first comparison, then performs
1764
- * the same stable insertion walk as Array.toSorted. Uint8Array's default
1765
- * comparator is numeric ascending, so its comparator-less form needs no
1766
- * string-conversion machinery. */
1767
- function lowerBytesToSortedCall(lowerer, call, access, bytesT) {
1768
- const loc = locOf(call);
1769
- if (call.arguments.length > 1 || call.arguments.some(ts.isSpreadElement)) {
1770
- lowerer.noLowering(`.toSorted with ${call.arguments.length} arguments on Uint8Array`, call);
1771
- }
1772
- const receiver = lowerer.lowerExpr(access.expression);
1773
- const undefinedArg = call.arguments[0]
1774
- ? lowerStaticallyUndefinedArg(lowerer, call.arguments[0])
1775
- : null;
1776
- if (call.arguments.length === 0 || undefinedArg) {
1777
- const key = "bytes.toSorted:u8:default";
1778
- let helper = lowerer.arrHofHelpers.get(key);
1779
- if (!helper) {
1780
- helper = `%bytes.toSorted.${lowerer.arrHofHelpers.size}`;
1781
- lowerer.arrHofHelpers.set(key, helper);
1782
- lowerer.liftedFns.push(buildBytesSortFn(helper, 0, false, loc));
1783
- }
1784
- if (!undefinedArg || droppableStatic(undefinedArg)) {
1785
- return { kind: "call", callee: helper, args: [receiver], type: bytesT, loc };
1786
- }
1787
- // The default helper takes no comparator argument. Snapshot the
1788
- // receiver into a hidden local so the discarded undefined argument
1789
- // still evaluates after the receiver and before the helper call.
1790
- const saved = lowerer.declareHiddenLocal("%bytesSortRecv", bytesT);
1791
- const savedRef = {
1792
- kind: "varRef",
1793
- localId: saved.id,
1794
- type: bytesT,
1795
- loc,
1796
- };
1797
- return {
1798
- kind: "seqExpr",
1799
- stmts: [
1800
- { kind: "varDecl", localId: saved.id, init: receiver, loc },
1801
- { kind: "exprStmt", expr: undefinedArg, loc: undefinedArg.loc },
1802
- ],
1803
- result: {
1804
- kind: "call",
1805
- callee: helper,
1806
- args: [savedRef],
1807
- type: bytesT,
1808
- loc,
1809
- },
1810
- type: bytesT,
1811
- loc,
1812
- };
1813
- }
1814
- const argNode = call.arguments[0];
1815
- const fnArg = lowerer.lowerExpr(argNode);
1816
- if (fnArg.type.kind !== "func" ||
1817
- fnArg.type.params.length > 2 ||
1818
- !fnArg.type.params.every((p) => p.kind === "f64") ||
1819
- fnArg.type.ret.kind !== "f64") {
1820
- lowerer.badType(argNode, lowerer.typeOf(argNode));
1821
- }
1822
- const arity = fnArg.type.params.length;
1823
- const key = `bytes.toSorted:u8:${arity}`;
1824
- let helper = lowerer.arrHofHelpers.get(key);
1825
- if (!helper) {
1826
- helper = `%bytes.toSorted.${lowerer.arrHofHelpers.size}`;
1827
- lowerer.arrHofHelpers.set(key, helper);
1828
- lowerer.liftedFns.push(buildBytesSortFn(helper, arity, true, loc));
1829
- }
1830
- return {
1831
- kind: "call",
1832
- callee: helper,
1833
- args: [receiver, fnArg],
1834
- type: bytesT,
1835
- loc,
1836
- };
1837
- }
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
1962
  /** `n = a.length` — the once-up-front length read every array HOF loop
2038
1963
  * starts with (locals a.0/n.0 by convention). */
2039
1964
  function readLenStmt(arrT, loc) {
@@ -2111,14 +2036,6 @@ function lowerArrayAtCall(lowerer, call, access, elem) {
2111
2036
  const undefTag = lowerer.armTag(resultT.unionId, UNDEFINED_T);
2112
2037
  if (undefTag < 0)
2113
2038
  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
2039
  const key = `at:${typeKey(elem)}:${typeKey(resultT)}`;
2123
2040
  let name = lowerer.arrHofHelpers.get(key);
2124
2041
  if (!name) {
@@ -2128,10 +2045,7 @@ function lowerArrayAtCall(lowerer, call, access, elem) {
2128
2045
  const t = varRef("t.0", F64, loc);
2129
2046
  const i = varRef("i.0", F64, loc);
2130
2047
  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 };
2048
+ const found = lowerer.coerceToExpected(arrayValueRead(lowerer, varRef("a.0", arrT, loc), t, elem, loc), resultT);
2135
2049
  const miss = {
2136
2050
  kind: "unionWrap",
2137
2051
  unionId: resultT.unionId,
@@ -2183,12 +2097,6 @@ function lowerArrayAtCall(lowerer, call, access, elem) {
2183
2097
  else_: null,
2184
2098
  loc,
2185
2099
  },
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
2100
  { kind: "return", value: found, loc },
2193
2101
  ];
2194
2102
  lowerer.liftedFns.push({
@@ -2203,7 +2111,6 @@ function lowerArrayAtCall(lowerer, call, access, elem) {
2203
2111
  { id: "i.0", name: "i", type: F64, mutable: true },
2204
2112
  { id: "n.0", name: "n", type: F64, mutable: false },
2205
2113
  { id: "t.0", name: "t", type: F64, mutable: true },
2206
- { id: "v.0", name: "v", type: elem, mutable: false },
2207
2114
  ],
2208
2115
  body,
2209
2116
  loc,
@@ -2435,7 +2342,15 @@ export function lowerMapMethodCall(lowerer, call, access) {
2435
2342
  const name = access.name.text;
2436
2343
  if (!MAP_METHODS.has(name) && !MAP_ITER_METHODS.has(name))
2437
2344
  return null;
2438
- const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
2345
+ let receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
2346
+ let probedUntyped = false;
2347
+ if (receiverIr?.kind !== "map" && isJsSourceFile(access.getSourceFile())) {
2348
+ const probed = tryLowerExpression(lowerer, access.expression);
2349
+ if (probed?.type.kind === "map") {
2350
+ receiverIr = probed.type;
2351
+ probedUntyped = true;
2352
+ }
2353
+ }
2439
2354
  if (receiverIr?.kind !== "map")
2440
2355
  return null;
2441
2356
  if (!lowerer.isStdlibMember(access))
@@ -2479,7 +2394,7 @@ export function lowerMapMethodCall(lowerer, call, access) {
2479
2394
  return lowerMapIterDrainCall(lowerer, call, receiver, receiverIr, name);
2480
2395
  }
2481
2396
  // forEach
2482
- return lowerer.lowerMapForEachCall(call, receiver, receiverIr);
2397
+ return lowerMapForEachCall(lowerer, call, receiver, receiverIr, probedUntyped);
2483
2398
  }
2484
2399
  /** The iterator methods the lowering DOES cover — in exactly one context. */
2485
2400
  const MAP_ITER_METHODS = new Set(["keys", "values", "entries"]);
@@ -2599,12 +2514,52 @@ function buildMapIterDrainFn(name, mapT, method, elemT, tupleT, loc) {
2599
2514
  * callback throws. That is JS's live-iteration contract, Node-verified
2600
2515
  * (SEMANTICS.md). The callback receives (value, key) like JS; declaring
2601
2516
  * fewer parameters — (value) or () — is ordinary TS and supported. */
2602
- export function lowerMapForEachCall(lowerer, call, receiver, mapT) {
2517
+ export function lowerMapForEachCall(lowerer, call, receiver, mapT, bindUntyped = false) {
2603
2518
  const loc = locOf(call);
2604
2519
  const argNode = call.arguments[0];
2605
2520
  if (!argNode)
2606
2521
  lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded
2607
- const fnArg = lowerer.lowerExpr(argNode);
2522
+ const previousRuntime = [];
2523
+ const previousImplicit = lowerer.implicitParamTypes;
2524
+ let contextualTs = null;
2525
+ if (bindUntyped && (ts.isArrowFunction(argNode) || ts.isFunctionExpression(argNode))) {
2526
+ const expected = [mapT.value, mapT.key];
2527
+ argNode.parameters.slice(0, 2).forEach((param, i) => {
2528
+ const type = expected[i];
2529
+ if (type === undefined || !ts.isIdentifier(param.name) || param.type || param.initializer || param.dotDotDotToken)
2530
+ return;
2531
+ const checkerType = lowerer.checker.getTypeAtLocation(param.name);
2532
+ if ((checkerType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) === 0)
2533
+ return;
2534
+ const symbol = lowerer.checker.getSymbolAtLocation(param.name);
2535
+ if (!symbol)
2536
+ return;
2537
+ previousRuntime.push({ symbol, type: lowerer.runtimeOptionalBindingTypes.get(symbol) });
2538
+ lowerer.runtimeOptionalBindingTypes.set(symbol, type);
2539
+ const primitive = type.kind === "string" ? lowerer.checker.getStringType() :
2540
+ type.kind === "f64" ? lowerer.checker.getNumberType() :
2541
+ type.kind === "bool" ? lowerer.checker.getBooleanType() : null;
2542
+ if (primitive !== null) {
2543
+ contextualTs ??= new Map(previousImplicit ?? []);
2544
+ contextualTs.set(symbol, primitive);
2545
+ }
2546
+ });
2547
+ }
2548
+ if (contextualTs !== null)
2549
+ lowerer.implicitParamTypes = contextualTs;
2550
+ let fnArg;
2551
+ try {
2552
+ fnArg = lowerer.lowerExpr(argNode);
2553
+ }
2554
+ finally {
2555
+ lowerer.implicitParamTypes = previousImplicit;
2556
+ for (const { symbol, type } of previousRuntime) {
2557
+ if (type === undefined)
2558
+ lowerer.runtimeOptionalBindingTypes.delete(symbol);
2559
+ else
2560
+ lowerer.runtimeOptionalBindingTypes.set(symbol, type);
2561
+ }
2562
+ }
2608
2563
  if (fnArg.type.kind !== "func" ||
2609
2564
  fnArg.type.params.length > 2 ||
2610
2565
  (fnArg.type.params.length >= 1 && !typeEquals(fnArg.type.params[0], mapT.value)) ||
@@ -2727,8 +2682,8 @@ export function lowerSetMethodCall(lowerer, call, access) {
2727
2682
  // Set<union-of-signatures>, but the VALUE lowered as a real Set of
2728
2683
  // identity tokens (the new-Set probe) — the lowered receiver's type
2729
2684
  // is the honest dispatch key.
2730
- if (receiverIr === null && isJsSourceFile(access.getSourceFile())) {
2731
- const probed = probeLower(lowerer, access.expression);
2685
+ if (receiverIr?.kind !== "set" && isJsSourceFile(access.getSourceFile())) {
2686
+ const probed = tryLowerExpression(lowerer, access.expression);
2732
2687
  if (probed?.type.kind === "set")
2733
2688
  receiverIr = probed.type;
2734
2689
  }
@@ -3906,7 +3861,7 @@ export function lowerForOfSearchParams(lowerer, stmt, iterable, proj) {
3906
3861
  export function lowerForOfArrayIter(lowerer, stmt, iterable, proj) {
3907
3862
  const loc = locOf(stmt);
3908
3863
  const arrT = iterable.type;
3909
- const elemT = arrT.kind === "array" ? arrT.elem : F64;
3864
+ const elemT = arrT.kind === "array" ? arrayValueType(lowerer, arrT.elem) : F64;
3910
3865
  const yieldsPair = proj === "entries";
3911
3866
  if (!ts.isVariableDeclarationList(stmt.initializer)) {
3912
3867
  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 +3880,7 @@ export function lowerForOfArrayIter(lowerer, stmt, iterable, proj) {
3925
3880
  i.mutable = true;
3926
3881
  const keyRead = () => varRef(i.id, F64, loc);
3927
3882
  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
- }
3883
+ ? arrayValueRead(lowerer, varRef(arr.id, arrT, loc), varRef(i.id, F64, loc), arrT.elem, loc)
3935
3884
  : {
3936
3885
  kind: "bytesIntrinsic",
3937
3886
  method: "get",
@@ -3954,6 +3903,11 @@ export function lowerForOfArrayIter(lowerer, stmt, iterable, proj) {
3954
3903
  binds.push({ kind: "varDecl", localId: kLocal.id, init: keyRead(), loc });
3955
3904
  if (els[1]) {
3956
3905
  const vLocal = lowerer.declareLocal(els[1].name, els[1].name.text, elemT, isLet);
3906
+ if (arrT.kind === "array" && !typeEquals(elemT, arrT.elem)) {
3907
+ const root = lowerer.runtimeOptionalRootOf(vLocal);
3908
+ lowerer.runtimeOptionalLocals.add(root);
3909
+ lowerer.runtimeOptionalStorageLocals.add(root);
3910
+ }
3957
3911
  binds.push({ kind: "varDecl", localId: vLocal.id, init: valRead(), loc });
3958
3912
  }
3959
3913
  }
@@ -3964,6 +3918,13 @@ export function lowerForOfArrayIter(lowerer, stmt, iterable, proj) {
3964
3918
  const declT = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(decl.name));
3965
3919
  if (yieldsPair) {
3966
3920
  const shape = declT?.kind === "record" ? lowerer.shapes.get(declT.shapeId) : null;
3921
+ if (declT?.kind === "record" && shape?.tuple && arrT.kind === "array") {
3922
+ const valueField = shape.fields.find((field) => field.name === "1");
3923
+ if (valueField && typeEquals(valueField.type, arrT.elem) && !typeEquals(elemT, arrT.elem)) {
3924
+ valueField.type = elemT;
3925
+ lowerer.runtimeOptionalFields.add(lowerer.runtimeOptionalFieldKey(declT.shapeId, "1"));
3926
+ }
3927
+ }
3967
3928
  if (declT?.kind !== "record" || !shape?.tuple || shape.fields.length !== 2 ||
3968
3929
  shape.fields.find((f) => f.name === "0")?.type.kind !== "f64" ||
3969
3930
  !typeEquals(shape.fields.find((f) => f.name === "1")?.type ?? F64, elemT)) {
@@ -4373,1226 +4334,6 @@ function buildMapSeedFn(mapT, name, arrT, tupleT, kT, vT, loc) {
4373
4334
  loc,
4374
4335
  };
4375
4336
  }
4376
- /** Regex method calls, both directions: `re.test(s)` on a regex receiver,
4377
- * and `s.replace(re, tpl)` / `s.replaceAll(re, tpl)` / `s.split(re)` on a
4378
- * string receiver whose FIRST ARGUMENT is a regex (the string-pattern
4379
- * overloads keep their island lowering — the argument's mapped type is
4380
- * what routes here, before lowerIslandMethodCall can claim the name).
4381
- * Null when neither shape matches. */
4382
- export function lowerRegexMethodCall(lowerer, call, access, dynReceiver) {
4383
- // chainBlocked, not a raw token test: an optional-chain re-dispatch
4384
- // (`rawName?.match(re)` — the receiver reads back chain-narrowed)
4385
- // rides the same lowering as the plain spelling.
4386
- if (lowerer.chainBlocked(access, call))
4387
- return null;
4388
- // A validated dyn receiver (lowerStringMethodCall's story) is a STRING
4389
- // by construction — the symbol gate doesn't apply to `any` receivers.
4390
- if (dynReceiver === undefined && !lowerer.isStdlibMember(access))
4391
- return null;
4392
- const name = access.name.text;
4393
- const receiverKind = dynReceiver ? "string" : lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind;
4394
- const lowerReceiver = () => (dynReceiver ? dynReceiver() : lowerer.lowerExpr(access.expression));
4395
- const loc = locOf(call);
4396
- if (receiverKind === "regex" && name === "test") {
4397
- // The statefulness fence, at compile time where the flags are
4398
- // visible: a literal receiver (possibly parenthesized). Values that
4399
- // flow through variables hit the same fence at runtime.
4400
- let recv = access.expression;
4401
- while (ts.isParenthesizedExpression(recv))
4402
- recv = recv.expression;
4403
- if (ts.isRegularExpressionLiteral(recv)) {
4404
- const flags = recv.text.slice(recv.text.lastIndexOf("/") + 1);
4405
- if (flags.includes("g") || flags.includes("y")) {
4406
- lowerer.unsupported("SC1121", call);
4407
- }
4408
- }
4409
- const receiver = lowerer.lowerExpr(access.expression);
4410
- const args = call.arguments.map((a) => lowerer.lowerExpr(a));
4411
- return { kind: "regexIntrinsic", method: "test", receiver, args, type: BOOL, loc };
4412
- }
4413
- // `re.exec(s)` for non-g/y regexes: spec-identical to `s.match(re)`
4414
- // (Symbol.match delegates to exec when lastIndex is out of play), so
4415
- // it lowers to the SAME match intrinsic with the operands swapped —
4416
- // the honest `string[] | null` slice, nonparticipating captures ""
4417
- // (match's documented rule). The g/y statefulness fence applies at
4418
- // compile time on literal receivers, exactly test()'s stance; values
4419
- // reaching the runtime with those flags abort there.
4420
- if (receiverKind === "regex" && name === "exec") {
4421
- if (call.arguments.length !== 1)
4422
- return null; // exec takes exactly the subject
4423
- let recv = access.expression;
4424
- while (ts.isParenthesizedExpression(recv))
4425
- recv = recv.expression;
4426
- if (ts.isRegularExpressionLiteral(recv)) {
4427
- const flags = recv.text.slice(recv.text.lastIndexOf("/") + 1);
4428
- if (flags.includes("g") || flags.includes("y")) {
4429
- lowerer.unsupported("SC1121", call);
4430
- }
4431
- }
4432
- const re = lowerer.lowerExpr(access.expression);
4433
- const subject = lowerer.lowerExprExpecting(call.arguments[0], STRING);
4434
- const resultT = { kind: "union", unionId: lowerer.unions.intern([arrayOf(STRING), { kind: "nullT" }]) };
4435
- return { kind: "regexIntrinsic", method: "match", receiver: subject, args: [re], type: resultT, loc };
4436
- }
4437
- // `s.match(re)` for non-g/y regexes: Node's exec-shaped result reduced
4438
- // to the honest slice — the `string[] | null` union holding
4439
- // [whole match, ...captures] or the null arm. The g-flag match returns
4440
- // EVERY match (a different shape) and /y is stateful — both fence at
4441
- // compile time on literal arguments (values reaching the runtime with
4442
- // those flags abort, the test() stance). `.index`/`.input` reads on
4443
- // the result fence per member (array-typed value); `.groups` reads
4444
- // desugar at their access sites when the regex is statically known
4445
- // (lowerMatchGroupsRead).
4446
- // `s.match(re)` also claims a NULLABLE string receiver (string + unit
4447
- // arms — `process.versions.openssl.match(...)`, the Dict<string>
4448
- // member the node suite's crypto helper reads): the checked
4449
- // extraction narrows to the string arm and a unit value throws the
4450
- // catchable TypeError at the read, where Node's own member read
4451
- // throws — tsc only admits the spelling in JS sources.
4452
- const nullableStringRecv = receiverKind === "union" &&
4453
- name === "match" &&
4454
- (() => {
4455
- const t = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
4456
- if (t?.kind !== "union")
4457
- return false;
4458
- const arms = lowerer.unions.get(t.unionId)?.arms ?? [];
4459
- return arms.some((a) => a.kind === "string") && arms.every((a) => a.kind === "string" || isUnitType(a));
4460
- })();
4461
- if ((receiverKind === "string" || nullableStringRecv) && name === "match") {
4462
- const arg0 = call.arguments[0];
4463
- if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex")
4464
- return null; // string-pattern match: the SC2020 fence
4465
- if (call.arguments.length !== 1)
4466
- return null;
4467
- let reNode = arg0;
4468
- while (ts.isParenthesizedExpression(reNode))
4469
- reNode = reNode.expression;
4470
- if (ts.isRegularExpressionLiteral(reNode)) {
4471
- const flags = reNode.text.slice(reNode.text.lastIndexOf("/") + 1);
4472
- if (flags.includes("g") || flags.includes("y")) {
4473
- 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
- }
4475
- }
4476
- const receiver = nullableStringRecv ? lowerer.lowerExprExpecting(access.expression, STRING) : lowerReceiver();
4477
- const re = lowerer.lowerExpr(arg0);
4478
- // RegExpMatchArray | null maps to the string[] | null union by
4479
- // itself; intern it directly when the checker's spelling doesn't —
4480
- // or when it maps to something WIDER (an optional-chain call node
4481
- // types `s?.match(re)` with the chain's `| undefined`; the intrinsic
4482
- // itself answers string[] | null, and the chain wrapper widens).
4483
- const exactT = { kind: "union", unionId: lowerer.unions.intern([arrayOf(STRING), { kind: "nullT" }]) };
4484
- const mapped = lowerer.mapTypeOf(lowerer.typeOf(call));
4485
- const resultT = mapped && typeEquals(mapped, exactT) ? mapped : exactT;
4486
- return { kind: "regexIntrinsic", method: "match", receiver, args: [re], type: resultT, loc };
4487
- }
4488
- // `s.matchAll(re)` — the every-match iterator drained EAGERLY into a
4489
- // string[][] (one honest match slice per row — match's rule). Lazy vs
4490
- // eager is unobservable here: strings are immutable, and the spec
4491
- // clones the regex at the call, so nothing can perturb the drain. The
4492
- // eager array IS what the two lowered consumers see anyway (the
4493
- // immediate [...spread] and the for-of walk); a stored iterator's
4494
- // .next() fences as an array member like any other. Non-global
4495
- // regexes throw Node's exact TypeError at runtime, catchably
4496
- // (replaceAll's stance).
4497
- if (receiverKind === "string" && name === "matchAll") {
4498
- const arg0 = call.arguments[0];
4499
- if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex")
4500
- return null; // string-pattern form: the SC2020 fence
4501
- if (call.arguments.length !== 1)
4502
- return null;
4503
- const receiver = lowerReceiver();
4504
- const re = lowerer.lowerExpr(arg0);
4505
- return {
4506
- kind: "regexIntrinsic",
4507
- method: "matchAll",
4508
- receiver,
4509
- args: [re],
4510
- type: arrayOf(arrayOf(STRING)),
4511
- loc,
4512
- };
4513
- }
4514
- // `s.search(re)` — the first match's UTF-16 index, or -1. No g/y fence
4515
- // (unlike test/match): Symbol.search neither reads nor writes lastIndex
4516
- // — a fresh exec from position 0, so /g is irrelevant and /y anchors at
4517
- // 0, exactly Node. Never throws.
4518
- if (receiverKind === "string" && name === "search") {
4519
- const arg0 = call.arguments[0];
4520
- if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex")
4521
- return null; // string-pattern form: the SC2020 fence
4522
- if (call.arguments.length !== 1)
4523
- return null;
4524
- const receiver = lowerReceiver();
4525
- const re = lowerer.lowerExpr(arg0);
4526
- return { kind: "regexIntrinsic", method: "search", receiver, args: [re], type: F64, loc };
4527
- }
4528
- if (receiverKind === "string" &&
4529
- (name === "replace" || name === "replaceAll" || name === "split")) {
4530
- const arg0 = call.arguments[0];
4531
- if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex")
4532
- return null;
4533
- const receiver = lowerReceiver();
4534
- // Split's omitted or undefined limit is 2^32-1. Complete it here so
4535
- // both IR backends and the runtime have one required (regex, limit)
4536
- // shape; an optional-number value selects the default at runtime.
4537
- const args = name === "split"
4538
- ? [lowerer.lowerExpr(arg0), lowerSplitLimitArg(lowerer, call.arguments[1], loc)]
4539
- : call.arguments.map((a) => lowerer.lowerExpr(a));
4540
- if (name !== "split" && args[1]?.type.kind !== "string") {
4541
- lowerer.unsupported("SC1120", call.arguments[1] ?? call, "function replacement values (replacements must be string templates)");
4542
- }
4543
- return {
4544
- kind: "regexIntrinsic",
4545
- method: name,
4546
- receiver,
4547
- args,
4548
- type: name === "split" ? arrayOf(STRING) : STRING,
4549
- loc,
4550
- };
4551
- }
4552
- return null;
4553
- }
4554
- /** `s.slice(1, 4)` and friends → strIntrinsic. Null when this isn't an
4555
- * ambient string method call (caller keeps its generic rejection). Missing
4556
- * optional arguments are omitted from `args` — the backend fills the
4557
- * documented defaults; the IR never encodes them (Infinity isn't
4558
- * JSON-safe). tsc has already checked arity and argument types against
4559
- * ambient/scriptc.d.ts. */
4560
- export function lowerStringMethodCall(lowerer, call, access, dynReceiver) {
4561
- if (lowerer.chainBlocked(access, call))
4562
- return null;
4563
- if (dynReceiver === undefined && access.name.text === "localeCompare")
4564
- return lowerLocaleCompareCall(lowerer, call, access);
4565
- const entry = own(STR_METHODS, access.name.text);
4566
- if (!entry)
4567
- return null;
4568
- // A validated dyn receiver (`pkg.name.replace(...)` on a JSON.parse
4569
- // value) arrives pre-extracted through `dynReceiver`; its checker type
4570
- // is `any`, so the type/symbol gates don't apply — the dyn value's
4571
- // methods can only BE the string intrinsics.
4572
- if (dynReceiver === undefined) {
4573
- const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
4574
- // `require.main?.filename.startsWith(...)`: the checker types the
4575
- // receiver `string | undefined` (the chain's short-circuit arm), but
4576
- // the entry-module fold lowers it to a compile-time STRING — the
4577
- // suite harness's skip() shape. Everything else keeps the strict
4578
- // string gate.
4579
- if (receiverIr?.kind !== "string" && !isRequireMainFilename(lowerer, access.expression))
4580
- return null;
4581
- if (!lowerer.isStdlibMember(access))
4582
- return null;
4583
- }
4584
- // The lib declares optional parameters beyond the lowered forms
4585
- // (includes/startsWith/endsWith take a position); fence the unlowered
4586
- // arities instead of passing arguments the runtime doesn't take.
4587
- if (call.arguments.length < entry.minArgs || call.arguments.length > entry.maxArgs) {
4588
- lowerer.noLowering(`.${access.name.text} with ${call.arguments.length} argument${call.arguments.length === 1 ? "" : "s"} on strings`, call);
4589
- }
4590
- const receiver = dynReceiver ? dynReceiver() : lowerer.lowerExpr(access.expression);
4591
- const args = entry.method === "split"
4592
- ? [lowerer.lowerExpr(call.arguments[0]), lowerSplitLimitArg(lowerer, call.arguments[1], locOf(call))]
4593
- : call.arguments.map((a) => lowerer.lowerExpr(a));
4594
- // split's separator must BE a string here (a regex argument was
4595
- // claimed by lowerRegexMethodCall before this path) — the lib's
4596
- // `string | RegExp` union has no lowering as a VALUE.
4597
- if (entry.method === "split" && args[0].type.kind !== "string") {
4598
- lowerer.unsupported("SC1090", call.arguments[0], `'.split()' on a '${lowerer.fmt(args[0].type)}' separator (pass a string, or a regex literal)`);
4599
- }
4600
- // padStart/padEnd with the fill omitted: Node pads with " " — the
4601
- // same call with the default made explicit.
4602
- if ((entry.method === "padStart" || entry.method === "padEnd") && args.length === 1) {
4603
- args.push({ kind: "strLit", value: " ", type: STRING, loc: locOf(call) });
4604
- }
4605
- return {
4606
- kind: "strIntrinsic",
4607
- method: entry.method,
4608
- receiver,
4609
- args,
4610
- type: entry.result,
4611
- loc: locOf(call),
4612
- };
4613
- }
4614
- /** `a.localeCompare(b)` — the one-argument form only (locales/options
4615
- * select ICU collations that do not exist here). Lowers to an interned
4616
- * synthetic function returning -1/0/1 by CODE-UNIT order — the same
4617
- * ordering as the string relational operators — NOT Node's ICU default
4618
- * collation: a documented divergence (SEMANTICS.md; e.g. Node says
4619
- * "a" < "B" under ICU while code units say "B" < "a"). For same-case
4620
- * ASCII the orders agree. Null when the receiver isn't a stdlib string
4621
- * (caller keeps its generic rejection). */
4622
- function lowerLocaleCompareCall(lowerer, call, access) {
4623
- const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
4624
- if (receiverIr?.kind !== "string")
4625
- return null;
4626
- if (!lowerer.isStdlibMember(access))
4627
- return null;
4628
- const loc = locOf(call);
4629
- if (call.arguments.length !== 1) {
4630
- lowerer.noLowering(`.localeCompare with ${call.arguments.length} arguments`, call, "the locales/options parameters select ICU collations that have no lowering — " +
4631
- "pass exactly the comparison string");
4632
- }
4633
- const receiver = lowerer.lowerExpr(access.expression);
4634
- const arg = lowerer.lowerExpr(call.arguments[0]);
4635
- if (arg.type.kind !== "string")
4636
- lowerer.badType(call.arguments[0], lowerer.typeOf(call.arguments[0]));
4637
- const key = "localeCompare";
4638
- let helper = lowerer.arrHofHelpers.get(key);
4639
- if (!helper) {
4640
- helper = `%str.localeCompare`;
4641
- lowerer.arrHofHelpers.set(key, helper);
4642
- lowerer.liftedFns.push(buildLocaleCompareFn(helper, loc));
4643
- }
4644
- return { kind: "call", callee: helper, args: [receiver, arg], type: F64, loc };
4645
- }
4646
- /** `return a < b ? -1 : a > b ? 1 : 0` over the strCmp primitive (the
4647
- * relational operators' exact machinery — one interned helper, no new IR
4648
- * or runtime surface). */
4649
- function buildLocaleCompareFn(name, loc) {
4650
- const cmp = (op) => ({ kind: "strCmp", op, left: varRef("a.0", STRING, loc), right: varRef("b.0", STRING, loc), type: BOOL, loc });
4651
- const body = [
4652
- {
4653
- kind: "return",
4654
- value: {
4655
- kind: "ternary",
4656
- cond: cmp("<"),
4657
- then: numLit(-1, loc),
4658
- else_: { kind: "ternary", cond: cmp(">"), then: numLit(1, loc), else_: numLit(0, loc), type: F64, loc },
4659
- type: F64,
4660
- loc,
4661
- },
4662
- loc,
4663
- },
4664
- ];
4665
- return {
4666
- name,
4667
- params: [
4668
- { localId: "a.0", name: "a", type: STRING },
4669
- { localId: "b.0", name: "b", type: STRING },
4670
- ],
4671
- returnType: F64,
4672
- locals: [
4673
- { id: "a.0", name: "a", type: STRING, mutable: true },
4674
- { id: "b.0", name: "b", type: STRING, mutable: true },
4675
- ],
4676
- body,
4677
- loc,
4678
- };
4679
- }
4680
- /* ── typed arrays / Buffer ─────────────────────────────────────────────── */
4681
- /** The typed-array constructors with a runtime representation, by lib
4682
- * interface name. The other TypedArray flavors (Int8Array, Float64Array,
4683
- * DataView, ...) fall through to the generic stdlib-constructor fence. */
4684
- const BYTES_CTORS = {
4685
- Uint8Array: "u8",
4686
- Uint32Array: "u32",
4687
- Int32Array: "i32",
4688
- Float32Array: "f32",
4689
- };
4690
- /** `new Uint8Array(...)` / `new Uint32Array(...)` / `new Float32Array(...)`
4691
- * (stdlib provenance — a user's own class with the name resolves through
4692
- * classBySymbol). Lowered argument shapes: none (empty), a length
4693
- * (zero-filled; ToIndex at runtime — invalid lengths throw Node's
4694
- * RangeError), a same-kind typed array or Buffer (an independent COPY —
4695
- * the readFile chain's `new Uint8Array(await readFile(p))`), a number[]
4696
- * literal (element-coerced; its contextual type is the lib's
4697
- * ArrayLike/Iterable union, which cannot map — the Set-seed pattern), or
4698
- * a number[]-typed value. ArrayBuffer/view forms are fenced: scriptc
4699
- * typed arrays own their storage. Null when this isn't a stdlib
4700
- * typed-array construction. */
4701
- export function lowerBytesNew(lowerer, expr, symbol) {
4702
- if (symbol && symbol.name === "DataView" && lowerer.isStdlibSymbol(symbol)) {
4703
- return lowerDataViewNew(lowerer, expr);
4704
- }
4705
- const elem = symbol ? own(BYTES_CTORS, symbol.name) : undefined;
4706
- if (!elem || !symbol || !lowerer.isStdlibSymbol(symbol))
4707
- return null;
4708
- const name = symbol.name;
4709
- const type = bytesOf(elem);
4710
- const loc = locOf(expr);
4711
- const args = expr.arguments ?? [];
4712
- if (args.length === 0)
4713
- return { kind: "bytesNew", source: null, type, loc };
4714
- if (args.length === 1 && !ts.isSpreadElement(args[0])) {
4715
- const argNode = args[0];
4716
- if (ts.isArrayLiteralExpression(argNode) && !argNode.elements.some(ts.isSpreadElement)) {
4717
- const elems = argNode.elements.map((el) => lowerer.lowerExprExpecting(el, F64));
4718
- const seed = { kind: "arrayLit", elems, type: arrayOf(F64), loc };
4719
- return { kind: "bytesNew", source: seed, type, loc };
4720
- }
4721
- // The SYNTACTIC `new T(new ArrayBuffer(n))` and `new T(new
4722
- // SharedArrayBuffer(n))` forms — fresh-buffer construction (the
4723
- // shared spelling is the Atomics.wait sleep idiom's). The buffer
4724
- // never exists as a value: n must be a byte-length LITERAL divisible
4725
- // by the element size (tsc would admit any number; a bad one is
4726
- // Node's RangeError — rejected at compile time instead of
4727
- // half-lowering), and the whole expression is a zero-filled typed
4728
- // array of n/elemSize elements. Erasing the buffer is exact: nothing
4729
- // else can ever reference it, so neither sharing (scriptc has no
4730
- // threads) nor aliasing is observable — SEMANTICS.md documents the
4731
- // stance. The RESIZABLE form (a maxByteLength options bag) fences by
4732
- // name: resize needs the buffer to exist as a value, and none does.
4733
- if (ts.isNewExpression(argNode) &&
4734
- ts.isIdentifier(argNode.expression) &&
4735
- (argNode.expression.text === "ArrayBuffer" ||
4736
- argNode.expression.text === "SharedArrayBuffer") &&
4737
- lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(argNode.expression) ?? undefined)) {
4738
- const bufCtor = argNode.expression.text;
4739
- if ((argNode.arguments?.length ?? 0) > 1) {
4740
- lowerer.noLowering(`new ${name} over a resizable ${bufCtor}`, argNode, "a maxByteLength options bag makes the buffer resizable, and resizing needs the buffer " +
4741
- "to exist as a runtime value — no free-standing ArrayBuffer value exists (the buffer here " +
4742
- "erases into the view): drop the options bag");
4743
- }
4744
- const elemSize = elem === "u8" ? 1 : 4;
4745
- const lenArg = argNode.arguments?.length === 1 ? argNode.arguments[0] : undefined;
4746
- const lenT = lenArg ? lowerer.typeOf(lenArg) : null;
4747
- const byteLen = lenT?.isNumberLiteralType() ? lenT.value : null;
4748
- if (byteLen === null || byteLen % elemSize !== 0 || byteLen < 0) {
4749
- lowerer.noLowering(`new ${name} over this ${bufCtor}`, argNode, `the byte length must be a number literal divisible by ${elemSize} — new ${name}(new ${bufCtor}(${elemSize}))`);
4750
- }
4751
- const count = { kind: "numLit", value: byteLen / elemSize, type: F64, loc };
4752
- return { kind: "bytesNew", source: count, type, loc };
4753
- }
4754
- const src = lowerer.lowerExpr(argNode);
4755
- if (src.type.kind === "f64" ||
4756
- typeEquals(src.type, type) ||
4757
- (src.type.kind === "array" && src.type.elem.kind === "f64")) {
4758
- return { kind: "bytesNew", source: src, type, loc };
4759
- }
4760
- if (src.type.kind === "bytes") {
4761
- lowerer.noLowering(`new ${name} over a '${lowerer.fmt(src.type)}'`, argNode, "cross-kind typed-array conversion has no lowering — copy element by element");
4762
- }
4763
- lowerer.noLowering(`new ${name} over '${lowerer.fmt(src.type)}' values`, argNode, `supported: new ${name}(), (length), (typedArray) — always a copy — or (number[]); ` +
4764
- "ArrayBuffers and views do not exist here (narrow unions first)");
4765
- }
4766
- lowerer.noLowering(`new ${name} with ${args.length} arguments`, expr, `supported: new ${name}(), (length), (typedArray), or (number[])`);
4767
- }
4768
- /** `new DataView(x.buffer, byteOffset?, byteLength?)` (stdlib provenance).
4769
- * The first argument must be the SYNTACTIC `.buffer` of a typed-array/
4770
- * Buffer value — `x.buffer` names x's own storage (scriptc typed arrays
4771
- * always own it whole, byteOffset 0), so the view construction takes x
4772
- * itself as the receiver and no ArrayBuffer value ever exists.
4773
- * byteOffset/byteLength are optional f64s (omitted args OMITTED, like
4774
- * slice); bad indices THROW Node's RangeErrors catchably. The result is
4775
- * a true borrowed view: reads and writes through it alias x. */
4776
- function lowerDataViewNew(lowerer, expr) {
4777
- const loc = locOf(expr);
4778
- const args = expr.arguments ?? [];
4779
- const bufNode = args[0];
4780
- if (!bufNode || args.length > 3 || args.some(ts.isSpreadElement)) {
4781
- lowerer.noLowering(`new DataView with ${args.length} arguments`, expr, "supported: new DataView(x.buffer), (x.buffer, byteOffset), or (x.buffer, byteOffset, byteLength)");
4782
- }
4783
- // The FRESH-BUFFER form: `new DataView(new ArrayBuffer(n), ...)` — the
4784
- // buffer erases into the view (nothing else can ever reference it, so
4785
- // the aliasing a shared buffer would exhibit is unobservable): the
4786
- // view's storage is a fresh zero-filled n-byte allocation. n must be a
4787
- // non-negative integer LITERAL (tsc admits any number; a fractional
4788
- // one is ToIndex truncation nothing here implements). byteOffset and
4789
- // byteLength keep their runtime Node-RangeError story. The RESIZABLE
4790
- // form (a maxByteLength options bag) fences by name.
4791
- if (ts.isNewExpression(bufNode) &&
4792
- ts.isIdentifier(bufNode.expression) &&
4793
- bufNode.expression.text === "ArrayBuffer" &&
4794
- lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(bufNode.expression) ?? undefined)) {
4795
- if ((bufNode.arguments?.length ?? 0) > 1) {
4796
- lowerer.noLowering("new DataView over a resizable ArrayBuffer", bufNode, "a maxByteLength options bag makes the buffer resizable, and resizing needs the buffer " +
4797
- "to exist as a runtime value — no free-standing ArrayBuffer value exists (the buffer here " +
4798
- "erases into the view): drop the options bag");
4799
- }
4800
- const lenArg = bufNode.arguments?.length === 1 ? bufNode.arguments[0] : undefined;
4801
- const lenT = lenArg ? lowerer.typeOf(lenArg) : null;
4802
- const byteLen = lenT?.isNumberLiteralType() ? lenT.value : null;
4803
- if (byteLen === null || !Number.isInteger(byteLen) || byteLen < 0) {
4804
- lowerer.noLowering("new DataView over this ArrayBuffer", bufNode, "the byte length must be a non-negative integer literal — new DataView(new ArrayBuffer(8))");
4805
- }
4806
- const count = { kind: "numLit", value: byteLen, type: F64, loc };
4807
- const receiver = { kind: "bytesNew", source: count, type: BYTES_U8, loc };
4808
- const idxArgs = args.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64));
4809
- return { kind: "bytesIntrinsic", method: "dataViewNew", receiver, args: idxArgs, type: BYTES_U8, loc };
4810
- }
4811
- const hint = "views compile over a typed array's own storage — new DataView(x.buffer, byteOffset?, byteLength?) " +
4812
- "where x is a Uint8Array/Uint32Array/Float32Array/Buffer value — or a fresh buffer erased into " +
4813
- "the view: new DataView(new ArrayBuffer(n), ...); free-standing ArrayBuffers have no representation";
4814
- if (!ts.isPropertyAccessExpression(bufNode) || bufNode.name.text !== "buffer" || bufNode.questionDotToken) {
4815
- lowerer.noLowering("new DataView over this buffer expression", bufNode, hint);
4816
- }
4817
- const srcIr = lowerer.mapTypeOf(lowerer.typeOf(bufNode.expression));
4818
- if (srcIr?.kind !== "bytes" || !lowerer.isStdlibMember(bufNode)) {
4819
- lowerer.noLowering(`new DataView over '.buffer' of a '${lowerer.checker.typeToString(lowerer.typeOf(bufNode.expression))}'`, bufNode, hint);
4820
- }
4821
- const receiver = lowerer.lowerExpr(bufNode.expression);
4822
- const idxArgs = args.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64));
4823
- return { kind: "bytesIntrinsic", method: "dataViewNew", receiver, args: idxArgs, type: BYTES_U8, loc };
4824
- }
4825
- /** Method calls on typed-array/Buffer receivers: slice and subarray (BOTH
4826
- * copy — subarray's sharing is the documented divergence), set(src,
4827
- * offset?), and the u8-only Buffer surface: toString(enc?) plus the
4828
- * whole numeric read/write family (fixed widths BE/LE and the
4829
- * variable-width read/writeUIntLE quartet — BUF_NUM_METHODS). Everything
4830
- * else the lib declares (fill, indexOf, reverse, ...) falls through to
4831
- * the SC2020 member fence. Null when this isn't a bytes method call. */
4832
- export function lowerBytesMethodCall(lowerer, call, access) {
4833
- if (lowerer.chainBlocked(access, call))
4834
- return null;
4835
- const name = access.name.text;
4836
- const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
4837
- if (receiverIr?.kind !== "bytes")
4838
- return null;
4839
- if (!lowerer.isStdlibMember(access))
4840
- return null;
4841
- const loc = locOf(call);
4842
- const nArgs = call.arguments.length;
4843
- if (receiverIr.elem === "u8" && name === "toSorted") {
4844
- return lowerBytesToSortedCall(lowerer, call, access, receiverIr);
4845
- }
4846
- if (receiverIr.elem === "u8" && name === "toReversed") {
4847
- if (nArgs !== 0) {
4848
- lowerer.noLowering(`.toReversed with ${nArgs} arguments on Uint8Array`, call);
4849
- }
4850
- return {
4851
- kind: "bytesIntrinsic",
4852
- method: "toReversed",
4853
- receiver: lowerer.lowerExpr(access.expression),
4854
- args: [],
4855
- type: receiverIr,
4856
- loc,
4857
- };
4858
- }
4859
- if (receiverIr.elem === "u8" && name === "with") {
4860
- if (nArgs !== 2 || call.arguments.some(ts.isSpreadElement)) {
4861
- lowerer.noLowering(`.with with ${nArgs} arguments on Uint8Array`, call);
4862
- }
4863
- return {
4864
- kind: "bytesIntrinsic",
4865
- method: "with",
4866
- receiver: lowerer.lowerExpr(access.expression),
4867
- args: [
4868
- lowerer.lowerExprExpecting(call.arguments[0], F64),
4869
- lowerer.lowerExprExpecting(call.arguments[1], F64),
4870
- ],
4871
- type: receiverIr,
4872
- loc,
4873
- };
4874
- }
4875
- if (receiverIr.elem === "u8" && name === "join") {
4876
- if (nArgs > 1 || call.arguments.some(ts.isSpreadElement)) {
4877
- lowerer.noLowering(`.join with ${nArgs} arguments on Uint8Array`, call);
4878
- }
4879
- const separatorDefault = {
4880
- kind: "strLit",
4881
- value: ",",
4882
- type: STRING,
4883
- loc,
4884
- };
4885
- const separator = call.arguments[0]
4886
- ? lowerOptionalDefaultArg(lowerer, call.arguments[0], STRING, separatorDefault)
4887
- : separatorDefault;
4888
- return {
4889
- kind: "bytesIntrinsic",
4890
- method: "join",
4891
- receiver: lowerer.lowerExpr(access.expression),
4892
- args: [separator],
4893
- type: STRING,
4894
- loc,
4895
- };
4896
- }
4897
- if (name === "slice" || name === "subarray") {
4898
- if (nArgs > 2) {
4899
- lowerer.noLowering(`.${name} with ${nArgs} arguments on typed arrays`, call);
4900
- }
4901
- const receiver = lowerer.lowerExpr(access.expression);
4902
- const args = call.arguments.map((a) => lowerer.lowerExprExpecting(a, F64));
4903
- // subarray is a VIEW (TypedArray.prototype.subarray aliases), and
4904
- // Buffer's slice() is subarray's deprecated Node alias — resolved by
4905
- // where the member is declared, the toString discipline below. Only
4906
- // the plain typed arrays' slice() copies (JS-exact).
4907
- const declaredOnBuffer = name === "slice" &&
4908
- (() => {
4909
- const nameSym = lowerer.checker.getSymbolAtLocation(access.name);
4910
- return nameSym !== undefined && lowerer.checker.declarationsOf(nameSym).some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer");
4911
- })();
4912
- const method = name === "subarray" || declaredOnBuffer ? "subarray" : "slice";
4913
- return { kind: "bytesIntrinsic", method, receiver, args, type: receiverIr, loc };
4914
- }
4915
- if (name === "fill" && receiverIr.elem !== "u8") {
4916
- // TypedArray.prototype.fill on the non-u8 kinds: per-element value
4917
- // coercion, slice-clamped relative indices, never throws. u8
4918
- // receivers keep the Buffer fill family (string patterns, throwing
4919
- // offset validation) — lowerBufferInstanceMethod's surface.
4920
- if (nArgs < 1 || nArgs > 3) {
4921
- lowerer.noLowering(`.fill with ${nArgs} arguments on typed arrays`, call);
4922
- }
4923
- const receiver = lowerer.lowerExpr(access.expression);
4924
- const v = lowerer.lowerExprExpecting(call.arguments[0], F64);
4925
- const idx = call.arguments.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64));
4926
- return { kind: "bytesIntrinsic", method: "fillElem", receiver, args: [v, ...idx], type: receiverIr, loc };
4927
- }
4928
- if (name === "set") {
4929
- if (nArgs < 1 || nArgs > 2) {
4930
- lowerer.noLowering(`.set with ${nArgs} arguments on typed arrays`, call);
4931
- }
4932
- const receiver = lowerer.lowerExpr(access.expression);
4933
- const src = lowerer.lowerExpr(call.arguments[0]);
4934
- if (!typeEquals(src.type, receiverIr)) {
4935
- lowerer.noLowering(`.set from '${lowerer.fmt(src.type)}' values`, call.arguments[0], "only a same-kind typed array copies in (number[] sources have no lowering — narrow unions first)");
4936
- }
4937
- const args = [src];
4938
- if (nArgs === 2)
4939
- args.push(lowerer.lowerExprExpecting(call.arguments[1], F64));
4940
- return { kind: "bytesIntrinsic", method: "setFrom", receiver, args, type: VOID, loc };
4941
- }
4942
- if (name === "toString") {
4943
- // Buffer's toString(encoding?) — utf8 by default. A 0-arg toString
4944
- // resolved against the plain Uint8Array interface is JS's
4945
- // Array-toString (comma join), a different operation: fenced.
4946
- const declaredOnBuffer = (() => {
4947
- const nameSym = lowerer.checker.getSymbolAtLocation(access.name);
4948
- return nameSym !== undefined && lowerer.checker.declarationsOf(nameSym).some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer");
4949
- })();
4950
- if (receiverIr.elem !== "u8" || !declaredOnBuffer) {
4951
- lowerer.noLowering("typed-array toString", call, 'Buffer.from(x).toString("utf8" | "hex" | "base64") is the lowered string conversion');
4952
- }
4953
- if (nArgs > 3)
4954
- lowerer.noLowering(`.toString with ${nArgs} arguments on Buffers`, call);
4955
- const receiver = lowerer.lowerExpr(access.expression);
4956
- const encNode = call.arguments[0];
4957
- let method = "toString";
4958
- let enc = { kind: "strLit", value: "utf8", type: STRING, loc };
4959
- if (encNode) {
4960
- const encType = lowerer.typeOf(encNode);
4961
- const encName = encType.isStringLiteralType()
4962
- ? knownBufEncoding(encType.value)
4963
- : undefined;
4964
- if (encName !== undefined) {
4965
- // Keep literals on the canonical, non-throwing fast path.
4966
- enc = { kind: "strLit", value: encName, type: STRING, loc };
4967
- }
4968
- else {
4969
- const undefinedArg = lowerStaticallyUndefinedArg(lowerer, encNode);
4970
- if (undefinedArg) {
4971
- // An explicit undefined is the omitted-encoding default. Keep
4972
- // the canonical, non-throwing path while preserving effects
4973
- // from equivalent spellings such as `void sideEffect()`.
4974
- enc = defaultAfterUndefined(undefinedArg, enc);
4975
- }
4976
- else {
4977
- // A BufferEncoding-typed variable selects its decoder at
4978
- // runtime. Optional variables default their undefined arm to
4979
- // utf8; the checked intrinsic canonicalizes every present
4980
- // alias/case and raises Node's ERR_UNKNOWN_ENCODING when a cast
4981
- // lets a bad value through.
4982
- enc = lowerOptionalDefaultArg(lowerer, encNode, STRING, enc);
4983
- method = "toStringVar";
4984
- }
4985
- }
4986
- }
4987
- // The range form toString(enc, start[, end]) decodes the clamped
4988
- // [start, end) byte window (Node's slice-then-decode). An omitted
4989
- // end stays omitted (2 intrinsic args) — the emitter supplies the
4990
- // receiver's length, because EXPLICIT negative ends clamp to empty
4991
- // in Node and no in-band sentinel can represent "omitted" safely.
4992
- if (nArgs > 1) {
4993
- const start = lowerer.lowerExprExpecting(call.arguments[1], F64);
4994
- if (nArgs === 3) {
4995
- const end = lowerer.lowerExprExpecting(call.arguments[2], F64);
4996
- return { kind: "bytesIntrinsic", method, receiver, args: [enc, start, end], type: STRING, loc };
4997
- }
4998
- return { kind: "bytesIntrinsic", method, receiver, args: [enc, start], type: STRING, loc };
4999
- }
5000
- return { kind: "bytesIntrinsic", method, receiver, args: [enc], type: STRING, loc };
5001
- }
5002
- // The Buffer-declared comparison/search/mutation surface. All of
5003
- // these resolve against the Buffer interface (the checker keeps most
5004
- // off plain typed arrays; where the lib DOES declare a same-named
5005
- // TypedArray member — indexOf, includes, fill — the semantics differ,
5006
- // so only Buffer-declared resolutions lower and u8 receivers gate the
5007
- // rest).
5008
- const declOnBuffer = (() => {
5009
- const nameSym = lowerer.checker.getSymbolAtLocation(access.name);
5010
- return nameSym !== undefined && lowerer.checker.declarationsOf(nameSym).some((d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer");
5011
- })();
5012
- if (declOnBuffer && receiverIr.elem === "u8") {
5013
- const bufMethod = lowerBufferInstanceMethod(lowerer, call, access, name, loc);
5014
- if (bufMethod)
5015
- return bufMethod;
5016
- }
5017
- // The Buffer numeric read/write families — every fixed-width kind in
5018
- // both endiannesses ("Uint" and "UInt" alike: Node aliases both), plus
5019
- // the variable-width read/writeUIntLE quartet. The kind token rides as
5020
- // a strLit args[0]; omitted offsets complete to Node's default 0.
5021
- const numKind = own(BUF_NUM_METHODS, name);
5022
- if (numKind !== undefined) {
5023
- if (receiverIr.elem !== "u8") {
5024
- lowerer.noLowering(`.${name} on a '${lowerer.fmt(receiverIr)}'`, call, "the numeric families read/write Buffer bytes");
5025
- }
5026
- const write = name.startsWith("write");
5027
- const required = write ? 1 : 0; // value; offset defaults to 0
5028
- if (nArgs < required || nArgs > required + 1) {
5029
- lowerer.noLowering(`.${name} with ${nArgs} arguments`, call);
5030
- }
5031
- const receiver = lowerer.lowerExpr(access.expression);
5032
- const kind = { kind: "strLit", value: numKind, type: STRING, loc };
5033
- const args = [kind, ...call.arguments.map((a) => lowerer.lowerExprExpecting(a, F64))];
5034
- if (nArgs === required) {
5035
- args.push({ kind: "numLit", value: 0, type: F64, loc }); // Node's offset default
5036
- }
5037
- return { kind: "bytesIntrinsic", method: write ? "writeNum" : "readNum", receiver, args, type: F64, loc };
5038
- }
5039
- const varKind = own(BUF_NUM_VAR_METHODS, name);
5040
- if (varKind !== undefined) {
5041
- if (receiverIr.elem !== "u8") {
5042
- lowerer.noLowering(`.${name} on a '${lowerer.fmt(receiverIr)}'`, call, "the numeric families read/write Buffer bytes");
5043
- }
5044
- const write = name.startsWith("write");
5045
- const required = write ? 3 : 2; // Node declares offset AND byteLength required
5046
- if (nArgs !== required) {
5047
- lowerer.noLowering(`.${name} with ${nArgs} arguments`, call);
5048
- }
5049
- const receiver = lowerer.lowerExpr(access.expression);
5050
- const kind = { kind: "strLit", value: varKind, type: STRING, loc };
5051
- const args = [kind, ...call.arguments.map((a) => lowerer.lowerExprExpecting(a, F64))];
5052
- return { kind: "bytesIntrinsic", method: write ? "writeNumVar" : "readNumVar", receiver, args, type: F64, loc };
5053
- }
5054
- // DataView getters (DataView maps to bytes<u8>, so the receiver kind
5055
- // and stdlib provenance land here; the checker keeps these names off
5056
- // typed arrays and Buffers). The multi-byte kinds take the optional
5057
- // littleEndian bool (omitted = big-endian, the JS default). All THROW
5058
- // Node's RangeError on a bad offset (may-throw seeds).
5059
- const dvGetter = own(DV_GETTERS, name);
5060
- if (dvGetter !== undefined && receiverIr.elem === "u8") {
5061
- if (dvGetter.method === "dvGetBigUint64Number" || dvGetter.method === "dvGetBigInt64Number") {
5062
- // bigint values have no representation — only the COMPOSED
5063
- // `Number(view.getBigUint64/getBigInt64(...))` compiles (the
5064
- // randomBytesToString pattern): the 8-byte integer converts to
5065
- // double exactly as Number(bigint) would, and the bigint never
5066
- // exists as a value (the f64-typed intrinsic result IS what the
5067
- // identity Number(f64) lowering hands back).
5068
- const p = call.parent;
5069
- const composed = ts.isCallExpression(p) &&
5070
- p.arguments.length === 1 &&
5071
- p.arguments[0] === call &&
5072
- ts.isIdentifier(p.expression) &&
5073
- p.expression.text === "Number" &&
5074
- lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(p.expression) ?? undefined);
5075
- if (!composed) {
5076
- lowerer.noLowering(`.${name} outside Number(...)`, call, `bigint values have no representation — Number(view.${name}(offset, littleEndian?)) is the lowered form`);
5077
- }
5078
- }
5079
- const maxArgs = dvGetter.le ? 2 : 1;
5080
- if (nArgs < 1 || nArgs > maxArgs) {
5081
- lowerer.noLowering(`.${name} with ${nArgs} arguments`, call);
5082
- }
5083
- const receiver = lowerer.lowerExpr(access.expression);
5084
- const args = [lowerer.lowerExprExpecting(call.arguments[0], F64)];
5085
- if (nArgs === 2)
5086
- args.push(lowerer.lowerExprExpecting(call.arguments[1], BOOL));
5087
- return { kind: "bytesIntrinsic", method: dvGetter.method, receiver, args, type: F64, loc };
5088
- }
5089
- // DataView setters — the getters' mirror: (byteOffset, value) plus the
5090
- // optional littleEndian bool on the multi-byte kinds. Void results;
5091
- // the same constant Node RangeError on a bad offset (may-throw seeds).
5092
- // setBigUint64/setBigInt64 never lower (bigint ARGUMENTS have no
5093
- // representation and no composed form exists) — they keep the member
5094
- // fence, as does setFloat16.
5095
- const dvSetter = own(DV_SETTERS, name);
5096
- if (dvSetter !== undefined && receiverIr.elem === "u8") {
5097
- const maxArgs = dvSetter.le ? 3 : 2;
5098
- if (nArgs < 2 || nArgs > maxArgs) {
5099
- lowerer.noLowering(`.${name} with ${nArgs} arguments`, call);
5100
- }
5101
- const receiver = lowerer.lowerExpr(access.expression);
5102
- const args = [
5103
- lowerer.lowerExprExpecting(call.arguments[0], F64),
5104
- lowerer.lowerExprExpecting(call.arguments[1], F64),
5105
- ];
5106
- if (nArgs === 3)
5107
- args.push(lowerer.lowerExprExpecting(call.arguments[2], BOOL));
5108
- return { kind: "bytesIntrinsic", method: dvSetter.method, receiver, args, type: VOID, loc };
5109
- }
5110
- return null;
5111
- }
5112
- /** Node's Buffer encoding names → the runtime's canonical spelling (every
5113
- * alias folds at compile time; the runtime sees only canonical names).
5114
- * Shared by toString, Buffer.from, and Buffer.byteLength. */
5115
- const BUF_ENCODINGS = {
5116
- utf8: "utf8",
5117
- "utf-8": "utf8",
5118
- hex: "hex",
5119
- base64: "base64",
5120
- base64url: "base64url",
5121
- latin1: "latin1",
5122
- binary: "latin1",
5123
- ascii: "ascii",
5124
- utf16le: "utf16le",
5125
- "utf-16le": "utf16le",
5126
- ucs2: "utf16le",
5127
- "ucs-2": "utf16le",
5128
- };
5129
- /** A literal encoding's canonical name, or undefined for a spelling Node
5130
- * does not know — the ladder callers (stream options) turn unknown
5131
- * literals into Node's runtime ERR_UNKNOWN_ENCODING throw. */
5132
- export function knownBufEncoding(name) {
5133
- return own(BUF_ENCODINGS, name);
5134
- }
5135
- /** The literal encoding argument of a Buffer surface, normalized — or a
5136
- * fence when it isn't a literal alias Node knows. */
5137
- export function bufEncoding(lowerer, what, encNode) {
5138
- const t = lowerer.typeOf(encNode);
5139
- const v = t.isStringLiteralType() ? own(BUF_ENCODINGS, t.value) : undefined;
5140
- if (v === undefined) {
5141
- lowerer.noLowering(`${what} with this encoding`, encNode, 'a literal "utf8", "hex", "base64", "base64url", "latin1", "binary", "ascii", "utf16le", or "ucs2" spelling is the lowered encoding surface');
5142
- }
5143
- return v;
5144
- }
5145
- /** The fixed-width Buffer numeric methods by source name → the readNum/
5146
- * writeNum kind token. Node declares BOTH capitalizations ("UInt" is the
5147
- * original, "Uint" the aliased spelling) — the tables carry both. */
5148
- const BUF_NUM_METHODS = (() => {
5149
- const out = {};
5150
- for (const rw of ["read", "write"]) {
5151
- for (const u of ["UInt", "Uint"]) {
5152
- out[`${rw}${u}8`] = "u8";
5153
- out[`${rw}${u}16BE`] = "u16be";
5154
- out[`${rw}${u}16LE`] = "u16le";
5155
- out[`${rw}${u}32BE`] = "u32be";
5156
- out[`${rw}${u}32LE`] = "u32le";
5157
- }
5158
- out[`${rw}Int8`] = "i8";
5159
- out[`${rw}Int16BE`] = "i16be";
5160
- out[`${rw}Int16LE`] = "i16le";
5161
- out[`${rw}Int32BE`] = "i32be";
5162
- out[`${rw}Int32LE`] = "i32le";
5163
- out[`${rw}FloatBE`] = "f32be";
5164
- out[`${rw}FloatLE`] = "f32le";
5165
- out[`${rw}DoubleBE`] = "f64be";
5166
- out[`${rw}DoubleLE`] = "f64le";
5167
- }
5168
- return out;
5169
- })();
5170
- /** The variable-width quartet (offset + byteLength) → sign/endian token. */
5171
- const BUF_NUM_VAR_METHODS = {
5172
- readUIntBE: "ube",
5173
- readUintBE: "ube",
5174
- readUIntLE: "ule",
5175
- readUintLE: "ule",
5176
- readIntBE: "ibe",
5177
- readIntLE: "ile",
5178
- writeUIntBE: "ube",
5179
- writeUintBE: "ube",
5180
- writeUIntLE: "ule",
5181
- writeUintLE: "ule",
5182
- writeIntBE: "ibe",
5183
- writeIntLE: "ile",
5184
- };
5185
- /** The Buffer-declared comparison/search/mutation methods on a u8
5186
- * receiver: equals, compare, indexOf/lastIndexOf/includes (number,
5187
- * string-with-encoding, and Buffer needles), fill, copy, swap16/32/64,
5188
- * and write. Null when `name` isn't one of them (the numeric families
5189
- * and the generic bytes surface try next). Encodings must be literals
5190
- * (bufEncoding's fence); a trailing string-typed argument is the
5191
- * encoding, exactly Node's overloads. */
5192
- function lowerBufferInstanceMethod(lowerer, call, access, name, loc) {
5193
- const args = call.arguments;
5194
- const nArgs = args.length;
5195
- if (args.some(ts.isSpreadElement))
5196
- return null;
5197
- const isStringArg = (i) => lowerer.mapTypeOf(lowerer.typeOf(args[i]))?.kind === "string";
5198
- const u8Arg = (i) => {
5199
- const v = lowerer.lowerExpr(args[i]);
5200
- if (!(v.type.kind === "bytes" && v.type.elem === "u8")) {
5201
- lowerer.noLowering(`.${name} of '${lowerer.fmt(v.type)}' values`, args[i], "a Buffer/Uint8Array argument is the lowered shape (narrow unions first)");
5202
- }
5203
- return v;
5204
- };
5205
- // The checked-dynamic crossing for a compare/equals argument slot: dyn
5206
- // passes through, convertible statics (the invalid-input probes'
5207
- // literals) wrap in dynFrom, `undefined` literals ride the same wrap.
5208
- // Fences (never returns) when the value cannot cross — an island jsval.
5209
- const chkArg = (i) => {
5210
- // Object literals take the dyn literal path directly (method members
5211
- // box as dyn functions — the typed record fence never applies).
5212
- if (ts.isObjectLiteralExpression(args[i])) {
5213
- return lowerDynObjectLiteral(lowerer, args[i]);
5214
- }
5215
- const v = lowerer.lowerExpr(args[i]);
5216
- if (v.type.kind === "dyn")
5217
- return v;
5218
- if (v.kind === "unitLit" || lowerer.dynConvertible(v.type)) {
5219
- return { kind: "dynFrom", value: v, type: DYN, loc: v.loc };
5220
- }
5221
- lowerer.noLowering(`.${name} of '${lowerer.fmt(v.type)}' values`, args[i], "a Buffer/Uint8Array argument is the lowered shape (narrow unions first)");
5222
- };
5223
- const argIrKind = (i) => lowerer.mapTypeOf(lowerer.typeOf(args[i]));
5224
- if (name === "equals") {
5225
- if (nArgs !== 1)
5226
- lowerer.noLowering(`.equals with ${nArgs} arguments`, call);
5227
- if (argIrKind(0)?.kind === "bytes") {
5228
- const other = u8Arg(0);
5229
- const receiver = lowerer.lowerExpr(access.expression);
5230
- return { kind: "bytesIntrinsic", method: "equals", receiver, args: [other], type: BOOL, loc };
5231
- }
5232
- // Not statically bytes (the invalid-input probes, untyped JS
5233
- // helpers): Node's "otherBuffer" argument ladder runs at runtime.
5234
- const receiver = lowerer.lowerExpr(access.expression);
5235
- return { kind: "libCall", fn: "bytes.equalsChk", args: [receiver, chkArg(0)], type: BOOL, loc };
5236
- }
5237
- if (name === "compare") {
5238
- if (nArgs > 5)
5239
- lowerer.noLowering(`.compare with ${nArgs} arguments`, call);
5240
- const fast = nArgs >= 1 &&
5241
- argIrKind(0)?.kind === "bytes" &&
5242
- call.arguments.slice(1).every((a) => lowerer.mapTypeOf(lowerer.typeOf(a))?.kind === "f64");
5243
- if (fast) {
5244
- const target = u8Arg(0);
5245
- const idx = call.arguments.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64));
5246
- const receiver = lowerer.lowerExpr(access.expression);
5247
- return { kind: "bytesIntrinsic", method: "compareBuf", receiver, args: [target, ...idx], type: F64, loc };
5248
- }
5249
- // An absent/ill-typed target or offset (`a.compare()`, string
5250
- // offsets, explicit undefined): Node's target/targetStart/targetEnd/
5251
- // sourceStart/sourceEnd ladder runs at runtime; absent slots pass
5252
- // the undefined dyn (Node defaults apply there).
5253
- const receiver = lowerer.lowerExpr(access.expression);
5254
- const slots = [];
5255
- for (let i = 0; i < 5; i++)
5256
- slots.push(i < nArgs ? chkArg(i) : dynUndefinedExpr(loc));
5257
- return { kind: "libCall", fn: "bytes.compareChk", args: [receiver, ...slots], type: F64, loc };
5258
- }
5259
- if (name === "indexOf" || name === "lastIndexOf" || name === "includes") {
5260
- if (nArgs < 1 || nArgs > 3)
5261
- lowerer.noLowering(`.${name} with ${nArgs} arguments`, call);
5262
- // The overloads: (v), (v, byteOffset), (v, encoding), (v, byteOffset,
5263
- // encoding) — a trailing string-typed arg is the encoding.
5264
- let encName = "utf8";
5265
- let offNode;
5266
- if (nArgs === 3) {
5267
- offNode = args[1];
5268
- encName = bufEncoding(lowerer, `.${name}`, args[2]);
5269
- }
5270
- else if (nArgs === 2) {
5271
- if (isStringArg(1))
5272
- encName = bufEncoding(lowerer, `.${name}`, args[1]);
5273
- else
5274
- offNode = args[1];
5275
- }
5276
- const resultT = name === "includes" ? BOOL : F64;
5277
- const vT = lowerer.mapTypeOf(lowerer.typeOf(args[0]));
5278
- const receiver = lowerer.lowerExpr(access.expression);
5279
- if (vT?.kind === "f64") {
5280
- // A number needle wraps & 0xFF at runtime (Buffer semantics; the
5281
- // encoding is irrelevant, like Node).
5282
- const v = lowerer.lowerExprExpecting(args[0], F64);
5283
- const numArgs = [v, ...(offNode ? [lowerer.lowerExprExpecting(offNode, F64)] : [])];
5284
- const method = name === "indexOf" ? "indexOfNum" : name === "lastIndexOf" ? "lastIndexOfNum" : "includesNum";
5285
- return { kind: "bytesIntrinsic", method, receiver, args: numArgs, type: resultT, loc };
5286
- }
5287
- let needle;
5288
- let align = 1;
5289
- if (vT?.kind === "string") {
5290
- const s = lowerer.lowerExprExpecting(args[0], STRING);
5291
- const enc = { kind: "strLit", value: encName, type: STRING, loc };
5292
- needle = { kind: "libCall", fn: "buffer.fromStr", args: [s, enc], type: BYTES_U8, loc };
5293
- if (encName === "utf16le")
5294
- align = 2;
5295
- }
5296
- else if (vT?.kind === "bytes" && vT.elem === "u8") {
5297
- needle = lowerer.lowerExpr(args[0]);
5298
- }
5299
- else {
5300
- lowerer.noLowering(`.${name} of '${lowerer.checker.typeToString(lowerer.typeOf(args[0]))}' values`, args[0], "string, number, and Buffer/Uint8Array needles search (narrow unions first)");
5301
- }
5302
- const alignLit = { kind: "numLit", value: align, type: F64, loc };
5303
- const searchArgs = [needle, alignLit, ...(offNode ? [lowerer.lowerExprExpecting(offNode, F64)] : [])];
5304
- return { kind: "bytesIntrinsic", method: name, receiver, args: searchArgs, type: resultT, loc };
5305
- }
5306
- if (name === "fill") {
5307
- if (nArgs < 1 || nArgs > 4)
5308
- lowerer.noLowering(`.fill with ${nArgs} arguments`, call);
5309
- // A trailing string-typed arg past the value is the encoding.
5310
- let encNode;
5311
- let idxNodes = call.arguments.slice(1);
5312
- if (idxNodes.length > 0 && isStringArg(nArgs - 1)) {
5313
- encNode = idxNodes[idxNodes.length - 1];
5314
- idxNodes = idxNodes.slice(0, -1);
5315
- }
5316
- if (idxNodes.length > 2)
5317
- lowerer.noLowering(`.fill with ${nArgs} arguments`, call);
5318
- const idx = idxNodes.map((a) => lowerer.lowerExprExpecting(a, F64));
5319
- const receiverT = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
5320
- if (receiverT?.kind !== "bytes")
5321
- lowerer.badType(access.expression, lowerer.typeOf(access.expression));
5322
- const vT = lowerer.mapTypeOf(lowerer.typeOf(args[0]));
5323
- const receiver = lowerer.lowerExpr(access.expression);
5324
- if (vT?.kind === "string") {
5325
- const encName = encNode ? bufEncoding(lowerer, ".fill", encNode) : "utf8";
5326
- const s = lowerer.lowerExprExpecting(args[0], STRING);
5327
- const enc = { kind: "strLit", value: encName, type: STRING, loc };
5328
- return { kind: "bytesIntrinsic", method: "fillStr", receiver, args: [s, enc, ...idx], type: BYTES_U8, loc };
5329
- }
5330
- if (encNode)
5331
- lowerer.noLowering(".fill with an encoding on a non-string value", encNode);
5332
- if (vT?.kind === "f64") {
5333
- const v = lowerer.lowerExprExpecting(args[0], F64);
5334
- return { kind: "bytesIntrinsic", method: "fillNum", receiver, args: [v, ...idx], type: BYTES_U8, loc };
5335
- }
5336
- const pattern = u8Arg(0);
5337
- return { kind: "bytesIntrinsic", method: "fill", receiver, args: [pattern, ...idx], type: BYTES_U8, loc };
5338
- }
5339
- if (name === "copy") {
5340
- if (nArgs < 1 || nArgs > 4)
5341
- lowerer.noLowering(`.copy with ${nArgs} arguments`, call);
5342
- const target = u8Arg(0);
5343
- const idx = call.arguments.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64));
5344
- const receiver = lowerer.lowerExpr(access.expression);
5345
- return { kind: "bytesIntrinsic", method: "copy", receiver, args: [target, ...idx], type: F64, loc };
5346
- }
5347
- if (name === "swap16" || name === "swap32" || name === "swap64") {
5348
- if (nArgs !== 0)
5349
- lowerer.noLowering(`.${name} with ${nArgs} arguments`, call);
5350
- const receiver = lowerer.lowerExpr(access.expression);
5351
- return { kind: "bytesIntrinsic", method: name, receiver, args: [], type: BYTES_U8, loc };
5352
- }
5353
- if (name === "write") {
5354
- if (nArgs < 1 || nArgs > 4)
5355
- lowerer.noLowering(`.write with ${nArgs} arguments`, call);
5356
- // (str), (str, enc), (str, offset), (str, offset, enc),
5357
- // (str, offset, length), (str, offset, length, enc).
5358
- let encNode;
5359
- let idxNodes = call.arguments.slice(1);
5360
- if (idxNodes.length > 0 && isStringArg(nArgs - 1)) {
5361
- encNode = idxNodes[idxNodes.length - 1];
5362
- idxNodes = idxNodes.slice(0, -1);
5363
- }
5364
- if (idxNodes.length > 2)
5365
- lowerer.noLowering(`.write with ${nArgs} arguments`, call);
5366
- const encName = encNode ? bufEncoding(lowerer, "Buffer.write", encNode) : "utf8";
5367
- const s = lowerer.lowerExprExpecting(args[0], STRING);
5368
- const enc = { kind: "strLit", value: encName, type: STRING, loc };
5369
- const offset = idxNodes[0]
5370
- ? lowerer.lowerExprExpecting(idxNodes[0], F64)
5371
- : { kind: "numLit", value: 0, type: F64, loc };
5372
- const writeArgs = [s, enc, offset, ...(idxNodes[1] ? [lowerer.lowerExprExpecting(idxNodes[1], F64)] : [])];
5373
- const receiver = lowerer.lowerExpr(access.expression);
5374
- return { kind: "bytesIntrinsic", method: "writeStr", receiver, args: writeArgs, type: F64, loc };
5375
- }
5376
- return null;
5377
- }
5378
- /** The DataView getter surface by source name. `le` marks the multi-byte
5379
- * kinds whose lib signature declares the optional littleEndian parameter
5380
- * (the 8-bit getters take none). The Big pair only compiles composed
5381
- * inside Number(...) — see the use site. */
5382
- const DV_GETTERS = {
5383
- getUint8: { method: "dvGetUint8", le: false },
5384
- getInt8: { method: "dvGetInt8", le: false },
5385
- getUint16: { method: "dvGetUint16", le: true },
5386
- getInt16: { method: "dvGetInt16", le: true },
5387
- getUint32: { method: "dvGetUint32", le: true },
5388
- getInt32: { method: "dvGetInt32", le: true },
5389
- getFloat32: { method: "dvGetFloat32", le: true },
5390
- getFloat64: { method: "dvGetFloat64", le: true },
5391
- getBigUint64: { method: "dvGetBigUint64Number", le: true },
5392
- getBigInt64: { method: "dvGetBigInt64Number", le: true },
5393
- };
5394
- const DV_SETTERS = {
5395
- setUint8: { method: "dvSetUint8", le: false },
5396
- setInt8: { method: "dvSetInt8", le: false },
5397
- setUint16: { method: "dvSetUint16", le: true },
5398
- setInt16: { method: "dvSetInt16", le: true },
5399
- setUint32: { method: "dvSetUint32", le: true },
5400
- setInt32: { method: "dvSetInt32", le: true },
5401
- setFloat32: { method: "dvSetFloat32", le: true },
5402
- setFloat64: { method: "dvSetFloat64", le: true },
5403
- };
5404
- /** The Buffer statics — `Buffer.from(...)`, `Buffer.alloc(n)`,
5405
- * `Buffer.concat(list)`, `Buffer.isBuffer(x)` — on THE stdlib Buffer
5406
- * global (name + provenance; fallback and @types/node alike). Null when
5407
- * the callee isn't a Buffer-static access. */
5408
- export function lowerBufferStaticCall(lowerer, call, access) {
5409
- if (call.questionDotToken || access.questionDotToken)
5410
- return null;
5411
- if (!lowerer.isStdlibGlobal(access.expression, "Buffer"))
5412
- return null;
5413
- const member = access.name.text;
5414
- const loc = locOf(call);
5415
- const args = call.arguments;
5416
- if (member === "from") {
5417
- // Buffer.from(x.buffer[, byteOffset[, length]]): a u8 VIEW sharing
5418
- // x's storage (Node shares the ArrayBuffer; length is in BYTES).
5419
- // The first argument must be the SYNTACTIC `.buffer` of a typed
5420
- // array — the DataView peel — and the construction rides the same
5421
- // dataViewNew intrinsic (offset/length validation throws Node's
5422
- // DataView-shaped RangeErrors; valid indices behave exactly).
5423
- if (args.length >= 1 && args.length <= 3 && !args.some(ts.isSpreadElement) &&
5424
- ts.isPropertyAccessExpression(args[0]) && args[0].name.text === "buffer") {
5425
- const srcNode = args[0].expression;
5426
- const srcIr = lowerer.mapTypeOf(lowerer.typeOf(srcNode));
5427
- if (srcIr?.kind === "bytes") {
5428
- const receiver = lowerer.lowerExpr(srcNode);
5429
- const idxArgs = args.slice(1).map((a) => lowerer.lowerExprExpecting(a, F64));
5430
- return { kind: "bytesIntrinsic", method: "dataViewNew", receiver, args: idxArgs, type: BYTES_U8, loc };
5431
- }
5432
- }
5433
- if (args.length >= 1 && args.length <= 2 && !ts.isSpreadElement(args[0])) {
5434
- const argNode = args[0];
5435
- if (ts.isArrayLiteralExpression(argNode) && !argNode.elements.some(ts.isSpreadElement)) {
5436
- // A number[] literal (contextually typed by the lib's readonly
5437
- // number[] slot — build it element-wise, the Set-seed pattern).
5438
- if (args.length === 1) {
5439
- const elems = argNode.elements.map((el) => lowerer.lowerExprExpecting(el, F64));
5440
- const seed = { kind: "arrayLit", elems, type: arrayOf(F64), loc };
5441
- return { kind: "bytesNew", source: seed, type: BYTES_U8, loc };
5442
- }
5443
- }
5444
- else {
5445
- const srcIr = lowerer.mapTypeOf(lowerer.typeOf(argNode));
5446
- if (srcIr?.kind === "string") {
5447
- const encNode = args[1];
5448
- const encName = encNode ? bufEncoding(lowerer, "Buffer.from", encNode) : "utf8";
5449
- const s = lowerer.lowerExprExpecting(argNode, STRING);
5450
- const enc = { kind: "strLit", value: encName, type: STRING, loc };
5451
- return { kind: "libCall", fn: "buffer.fromStr", args: [s, enc], type: BYTES_U8, loc };
5452
- }
5453
- if (args.length === 1 && srcIr?.kind === "bytes" && srcIr.elem === "u8") {
5454
- return { kind: "bytesNew", source: lowerer.lowerExpr(argNode), type: BYTES_U8, loc };
5455
- }
5456
- if (args.length === 1 && srcIr?.kind === "array" && srcIr.elem.kind === "f64") {
5457
- return { kind: "bytesNew", source: lowerer.lowerExpr(argNode), type: BYTES_U8, loc };
5458
- }
5459
- }
5460
- }
5461
- lowerer.noLowering("Buffer.from with this argument shape", call, "supported: Buffer.from(string, encoding?) with a literal encoding, Buffer.from(u8Array) — a copy — " +
5462
- "Buffer.from(number[]), or Buffer.from(x.buffer, byteOffset?, length?) — a view sharing x's " +
5463
- "storage (no free-standing ArrayBuffer value exists; narrow unions first)");
5464
- }
5465
- if (member === "alloc" || member === "allocUnsafe") {
5466
- const maxArgs = member === "alloc" ? 3 : 1;
5467
- if (args.length < 1 || args.length > maxArgs || args.some(ts.isSpreadElement)) {
5468
- lowerer.noLowering(`Buffer.${member} with ${args.length} arguments`, call);
5469
- }
5470
- // allocUnsafe's contents are UNSPECIFIED in Node (uninitialized pool
5471
- // memory); a zero-filled buffer is a valid instance of unspecified,
5472
- // and the deterministic choice — a program observing the difference
5473
- // is depending on garbage.
5474
- const size = lowerer.lowerExprExpecting(args[0], F64);
5475
- const fresh = { kind: "bytesNew", source: size, type: BYTES_U8, loc };
5476
- if (args.length === 1)
5477
- return fresh;
5478
- // alloc(size, fill, encoding?): the fill semantics ARE fill()'s —
5479
- // the fresh buffer is the receiver of a whole-range fill.
5480
- const fillT = lowerer.mapTypeOf(lowerer.typeOf(args[1]));
5481
- if (fillT?.kind === "string") {
5482
- const encName = args[2] ? bufEncoding(lowerer, "Buffer.alloc", args[2]) : "utf8";
5483
- const s = lowerer.lowerExprExpecting(args[1], STRING);
5484
- const enc = { kind: "strLit", value: encName, type: STRING, loc };
5485
- return { kind: "bytesIntrinsic", method: "fillStr", receiver: fresh, args: [s, enc], type: BYTES_U8, loc };
5486
- }
5487
- if (args.length > 2) {
5488
- lowerer.noLowering("Buffer.alloc with an encoding on a non-string fill", args[2]);
5489
- }
5490
- if (fillT?.kind === "f64") {
5491
- const v = lowerer.lowerExprExpecting(args[1], F64);
5492
- return { kind: "bytesIntrinsic", method: "fillNum", receiver: fresh, args: [v], type: BYTES_U8, loc };
5493
- }
5494
- if (fillT?.kind === "bytes" && fillT.elem === "u8") {
5495
- const pattern = lowerer.lowerExpr(args[1]);
5496
- return { kind: "bytesIntrinsic", method: "fill", receiver: fresh, args: [pattern], type: BYTES_U8, loc };
5497
- }
5498
- lowerer.noLowering(`Buffer.alloc with a '${lowerer.checker.typeToString(lowerer.typeOf(args[1]))}' fill`, args[1], "number, string, and Buffer/Uint8Array fills are the lowered shapes");
5499
- }
5500
- if (member === "concat") {
5501
- if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) {
5502
- lowerer.noLowering(`Buffer.concat with ${args.length} arguments`, call);
5503
- }
5504
- const argNode = args[0];
5505
- let list;
5506
- if (ts.isArrayLiteralExpression(argNode) && !argNode.elements.some(ts.isSpreadElement)) {
5507
- const elems = argNode.elements.map((el) => lowerer.lowerExprExpecting(el, BYTES_U8));
5508
- list = { kind: "arrayLit", elems, type: arrayOf(BYTES_U8), loc };
5509
- }
5510
- else {
5511
- list = lowerer.lowerExpr(argNode);
5512
- if (!typeEquals(list.type, arrayOf(BYTES_U8))) {
5513
- lowerer.noLowering(`Buffer.concat of '${lowerer.fmt(list.type)}' values`, argNode, "one Uint8Array[]/Buffer[] value (or literal) is the lowered list shape");
5514
- }
5515
- }
5516
- if (args.length === 2) {
5517
- // The totalLength form truncates or zero-pads (and THROWS Node's
5518
- // 'length' RangeError on bad totals).
5519
- const total = lowerer.lowerExprExpecting(args[1], F64);
5520
- return { kind: "libCall", fn: "buffer.concatLen", args: [list, total], type: BYTES_U8, loc };
5521
- }
5522
- return { kind: "libCall", fn: "buffer.concat", args: [list], type: BYTES_U8, loc };
5523
- }
5524
- if (member === "compare") {
5525
- // The static form: Buffer.compare(a, b) IS a.compare(b).
5526
- if (args.length !== 2 || args.some(ts.isSpreadElement)) {
5527
- lowerer.noLowering(`Buffer.compare with ${args.length} arguments`, call);
5528
- }
5529
- const sides = args.map((a) => lowerer.lowerExpr(a));
5530
- if (sides.every((v) => v.type.kind === "bytes" && v.type.elem === "u8")) {
5531
- return { kind: "bytesIntrinsic", method: "compareBuf", receiver: sides[0], args: [sides[1]], type: F64, loc };
5532
- }
5533
- // A side that is not statically bytes (the invalid-input probes,
5534
- // untyped JS helpers): Node's "buf1"/"buf2" argument ladder runs
5535
- // at runtime — a well-typed dyn still compares.
5536
- const dyns = sides.map((v, i) => {
5537
- if (v.type.kind === "dyn")
5538
- return v;
5539
- if (v.kind === "unitLit" || lowerer.dynConvertible(v.type)) {
5540
- return { kind: "dynFrom", value: v, type: DYN, loc: v.loc };
5541
- }
5542
- lowerer.noLowering(`Buffer.compare of '${lowerer.fmt(v.type)}' values`, args[i], "Buffer/Uint8Array values compare (narrow unions first)");
5543
- });
5544
- return { kind: "libCall", fn: "buffer.compareChk", args: dyns, type: F64, loc };
5545
- }
5546
- if (member === "isBuffer") {
5547
- // The type-predicate narrowing test. Lowered where it DECIDES
5548
- // something: a union-typed argument with a Buffer arm becomes a
5549
- // runtime tag test (tsc's narrowing then types the branches, the
5550
- // discriminated-union machinery). Statically-decided arguments are
5551
- // fenced — the answer is a constant, and the operand's evaluation
5552
- // would have to be discarded.
5553
- if (args.length === 1 && !ts.isSpreadElement(args[0])) {
5554
- const v = lowerer.lowerExpr(args[0]);
5555
- if (v.type.kind === "union") {
5556
- const def = lowerer.unions.get(v.type.unionId);
5557
- const tag = def ? def.arms.findIndex((a) => a.kind === "bytes" && a.elem === "u8") : -1;
5558
- if (tag >= 0) {
5559
- return { kind: "unionIsTag", unionId: v.type.unionId, tag, negated: false, value: v, type: BOOL, loc };
5560
- }
5561
- }
5562
- lowerer.noLowering(`Buffer.isBuffer of '${lowerer.fmt(v.type)}' values`, args[0], "the check lowers as a runtime tag test on unions with a Buffer arm — here the answer is static");
5563
- }
5564
- lowerer.noLowering("Buffer.isBuffer with this argument shape", call);
5565
- }
5566
- if (member === "byteLength") {
5567
- // Buffer.byteLength(string, enc?) — the UTF-16-aware per-encoding
5568
- // count — or of a typed array/Buffer (its byte length, a property
5569
- // read at heart).
5570
- if (args.length >= 1 && args.length <= 2 && !ts.isSpreadElement(args[0])) {
5571
- const srcIr = lowerer.mapTypeOf(lowerer.typeOf(args[0]));
5572
- if (srcIr?.kind === "string") {
5573
- const encName = args[1] ? bufEncoding(lowerer, "Buffer.byteLength", args[1]) : "utf8";
5574
- const s = lowerer.lowerExprExpecting(args[0], STRING);
5575
- const enc = { kind: "strLit", value: encName, type: STRING, loc };
5576
- return { kind: "libCall", fn: "buffer.byteLenStr", args: [s, enc], type: F64, loc };
5577
- }
5578
- if (srcIr?.kind === "bytes" && args.length === 1) {
5579
- const receiver = lowerer.lowerExpr(args[0]);
5580
- return { kind: "bytesIntrinsic", method: "byteLength", receiver, args: [], type: F64, loc };
5581
- }
5582
- }
5583
- lowerer.noLowering("Buffer.byteLength with this argument shape", call, "supported: Buffer.byteLength(string, literalEncoding?) or Buffer.byteLength(typedArray)");
5584
- }
5585
- if (member === "isEncoding") {
5586
- // The runtime alias-set test — a plain bool over any string value
5587
- // (Node's case-insensitive normalizeEncoding check).
5588
- if (args.length === 1 && !ts.isSpreadElement(args[0]) && lowerer.mapTypeOf(lowerer.typeOf(args[0]))?.kind === "string") {
5589
- const s = lowerer.lowerExprExpecting(args[0], STRING);
5590
- return { kind: "libCall", fn: "buffer.isEncoding", args: [s], type: BOOL, loc };
5591
- }
5592
- lowerer.noLowering("Buffer.isEncoding with this argument shape", call, "one string-typed argument is the lowered form");
5593
- }
5594
- return null; // of, copyBytesFrom, ... → the SC2020 member fence
5595
- }
5596
4337
  /** `Object.keys/values/entries` over an INDEX-SIGNATURE (overflow-carrying)
5597
4338
  * record shape: declared fields answer first from the compile-time field
5598
4339
  * list (declaration order, undefined-valued fields skipped — exactly the