@scriptc/compiler 0.1.4 → 0.1.5

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 (150) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +75 -11
  2. package/dist/backend/c/c-emitter.d.ts +5 -1
  3. package/dist/backend/c/c-emitter.js +22 -9
  4. package/dist/backend/c/c-emitter.js.map +1 -1
  5. package/dist/backend/c/exprs.d.ts +2 -2
  6. package/dist/backend/c/exprs.js +264 -21
  7. package/dist/backend/c/exprs.js.map +1 -1
  8. package/dist/backend/c/stmts.js +5 -3
  9. package/dist/backend/c/stmts.js.map +1 -1
  10. package/dist/backend/c/types.d.ts +2 -3
  11. package/dist/backend/c/types.js +8 -1
  12. package/dist/backend/c/types.js.map +1 -1
  13. package/dist/backend/c/walkers.js +1 -1
  14. package/dist/backend/c/walkers.js.map +1 -1
  15. package/dist/backend/link-plan.d.ts +1 -0
  16. package/dist/backend/link-plan.js +2 -1
  17. package/dist/backend/link-plan.js.map +1 -1
  18. package/dist/backend/llvm/dyn.js +1 -1
  19. package/dist/backend/llvm/dyn.js.map +1 -1
  20. package/dist/backend/llvm/emitter.js +25 -9
  21. package/dist/backend/llvm/emitter.js.map +1 -1
  22. package/dist/backend/llvm/expr-bytes.d.ts +1 -2
  23. package/dist/backend/llvm/expr-bytes.js +22 -17
  24. package/dist/backend/llvm/expr-bytes.js.map +1 -1
  25. package/dist/backend/llvm/expr-containers.d.ts +1 -0
  26. package/dist/backend/llvm/expr-containers.js +32 -4
  27. package/dist/backend/llvm/expr-containers.js.map +1 -1
  28. package/dist/backend/llvm/expr-context.d.ts +6 -2
  29. package/dist/backend/llvm/expr-primitives.js +22 -7
  30. package/dist/backend/llvm/expr-primitives.js.map +1 -1
  31. package/dist/backend/llvm/lib-filesystem.js +12 -0
  32. package/dist/backend/llvm/lib-filesystem.js.map +1 -1
  33. package/dist/backend/llvm/lib-network.js +53 -10
  34. package/dist/backend/llvm/lib-network.js.map +1 -1
  35. package/dist/backend/llvm/lib-shared.js +64 -0
  36. package/dist/backend/llvm/lib-shared.js.map +1 -1
  37. package/dist/backend/native-toolchain.d.ts +2 -0
  38. package/dist/backend/native-toolchain.js +2 -1
  39. package/dist/backend/native-toolchain.js.map +1 -1
  40. package/dist/backend/targets.d.ts +3 -0
  41. package/dist/backend/targets.js +13 -0
  42. package/dist/backend/targets.js.map +1 -1
  43. package/dist/coverage/surface-manifest.js +6 -4
  44. package/dist/coverage/surface-manifest.js.map +1 -1
  45. package/dist/executable/executable-cache.d.ts +2 -0
  46. package/dist/executable/executable-cache.js +3 -1
  47. package/dist/executable/executable-cache.js.map +1 -1
  48. package/dist/executable/executable-cache.test.js +3 -3
  49. package/dist/executable/executable-cache.test.js.map +1 -1
  50. package/dist/frontend/lowering/containers/bytes.d.ts +1 -1
  51. package/dist/frontend/lowering/containers/bytes.js +6 -5
  52. package/dist/frontend/lowering/containers/bytes.js.map +1 -1
  53. package/dist/frontend/lowering/containers/string-and-regexp.d.ts +7 -6
  54. package/dist/frontend/lowering/containers/string-and-regexp.js +237 -28
  55. package/dist/frontend/lowering/containers/string-and-regexp.js.map +1 -1
  56. package/dist/frontend/lowering/expressions/object-literals.js +8 -0
  57. package/dist/frontend/lowering/expressions/object-literals.js.map +1 -1
  58. package/dist/frontend/lowering/lower-builtins.d.ts +4 -5
  59. package/dist/frontend/lowering/lower-builtins.js +18 -10
  60. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  61. package/dist/frontend/lowering/lower-calls.d.ts +3 -2
  62. package/dist/frontend/lowering/lower-calls.js +265 -88
  63. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  64. package/dist/frontend/lowering/lower-classes.js +2 -2
  65. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  66. package/dist/frontend/lowering/lower-containers.d.ts +7 -9
  67. package/dist/frontend/lowering/lower-containers.js +143 -51
  68. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  69. package/dist/frontend/lowering/lower-exprs.d.ts +12 -0
  70. package/dist/frontend/lowering/lower-exprs.js +128 -10
  71. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  72. package/dist/frontend/lowering/lower-island.js +17 -6
  73. package/dist/frontend/lowering/lower-island.js.map +1 -1
  74. package/dist/frontend/lowering/lower-server.d.ts +2 -4
  75. package/dist/frontend/lowering/lower-server.js +288 -32
  76. package/dist/frontend/lowering/lower-server.js.map +1 -1
  77. package/dist/frontend/lowering/lower-stmts.js +4 -4
  78. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  79. package/dist/frontend/lowering/lowerer.js +37 -2
  80. package/dist/frontend/lowering/lowerer.js.map +1 -1
  81. package/dist/frontend/lowering/optional-arguments.d.ts +9 -1
  82. package/dist/frontend/lowering/optional-arguments.js +67 -1
  83. package/dist/frontend/lowering/optional-arguments.js.map +1 -1
  84. package/dist/frontend/lowering/pairs-snapshot.d.ts +3 -1
  85. package/dist/frontend/lowering/pairs-snapshot.js +12 -4
  86. package/dist/frontend/lowering/pairs-snapshot.js.map +1 -1
  87. package/dist/frontend/lowering/surfaces.d.ts +8 -14
  88. package/dist/frontend/lowering/surfaces.js +37 -27
  89. package/dist/frontend/lowering/surfaces.js.map +1 -1
  90. package/dist/frontend/npm-static-prune.d.ts +6 -0
  91. package/dist/frontend/npm-static-prune.js +203 -0
  92. package/dist/frontend/npm-static-prune.js.map +1 -0
  93. package/dist/frontend/npm-static-prune.test.d.ts +1 -0
  94. package/dist/frontend/npm-static-prune.test.js +48 -0
  95. package/dist/frontend/npm-static-prune.test.js.map +1 -0
  96. package/dist/frontend/npm.js +7 -6
  97. package/dist/frontend/npm.js.map +1 -1
  98. package/dist/frontend/program.js +15 -4
  99. package/dist/frontend/program.js.map +1 -1
  100. package/dist/frontend/provenance.js +8 -3
  101. package/dist/frontend/provenance.js.map +1 -1
  102. package/dist/frontend/type-mapper.js +16 -5
  103. package/dist/frontend/type-mapper.js.map +1 -1
  104. package/dist/index.d.ts +3 -3
  105. package/dist/index.js +13 -41
  106. package/dist/index.js.map +1 -1
  107. package/dist/ir/analysis.d.ts +5 -3
  108. package/dist/ir/analysis.js +25 -11
  109. package/dist/ir/analysis.js.map +1 -1
  110. package/dist/ir/analysis.test.js +50 -2
  111. package/dist/ir/analysis.test.js.map +1 -1
  112. package/dist/ir/constant-tables.d.ts +17 -0
  113. package/dist/ir/constant-tables.js +99 -0
  114. package/dist/ir/constant-tables.js.map +1 -0
  115. package/dist/ir/constant-tables.test.d.ts +1 -0
  116. package/dist/ir/constant-tables.test.js +78 -0
  117. package/dist/ir/constant-tables.test.js.map +1 -0
  118. package/dist/ir/integer-ranges.d.ts +13 -0
  119. package/dist/ir/integer-ranges.js +122 -0
  120. package/dist/ir/integer-ranges.js.map +1 -0
  121. package/dist/ir/integer-ranges.test.d.ts +1 -0
  122. package/dist/ir/integer-ranges.test.js +63 -0
  123. package/dist/ir/integer-ranges.test.js.map +1 -0
  124. package/dist/ir/ir.d.ts +42 -34
  125. package/dist/ir/ir.js +7 -1
  126. package/dist/ir/ir.js.map +1 -1
  127. package/dist/ir/ir.test.js +6 -0
  128. package/dist/ir/ir.test.js.map +1 -1
  129. package/dist/ir/scalar-records.d.ts +8 -0
  130. package/dist/ir/scalar-records.js +216 -0
  131. package/dist/ir/scalar-records.js.map +1 -0
  132. package/dist/ir/scalar-records.test.d.ts +1 -0
  133. package/dist/ir/scalar-records.test.js +142 -0
  134. package/dist/ir/scalar-records.test.js.map +1 -0
  135. package/dist/ir/validate.js +92 -8
  136. package/dist/ir/validate.js.map +1 -1
  137. package/dist/ir/validate.test.d.ts +1 -0
  138. package/dist/ir/validate.test.js +31 -0
  139. package/dist/ir/validate.test.js.map +1 -0
  140. package/dist/library/cache-primitives.d.ts +1 -1
  141. package/dist/library/cache-primitives.js +3 -3
  142. package/dist/library/cache-primitives.js.map +1 -1
  143. package/dist/library/fence-eval.js +11 -4
  144. package/dist/library/fence-eval.js.map +1 -1
  145. package/dist/library/library-cache.js +0 -1
  146. package/dist/library/library-cache.js.map +1 -1
  147. package/dist/library/library-cache.test.js +3 -3
  148. package/dist/library/library-cache.test.js.map +1 -1
  149. package/package.json +17 -17
  150. package/surface-manifest.json +55 -42
