@scriptc/compiler 0.0.36 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +7 -5
  2. package/ambient/scriptc-overrides.d.ts +0 -7
  3. package/ambient/scriptc.d.ts +1 -1
  4. package/dist/backend/c/exprs.js +69 -45
  5. package/dist/backend/c/exprs.js.map +1 -1
  6. package/dist/backend/c/may-throw.js +3 -0
  7. package/dist/backend/c/may-throw.js.map +1 -1
  8. package/dist/backend/c/stmts.js +18 -0
  9. package/dist/backend/c/stmts.js.map +1 -1
  10. package/dist/backend/c/walkers.js +5 -7
  11. package/dist/backend/c/walkers.js.map +1 -1
  12. package/dist/backend/external-c.d.ts +33 -0
  13. package/dist/backend/external-c.js +51 -0
  14. package/dist/backend/external-c.js.map +1 -0
  15. package/dist/backend/link-plan.d.ts +37 -0
  16. package/dist/backend/link-plan.js +26 -0
  17. package/dist/backend/link-plan.js.map +1 -0
  18. package/dist/backend/linker.d.ts +22 -0
  19. package/dist/backend/linker.js +236 -0
  20. package/dist/backend/linker.js.map +1 -0
  21. package/dist/backend/llvm/dyn.js +7 -0
  22. package/dist/backend/llvm/dyn.js.map +1 -1
  23. package/dist/backend/llvm/emitter.js +34 -7
  24. package/dist/backend/llvm/emitter.js.map +1 -1
  25. package/dist/backend/llvm/expr-containers.js +68 -54
  26. package/dist/backend/llvm/expr-containers.js.map +1 -1
  27. package/dist/backend/llvm/expr-context.d.ts +1 -1
  28. package/dist/backend/llvm/expr-dispatch.js +2 -0
  29. package/dist/backend/llvm/expr-dispatch.js.map +1 -1
  30. package/dist/backend/llvm/expr-primitives.d.ts +1 -1
  31. package/dist/backend/llvm/expr-primitives.js +20 -0
  32. package/dist/backend/llvm/expr-primitives.js.map +1 -1
  33. package/dist/backend/llvm/expr-stream-bridges.js +25 -1
  34. package/dist/backend/llvm/expr-stream-bridges.js.map +1 -1
  35. package/dist/backend/llvm/walkers.js +31 -4
  36. package/dist/backend/llvm/walkers.js.map +1 -1
  37. package/dist/backend/native-codegen.d.ts +8 -1
  38. package/dist/backend/native-codegen.js +39 -25
  39. package/dist/backend/native-codegen.js.map +1 -1
  40. package/dist/backend/native-codegen.test.js +42 -4
  41. package/dist/backend/native-codegen.test.js.map +1 -1
  42. package/dist/backend/native-link-info.d.ts +1 -1
  43. package/dist/backend/native-link-info.js +42 -18
  44. package/dist/backend/native-link-info.js.map +1 -1
  45. package/dist/backend/native-toolchain.test.js +5 -1
  46. package/dist/backend/native-toolchain.test.js.map +1 -1
  47. package/dist/backend/runtime-pack-targets.test.d.ts +1 -0
  48. package/dist/backend/runtime-pack-targets.test.js +54 -0
  49. package/dist/backend/runtime-pack-targets.test.js.map +1 -0
  50. package/dist/backend/runtime-pack.d.ts +5 -30
  51. package/dist/backend/runtime-pack.js +46 -141
  52. package/dist/backend/runtime-pack.js.map +1 -1
  53. package/dist/backend/runtime-pack.test.js +49 -14
  54. package/dist/backend/runtime-pack.test.js.map +1 -1
  55. package/dist/backend/targets.d.ts +63 -17
  56. package/dist/backend/targets.js +251 -35
  57. package/dist/backend/targets.js.map +1 -1
  58. package/dist/backend/targets.test.js +27 -5
  59. package/dist/backend/targets.test.js.map +1 -1
  60. package/dist/coverage/surface-manifest.js +10 -1
  61. package/dist/coverage/surface-manifest.js.map +1 -1
  62. package/dist/frontend/lowering/array-values.d.ts +8 -0
  63. package/dist/frontend/lowering/array-values.js +60 -0
  64. package/dist/frontend/lowering/array-values.js.map +1 -0
  65. package/dist/frontend/lowering/lower-array-sort.d.ts +22 -0
  66. package/dist/frontend/lowering/lower-array-sort.js +619 -0
  67. package/dist/frontend/lowering/lower-array-sort.js.map +1 -0
  68. package/dist/frontend/lowering/lower-builtins.js +164 -4
  69. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  70. package/dist/frontend/lowering/lower-calls.d.ts +3 -4
  71. package/dist/frontend/lowering/lower-calls.js +260 -27
  72. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  73. package/dist/frontend/lowering/lower-containers.js +728 -655
  74. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  75. package/dist/frontend/lowering/lower-exprs.d.ts +4 -0
  76. package/dist/frontend/lowering/lower-exprs.js +769 -101
  77. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  78. package/dist/frontend/lowering/lower-inspect.js +47 -33
  79. package/dist/frontend/lowering/lower-inspect.js.map +1 -1
  80. package/dist/frontend/lowering/lower-island.d.ts +5 -4
  81. package/dist/frontend/lowering/lower-island.js +10 -5
  82. package/dist/frontend/lowering/lower-island.js.map +1 -1
  83. package/dist/frontend/lowering/lower-server.js +10 -0
  84. package/dist/frontend/lowering/lower-server.js.map +1 -1
  85. package/dist/frontend/lowering/lower-stmts.js +302 -71
  86. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  87. package/dist/frontend/lowering/lowerer.d.ts +74 -2
  88. package/dist/frontend/lowering/lowerer.js +1087 -10
  89. package/dist/frontend/lowering/lowerer.js.map +1 -1
  90. package/dist/frontend/lowering/surfaces.d.ts +10 -2
  91. package/dist/frontend/lowering/surfaces.js +15 -3
  92. package/dist/frontend/lowering/surfaces.js.map +1 -1
  93. package/dist/index.d.ts +3 -3
  94. package/dist/index.js +45 -23
  95. package/dist/index.js.map +1 -1
  96. package/dist/ir/ir.d.ts +65 -11
  97. package/dist/ir/ir.js +25 -25
  98. package/dist/ir/ir.js.map +1 -1
  99. package/dist/ir/serialize.d.ts +1 -1
  100. package/dist/ir/serialize.js +1 -1
  101. package/dist/ir/validate.js +84 -37
  102. package/dist/ir/validate.js.map +1 -1
  103. package/dist/library/fence-eval.js +3 -1
  104. package/dist/library/fence-eval.js.map +1 -1
  105. package/dist/library/int-infer.js +12 -1
  106. package/dist/library/int-infer.js.map +1 -1
  107. package/dist/library/int-infer.test.js +4 -4
  108. package/dist/library/library-cache.test.js +5 -5
  109. package/dist/startup-cache.d.ts +9 -1
  110. package/dist/startup-cache.js +16 -1
  111. package/dist/startup-cache.js.map +1 -1
  112. package/package.json +17 -4
  113. package/surface-manifest.json +5 -5
@@ -7,6 +7,7 @@ import { InternalCompilerError } from "../../errors.js";
7
7
  * (FieldTarget). */
8
8
  import * as ts from "../ts7/adapter.js";
9
9
  import { dirname, posix } from "node:path";
10
+ import { pathToFileURL } from "node:url";
10
11
  import { wasiGuestPath } from "../../wasi-paths.js";
11
12
  import { BOOL, CAUGHT, DYN, DYN_HANDLE_KINDS, F64, JSVAL, NULL_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_ERROR_CLASSES, SEARCH_PARAMS_T, STRING, UNDEFINED_T, VOID, arrayOf, canAdaptDynFuncTo, canBoxFuncIntoDyn, canDynCheckTo, funcOf, isJsonSafeType, isUnitType, jsOpResultKind, shapeHasAccessorSlots, typeEquals, typeKey, unionFuncSetArmsOk } from "../../ir/ir.js";
12
13
  import { cjsClassExprWholeExportOf, cjsExportAssignmentOf, cjsExportDiscardReason, isCjsExportTableLiteral, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeEsmFile, locOf } from "../program.js";
@@ -14,6 +15,7 @@ import { ARRAY_METHODS, builtinConstLit, builtinFenceHintOf, builtinModuleConstO
14
15
  import { blockedBindingUseDiag, recordShapeMismatchDiag, requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
15
16
  import { PoisonError, dynUndefinedExpr, jsFuncNameOf, neverTaintedJsType, nodeThrowExpr, own } from "./lowerer.js";
16
17
  import { lowerIndexMergeHelper, lowerNpmStaticSafeIndexRead, lowerSafeIndexRead, strCharsCall } from "./lower-containers.js";
18
+ import { arrayValueStore } from "./array-values.js";
17
19
  import { npmStaticPackageOfPath } from "../npm-static.js";
18
20
  import { unsupportedModuleFeatureOf } from "../builtin-modules.js";
19
21
  import { fenceEnumObjectValue, lowerEnumAccess } from "./lower-enums.js";
@@ -640,9 +642,13 @@ function lowerExprInner(lowerer, expr) {
640
642
  const symbol = lowerer.resolveValueSymbol(expr);
641
643
  const runtimeOptionalRoot = lowerer.runtimeOptionalRootOf(local);
642
644
  const active = lowerer.runtimeOptionalLocals.has(runtimeOptionalRoot);
645
+ const arithmetic = lowerer.runtimeOptionalArithmeticLocals.has(runtimeOptionalRoot);
643
646
  const captured = !!symbol &&
644
647
  lowerer.runtimeOptionalStorageLocals.has(runtimeOptionalRoot) &&
645
- lowerer.ctx.captureBySymbol.get(symbol) === local;
648
+ (lowerer.ctx.captureBySymbol.get(symbol) === local || runtimeOptionalRoot !== local);
649
+ if (arithmetic && !runtimeOptionalUseOf(expr)) {
650
+ return { kind: "varRef", localId: local.id, type: local.type, loc };
651
+ }
646
652
  if (active || captured) {
647
653
  const use = runtimeOptionalUseOf(expr);
648
654
  if (use?.complex) {
@@ -665,8 +671,20 @@ function lowerExprInner(lowerer, expr) {
665
671
  }
666
672
  return runtimeOptionalReceiverRead(lowerer, expr, local, null, loc);
667
673
  }
668
- if (active || captured)
669
- return { kind: "varRef", localId: local.id, type: local.type, loc };
674
+ if (runtimeOptionalObjectWalkerArg(lowerer, expr)) {
675
+ const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr));
676
+ if (local.type.kind === "union" && narrowed) {
677
+ const helper = narrowed.kind === "union"
678
+ ? lowerer.narrowedRetagHelper(expr, local.type.unionId, narrowed.unionId, loc)
679
+ : !isUnitType(narrowed)
680
+ ? lowerer.narrowedArmHelper(local.type.unionId, narrowed, loc)
681
+ : null;
682
+ if (helper) {
683
+ return { kind: "call", callee: helper, args: [varRef(local.id, local.type, loc)], type: narrowed, loc };
684
+ }
685
+ }
686
+ }
687
+ return { kind: "varRef", localId: local.id, type: local.type, loc };
670
688
  }
671
689
  return lowerer.maybeNarrow({ kind: "varRef", localId: local.id, type: local.type, loc }, expr);
672
690
  }