@@ -19,20 +19,18 @@ export declare function lowerArrayMethodCall(lowerer: Lowerer, call: ts.CallExpr
19
19
  * parameter type). Null when this isn't a tuple slice/map — writers and
20
20
  * the rest of the surface keep their fences. */
21
21
  export declare function lowerTupleReadMethodCall(lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression): IrExpr | null;
22
- /** An OOB-SAFE indexed read: `xs[i]`
23
- * answers the interned `elem | undefined` union — the element when `i`
24
- * is an integer in [0, len), JS's property-miss undefined otherwise —
25
- * instead of the trap divergence 4 documents for program code.
26
- * Package JS is inference-typed, guard-style code (`registeredArguments
27
- * .slice(-1)[0]`, commander's last-element probe), and the trap would
28
- * fire on working Node idioms; program files keep the documented trap
29
- * (their annotations can prove bounds). Existing element unions retag
30
- * into the canonical union with an added undefined arm. */
22
+ /** An ordinary indexed read: values retain their element type, while
23
+ * holes, missing properties, and present undefined yield undefined. */
31
24
  export declare function lowerSafeIndexRead(lowerer: Lowerer, arr: IrExpr & {
32
25
  type: {
33
26
  kind: "array";
34
27
  };
35
28
  }, index: IrExpr, loc: SrcLoc): IrExpr | null;
29
+ /** Fuse only our own f64-array read helper with immediate ToNumber.
30
+ * Reuse its arguments verbatim so the receiver and index still evaluate
31
+ * exactly once, in order, before the read. Do not specialize user calls,
32
+ * union-element arrays, or optional values stored in locals. */
33
+ export declare function tryLowerNumericIndexRead(lowerer: Lowerer, operand: IrExpr, loc: SrcLoc): IrExpr | null;
36
34
  /** Backward-compatible name for the npm-static probe path. */
37
35
  export declare const lowerNpmStaticSafeIndexRead: typeof lowerSafeIndexRead;
38
36
  /** `Array.from({ length: n }, mapfn)` — the counted-generation idiom — on
@@ -15,7 +15,7 @@ import { buildArraySortFn } from "./lower-array-sort.js";
15
15
  import { arrayIndexPresent, arrayValueRead, arrayValueStore, arrayValueType, currentArrayIndexPresent } from "./array-values.js";
16
16
  import { typeKey } from "../type-mapper.js";
17
17
  import { boolLit, countedFor, numLit, varRef } from "../../ir/build.js";
18
- import { lowerOptionalArgument } from "./optional-arguments.js";
18
+ import { lowerOptionalArgument, lowerPositionArgument, positionNumber } from "./optional-arguments.js";
19
19
  /** Build the argument array for a mutating/copying operation while keeping
20
20
  * the receiver's scalar payload ABI. A missing read is represented as the
21
21
  * present UNDEFINED state in this temporary, so push/unshift and toSpliced
@@ -287,14 +287,14 @@ export function lowerArrayMethodCall(lowerer, call, access) {
287
287
  };
288
288
  }
289
289
  // The lib declares wider call forms than the lowered surface —
290
- // indexOf/includes take a fromIndex, join's separator is optional, the
291
- // predicate/mapping HOFs take a thisArg. Each unlowered form is fenced
290
+ // join's separator is optional, and the predicate/mapping HOFs take
291
+ // a thisArg. Each unlowered form is fenced
292
292
  // per site (SC2020), never silently truncated to the supported
293
293
  // arguments. push/unshift lower every declared form (variadic, 0 args
294
294
  // included — Node returns the unchanged length); reduce/reduceRight
295
295
  // lower both declared forms (with and without an initial value).
296
296
  const arity = {
297
- push: [0, Number.MAX_SAFE_INTEGER], unshift: [0, Number.MAX_SAFE_INTEGER], pop: [0, 0], indexOf: [1, 1], includes: [1, 1], join: [1, 1],
297
+ push: [0, Number.MAX_SAFE_INTEGER], unshift: [0, Number.MAX_SAFE_INTEGER], pop: [0, 0], indexOf: [1, 2], lastIndexOf: [1, 2], includes: [0, 2], join: [1, 1],
298
298
  concat: [0, Number.MAX_SAFE_INTEGER],
299
299
  slice: [0, 2], shift: [0, 0], splice: [1, 2], at: [1, 1],
300
300
  map: [1, 1], filter: [1, 1], forEach: [1, 1], find: [1, 1], findIndex: [1, 1], some: [1, 1],
@@ -306,12 +306,10 @@ export function lowerArrayMethodCall(lowerer, call, access) {
306
306
  ? "the removal forms lower — splice(start, deleteCount?); to insert, build a new array with slice and push"
307
307
  : name === "join"
308
308
  ? 'pass the separator explicitly: join(",")'
309
- : name === "indexOf" || name === "includes"
310
- ? "the fromIndex parameter has no lowering — slice first, or loop"
311
- : name === "map" || name === "filter" || name === "forEach" ||
312
- name === "find" || name === "some" || name === "every" || name === "flatMap"
313
- ? "the thisArg parameter has no lowering — use an arrow function"
314
- : undefined;
309
+ : name === "map" || name === "filter" || name === "forEach" ||
310
+ name === "find" || name === "some" || name === "every" || name === "flatMap"
311
+ ? "the thisArg parameter has no lowering — use an arrow function"
312
+ : undefined;
315
313
  lowerer.noLowering(`.${name} with ${call.arguments.length} argument${call.arguments.length === 1 ? "" : "s"}`, call, hint);
316
314
  }
317
315
  if (name === "push" || name === "unshift") {
@@ -393,7 +391,7 @@ export function lowerArrayMethodCall(lowerer, call, access) {
393
391
  loc,
394
392
  };
395
393
  }
396
- if (name === "indexOf" || name === "includes") {
394
+ if (name === "indexOf" || name === "lastIndexOf" || name === "includes") {
397
395
  return lowerArraySearchCall(lowerer, call, access, name, elem);
398
396
  }
399
397
  if (name === "concat") {
@@ -531,14 +529,23 @@ export function lowerArrayMethodCall(lowerer, call, access) {
531
529
  /** `indexOf` and `includes` normally use the runtime's typed search helpers,
532
530
  * but an indexed array read is a `T | undefined` value. Keep that union
533
531
  * through a small lowered loop so the needle's missing state is observable:
534
- * indexOf skips holes, while includes treats holes as undefined. The same
535
- * loop also gives union-element arrays their value equality rather than
536
- * comparing the compiler's union boxes by pointer. */
532
+ * indexOf and lastIndexOf skip holes, while includes treats holes as
533
+ * undefined. The same loop also gives union-element arrays their value
534
+ * equality rather than comparing the compiler's union boxes by pointer. */
537
535
  function lowerArraySearchCall(lowerer, call, access, method, elem) {
538
536
  const loc = locOf(call);
539
537
  const receiver = lowerer.lowerExpr(access.expression);
540
- const argNode = call.arguments[0];
541
- let needle = lowerer.lowerExpr(argNode);
538
+ const argNode = call.arguments[0] ?? call;
539
+ if (call.arguments.some(ts.isSpreadElement))
540
+ lowerer.noLowering(`.${method} with spread arguments`, call);
541
+ let needle = call.arguments[0]
542
+ ? lowerer.lowerExpr(call.arguments[0])
543
+ : { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc };
544
+ const fromIndex = call.arguments[1]
545
+ ? lowerPositionArgument(lowerer, call.arguments[1], numLit(0, loc))
546
+ : method === "lastIndexOf"
547
+ ? { kind: "bin", op: "/", left: numLit(1, loc), right: numLit(0, loc), type: F64, loc }
548
+ : numLit(0, loc);
542
549
  // A derived CLASS VALUE against a base-classval element widens (the same
543
550
  // pointer — identity search is exact); the coercion path owns the ABI gate.
544
551
  if (needle.type.kind === "classval" && elem.kind === "classval" && !typeEquals(needle.type, elem)) {
@@ -546,21 +553,25 @@ function lowerArraySearchCall(lowerer, call, access, method, elem) {
546
553
  }
547
554
  // Preserve the existing fast path for an ordinary, non-union needle. Its
548
555
  // runtime implementation already has exact primitive/reference equality.
549
- if (elem.kind !== "union" && elem.kind !== "bigint" && typeEquals(needle.type, elem)) {
556
+ if (method !== "lastIndexOf" && call.arguments.length === 1 && elem.kind !== "union" && elem.kind !== "bigint" && typeEquals(needle.type, elem)) {
550
557
  return {
551
558
  kind: "arrIntrinsic",
552
559
  method,
553
560
  receiver,
554
561
  args: [needle],
555
- type: method === "indexOf" ? F64 : BOOL,
562
+ type: method === "includes" ? BOOL : F64,
556
563
  loc,
557
564
  };
558
565
  }
559
- const valueT = arrayValueType(lowerer, elem);
566
+ let valueT = arrayValueType(lowerer, elem);
560
567
  // jsval/dyn arrays have a single engine value representation rather than a
561
568
  // static undefined arm. Keep their existing runtime path and its explicit
562
569
  // type fence; typed static arrays take the state-aware helper below.
563
570
  if (valueT.kind !== "union") {
571
+ if (method === "lastIndexOf")
572
+ lowerer.noLowering(".lastIndexOf on an array of engine values", call);
573
+ if (call.arguments.length > 1)
574
+ lowerer.noLowering(`.${method} with fromIndex on '${lowerer.fmt(elem)}' elements`, call);
564
575
  if (!typeEquals(needle.type, elem))
565
576
  lowerer.badType(argNode, lowerer.typeOf(argNode));
566
577
  return {
@@ -568,15 +579,38 @@ function lowerArraySearchCall(lowerer, call, access, method, elem) {
568
579
  method,
569
580
  receiver,
570
581
  args: [needle],
571
- type: method === "indexOf" ? F64 : BOOL,
582
+ type: method === "includes" ? BOOL : F64,
572
583
  loc,
573
584
  };
574
585
  }
575
- const searchNeedle = lowerer.coerceInto(argNode, needle, valueT);
576
- if (!typeEquals(searchNeedle.type, valueT))
586
+ // Search uses strict equality, never conversion to the array's element
587
+ // type. Retain primitive needles of a different kind in the comparison
588
+ // union so a nonmatching kind returns a miss instead of throwing.
589
+ const needleArms = needle.type.kind === "union"
590
+ ? lowerer.unions.get(needle.type.unionId).arms
591
+ : [needle.type];
592
+ const scalarNeedle = needleArms.every((arm) => arm.kind === "f64" || arm.kind === "string" || arm.kind === "bool" ||
593
+ arm.kind === "bigint" || arm.kind === "symbol" || isUnitType(arm));
594
+ const valueArms = lowerer.unions.get(valueT.unionId).arms;
595
+ if (scalarNeedle && !valueArms.some((arm) => arm.kind === "func" || arm.kind === "set")) {
596
+ const arms = [...valueArms];
597
+ for (const arm of needleArms)
598
+ if (!arms.some((existing) => typeEquals(existing, arm)))
599
+ arms.push(arm);
600
+ if (arms.length !== valueArms.length)
601
+ valueT = { kind: "union", unionId: lowerer.unions.intern(arms) };
602
+ }
603
+ const directNeedle = method === "lastIndexOf" && elem.kind !== "union" && typeEquals(needle.type, elem);
604
+ const searchNeedle = directNeedle ? needle : lowerer.coerceInto(argNode, needle, valueT);
605
+ // Checked dynamic extraction can copy a composite. Its new identity cannot
606
+ // be compared to an array element as though it were the original value.
607
+ if (method === "lastIndexOf" && searchNeedle.kind === "dynCheck" && (elem.kind === "record" || elem.kind === "array")) {
608
+ lowerer.noLowering(".lastIndexOf with a checked dynamic reference needle", call);
609
+ }
610
+ if (!directNeedle && !typeEquals(searchNeedle.type, valueT))
577
611
  lowerer.badType(argNode, lowerer.typeOf(argNode));
578
- const helper = arraySearchHelper(lowerer, method, elem, valueT, loc);
579
- return { kind: "call", callee: helper, args: [receiver, searchNeedle], type: method === "indexOf" ? F64 : BOOL, loc };
612
+ const helper = arraySearchHelper(lowerer, method, elem, valueT, searchNeedle.type, directNeedle, fromIndex.type, call.arguments[1] ?? call, loc);
613
+ return { kind: "call", callee: helper, args: [receiver, searchNeedle, fromIndex], type: method === "includes" ? BOOL : F64, loc };
580
614
  }
581
615
  /** SameValueZero over a static value union. `unionEq` supplies strict
582
616
  * equality for every arm; the extra f64-arm test is the only difference for
@@ -620,25 +654,65 @@ function arraySearchEquality(lowerer, left, right, sameValueZero, loc) {
620
654
  };
621
655
  return { kind: "logical", op: "||", left: strict, right: bothNaN, type: BOOL, loc };
622
656
  }
623
- function arraySearchHelper(lowerer, method, elem, valueT, loc) {
624
- const key = `${method}:${typeKey(elem)}:${typeKey(valueT)}`;
657
+ function arraySearchHelper(lowerer, method, elem, valueT, needleT, directNeedle, fromT, fromNode, loc) {
658
+ const key = `${method}:${typeKey(elem)}:${typeKey(valueT)}:${typeKey(needleT)}:${directNeedle}:${typeKey(fromT)}`;
625
659
  const existing = lowerer.arrHofHelpers.get(key);
626
660
  if (existing)
627
661
  return existing;
628
662
  const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`;
629
663
  lowerer.arrHofHelpers.set(key, name);
630
664
  const arrT = arrayOf(elem);
631
- const resultT = method === "indexOf" ? F64 : BOOL;
665
+ const resultT = method === "includes" ? BOOL : F64;
632
666
  const arrRef = varRef("a.0", arrT, loc);
633
667
  const indexRef = varRef("i.0", F64, loc);
634
- const needleRef = varRef("needle.0", valueT, loc);
668
+ const needleRef = varRef("needle.0", needleT, loc);
635
669
  const stateRef = varRef("state.0", F64, loc);
636
670
  const nextRef = varRef("next.0", F64, loc);
671
+ const fromRef = varRef("from.0", fromT, loc);
672
+ const numberRef = varRef("number.0", F64, loc);
673
+ const integerRef = varRef("integer.0", F64, loc);
674
+ // Snapshot length before coercing fromIndex, then apply ToIntegerOrInfinity.
675
+ const number = positionNumber(lowerer, fromRef, numLit(0, loc), fromNode, "array search position");
676
+ const integer = {
677
+ kind: "ternary",
678
+ cond: { kind: "libCall", fn: "num.isNaN", args: [numberRef], type: BOOL, loc },
679
+ then: numLit(0, loc),
680
+ else_: { kind: "libCall", fn: "math.trunc", args: [numberRef], type: F64, loc },
681
+ type: F64,
682
+ loc,
683
+ };
684
+ const forwardStart = {
685
+ kind: "ternary",
686
+ cond: { kind: "bin", op: "<", left: integerRef, right: numLit(0, loc), type: BOOL, loc },
687
+ then: {
688
+ kind: "libCall", fn: "math.max",
689
+ args: [{ kind: "bin", op: "+", left: varRef("n.0", F64, loc), right: integerRef, type: F64, loc }, numLit(0, loc)],
690
+ type: F64, loc,
691
+ },
692
+ // Adding positive zero canonicalizes a truncated negative zero.
693
+ else_: { kind: "bin", op: "+", left: integerRef, right: numLit(0, loc), type: F64, loc },
694
+ type: F64,
695
+ loc,
696
+ };
697
+ const start = method === "lastIndexOf" ? {
698
+ kind: "ternary",
699
+ cond: { kind: "bin", op: "<", left: integerRef, right: numLit(0, loc), type: BOOL, loc },
700
+ then: { kind: "bin", op: "+", left: varRef("n.0", F64, loc), right: integerRef, type: F64, loc },
701
+ else_: {
702
+ kind: "libCall", fn: "math.min",
703
+ args: [
704
+ { kind: "bin", op: "+", left: integerRef, right: numLit(0, loc), type: F64, loc },
705
+ { kind: "bin", op: "-", left: varRef("n.0", F64, loc), right: numLit(1, loc), type: F64, loc },
706
+ ],
707
+ type: F64, loc,
708
+ },
709
+ type: F64, loc,
710
+ } : forwardStart;
637
711
  const rawRead = { kind: "arrayGet", arr: arrRef, index: indexRef, type: elem, loc };
638
712
  const missing = lowerer.wrappedUndefined(valueT, loc);
639
713
  if (!missing)
640
714
  throw new InternalCompilerError("array search helper needs an undefined arm");
641
- const value = {
715
+ const optionalValue = {
642
716
  kind: "ternary",
643
717
  cond: { kind: "bin", op: "===", left: stateRef, right: numLit(1, loc), type: BOOL, loc },
644
718
  then: typeEquals(elem, valueT) ? rawRead : lowerer.coerceToExpected(rawRead, valueT),
@@ -646,12 +720,14 @@ function arraySearchHelper(lowerer, method, elem, valueT, loc) {
646
720
  type: valueT,
647
721
  loc,
648
722
  };
649
- const equal = arraySearchEquality(lowerer, varRef("value.0", valueT, loc), needleRef, method === "includes", loc);
650
- const match = method === "indexOf"
723
+ const value = directNeedle ? rawRead : optionalValue;
724
+ const readT = directNeedle ? elem : valueT;
725
+ const equal = arraySearchEquality(lowerer, varRef("value.0", readT, loc), needleRef, method === "includes", loc);
726
+ const match = method !== "includes"
651
727
  ? {
652
728
  kind: "logical",
653
729
  op: "&&",
654
- left: { kind: "bin", op: "!==", left: stateRef, right: numLit(0, loc), type: BOOL, loc },
730
+ left: { kind: "bin", op: directNeedle ? "===" : "!==", left: stateRef, right: numLit(directNeedle ? 1 : 0, loc), type: BOOL, loc },
655
731
  right: equal,
656
732
  type: BOOL,
657
733
  loc,
@@ -660,7 +736,7 @@ function arraySearchHelper(lowerer, method, elem, valueT, loc) {
660
736
  const undefinedTag = lowerer.armTag(valueT.unionId, UNDEFINED_T);
661
737
  if (undefinedTag < 0)
662
738
  throw new InternalCompilerError("array search helper needs an undefined tag");
663
- const needleIsUndefined = {
739
+ const needleIsUndefined = directNeedle ? boolLit(false, loc) : {
664
740
  kind: "unionIsTag",
665
741
  unionId: valueT.unionId,
666
742
  tag: undefinedTag,
@@ -672,7 +748,7 @@ function arraySearchHelper(lowerer, method, elem, valueT, loc) {
672
748
  const advance = {
673
749
  kind: "assign",
674
750
  localId: "i.0",
675
- value: { kind: "bin", op: "+", left: indexRef, right: numLit(1, loc), type: F64, loc },
751
+ value: { kind: "bin", op: method === "lastIndexOf" ? "-" : "+", left: indexRef, right: numLit(1, loc), type: F64, loc },
676
752
  loc,
677
753
  };
678
754
  const visitPresent = [
@@ -680,13 +756,13 @@ function arraySearchHelper(lowerer, method, elem, valueT, loc) {
680
756
  {
681
757
  kind: "if",
682
758
  cond: match,
683
- then: [{ kind: "return", value: method === "indexOf" ? indexRef : boolLit(true, loc), loc }],
759
+ then: [{ kind: "return", value: method === "includes" ? boolLit(true, loc) : indexRef, loc }],
684
760
  else_: null,
685
761
  loc,
686
762
  },
687
763
  advance,
688
764
  ];
689
- const skipHole = [
765
+ const skipHole = method === "lastIndexOf" ? [advance] : [
690
766
  {
691
767
  kind: "varDecl",
692
768
  localId: "next.0",
@@ -712,28 +788,36 @@ function arraySearchHelper(lowerer, method, elem, valueT, loc) {
712
788
  name,
713
789
  params: [
714
790
  { localId: "a.0", name: "a", type: arrT },
715
- { localId: "needle.0", name: "needle", type: valueT },
791
+ { localId: "needle.0", name: "needle", type: needleT },
792
+ { localId: "from.0", name: "from", type: fromT },
716
793
  ],
717
794
  returnType: resultT,
718
795
  locals: [
719
796
  { id: "a.0", name: "a", type: arrT, mutable: true },
720
- { id: "needle.0", name: "needle", type: valueT, mutable: true },
797
+ { id: "needle.0", name: "needle", type: needleT, mutable: true },
798
+ { id: "from.0", name: "from", type: fromT, mutable: false },
799
+ { id: "number.0", name: "number", type: F64, mutable: false },
800
+ { id: "integer.0", name: "integer", type: F64, mutable: false },
721
801
  { id: "n.0", name: "n", type: F64, mutable: false },
722
802
  { id: "i.0", name: "i", type: F64, mutable: true },
723
803
  { id: "state.0", name: "state", type: F64, mutable: false },
724
804
  { id: "next.0", name: "next", type: F64, mutable: false },
725
- { id: "value.0", name: "value", type: valueT, mutable: false },
805
+ { id: "value.0", name: "value", type: readT, mutable: false },
726
806
  ],
727
807
  body: [
728
808
  readLenStmt(arrT, loc),
729
- { kind: "varDecl", localId: "i.0", init: numLit(0, loc), loc },
809
+ { kind: "varDecl", localId: "number.0", init: number, loc },
810
+ { kind: "varDecl", localId: "integer.0", init: integer, loc },
811
+ { kind: "varDecl", localId: "i.0", init: start, loc },
730
812
  {
731
813
  kind: "while",
732
- cond: { kind: "bin", op: "<", left: indexRef, right: varRef("n.0", F64, loc), type: BOOL, loc },
814
+ cond: method === "lastIndexOf"
815
+ ? { kind: "bin", op: ">=", left: indexRef, right: numLit(0, loc), type: BOOL, loc }
816
+ : { kind: "bin", op: "<", left: indexRef, right: varRef("n.0", F64, loc), type: BOOL, loc },
733
817
  body: loopBody,
734
818
  loc,
735
819
  },
736
- { kind: "return", value: method === "indexOf" ? numLit(-1, loc) : boolLit(false, loc), loc },
820
+ { kind: "return", value: method === "includes" ? boolLit(false, loc) : numLit(-1, loc), loc },
737
821
  ],
738
822
  loc,
739
823
  });
@@ -1291,15 +1375,8 @@ export function lowerTupleReadMethodCall(lowerer, call, access) {
1291
1375
  const helper = arrayHofHelper(lowerer, "map", snapshotElem, fnRet, arity, loc, outElem);
1292
1376
  return { kind: "call", callee: helper, args: [snapshot, fnArg], type: arrayOf(outElem), loc };
1293
1377
  }
1294
- /** An OOB-SAFE indexed read: `xs[i]`
1295
- * answers the interned `elem | undefined` union — the element when `i`
1296
- * is an integer in [0, len), JS's property-miss undefined otherwise —
1297
- * instead of the trap divergence 4 documents for program code.
1298
- * Package JS is inference-typed, guard-style code (`registeredArguments
1299
- * .slice(-1)[0]`, commander's last-element probe), and the trap would
1300
- * fire on working Node idioms; program files keep the documented trap
1301
- * (their annotations can prove bounds). Existing element unions retag
1302
- * into the canonical union with an added undefined arm. */
1378
+ /** An ordinary indexed read: values retain their element type, while
1379
+ * holes, missing properties, and present undefined yield undefined. */
1303
1380
  export function lowerSafeIndexRead(lowerer, arr, index, loc) {
1304
1381
  const elem = arr.type.elem;
1305
1382
  if (elem.kind === "void" || elem.kind === "dyn")
@@ -1328,6 +1405,21 @@ export function lowerSafeIndexRead(lowerer, arr, index, loc) {
1328
1405
  }
1329
1406
  return { kind: "call", callee: name, args: [arr, index], type: resultT, loc };
1330
1407
  }
1408
+ /** Fuse only our own f64-array read helper with immediate ToNumber.
1409
+ * Reuse its arguments verbatim so the receiver and index still evaluate
1410
+ * exactly once, in order, before the read. Do not specialize user calls,
1411
+ * union-element arrays, or optional values stored in locals. */
1412
+ export function tryLowerNumericIndexRead(lowerer, operand, loc) {
1413
+ if (operand.kind !== "call" || operand.callee !== lowerer.arrHofHelpers.get(`idxOr:${typeKey(F64)}`))
1414
+ return null;
1415
+ const [arr, index] = operand.args;
1416
+ if (operand.args.length !== 2 || arr?.type.kind !== "array" || arr.type.elem.kind !== "f64" || index?.type.kind !== "f64")
1417
+ return null;
1418
+ // The intrinsic owns the evaluated receiver through index evaluation,
1419
+ // then borrows it for one slot lookup. No extra helper parameter or
1420
+ // separate state/getter expressions need to retain the array again.
1421
+ return { kind: "arrIntrinsic", method: "getNumber", receiver: arr, args: [index], type: F64, loc };
1422
+ }
1331
1423
  /** Backward-compatible name for the npm-static probe path. */
1332
1424
  export const lowerNpmStaticSafeIndexRead = lowerSafeIndexRead;
1333
1425
  /** Interned synthetic function for one (elem, argument-shape) concat —