@@ -686,7 +704,53 @@ function lowerExprInner(lowerer, expr) {
686
704
  lowerer.pushDiag(blockedBindingUseDiag(expr.text, loc));
687
705
  throw new PoisonError();
688
706
  }
689
- return lowerer.maybeNarrow({ kind: "varRef", localId: g.id, type: g.type, loc }, expr);
707
+ const ref = { kind: "varRef", localId: g.id, type: g.type, loc };
708
+ if (lowerer.isRuntimeOptionalArithmeticGlobal(g) && !runtimeOptionalUseOf(expr)) {
709
+ return ref;
710
+ }
711
+ const storageHasUndefined = g.type.kind === "union" &&
712
+ lowerer.armTag(g.type.unionId, UNDEFINED_T) >= 0;
713
+ if (lowerer.isRuntimeOptionalGlobal(g) || storageHasUndefined) {
714
+ // A checker-narrowed global receiver (`if (tags !== undefined)
715
+ // tags[tags.length] = ...`, or `a ? Object.keys(a) : ...`) is
716
+ // still stored as the runtime optional union. Extract the proven
717
+ // value arm before routing the member/element operation so dynamic
718
+ // keys and builtin record walkers do not see a union receiver.
719
+ const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr));
720
+ const arithmeticUnion = g.type.kind === "union" &&
721
+ (lowerer.unions.get(g.type.unionId)?.arms.length === 2) &&
722
+ lowerer.unions.get(g.type.unionId).arms.some((a) => a.kind === "f64") &&
723
+ lowerer.unions.get(g.type.unionId).arms.some((a) => a.kind === "string");
724
+ if (!arithmeticUnion && g.type.kind === "union" && narrowed && narrowed.kind !== "union" &&
725
+ !isUnitType(narrowed) &&
726
+ narrowed.kind !== "f64" && narrowed.kind !== "string" && narrowed.kind !== "bool" &&
727
+ lowerer.armTag(g.type.unionId, narrowed) >= 0) {
728
+ const helper = lowerer.narrowedArmHelper(g.type.unionId, narrowed, loc);
729
+ if (helper)
730
+ return { kind: "call", callee: helper, args: [ref], type: narrowed, loc };
731
+ }
732
+ const use = runtimeOptionalUseOf(expr);
733
+ // The enclosing logical/conditional expression consumes the union
734
+ // before any member operation runs (`(value ?? fallback).length`).
735
+ // Keep the tagged value intact and let that lazy expression decide
736
+ // its branch; extracting here would evaluate or throw too early.
737
+ if (use?.complex)
738
+ return ref;
739
+ if (use?.optional)
740
+ return ref;
741
+ if (use?.kind === "property")
742
+ return runtimeOptionalReceiverRead(lowerer, expr, g, use.access.name.text, loc);
743
+ if (use?.kind === "element") {
744
+ const key = runtimeOptionalElementKey(use.access.argumentExpression);
745
+ if (key === null)
746
+ lowerer.unsupported("SC1090", use.access, "a computed read from a runtime-optional global (use a literal key)");
747
+ return runtimeOptionalReceiverRead(lowerer, expr, g, key, loc);
748
+ }
749
+ if (use?.kind === "call")
750
+ return runtimeOptionalReceiverRead(lowerer, expr, g, null, loc);
751
+ return ref;
752
+ }
753
+ return lowerer.maybeNarrow(ref, expr);
690
754
  }
691
755
  // `undefined` — tsc's intrinsic global (a local shadowing the name
692
756
  // resolved above): a unit literal, exactly like the `null` keyword.
@@ -709,9 +773,7 @@ function lowerExprInner(lowerer, expr) {
709
773
  // directory at `/tmp`. Bake the matching GUEST spelling so resource
710
774
  // lookups relative to these CommonJS globals stay inside the exposed
711
775
  // capability instead of naming an unreachable host-absolute path.
712
- const fileName = lowerer.targetPlatform === "wasi"
713
- ? wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/")
714
- : sf.fileName;
776
+ const fileName = moduleFileName(lowerer, sf);
715
777
  const value = expr.text === "__dirname"
716
778
  ? (lowerer.targetPlatform === "wasi" ? posix.dirname(fileName) : dirname(fileName))
717
779
  : fileName;
@@ -1243,6 +1305,15 @@ function lowerExprInner(lowerer, expr) {
1243
1305
  if (en)
1244
1306
  return en;
1245
1307
  }
1308
+ // `import.meta.<field>` — the file-backed ESM metadata that survives
1309
+ // compilation as a constant. Only direct, identifier-named reads are
1310
+ // claimed here; the bare meta object, computed access, writes, and
1311
+ // unsupported members continue through the generic module-loader fence
1312
+ // below. The source file is the module identity, so imported modules
1313
+ // receive their own filename/dirname and only the entry receives main.
1314
+ const importMeta = importMetaProperty(lowerer, expr);
1315
+ if (importMeta)
1316
+ return importMeta;
1246
1317
  // `require.main.filename` / `require.main?.filename` — CommonJS
1247
1318
  // entry-module identity: in a compiled binary require.main IS the
1248
1319
  // entry module (never undefined in a CJS graph, so the chain's guard
@@ -1536,8 +1607,14 @@ function lowerExprInner(lowerer, expr) {
1536
1607
  lowerer.lowerFieldRead(expr);
1537
1608
  // A union-typed field read narrows like an identifier when the
1538
1609
  // checker has narrowed this use to one arm.
1539
- if (handled)
1610
+ if (handled) {
1611
+ if (handled.type.kind === "union" &&
1612
+ lowerer.armTag(handled.type.unionId, UNDEFINED_T) >= 0 &&
1613
+ handled.kind === "recordGet" &&
1614
+ lowerer.isRuntimeOptionalField(handled.shapeId, handled.field))
1615
+ return handled;
1540
1616
  return lowerer.maybeNarrow(handled, expr);
1617
+ }
1541
1618
  // `globalThis.<name>` that no lowering above claimed
1542
1619
  // (globalThis.crypto, globalThis.SubtleCrypto, globalThis.localStorage
1543
1620
  // — the harness's capability-conditional knownGlobals adds): the
@@ -1778,6 +1855,46 @@ function lowerExprInner(lowerer, expr) {
1778
1855
  lowerer.unsupported(entry.code, expr, entry.feature);
1779
1856
  lowerer.unsupported("SC1090", expr, `syntax '${ts.SyntaxKind[expr.kind]}'`);
1780
1857
  }
1858
+ function moduleFileName(lowerer, sf) {
1859
+ return lowerer.targetPlatform === "wasi"
1860
+ ? wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/")
1861
+ : sf.fileName;
1862
+ }
1863
+ function importMetaProperty(lowerer, expr) {
1864
+ if (expr.questionDotToken !== undefined || !ts.isMetaProperty(expr.expression))
1865
+ return null;
1866
+ const meta = expr.expression;
1867
+ if (meta.keywordToken !== ts.SyntaxKind.ImportKeyword || meta.name.text !== "meta")
1868
+ return null;
1869
+ const sf = expr.getSourceFile();
1870
+ const fileName = moduleFileName(lowerer, sf);
1871
+ if (lowerer.dynamic) {
1872
+ lowerer.unsupported("SC1090", expr, `'import.meta.${expr.name.text}' in a dynamic island (module-loader metadata is supported only in static ESM)`);
1873
+ }
1874
+ switch (expr.name.text) {
1875
+ case "url":
1876
+ return {
1877
+ kind: "strLit",
1878
+ value: pathToFileURL(fileName, { windows: lowerer.targetPlatform === "win32" }).href,
1879
+ type: STRING,
1880
+ loc: locOf(expr),
1881
+ };
1882
+ case "filename":
1883
+ return { kind: "strLit", value: fileName, type: STRING, loc: locOf(expr) };
1884
+ case "dirname":
1885
+ return {
1886
+ kind: "strLit",
1887
+ value: lowerer.targetPlatform === "wasi" ? posix.dirname(fileName) : dirname(fileName),
1888
+ type: STRING,
1889
+ loc: locOf(expr),
1890
+ };
1891
+ case "main":
1892
+ return { kind: "boolLit", value: sf === lowerer.entry, type: BOOL, loc: locOf(expr) };
1893
+ default:
1894
+ lowerer.unsupported("SC1090", expr, `'import.meta.${expr.name.text}' (only url, filename, dirname, and main are supported in static ESM)`);
1895
+ return null;
1896
+ }
1897
+ }
1781
1898
  /** `c ? a : b` — see the inline comments. `expected` plays the contextual
1782
1899
  * array type's role when the caller knows the slot's array type and tsc's
1783
1900
  * API doesn't surface it (a ternary as a SPREAD source — lowerArrayLiteral
@@ -2125,19 +2242,65 @@ export function maybeNarrow(lowerer, expr, node) {
2125
2242
  * even though an OOB-safe assignment may have restored undefined. Check
2126
2243
  * that hidden arm and throw the member-read TypeError Node would produce. */
2127
2244
  function runtimeOptionalReceiverRead(lowerer, expr, local, property, loc) {
2128
- if (local.type.kind !== "union")
2129
- throw new InternalCompilerError("runtime-optional local is not union-typed");
2130
- const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr));
2131
- if (!narrowed || narrowed.kind === "union" || isUnitType(narrowed)) {
2245
+ if (local.type.kind !== "union") {
2246
+ lowerer.runtimeOptionalLocals.delete(local);
2247
+ lowerer.runtimeOptionalStorageLocals.delete(local);
2248
+ return varRef(local.id, local.type, loc);
2249
+ }
2250
+ // A non-null/type assertion around the receiver carries the useful
2251
+ // checker narrowing on the wrapper rather than on the identifier token
2252
+ // itself (`gg!.day`). Follow only erased wrappers; the member/call node
2253
+ // still receives the checked extraction built below.
2254
+ let narrowedNode = expr;
2255
+ while ((ts.isParenthesizedExpression(narrowedNode.parent) ||
2256
+ ts.isAssertionExpression(narrowedNode.parent) ||
2257
+ ts.isSatisfiesExpression(narrowedNode.parent) ||
2258
+ ts.isNonNullExpression(narrowedNode.parent)) &&
2259
+ narrowedNode.parent.expression === narrowedNode) {
2260
+ narrowedNode = narrowedNode.parent;
2261
+ }
2262
+ const narrowed = lowerer.mapTypeOf(lowerer.typeOf(narrowedNode));
2263
+ if (!narrowed || isUnitType(narrowed)) {
2132
2264
  lowerer.unsupported("SC1090", expr, "a runtime-optional capture whose narrowed value type is not representable");
2133
2265
  }
2266
+ if (narrowed.kind === "union") {
2267
+ // The checker may keep the receiver at the storage union unchanged
2268
+ // (`string | undefined` on a JS `.match()` call). There is no
2269
+ // sub-union to re-tag: preserve the value for the downstream
2270
+ // nullable-receiver lowering, which performs the member-specific
2271
+ // checked extraction. Refusing here deferred the whole statement to
2272
+ // a runtime fence before that lowering could run.
2273
+ if (typeEquals(local.type, narrowed)) {
2274
+ return varRef(local.id, local.type, loc);
2275
+ }
2276
+ // A captured array element can itself be a union of records (for
2277
+ // example Circle | Square). The optional storage union has one extra
2278
+ // undefined arm; re-tag it to the checker-narrowed value union with a
2279
+ // checked trap for that arm, then let union property lowering inspect
2280
+ // the surviving record arms.
2281
+ const helper = lowerer.narrowedRetagHelper(expr, local.type.unionId, narrowed.unionId, loc);
2282
+ if (!helper) {
2283
+ lowerer.unsupported("SC1090", expr, "a runtime-optional capture whose narrowed union is not representable");
2284
+ }
2285
+ return { kind: "call", callee: helper, args: [varRef(local.id, local.type, loc)], type: narrowed, loc };
2286
+ }
2134
2287
  const valueTag = lowerer.armTag(local.type.unionId, narrowed);
2135
2288
  const undefTag = lowerer.armTag(local.type.unionId, UNDEFINED_T);
2136
2289
  if (valueTag < 0 || undefTag < 0)
2137
2290
  throw new InternalCompilerError("runtime-optional local is missing its value or undefined arm");
2138
2291
  const def = lowerer.unions.get(local.type.unionId);
2139
2292
  if (!def || def.arms.length !== 2) {
2140
- lowerer.unsupported("SC1090", expr, "a direct read from a runtime-optional capture with multiple value arms");
2293
+ // The checker may narrow a runtime-optional capture to one arm of a
2294
+ // value union (`Circle | Square | undefined` -> `Circle`). The
2295
+ // two-arm fast path below cannot extract that arm safely: a later
2296
+ // array mutation can leave any of the stored tags in the slot. Use
2297
+ // the checked helper so every other arm, including undefined, remains
2298
+ // a catchable TypeError rather than an unchecked payload read.
2299
+ const helper = lowerer.narrowedArmHelper(local.type.unionId, narrowed, loc);
2300
+ if (!helper) {
2301
+ lowerer.unsupported("SC1090", expr, "a direct read from a runtime-optional capture with multiple value arms");
2302
+ }
2303
+ return { kind: "call", callee: helper, args: [varRef(local.id, local.type, loc)], type: narrowed, loc };
2141
2304
  }
2142
2305
  const message = property === null
2143
2306
  ? `${expr.text} is not a function`
@@ -2209,6 +2372,25 @@ function runtimeOptionalUseOf(expr) {
2209
2372
  }
2210
2373
  return null;
2211
2374
  }
2375
+ /** Object.keys/Object.entries require an object receiver, but their first
2376
+ * argument is not syntactically a member receiver. Recognize that one
2377
+ * standard-library position so a runtime-optional loop element can be
2378
+ * checked against the checker-narrowed record arm before builtin lowering. */
2379
+ function runtimeOptionalObjectWalkerArg(lowerer, expr) {
2380
+ let arg = expr;
2381
+ while ((ts.isParenthesizedExpression(arg.parent) ||
2382
+ ts.isAssertionExpression(arg.parent) ||
2383
+ ts.isSatisfiesExpression(arg.parent) ||
2384
+ ts.isNonNullExpression(arg.parent)) &&
2385
+ arg.parent.expression === arg) {
2386
+ arg = arg.parent;
2387
+ }
2388
+ const call = arg.parent;
2389
+ if (!ts.isCallExpression(call) || call.arguments[0] !== arg || !ts.isPropertyAccessExpression(call.expression))
2390
+ return false;
2391
+ const access = call.expression;
2392
+ return (access.name.text === "keys" || access.name.text === "entries") && lowerer.isStdlibMember(access);
2393
+ }
2212
2394
  function runtimeOptionalElementKey(expr) {
2213
2395
  if (ts.isStringLiteral(expr) || ts.isNumericLiteral(expr))
2214
2396
  return expr.text;
@@ -2758,7 +2940,8 @@ export function lowerOptionalChain(lowerer, expr) {
2758
2940
  dotNode = tail;
2759
2941
  recvNode = tail.expression;
2760
2942
  }
2761
- const receiver = lowerer.lowerExpr(recvNode);
2943
+ const loweredReceiver = lowerer.lowerExpr(recvNode);
2944
+ const receiver = lowerer.runtimeOptionalSourceValue(recvNode, loweredReceiver) ?? loweredReceiver;
2762
2945
  if (receiver.type.kind === "dyn") {
2763
2946
  // `pkg?.name` / `pkg?.scripts?.[k]` on a JSON.parse result: dyn
2764
2947
  // represents undefined directly, so the chain step IS the keyed
@@ -3153,6 +3336,10 @@ export function lowerIntrinsicProperty(lowerer, expr) {
3153
3336
  if (kind === "child" ? !isChildSurfaceMember(lowerer, expr) : !tupleLengthOnArray && !lowerer.isStdlibMember(expr))
3154
3337
  return null;
3155
3338
  const name = expr.name.text;
3339
+ const strictReceiver = (expected) => {
3340
+ const value = lowerer.lowerExpr(expr.expression);
3341
+ return lowerer.runtimeOptionalPropertyReceiver(expr.expression, value, expected, name) ?? value;
3342
+ };
3156
3343
  if (kind === "child") {
3157
3344
  const loc = locOf(expr);
3158
3345
  // The lifecycle reads, Node's exact shapes (SEMANTICS.md pins the
@@ -3315,7 +3502,7 @@ export function lowerIntrinsicProperty(lowerer, expr) {
3315
3502
  }
3316
3503
  if (kind === "regex") {
3317
3504
  if (name === "source" || name === "flags") {
3318
- const receiver = lowerer.lowerExpr(expr.expression);
3505
+ const receiver = strictReceiver({ kind: "regex" });
3319
3506
  return { kind: "regexIntrinsic", method: name, receiver, args: [], type: STRING, loc: locOf(expr) };
3320
3507
  }
3321
3508
  if (name === "test") {
@@ -3334,7 +3521,19 @@ export function lowerIntrinsicProperty(lowerer, expr) {
3334
3521
  if (kind === "bytes") {
3335
3522
  const loc = locOf(expr);
3336
3523
  if (name === "length" || name === "byteLength") {
3337
- const receiver = lowerer.lowerExpr(expr.expression);
3524
+ const expected = lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
3525
+ let receiver = expected?.kind === "bytes"
3526
+ ? strictReceiver(expected)
3527
+ : lowerer.lowerExpr(expr.expression);
3528
+ if (receiver.type.kind === "union" && lowerer.armTag(receiver.type.unionId, UNDEFINED_T) >= 0) {
3529
+ const present = lowerer.stripUndefinedArm(receiver.type);
3530
+ const helper = present.kind === "bytes"
3531
+ ? lowerer.narrowedArmHelper(receiver.type.unionId, present, locOf(expr.expression))
3532
+ : null;
3533
+ receiver = helper
3534
+ ? { kind: "call", callee: helper, args: [receiver], type: present, loc: locOf(expr.expression) }
3535
+ : lowerer.maybeNarrow(receiver, expr.expression);
3536
+ }
3338
3537
  // An island handle behind a typed-array .d.ts surface: the engine
3339
3538
  // property read, exiting at the declared number type (the array
3340
3539
  // .length rule). Chain-handled reads stay jsval.
@@ -3387,7 +3586,21 @@ export function lowerIntrinsicProperty(lowerer, expr) {
3387
3586
  return null;
3388
3587
  }
3389
3588
  if (name === "length") {
3390
- const receiver = lowerer.lowerExpr(expr.expression);
3589
+ const expected = kind === "string"
3590
+ ? STRING
3591
+ : lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
3592
+ let receiver = expected && (expected.kind === "string" || expected.kind === "array")
3593
+ ? strictReceiver(expected)
3594
+ : lowerer.lowerExpr(expr.expression);
3595
+ if (receiver.type.kind === "union" && lowerer.armTag(receiver.type.unionId, UNDEFINED_T) >= 0 && kind === "array") {
3596
+ const present = lowerer.stripUndefinedArm(receiver.type);
3597
+ const helper = present.kind === "array"
3598
+ ? lowerer.narrowedArmHelper(receiver.type.unionId, present, locOf(expr.expression))
3599
+ : null;
3600
+ receiver = helper
3601
+ ? { kind: "call", callee: helper, args: [receiver], type: present, loc: locOf(expr.expression) }
3602
+ : lowerer.maybeNarrow(receiver, expr.expression);
3603
+ }
3391
3604
  // An island handle behind an array-typed .d.ts surface
3392
3605
  // (`parts().length` on a declared `string[]` return — arrays never
3393
3606
  // exit eagerly, so the value stays jsval): an engine property read,
@@ -3597,8 +3810,25 @@ export function lowerArrayLiteral(lowerer, expr, expected) {
3597
3810
  }
3598
3811
  lowerer.badType(expr, tsType);
3599
3812
  }
3600
- const type = mapped;
3813
+ let type = mapped;
3814
+ // Keep the array payload type fixed. Optional reads are stored through
3815
+ // arrayValueStore, which records UNDEFINED in the state byte while the
3816
+ // payload remains number/string/etc.; widening the array element here
3817
+ // would force boxed union storage and change aliases.
3601
3818
  const spreads = [];
3819
+ const holes = new Set();
3820
+ const runtimeOptionalElements = new Set();
3821
+ let needsStateBuild = false;
3822
+ for (let i = 0; i < expr.elements.length; i++) {
3823
+ const candidate = expr.elements[i];
3824
+ if (ts.isSpreadElement(candidate) || ts.isOmittedExpression(candidate))
3825
+ continue;
3826
+ const probe = probeLower(lowerer, candidate);
3827
+ if (probe && lowerer.runtimeOptionalWidening(probe.type, type.elem) !== null) {
3828
+ runtimeOptionalElements.add(i);
3829
+ needsStateBuild = true;
3830
+ }
3831
+ }
3602
3832
  const elems = expr.elements.map((el, i) => {
3603
3833
  if (ts.isSpreadElement(el)) {
3604
3834
  // `[...xs, b]`: xs must be an array of the literal's own element
@@ -3674,16 +3904,14 @@ export function lowerArrayLiteral(lowerer, expr, expected) {
3674
3904
  spreads.push(i);
3675
3905
  return src;
3676
3906
  }
3677
- // A HOLE (`[,]` — an elision): the slot materializes the undefined
3678
- // arm — reads, length, and JSON answer exactly Node (JSON.stringify
3679
- // prints null for holes AND for undefined elements). Iteration
3680
- // methods do not skip the position the way JS skips holes — the
3681
- // documented sparse-literal divergence.
3907
+ // A HOLE (`[,]` — an elision) remains absent. Ordinary reads still
3908
+ // answer undefined, while presence-sensitive methods and concat keep
3909
+ // the hole. The state-building path below advances length without
3910
+ // writing a value into this slot.
3682
3911
  if (ts.isOmittedExpression(el)) {
3683
- const hole = type.elem.kind === "union" ? lowerer.wrappedUndefined(type.elem, locOf(el)) : null;
3684
- if (hole)
3685
- return hole;
3686
- lowerer.unsupported("SC1090", el, "holes in array literals of this element type");
3912
+ needsStateBuild = true;
3913
+ holes.add(i);
3914
+ return { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc: locOf(el) };
3687
3915
  }
3688
3916
  // An ARRAY-LITERAL element under a UNION element slot whose own type
3689
3917
  // has no lift into the slot builds against the union's single array
@@ -3716,12 +3944,80 @@ export function lowerArrayLiteral(lowerer, expr, expected) {
3716
3944
  // Elements flow into the element slot like an assignment: union
3717
3945
  // elements wrap plain arm values (`[1, "a"]` as (number | string)[]),
3718
3946
  // holes reject inside lowerExpr.
3719
- const lowered = lowerer.lowerExprExpecting(el, type.elem);
3947
+ let lowered;
3948
+ const directOptionalCandidate = runtimeOptionalElements.has(i) ||
3949
+ ts.isElementAccessExpression(el) ||
3950
+ (ts.isCallExpression(el) && ts.isPropertyAccessExpression(el.expression) &&
3951
+ (el.expression.name.text === "pop" || el.expression.name.text === "shift"));
3952
+ if (directOptionalCandidate) {
3953
+ const raw = lowerer.lowerExpr(el);
3954
+ if (!typeEquals(raw.type, type.elem)) {
3955
+ needsStateBuild = true;
3956
+ return raw;
3957
+ }
3958
+ lowered = raw;
3959
+ }
3960
+ else {
3961
+ lowered = lowerer.lowerExprExpecting(el, type.elem);
3962
+ }
3720
3963
  if (!typeEquals(lowered.type, type.elem))
3721
3964
  lowerer.badType(el, lowerer.typeOf(el));
3722
3965
  return lowered;
3723
3966
  });
3724
- return { kind: "arrayLit", elems, ...(spreads.length > 0 ? { spreads } : {}), type, loc };
3967
+ if (!needsStateBuild) {
3968
+ return { kind: "arrayLit", elems, ...(spreads.length > 0 ? { spreads } : {}), type, loc };
3969
+ }
3970
+ const out = lowerer.declareHiddenLocal("%arrayLit", type);
3971
+ const outRef = varRef(out.id, type, loc);
3972
+ const body = [
3973
+ { kind: "varDecl", localId: out.id, init: { kind: "arrayLit", elems: [], type, loc }, loc },
3974
+ ];
3975
+ const spreadSet = new Set(spreads);
3976
+ const currentLength = () => ({
3977
+ kind: "arrIntrinsic",
3978
+ method: "length",
3979
+ receiver: outRef,
3980
+ args: [],
3981
+ type: F64,
3982
+ loc,
3983
+ });
3984
+ const nextLength = () => ({
3985
+ kind: "bin",
3986
+ op: "+",
3987
+ left: currentLength(),
3988
+ right: { kind: "numLit", value: 1, type: F64, loc },
3989
+ type: F64,
3990
+ loc,
3991
+ });
3992
+ for (let i = 0; i < elems.length; i++) {
3993
+ const el = elems[i];
3994
+ if (spreadSet.has(i)) {
3995
+ body.push({
3996
+ kind: "exprStmt",
3997
+ expr: { kind: "arrIntrinsic", method: "pushSpread", receiver: outRef, args: [el], type: F64, loc },
3998
+ loc,
3999
+ });
4000
+ continue;
4001
+ }
4002
+ if (holes.has(i)) {
4003
+ body.push({ kind: "arraySetLength", arr: outRef, length: nextLength(), loc });
4004
+ continue;
4005
+ }
4006
+ let value = el;
4007
+ if (!typeEquals(value.type, type.elem)) {
4008
+ const temp = lowerer.declareHiddenLocal("%arrayValue", value.type);
4009
+ body.push({ kind: "varDecl", localId: temp.id, init: value, loc });
4010
+ value = varRef(temp.id, temp.type, loc);
4011
+ }
4012
+ body.push(arrayValueStore(lowerer, outRef, currentLength(), value, type.elem, loc));
4013
+ }
4014
+ return {
4015
+ kind: "seqExpr",
4016
+ stmts: body,
4017
+ result: outRef,
4018
+ type,
4019
+ loc,
4020
+ };
3725
4021
  }
3726
4022
  /** `{ a: 1, b: "x" }` → recordLit. The record type comes from the
3727
4023
  * contextual type when tsc has one (annotated declarations, arguments,
@@ -4032,6 +4328,42 @@ function fenceAccessorSpreadSource(lowerer, prop, srcShape) {
4032
4328
  lowerer.unsupported("SC1090", prop, "object spread of sources carrying get/set accessor properties (Node invokes each getter once during the copy — the field-copy desugar cannot model the calls; bind the reads to consts first)");
4033
4329
  }
4034
4330
  }
4331
+ /** Convert an undefined-armed numeric value at an arithmetic use. Ordinary
4332
+ * reads preserve their tagged value; JavaScript's ToNumber(undefined) is NaN,
4333
+ * so compounds and binary arithmetic use this checked one-time extraction. */
4334
+ export function lowerOptionalNumber(lowerer, operand, loc, narrowedNode) {
4335
+ if (operand.type.kind !== "union" || lowerer.armTag(operand.type.unionId, UNDEFINED_T) < 0)
4336
+ return operand;
4337
+ const directNumber = lowerer.stripUndefinedArm(operand.type).kind === "f64";
4338
+ const checkerNumber = narrowedNode !== undefined && lowerer.mapTypeOf(lowerer.typeOf(narrowedNode))?.kind === "f64";
4339
+ if (!directNumber && (!checkerNumber || lowerer.armTag(operand.type.unionId, F64) < 0))
4340
+ return operand;
4341
+ const undefTag = lowerer.armTag(operand.type.unionId, UNDEFINED_T);
4342
+ const valueTag = lowerer.armTag(operand.type.unionId, F64);
4343
+ if (undefTag < 0 || valueTag < 0)
4344
+ return operand;
4345
+ const checked = directNumber ? null : lowerer.narrowedArmHelper(operand.type.unionId, F64, loc);
4346
+ if (!directNumber && !checked)
4347
+ return operand;
4348
+ let stable = operand;
4349
+ let prefix = [];
4350
+ if (!pureReemittable(operand)) {
4351
+ const tmp = lowerer.declareHiddenLocal("%arith", operand.type);
4352
+ stable = varRef(tmp.id, operand.type, loc);
4353
+ prefix = [{ kind: "varDecl", localId: tmp.id, init: operand, loc }];
4354
+ }
4355
+ const value = {
4356
+ kind: "ternary",
4357
+ cond: { kind: "unionIsTag", unionId: operand.type.unionId, tag: undefTag, negated: false, value: stable, type: BOOL, loc },
4358
+ then: { kind: "bin", op: "/", left: numLit(0, loc), right: numLit(0, loc), type: F64, loc },
4359
+ else_: checked
4360
+ ? { kind: "call", callee: checked, args: [stable], type: F64, loc }
4361
+ : { kind: "unionNarrow", unionId: operand.type.unionId, tag: valueTag, value: stable, type: F64, loc },
4362
+ type: F64,
4363
+ loc,
4364
+ };
4365
+ return prefix.length === 0 ? value : { kind: "seqExpr", stmts: prefix, result: value, type: F64, loc };
4366
+ }
4035
4367
  /** The JS trap-closure fallback for FUNCTION-VALUED object properties: a
4036
4368
  * lambda whose body fails to lower inside a JS object literal becomes a
4037
4369
  * closure of the field's exact func type whose body is the runtime fence —
@@ -4179,6 +4511,39 @@ function literalUnionArmOf(lowerer, expr, tsType, recordArms) {
4179
4511
  const [only] = candidates;
4180
4512
  return recordArms.find((a) => a.shapeId === only) ?? null;
4181
4513
  }
4514
+ /** Object spread treats nullish sources as empty. Preserve evaluation once,
4515
+ * then turn `Record<K, V> | undefined` into either that record or a fresh
4516
+ * empty record of the same represented shape. */
4517
+ function lowerOptionalIndexSpreadSource(lowerer, source, loc) {
4518
+ if (source.type.kind !== "union")
4519
+ return null;
4520
+ const def = lowerer.unions.get(source.type.unionId);
4521
+ if (!def || def.arms.length !== 2)
4522
+ return null;
4523
+ const undefinedTag = def.arms.findIndex((arm) => arm.kind === "undefinedT");
4524
+ const recordTag = def.arms.findIndex((arm) => arm.kind === "record");
4525
+ if (undefinedTag < 0 || recordTag < 0)
4526
+ return null;
4527
+ const recordType = def.arms[recordTag];
4528
+ if (recordType.kind !== "record")
4529
+ return null;
4530
+ const prefix = [];
4531
+ let stable = source;
4532
+ if (!pureReemittable(source)) {
4533
+ const local = lowerer.declareHiddenLocal("%spread", source.type);
4534
+ prefix.push({ kind: "varDecl", localId: local.id, init: source, loc });
4535
+ stable = varRef(local.id, source.type, loc);
4536
+ }
4537
+ const selected = {
4538
+ kind: "ternary",
4539
+ cond: { kind: "unionIsTag", unionId: source.type.unionId, tag: undefinedTag, negated: false, value: stable, type: BOOL, loc },
4540
+ then: { kind: "recordLit", fields: [], type: recordType, loc },
4541
+ else_: { kind: "unionNarrow", unionId: source.type.unionId, tag: recordTag, value: stable, type: recordType, loc },
4542
+ type: recordType,
4543
+ loc,
4544
+ };
4545
+ return prefix.length === 0 ? selected : { kind: "seqExpr", stmts: prefix, result: selected, type: recordType, loc };
4546
+ }
4182
4547
  export function lowerObjectLiteral(lowerer, expr) {
4183
4548
  const loc = locOf(expr);
4184
4549
  // The RUNTIME-KEYED literal (JS): a computed key that doesn't fold to a
@@ -4464,6 +4829,23 @@ export function lowerObjectLiteral(lowerer, expr) {
4464
4829
  }
4465
4830
  }
4466
4831
  const fieldTypes = new Map(shape.fields.map((f) => [f.name, f.type]));
4832
+ // File-scope JavaScript object bindings live in the checked-dynamic tree
4833
+ // to preserve open writes and identity. Build an optional-field literal
4834
+ // from its actually written keys so Object.hasOwn can distinguish an
4835
+ // omitted property from a present property whose value is undefined.
4836
+ const topLevelJsDecl = ts.isVariableDeclaration(expr.parent) &&
4837
+ expr.parent.initializer === expr &&
4838
+ ts.isVariableStatement(expr.parent.parent.parent) &&
4839
+ ts.isSourceFile(expr.parent.parent.parent.parent);
4840
+ const assignedProperties = expr.properties.filter((prop) => ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop));
4841
+ if (topLevelJsDecl && isJsSourceFile(expr.getSourceFile()) &&
4842
+ assignedProperties.length === expr.properties.length) {
4843
+ const provided = new Set(assignedProperties.map((prop) => propNameText(lowerer, prop.name)));
4844
+ const omittedOptional = shape.fields.some((field) => !provided.has(field.name) && field.type.kind === "union" &&
4845
+ lowerer.armTag(field.type.unionId, UNDEFINED_T) >= 0);
4846
+ if (omittedOptional)
4847
+ return lowerDynObjectLiteral(lowerer, expr);
4848
+ }
4467
4849
  // The clone probe may lower the leading spread before declining (a
4468
4850
  // static `as` cast can make the checker-visible shape match while its
4469
4851
  // erased IR value keeps a wider shape). Reuse that exact value in the
@@ -4575,7 +4957,8 @@ export function lowerObjectLiteral(lowerer, expr) {
4575
4957
  });
4576
4958
  continue;
4577
4959
  }
4578
- const src = lowerer.lowerExpr(prop.expression);
4960
+ let src = lowerer.lowerExpr(prop.expression);
4961
+ src = lowerOptionalIndexSpreadSource(lowerer, src, locOf(prop)) ?? src;
4579
4962
  if (src.type.kind !== "record") {
4580
4963
  lowerer.unsupported("SC1090", prop, `object spread of '${lowerer.fmt(src.type)}' sources into an index-signature shape (only index-signature records merge — ${NARROW_FIRST})`);
4581
4964
  }
@@ -4658,7 +5041,7 @@ export function lowerObjectLiteral(lowerer, expr) {
4658
5041
  }
4659
5042
  const shapeMismatch = (node) => {
4660
5043
  const own = lowerer.mapTypeOf(lowerer.typeOf(expr));
4661
- lowerer.pushDiag(recordShapeMismatchDiag(lowerer.fmt(type), own ? lowerer.fmt(own) : lowerer.checker.typeToString(lowerer.typeOf(expr)), locOf(node), own?.kind === "record"
5044
+ lowerer.pushDiag(recordShapeMismatchDiag(lowerer.fmt(type), own ? lowerer.fmt(own) : lowerer.checker.typeToString(lowerer.typeOf(expr)), locOf(node), own?.kind === "record" && type.kind === "record"
4662
5045
  ? (lowerer.describeRecordWidthBlocker(own.shapeId, type.shapeId) ?? undefined)
4663
5046
  : undefined));
4664
5047
  throw new PoisonError();
@@ -5035,7 +5418,7 @@ export function lowerObjectLiteral(lowerer, expr) {
5035
5418
  continue;
5036
5419
  }
5037
5420
  const name = propNameText(lowerer, prop.name); // key-form-checked above
5038
- const fieldType = fieldTypes.get(name);
5421
+ let fieldType = fieldTypes.get(name);
5039
5422
  // An undeclared name against an index-signature shape is an OVERFLOW
5040
5423
  // entry (tsc typechecked it against the signature's value type);
5041
5424
  // against a plain shape it is the width mismatch it always was —
@@ -5155,6 +5538,14 @@ export function lowerObjectLiteral(lowerer, expr) {
5155
5538
  fields.push({ name, value: slotted, overflow: true });
5156
5539
  continue;
5157
5540
  }
5541
+ const promotedField = fieldType ? lowerer.runtimeOptionalWidening(value.type, fieldType) : null;
5542
+ if (promotedField && fieldType) {
5543
+ type = lowerer.runtimeOptionalRecordField(type, name, promotedField);
5544
+ if (type.kind === "record")
5545
+ shape = lowerer.shapes.get(type.shapeId);
5546
+ fieldTypes.set(name, promotedField);
5547
+ fieldType = promotedField;
5548
+ }
5158
5549
  value = lowerer.coerceInto(valueNode, value, fieldType); // union-typed fields wrap arm values
5159
5550
  if (!typeEquals(value.type, fieldType))
5160
5551
  lowerer.badType(valueNode, lowerer.typeOf(valueNode));
@@ -5592,7 +5983,16 @@ export function lowerElementAccess(lowerer, expr) {
5592
5983
  // result; any invalid index traps (the array discipline; JS would
5593
5984
  // read undefined — divergence 4's policy).
5594
5985
  if (receiverIr?.kind === "bytes") {
5595
- const recv = lowerer.lowerExpr(expr.expression);
5986
+ let recv = lowerer.lowerExpr(expr.expression);
5987
+ if (recv.type.kind === "union" && lowerer.armTag(recv.type.unionId, UNDEFINED_T) >= 0) {
5988
+ const present = lowerer.stripUndefinedArm(recv.type);
5989
+ const helper = present.kind === "bytes"
5990
+ ? lowerer.narrowedArmHelper(recv.type.unionId, present, locOf(expr.expression))
5991
+ : null;
5992
+ recv = helper
5993
+ ? { kind: "call", callee: helper, args: [recv], type: present, loc: locOf(expr.expression) }
5994
+ : lowerer.maybeNarrow(recv, expr.expression);
5995
+ }
5596
5996
  // A typed-array .d.ts surface whose VALUE is an island handle
5597
5997
  // (`data()[0]` on a declared Uint8Array return — call results only
5598
5998
  // exit primitives eagerly): the engine element read, exiting at the
@@ -5612,7 +6012,7 @@ export function lowerElementAccess(lowerer, expr) {
5612
6012
  if (receiverIr?.kind === "record") {
5613
6013
  const shape = lowerer.shapes.get(receiverIr.shapeId);
5614
6014
  if (shape?.tuple) {
5615
- const obj = lowerer.lowerExpr(expr.expression);
6015
+ let obj = lowerer.lowerExpr(expr.expression);
5616
6016
  // A tuple-typed .d.ts surface whose VALUE is an island handle
5617
6017
  // (`pair()[0]` on a declared `[number, string]` return — the
5618
6018
  // anonymous tuple type has no npm symbol, so the checker maps it
@@ -5621,22 +6021,38 @@ export function lowerElementAccess(lowerer, expr) {
5621
6021
  // like the array path.
5622
6022
  if (obj.type.kind === "jsval")
5623
6023
  return islandElementRead(lowerer, expr, obj);
6024
+ if (obj.type.kind === "union" && lowerer.armTag(obj.type.unionId, UNDEFINED_T) >= 0) {
6025
+ // The checker sees the outer `arrays[i]` as a tuple when
6026
+ // noUncheckedIndexedAccess is disabled, but the lowered read is
6027
+ // still tuple | undefined. A nested tuple field must validate the
6028
+ // outer tag before recordGet; an unchecked unionNarrow would let a
6029
+ // missing outer element reach the record backend as a bad pointer.
6030
+ const present = lowerer.stripUndefinedArm(obj.type);
6031
+ const helper = present.kind === "record"
6032
+ ? lowerer.narrowedArmHelper(obj.type.unionId, present, locOf(expr.expression))
6033
+ : null;
6034
+ obj = helper
6035
+ ? { kind: "call", callee: helper, args: [obj], type: present, loc: locOf(expr.expression) }
6036
+ : lowerer.maybeNarrow(obj, expr.expression);
6037
+ }
5624
6038
  const idx = tupleLiteralIndex(expr.argumentExpression);
5625
6039
  if (idx === null) {
5626
6040
  lowerer.unsupported("SC1090", expr.argumentExpression, "tuple indexing with a non-literal index (tuples are fixed-shape — use t[0], t[1], ...)");
5627
6041
  }
5628
- const fieldType = shape.fields.find((f) => f.name === String(idx))?.type;
6042
+ const shapeId = obj.type.kind === "record" ? obj.type.shapeId : receiverIr.shapeId;
6043
+ const actualShape = lowerer.shapes.get(shapeId) ?? shape;
6044
+ const fieldType = actualShape.fields.find((f) => f.name === String(idx))?.type;
5629
6045
  if (fieldType === undefined)
5630
6046
  lowerer.badType(expr, lowerer.typeOf(expr)); // OOB smuggled past tsc
5631
6047
  const get = {
5632
6048
  kind: "recordGet",
5633
6049
  obj,
5634
- shapeId: receiverIr.shapeId,
6050
+ shapeId,
5635
6051
  field: String(idx),
5636
6052
  type: fieldType,
5637
6053
  loc: locOf(expr),
5638
6054
  };
5639
- return lowerer.maybeNarrow(get, expr);
6055
+ return lowerer.isRuntimeOptionalField(shapeId, String(idx)) ? get : lowerer.maybeNarrow(get, expr);
5640
6056
  }
5641
6057
  if (shape && !shape.tuple) {
5642
6058
  // A record-typed .d.ts surface whose VALUE is an island handle
@@ -5751,6 +6167,65 @@ export function lowerElementAccess(lowerer, expr) {
5751
6167
  lowerer.unsupported("SC1090", expr, "element access on non-array values");
5752
6168
  }
5753
6169
  let arr = lowerer.lowerExpr(expr.expression);
6170
+ const runtimeOptionalSource = lowerer.runtimeOptionalSourceValue(expr.expression, arr);
6171
+ if (runtimeOptionalSource?.kind === "varRef" &&
6172
+ runtimeOptionalSource.type.kind === "union" &&
6173
+ arr.type.kind === "array") {
6174
+ // lowerer.lowerElementAccess has already recognized a captured
6175
+ // array | undefined receiver and supplied its checked extraction as
6176
+ // the expression override. Rebuild that check here so a missing
6177
+ // receiver throws the member-access error Node exposes, rather than
6178
+ // the generic target-union narrowing error. The stored receiver is a
6179
+ // varRef, so both the tag test and extraction read the same value
6180
+ // without repeating an effect.
6181
+ const stored = runtimeOptionalSource;
6182
+ if (stored.type.kind !== "union")
6183
+ throw new InternalCompilerError("runtime-optional array source lost its union storage");
6184
+ const unionId = stored.type.unionId;
6185
+ const undefTag = lowerer.armTag(unionId, UNDEFINED_T);
6186
+ const valueTag = lowerer.armTag(unionId, arr.type);
6187
+ const property = runtimeOptionalElementKey(expr.argumentExpression);
6188
+ if (undefTag >= 0 && valueTag >= 0 && property !== null) {
6189
+ arr = {
6190
+ kind: "ternary",
6191
+ cond: {
6192
+ kind: "unionIsTag",
6193
+ unionId,
6194
+ tag: undefTag,
6195
+ negated: false,
6196
+ value: stored,
6197
+ type: BOOL,
6198
+ loc: locOf(expr.expression),
6199
+ },
6200
+ then: nodeThrowExpr(1, "", `Cannot read properties of undefined (reading '${property}')`, arr.type, locOf(expr.expression)),
6201
+ else_: {
6202
+ kind: "unionNarrow",
6203
+ unionId,
6204
+ tag: valueTag,
6205
+ value: stored,
6206
+ type: arr.type,
6207
+ loc: locOf(expr.expression),
6208
+ },
6209
+ type: arr.type,
6210
+ loc: locOf(expr.expression),
6211
+ };
6212
+ }
6213
+ }
6214
+ if (arr.type.kind === "union" &&
6215
+ lowerer.armTag(arr.type.unionId, UNDEFINED_T) >= 0 &&
6216
+ receiverIr.kind === "array") {
6217
+ // As with tuple receivers above, an unchecked outer read can be
6218
+ // undefined even when the checker reports an array. Extract the
6219
+ // present array arm with a runtime tag check before arrayGet or the
6220
+ // safe indexed-read helper runs.
6221
+ const present = lowerer.stripUndefinedArm(arr.type);
6222
+ const helper = present.kind === "array"
6223
+ ? lowerer.narrowedArmHelper(arr.type.unionId, present, locOf(expr.expression))
6224
+ : null;
6225
+ arr = helper
6226
+ ? { kind: "call", callee: helper, args: [arr], type: present, loc: locOf(expr.expression) }
6227
+ : lowerer.maybeNarrow(arr, expr.expression);
6228
+ }
5754
6229
  // The checker sees an array but the VALUE is an island handle (an
5755
6230
  // array-typed .d.ts surface: `parts()[0]` on a declared `string[]`
5756
6231
  // return, `issue.path[0]` on a declared member — arrays never exit
@@ -5782,12 +6257,22 @@ export function lowerElementAccess(lowerer, expr) {
5782
6257
  // signature where the .d.ts says Function[], which would flatten the
5783
6258
  // element to a zero-parameter closure).
5784
6259
  const elemT = arr.type.kind === "array" ? arr.type.elem : receiverIr.elem;
5785
- // OOB-SAFE reads over fresh/probe array results. These expressions are
5786
- // commonly used to ask for a first/last match (`filter(...).sort(...)[0]`,
5787
- // `slice(-1)[0]`); an empty result is ordinary undefined, not a bounds
5788
- // failure. Downstream lowering keeps the explicit optional union, so an
5789
- // unchecked consumer still cannot place it into a plain element slot.
5790
- if (arr.type.kind === "array" && isFreshArrayProbe(expr.expression)) {
6260
+ // Every ordinary array read is a JavaScript property read: an invalid
6261
+ // index answers undefined, even when the checker was built without
6262
+ // noUncheckedIndexedAccess. Keep the explicit optional union in the IR
6263
+ // so it can flow through templates, locals, calls, returns, operators,
6264
+ // and narrowing. The helper performs one length/proof check and then
6265
+ // uses the dense getter for the proven-present path, so this does not
6266
+ // change the storage representation or the low-level getter itself.
6267
+ if (arr.type.kind === "array") {
6268
+ // A non-null assertion is the explicit proven-present form. Preserve
6269
+ // the dense getter's established bounds trap here; turning `xs[i]!`
6270
+ // into an optional union would change library ABI checks and the
6271
+ // existing catchable RangeError contract for an out-of-bounds assert.
6272
+ const provenPresent = ts.isNonNullExpression(expr.parent) && expr.parent.expression === expr;
6273
+ if (provenPresent) {
6274
+ return lowerer.maybeNarrow({ kind: "arrayGet", arr, index, type: elemT, loc: locOf(expr) }, expr);
6275
+ }
5791
6276
  const safe = lowerSafeIndexRead(lowerer, arr, index, locOf(expr));
5792
6277
  if (safe)
5793
6278
  return safe;
@@ -5807,16 +6292,6 @@ export function lowerElementAccess(lowerer, expr) {
5807
6292
  // narrows literal-index element accesses).
5808
6293
  return lowerer.maybeNarrow({ kind: "arrayGet", arr, index, type: elemT, loc: locOf(expr) }, expr);
5809
6294
  }
5810
- function isFreshArrayProbe(node) {
5811
- let current = node;
5812
- while (ts.isParenthesizedExpression(current))
5813
- current = current.expression;
5814
- return (ts.isCallExpression(current) &&
5815
- ts.isPropertyAccessExpression(current.expression) &&
5816
- (current.expression.name.text === "filter" ||
5817
- current.expression.name.text === "slice" ||
5818
- current.expression.name.text === "sort"));
5819
- }
5820
6295
  /** `o[k]` where the RECEIVER is an island value — a jsval-mapped checker
5821
6296
  * type, or an array/tuple-typed .d.ts surface whose lowered value is a
5822
6297
  * handle. The key marshals in (any JS key kind — the engine does its
@@ -5883,8 +6358,7 @@ export function lowerRecordKeyRead(lowerer, expr, shapeId, shape, includeUndefin
5883
6358
  // Number AND checked-dynamic keys ride the JS-exact formatter —
5884
6359
  // property keys ARE strings (o[k] is o[String(k)] in JS), and a dyn
5885
6360
  // key (agent.sockets[agent.getName(...)]) stringifies the same way.
5886
- if (dk.type.kind === "f64" || dk.type.kind === "dyn")
5887
- dk = lowerer.ensureString(dk, keyNode);
6361
+ dk = lowerRecordPropertyKey(lowerer, dk, keyNode);
5888
6362
  if (dk.type.kind !== "string") {
5889
6363
  lowerer.unsupported("SC1090", keyNode, "indexing records with non-string or non-number keys");
5890
6364
  }
@@ -5914,8 +6388,7 @@ export function lowerRecordKeyRead(lowerer, expr, shapeId, shape, includeUndefin
5914
6388
  : lowerer.lowerExpr(keyNode);
5915
6389
  // Runtime NUMBER keys ride the JS-exact formatter (o[n] is o[String(n)]
5916
6390
  // in JS — the number-keyed-signature access path).
5917
- if (key.type.kind === "f64")
5918
- key = lowerer.ensureString(key, keyNode);
6391
+ key = lowerRecordPropertyKey(lowerer, key, keyNode);
5919
6392
  if (key.type.kind !== "string") {
5920
6393
  lowerer.unsupported("SC1090", keyNode, "indexing records with non-string or non-number keys");
5921
6394
  }
@@ -5958,6 +6431,16 @@ export function lowerRecordKeyRead(lowerer, expr, shapeId, shape, includeUndefin
5958
6431
  };
5959
6432
  return includeUndefined ? read : lowerer.maybeNarrow(read, expr);
5960
6433
  }
6434
+ /** ToPropertyKey for represented record keys. Missing array-derived string
6435
+ * values remain real undefined values, whose JavaScript property spelling
6436
+ * is the string "undefined". */
6437
+ function lowerRecordPropertyKey(lowerer, key, node) {
6438
+ if (key.type.kind === "f64" || key.type.kind === "bool" || key.type.kind === "dyn" ||
6439
+ lowerer.runtimeOptionalWidening(key.type, STRING) !== null) {
6440
+ return lowerer.ensureString(key, node);
6441
+ }
6442
+ return key;
6443
+ }
5961
6444
  /** A keyed read used only to observe absence (`if (map[k])`, or the left
5962
6445
  * side of `||`/`??`). JavaScript answers undefined for missing record keys
5963
6446
  * and array indices even when noUncheckedIndexedAccess is disabled. Keep
@@ -6229,8 +6712,7 @@ export function lowerElementWrite(lowerer, expr) {
6229
6712
  let key = litKey !== null
6230
6713
  ? { kind: "strLit", value: litKey, type: STRING, loc: locOf(target.argumentExpression) }
6231
6714
  : lowerer.lowerExpr(target.argumentExpression);
6232
- if (key.type.kind === "f64")
6233
- key = lowerer.ensureString(key, target.argumentExpression);
6715
+ key = lowerRecordPropertyKey(lowerer, key, target.argumentExpression);
6234
6716
  if (key.type.kind !== "string") {
6235
6717
  lowerer.unsupported("SC1090", target.argumentExpression, "indexing records with non-string or non-number keys");
6236
6718
  }
@@ -6308,11 +6790,7 @@ export function lowerElementWrite(lowerer, expr) {
6308
6790
  : lowerer.lowerExpr(target.argumentExpression);
6309
6791
  // Runtime number/boolean/dyn keys stringify — ToPropertyKey,
6310
6792
  // exactly the index-signature path's rule.
6311
- if (key.type.kind === "f64")
6312
- key = lowerer.ensureString(key, target.argumentExpression);
6313
- if (key.type.kind === "bool" || key.type.kind === "dyn") {
6314
- key = { kind: "toString", operand: key, type: STRING, loc: locOf(target.argumentExpression) };
6315
- }
6793
+ key = lowerRecordPropertyKey(lowerer, key, target.argumentExpression);
6316
6794
  if (key.type.kind !== "string") {
6317
6795
  lowerer.unsupported("SC1090", target.argumentExpression, "indexing records with non-string or non-number keys");
6318
6796
  }
@@ -6341,8 +6819,7 @@ export function lowerElementWrite(lowerer, expr) {
6341
6819
  : lowerer.lowerExpr(target.argumentExpression);
6342
6820
  // Runtime NUMBER keys canonicalize through the JS-exact formatter
6343
6821
  // (o[n] = v writes o[String(n)] — the number-keyed-signature path).
6344
- if (key.type.kind === "f64")
6345
- key = lowerer.ensureString(key, target.argumentExpression);
6822
+ key = lowerRecordPropertyKey(lowerer, key, target.argumentExpression);
6346
6823
  if (key.type.kind !== "string") {
6347
6824
  lowerer.unsupported("SC1090", target.argumentExpression, "indexing records with non-string or non-number keys");
6348
6825
  }
@@ -6376,11 +6853,7 @@ export function lowerElementWrite(lowerer, expr) {
6376
6853
  let key = litKey !== null
6377
6854
  ? { kind: "strLit", value: litKey, type: STRING, loc: locOf(target.argumentExpression) }
6378
6855
  : lowerer.lowerExpr(target.argumentExpression);
6379
- if (key.type.kind === "f64")
6380
- key = lowerer.ensureString(key, target.argumentExpression);
6381
- if (key.type.kind === "bool" || key.type.kind === "dyn") {
6382
- key = { kind: "toString", operand: key, type: STRING, loc: locOf(target.argumentExpression) };
6383
- }
6856
+ key = lowerRecordPropertyKey(lowerer, key, target.argumentExpression);
6384
6857
  if (key.type.kind !== "string") {
6385
6858
  lowerer.unsupported("SC1090", target.argumentExpression, "indexing checked-dynamic values with non-string or non-number keys");
6386
6859
  }
@@ -6394,7 +6867,7 @@ export function lowerElementWrite(lowerer, expr) {
6394
6867
  if (receiverIr?.kind !== "array") {
6395
6868
  lowerer.unsupported("SC1090", target, "assignment to non-array elements");
6396
6869
  }
6397
- const arr = lowerer.lowerExpr(target.expression);
6870
+ let arr = lowerer.lowerExpr(target.expression);
6398
6871
  // The write twin of the island element read: an array-typed .d.ts
6399
6872
  // surface whose VALUE is a handle takes the engine keyed write, never
6400
6873
  // a static arraySet over a jsval.
@@ -6404,17 +6877,29 @@ export function lowerElementWrite(lowerer, expr) {
6404
6877
  const loc = locOf(expr);
6405
6878
  return { kind: "exprStmt", expr: { kind: "jsOp", op: "setIdx", args: [arr, key, value], type: VOID, loc }, loc };
6406
6879
  }
6880
+ if (arr.type.kind === "union" &&
6881
+ lowerer.armTag(arr.type.unionId, UNDEFINED_T) >= 0 &&
6882
+ receiverIr.kind === "array") {
6883
+ const present = lowerer.stripUndefinedArm(arr.type);
6884
+ const helper = present.kind === "array"
6885
+ ? lowerer.narrowedArmHelper(arr.type.unionId, present, locOf(target.expression))
6886
+ : null;
6887
+ if (helper) {
6888
+ arr = { kind: "call", callee: helper, args: [arr], type: present, loc: locOf(target.expression) };
6889
+ }
6890
+ }
6891
+ if (arr.type.kind !== "array") {
6892
+ lowerer.unsupported("SC1090", target.expression, "assignment through a possibly missing nested array receiver");
6893
+ }
6407
6894
  const index = lowerer.lowerExpr(target.argumentExpression);
6408
6895
  if (index.type.kind !== "f64") {
6409
6896
  lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys");
6410
6897
  }
6411
- // The value flows into the element slot like an assignment: union
6412
- // elements wrap plain arm values.
6413
- const value = lowerer.lowerExprExpecting(expr.right, receiverIr.elem);
6414
- if (!typeEquals(value.type, receiverIr.elem)) {
6415
- lowerer.badType(expr.right, lowerer.typeOf(expr.right));
6416
- }
6417
- return { kind: "arraySet", arr, index, value, loc: locOf(expr) };
6898
+ // Optional array reads carry the source array's payload type plus an
6899
+ // undefined arm. arrayValueStore turns that arm into the runtime's
6900
+ // UNDEFINED state while keeping number[]/string[] payload storage scalar.
6901
+ const value = lowerer.lowerExpr(expr.right);
6902
+ return arrayValueStore(lowerer, arr, index, value, receiverIr.elem, locOf(expr));
6418
6903
  }
6419
6904
  export function ensureString(lowerer, e, node) {
6420
6905
  if (e.type.kind === "string")
@@ -6717,10 +7202,11 @@ export function lowerPrefixUnary(lowerer, expr) {
6717
7202
  const loc = locOf(expr);
6718
7203
  switch (expr.operator) {
6719
7204
  case ts.SyntaxKind.MinusToken: {
6720
- const operand = lowerer.lowerExpr(expr.operand);
6721
- if (operand.type.kind === "jsval") {
6722
- return { kind: "jsOp", op: "neg", args: [operand], type: JSVAL, loc };
7205
+ const raw = lowerer.lowerExpr(expr.operand);
7206
+ if (raw.type.kind === "jsval") {
7207
+ return { kind: "jsOp", op: "neg", args: [raw], type: JSVAL, loc };
6723
7208
  }
7209
+ const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand);
6724
7210
  if (operand.type.kind !== "f64")
6725
7211
  lowerer.unsupported("SC1043", expr);
6726
7212
  if (operand.kind === "numLit")
@@ -6728,16 +7214,17 @@ export function lowerPrefixUnary(lowerer, expr) {
6728
7214
  return { kind: "unary", op: "-", operand, type: F64, loc };
6729
7215
  }
6730
7216
  case ts.SyntaxKind.PlusToken: {
6731
- const operand = lowerer.lowerExpr(expr.operand);
7217
+ const raw = lowerer.lowerExpr(expr.operand);
6732
7218
  // Unary + is ToNumber; on an already-number operand it's identity,
6733
7219
  // and a STRING operand runs the runtime's ECMA-exact StringToNumber
6734
7220
  // (num.fromString — Number(aString)'s lowering, scr_string.c).
6735
- if (operand.type.kind === "jsval") {
6736
- return { kind: "jsOp", op: "plus", args: [operand], type: JSVAL, loc };
7221
+ if (raw.type.kind === "jsval") {
7222
+ return { kind: "jsOp", op: "plus", args: [raw], type: JSVAL, loc };
6737
7223
  }
6738
- if (operand.type.kind === "string") {
6739
- return { kind: "libCall", fn: "num.fromString", args: [operand], type: F64, loc };
7224
+ if (raw.type.kind === "string") {
7225
+ return { kind: "libCall", fn: "num.fromString", args: [raw], type: F64, loc };
6740
7226
  }
7227
+ const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand);
6741
7228
  if (operand.type.kind !== "f64")
6742
7229
  lowerer.unsupported("SC1043", expr);
6743
7230
  return operand;
@@ -6749,7 +7236,7 @@ export function lowerPrefixUnary(lowerer, expr) {
6749
7236
  }
6750
7237
  case ts.SyntaxKind.TildeToken: {
6751
7238
  // `~x`: ToInt32, complement, back to f64 (JS-exact, incl. NaN → -1).
6752
- const operand = lowerer.lowerExpr(expr.operand);
7239
+ const operand = lowerOptionalNumber(lowerer, lowerer.lowerExpr(expr.operand), loc, expr.operand);
6753
7240
  if (operand.type.kind !== "f64")
6754
7241
  lowerer.unsupported("SC1043", expr);
6755
7242
  return { kind: "unary", op: "~", operand, type: F64, loc };
@@ -6815,9 +7302,9 @@ function lowerIncDec(lowerer, expr, prefix) {
6815
7302
  lowerer.unsupported("SC1043", expr);
6816
7303
  return { kind: "incDec", op, prefix, localId: target.id, type: F64, loc };
6817
7304
  }
6818
- /** Whether a lowered statement can live inside a seqExpr — the
6819
- * validator's straight-line set (writes and effect expressions only; the
6820
- * C emission point is mid-expression, so no control flow, no jumps).
7305
+ /** Whether a lowered statement can live inside a seqExpr. These statements
7306
+ * do not leave the expression through a jump; the native emitters can place
7307
+ * their writes and local state branches before evaluating the final result.
6821
7308
  * Blocks check their bodies recursively. */
6822
7309
  function seqExprSafeStmt(s) {
6823
7310
  switch (s.kind) {
@@ -6828,8 +7315,13 @@ function seqExprSafeStmt(s) {
6828
7315
  case "recordSet":
6829
7316
  case "recordKeySet":
6830
7317
  case "arraySet":
7318
+ case "arraySetLength":
6831
7319
  case "bytesSet":
7320
+ case "arraySetUndefined":
7321
+ case "arrayDelete":
6832
7322
  return true;
7323
+ case "if":
7324
+ return s.then.every(seqExprSafeStmt) && (s.else_?.every(seqExprSafeStmt) ?? true);
6833
7325
  case "block":
6834
7326
  return s.body.every(seqExprSafeStmt);
6835
7327
  default:
@@ -7335,12 +7827,118 @@ export function lowerBinary(lowerer, expr) {
7335
7827
  type, loc,
7336
7828
  };
7337
7829
  }
7338
- const bothNum = left.type.kind === "f64" && right.type.kind === "f64";
7830
+ // An unchecked array read keeps its undefined arm in the IR even when
7831
+ // the checker typed the binding as `string`. An optional string is not
7832
+ // itself a string operand for `+`: `undefined + 1` and
7833
+ // `undefined + undefined` are NaN, while a present string concatenates.
7834
+ // Keep this predicate exact so a nullable string does not silently take
7835
+ // the string path either.
7836
+ const optionalStringType = (type) => {
7837
+ if (type.kind !== "union")
7838
+ return false;
7839
+ const arms = lowerer.unions.get(type.unionId)?.arms;
7840
+ return !!arms && arms.length === 2 &&
7841
+ arms.some((arm) => arm.kind === "string") &&
7842
+ arms.some((arm) => arm.kind === "undefinedT");
7843
+ };
7844
+ const arithmeticNumber = (operand, node) => lowerOptionalNumber(lowerer, operand, loc, node);
7845
+ const arithmeticLeft = strictEquality ? left : arithmeticNumber(left, expr.left);
7846
+ const arithmeticRight = strictEquality ? right : arithmeticNumber(right, expr.right);
7847
+ const plainBothNum = left.type.kind === "f64" && right.type.kind === "f64";
7848
+ const bothNum = arithmeticLeft.type.kind === "f64" && arithmeticRight.type.kind === "f64";
7339
7849
  const bothStr = left.type.kind === "string" && right.type.kind === "string";
7850
+ const lowerOptionalStringPlus = () => {
7851
+ const leftOptional = optionalStringType(left.type);
7852
+ const rightOptional = optionalStringType(right.type);
7853
+ if (!leftOptional && !rightOptional)
7854
+ return null;
7855
+ const allowed = (type) => type.kind === "f64" || type.kind === "string" || optionalStringType(type);
7856
+ if (!allowed(left.type) || !allowed(right.type))
7857
+ return null;
7858
+ // The tag tests and the chosen concatenation branch must see the same
7859
+ // operand values. This matters for `take()[0] + 1` and for two reads
7860
+ // with observable getters/calls.
7861
+ const prefix = [];
7862
+ const stable = (operand) => {
7863
+ if (pureReemittable(operand) || droppableStatic(operand))
7864
+ return operand;
7865
+ const tmp = lowerer.declareHiddenLocal("%plus", operand.type);
7866
+ prefix.push({ kind: "varDecl", localId: tmp.id, init: operand, loc });
7867
+ return varRef(tmp.id, operand.type, loc);
7868
+ };
7869
+ const stableLeft = stable(left);
7870
+ const stableRight = stable(right);
7871
+ const leftString = lowerer.ensureString(stableLeft, expr.left);
7872
+ const rightString = lowerer.ensureString(stableRight, expr.right);
7873
+ const concat = { kind: "strConcat", left: leftString, right: rightString, type: STRING, loc };
7874
+ const nan = { kind: "bin", op: "/", left: numLit(0, loc), right: numLit(0, loc), type: F64, loc };
7875
+ const resultT = { kind: "union", unionId: lowerer.unions.intern([F64, STRING]) };
7876
+ lowerer.runtimeOptionalArithmeticTypes.set(expr, resultT);
7877
+ const numberTag = lowerer.armTag(resultT.unionId, F64);
7878
+ const stringTag = lowerer.armTag(resultT.unionId, STRING);
7879
+ if (numberTag < 0 || stringTag < 0)
7880
+ throw new InternalCompilerError("lowerer bug: optional string plus result union");
7881
+ const number = () => ({
7882
+ kind: "unionWrap", unionId: resultT.unionId, tag: numberTag, value: nan, type: resultT, loc,
7883
+ });
7884
+ const string = () => ({
7885
+ kind: "unionWrap", unionId: resultT.unionId, tag: stringTag, value: concat, type: resultT, loc,
7886
+ });
7887
+ const undefinedTest = (operand) => ({
7888
+ kind: "unionIsTag",
7889
+ unionId: operand.type.kind === "union" ? operand.type.unionId : "",
7890
+ tag: operand.type.kind === "union" ? lowerer.armTag(operand.type.unionId, UNDEFINED_T) : -1,
7891
+ negated: false,
7892
+ value: operand,
7893
+ type: BOOL,
7894
+ loc,
7895
+ });
7896
+ let result;
7897
+ if (leftOptional && rightOptional) {
7898
+ const rightMissing = undefinedTest(stableRight);
7899
+ const bothMissing = {
7900
+ kind: "ternary",
7901
+ cond: rightMissing,
7902
+ then: number(),
7903
+ else_: string(),
7904
+ type: resultT,
7905
+ loc,
7906
+ };
7907
+ result = {
7908
+ kind: "ternary",
7909
+ cond: undefinedTest(stableLeft),
7910
+ then: bothMissing,
7911
+ else_: string(),
7912
+ type: resultT,
7913
+ loc,
7914
+ };
7915
+ }
7916
+ else if (leftOptional) {
7917
+ result = {
7918
+ kind: "ternary",
7919
+ cond: undefinedTest(stableLeft),
7920
+ then: number(),
7921
+ else_: string(),
7922
+ type: resultT,
7923
+ loc,
7924
+ };
7925
+ }
7926
+ else {
7927
+ result = {
7928
+ kind: "ternary",
7929
+ cond: undefinedTest(stableRight),
7930
+ then: number(),
7931
+ else_: string(),
7932
+ type: resultT,
7933
+ loc,
7934
+ };
7935
+ }
7936
+ return prefix.length === 0 ? result : { kind: "seqExpr", stmts: prefix, result, type: resultT, loc };
7937
+ };
7340
7938
  switch (op) {
7341
7939
  case ts.SyntaxKind.PlusToken:
7342
7940
  if (bothNum)
7343
- return { kind: "bin", op: "+", left, right, type: F64, loc };
7941
+ return { kind: "bin", op: "+", left: arithmeticLeft, right: arithmeticRight, type: F64, loc };
7344
7942
  if (left.type.kind === "string" || right.type.kind === "string") {
7345
7943
  return {
7346
7944
  kind: "strConcat",
@@ -7349,6 +7947,9 @@ export function lowerBinary(lowerer, expr) {
7349
7947
  type: STRING, loc,
7350
7948
  };
7351
7949
  }
7950
+ const optionalStringPlus = lowerOptionalStringPlus();
7951
+ if (optionalStringPlus)
7952
+ return optionalStringPlus;
7352
7953
  lowerer.unsupported("SC1043", expr);
7353
7954
  break;
7354
7955
  case ts.SyntaxKind.MinusToken:
@@ -7362,7 +7963,7 @@ export function lowerBinary(lowerer, expr) {
7362
7963
  : op === ts.SyntaxKind.AsteriskToken ? "*"
7363
7964
  : op === ts.SyntaxKind.SlashToken ? "/"
7364
7965
  : op === ts.SyntaxKind.PercentToken ? "%" : "**";
7365
- return { kind: "bin", op: binOp, left, right, type: F64, loc };
7966
+ return { kind: "bin", op: binOp, left: arithmeticLeft, right: arithmeticRight, type: F64, loc };
7366
7967
  }
7367
7968
  // The bitwise six, JS-exact: operands through ToInt32/ToUint32
7368
7969
  // (NaN/±Infinity → 0, truncate, wrap mod 2^32), the operation in
@@ -7381,12 +7982,12 @@ export function lowerBinary(lowerer, expr) {
7381
7982
  : op === ts.SyntaxKind.CaretToken ? "^"
7382
7983
  : op === ts.SyntaxKind.LessThanLessThanToken ? "<<"
7383
7984
  : op === ts.SyntaxKind.GreaterThanGreaterThanToken ? ">>" : ">>>";
7384
- return { kind: "bin", op: bitOp, left, right, type: F64, loc };
7985
+ return { kind: "bin", op: bitOp, left: arithmeticLeft, right: arithmeticRight, type: F64, loc };
7385
7986
  }
7386
7987
  case ts.SyntaxKind.EqualsEqualsEqualsToken:
7387
7988
  case ts.SyntaxKind.ExclamationEqualsEqualsToken: {
7388
7989
  const negated = op === ts.SyntaxKind.ExclamationEqualsEqualsToken;
7389
- if (bothNum)
7990
+ if (plainBothNum)
7390
7991
  return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc };
7391
7992
  if (bothStr)
7392
7993
  return { kind: "strEq", negated, left, right, type: BOOL, loc };
@@ -7445,6 +8046,38 @@ export function lowerBinary(lowerer, expr) {
7445
8046
  loc,
7446
8047
  };
7447
8048
  }
8049
+ if (left.type.kind === "union" && right.type.kind === "union") {
8050
+ const leftDef = lowerer.unions.get(left.type.unionId);
8051
+ const rightDef = lowerer.unions.get(right.type.unionId);
8052
+ if (leftDef && rightDef) {
8053
+ const leftValues = leftDef.arms.filter((arm) => !isUnitType(arm));
8054
+ const rightValues = rightDef.arms.filter((arm) => !isUnitType(arm));
8055
+ const sameValueArms = leftValues.length === rightValues.length &&
8056
+ leftValues.every((arm) => rightValues.some((candidate) => typeEquals(candidate, arm)));
8057
+ if (sameValueArms) {
8058
+ const arms = [...leftDef.arms];
8059
+ for (const arm of rightDef.arms) {
8060
+ if (!arms.some((candidate) => typeEquals(candidate, arm)))
8061
+ arms.push(arm);
8062
+ }
8063
+ arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
8064
+ const common = {
8065
+ kind: "union",
8066
+ unionId: lowerer.unions.intern(arms),
8067
+ };
8068
+ return {
8069
+ kind: "unionEq",
8070
+ unionId: common.unionId,
8071
+ negated,
8072
+ sameValue: false,
8073
+ left: lowerer.coerceInto(expr.left, left, common),
8074
+ right: lowerer.coerceInto(expr.right, right, common),
8075
+ type: BOOL,
8076
+ loc,
8077
+ };
8078
+ }
8079
+ }
8080
+ }
7448
8081
  lowerer.unsupported("SC1090", expr, `comparisons of union-typed values (${NARROW_FIRST})`);
7449
8082
  }
7450
8083
  // Arrays, maps, functions, class instances and records compare by
@@ -7509,7 +8142,7 @@ export function lowerBinary(lowerer, expr) {
7509
8142
  : op === ts.SyntaxKind.LessThanEqualsToken ? "<="
7510
8143
  : op === ts.SyntaxKind.GreaterThanToken ? ">" : ">=";
7511
8144
  if (bothNum)
7512
- return { kind: "bin", op: cmpOp, left, right, type: BOOL, loc };
8145
+ return { kind: "bin", op: cmpOp, left: arithmeticLeft, right: arithmeticRight, type: BOOL, loc };
7513
8146
  if (bothStr)
7514
8147
  return { kind: "strCmp", op: cmpOp, left, right, type: BOOL, loc };
7515
8148
  lowerer.unsupported("SC1043", expr);
@@ -9638,8 +10271,21 @@ export function fieldTarget(lowerer, access) {
9638
10271
  return null;
9639
10272
  }
9640
10273
  const fieldType = info.fields.get(access.name.text);
10274
+ const lowerObjectReceiver = () => {
10275
+ let obj = lowerer.lowerExpr(access.expression);
10276
+ if (obj.type.kind === "union" && lowerer.armTag(obj.type.unionId, UNDEFINED_T) >= 0) {
10277
+ const present = lowerer.stripUndefinedArm(obj.type);
10278
+ const helper = present.kind === "object"
10279
+ ? lowerer.narrowedArmHelper(obj.type.unionId, present, locOf(access.expression))
10280
+ : null;
10281
+ obj = helper
10282
+ ? { kind: "call", callee: helper, args: [obj], type: present, loc: locOf(access.expression) }
10283
+ : lowerer.maybeNarrow(obj, access.expression);
10284
+ }
10285
+ return obj;
10286
+ };
9641
10287
  if (fieldType) {
9642
- const obj = lowerer.lowerExpr(access.expression);
10288
+ const obj = lowerObjectReceiver();
9643
10289
  return { container: "class", obj, className: receiverIr.className, field: access.name.text, fieldType };
9644
10290
  }
9645
10291
  // Accessor property: either half declared anywhere on the chain
@@ -9648,7 +10294,7 @@ export function fieldTarget(lowerer, access) {
9648
10294
  const getF = lowerer.findMethodOn(info, `get:${access.name.text}`);
9649
10295
  const setF = lowerer.findMethodOn(info, `set:${access.name.text}`);
9650
10296
  if (getF || setF) {
9651
- const obj = lowerer.lowerExpr(access.expression);
10297
+ const obj = lowerObjectReceiver();
9652
10298
  return {
9653
10299
  container: "accessor",
9654
10300
  obj,
@@ -9663,13 +10309,35 @@ export function fieldTarget(lowerer, access) {
9663
10309
  const shape = lowerer.shapes.get(receiverIr.shapeId);
9664
10310
  const fieldType = shape?.fields.find((f) => f.name === access.name.text)?.type;
9665
10311
  if (fieldType) {
9666
- const obj = lowerer.lowerExpr(access.expression);
10312
+ let obj = lowerer.lowerExpr(access.expression);
9667
10313
  // A checker-record receiver whose VALUE stayed dyn (the erased
9668
10314
  // all-unknown-fields cast — `(err as { code?: unknown }).code`):
9669
10315
  // decline, and the dyn keyed-read fallback answers.
10316
+ if (obj.type.kind === "union" && lowerer.armTag(obj.type.unionId, UNDEFINED_T) >= 0) {
10317
+ // A nested property read such as `rows[i].value` has the same
10318
+ // unchecked outer array-read issue as tuple indexing. Validate
10319
+ // the record arm before fieldGet/recordGet so a missing outer
10320
+ // value raises a catchable TypeError rather than reaching the
10321
+ // backend with a union receiver.
10322
+ const present = lowerer.stripUndefinedArm(obj.type);
10323
+ const helper = present.kind === "record"
10324
+ ? lowerer.narrowedArmHelper(obj.type.unionId, present, locOf(access.expression))
10325
+ : null;
10326
+ obj = helper
10327
+ ? { kind: "call", callee: helper, args: [obj], type: present, loc: locOf(access.expression) }
10328
+ : lowerer.maybeNarrow(obj, access.expression);
10329
+ }
9670
10330
  if (obj.type.kind !== "record")
9671
10331
  return null;
9672
- return { container: "record", obj, shapeId: receiverIr.shapeId, field: access.name.text, fieldType };
10332
+ const actualShape = lowerer.shapes.get(obj.type.shapeId);
10333
+ const actualField = actualShape?.fields.find((f) => f.name === access.name.text);
10334
+ return {
10335
+ container: "record",
10336
+ obj,
10337
+ shapeId: obj.type.shapeId,
10338
+ field: access.name.text,
10339
+ fieldType: actualField?.type ?? fieldType,
10340
+ };
9673
10341
  }
9674
10342
  // A RECORD accessor property: either slot present makes the name an
9675
10343
  // accessor target (get/set share the property namespace — tsc