@scriptc/compiler 0.0.36 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +52 -22
  2. package/ambient/scriptc-overrides.d.ts +0 -7
  3. package/ambient/scriptc.d.ts +3 -2
  4. package/dist/backend/build-cache.js.map +1 -1
  5. package/dist/backend/c/async.js +24 -2
  6. package/dist/backend/c/async.js.map +1 -1
  7. package/dist/backend/c/c-emitter.d.ts +22 -10
  8. package/dist/backend/c/c-emitter.js +60 -15
  9. package/dist/backend/c/c-emitter.js.map +1 -1
  10. package/dist/backend/c/exprs.js +289 -55
  11. package/dist/backend/c/exprs.js.map +1 -1
  12. package/dist/backend/c/island.js.map +1 -1
  13. package/dist/backend/c/may-throw.js +3 -0
  14. package/dist/backend/c/may-throw.js.map +1 -1
  15. package/dist/backend/c/stmts.d.ts +3 -5
  16. package/dist/backend/c/stmts.js +76 -17
  17. package/dist/backend/c/stmts.js.map +1 -1
  18. package/dist/backend/c/types.js.map +1 -1
  19. package/dist/backend/c/walkers.js +36 -12
  20. package/dist/backend/c/walkers.js.map +1 -1
  21. package/dist/backend/external-c.d.ts +33 -0
  22. package/dist/backend/external-c.js +51 -0
  23. package/dist/backend/external-c.js.map +1 -0
  24. package/dist/backend/link-plan.d.ts +37 -0
  25. package/dist/backend/link-plan.js +26 -0
  26. package/dist/backend/link-plan.js.map +1 -0
  27. package/dist/backend/linker.d.ts +22 -0
  28. package/dist/backend/linker.js +236 -0
  29. package/dist/backend/linker.js.map +1 -0
  30. package/dist/backend/llvm/blocks.js.map +1 -1
  31. package/dist/backend/llvm/dyn.js +63 -3
  32. package/dist/backend/llvm/dyn.js.map +1 -1
  33. package/dist/backend/llvm/emitter.js +132 -26
  34. package/dist/backend/llvm/emitter.js.map +1 -1
  35. package/dist/backend/llvm/expr-async.js +85 -3
  36. package/dist/backend/llvm/expr-async.js.map +1 -1
  37. package/dist/backend/llvm/expr-bytes.js.map +1 -1
  38. package/dist/backend/llvm/expr-calls.js.map +1 -1
  39. package/dist/backend/llvm/expr-containers.js +68 -54
  40. package/dist/backend/llvm/expr-containers.js.map +1 -1
  41. package/dist/backend/llvm/expr-context.d.ts +2 -1
  42. package/dist/backend/llvm/expr-control.js.map +1 -1
  43. package/dist/backend/llvm/expr-dispatch.js +2 -0
  44. package/dist/backend/llvm/expr-dispatch.js.map +1 -1
  45. package/dist/backend/llvm/expr-dynamic.js +6 -3
  46. package/dist/backend/llvm/expr-dynamic.js.map +1 -1
  47. package/dist/backend/llvm/expr-island.js.map +1 -1
  48. package/dist/backend/llvm/expr-primitives.d.ts +1 -1
  49. package/dist/backend/llvm/expr-primitives.js +20 -0
  50. package/dist/backend/llvm/expr-primitives.js.map +1 -1
  51. package/dist/backend/llvm/expr-records.js.map +1 -1
  52. package/dist/backend/llvm/expr-stream-bridges.js +245 -10
  53. package/dist/backend/llvm/expr-stream-bridges.js.map +1 -1
  54. package/dist/backend/llvm/lib-dispatch.js +2 -0
  55. package/dist/backend/llvm/lib-dispatch.js.map +1 -1
  56. package/dist/backend/llvm/lib-shared.js +18 -0
  57. package/dist/backend/llvm/lib-shared.js.map +1 -1
  58. package/dist/backend/llvm/lib-stream.js +6 -3
  59. package/dist/backend/llvm/lib-stream.js.map +1 -1
  60. package/dist/backend/llvm/split.js.map +1 -1
  61. package/dist/backend/llvm/unsupported.js.map +1 -1
  62. package/dist/backend/llvm/walkers.js +31 -4
  63. package/dist/backend/llvm/walkers.js.map +1 -1
  64. package/dist/backend/native-codegen.d.ts +8 -1
  65. package/dist/backend/native-codegen.js +39 -25
  66. package/dist/backend/native-codegen.js.map +1 -1
  67. package/dist/backend/native-codegen.test.js +44 -4
  68. package/dist/backend/native-codegen.test.js.map +1 -1
  69. package/dist/backend/native-link-info.d.ts +1 -1
  70. package/dist/backend/native-link-info.js +42 -18
  71. package/dist/backend/native-link-info.js.map +1 -1
  72. package/dist/backend/native-toolchain.js.map +1 -1
  73. package/dist/backend/native-toolchain.test.js +5 -1
  74. package/dist/backend/native-toolchain.test.js.map +1 -1
  75. package/dist/backend/runtime-pack-targets.test.d.ts +1 -0
  76. package/dist/backend/runtime-pack-targets.test.js +54 -0
  77. package/dist/backend/runtime-pack-targets.test.js.map +1 -0
  78. package/dist/backend/runtime-pack.d.ts +5 -30
  79. package/dist/backend/runtime-pack.js +46 -141
  80. package/dist/backend/runtime-pack.js.map +1 -1
  81. package/dist/backend/runtime-pack.test.js +49 -14
  82. package/dist/backend/runtime-pack.test.js.map +1 -1
  83. package/dist/backend/targets.d.ts +63 -17
  84. package/dist/backend/targets.js +251 -35
  85. package/dist/backend/targets.js.map +1 -1
  86. package/dist/backend/targets.test.js +27 -5
  87. package/dist/backend/targets.test.js.map +1 -1
  88. package/dist/backend/vendor-archives.js.map +1 -1
  89. package/dist/compat/fetch-profile.js +4 -2
  90. package/dist/compat/fetch-profile.js.map +1 -1
  91. package/dist/coverage/report.js.map +1 -1
  92. package/dist/coverage/surface-manifest.js +22 -1
  93. package/dist/coverage/surface-manifest.js.map +1 -1
  94. package/dist/diagnostics/diagnostic.d.ts +6 -5
  95. package/dist/diagnostics/diagnostic.js +7 -6
  96. package/dist/diagnostics/diagnostic.js.map +1 -1
  97. package/dist/diagnostics/render.js.map +1 -1
  98. package/dist/ffi/ffi-manifest.js.map +1 -1
  99. package/dist/frontend/cjs-lexer.js.map +1 -1
  100. package/dist/frontend/dts-paths.js.map +1 -1
  101. package/dist/frontend/input-tracker.d.ts +10 -0
  102. package/dist/frontend/input-tracker.js +26 -3
  103. package/dist/frontend/input-tracker.js.map +1 -1
  104. package/dist/frontend/input-tracker.test.js +8 -1
  105. package/dist/frontend/input-tracker.test.js.map +1 -1
  106. package/dist/frontend/lib-contract.js.map +1 -1
  107. package/dist/frontend/lowering/array-values.d.ts +12 -0
  108. package/dist/frontend/lowering/array-values.js +76 -0
  109. package/dist/frontend/lowering/array-values.js.map +1 -0
  110. package/dist/frontend/lowering/callback-arg.js.map +1 -1
  111. package/dist/frontend/lowering/containers/bytes.d.ts +35 -0
  112. package/dist/frontend/lowering/containers/bytes.js +1021 -0
  113. package/dist/frontend/lowering/containers/bytes.js.map +1 -0
  114. package/dist/frontend/lowering/containers/string-and-regexp.d.ts +17 -0
  115. package/dist/frontend/lowering/containers/string-and-regexp.js +345 -0
  116. package/dist/frontend/lowering/containers/string-and-regexp.js.map +1 -0
  117. package/dist/frontend/lowering/expressions/evaluation-safety.d.ts +13 -0
  118. package/dist/frontend/lowering/expressions/evaluation-safety.js +76 -0
  119. package/dist/frontend/lowering/expressions/evaluation-safety.js.map +1 -0
  120. package/dist/frontend/lowering/expressions/object-literals.d.ts +44 -0
  121. package/dist/frontend/lowering/expressions/object-literals.js +1815 -0
  122. package/dist/frontend/lowering/expressions/object-literals.js.map +1 -0
  123. package/dist/frontend/lowering/expressions/optional-chains.d.ts +31 -0
  124. package/dist/frontend/lowering/expressions/optional-chains.js +382 -0
  125. package/dist/frontend/lowering/expressions/optional-chains.js.map +1 -0
  126. package/dist/frontend/lowering/expressions/try-lower-expression.d.ts +7 -0
  127. package/dist/frontend/lowering/expressions/try-lower-expression.js +26 -0
  128. package/dist/frontend/lowering/expressions/try-lower-expression.js.map +1 -0
  129. package/dist/frontend/lowering/lib-boundary.js +7 -2
  130. package/dist/frontend/lowering/lib-boundary.js.map +1 -1
  131. package/dist/frontend/lowering/lower-array-sort.d.ts +22 -0
  132. package/dist/frontend/lowering/lower-array-sort.js +619 -0
  133. package/dist/frontend/lowering/lower-array-sort.js.map +1 -0
  134. package/dist/frontend/lowering/lower-assert.js.map +1 -1
  135. package/dist/frontend/lowering/lower-async-iteration.d.ts +7 -0
  136. package/dist/frontend/lowering/lower-async-iteration.js +657 -0
  137. package/dist/frontend/lowering/lower-async-iteration.js.map +1 -0
  138. package/dist/frontend/lowering/lower-builtins.d.ts +18 -0
  139. package/dist/frontend/lowering/lower-builtins.js +379 -48
  140. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  141. package/dist/frontend/lowering/lower-calls.d.ts +16 -8
  142. package/dist/frontend/lowering/lower-calls.js +667 -91
  143. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  144. package/dist/frontend/lowering/lower-classes.d.ts +7 -13
  145. package/dist/frontend/lowering/lower-classes.js +128 -35
  146. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  147. package/dist/frontend/lowering/lower-comptime.js.map +1 -1
  148. package/dist/frontend/lowering/lower-containers.d.ts +1 -47
  149. package/dist/frontend/lowering/lower-containers.js +779 -2038
  150. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  151. package/dist/frontend/lowering/lower-event-emitter.js +51 -5
  152. package/dist/frontend/lowering/lower-event-emitter.js.map +1 -1
  153. package/dist/frontend/lowering/lower-exprs.d.ts +4 -78
  154. package/dist/frontend/lowering/lower-exprs.js +1128 -2409
  155. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  156. package/dist/frontend/lowering/lower-generators.d.ts +18 -3
  157. package/dist/frontend/lowering/lower-generators.js +145 -9
  158. package/dist/frontend/lowering/lower-generators.js.map +1 -1
  159. package/dist/frontend/lowering/lower-inspect.js +50 -36
  160. package/dist/frontend/lowering/lower-inspect.js.map +1 -1
  161. package/dist/frontend/lowering/lower-island.d.ts +11 -4
  162. package/dist/frontend/lowering/lower-island.js +47 -10
  163. package/dist/frontend/lowering/lower-island.js.map +1 -1
  164. package/dist/frontend/lowering/lower-modules.js +52 -15
  165. package/dist/frontend/lowering/lower-modules.js.map +1 -1
  166. package/dist/frontend/lowering/lower-node-module.d.ts +18 -0
  167. package/dist/frontend/lowering/lower-node-module.js +302 -0
  168. package/dist/frontend/lowering/lower-node-module.js.map +1 -0
  169. package/dist/frontend/lowering/lower-server.js +11 -1
  170. package/dist/frontend/lowering/lower-server.js.map +1 -1
  171. package/dist/frontend/lowering/lower-stmts.d.ts +5 -24
  172. package/dist/frontend/lowering/lower-stmts.js +811 -314
  173. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  174. package/dist/frontend/lowering/lower-stream.js +4 -4
  175. package/dist/frontend/lowering/lower-stream.js.map +1 -1
  176. package/dist/frontend/lowering/lowerer.d.ts +109 -34
  177. package/dist/frontend/lowering/lowerer.js +1225 -62
  178. package/dist/frontend/lowering/lowerer.js.map +1 -1
  179. package/dist/frontend/lowering/optional-arguments.d.ts +9 -0
  180. package/dist/frontend/lowering/optional-arguments.js +57 -0
  181. package/dist/frontend/lowering/optional-arguments.js.map +1 -0
  182. package/dist/frontend/lowering/surfaces.d.ts +10 -2
  183. package/dist/frontend/lowering/surfaces.js +15 -3
  184. package/dist/frontend/lowering/surfaces.js.map +1 -1
  185. package/dist/frontend/npm-static-declarations.d.ts +35 -0
  186. package/dist/frontend/npm-static-declarations.js +506 -0
  187. package/dist/frontend/npm-static-declarations.js.map +1 -0
  188. package/dist/frontend/npm-static-declarations.test.d.ts +1 -0
  189. package/dist/frontend/npm-static-declarations.test.js +122 -0
  190. package/dist/frontend/npm-static-declarations.test.js.map +1 -0
  191. package/dist/frontend/npm-static.d.ts +2 -0
  192. package/dist/frontend/npm-static.js +26 -10
  193. package/dist/frontend/npm-static.js.map +1 -1
  194. package/dist/frontend/npm.d.ts +60 -0
  195. package/dist/frontend/npm.js +38 -4
  196. package/dist/frontend/npm.js.map +1 -1
  197. package/dist/frontend/npm.test.d.ts +1 -0
  198. package/dist/frontend/npm.test.js +50 -0
  199. package/dist/frontend/npm.test.js.map +1 -0
  200. package/dist/frontend/program.d.ts +3 -2
  201. package/dist/frontend/program.js +495 -68
  202. package/dist/frontend/program.js.map +1 -1
  203. package/dist/frontend/resolve.d.ts +10 -2
  204. package/dist/frontend/resolve.js +90 -14
  205. package/dist/frontend/resolve.js.map +1 -1
  206. package/dist/frontend/resolve.test.js +4 -1
  207. package/dist/frontend/resolve.test.js.map +1 -1
  208. package/dist/frontend/runtime-resolve.d.ts +22 -0
  209. package/dist/frontend/runtime-resolve.js +101 -0
  210. package/dist/frontend/runtime-resolve.js.map +1 -0
  211. package/dist/frontend/runtime-resolve.test.d.ts +1 -0
  212. package/dist/frontend/runtime-resolve.test.js +18 -0
  213. package/dist/frontend/runtime-resolve.test.js.map +1 -0
  214. package/dist/frontend/ts7/adapter.js +12 -13
  215. package/dist/frontend/ts7/adapter.js.map +1 -1
  216. package/dist/frontend/ts7/ast.js.map +1 -1
  217. package/dist/frontend/ts7/census-check.js +3 -3
  218. package/dist/frontend/ts7/census-check.js.map +1 -1
  219. package/dist/frontend/ts7/checker.d.ts +3 -0
  220. package/dist/frontend/ts7/checker.js +12 -3
  221. package/dist/frontend/ts7/checker.js.map +1 -1
  222. package/dist/frontend/ts7/enums.js.map +1 -1
  223. package/dist/frontend/ts7/program-adapter.js +42 -3
  224. package/dist/frontend/ts7/program-adapter.js.map +1 -1
  225. package/dist/frontend/type-mapper.js +120 -7
  226. package/dist/frontend/type-mapper.js.map +1 -1
  227. package/dist/index.d.ts +3 -3
  228. package/dist/index.js +50 -28
  229. package/dist/index.js.map +1 -1
  230. package/dist/ir/analysis.js.map +1 -1
  231. package/dist/ir/ir.d.ts +125 -41
  232. package/dist/ir/ir.js +82 -31
  233. package/dist/ir/ir.js.map +1 -1
  234. package/dist/ir/serialize.d.ts +1 -1
  235. package/dist/ir/serialize.js +1 -1
  236. package/dist/ir/validate.js +127 -95
  237. package/dist/ir/validate.js.map +1 -1
  238. package/dist/library/cache-primitives.js.map +1 -1
  239. package/dist/library/cache-reset.test.js.map +1 -1
  240. package/dist/library/compiler-self-identity.js.map +1 -1
  241. package/dist/library/fence-eval.js +3 -1
  242. package/dist/library/fence-eval.js.map +1 -1
  243. package/dist/library/int-infer.js +12 -1
  244. package/dist/library/int-infer.js.map +1 -1
  245. package/dist/library/int-infer.test.js +4 -4
  246. package/dist/library/int-infer.test.js.map +1 -1
  247. package/dist/library/library-cache.test.js +5 -5
  248. package/dist/library/library-profile.js.map +1 -1
  249. package/dist/library/sidecar-validate.js.map +1 -1
  250. package/dist/library/sidecar.js.map +1 -1
  251. package/dist/startup-cache.d.ts +9 -1
  252. package/dist/startup-cache.js +16 -1
  253. package/dist/startup-cache.js.map +1 -1
  254. package/dist/wasi-paths.js.map +1 -1
  255. package/package.json +18 -5
  256. package/surface-manifest.json +64 -8
@@ -7,13 +7,15 @@ 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
- 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
+ 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, funcOf, isDynTypedRefType, isJsonSafeType, isSupportedArrayElem, 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";
13
14
  import { ARRAY_METHODS, builtinConstLit, builtinFenceHintOf, builtinModuleConstOf, builtinModulesArrayLit, builtinModuleFnOf, ISLAND_SURFACE, isChildSurfaceMember, MAP_METHODS, NARROW_FIRST, SET_METHODS, STR_METHODS, UNSUPPORTED_EXPR, sideEffectFreeOptionValue, stdlibGlobalNameOf } from "./surfaces.js";
14
- import { blockedBindingUseDiag, recordShapeMismatchDiag, requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
15
+ import { blockedBindingUseDiag, requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
15
16
  import { PoisonError, dynUndefinedExpr, jsFuncNameOf, neverTaintedJsType, nodeThrowExpr, own } from "./lowerer.js";
16
- import { lowerIndexMergeHelper, lowerNpmStaticSafeIndexRead, lowerSafeIndexRead, strCharsCall } from "./lower-containers.js";
17
+ import { 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";
@@ -27,6 +29,11 @@ import { isConstAssertionTypeNode, isGenericCallableMemberType, isParseArgsDynTy
27
29
  import { lowerYield } from "./lower-generators.js";
28
30
  import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "./lower-stream.js";
29
31
  import { countedFor, numLit, varRef } from "../../ir/build.js";
32
+ import { isSafeToDiscard, isSafeToMoveConditionEarlier, isSafeToRepeat } from "./expressions/evaluation-safety.js";
33
+ import { hasOptionalChainGuard, isOptionalChainTail, isRequireMainFilename } from "./expressions/optional-chains.js";
34
+ import { conditionalSpreadOf, foldedStringKeyOf } from "./expressions/object-literals.js";
35
+ import { tryLowerExpression } from "./expressions/try-lower-expression.js";
36
+ import { fenceNodeModuleMutation, isNodeModuleValue, lowerNodeModuleIdentifier, lowerNodeModuleProperty, lowerRequireCacheElement, lowerRequireCacheHas, lowerRequireMainProperty } from "./lower-node-module.js";
30
37
  /** A template piece's RAW text (String.raw's contract: escapes stay
31
38
  * characters). 7's client AST ships no rawText at runtime (the typing
32
39
  * declares it; the serialized node data omits it), so the raw span comes
@@ -403,7 +410,7 @@ function lowerExprInner(lowerer, expr) {
403
410
  lowerer.unsupported("SC1090", prop, "a conditional spread mixed with plain spreads in an 'any'-typed object literal");
404
411
  }
405
412
  const cond = lowerer.lowerCondition(cs.cond);
406
- if (!pureCondExpr(cond)) {
413
+ if (!isSafeToMoveConditionEarlier(cond)) {
407
414
  lowerer.unsupported("SC1090", prop, "conditional spreads with effectful conditions in an 'any'-typed object literal (bind the condition to a const first)");
408
415
  }
409
416
  spread = { cond, whenTrue: cs.whenTrue };
@@ -531,6 +538,14 @@ function lowerExprInner(lowerer, expr) {
531
538
  return { kind: "jsOp", op: "arrLit", args, type: JSVAL, loc };
532
539
  }
533
540
  if (ts.isTypeOfExpression(expr)) {
541
+ if (isNodeModuleValue(lowerer, expr.expression)) {
542
+ return { kind: "strLit", value: "object", type: STRING, loc };
543
+ }
544
+ if (ts.isPropertyAccessExpression(expr.expression)) {
545
+ const presence = lowerPromiseThenPresence(lowerer, expr.expression, { kind: "strLit", value: "function", type: STRING, loc });
546
+ if (presence !== null)
547
+ return presence;
548
+ }
534
549
  // `typeof queueMicrotask` / `typeof DOMException` on a STDLIB global
535
550
  // whose declared type is callable or constructable: folds to
536
551
  // "function" BEFORE the operand lowers — the identity-token story
@@ -576,7 +591,7 @@ function lowerExprInner(lowerer, expr) {
576
591
  undefinedT: "undefined", nullT: "object",
577
592
  };
578
593
  const folded = FOLD[operand.type.kind];
579
- if (folded && droppableStatic(operand)) {
594
+ if (folded && isSafeToDiscard(operand)) {
580
595
  return { kind: "strLit", value: folded, type: STRING, loc };
581
596
  }
582
597
  // A union operand: every arm's typeof answer is static, so the value
@@ -585,7 +600,7 @@ function lowerExprInner(lowerer, expr) {
585
600
  // tests, so only side-effect-free reads compose — and when every arm
586
601
  // agrees the whole expression folds to that one string (dropping only
587
602
  // the pure read, the trust-the-checker bet lowerUnitComparison makes).
588
- if (operand.type.kind === "union" && pureReemittable(operand)) {
603
+ if (operand.type.kind === "union" && isSafeToRepeat(operand)) {
589
604
  const def = lowerer.unions.get(operand.type.unionId);
590
605
  const answers = def?.arms.map(typeofAnswer);
591
606
  if (def && answers && answers.every((s) => s !== null)) {
@@ -612,6 +627,9 @@ function lowerExprInner(lowerer, expr) {
612
627
  lowerer.unsupported("SC1090", expr, "typeof expressions on statically-typed values");
613
628
  }
614
629
  if (ts.isIdentifier(expr)) {
630
+ const moduleValue = lowerNodeModuleIdentifier(lowerer, expr);
631
+ if (moduleValue)
632
+ return moduleValue;
615
633
  if (lowerer.isSelfReference(expr)) {
616
634
  return { kind: "selfRef", type: lowerer.ctx.selfType, loc };
617
635
  }
@@ -640,9 +658,13 @@ function lowerExprInner(lowerer, expr) {
640
658
  const symbol = lowerer.resolveValueSymbol(expr);
641
659
  const runtimeOptionalRoot = lowerer.runtimeOptionalRootOf(local);
642
660
  const active = lowerer.runtimeOptionalLocals.has(runtimeOptionalRoot);
661
+ const arithmetic = lowerer.runtimeOptionalArithmeticLocals.has(runtimeOptionalRoot);
643
662
  const captured = !!symbol &&
644
663
  lowerer.runtimeOptionalStorageLocals.has(runtimeOptionalRoot) &&
645
- lowerer.ctx.captureBySymbol.get(symbol) === local;
664
+ (lowerer.ctx.captureBySymbol.get(symbol) === local || runtimeOptionalRoot !== local);
665
+ if (arithmetic && !runtimeOptionalUseOf(expr)) {
666
+ return { kind: "varRef", localId: local.id, type: local.type, loc };
667
+ }
646
668
  if (active || captured) {
647
669
  const use = runtimeOptionalUseOf(expr);
648
670
  if (use?.complex) {
@@ -665,8 +687,20 @@ function lowerExprInner(lowerer, expr) {
665
687
  }
666
688
  return runtimeOptionalReceiverRead(lowerer, expr, local, null, loc);
667
689
  }
668
- if (active || captured)
669
- return { kind: "varRef", localId: local.id, type: local.type, loc };
690
+ if (runtimeOptionalObjectWalkerArg(lowerer, expr)) {
691
+ const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr));
692
+ if (local.type.kind === "union" && narrowed) {
693
+ const helper = narrowed.kind === "union"
694
+ ? lowerer.narrowedRetagHelper(expr, local.type.unionId, narrowed.unionId, loc)
695
+ : !isUnitType(narrowed)
696
+ ? lowerer.narrowedArmHelper(local.type.unionId, narrowed, loc)
697
+ : null;
698
+ if (helper) {
699
+ return { kind: "call", callee: helper, args: [varRef(local.id, local.type, loc)], type: narrowed, loc };
700
+ }
701
+ }
702
+ }
703
+ return { kind: "varRef", localId: local.id, type: local.type, loc };
670
704
  }
671
705
  return lowerer.maybeNarrow({ kind: "varRef", localId: local.id, type: local.type, loc }, expr);
672
706
  }
@@ -686,7 +720,53 @@ function lowerExprInner(lowerer, expr) {
686
720
  lowerer.pushDiag(blockedBindingUseDiag(expr.text, loc));
687
721
  throw new PoisonError();
688
722
  }
689
- return lowerer.maybeNarrow({ kind: "varRef", localId: g.id, type: g.type, loc }, expr);
723
+ const ref = { kind: "varRef", localId: g.id, type: g.type, loc };
724
+ if (lowerer.isRuntimeOptionalArithmeticGlobal(g) && !runtimeOptionalUseOf(expr)) {
725
+ return ref;
726
+ }
727
+ const storageHasUndefined = g.type.kind === "union" &&
728
+ lowerer.armTag(g.type.unionId, UNDEFINED_T) >= 0;
729
+ if (lowerer.isRuntimeOptionalGlobal(g) || storageHasUndefined) {
730
+ // A checker-narrowed global receiver (`if (tags !== undefined)
731
+ // tags[tags.length] = ...`, or `a ? Object.keys(a) : ...`) is
732
+ // still stored as the runtime optional union. Extract the proven
733
+ // value arm before routing the member/element operation so dynamic
734
+ // keys and builtin record walkers do not see a union receiver.
735
+ const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr));
736
+ const arithmeticUnion = g.type.kind === "union" &&
737
+ (lowerer.unions.get(g.type.unionId)?.arms.length === 2) &&
738
+ lowerer.unions.get(g.type.unionId).arms.some((a) => a.kind === "f64") &&
739
+ lowerer.unions.get(g.type.unionId).arms.some((a) => a.kind === "string");
740
+ if (!arithmeticUnion && g.type.kind === "union" && narrowed && narrowed.kind !== "union" &&
741
+ !isUnitType(narrowed) &&
742
+ narrowed.kind !== "f64" && narrowed.kind !== "string" && narrowed.kind !== "bool" &&
743
+ lowerer.armTag(g.type.unionId, narrowed) >= 0) {
744
+ const helper = lowerer.narrowedArmHelper(g.type.unionId, narrowed, loc);
745
+ if (helper)
746
+ return { kind: "call", callee: helper, args: [ref], type: narrowed, loc };
747
+ }
748
+ const use = runtimeOptionalUseOf(expr);
749
+ // The enclosing logical/conditional expression consumes the union
750
+ // before any member operation runs (`(value ?? fallback).length`).
751
+ // Keep the tagged value intact and let that lazy expression decide
752
+ // its branch; extracting here would evaluate or throw too early.
753
+ if (use?.complex)
754
+ return ref;
755
+ if (use?.optional)
756
+ return ref;
757
+ if (use?.kind === "property")
758
+ return runtimeOptionalReceiverRead(lowerer, expr, g, use.access.name.text, loc);
759
+ if (use?.kind === "element") {
760
+ const key = runtimeOptionalElementKey(use.access.argumentExpression);
761
+ if (key === null)
762
+ lowerer.unsupported("SC1090", use.access, "a computed read from a runtime-optional global (use a literal key)");
763
+ return runtimeOptionalReceiverRead(lowerer, expr, g, key, loc);
764
+ }
765
+ if (use?.kind === "call")
766
+ return runtimeOptionalReceiverRead(lowerer, expr, g, null, loc);
767
+ return ref;
768
+ }
769
+ return lowerer.maybeNarrow(ref, expr);
690
770
  }
691
771
  // `undefined` — tsc's intrinsic global (a local shadowing the name
692
772
  // resolved above): a unit literal, exactly like the `null` keyword.
@@ -709,9 +789,7 @@ function lowerExprInner(lowerer, expr) {
709
789
  // directory at `/tmp`. Bake the matching GUEST spelling so resource
710
790
  // lookups relative to these CommonJS globals stay inside the exposed
711
791
  // 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;
792
+ const fileName = moduleFileName(lowerer, sf);
715
793
  const value = expr.text === "__dirname"
716
794
  ? (lowerer.targetPlatform === "wasi" ? posix.dirname(fileName) : dirname(fileName))
717
795
  : fileName;
@@ -1214,8 +1292,8 @@ function lowerExprInner(lowerer, expr) {
1214
1292
  // The tail of an optional chain whose `?.` sits deeper
1215
1293
  // (`x?.trim().toLowerCase()`): the whole tail short-circuits with
1216
1294
  // the guard, so it lowers as one chain (dyn/island receivers keep
1217
- // their own undefined-propagating reads — see chainTailClaimed).
1218
- if (chainTailClaimed(lowerer, expr))
1295
+ // their own undefined-propagating reads — see isOptionalChainTail).
1296
+ if (isOptionalChainTail(lowerer, expr))
1219
1297
  return lowerer.lowerOptionalChain(expr);
1220
1298
  return lowerer.lowerCall(expr);
1221
1299
  }
@@ -1243,6 +1321,15 @@ function lowerExprInner(lowerer, expr) {
1243
1321
  if (en)
1244
1322
  return en;
1245
1323
  }
1324
+ // `import.meta.<field>` — the file-backed ESM metadata that survives
1325
+ // compilation as a constant. Only direct, identifier-named reads are
1326
+ // claimed here; the bare meta object, computed access, writes, and
1327
+ // unsupported members continue through the generic module-loader fence
1328
+ // below. The source file is the module identity, so imported modules
1329
+ // receive their own filename/dirname and only the entry receives main.
1330
+ const importMeta = importMetaProperty(lowerer, expr);
1331
+ if (importMeta)
1332
+ return importMeta;
1246
1333
  // `require.main.filename` / `require.main?.filename` — CommonJS
1247
1334
  // entry-module identity: in a compiled binary require.main IS the
1248
1335
  // entry module (never undefined in a CJS graph, so the chain's guard
@@ -1254,6 +1341,17 @@ function lowerExprInner(lowerer, expr) {
1254
1341
  if (isRequireMainFilename(lowerer, expr)) {
1255
1342
  return { kind: "strLit", value: lowerer.entry.fileName, type: STRING, loc };
1256
1343
  }
1344
+ const requireMain = lowerRequireMainProperty(lowerer, expr);
1345
+ if (requireMain)
1346
+ return requireMain;
1347
+ const moduleProperty = lowerNodeModuleProperty(lowerer, expr);
1348
+ if (moduleProperty)
1349
+ return moduleProperty;
1350
+ if (expr.questionDotToken && expr.name.text === "electron") {
1351
+ const processProperty = lowerer.lowerProcessProperty(expr);
1352
+ if (processProperty)
1353
+ return processProperty;
1354
+ }
1257
1355
  // Optional chaining `a?.b`: the guard lowers here (a tag test around
1258
1356
  // the plain property lowering below); the handled marker keeps this
1259
1357
  // re-entrant dispatch from looping.
@@ -1262,7 +1360,7 @@ function lowerExprInner(lowerer, expr) {
1262
1360
  }
1263
1361
  // `a?.b.c` — the tail of a chain whose token sits deeper: the whole
1264
1362
  // tail short-circuits with the guard.
1265
- if (!expr.questionDotToken && chainTailClaimed(lowerer, expr)) {
1363
+ if (!expr.questionDotToken && isOptionalChainTail(lowerer, expr)) {
1266
1364
  return lowerer.lowerOptionalChain(expr);
1267
1365
  }
1268
1366
  // Island receiver: o.x is an engine property read (getProp may throw
@@ -1536,8 +1634,14 @@ function lowerExprInner(lowerer, expr) {
1536
1634
  lowerer.lowerFieldRead(expr);
1537
1635
  // A union-typed field read narrows like an identifier when the
1538
1636
  // checker has narrowed this use to one arm.
1539
- if (handled)
1637
+ if (handled) {
1638
+ if (handled.type.kind === "union" &&
1639
+ lowerer.armTag(handled.type.unionId, UNDEFINED_T) >= 0 &&
1640
+ handled.kind === "recordGet" &&
1641
+ lowerer.isRuntimeOptionalField(handled.shapeId, handled.field))
1642
+ return handled;
1540
1643
  return lowerer.maybeNarrow(handled, expr);
1644
+ }
1541
1645
  // `globalThis.<name>` that no lowering above claimed
1542
1646
  // (globalThis.crypto, globalThis.SubtleCrypto, globalThis.localStorage
1543
1647
  // — the harness's capability-conditional knownGlobals adds): the
@@ -1585,7 +1689,7 @@ function lowerExprInner(lowerer, expr) {
1585
1689
  const recv = lowerer.lowerExpr(expr.expression);
1586
1690
  if (recv.type.kind === "dyn") {
1587
1691
  const key = { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) };
1588
- const opt = chainGuardedByQuestionDot(expr.expression);
1692
+ const opt = hasOptionalChainGuard(expr.expression);
1589
1693
  return lowerer.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: recv, type: DYN, loc }, expr);
1590
1694
  }
1591
1695
  }
@@ -1599,7 +1703,7 @@ function lowerExprInner(lowerer, expr) {
1599
1703
  // `f.call` as a value). JS files only; TS keeps the fence.
1600
1704
  if (!["apply", "bind", "call", "toString", "constructor", "prototype", "caller", "arguments"].includes(expr.name.text) &&
1601
1705
  isJsSourceFile(expr.getSourceFile())) {
1602
- const probed = probeLower(lowerer, expr.expression);
1706
+ const probed = tryLowerExpression(lowerer, expr.expression);
1603
1707
  if (probed?.type.kind === "func" &&
1604
1708
  canBoxFuncIntoDyn(probed.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) {
1605
1709
  const fnName = jsFuncNameOf(expr.expression);
@@ -1664,7 +1768,7 @@ function lowerExprInner(lowerer, expr) {
1664
1768
  type: STRING,
1665
1769
  loc: locOf(expr.name),
1666
1770
  };
1667
- const opt = chainGuardedByQuestionDot(expr.expression);
1771
+ const opt = hasOptionalChainGuard(expr.expression);
1668
1772
  return lowerer.maybeNarrow({
1669
1773
  kind: "dynKeyGet",
1670
1774
  key,
@@ -1724,7 +1828,7 @@ function lowerExprInner(lowerer, expr) {
1724
1828
  // validated dynCheck as usual.
1725
1829
  if (recvLowered.type.kind === "dyn") {
1726
1830
  const key = { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) };
1727
- const opt = chainGuardedByQuestionDot(expr.expression);
1831
+ const opt = hasOptionalChainGuard(expr.expression);
1728
1832
  return lowerer.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: recvLowered, type: DYN, loc }, expr);
1729
1833
  }
1730
1834
  // The lowered receiver is a RECORD the checker spelled wider —
@@ -1778,6 +1882,43 @@ function lowerExprInner(lowerer, expr) {
1778
1882
  lowerer.unsupported(entry.code, expr, entry.feature);
1779
1883
  lowerer.unsupported("SC1090", expr, `syntax '${ts.SyntaxKind[expr.kind]}'`);
1780
1884
  }
1885
+ function moduleFileName(lowerer, sf) {
1886
+ return lowerer.targetPlatform === "wasi"
1887
+ ? wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/")
1888
+ : sf.fileName;
1889
+ }
1890
+ function importMetaProperty(lowerer, expr) {
1891
+ if (expr.questionDotToken !== undefined || !ts.isMetaProperty(expr.expression))
1892
+ return null;
1893
+ const meta = expr.expression;
1894
+ if (meta.keywordToken !== ts.SyntaxKind.ImportKeyword || meta.name.text !== "meta")
1895
+ return null;
1896
+ const sf = expr.getSourceFile();
1897
+ const fileName = moduleFileName(lowerer, sf);
1898
+ switch (expr.name.text) {
1899
+ case "url":
1900
+ return {
1901
+ kind: "strLit",
1902
+ value: pathToFileURL(fileName, { windows: lowerer.targetPlatform === "win32" }).href,
1903
+ type: STRING,
1904
+ loc: locOf(expr),
1905
+ };
1906
+ case "filename":
1907
+ return { kind: "strLit", value: fileName, type: STRING, loc: locOf(expr) };
1908
+ case "dirname":
1909
+ return {
1910
+ kind: "strLit",
1911
+ value: lowerer.targetPlatform === "wasi" ? posix.dirname(fileName) : dirname(fileName),
1912
+ type: STRING,
1913
+ loc: locOf(expr),
1914
+ };
1915
+ case "main":
1916
+ return { kind: "boolLit", value: sf === lowerer.entry, type: BOOL, loc: locOf(expr) };
1917
+ default:
1918
+ lowerer.unsupported("SC1090", expr, `'import.meta.${expr.name.text}' (only url, filename, dirname, and main are supported in static ESM)`);
1919
+ return null;
1920
+ }
1921
+ }
1781
1922
  /** `c ? a : b` — see the inline comments. `expected` plays the contextual
1782
1923
  * array type's role when the caller knows the slot's array type and tsc's
1783
1924
  * API doesn't surface it (a ternary as a SPREAD source — lowerArrayLiteral
@@ -2125,19 +2266,65 @@ export function maybeNarrow(lowerer, expr, node) {
2125
2266
  * even though an OOB-safe assignment may have restored undefined. Check
2126
2267
  * that hidden arm and throw the member-read TypeError Node would produce. */
2127
2268
  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)) {
2269
+ if (local.type.kind !== "union") {
2270
+ lowerer.runtimeOptionalLocals.delete(local);
2271
+ lowerer.runtimeOptionalStorageLocals.delete(local);
2272
+ return varRef(local.id, local.type, loc);
2273
+ }
2274
+ // A non-null/type assertion around the receiver carries the useful
2275
+ // checker narrowing on the wrapper rather than on the identifier token
2276
+ // itself (`gg!.day`). Follow only erased wrappers; the member/call node
2277
+ // still receives the checked extraction built below.
2278
+ let narrowedNode = expr;
2279
+ while ((ts.isParenthesizedExpression(narrowedNode.parent) ||
2280
+ ts.isAssertionExpression(narrowedNode.parent) ||
2281
+ ts.isSatisfiesExpression(narrowedNode.parent) ||
2282
+ ts.isNonNullExpression(narrowedNode.parent)) &&
2283
+ narrowedNode.parent.expression === narrowedNode) {
2284
+ narrowedNode = narrowedNode.parent;
2285
+ }
2286
+ const narrowed = lowerer.mapTypeOf(lowerer.typeOf(narrowedNode));
2287
+ if (!narrowed || isUnitType(narrowed)) {
2132
2288
  lowerer.unsupported("SC1090", expr, "a runtime-optional capture whose narrowed value type is not representable");
2133
2289
  }
2290
+ if (narrowed.kind === "union") {
2291
+ // The checker may keep the receiver at the storage union unchanged
2292
+ // (`string | undefined` on a JS `.match()` call). There is no
2293
+ // sub-union to re-tag: preserve the value for the downstream
2294
+ // nullable-receiver lowering, which performs the member-specific
2295
+ // checked extraction. Refusing here deferred the whole statement to
2296
+ // a runtime fence before that lowering could run.
2297
+ if (typeEquals(local.type, narrowed)) {
2298
+ return varRef(local.id, local.type, loc);
2299
+ }
2300
+ // A captured array element can itself be a union of records (for
2301
+ // example Circle | Square). The optional storage union has one extra
2302
+ // undefined arm; re-tag it to the checker-narrowed value union with a
2303
+ // checked trap for that arm, then let union property lowering inspect
2304
+ // the surviving record arms.
2305
+ const helper = lowerer.narrowedRetagHelper(expr, local.type.unionId, narrowed.unionId, loc);
2306
+ if (!helper) {
2307
+ lowerer.unsupported("SC1090", expr, "a runtime-optional capture whose narrowed union is not representable");
2308
+ }
2309
+ return { kind: "call", callee: helper, args: [varRef(local.id, local.type, loc)], type: narrowed, loc };
2310
+ }
2134
2311
  const valueTag = lowerer.armTag(local.type.unionId, narrowed);
2135
2312
  const undefTag = lowerer.armTag(local.type.unionId, UNDEFINED_T);
2136
2313
  if (valueTag < 0 || undefTag < 0)
2137
2314
  throw new InternalCompilerError("runtime-optional local is missing its value or undefined arm");
2138
2315
  const def = lowerer.unions.get(local.type.unionId);
2139
2316
  if (!def || def.arms.length !== 2) {
2140
- lowerer.unsupported("SC1090", expr, "a direct read from a runtime-optional capture with multiple value arms");
2317
+ // The checker may narrow a runtime-optional capture to one arm of a
2318
+ // value union (`Circle | Square | undefined` -> `Circle`). The
2319
+ // two-arm fast path below cannot extract that arm safely: a later
2320
+ // array mutation can leave any of the stored tags in the slot. Use
2321
+ // the checked helper so every other arm, including undefined, remains
2322
+ // a catchable TypeError rather than an unchecked payload read.
2323
+ const helper = lowerer.narrowedArmHelper(local.type.unionId, narrowed, loc);
2324
+ if (!helper) {
2325
+ lowerer.unsupported("SC1090", expr, "a direct read from a runtime-optional capture with multiple value arms");
2326
+ }
2327
+ return { kind: "call", callee: helper, args: [varRef(local.id, local.type, loc)], type: narrowed, loc };
2141
2328
  }
2142
2329
  const message = property === null
2143
2330
  ? `${expr.text} is not a function`
@@ -2209,6 +2396,25 @@ function runtimeOptionalUseOf(expr) {
2209
2396
  }
2210
2397
  return null;
2211
2398
  }
2399
+ /** Object.keys/Object.entries require an object receiver, but their first
2400
+ * argument is not syntactically a member receiver. Recognize that one
2401
+ * standard-library position so a runtime-optional loop element can be
2402
+ * checked against the checker-narrowed record arm before builtin lowering. */
2403
+ function runtimeOptionalObjectWalkerArg(lowerer, expr) {
2404
+ let arg = expr;
2405
+ while ((ts.isParenthesizedExpression(arg.parent) ||
2406
+ ts.isAssertionExpression(arg.parent) ||
2407
+ ts.isSatisfiesExpression(arg.parent) ||
2408
+ ts.isNonNullExpression(arg.parent)) &&
2409
+ arg.parent.expression === arg) {
2410
+ arg = arg.parent;
2411
+ }
2412
+ const call = arg.parent;
2413
+ if (!ts.isCallExpression(call) || call.arguments[0] !== arg || !ts.isPropertyAccessExpression(call.expression))
2414
+ return false;
2415
+ const access = call.expression;
2416
+ return (access.name.text === "keys" || access.name.text === "entries") && lowerer.isStdlibMember(access);
2417
+ }
2212
2418
  function runtimeOptionalElementKey(expr) {
2213
2419
  if (ts.isStringLiteral(expr) || ts.isNumericLiteral(expr))
2214
2420
  return expr.text;
@@ -2255,7 +2461,7 @@ export function lowerUnitComparison(lowerer, left, right, negated, loc) {
2255
2461
  // callback's effects are observable) rides one throwaway tag
2256
2462
  // test; droppable reads fold to the bare literal.
2257
2463
  const answer = { kind: "boolLit", value: negated, type: BOOL, loc };
2258
- if (droppableStatic(other))
2464
+ if (isSafeToDiscard(other))
2259
2465
  return answer;
2260
2466
  const evalOnce = {
2261
2467
  kind: "unionIsTag", unionId: other.type.unionId, tag: 0, negated: false, value: other, type: BOOL, loc,
@@ -2363,7 +2569,7 @@ function lowerLooseNullCompare(lowerer, expr, loc) {
2363
2569
  // backend emits it per test — restricted to re-emittable pure reads;
2364
2570
  // anything effectful gets its own actionable fence (the generic
2365
2571
  // SC1040 hint would wrongly suggest `== null` as the fix).
2366
- if (!pureReemittable(other)) {
2572
+ if (!isSafeToRepeat(other)) {
2367
2573
  lowerer.unsupported("SC1090", expr, `'== null' / '!= null' on a '${lowerer.fmt(ut)}' value that isn't a plain read ` +
2368
2574
  `(both unit arms need the operand twice — bind it to a const first)`);
2369
2575
  }
@@ -2393,88 +2599,6 @@ function lowerLooseNullCompare(lowerer, expr, loc) {
2393
2599
  // `== null` is statically false, `!= null` statically true.
2394
2600
  return { kind: "boolLit", value: negated, type: BOOL, loc };
2395
2601
  }
2396
- /** Safe to EMIT twice: plain reads with no side effects — local/global
2397
- * reads and record/class field reads over such (accessor properties
2398
- * lower to getter CALLS, so they never appear as field reads). Used by
2399
- * the two-unit-arm `== null` composition, the union `typeof` forms, and
2400
- * the union-switch desugar, whose operands ride several tests. */
2401
- /** A static expression whose evaluation is unobservable — safe to DROP
2402
- * (typeof's constant fold: JS evaluates the operand, but when nothing in
2403
- * it can throw, call, write, or allocate observably, folding it away
2404
- * changes nothing). Strictly wider than pureReemittable: numeric
2405
- * arithmetic (f64 ops never throw — division by zero is Infinity),
2406
- * string concatenation, logical/ternary composition, and unit/literal
2407
- * leaves compose; anything with a call, an island op, an index read (can
2408
- * trap), or an unknown kind answers false. */
2409
- export function droppableStatic(e) {
2410
- switch (e.kind) {
2411
- case "numLit":
2412
- case "strLit":
2413
- case "boolLit":
2414
- case "unitLit":
2415
- return true;
2416
- case "bin":
2417
- return droppableStatic(e.left) && droppableStatic(e.right);
2418
- case "unary":
2419
- return droppableStatic(e.operand);
2420
- case "logical":
2421
- return droppableStatic(e.left) && droppableStatic(e.right);
2422
- case "ternary":
2423
- return droppableStatic(e.cond) && droppableStatic(e.then) && droppableStatic(e.else_);
2424
- case "strConcat":
2425
- return droppableStatic(e.left) && droppableStatic(e.right);
2426
- case "toBool":
2427
- return droppableStatic(e.operand);
2428
- case "unionIsTag":
2429
- return droppableStatic(e.value);
2430
- case "unionWrap":
2431
- return droppableStatic(e.value);
2432
- // Fresh allocations are unobservable too — only their PIECES can
2433
- // carry effects (a spread re-reads its source: pure; element and
2434
- // field initializers recurse).
2435
- case "arrayLit":
2436
- return e.elems.every(droppableStatic);
2437
- case "recordLit":
2438
- return e.fields.every((f) => droppableStatic(f.value));
2439
- case "recordClone":
2440
- return droppableStatic(e.source) && e.overrides.every((f) => droppableStatic(f.value));
2441
- case "closure":
2442
- return true;
2443
- default:
2444
- return pureReemittable(e);
2445
- }
2446
- }
2447
- export function pureReemittable(e) {
2448
- if (e.kind === "varRef")
2449
- return true;
2450
- if (e.kind === "recordGet" || e.kind === "fieldGet")
2451
- return pureReemittable(e.obj);
2452
- // The trust-the-checker narrowing bridges over a pure read: extraction
2453
- // and downcast are reads too (the +1 on ref payloads is RC bookkeeping,
2454
- // not an observable effect — each emission owns its own copy).
2455
- if (e.kind === "unionNarrow" || e.kind === "downcast")
2456
- return pureReemittable(e.value);
2457
- return false;
2458
- }
2459
- /** A side-effect-free CONDITION: the ToBoolean/test wrappers lowerCondition
2460
- * builds over pure reads (evaluating one early or twice changes nothing
2461
- * observable). Used by the island-literal conditional spread, whose
2462
- * desugar hoists the condition ahead of the properties. */
2463
- function pureCondExpr(e) {
2464
- if (e.kind === "boolLit")
2465
- return true;
2466
- if (e.kind === "toBool")
2467
- return pureReemittable(e.operand);
2468
- if (e.kind === "unionIsTag" || e.kind === "dynTest")
2469
- return pureReemittable(e.value);
2470
- if (e.kind === "unary" && e.op === "!")
2471
- return pureCondExpr(e.operand);
2472
- if (e.kind === "logical")
2473
- return pureCondExpr(e.left) && pureCondExpr(e.right);
2474
- if (e.kind === "jsOp" && (e.op === "truthy" || e.op === "not"))
2475
- return e.args.every(pureReemittable);
2476
- return pureReemittable(e);
2477
- }
2478
2602
  /** `a ?? b` — JS-exact nullish coalescing: ONLY null/undefined take the
2479
2603
  * default (0, "", and false do not), and the right side evaluates lazily.
2480
2604
  * On a unit-armed union left this is the `nullish` node (a runtime tag
@@ -2523,6 +2647,42 @@ export function lowerNullishCoalesce(lowerer, expr, loc) {
2523
2647
  const right = lowerer.lowerExprExpecting(expr.right, type);
2524
2648
  return { kind: "nullish", left, right, type, loc };
2525
2649
  }
2650
+ if (type.kind !== "union" && rest.length > 1 && rest.some((arm) => typeEquals(arm, type))) {
2651
+ const helper = lowerer.narrowedArmHelper(left.type.unionId, type, loc);
2652
+ if (helper !== null) {
2653
+ const stmts = [];
2654
+ let stable = left;
2655
+ if (!isSafeToRepeat(left)) {
2656
+ const local = lowerer.declareHiddenLocal("%nullish", left.type);
2657
+ stmts.push({ kind: "varDecl", localId: local.id, init: left, loc });
2658
+ stable = varRef(local.id, left.type, loc);
2659
+ }
2660
+ const unitTags = def.arms
2661
+ .map((arm, tag) => ({ arm, tag }))
2662
+ .filter(({ arm }) => isUnitType(arm))
2663
+ .map(({ tag }) => tag);
2664
+ let isNullish = { kind: "unionIsTag", unionId: left.type.unionId, tag: unitTags[0], negated: false, value: stable, type: BOOL, loc };
2665
+ for (const tag of unitTags.slice(1)) {
2666
+ isNullish = {
2667
+ kind: "logical",
2668
+ op: "||",
2669
+ left: isNullish,
2670
+ right: { kind: "unionIsTag", unionId: left.type.unionId, tag, negated: false, value: stable, type: BOOL, loc },
2671
+ type: BOOL,
2672
+ loc,
2673
+ };
2674
+ }
2675
+ const result = {
2676
+ kind: "ternary",
2677
+ cond: isNullish,
2678
+ then: lowerer.lowerExprExpecting(expr.right, type),
2679
+ else_: { kind: "call", callee: helper, args: [stable], type, loc },
2680
+ type,
2681
+ loc,
2682
+ };
2683
+ return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type, loc };
2684
+ }
2685
+ }
2526
2686
  // The RETAGGED shape — the default changes the result union (`s ??
2527
2687
  // null` over `string | undefined` answers `null | string`): every
2528
2688
  // non-unit arm of the left has a home in the result, so an interned
@@ -2533,9 +2693,72 @@ export function lowerNullishCoalesce(lowerer, expr, loc) {
2533
2693
  if (type.kind === "union") {
2534
2694
  const leftT = left.type;
2535
2695
  const armPairs = rest.map((a) => ({ arm: a, src: lowerer.armTag(leftT.unionId, a), dst: lowerer.armTag(type.unionId, a) }));
2696
+ if (rest.length > 0 && armPairs.every((pair) => pair.src >= 0 && pair.dst >= 0)) {
2697
+ const stmts = [];
2698
+ let stable = left;
2699
+ if (!isSafeToRepeat(left)) {
2700
+ const local = lowerer.declareHiddenLocal("%nullish", leftT);
2701
+ stmts.push({ kind: "varDecl", localId: local.id, init: left, loc });
2702
+ stable = varRef(local.id, leftT, loc);
2703
+ }
2704
+ const unitTags = def.arms
2705
+ .map((arm, tag) => ({ arm, tag }))
2706
+ .filter(({ arm }) => isUnitType(arm))
2707
+ .map(({ tag }) => tag);
2708
+ let isNullish = {
2709
+ kind: "unionIsTag",
2710
+ unionId: leftT.unionId,
2711
+ tag: unitTags[0],
2712
+ negated: false,
2713
+ value: stable,
2714
+ type: BOOL,
2715
+ loc,
2716
+ };
2717
+ for (const tag of unitTags.slice(1)) {
2718
+ isNullish = {
2719
+ kind: "logical",
2720
+ op: "||",
2721
+ left: isNullish,
2722
+ right: { kind: "unionIsTag", unionId: leftT.unionId, tag, negated: false, value: stable, type: BOOL, loc },
2723
+ type: BOOL,
2724
+ loc,
2725
+ };
2726
+ }
2727
+ let present = null;
2728
+ for (let i = armPairs.length - 1; i >= 0; i--) {
2729
+ const pair = armPairs[i];
2730
+ const wrapped = {
2731
+ kind: "unionWrap",
2732
+ unionId: type.unionId,
2733
+ tag: pair.dst,
2734
+ value: { kind: "unionNarrow", unionId: leftT.unionId, tag: pair.src, value: stable, type: pair.arm, loc },
2735
+ type,
2736
+ loc,
2737
+ };
2738
+ present = present === null
2739
+ ? wrapped
2740
+ : {
2741
+ kind: "ternary",
2742
+ cond: { kind: "unionIsTag", unionId: leftT.unionId, tag: pair.src, negated: false, value: stable, type: BOOL, loc },
2743
+ then: wrapped,
2744
+ else_: present,
2745
+ type,
2746
+ loc,
2747
+ };
2748
+ }
2749
+ const result = {
2750
+ kind: "ternary",
2751
+ cond: isNullish,
2752
+ then: lowerer.lowerExprExpecting(expr.right, type),
2753
+ else_: present,
2754
+ type,
2755
+ loc,
2756
+ };
2757
+ return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type, loc };
2758
+ }
2536
2759
  if (armPairs.every((p) => p.src >= 0 && p.dst >= 0)) {
2537
2760
  const right = lowerer.lowerExprExpecting(expr.right, type);
2538
- if (droppableStatic(right)) {
2761
+ if (isSafeToDiscard(right)) {
2539
2762
  const helper = nullishRetagHelper(lowerer, leftT, type, loc);
2540
2763
  return { kind: "call", callee: helper, args: [left, right], type, loc };
2541
2764
  }
@@ -2602,383 +2825,6 @@ function nullishRetagHelper(lowerer, leftT, resT, loc) {
2602
2825
  });
2603
2826
  return name;
2604
2827
  }
2605
- /** One optional-chain STEP: `a?.b`, `a?.m(...)`, `a?.[i]` (the token on
2606
- * the member access) and `f?.()` (the token on the call). The receiver
2607
- * lowers once; when it is a unit-armed union with ONE non-unit arm, the
2608
- * member/call lowers exactly as its non-optional spelling would — the
2609
- * receiver node reads back as the chain's bound narrowed value
2610
- * (chainRecv) and types as its non-nullish type — and the whole thing
2611
- * becomes an optChain node: tag test, undefined on the unit path (JS:
2612
- * null receivers still yield undefined), the body lazily otherwise,
2613
- * argument side effects included. A receiver the checker types
2614
- * never-nullish makes `?.` behave exactly like `.` — the guard folds
2615
- * away and the plain lowering is the value (trust-the-checker, like
2616
- * ??'s fold). Multi-step TAILS (`a?.b.c`, `x?.trim().toLowerCase()`)
2617
- * short-circuit whole: the guarded member step is the chain's dot and
2618
- * every later step lowers inside the guard, checker-narrowed non-nullish
2619
- * (see chainTailDot); sub-union receivers are fenced with rewrite
2620
- * hints. */
2621
- /** The unhandled `?.`-carrying MEMBER step in this node's receiver
2622
- * spine, when the node is the TAIL of an optional chain whose token sits
2623
- * deeper — `x?.trim().toLowerCase()` reads nothing past the guard when x
2624
- * is nullish, so the whole tail must lower inside it. Walks
2625
- * property/element accesses and call steps only (parens and `!` break
2626
- * the chain per the grammar: `(a?.b).c` throws on undefined in JS);
2627
- * the node's OWN token — and a call's immediate callee token — are the
2628
- * plain entries' cases, not tails, and handled markers make the chain
2629
- * lowering's re-dispatch walk past its own step. `f?.()` steps stay out
2630
- * (a call carrying the token has no member step to re-enter — the
2631
- * split-it fence keeps them). */
2632
- function chainTailDot(lowerer, expr) {
2633
- let cur = expr;
2634
- for (;;) {
2635
- // An active chain's bound receiver read is a LEAF — its own token
2636
- // was consumed by the chain that bound it.
2637
- if (lowerer.chainRecvByNode.has(cur))
2638
- return null;
2639
- if (ts.isCallExpression(cur)) {
2640
- // `f?.()` steps route through their own entry (or are being
2641
- // handled); a deeper token under one is that chain's business.
2642
- if (cur.questionDotToken)
2643
- return null;
2644
- cur = cur.expression;
2645
- continue;
2646
- }
2647
- if (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur)) {
2648
- if (cur.questionDotToken) {
2649
- // The node's own token and a call's immediate callee token are
2650
- // the plain entries' cases; a handled one is mid-re-dispatch
2651
- // (its receiver read, further down, ends the walk). The walk
2652
- // NEVER continues past an unhandled deeper token: that guard is
2653
- // where this tail's chain enters — anything below it belongs to
2654
- // the receiver's own (nested) chain.
2655
- if (cur === expr || lowerer.chainHandled.has(cur)) {
2656
- cur = cur.expression;
2657
- continue;
2658
- }
2659
- if (ts.isCallExpression(expr) && expr.expression === cur)
2660
- return null;
2661
- return cur;
2662
- }
2663
- cur = cur.expression;
2664
- continue;
2665
- }
2666
- return null;
2667
- }
2668
- }
2669
- /** True when `expr` is `require.main.filename` (either dot optional) on
2670
- * the AMBIENT CommonJS require — the entry-module identity read. The
2671
- * value is the ENTRY file's path, a compile-time constant like
2672
- * __filename; ESM files stay out (Node never defines require there —
2673
- * the ambient symbol would not resolve anyway). Shared by the property
2674
- * lowering (the fold) and lowerStringMethodCall's receiver gate (the
2675
- * checker types the chain `string | undefined`, but the folded receiver
2676
- * IS a string). */
2677
- export function isRequireMainFilename(lowerer, expr) {
2678
- if (!ts.isPropertyAccessExpression(expr) || expr.name.text !== "filename")
2679
- return false;
2680
- const main = expr.expression;
2681
- if (!ts.isPropertyAccessExpression(main) || main.name.text !== "main")
2682
- return false;
2683
- if (!lowerer.isStdlibGlobal(main.expression, "require"))
2684
- return false;
2685
- return !isNodeEsmFile(expr.getSourceFile());
2686
- }
2687
- /** True when `expr` is the tail of an optional chain the STATIC chain
2688
- * machinery should short-circuit whole: an unhandled deeper `?.` whose
2689
- * guarded receiver lowers as a unit-armed union. Dyn ('unknown') and
2690
- * island ('any') receivers answer their tails through their own
2691
- * undefined-propagating reads and stay out; never-nullish receivers fold
2692
- * at the token's own entry. */
2693
- function chainTailClaimed(lowerer, expr) {
2694
- const tail = chainTailDot(lowerer, expr);
2695
- if (!tail)
2696
- return false;
2697
- const recvT = lowerer.mapTypeOf(lowerer.typeOf(tail.expression));
2698
- if (recvT?.kind !== "union")
2699
- return false;
2700
- const def = lowerer.unions.get(recvT.unionId);
2701
- return !!def && def.arms.some(isUnitType);
2702
- }
2703
- /** True when an EARLIER step of this access chain carries `?.` — JS
2704
- * short-circuits the whole tail (`a?.b.c` reads nothing when a is
2705
- * nullish), so a dyn tail read must answer undefined instead of
2706
- * throwing. Walks the receiver spine only; the argument of the access
2707
- * itself is not part of the guard. */
2708
- function chainGuardedByQuestionDot(expr) {
2709
- let cur = expr;
2710
- for (;;) {
2711
- if (ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur)) {
2712
- cur = cur.expression;
2713
- continue;
2714
- }
2715
- if (ts.isPropertyAccessExpression(cur) ||
2716
- ts.isElementAccessExpression(cur) ||
2717
- ts.isCallExpression(cur)) {
2718
- if (cur.questionDotToken)
2719
- return true;
2720
- cur = cur.expression;
2721
- continue;
2722
- }
2723
- return false;
2724
- }
2725
- }
2726
- export function lowerOptionalChain(lowerer, expr) {
2727
- const loc = locOf(expr);
2728
- // The node CARRYING the ?. token and the receiver expression it guards.
2729
- let dotNode;
2730
- let recvNode;
2731
- if (ts.isCallExpression(expr) && expr.questionDotToken) {
2732
- dotNode = expr; // f?.()
2733
- recvNode = expr.expression;
2734
- }
2735
- else if (ts.isCallExpression(expr) &&
2736
- ts.isPropertyAccessExpression(expr.expression) &&
2737
- expr.expression.questionDotToken) {
2738
- dotNode = expr.expression; // a?.m()
2739
- recvNode = expr.expression.expression;
2740
- }
2741
- else if ((ts.isPropertyAccessExpression(expr) || ts.isElementAccessExpression(expr)) &&
2742
- expr.questionDotToken) {
2743
- dotNode = expr; // a?.b / a?.[i]
2744
- recvNode = expr.expression;
2745
- }
2746
- else {
2747
- // TAIL entry: the token sits deeper in the receiver spine and JS
2748
- // short-circuits the WHOLE tail with it (`x?.trim().toLowerCase()`
2749
- // reads nothing when x is nullish). The guarded member step becomes
2750
- // the chain's dot; every step above it lowers inside the guard, its
2751
- // checker type narrowed non-nullish below (the tag test proved it).
2752
- const tail = chainTailDot(lowerer, expr);
2753
- if (!tail) {
2754
- // A `f?.()` step under a member tail, or no token at all (a
2755
- // dispatch bug): short-circuiting those is not modeled.
2756
- lowerer.unsupported("SC1090", expr, "multi-step optional chains (split them: const v = a?.b; then use v?.c)");
2757
- }
2758
- dotNode = tail;
2759
- recvNode = tail.expression;
2760
- }
2761
- const receiver = lowerer.lowerExpr(recvNode);
2762
- if (receiver.type.kind === "dyn") {
2763
- // `pkg?.name` / `pkg?.scripts?.[k]` on a JSON.parse result: dyn
2764
- // represents undefined directly, so the chain step IS the keyed
2765
- // read with the optional (unit-answers-undefined) policy — no
2766
- // optChain wrapper needed; nested steps compose the same way.
2767
- if (dotNode === expr && ts.isPropertyAccessExpression(expr)) {
2768
- const key = { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) };
2769
- return lowerer.maybeNarrow({ kind: "dynKeyGet", key, optional: true, value: receiver, type: DYN, loc }, expr);
2770
- }
2771
- if (dotNode === expr && ts.isElementAccessExpression(expr)) {
2772
- const key = lowerer.lowerExpr(expr.argumentExpression);
2773
- if (key.type.kind === "string") {
2774
- return lowerer.maybeNarrow({ kind: "dynKeyGet", key, optional: true, value: receiver, type: DYN, loc }, expr);
2775
- }
2776
- // NUMBER-typed indices (`entries?.[0]`): the property key is
2777
- // ToString(i), exactly JS — the canonical number text answers
2778
- // array indices in the dyn helper, anything else (fractions,
2779
- // negatives, NaN) reads as an absent key.
2780
- if (key.type.kind === "f64") {
2781
- const skey = { kind: "toString", operand: key, type: STRING, loc: key.loc };
2782
- return lowerer.maybeNarrow({ kind: "dynKeyGet", key: skey, optional: true, value: receiver, type: DYN, loc }, expr);
2783
- }
2784
- }
2785
- // The METHOD-call step (`rawName?.match(re)` on a dyn value): a
2786
- // nullish receiver short-circuits to the undefined dyn singleton
2787
- // (dyn represents undefined directly); anything else runs the
2788
- // VALIDATED dynamic dispatch — the same one the truthy-guarded
2789
- // spelling (`v ? v.match(...) : null`) compiles to, Node-shaped
2790
- // TypeError on a kind mismatch included. The dispatch's static
2791
- // result converts back into the checked-dynamic tree (the chain's checker type is
2792
- // the error-any world's `any`, so dyn IS its representation).
2793
- if (ts.isCallExpression(expr) &&
2794
- ts.isPropertyAccessExpression(dotNode) &&
2795
- dotNode === expr.expression) {
2796
- const m = dotNode.name.text;
2797
- const id = `chain.${lowerer.chainCounter++}`;
2798
- const recvRef = { kind: "chainRecv", id, type: DYN, loc: locOf(recvNode) };
2799
- lowerer.chainRecvByNode.set(recvNode, recvRef);
2800
- lowerer.chainHandled.add(dotNode);
2801
- let body;
2802
- try {
2803
- body = lowerer.lowerExpr(expr);
2804
- }
2805
- finally {
2806
- lowerer.chainRecvByNode.delete(recvNode);
2807
- lowerer.chainHandled.delete(dotNode);
2808
- }
2809
- if (body.type.kind !== "dyn") {
2810
- if (body.type.kind !== "void" && !lowerer.dynConvertible(body.type)) {
2811
- lowerer.unsupported("SC1100", expr, `optional METHOD calls on 'unknown' values where the result ('${lowerer.fmt(body.type)}' ` +
2812
- `from '.${m}(...)') has no dynamic representation`);
2813
- }
2814
- if (body.type.kind === "void") {
2815
- return { kind: "optChain", id, receiver, body, type: VOID, loc };
2816
- }
2817
- body = { kind: "dynFrom", value: body, type: DYN, loc };
2818
- }
2819
- return { kind: "optChain", id, receiver, body, type: DYN, loc };
2820
- }
2821
- lowerer.unsupported("SC1100", expr, "optional chaining on 'unknown' values");
2822
- }
2823
- // An 'any' (island-handle) receiver: the nullish test asks the ENGINE
2824
- // value at runtime — null/undefined short-circuit to the engine's
2825
- // undefined (JS: null receivers yield undefined too), anything else
2826
- // proceeds as the plain island operation with the receiver evaluated
2827
- // once (argument side effects stay lazy, like every optChain). The
2828
- // result is an island value again ('any' in, 'any' out).
2829
- if (receiver.type.kind === "jsval") {
2830
- const id = `chain.${lowerer.chainCounter++}`;
2831
- const recvRef = { kind: "chainRecv", id, type: JSVAL, loc: locOf(recvNode) };
2832
- if (dotNode === expr && ts.isCallExpression(expr)) {
2833
- // `a.b?.(...)` — a MEMBER callee: JS calls it with `this` bound to
2834
- // `a` and short-circuits a nullish member — exactly the engine's
2835
- // own `o.name?.()` (optCallMethod: the RECEIVER is `a`, evaluated
2836
- // once through the chain; a nullish `a` itself short-circuits
2837
- // earlier like any optional chain). Computed members keep a fence.
2838
- if (ts.isPropertyAccessExpression(recvNode) || ts.isElementAccessExpression(recvNode)) {
2839
- if (ts.isPropertyAccessExpression(recvNode) &&
2840
- !recvNode.questionDotToken) {
2841
- const obj = lowerer.lowerExpr(recvNode.expression);
2842
- if (obj.type.kind !== "jsval")
2843
- lowerer.badType(recvNode.expression, lowerer.typeOf(recvNode.expression));
2844
- const args = expr.arguments.map((a) => lowerer.jsvalIn(lowerer.lowerExpr(a), a));
2845
- return { kind: "jsOp", op: "optCallMethod", name: recvNode.name.text, args: [obj, ...args], type: JSVAL, loc };
2846
- }
2847
- lowerer.unsupported("SC1090", expr, "optional calls of computed 'any' member values (a[k]?.() — bind the member to a const first)");
2848
- }
2849
- const args = expr.arguments.map((a) => lowerer.jsvalIn(lowerer.lowerExpr(a), a));
2850
- const body = { kind: "jsOp", op: "callFn", args: [recvRef, ...args], type: JSVAL, loc };
2851
- return { kind: "optChain", id, receiver, body, type: JSVAL, loc };
2852
- }
2853
- // Member forms (`x?.y`, `x?.y(...)`, `x?.[i]`): re-dispatch the plain
2854
- // island lowering with the receiver node bound to the chain.
2855
- lowerer.chainRecvByNode.set(recvNode, recvRef);
2856
- lowerer.chainHandled.add(dotNode);
2857
- let body;
2858
- try {
2859
- body = lowerer.lowerExpr(expr);
2860
- }
2861
- finally {
2862
- lowerer.chainRecvByNode.delete(recvNode);
2863
- lowerer.chainHandled.delete(dotNode);
2864
- }
2865
- if (body.type.kind !== "jsval")
2866
- lowerer.badType(expr, lowerer.typeOf(expr));
2867
- return { kind: "optChain", id, receiver, body, type: JSVAL, loc };
2868
- }
2869
- const def = receiver.type.kind === "union" ? lowerer.unions.get(receiver.type.unionId) : undefined;
2870
- if (!def || !def.arms.some(isUnitType)) {
2871
- // Never nullish: `?.` IS `.` — re-dispatch the plain lowering. The
2872
- // receiver subtree above is discarded (lowering is pure IR
2873
- // construction); the fresh dispatch lowers it again in place. The
2874
- // checker may still SPELL the receiver nullable while the lowering
2875
- // answers the plain value (`process.getuid?.()` is the POSIX
2876
- // number), so narrow the node too — downstream receiver-typed
2877
- // dispatch must agree with the lowered value, not the spelling.
2878
- lowerer.chainHandled.add(dotNode);
2879
- const hadNarrow = lowerer.chainNarrowedType.has(recvNode);
2880
- if (!hadNarrow) {
2881
- lowerer.chainNarrowedType.set(recvNode, lowerer.checker.getNonNullableType(lowerer.checker.getTypeAtLocation(recvNode)));
2882
- }
2883
- try {
2884
- return lowerer.lowerExpr(expr);
2885
- }
2886
- finally {
2887
- lowerer.chainHandled.delete(dotNode);
2888
- if (!hadNarrow)
2889
- lowerer.chainNarrowedType.delete(recvNode);
2890
- }
2891
- }
2892
- const rest = def.arms.filter((a) => !isUnitType(a));
2893
- if (rest.length !== 1) {
2894
- lowerer.unsupported("SC1090", expr, `'?.' on '${lowerer.fmt(receiver.type)}' (the guarded receiver is a sub-union; check a discriminant field first)`);
2895
- }
2896
- const narrowed = rest[0];
2897
- const id = `chain.${lowerer.chainCounter++}`;
2898
- const recvRef = { kind: "chainRecv", id, type: narrowed, loc: locOf(recvNode) };
2899
- // `f?.()`: the callee IS the guarded value — build the indirect call
2900
- // directly (no member dispatch exists to re-enter).
2901
- if (dotNode === expr && ts.isCallExpression(expr)) {
2902
- if (narrowed.kind !== "func")
2903
- lowerer.badType(recvNode, lowerer.typeOf(recvNode));
2904
- const params = narrowed.params;
2905
- const args = expr.arguments.map((a, i) => lowerer.lowerExprExpecting(a, params[i]));
2906
- const body = { kind: "callValue", callee: recvRef, args, type: narrowed.ret, loc };
2907
- return lowerer.finishOptionalChain(expr, id, receiver, body, loc);
2908
- }
2909
- // Member forms: re-dispatch the normal lowering with the receiver node
2910
- // bound to the chain (reads as chainRecv, types as non-nullish).
2911
- const narrowedTs = lowerer.checker.getNonNullableType(lowerer.checker.getTypeAtLocation(recvNode));
2912
- lowerer.chainRecvByNode.set(recvNode, recvRef);
2913
- lowerer.chainNarrowedType.set(recvNode, narrowedTs);
2914
- lowerer.chainHandled.add(dotNode);
2915
- // A TAIL entry's intermediate steps (`x?.trim()` inside
2916
- // `x?.trim().toLowerCase()`) checker-type with the chain's `|
2917
- // undefined` even though inside the guard they are proven non-nullish
2918
- // — narrow each so the downstream method/member dispatch rides the
2919
- // real receiver kind. Only nodes this chain registers are cleaned up.
2920
- const tailSteps = [];
2921
- if (dotNode !== expr && !(ts.isCallExpression(expr) && expr.expression === dotNode)) {
2922
- for (let cur = expr; cur !== dotNode;) {
2923
- const next = ts.isCallExpression(cur)
2924
- ? cur.expression
2925
- : cur.expression;
2926
- if (next === dotNode)
2927
- break;
2928
- if (!lowerer.chainNarrowedType.has(next)) {
2929
- lowerer.chainNarrowedType.set(next, lowerer.checker.getNonNullableType(lowerer.checker.getTypeAtLocation(next)));
2930
- tailSteps.push(next);
2931
- }
2932
- cur = next;
2933
- }
2934
- if (!lowerer.chainNarrowedType.has(dotNode)) {
2935
- lowerer.chainNarrowedType.set(dotNode, lowerer.checker.getNonNullableType(lowerer.checker.getTypeAtLocation(dotNode)));
2936
- tailSteps.push(dotNode);
2937
- }
2938
- }
2939
- let body;
2940
- try {
2941
- body = lowerer.lowerExpr(expr);
2942
- }
2943
- finally {
2944
- lowerer.chainRecvByNode.delete(recvNode);
2945
- lowerer.chainNarrowedType.delete(recvNode);
2946
- lowerer.chainHandled.delete(dotNode);
2947
- for (const n of tailSteps)
2948
- lowerer.chainNarrowedType.delete(n);
2949
- }
2950
- return lowerer.finishOptionalChain(expr, id, receiver, body, loc);
2951
- }
2952
- /** The optChain node around a lowered chain body: void bodies keep the
2953
- * statement form (`cb?.();` — the checker's `void | undefined` result IS
2954
- * void here); value bodies wrap into the checker's undefined-armed
2955
- * result union. */
2956
- export function finishOptionalChain(lowerer, expr, id, receiver, body, loc) {
2957
- if (body.type.kind === "void") {
2958
- return { kind: "optChain", id, receiver, body, type: VOID, loc };
2959
- }
2960
- // A dyn body (`pricing?.[key]` — an unknown-valued index-signature
2961
- // read) stays dyn: `unknown | undefined` IS unknown, and dyn represents
2962
- // undefined directly (the unit path yields the undefined dyn value).
2963
- if (body.type.kind === "dyn") {
2964
- return { kind: "optChain", id, receiver, body, type: DYN, loc };
2965
- }
2966
- let type = lowerer.irTypeOf(expr);
2967
- const hiddenOptionalResult = (type.kind !== "union" || lowerer.armTag(type.unionId, UNDEFINED_T) < 0) &&
2968
- receiver.type.kind === "union" && lowerer.armTag(receiver.type.unionId, UNDEFINED_T) >= 0 &&
2969
- body.type.kind !== "generator" && body.type.kind !== "jsval"
2970
- ? lowerer.withUndefinedArmOf(body.type)
2971
- : null;
2972
- if (hiddenOptionalResult)
2973
- type = hiddenOptionalResult;
2974
- if (type.kind !== "union" || lowerer.armTag(type.unionId, UNDEFINED_T) < 0) {
2975
- lowerer.unsupported("SC1090", expr, `'?.' where the result '${lowerer.fmt(type)}' has no undefined arm (narrow the receiver with 'if (x !== undefined)' instead)`);
2976
- }
2977
- const wrapped = lowerer.coerceToExpected(body, type);
2978
- if (!typeEquals(wrapped.type, type))
2979
- lowerer.badType(expr, lowerer.typeOf(expr));
2980
- return { kind: "optChain", id, receiver, body: wrapped, type, loc };
2981
- }
2982
2828
  /** A CONDITION-position expression: the result is consumed as a bool
2983
2829
  * only, so `&&`/`||` descend recursively over ToBoolean'd operands —
2984
2830
  * JS-exact (`ToBoolean(a && b)` ≡ `ToBoolean(a) && ToBoolean(b)`), still
@@ -2988,6 +2834,11 @@ export function lowerCondition(lowerer, expr) {
2988
2834
  let e = expr;
2989
2835
  while (ts.isParenthesizedExpression(e))
2990
2836
  e = e.expression;
2837
+ if (ts.isPropertyAccessExpression(e)) {
2838
+ const presence = lowerPromiseThenPresence(lowerer, e, { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) });
2839
+ if (presence !== null)
2840
+ return presence;
2841
+ }
2991
2842
  if (ts.isBinaryExpression(e)) {
2992
2843
  const op = e.operatorToken.kind;
2993
2844
  if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) {
@@ -3021,6 +2872,47 @@ export function lowerCondition(lowerer, expr) {
3021
2872
  }
3022
2873
  return lowerer.ensureBool(lowerAbsenceProbe(lowerer, expr) ?? lowerer.lowerExpr(expr), expr);
3023
2874
  }
2875
+ function lowerPromiseThenPresence(lowerer, access, result) {
2876
+ if (access.name.text !== "then")
2877
+ return null;
2878
+ const mapped = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
2879
+ const promiseType = mapped?.kind === "promise"
2880
+ ? mapped
2881
+ : mapped?.kind === "union"
2882
+ ? (() => {
2883
+ const arms = lowerer.unions.get(mapped.unionId)?.arms ?? [];
2884
+ const promises = arms.filter((arm) => arm.kind === "promise");
2885
+ return promises.length === 1 && arms.every((arm) => arm.kind === "promise" || isUnitType(arm))
2886
+ ? promises[0]
2887
+ : null;
2888
+ })()
2889
+ : null;
2890
+ if (promiseType === null)
2891
+ return null;
2892
+ let receiver = lowerer.lowerExpr(access.expression);
2893
+ if (receiver.type.kind === "union") {
2894
+ const optional = lowerer.runtimeOptionalPropertyReceiver(access.expression, receiver, promiseType, access.name.text);
2895
+ if (optional !== null)
2896
+ receiver = optional;
2897
+ else {
2898
+ const helper = lowerer.narrowedArmHelper(receiver.type.unionId, promiseType, receiver.loc);
2899
+ if (helper === null)
2900
+ return null;
2901
+ receiver = { kind: "call", callee: helper, args: [receiver], type: promiseType, loc: receiver.loc };
2902
+ }
2903
+ }
2904
+ if (receiver.type.kind !== "promise")
2905
+ return null;
2906
+ if (isSafeToDiscard(receiver))
2907
+ return result;
2908
+ return {
2909
+ kind: "seqExpr",
2910
+ stmts: [{ kind: "exprStmt", expr: receiver, loc: receiver.loc }],
2911
+ result,
2912
+ type: result.type,
2913
+ loc: result.loc,
2914
+ };
2915
+ }
3024
2916
  function runtimeOptionalLocalOf(lowerer, node) {
3025
2917
  let expr = node;
3026
2918
  while (ts.isParenthesizedExpression(expr))
@@ -3153,6 +3045,10 @@ export function lowerIntrinsicProperty(lowerer, expr) {
3153
3045
  if (kind === "child" ? !isChildSurfaceMember(lowerer, expr) : !tupleLengthOnArray && !lowerer.isStdlibMember(expr))
3154
3046
  return null;
3155
3047
  const name = expr.name.text;
3048
+ const strictReceiver = (expected) => {
3049
+ const value = lowerer.lowerExpr(expr.expression);
3050
+ return lowerer.runtimeOptionalPropertyReceiver(expr.expression, value, expected, name) ?? value;
3051
+ };
3156
3052
  if (kind === "child") {
3157
3053
  const loc = locOf(expr);
3158
3054
  // The lifecycle reads, Node's exact shapes (SEMANTICS.md pins the
@@ -3315,7 +3211,7 @@ export function lowerIntrinsicProperty(lowerer, expr) {
3315
3211
  }
3316
3212
  if (kind === "regex") {
3317
3213
  if (name === "source" || name === "flags") {
3318
- const receiver = lowerer.lowerExpr(expr.expression);
3214
+ const receiver = strictReceiver({ kind: "regex" });
3319
3215
  return { kind: "regexIntrinsic", method: name, receiver, args: [], type: STRING, loc: locOf(expr) };
3320
3216
  }
3321
3217
  if (name === "test") {
@@ -3334,7 +3230,19 @@ export function lowerIntrinsicProperty(lowerer, expr) {
3334
3230
  if (kind === "bytes") {
3335
3231
  const loc = locOf(expr);
3336
3232
  if (name === "length" || name === "byteLength") {
3337
- const receiver = lowerer.lowerExpr(expr.expression);
3233
+ const expected = lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
3234
+ let receiver = expected?.kind === "bytes"
3235
+ ? strictReceiver(expected)
3236
+ : lowerer.lowerExpr(expr.expression);
3237
+ if (receiver.type.kind === "union" && lowerer.armTag(receiver.type.unionId, UNDEFINED_T) >= 0) {
3238
+ const present = lowerer.stripUndefinedArm(receiver.type);
3239
+ const helper = present.kind === "bytes"
3240
+ ? lowerer.narrowedArmHelper(receiver.type.unionId, present, locOf(expr.expression))
3241
+ : null;
3242
+ receiver = helper
3243
+ ? { kind: "call", callee: helper, args: [receiver], type: present, loc: locOf(expr.expression) }
3244
+ : lowerer.maybeNarrow(receiver, expr.expression);
3245
+ }
3338
3246
  // An island handle behind a typed-array .d.ts surface: the engine
3339
3247
  // property read, exiting at the declared number type (the array
3340
3248
  // .length rule). Chain-handled reads stay jsval.
@@ -3387,7 +3295,21 @@ export function lowerIntrinsicProperty(lowerer, expr) {
3387
3295
  return null;
3388
3296
  }
3389
3297
  if (name === "length") {
3390
- const receiver = lowerer.lowerExpr(expr.expression);
3298
+ const expected = kind === "string"
3299
+ ? STRING
3300
+ : lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
3301
+ let receiver = expected && (expected.kind === "string" || expected.kind === "array")
3302
+ ? strictReceiver(expected)
3303
+ : lowerer.lowerExpr(expr.expression);
3304
+ if (receiver.type.kind === "union" && lowerer.armTag(receiver.type.unionId, UNDEFINED_T) >= 0 && kind === "array") {
3305
+ const present = lowerer.stripUndefinedArm(receiver.type);
3306
+ const helper = present.kind === "array"
3307
+ ? lowerer.narrowedArmHelper(receiver.type.unionId, present, locOf(expr.expression))
3308
+ : null;
3309
+ receiver = helper
3310
+ ? { kind: "call", callee: helper, args: [receiver], type: present, loc: locOf(expr.expression) }
3311
+ : lowerer.maybeNarrow(receiver, expr.expression);
3312
+ }
3391
3313
  // An island handle behind an array-typed .d.ts surface
3392
3314
  // (`parts().length` on a declared `string[]` return — arrays never
3393
3315
  // exit eagerly, so the value stays jsval): an engine property read,
@@ -3516,6 +3438,27 @@ export function lowerArrayLiteral(lowerer, expr, expected) {
3516
3438
  }
3517
3439
  }
3518
3440
  }
3441
+ // An array literal inside an npm-static implicit-any instance can
3442
+ // inherit the checker's poisoned any[] context even after the
3443
+ // active binding gives every element one concrete lowering. Recover a
3444
+ // static array only when every spelled element lowers to the same
3445
+ // supported type. An explicit checked-dynamic destination still owns
3446
+ // the later slot coercion; heterogeneous literals, holes, and spreads
3447
+ // keep the checked-dynamic fallback below.
3448
+ if (lowerer.implicitParamTypes !== null &&
3449
+ npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null &&
3450
+ (mapped === null || mapped.kind === "dyn" ||
3451
+ (mapped.kind === "array" && mapped.elem.kind === "dyn")) &&
3452
+ expr.elements.length > 0 &&
3453
+ expr.elements.every((element) => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element))) {
3454
+ const elements = expr.elements.map((element) => tryLowerExpression(lowerer, element));
3455
+ const first = elements[0];
3456
+ if (first !== undefined && first !== null &&
3457
+ isSupportedArrayElem(first.type) &&
3458
+ elements.every((element) => element !== null && typeEquals(element.type, first.type))) {
3459
+ mapped = arrayOf(first.type);
3460
+ }
3461
+ }
3519
3462
  // An EMPTY literal under a CONST ASSERTION whose type queries panicked
3520
3463
  // (tsgo's readonly-[] TupleType conversion — the facade answers `any`,
3521
3464
  // which maps to nothing statically and to an island value under
@@ -3598,7 +3541,24 @@ export function lowerArrayLiteral(lowerer, expr, expected) {
3598
3541
  lowerer.badType(expr, tsType);
3599
3542
  }
3600
3543
  const type = mapped;
3544
+ // Keep the array payload type fixed. Optional reads are stored through
3545
+ // arrayValueStore, which records UNDEFINED in the state byte while the
3546
+ // payload remains number/string/etc.; widening the array element here
3547
+ // would force boxed union storage and change aliases.
3601
3548
  const spreads = [];
3549
+ const holes = new Set();
3550
+ const runtimeOptionalElements = new Set();
3551
+ let needsStateBuild = false;
3552
+ for (let i = 0; i < expr.elements.length; i++) {
3553
+ const candidate = expr.elements[i];
3554
+ if (ts.isSpreadElement(candidate) || ts.isOmittedExpression(candidate))
3555
+ continue;
3556
+ const probe = tryLowerExpression(lowerer, candidate);
3557
+ if (probe && lowerer.runtimeOptionalWidening(probe.type, type.elem) !== null) {
3558
+ runtimeOptionalElements.add(i);
3559
+ needsStateBuild = true;
3560
+ }
3561
+ }
3602
3562
  const elems = expr.elements.map((el, i) => {
3603
3563
  if (ts.isSpreadElement(el)) {
3604
3564
  // `[...xs, b]`: xs must be an array of the literal's own element
@@ -3664,1807 +3624,166 @@ export function lowerArrayLiteral(lowerer, expr, expected) {
3664
3624
  // (string | symbol)[] literal — per-element wrap/width copy):
3665
3625
  // the interned width helper reshapes before the spread copies.
3666
3626
  if (src.type.kind === "array" && !typeEquals(src.type, type)) {
3667
- const w = lowerer.widthCoerce(src, type);
3668
- if (w)
3669
- src = w;
3670
- }
3671
- if (!typeEquals(src.type, type)) {
3672
- lowerer.unsupported("SC1090", el, `spreading '${lowerer.fmt(src.type)}' into a '${lowerer.fmt(type)}' literal (only a same-element-type array spreads)`);
3673
- }
3674
- spreads.push(i);
3675
- return src;
3676
- }
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.
3682
- 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");
3687
- }
3688
- // An ARRAY-LITERAL element under a UNION element slot whose own type
3689
- // has no lift into the slot builds against the union's single array
3690
- // arm instead (`['pwd', []]` as (string | string[])[] — tsc pushes no
3691
- // contextual type through the unannotated chain, so the empty nested
3692
- // literal types never[], whose f64-element representation re-tags
3693
- // into nothing). One array arm is unambiguous — the union-slot rule's
3694
- // stance; several keep the fence.
3695
- if (type.elem.kind === "union" || type.elem.kind === "array") {
3696
- let x = el;
3697
- while (ts.isParenthesizedExpression(x))
3698
- x = x.expression;
3699
- if (ts.isArrayLiteralExpression(x)) {
3700
- const ownT = lowerer.mapTypeOf(lowerer.checker.getContextualType(x) ?? lowerer.typeOf(x));
3701
- if (ownT === null || lowerer.widthLiftPlan(ownT, type.elem) === null) {
3702
- // An ARRAY-typed slot takes the literal directly; a union slot
3703
- // routes through its single array arm and wraps.
3704
- if (type.elem.kind === "array") {
3705
- return lowerer.lowerArrayLiteral(x, type.elem);
3706
- }
3707
- const def = lowerer.unions.get(type.elem.unionId);
3708
- const arrayArms = def?.arms.filter((a) => a.kind === "array") ?? [];
3709
- if (arrayArms.length === 1) {
3710
- const built = lowerer.lowerArrayLiteral(x, arrayArms[0]);
3711
- return lowerer.coerceInto(el, built, type.elem);
3712
- }
3713
- }
3714
- }
3715
- }
3716
- // Elements flow into the element slot like an assignment: union
3717
- // elements wrap plain arm values (`[1, "a"]` as (number | string)[]),
3718
- // holes reject inside lowerExpr.
3719
- const lowered = lowerer.lowerExprExpecting(el, type.elem);
3720
- if (!typeEquals(lowered.type, type.elem))
3721
- lowerer.badType(el, lowerer.typeOf(el));
3722
- return lowered;
3723
- });
3724
- return { kind: "arrayLit", elems, ...(spreads.length > 0 ? { spreads } : {}), type, loc };
3725
- }
3726
- /** `{ a: 1, b: "x" }` → recordLit. The record type comes from the
3727
- * contextual type when tsc has one (annotated declarations, arguments,
3728
- * nested literals) and from the literal's own type otherwise — both intern
3729
- * to the same shapeId unless width subtyping is in play (an `as` cast can
3730
- * smuggle a wider/narrower literal past tsc's freshness check), which the
3731
- * exact-shape checks below reject with SC2002. Fields lower IN SOURCE
3732
- * ORDER: JS evaluates property values in source order. */
3733
- /** The dropped-field names of the PromiseSettledResult honest subset
3734
- * (SEMANTICS.md 46): when the literal's target type is (a union
3735
- * containing) the lib's PromiseFulfilledResult / PromiseRejectedResult,
3736
- * the record mapping kept only `status` — `value` and `reason` evaluate
3737
- * for effect and are not stored. Null for every other target. */
3738
- function settledDropNames(lowerer, tsType) {
3739
- let names = null;
3740
- const parts = tsType.isUnionType() ? ts.constituentTypes(tsType) : [tsType];
3741
- for (const part of parts) {
3742
- const sym = part.getSymbol();
3743
- if (!sym ||
3744
- !lowerer.checker.declarationsOf(sym).some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()))) {
3745
- continue;
3746
- }
3747
- if (sym.name === "PromiseFulfilledResult")
3748
- (names ??= new Set()).add("value");
3749
- if (sym.name === "PromiseRejectedResult")
3750
- (names ??= new Set()).add("reason");
3751
- }
3752
- return names;
3753
- }
3754
- /** `{ [KEY]: v }` where KEY is a compile-time-known string or number:
3755
- * the key folds into an ordinary (spelled) property name — the record
3756
- * shape is exactly what tsc computed for the literal (the late-bound
3757
- * name IS the checker's literal type), so the fold is just spelling.
3758
- * PURE key forms only, so skipping the key's evaluation is exact (JS
3759
- * evaluates the key before the value; effectful expressions keep the
3760
- * fence): any side-effect-free expression — identifiers, property
3761
- * chains with no accessor on them (enum members, `other.name` reads),
3762
- * literals, parenthesized/as-wrapped forms, operators over those —
3763
- * whose checker type is ONE string or number literal (or a union whose
3764
- * arms all spell the same name — `E1.x || E2.x` where both are 0).
3765
- * Number keys take JS's canonical spelling (`{ [E.member]: v }` stores
3766
- * "0" — ToPropertyKey), exactly the name tsc late-bound. Templates
3767
- * fold structurally span by span as well (a template of literals whose
3768
- * checker type widened still spells one string). Symbol-typed and
3769
- * runtime-valued keys stay out. */
3770
- function literalComputedKey(lowerer, name) {
3771
- return foldedStringKeyOf(lowerer, name.expression);
3772
- }
3773
- export function foldedStringKeyOf(lowerer, expr) {
3774
- if (ts.isParenthesizedExpression(expr))
3775
- return foldedStringKeyOf(lowerer, expr.expression);
3776
- if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr))
3777
- return expr.text;
3778
- if (ts.isTemplateExpression(expr)) {
3779
- // Structural fold first — exact even where the checker widened the
3780
- // template's own type; the general type-directed fold below is the
3781
- // fallback (a template the checker DID type as one literal).
3782
- let out = expr.head.text;
3783
- let folded = true;
3784
- for (const span of expr.templateSpans) {
3785
- const part = foldedStringKeyOf(lowerer, span.expression);
3786
- if (part === null) {
3787
- folded = false;
3788
- break;
3789
- }
3790
- out += part + span.literal.text;
3791
- }
3792
- if (folded)
3793
- return out;
3794
- }
3795
- // The general fold: a PURE expression whose checker type spells one
3796
- // property name. An as-cast folds by its OWN checker type — that is
3797
- // the name tsc late-bound the property under, so shape and storage
3798
- // stay one name (the standard trust-the-checker bet); a cast that
3799
- // WIDENS to string un-late-binds the property, its type is no literal,
3800
- // and the fence stays.
3801
- if (!pureKeyExpr(lowerer, expr))
3802
- return null;
3803
- return literalKeySpellingOf(lowerer.typeOf(expr));
3804
- }
3805
- /** The property-name spelling of a single-literal checker type: string
3806
- * literals directly, number literals in JS's canonical ToString spelling
3807
- * (enum members included — their literal flags carry the value), and
3808
- * unions whose arms all spell the SAME name (`E1.x || E2.x` — two enum
3809
- * literal types, one value). Null for everything else. */
3810
- function literalKeySpellingOf(t) {
3811
- if (t.isStringLiteralType())
3812
- return t.value;
3813
- if (t.isNumberLiteralType())
3814
- return String(t.value);
3815
- if (t.isUnionType()) {
3816
- let out = null;
3817
- for (const arm of ts.constituentTypes(t)) {
3818
- const s = arm.isStringLiteralType() ? arm.value : arm.isNumberLiteralType() ? String(arm.value) : null;
3819
- if (s === null || (out !== null && s !== out))
3820
- return null;
3821
- out = s;
3822
- }
3823
- return out;
3824
- }
3825
- return null;
3826
- }
3827
- /** True iff evaluating `expr` can have no observable effect, so a fold
3828
- * may skip it: literals, identifier reads, property chains whose every
3829
- * member is a plain field/enum member (an accessor anywhere on the chain
3830
- * is a call), operators and casts over those. Calls, `new`, assignments,
3831
- * and element accesses stay impure. */
3832
- function pureKeyExpr(lowerer, expr) {
3833
- if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr))
3834
- return pureKeyExpr(lowerer, expr.expression);
3835
- if (ts.isAsExpression(expr) || ts.isTypeAssertion(expr))
3836
- return pureKeyExpr(lowerer, expr.expression);
3837
- if (ts.isIdentifier(expr))
3838
- return true;
3839
- if (ts.isStringLiteralLike(expr) || ts.isNumericLiteral(expr))
3840
- return true;
3841
- if (expr.kind === ts.SyntaxKind.TrueKeyword || expr.kind === ts.SyntaxKind.FalseKeyword ||
3842
- expr.kind === ts.SyntaxKind.NullKeyword)
3843
- return true;
3844
- if (ts.isPropertyAccessExpression(expr)) {
3845
- const sym = lowerer.checker.getSymbolAtLocation(expr.name);
3846
- if (sym && sym.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor))
3847
- return false;
3848
- return pureKeyExpr(lowerer, expr.expression);
3849
- }
3850
- if (ts.isPrefixUnaryExpression(expr) &&
3851
- (expr.operator === ts.SyntaxKind.MinusToken || expr.operator === ts.SyntaxKind.PlusToken ||
3852
- expr.operator === ts.SyntaxKind.ExclamationToken || expr.operator === ts.SyntaxKind.TildeToken)) {
3853
- return pureKeyExpr(lowerer, expr.operand);
3854
- }
3855
- if (ts.isBinaryExpression(expr)) {
3856
- const op = expr.operatorToken.kind;
3857
- const pureOp = op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken ||
3858
- op === ts.SyntaxKind.QuestionQuestionToken ||
3859
- (op >= ts.SyntaxKind.LessThanToken && op <= ts.SyntaxKind.CaretToken &&
3860
- op !== ts.SyntaxKind.InstanceOfKeyword && op !== ts.SyntaxKind.InKeyword);
3861
- return pureOp && pureKeyExpr(lowerer, expr.left) && pureKeyExpr(lowerer, expr.right);
3862
- }
3863
- if (ts.isConditionalExpression(expr)) {
3864
- return pureKeyExpr(lowerer, expr.condition) && pureKeyExpr(lowerer, expr.whenTrue) && pureKeyExpr(lowerer, expr.whenFalse);
3865
- }
3866
- if (ts.isTemplateExpression(expr))
3867
- return expr.templateSpans.every((s) => pureKeyExpr(lowerer, s.expression));
3868
- return false;
3869
- }
3870
- /** A property's field name: identifier/string-literal keys directly,
3871
- * foldable computed keys through literalComputedKey (the syntax pass
3872
- * fenced everything else). */
3873
- function propNameText(lowerer, name) {
3874
- if (ts.isComputedPropertyName(name)) {
3875
- const k = literalComputedKey(lowerer, name);
3876
- if (k === null)
3877
- throw new InternalCompilerError("lowerer bug: unfoldable computed key past the fence");
3878
- return k;
3879
- }
3880
- // Numeric literals name their canonical string key (JS's ToPropertyKey
3881
- // — the same name the checker gives the property symbol).
3882
- if (ts.isNumericLiteral(name))
3883
- return String(Number(name.text));
3884
- return name.text;
3885
- }
3886
- /** The runtime-keyed JS object literal (a computed key that doesn't fold):
3887
- * builds a dyn object member-by-member. Keys evaluate before their values,
3888
- * properties in source order — JS's object-literal evaluation exactly.
3889
- * Identifier/string keys are compile-time strings; numeric keys take
3890
- * their canonical number string (`{ 0x10: v }` stores "16" — JS's
3891
- * ToPropertyKey); computed keys evaluate and pass through ToString (the
3892
- * dyn's String() for dyn operands — `{ [field]: v }` where field is a
3893
- * checked-dynamic param). Values convert through the usual dyn boundary
3894
- * (dynFrom's domain, functions box); a value with no dyn representation
3895
- * fences per property. Spreads copy through dyn.assign in source order;
3896
- * accessors stay fenced. */
3897
- export function lowerDynObjectLiteral(lowerer, expr, boxValue) {
3898
- const loc = locOf(expr);
3899
- let fields = [];
3900
- let acc = null;
3901
- const flushFields = () => {
3902
- if (fields.length === 0)
3903
- return;
3904
- const chunk = { kind: "dynObjLit", fields, type: DYN, loc };
3905
- acc = acc === null
3906
- ? chunk
3907
- : { kind: "libCall", fn: "dyn.assign", args: [acc, chunk], type: DYN, loc };
3908
- fields = [];
3909
- };
3910
- for (const prop of expr.properties) {
3911
- if (ts.isSpreadAssignment(prop)) {
3912
- flushFields();
3913
- const raw = lowerer.lowerExpr(prop.expression);
3914
- const source = boxValue
3915
- ? boxValue(prop.expression, raw)
3916
- : lowerer.coerceToExpected(raw, DYN);
3917
- if (source.type.kind !== "dyn") {
3918
- lowerer.unsupported("SC1101", prop.expression, `spreading '${lowerer.fmt(source.type)}' into a checked-dynamic object literal`);
3919
- }
3920
- acc ??= { kind: "dynObjLit", fields: [], type: DYN, loc };
3921
- acc = { kind: "libCall", fn: "dyn.assign", args: [acc, source], type: DYN, loc: locOf(prop) };
3922
- continue;
3923
- }
3924
- if (!ts.isPropertyAssignment(prop) &&
3925
- !ts.isShorthandPropertyAssignment(prop) &&
3926
- !ts.isMethodDeclaration(prop)) {
3927
- lowerer.unsupported("SC1090", prop, "accessors in a runtime-keyed (computed-key) object literal");
3928
- }
3929
- const name = prop.name;
3930
- let key;
3931
- if (ts.isComputedPropertyName(name)) {
3932
- const folded = literalComputedKey(lowerer, name);
3933
- if (folded !== null) {
3934
- key = { kind: "strLit", value: folded, type: STRING, loc: locOf(name) };
3935
- }
3936
- else {
3937
- let k = lowerer.lowerExpr(name.expression);
3938
- if (k.type.kind === "f64" || k.type.kind === "bool" || k.type.kind === "dyn") {
3939
- k = { kind: "toString", operand: k, type: STRING, loc: locOf(name) };
3940
- }
3941
- if (k.type.kind !== "string") {
3942
- lowerer.unsupported("SC1090", name, `'${lowerer.fmt(k.type)}'-typed computed property keys (string, number, boolean, and unknown keys stringify)`);
3943
- }
3944
- key = k;
3945
- }
3946
- }
3947
- else if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
3948
- key = { kind: "strLit", value: name.text, type: STRING, loc: locOf(name) };
3949
- }
3950
- else if (ts.isNumericLiteral(name)) {
3951
- key = { kind: "strLit", value: String(Number(name.text)), type: STRING, loc: locOf(name) };
3952
- }
3953
- else {
3954
- lowerer.unsupported("SC1090", prop, "non-identifier property names");
3955
- }
3956
- const valueExpr = ts.isMethodDeclaration(prop)
3957
- ? prop
3958
- : ts.isShorthandPropertyAssignment(prop)
3959
- ? prop.name
3960
- : prop.initializer;
3961
- // Lambda values that fail to lower become trap closures (the
3962
- // per-call fence granularity — fenceClosureProbe) so the object
3963
- // still builds; everything else keeps the per-property fence below.
3964
- let raw;
3965
- const propDiagsBefore = lowerer.diags.length;
3966
- try {
3967
- const lowerValue = () => ts.isMethodDeclaration(prop)
3968
- ? (lowerer.rejectThisInObjectMethod(prop.body ?? prop), lowerer.lowerLambda(prop))
3969
- : lowerer.lowerExpr(valueExpr);
3970
- raw =
3971
- fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ??
3972
- lowerValue();
3973
- }
3974
- catch (err) {
3975
- // A PURE member read a JS file cannot lower (a namespace object
3976
- // in an export aggregate): the slot takes a boxed fence closure —
3977
- // the diagnostics defer to the runtime-fence ledger, the object
3978
- // builds, and only USING the member stops the run.
3979
- const pureMember = ts.isIdentifier(valueExpr);
3980
- if (!(err instanceof PoisonError) || !pureMember)
3981
- throw err;
3982
- const fence = lowerer.deferToRuntimeFence(propDiagsBefore, prop, {
3983
- kind: "closure",
3984
- name: () => `%fn${lowerer.lambdaCounter++}_dynfence`,
3985
- returnType: VOID,
3986
- type: { kind: "func", params: [], ret: VOID },
3987
- });
3988
- if (!fence)
3989
- throw err;
3990
- raw = fence;
3991
- }
3992
- let v = boxValue
3993
- ? boxValue(valueExpr, raw)
3994
- : lowerer.coerceToExpected(raw, DYN);
3995
- if (v.type.kind !== "dyn") {
3996
- const convDiagsBefore = lowerer.diags.length;
3997
- try {
3998
- lowerer.unsupported("SC1101", valueExpr, `holding '${lowerer.fmt(v.type)}' values in a runtime-keyed (computed-key) object literal`);
3999
- }
4000
- catch (err) {
4001
- // A member value the checked-dynamic tree cannot hold — a func whose signature
4002
- // cannot box, an island ('any') handle, a record carrying func
4003
- // fields (the export aggregate's typed utilities and npm-handle
4004
- // members): the same call-time fence deferral as an unlowerable
4005
- // member — the slot takes a boxed fence closure; only USING it
4006
- // stops the run. Probe mode and ICEs keep the poison.
4007
- if (!(err instanceof PoisonError))
4008
- throw err;
4009
- const fence = lowerer.deferToRuntimeFence(convDiagsBefore, prop, {
4010
- kind: "closure",
4011
- name: () => `%fn${lowerer.lambdaCounter++}_dynfence`,
4012
- returnType: VOID,
4013
- type: { kind: "func", params: [], ret: VOID },
4014
- });
4015
- if (!fence)
4016
- throw err;
4017
- v = lowerer.coerceToExpected(fence, DYN);
4018
- }
4019
- }
4020
- fields.push({ key, value: v });
4021
- }
4022
- flushFields();
4023
- return acc ?? { kind: "dynObjLit", fields: [], type: DYN, loc };
4024
- }
4025
- /** Spreading a source whose shape carries accessor slots: Node's copy
4026
- * invokes each getter exactly once in insertion order — even for keys a
4027
- * later contributor overrides — while the field-copy desugar assumes pure
4028
- * reads it may drop or reorder. Getter-call counts would silently diverge,
4029
- * so accessor-carrying sources fence by name. */
4030
- function fenceAccessorSpreadSource(lowerer, prop, srcShape) {
4031
- if (srcShape && shapeHasAccessorSlots(srcShape)) {
4032
- 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
- }
4034
- }
4035
- /** The JS trap-closure fallback for FUNCTION-VALUED object properties: a
4036
- * lambda whose body fails to lower inside a JS object literal becomes a
4037
- * closure of the field's exact func type whose body is the runtime fence —
4038
- * the object still BUILDS (commander's `_outputConfiguration` table: the
4039
- * driven writeOut/writeErr entries work; an unloweraable color probe traps
4040
- * only if something CALLS it). The failed lambda's diagnostics move to the
4041
- * runtime-fence inventory, exactly like a fenced JS statement. Null when
4042
- * the fallback does not apply (not a JS file, not a lambda, no func slot)
4043
- * — the caller lowers normally. ICEs (SC9001) never convert. */
4044
- function fenceClosureProbe(lowerer, node, slotType, attempt) {
4045
- if (!isJsSourceFile(node.getSourceFile()))
4046
- return null;
4047
- let inner = node;
4048
- while (ts.isParenthesizedExpression(inner))
4049
- inner = inner.expression;
4050
- if (!ts.isArrowFunction(inner) && !ts.isFunctionExpression(inner) && !ts.isMethodDeclaration(inner)) {
4051
- return null;
4052
- }
4053
- // The trap's ABI: the field's func slot when one exists; otherwise (the
4054
- // dyn-object literal path — the value boxes into dyn) the all-dyn
4055
- // signature of the lambda's own arity, which canBoxFuncIntoDyn always
4056
- // admits.
4057
- const fieldType = slotType !== undefined && slotType.kind === "func"
4058
- ? slotType
4059
- : { kind: "func", params: inner.parameters.map(() => DYN), ret: DYN };
4060
- if (slotType !== undefined && slotType.kind !== "func")
4061
- return null;
4062
- const diagsBefore = lowerer.diags.length;
4063
- try {
4064
- return attempt();
4065
- }
4066
- catch (e) {
4067
- if (!(e instanceof PoisonError))
4068
- throw e;
4069
- const params = fieldType.params.map((t, i) => ({ localId: `p.${i}`, name: `p${i}`, type: t }));
4070
- const fence = lowerer.deferToRuntimeFence(diagsBefore, node, {
4071
- kind: "closure",
4072
- name: () => `%fence.fn.${lowerer.liftedFns.length}`,
4073
- params,
4074
- returnType: fieldType.ret,
4075
- paramsMutable: true,
4076
- type: fieldType,
4077
- fallback: { code: "SC1090", message: "this function's body has no static lowering" },
4078
- bareMessage: true,
4079
- allowDiagSink: true,
4080
- });
4081
- if (!fence)
4082
- throw e;
4083
- return fence;
4084
- }
4085
- }
4086
- /** The union arm an object LITERAL inhabits when its fields must widen
4087
- * PER FIELD into exactly one record arm — the reducer-action pattern
4088
- * (`{ kind: "a", parsed: 1 }` into `{ kind: "a"; parsed: number | null }
4089
- * | { kind: "b" }`). The IR shapes erased the literal types tsc
4090
- * discriminated on, so the probe runs against the contextual union's
4091
- * CHECKER members: every literal field must exist on the member
4092
- * (excess-property freshness — tsc already enforced it), every arm field
4093
- * missing from the literal must be optional-flavored (an undefined-armed
4094
- * union, or a dyn slot — the absent-completion rule), and each present
4095
- * field's LITERAL type must fit the member's field type — literal against
4096
- * literal decides by VALUE (the discriminant), everything else by the
4097
- * widened IR pair under the width-lift relation. Exactly ONE fitting arm
4098
- * answers it; zero or several answer null and the caller keeps its
4099
- * fences. Plain property-assignment/shorthand literals only — spreads,
4100
- * accessors, methods, and unfoldable computed keys keep their own paths. */
4101
- function literalUnionArmOf(lowerer, expr, tsType, recordArms) {
4102
- if (!tsType.isUnionType())
4103
- return null;
4104
- const props = [];
4105
- for (const p of expr.properties) {
4106
- if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) {
4107
- props.push({ name: propNameText(lowerer, p.name), node: p.initializer });
4108
- }
4109
- else if (ts.isShorthandPropertyAssignment(p) && ts.isIdentifier(p.name)) {
4110
- props.push({ name: p.name.text, node: p.name });
4111
- }
4112
- else {
4113
- return null;
4114
- }
4115
- }
4116
- /** litT fits ftT: unions per arm; literal-vs-literal by value; unit
4117
- * types only into their own unit; otherwise the widened IR pair must be
4118
- * equal or width-liftable. */
4119
- const fits = (litT, ftT) => {
4120
- if (ftT.isUnionType())
4121
- return ts.constituentTypes(ftT).some((a) => fits(litT, a));
4122
- if (ftT.isStringLiteralType())
4123
- return litT.isStringLiteralType() && litT.value === ftT.value;
4124
- if (ftT.isNumberLiteralType())
4125
- return litT.isNumberLiteralType() && litT.value === ftT.value;
4126
- if (ftT.flags & ts.TypeFlags.BooleanLiteral) {
4127
- return (litT.flags & ts.TypeFlags.BooleanLiteral) !== 0 && lowerer.checker.typeToString(litT) === lowerer.checker.typeToString(ftT);
4128
- }
4129
- if (ftT.flags & ts.TypeFlags.Null)
4130
- return (litT.flags & ts.TypeFlags.Null) !== 0;
4131
- if (ftT.flags & ts.TypeFlags.Undefined)
4132
- return (litT.flags & ts.TypeFlags.Undefined) !== 0;
4133
- if (litT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined))
4134
- return false;
4135
- const li = lowerer.mapTypeOf(lowerer.checker.getBaseTypeOfLiteralType(litT));
4136
- const fi = lowerer.mapTypeOf(ftT);
4137
- if (!li || !fi)
4138
- return false;
4139
- return typeEquals(li, fi) || lowerer.widthLiftPlan(li, fi) !== null;
4140
- };
4141
- const armShapeIds = new Set(recordArms.map((a) => a.shapeId));
4142
- const candidates = new Set();
4143
- for (const member of ts.constituentTypes(tsType)) {
4144
- const mMapped = lowerer.mapTypeOf(member);
4145
- if (mMapped?.kind !== "record" || !armShapeIds.has(mMapped.shapeId) || candidates.has(mMapped.shapeId))
4146
- continue;
4147
- const shape = lowerer.shapes.get(mMapped.shapeId);
4148
- if (!shape || shape.tuple || shape.indexValue)
4149
- continue;
4150
- // Excess-property freshness: every literal field must exist on the
4151
- // member (tsc rejected the others for a fresh literal).
4152
- if (!props.every((p) => lowerer.checker.getPropertyOfType(member, p.name) !== undefined))
4153
- continue;
4154
- // Arm fields the literal leaves unset must be optional-flavored (the
4155
- // absent-completion rule: an undefined-armed union or a dyn slot).
4156
- const names = new Set(props.map((p) => p.name));
4157
- const absentOk = shape.fields.every((f) => {
4158
- if (names.has(f.name))
4159
- return true;
4160
- if (f.type.kind === "dyn")
4161
- return true;
4162
- if (f.type.kind !== "union")
4163
- return false;
4164
- return lowerer.unions.get(f.type.unionId)?.arms.some((a) => a.kind === "undefinedT") ?? false;
4165
- });
4166
- if (!absentOk)
4167
- continue;
4168
- const fieldsFit = props.every((p) => {
4169
- const sym = lowerer.checker.getPropertyOfType(member, p.name);
4170
- if (!sym)
4171
- return false;
4172
- return fits(lowerer.typeOf(p.node), lowerer.checker.getTypeOfSymbol(sym));
4173
- });
4174
- if (fieldsFit)
4175
- candidates.add(mMapped.shapeId);
4176
- }
4177
- if (candidates.size !== 1)
4178
- return null;
4179
- const [only] = candidates;
4180
- return recordArms.find((a) => a.shapeId === only) ?? null;
4181
- }
4182
- export function lowerObjectLiteral(lowerer, expr) {
4183
- const loc = locOf(expr);
4184
- // The RUNTIME-KEYED literal (JS): a computed key that doesn't fold to a
4185
- // compile-time string means the literal's shape is not a compile-time
4186
- // fact — no record shape can hold it. The whole literal builds as a dyn
4187
- // object instead (`{ [field]: criteria, actual: 0, ... }` —
4188
- // test/common's _mustCallInner context), where keys are runtime string
4189
- // values. TypeScript keeps the record world and its fence: this shape
4190
- // only arises in checked-dynamic JS.
4191
- if (isJsSourceFile(expr.getSourceFile()) &&
4192
- expr.properties.some((p) => {
4193
- const n = ts.isSpreadAssignment(p) ? undefined : p.name;
4194
- return n !== undefined && ts.isComputedPropertyName(n) && literalComputedKey(lowerer, n) === null;
4195
- })) {
4196
- return lowerDynObjectLiteral(lowerer, expr);
4197
- }
4198
- // Syntax fence FIRST: unsupported member forms get their specific
4199
- // message even when they also make the literal's type unmappable
4200
- // (a getter or a `this`-returning method would otherwise surface as an
4201
- // opaque type fence on the whole literal).
4202
- for (const prop of expr.properties) {
4203
- if (ts.isSpreadAssignment(prop)) {
4204
- // Supported shapes: FULL spreads of known record shapes read as
4205
- // identifiers or other side-effect-free reads, all BEFORE any
4206
- // explicit property (the field-copy desugar reads spread fields
4207
- // first, exactly JS's eager copy order); and CONDITIONAL spreads
4208
- // `...(c ? {k: v} : {})` — the optional-field idiom tsc types as
4209
- // `k?: ...` — which desugar to one conditional field at the
4210
- // spread's own position (any position: they introduce one fresh
4211
- // name, collision-fenced below). Everything else stays fenced.
4212
- const cs = conditionalSpreadOf(prop.expression);
4213
- if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
4214
- lowerer.unsupported("SC1090", prop, "conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm — spell other shapes as optional fields)");
4215
- }
4216
- // Spread ORDER is fenced in the field-by-field desugar below, not
4217
- // here: the index-signature merge path supports any order (keyed
4218
- // last-write-wins writes), so `{ K: v, ...extra }` into a pure
4219
- // Record shape compiles — the buildServiceEnv pattern.
4220
- continue;
4221
- }
4222
- if (ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop)) {
4223
- // JavaScript CJS-export tables reach reads through the lifted-
4224
- // accessor path (cjsExportAccessorRead) — the literal VALUE
4225
- // narrows to its plain fields below (accessor names are not
4226
- // record storage; SEMANTICS.md documents the enumeration
4227
- // divergence). TypeScript accessors lower into the shape's
4228
- // reserved closure slots (%get:/%set: — see accessorSlotProp);
4229
- // `this` in the body is fenced up front: the closure slot has no
4230
- // receiver (capturing the record under construction would be an
4231
- // RC cycle), and the generic lexical-this walk would silently
4232
- // capture an ENCLOSING method's `this` — the object-method rule.
4233
- if (!isJsSourceFile(expr.getSourceFile())) {
4234
- rejectThisInObjectAccessor(lowerer, prop.body ?? prop);
4235
- }
4236
- }
4237
- // Unfoldable computed keys are NOT fenced here: an index-signature
4238
- // target lowers them as runtime keyed writes (the merge path below);
4239
- // every other target re-fences them after the merge path declined
4240
- // (fenceUnfoldableComputedKeys).
4241
- // Identifier keys, STRING-LITERAL keys (`"content-type": v` —
4242
- // record field names are data, never C identifiers; the mangler
4243
- // encodes what C can't spell), NUMERIC-LITERAL keys in their
4244
- // canonical string spelling (`{ 0: v }` stores "0", `{ 0x10: v }`
4245
- // stores "16" — JS's ToPropertyKey; the checker names the property
4246
- // symbol the same way), and FOLDABLE computed keys (above).
4247
- if (!prop.name ||
4248
- !(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ||
4249
- ts.isNumericLiteral(prop.name) || ts.isComputedPropertyName(prop.name))) {
4250
- lowerer.unsupported("SC1090", prop, "non-identifier property names");
4251
- }
4252
- // Shorthand methods are just closure-valued fields — but their `this`
4253
- // is dynamically bound (typed as the literal by tsc, so noImplicitThis
4254
- // lets it through) and records don't model it; the generic
4255
- // lexical-this walk would silently capture an ENCLOSING method's
4256
- // `this`, so any `this` in the body is rejected up front.
4257
- if (ts.isMethodDeclaration(prop))
4258
- lowerer.rejectThisInObjectMethod(prop.body ?? prop);
4259
- }
4260
- let tsType = lowerer.checker.getContextualType(expr) ?? lowerer.typeOf(expr);
4261
- // `lit satisfies T` is TYPE-LEVEL only: the expression's checker type —
4262
- // and therefore the shape every downstream consumer sees — is the
4263
- // literal's OWN type (T still contextually types members, so inferred
4264
- // parameter types flow). Building at T would reshape the value tsc
4265
- // says has the literal's type: `{...} satisfies Movable &
4266
- // Record<string, unknown>` must NOT become an index-signature record.
4267
- // Own type wins whenever it maps to a record; an unmappable own type
4268
- // keeps the contextual fallback (a bare-null field whose satisfies
4269
- // target names the wider slot type).
4270
- {
4271
- let p = expr.parent;
4272
- while (ts.isParenthesizedExpression(p))
4273
- p = p.parent;
4274
- if (ts.isSatisfiesExpression(p)) {
4275
- const own = lowerer.typeOf(expr);
4276
- if (lowerer.mapTypeOf(own)?.kind === "record")
4277
- tsType = own;
4278
- }
4279
- }
4280
- // An async function's return position types the literal
4281
- // `T | PromiseLike<T>` (the lib's await-unwrapping contract). The
4282
- // PromiseLike arm never maps, and the record the return slot actually
4283
- // holds is exactly the checker's awaited type — strip to it BEFORE the
4284
- // own-type fallback below: the awaited contextual type carries the
4285
- // slot's field types (`lanIp: string | null`), which the literal's own
4286
- // type narrows away (a field written as `lanIp: null` types as bare
4287
- // `null`, which maps to nothing on its own).
4288
- if (tsType.isUnionType() && ts.constituentTypes(tsType).some((t) => t.getSymbol()?.name === "PromiseLike")) {
4289
- tsType = lowerer.checker.getAwaitedType(tsType) ?? tsType;
4290
- }
4291
- let mapped = lowerer.mapTypeOf(tsType);
4292
- // An EMPTY-record context under a NON-empty literal (`Object.keys({
4293
- // ...process.env })` — the lib's `{}`-typed parameters admit every
4294
- // object): `{}` carries no shape information, so the literal builds as
4295
- // its OWN type, exactly the unmappable-context fallback below. An
4296
- // empty LITERAL keeps the context (the shapes agree).
4297
- if (mapped?.kind === "record" && expr.properties.length > 0) {
4298
- const ctxShape = lowerer.shapes.get(mapped.shapeId);
4299
- if (ctxShape && ctxShape.fields.length === 0 && !ctxShape.indexValue && !ctxShape.tuple) {
4300
- mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped;
4301
- }
4302
- }
4303
- // A literal with a property the contextual TYPE ITSELF lacks — only
4304
- // reachable through type assertions and satisfies (fresh-literal
4305
- // excess-property checks reject the direct spelling): the context
4306
- // cannot hold the value, so the literal builds at its OWN type and the
4307
- // slot's width coercion narrows it (divergence 36's copy stance — the
4308
- // extra fields drop in the copy). The probe is against the CHECKER's
4309
- // contextual type, not the mapped shape: a property the contextual
4310
- // type carries but the shape dropped (settled value/reason,
4311
- // generic-callable members) belongs to the drop paths below, and
4312
- // index-signature contexts keep every key (overflow capture).
4313
- if (mapped?.kind === "record" && expr.properties.length > 0) {
4314
- const ctxShape = lowerer.shapes.get(mapped.shapeId);
4315
- if (ctxShape && !ctxShape.indexValue && !ctxShape.tuple) {
4316
- const names = new Set(ctxShape.fields.map((f) => f.name));
4317
- const ctxHasProp = (name) => {
4318
- const members = tsType.isUnionType() ? ts.constituentTypes(tsType) : [tsType];
4319
- return members.some((m) => lowerer.checker.getPropertyOfType(m, name) !== undefined);
4320
- };
4321
- const extraOf = (text) => !names.has(text) && !ctxHasProp(text);
4322
- const extra = expr.properties.some((p) => {
4323
- if (ts.isSpreadAssignment(p) || !p.name)
4324
- return false;
4325
- if (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))
4326
- return extraOf(p.name.text);
4327
- if (ts.isNumericLiteral(p.name))
4328
- return extraOf(String(Number(p.name.text)));
4329
- return false; // computed keys keep their existing paths
4330
- });
4331
- if (extra)
4332
- mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped;
4333
- }
4334
- }
4335
- // A union-typed slot (`const r: Res = { kind: "ok", ... }`) contextually
4336
- // types the literal as the WHOLE union; build the literal as its own
4337
- // (arm) shape and let the slot's coercion wrap it into the union. An
4338
- // unknown-typed slot (`JSON.stringify({ a: 1 })`) likewise. An
4339
- // UNMAPPABLE context falls back the same way — the literal's own type
4340
- // is the plain record the slot coerces. A CLASS-INSTANCE context
4341
- // (`const p: Point = { x, y }` — tsc's structural view of data
4342
- // classes) falls back too: the literal builds as its own record and
4343
- // the slot's width coercion constructs through the trivial
4344
- // parameter-property constructor (recordToClassPlan), or fences with
4345
- // the record-shape story.
4346
- // A jsval-mapped context reaches here only when lowerExpr's island
4347
- // gate DECLINED it (a project-declared typedef that absorbed to the
4348
- // island through checker-`any` field residue): the literal builds at
4349
- // its own type like every unmappable context.
4350
- if (mapped === null || mapped.kind === "union" || mapped.kind === "dyn" || mapped.kind === "object" || mapped.kind === "jsval") {
4351
- const ctxUnion = mapped?.kind === "union" ? mapped : null;
4352
- mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped;
4353
- // A literal whose own shape re-tags into NO arm of the contextual
4354
- // union, where the union has exactly ONE record arm: build AS that
4355
- // arm — there is no ambiguity (tsc already checked the literal
4356
- // against the union, and the record arm is the only shape it can
4357
- // inhabit), and the arm's field types drive every property's
4358
- // coercion (`{ env: {...spread...}, onCleanup }` against an
4359
- // optional options param — the env value builds by ITS contextual
4360
- // index-signature type and wraps into the arm's `| undefined`
4361
- // field, where the literal's own inferred width would mismatch).
4362
- // Empty literals (`_env = {}` — the fieldless own shape) and
4363
- // literals against PURE index-signature arms (`{ OPENSSL_CONF:
4364
- // candidate }` — keys become overflow entries) are the same rule's
4365
- // simplest cases. The empty-array-in-union rule, record form.
4366
- if (ctxUnion) {
4367
- const def = lowerer.unions.get(ctxUnion.unionId);
4368
- const recordArms = def?.arms.filter((a) => a.kind === "record") ?? [];
4369
- if (recordArms.length === 1) {
4370
- const armShape = lowerer.shapes.get(recordArms[0].shapeId);
4371
- if ((mapped?.kind !== "record" || mapped.shapeId !== recordArms[0].shapeId) &&
4372
- !armShape?.tuple) {
4373
- mapped = recordArms[0];
4374
- }
4375
- }
4376
- else if (recordArms.length > 1) {
4377
- const ownShapeId = mapped?.kind === "record" ? mapped.shapeId : null;
4378
- // SEVERAL record arms (the reducer-action / discriminated-message
4379
- // pattern): the literal's own inferred shape re-tags into no arm
4380
- // — its field types widened per field (`parsed: 1` against
4381
- // `parsed: number | null`), so the IR-level candidate probe is
4382
- // ambiguous (a narrower arm also admits the literal by dropping
4383
- // fields). The LITERAL types tsc checked carry the discriminant
4384
- // the shapes erased: match the literal's fields against each
4385
- // union member's CHECKER types (literal-vs-literal field pairs
4386
- // decide by value — the `kind: "a"` discriminant), and when
4387
- // exactly ONE member fits, build AS that arm — its field types
4388
- // drive every property's coercion, exactly the single-record-arm
4389
- // rule above. Ambiguous literals keep the SC2003 fence.
4390
- if (ownShapeId === null || !recordArms.some((a) => a.shapeId === ownShapeId)) {
4391
- const arm = literalUnionArmOf(lowerer, expr, tsType, recordArms);
4392
- if (arm)
4393
- mapped = arm;
4394
- }
4395
- }
4396
- }
4397
- }
4398
- // The CJS EXPORT-TABLE literal in VALUE position (JS): importers reach
4399
- // every member through alias plumbing and accessor lifts — the record
4400
- // VALUE exists for the module's own reads (Object.keys, internal
4401
- // member reads), so it keeps exactly the plain fields whose values
4402
- // lower; accessor entries and members with no value representation
4403
- // (rest-param function types) narrow away. SEMANTICS.md documents the
4404
- // enumeration divergence.
4405
- if (isJsSourceFile(expr.getSourceFile()) &&
4406
- (!mapped || expr.properties.some((p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p))) &&
4407
- isCjsExportTableLiteral(expr)) {
4408
- const fields = [];
4409
- for (const prop of expr.properties) {
4410
- if (ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop))
4411
- continue;
4412
- const propName = ts.isSpreadAssignment(prop) ? undefined : prop.name;
4413
- if (!propName || !(ts.isIdentifier(propName) || ts.isStringLiteral(propName)))
4414
- continue;
4415
- const name = propName.text;
4416
- let v = null;
4417
- if (ts.isShorthandPropertyAssignment(prop)) {
4418
- v = probeLower(lowerer, propName);
4419
- }
4420
- else if (ts.isPropertyAssignment(prop)) {
4421
- v = probeLower(lowerer, prop.initializer);
4422
- }
4423
- if (!v || v.type.kind === "void" || v.type.kind === "caught" || v.type.kind === "jsval")
4424
- continue;
4425
- if (isUnitType(v.type))
4426
- continue;
4427
- fields.push({ name, value: v });
4428
- }
4429
- // Canonical (sorted) field order — the shape registry's invariant;
4430
- // the dropped reads are all pure, so reordering loses nothing.
4431
- fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
4432
- const shapeId = lowerer.shapes.intern(fields.map((f) => ({ name: f.name, type: f.value.type })));
4433
- return { kind: "recordLit", fields, type: { kind: "record", shapeId }, loc };
4434
- }
4435
- // The JS declaration fallback, literal-side (the checked-dynamic tree-array rule's
4436
- // object form): a literal whose shape has no static home — an
4437
- // unmappable contextual type over unmappable own fields (the
4438
- // PropertyDescriptorMap argument of Object.defineProperties, nested
4439
- // descriptor records with `any` values) — builds as a dyn OBJECT:
4440
- // each field converts through the usual dyn boundary, and dynamic
4441
- // consumers ride the keyed-dyn paths. TypeScript keeps the fence.
4442
- if ((!mapped || mapped.kind === "dyn") && isJsSourceFile(expr.getSourceFile())) {
4443
- return lowerDynObjectLiteral(lowerer, expr);
4444
- }
4445
- if (!mapped || mapped.kind !== "record")
4446
- lowerer.badType(expr, tsType);
4447
- let type = mapped;
4448
- let shape = lowerer.shapes.get(type.shapeId);
4449
- // ACCESSOR properties, JS literals only (TS accessors fill the shape's
4450
- // %get:/%set: closure slots below): no record storage exists for them,
4451
- // so the literal's shape NARROWS to its plain fields (reads resolve
4452
- // through the lifted accessors; Object.keys over the value omits
4453
- // accessor names — the documented divergence).
4454
- if (isJsSourceFile(expr.getSourceFile())) {
4455
- const accessorNames = new Set(expr.properties
4456
- .filter((p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p))
4457
- .map((p) => (p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) ? p.name.text : ""))
4458
- .filter((n) => n !== ""));
4459
- if (accessorNames.size > 0) {
4460
- const narrowed = shape.fields.filter((f) => !accessorNames.has(f.name));
4461
- const narrowedId = lowerer.shapes.intern(narrowed.map((f) => ({ name: f.name, type: f.type })), false, shape.indexValue);
4462
- type = { kind: "record", shapeId: narrowedId };
4463
- shape = lowerer.shapes.get(narrowedId);
4464
- }
4465
- }
4466
- const fieldTypes = new Map(shape.fields.map((f) => [f.name, f.type]));
4467
- // The clone probe may lower the leading spread before declining (a
4468
- // static `as` cast can make the checker-visible shape match while its
4469
- // erased IR value keeps a wider shape). Reuse that exact value in the
4470
- // ordinary spread path so lowering still happens once.
4471
- let leadingSpreadLowered = null;
4472
- // The overwhelmingly common immutable-update form over a record:
4473
- // `{ ...model, changed, other: value }`. The historic lowering expanded
4474
- // the spread into one recordGet per untouched field at EVERY site, then
4475
- // the backends inlined all of those retains and stores into the caller.
4476
- // A 178-field model updated hundreds of times consequently produced a
4477
- // half-million-line LLVM function. Keep the same evaluation/ownership
4478
- // semantics in one compact IR node: source first, explicit overrides in
4479
- // source order, and one backend clone helper per shape.
4480
- //
4481
- // Stay deliberately narrow. Tuple/index/accessor shapes, conditional or
4482
- // multiple spreads, spread-after-explicit order, and shape-changing width
4483
- // copies retain their existing lowering and diagnostics.
4484
- if (!isJsSourceFile(expr.getSourceFile()) &&
4485
- !shape.indexValue &&
4486
- !shape.tuple &&
4487
- !shapeHasAccessorSlots(shape) &&
4488
- expr.properties.length >= 2 &&
4489
- ts.isSpreadAssignment(expr.properties[0]) &&
4490
- !conditionalSpreadOf(expr.properties[0].expression) &&
4491
- expr.properties.slice(1).every((p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
4492
- p.name !== undefined &&
4493
- (ts.isIdentifier(p.name) ||
4494
- ts.isStringLiteral(p.name) ||
4495
- ts.isNumericLiteral(p.name) ||
4496
- (ts.isComputedPropertyName(p.name) && literalComputedKey(lowerer, p.name) !== null)))) {
4497
- const spread = expr.properties[0];
4498
- const props = expr.properties.slice(1);
4499
- const names = props.map((p) => propNameText(lowerer, p.name));
4500
- const unique = new Set(names);
4501
- const sourceType = lowerer.mapTypeOf(lowerer.typeOf(spread.expression));
4502
- if (sourceType?.kind === "record" &&
4503
- sourceType.shapeId === type.shapeId &&
4504
- unique.size === names.length &&
4505
- names.every((name) => fieldTypes.has(name))) {
4506
- const source = lowerer.lowerExpr(spread.expression);
4507
- if (source.type.kind === "record" && source.type.shapeId === type.shapeId) {
4508
- const overrides = [];
4509
- for (let i = 0; i < props.length; i++) {
4510
- const p = props[i];
4511
- const name = names[i];
4512
- const fieldType = fieldTypes.get(name);
4513
- const valueNode = ts.isPropertyAssignment(p) ? p.initializer : p;
4514
- let value = ts.isPropertyAssignment(p)
4515
- ? lowerer.lowerExpr(p.initializer)
4516
- : lowerer.lowerShorthandValue(p);
4517
- value = lowerer.coerceInto(valueNode, value, fieldType);
4518
- if (!typeEquals(value.type, fieldType))
4519
- lowerer.badType(valueNode, lowerer.typeOf(valueNode));
4520
- overrides.push({ name, value });
4521
- }
4522
- return { kind: "recordClone", source, overrides, type, loc };
4523
- }
4524
- leadingSpreadLowered = source;
4525
- }
4526
- }
4527
- // A PURE index-signature target with spreads — `{ ...process.env }`,
4528
- // `{ ...process.env, ...extraEnv }`, `{ ...env, PATH: p }` (the
4529
- // spawn-env pattern): the field-by-field desugar below cannot
4530
- // enumerate runtime overflow keys, so the literal lowers as ONE
4531
- // interned merge-helper call — contributors apply in literal order
4532
- // with keyed writes (JS last-write-wins), sources and values evaluate
4533
- // once each in source order (the call's argument order). A CONDITIONAL
4534
- // spread `...(c ? { k: v } : {})` contributes its one key as a ternary
4535
- // — cond once, v lazily, the empty arm holding the value slot's
4536
- // undefined arm (the explicit-undefined-is-absent stance: JSON and
4537
- // child-env builders drop it, exactly Node's absent key); targets
4538
- // with declared fields keep the historic desugar below.
4539
- const isRuntimeComputedKey = (p) => !ts.isSpreadAssignment(p) &&
4540
- p.name !== undefined &&
4541
- ts.isComputedPropertyName(p.name) &&
4542
- literalComputedKey(lowerer, p.name) === null;
4543
- if (shape.indexValue &&
4544
- shape.fields.length === 0 &&
4545
- !shape.tuple &&
4546
- (expr.properties.some((p) => ts.isSpreadAssignment(p)) || expr.properties.some(isRuntimeComputedKey))) {
4547
- const contributors = [];
4548
- let mergeable = true;
4549
- for (const prop of expr.properties) {
4550
- if (ts.isSpreadAssignment(prop)) {
4551
- const cs = conditionalSpreadOf(prop.expression);
4552
- if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
4553
- lowerer.unsupported("SC1090", prop, "conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm)");
4554
- }
4555
- if (cs) {
4556
- const absent = lowerer.wrappedUndefined(shape.indexValue, locOf(prop));
4557
- if (!absent) {
4558
- lowerer.unsupported("SC1090", prop, `conditional spreads into '${lowerer.fmt(shape.indexValue)}'-valued index-signature keys (the empty arm needs an undefined arm to hold — write the key in an if statement instead)`);
4559
- }
4560
- const csProp = cs.props[0];
4561
- const cond = lowerer.lowerCondition(cs.cond);
4562
- const vNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp;
4563
- const v = lowerer.intoIndexValueSlot(ts.isPropertyAssignment(csProp) ? lowerer.lowerExpr(csProp.initializer) : lowerer.lowerShorthandValue(csProp), shape.indexValue, vNode);
4564
- contributors.push({
4565
- kind: "field",
4566
- name: csProp.name.text,
4567
- value: {
4568
- kind: "ternary",
4569
- cond,
4570
- then: cs.whenTrue ? v : absent,
4571
- else_: cs.whenTrue ? absent : v,
4572
- type: shape.indexValue,
4573
- loc: locOf(prop),
4574
- },
4575
- });
4576
- continue;
4577
- }
4578
- const src = lowerer.lowerExpr(prop.expression);
4579
- if (src.type.kind !== "record") {
4580
- 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
- }
4582
- fenceAccessorSpreadSource(lowerer, prop, lowerer.shapes.get(src.type.shapeId));
4583
- contributors.push({ kind: "spread", shapeId: src.type.shapeId, value: src });
4584
- continue;
4585
- }
4586
- if ((ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) &&
4587
- prop.name &&
4588
- (ts.isIdentifier(prop.name) ||
4589
- ts.isStringLiteral(prop.name) ||
4590
- ts.isNumericLiteral(prop.name) ||
4591
- (ts.isComputedPropertyName(prop.name) && literalComputedKey(lowerer, prop.name) !== null))) {
4592
- const value = ts.isPropertyAssignment(prop)
4593
- ? lowerer.intoIndexValueSlot(lowerer.lowerExpr(prop.initializer), shape.indexValue, prop.initializer)
4594
- : lowerer.intoIndexValueSlot(lowerer.lowerShorthandValue(prop), shape.indexValue, prop);
4595
- contributors.push({ kind: "field", name: propNameText(lowerer, prop.name), value });
4596
- continue;
4597
- }
4598
- // A RUNTIME-keyed property (`{ ...m, ["a" + "b"]: "" }`, `{ [K]:
4599
- // v }` where K is a runtime string): the key evaluates before its
4600
- // value (JS's per-property order — both are helper arguments) and
4601
- // writes through the signature exactly like a spread's keys.
4602
- // Number/boolean/unknown keys stringify (ToPropertyKey).
4603
- if (ts.isPropertyAssignment(prop) && ts.isComputedPropertyName(prop.name)) {
4604
- let k = lowerer.lowerExpr(prop.name.expression);
4605
- if (k.type.kind === "f64" || k.type.kind === "bool" || k.type.kind === "dyn") {
4606
- k = { kind: "toString", operand: k, type: STRING, loc: locOf(prop.name) };
4607
- }
4608
- if (k.type.kind !== "string") {
4609
- lowerer.unsupported("SC1090", prop.name, `'${lowerer.fmt(k.type)}'-typed computed property keys (string, number, boolean, and unknown keys stringify)`);
4610
- }
4611
- const value = lowerer.intoIndexValueSlot(lowerer.lowerExpr(prop.initializer), shape.indexValue, prop.initializer);
4612
- contributors.push({ kind: "keyedField", key: k, value });
4613
- continue;
4614
- }
4615
- mergeable = false;
4616
- break;
4617
- }
4618
- if (mergeable) {
4619
- const helper = lowerIndexMergeHelper(lowerer, type.shapeId, contributors, loc);
4620
- if (helper === null) {
4621
- lowerer.unsupported("SC1090", expr, `object spread into '${lowerer.fmt(type)}' from these source shapes (spread sources must be index-signature records whose value type is, or lifts into, the target's)`);
4622
- }
4623
- return {
4624
- kind: "call",
4625
- callee: helper,
4626
- args: contributors.flatMap((c) => (c.kind === "keyedField" ? [c.key, c.value] : [c.value])),
4627
- type,
4628
- loc,
4629
- };
4630
- }
4631
- }
4632
- // Unfoldable computed keys outside the index-signature merge path:
4633
- // no record shape can hold a runtime-decided name.
4634
- for (const prop of expr.properties) {
4635
- if (isRuntimeComputedKey(prop)) {
4636
- lowerer.unsupported("SC1090", prop.name, "computed property keys (compile-time-known keys fold — a pure expression whose checker type is one string or number literal: consts, enum members, quoted keys, templates of those — `{ [MARKER]: v }`; runtime string keys write through an index-signature target; symbol keys stay out)");
4637
- }
4638
- }
4639
- // A DECLARED-fields target built ENTIRELY from index-signature spreads —
4640
- // `{ ...Object.fromEntries(...), ...Object.fromEntries(...) }` typed
4641
- // AppConfig (the defaults-merge idiom over runtime-keyed sources): the
4642
- // field-by-field desugar cannot enumerate runtime keys, so the literal
4643
- // lowers as ONE interned merge-helper call — sources evaluate once each
4644
- // in source order (the call's argument order — computed sources
4645
- // included, no re-read), contributors apply in order with per-key
4646
- // dispatch onto the declared fields (JS last-write-wins). Divergence 68
4647
- // has the runtime rules (validated collisions, extra keys dropped).
4648
- if (!shape.indexValue &&
4649
- !shape.tuple &&
4650
- shape.fields.length > 0 &&
4651
- expr.properties.length > 0 &&
4652
- expr.properties.every((p) => ts.isSpreadAssignment(p) && !conditionalSpreadOf(p.expression)) &&
4653
- expr.properties.some((p) => {
4654
- const t = lowerer.mapTypeOf(lowerer.typeOf(p.expression));
4655
- return t?.kind === "record" && !!lowerer.shapes.get(t.shapeId)?.indexValue;
4656
- })) {
4657
- return lowerDeclaredSpreadMerge(lowerer, expr, type, shape, loc);
4658
- }
4659
- const shapeMismatch = (node) => {
4660
- 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"
4662
- ? (lowerer.describeRecordWidthBlocker(own.shapeId, type.shapeId) ?? undefined)
4663
- : undefined));
4664
- throw new PoisonError();
4665
- };
4666
- const fields = [];
4667
- // Field names introduced by conditional spreads: their ternary carries
4668
- // the spread's whole evaluation (cond once, value lazily), so a LATER
4669
- // contributor overriding one would silently drop that evaluation —
4670
- // collisions fence in both directions.
4671
- const conditionalNames = new Set();
4672
- // Member names DROPPED from the shape (JS deferral — unlowerable pure
4673
- // member reads narrow away; see the catch in the property loop).
4674
- const droppedNames = new Set();
4675
- for (const prop of expr.properties) {
4676
- if (ts.isSpreadAssignment(prop)) {
4677
- const cs = conditionalSpreadOf(prop.expression);
4678
- if (cs && cs !== "unsupported") {
4679
- // `...(c ? { k: v } : {})` — ONE conditional field at the
4680
- // spread's own position: cond evaluates exactly once, `v` only
4681
- // when the non-empty arm is taken (ternary arms are lazy), and
4682
- // the empty arm holds the interned undefined arm — exactly the
4683
- // omitted-optional-field representation, which is also why the
4684
- // target field must be optional (tsc types the idiom that way).
4685
- const csProp = cs.props[0]; // single-prop-checked in the fence pass
4686
- const name = csProp.name.text;
4687
- const fieldType = fieldTypes.get(name);
4688
- if (!fieldType) {
4689
- if (shape.indexValue) {
4690
- lowerer.unsupported("SC1090", prop, "conditional spreads into index-signature keys (overflow entries model presence — write the key in an if statement instead)");
4691
- }
4692
- throw shapeMismatch(prop);
4693
- }
4694
- const absent = lowerer.wrappedUndefined(fieldType, locOf(prop));
4695
- if (!absent) {
4696
- lowerer.unsupported("SC1090", prop, `conditional spreads onto the required field '${name}' (the empty arm leaves it undefined — declare the field optional)`);
4697
- }
4698
- if (fields.some((f) => f.name === name)) {
4699
- lowerer.unsupported("SC1090", prop, `conditional spread of '${name}' over an earlier '${name}' (the desugar keeps one entry per name — restructure so each name has one contributor)`);
4700
- }
4701
- const cond = lowerer.lowerCondition(cs.cond);
4702
- const valueNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp;
4703
- let v = ts.isPropertyAssignment(csProp)
4704
- ? lowerer.lowerExpr(csProp.initializer)
4705
- : lowerer.lowerShorthandValue(csProp);
4706
- v = lowerer.coerceInto(valueNode, v, fieldType);
4707
- if (!typeEquals(v.type, fieldType))
4708
- lowerer.badType(valueNode, lowerer.typeOf(valueNode));
4709
- conditionalNames.add(name);
4710
- fields.push({
4711
- name,
4712
- value: {
4713
- kind: "ternary",
4714
- cond,
4715
- then: cs.whenTrue ? v : absent,
4716
- else_: cs.whenTrue ? absent : v,
4717
- type: fieldType,
4718
- loc: locOf(prop),
4719
- },
4720
- });
4721
- continue;
4722
- }
4723
- // `{ ...base, ... }` — field-by-field copy of a known record
4724
- // shape. Every source field must land on the target shape with an
4725
- // equal type (a wider source would silently DROP fields JS keeps —
4726
- // the width fence, same as literals). Later contributors override
4727
- // earlier ones (JS last-write-wins; the reads are side-effect-free,
4728
- // so dropping the earlier read is exact). Identifier sources
4729
- // re-read per field (historic path); any OTHER source must be a
4730
- // re-emittable pure read, sharing one lowered node per field.
4731
- // The desugar's one-entry-per-name list reads spread fields
4732
- // EAGERLY at the spread's position, so an explicit property
4733
- // BEFORE a spread would need JS's overwrite — order-fenced (the
4734
- // index-signature merge path above takes any order).
4735
- if (expr.properties
4736
- .slice(0, expr.properties.indexOf(prop))
4737
- .some((p) => !ts.isSpreadAssignment(p))) {
4738
- lowerer.unsupported("SC1090", prop, "object spread after explicit properties (spreads must come first — a later spread would overwrite them with JS semantics the desugar does not model)");
4739
- }
4740
- let srcNode = prop.expression;
4741
- while (ts.isParenthesizedExpression(srcNode))
4742
- srcNode = srcNode.expression;
4743
- const srcLowered = prop === expr.properties[0] && leadingSpreadLowered !== null
4744
- ? leadingSpreadLowered
4745
- : ts.isIdentifier(srcNode)
4746
- ? null
4747
- : lowerer.lowerExpr(srcNode);
4748
- const srcType = srcLowered ? srcLowered.type : lowerer.mapTypeOf(lowerer.typeOf(srcNode));
4749
- // `...options.installConfig` — a spread of `Partial<X> | undefined`
4750
- // (the optional-options merge idiom `{ ...DEFAULTS, ...overrides }`):
4751
- // JS spreads nothing for the unit arm and copies present keys
4752
- // otherwise. Per target field the desugar builds
4753
- // `present ? extracted : earlier` — present tests the source's
4754
- // record tag AND (optional source fields) the field's own value
4755
- // arm; absent keeps the earlier contributor's value (both reads
4756
- // are pure, so the reorder into the ternary is unobservable).
4757
- if (srcType?.kind === "union") {
4758
- const def = lowerer.unions.get(srcType.unionId);
4759
- const recArms = def?.arms.filter((a) => a.kind === "record") ?? [];
4760
- if (!def ||
4761
- recArms.length !== 1 ||
4762
- !def.arms.every((a) => a.kind === "record" || isUnitType(a))) {
4763
- lowerer.unsupported("SC1090", prop, `object spread of '${lowerer.fmt(srcType)}' sources (only known record shapes spread — ${NARROW_FIRST})`);
4764
- }
4765
- if (srcLowered && !pureReemittable(srcLowered)) {
4766
- lowerer.unsupported("SC1090", prop, "object spread of computed sources (the field copies re-read the source — bind it to a const first)");
4767
- }
4768
- const recArm = recArms[0];
4769
- const recTag = def.arms.indexOf(recArm);
4770
- // A checked-dynamic VALUE under the union-mapped checker type
4771
- // (a JS dyn-holding binding): the present/absent desugar tests
4772
- // union tags a dyn box does not carry — fence honestly instead
4773
- // of the validator's ICE.
4774
- const probedSrc = srcLowered ?? probeLower(lowerer, srcNode);
4775
- if (probedSrc?.type.kind === "dyn") {
4776
- lowerer.unsupported("SC1090", prop, `object spread of a checked-dynamic '${lowerer.fmt(srcType)}' source (${NARROW_FIRST})`);
4777
- }
4778
- const srcShape = lowerer.shapes.get(recArm.shapeId);
4779
- if (!srcShape)
4780
- throw new InternalCompilerError(`lowerer bug: spread of unknown shape ${recArm.shapeId}`);
4781
- fenceAccessorSpreadSource(lowerer, prop, srcShape);
4782
- if (srcShape.indexValue || shape.indexValue) {
4783
- lowerer.unsupported("SC1090", prop, "object spread involving index-signature shapes (overflow keys are runtime state — copy the fields you need explicitly)");
4784
- }
4785
- const laterNames = new Set();
4786
- for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
4787
- if (ts.isSpreadAssignment(later)) {
4788
- if (conditionalSpreadOf(later.expression))
4789
- continue;
4790
- const lt = lowerer.mapTypeOf(lowerer.typeOf(later.expression));
4791
- if (lt?.kind === "record") {
4792
- for (const lf of lowerer.shapes.get(lt.shapeId)?.fields ?? [])
4793
- laterNames.add(lf.name);
4794
- }
4795
- continue;
4796
- }
4797
- if (later.name &&
4798
- (ts.isIdentifier(later.name) ||
4799
- ts.isStringLiteral(later.name) ||
4800
- ts.isNumericLiteral(later.name) ||
4801
- (ts.isComputedPropertyName(later.name) && literalComputedKey(lowerer, later.name) !== null))) {
4802
- laterNames.add(propNameText(lowerer, later.name));
4803
- }
4804
- }
4805
- const srcRef = () => srcLowered ?? lowerer.lowerExpr(srcNode);
4806
- for (const f of srcShape.fields) {
4807
- if (laterNames.has(f.name))
4808
- continue;
4809
- const targetType = fieldTypes.get(f.name);
4810
- // No slot on the target shape: the copy DROPS the field
4811
- // (divergence 36's stance, same as the plain-record spread).
4812
- if (!targetType)
4813
- continue;
4814
- if (conditionalNames.has(f.name)) {
4815
- lowerer.unsupported("SC1090", prop, `spread of '${f.name}' over an earlier conditional spread (the desugar keeps one entry per name — restructure so each name has one contributor)`);
4816
- }
4817
- const fRead = () => ({
4818
- kind: "recordGet",
4819
- obj: { kind: "unionNarrow", unionId: srcType.unionId, tag: recTag, value: srcRef(), type: recArm, loc: locOf(prop) },
4820
- shapeId: recArm.shapeId,
4821
- field: f.name,
4822
- type: f.type,
4823
- loc: locOf(prop),
4824
- });
4825
- let cond = { kind: "unionIsTag", unionId: srcType.unionId, tag: recTag, negated: false, value: srcRef(), type: BOOL, loc: locOf(prop) };
4826
- let thenVal;
4827
- if (typeEquals(f.type, targetType)) {
4828
- thenVal = fRead();
4829
- }
4830
- else if (f.type.kind === "union" &&
4831
- lowerer.armTag(f.type.unionId, UNDEFINED_T) >= 0 &&
4832
- typeEquals(lowerer.stripUndefinedArm(f.type), targetType)) {
4833
- // Optional source field into a required target slot: present
4834
- // means the source holds the record AND the field its value
4835
- // arm — the spread-override completion, union-source form.
4836
- const ftUndef = lowerer.armTag(f.type.unionId, UNDEFINED_T);
4837
- cond = {
4838
- kind: "logical",
4839
- op: "&&",
4840
- left: cond,
4841
- right: { kind: "unionIsTag", unionId: f.type.unionId, tag: ftUndef, negated: true, value: fRead(), type: BOOL, loc: locOf(prop) },
4842
- type: BOOL,
4843
- loc: locOf(prop),
4844
- };
4845
- const ftDef = lowerer.unions.get(f.type.unionId);
4846
- if (targetType.kind === "union") {
4847
- const retag = lowerer.unionRetagHelper(f.type.unionId, targetType.unionId, locOf(prop));
4848
- if (!retag) {
4849
- lowerer.pushDiag(recordShapeMismatchDiag(lowerer.fmt(type), lowerer.fmt(recArm), locOf(prop), `spread field '${f.name}': '${lowerer.fmt(f.type)}' cannot re-tag into '${lowerer.fmt(targetType)}' behind the present-test`));
4850
- throw new PoisonError();
4851
- }
4852
- thenVal = { kind: "call", callee: retag, args: [fRead()], type: targetType, loc: locOf(prop) };
4853
- }
4854
- else if (ftDef && ftDef.arms.length === 2 && lowerer.armTag(f.type.unionId, targetType) >= 0) {
4855
- thenVal = { kind: "unionNarrow", unionId: f.type.unionId, tag: lowerer.armTag(f.type.unionId, targetType), value: fRead(), type: targetType, loc: locOf(prop) };
4856
- }
4857
- else {
4858
- lowerer.pushDiag(recordShapeMismatchDiag(lowerer.fmt(type), lowerer.fmt(recArm), locOf(prop), `spread field '${f.name}': '${lowerer.fmt(f.type)}' cannot narrow into '${lowerer.fmt(targetType)}' behind the present-test`));
4859
- throw new PoisonError();
4860
- }
4861
- }
4862
- else {
4863
- // The width-lift fallback (arm wrap, re-tag, nested
4864
- // reshape) — the same per-field rule the slot coercion
4865
- // applies. Runs AFTER the optional-completion branch: a
4866
- // present-test has its own semantics a re-tag's stranded
4867
- // undefined trap must not shadow.
4868
- const lift = lowerer.widthLiftPlan(f.type, targetType);
4869
- if (!lift) {
4870
- lowerer.pushDiag(recordShapeMismatchDiag(lowerer.fmt(type), lowerer.fmt(recArm), locOf(prop), `spread field '${f.name}': '${lowerer.fmt(f.type)}' does not lift into '${lowerer.fmt(targetType)}'`));
4871
- throw new PoisonError();
4872
- }
4873
- thenVal = lowerer.applyWidthLift(lift, fRead(), targetType, locOf(prop));
4874
- }
4875
- const at = fields.findIndex((x) => x.name === f.name && !x.drop);
4876
- const elseVal = at >= 0 ? fields[at].value : lowerer.wrappedUndefined(targetType, locOf(prop));
4877
- if (!elseVal) {
4878
- lowerer.unsupported("SC1090", prop, `object spread of '${lowerer.fmt(srcType)}' sources where '${f.name}' has no earlier contributor (the absent arm leaves the required field unset — spread defaults first: { ...defaults, ...overrides })`);
4879
- }
4880
- const merged = { kind: "ternary", cond, then: thenVal, else_: elseVal, type: targetType, loc: locOf(prop) };
4881
- if (at >= 0)
4882
- fields[at] = { name: f.name, value: merged };
4883
- else
4884
- fields.push({ name: f.name, value: merged });
4885
- }
4886
- continue;
4887
- }
4888
- if (srcType?.kind !== "record") {
4889
- lowerer.unsupported("SC1090", prop, `object spread of '${srcType ? lowerer.fmt(srcType) : lowerer.checker.typeToString(lowerer.typeOf(srcNode))}' sources (only known record shapes spread — ${NARROW_FIRST})`);
4890
- }
4891
- if (srcLowered && !pureReemittable(srcLowered)) {
4892
- lowerer.unsupported("SC1090", prop, "object spread of computed sources (the field copies re-read the source — bind it to a const first)");
4893
- }
4894
- const srcShape = lowerer.shapes.get(srcType.shapeId);
4895
- if (!srcShape)
4896
- throw new InternalCompilerError(`lowerer bug: spread of unknown shape ${srcType.shapeId}`);
4897
- fenceAccessorSpreadSource(lowerer, prop, srcShape);
4898
- // Index-signature shapes carry runtime-keyed overflow entries the
4899
- // field-by-field desugar cannot enumerate — fenced on either side.
4900
- if (srcShape.indexValue || shape.indexValue) {
4901
- lowerer.unsupported("SC1090", prop, "object spread involving index-signature shapes (overflow keys are runtime state — copy the fields you need explicitly)");
4902
- }
4903
- // Names a LATER contributor unconditionally defines: copying such a
4904
- // source field is dead under JS last-write-wins (and spread reads
4905
- // are side-effect-free), so it neither lowers nor width-checks —
4906
- // the spread-then-override completion `{ ...config, stateDir }`
4907
- // narrows an optional source field into a required target slot
4908
- // exactly like Node does. Conditional spreads don't count (their
4909
- // empty arm defines nothing) — their own collision fences hold.
4910
- const laterNames = new Set();
4911
- for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
4912
- if (ts.isSpreadAssignment(later)) {
4913
- if (conditionalSpreadOf(later.expression))
4914
- continue;
4915
- const lt = lowerer.mapTypeOf(lowerer.typeOf(later.expression));
4916
- if (lt?.kind === "record") {
4917
- for (const lf of lowerer.shapes.get(lt.shapeId)?.fields ?? [])
4918
- laterNames.add(lf.name);
4919
- }
4920
- continue;
4921
- }
4922
- if (later.name &&
4923
- (ts.isIdentifier(later.name) ||
4924
- ts.isStringLiteral(later.name) ||
4925
- ts.isNumericLiteral(later.name) ||
4926
- (ts.isComputedPropertyName(later.name) && literalComputedKey(lowerer, later.name) !== null))) {
4927
- laterNames.add(propNameText(lowerer, later.name));
4928
- }
4929
- }
4930
- for (const f of srcShape.fields) {
4931
- if (laterNames.has(f.name))
4932
- continue;
4933
- const targetType = fieldTypes.get(f.name);
4934
- // A source field with NO slot on the target shape: the copy
4935
- // DROPS it — a spread of a wider record into a narrower literal
4936
- // is width subtyping in spread clothing, divergence 36's stance
4937
- // (Node's object would keep the key); the read is pure, so
4938
- // skipping evaluates nothing.
4939
- if (!targetType)
4940
- continue;
4941
- const lift = typeEquals(f.type, targetType)
4942
- ? null
4943
- : lowerer.widthLiftPlan(f.type, targetType);
4944
- if (!typeEquals(f.type, targetType) && !lift) {
4945
- // Print the SOURCE shape, not the literal's own type: the
4946
- // checker's own type already has later overrides applied, so
4947
- // it can render identically to the target while the spread
4948
- // source (the thing that actually mismatches) differs — an
4949
- // invisible difference is a diagnostics bug.
4950
- lowerer.pushDiag(recordShapeMismatchDiag(lowerer.fmt(type), lowerer.fmt(srcType), locOf(prop), `spread field '${f.name}': '${lowerer.fmt(f.type)}' does not lift into '${lowerer.fmt(targetType)}'`));
4951
- throw new PoisonError();
4952
- }
4953
- const obj = srcLowered ?? lowerer.lowerExpr(srcNode);
4954
- // A record-mapped CHECKER type whose VALUE lives in the checked-dynamic tree (a
4955
- // JS file-scope object-literal global): read each field from
4956
- // the checked-dynamic tree (dynKeyGet) and VALIDATE it into the source shape's
4957
- // field type (dynCheck) — the checked-dynamic member-read
4958
- // discipline. A missing key answers the dyn undefined, exactly
4959
- // the undefined-armed optional's absent case; a mismatched
4960
- // runtime value throws the catchable TypeError, never a silent
4961
- // wrong copy. Runtime-ADDED keys drop — width subtyping in
4962
- // spread clothing, divergence 36's stance.
4963
- let value;
4964
- if (obj.type.kind === "dyn") {
4965
- if (!canDynCheckTo(f.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) {
4966
- lowerer.unsupported("SC1100", prop, `object spread of a checked-dynamic source whose field '${f.name}' ('${lowerer.fmt(f.type)}') cannot validate out of the checked-dynamic tree (copy the fields explicitly)`);
4967
- }
4968
- value = {
4969
- kind: "dynCheck",
4970
- value: {
4971
- kind: "dynKeyGet",
4972
- key: { kind: "strLit", value: f.name, type: STRING, loc: locOf(prop) },
4973
- value: obj,
4974
- type: DYN,
4975
- loc: locOf(prop),
4976
- },
4977
- type: f.type,
4978
- loc: locOf(prop),
4979
- };
4980
- }
4981
- else {
4982
- value = {
4983
- kind: "recordGet",
4984
- obj,
4985
- shapeId: srcType.shapeId,
4986
- field: f.name,
4987
- type: f.type,
4988
- loc: locOf(prop),
4989
- };
4990
- }
4991
- // A liftable field widens into the target slot (arm wrap,
4992
- // re-tag, nested reshape) — the same per-field rule the slot
4993
- // coercion applies.
4994
- if (lift)
4995
- value = lowerer.applyWidthLift(lift, value, targetType, locOf(prop));
4996
- if (conditionalNames.has(f.name)) {
4997
- lowerer.unsupported("SC1090", prop, `spread of '${f.name}' over an earlier conditional spread (the desugar keeps one entry per name — restructure so each name has one contributor)`);
4998
- }
4999
- const at = fields.findIndex((x) => x.name === f.name);
5000
- if (at >= 0)
5001
- fields[at] = { name: f.name, value };
5002
- else
5003
- fields.push({ name: f.name, value });
3627
+ const w = lowerer.widthCoerce(src, type);
3628
+ if (w)
3629
+ src = w;
5004
3630
  }
5005
- continue;
5006
- }
5007
- if (ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop)) {
5008
- // JS accessor entries carry no storage (narrowed away above); TS
5009
- // accessors fill the shape's reserved closure slots — the getter
5010
- // body lowers as an ordinary zero-arg closure invoked per property
5011
- // READ, the setter as a one-arg closure invoked per WRITE
5012
- // (fieldGetExpr/fieldSetStmt dispatch on the slots). Creating the
5013
- // closures is side-effect-free, so their position among the data
5014
- // fields' source-order evaluation is unobservable — exactly JS,
5015
- // where accessor definitions evaluate nothing.
5016
- if (isJsSourceFile(expr.getSourceFile()))
5017
- continue;
5018
- const name = propNameText(lowerer, prop.name); // key-form-checked above
5019
- const slotName = `${ts.isGetAccessorDeclaration(prop) ? "%get" : "%set"}:${name}`;
5020
- const slotT = fieldTypes.get(slotName);
5021
- if (!slotT || slotT.kind !== "func") {
5022
- // The contextual shape stores a DATA value under this name
5023
- // (`const p: { x: number } = { get x() {...} }` — tsc lets a
5024
- // live accessor satisfy a data member): the slot would freeze
5025
- // one getter answer where Node keeps the accessor live.
5026
- if (fieldTypes.has(name)) {
5027
- lowerer.unsupported("SC1090", prop, `a get/set accessor satisfying the data property '${name}' of '${lowerer.fmt(type)}' (the record slot stores a plain value — Node would keep the accessor live through this type)`);
5028
- }
5029
- throw shapeMismatch(prop);
3631
+ if (!typeEquals(src.type, type)) {
3632
+ lowerer.unsupported("SC1090", el, `spreading '${lowerer.fmt(src.type)}' into a '${lowerer.fmt(type)}' literal (only a same-element-type array spreads)`);
5030
3633
  }
5031
- const closure = lowerer.coerceInto(prop, lowerer.lowerLambda(prop), slotT);
5032
- if (!typeEquals(closure.type, slotT))
5033
- lowerer.badType(prop, lowerer.typeOf(prop));
5034
- fields.push({ name: slotName, value: closure });
5035
- continue;
3634
+ spreads.push(i);
3635
+ return src;
5036
3636
  }
5037
- const name = propNameText(lowerer, prop.name); // key-form-checked above
5038
- const fieldType = fieldTypes.get(name);
5039
- // An undeclared name against an index-signature shape is an OVERFLOW
5040
- // entry (tsc typechecked it against the signature's value type);
5041
- // against a plain shape it is the width mismatch it always was —
5042
- // EXCEPT the PromiseSettledResult honest subset's dropped fields
5043
- // (value/reason — SEMANTICS.md 46): those evaluate for effect in
5044
- // their source-order slot and store nothing.
5045
- // A GENERIC-callable member (a generic method `m<T>(x: T) {...}` or a
5046
- // generic arrow/function-expression property): excluded from the
5047
- // record shape (isGenericCallableMemberType — no single closure slot
5048
- // can hold it), so the literal stores nothing for it. Pure
5049
- // function-creating forms skip outright (creating a closure has no
5050
- // side effects; calls resolve statically against this declaration);
5051
- // a computed initializer would need its evaluation kept — fenced.
5052
- if (!fieldType && !ts.isSpreadAssignment(prop)) {
5053
- const memberSym = prop.name && lowerer.checker.getSymbolAtLocation(prop.name);
5054
- const memberT = memberSym ? lowerer.checker.getTypeOfSymbol(memberSym) : undefined;
5055
- if (memberT && isGenericCallableMemberType(memberT, lowerer.checker)) {
5056
- const pureInit = (() => {
5057
- if (ts.isMethodDeclaration(prop))
5058
- return true;
5059
- if (ts.isShorthandPropertyAssignment(prop))
5060
- return true; // a pure read
5061
- if (!ts.isPropertyAssignment(prop))
5062
- return false;
5063
- let init = prop.initializer;
5064
- while (ts.isParenthesizedExpression(init))
5065
- init = init.expression;
5066
- return ts.isArrowFunction(init) || ts.isFunctionExpression(init) || ts.isIdentifier(init);
5067
- })();
5068
- if (!pureInit) {
5069
- lowerer.unsupported("SC1090", prop, `generic-function-valued properties with computed initializers ('${name}' has no record slot — its evaluation would be dropped; bind the function to a top-level declaration instead)`);
5070
- }
5071
- continue;
5072
- }
3637
+ // A HOLE (`[,]` — an elision) remains absent. Ordinary reads still
3638
+ // answer undefined, while presence-sensitive methods and concat keep
3639
+ // the hole. The state-building path below advances length without
3640
+ // writing a value into this slot.
3641
+ if (ts.isOmittedExpression(el)) {
3642
+ needsStateBuild = true;
3643
+ holes.add(i);
3644
+ return { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc: locOf(el) };
5073
3645
  }
5074
- if (!fieldType && !shape.indexValue) {
5075
- if (settledDropNames(lowerer, tsType)?.has(name)) {
5076
- // Identifier and shorthand initializers are effect-free reads —
5077
- // skipped outright. The caught `reason` MUST skip: lowering the
5078
- // read would hit the catch-binding narrowness fence, and the
5079
- // snapshot it names needs no evaluation.
5080
- if (ts.isPropertyAssignment(prop) && !ts.isIdentifier(prop.initializer)) {
5081
- const v = lowerer.lowerExpr(prop.initializer);
5082
- // Effect-free lowerings (literals, plain reads) drop at
5083
- // compile time; anything else — the awaited mapper — runs
5084
- // (and may throw into the enclosing catch) with its result
5085
- // released by the statement frame.
5086
- if (v.kind !== "unitLit" && v.kind !== "numLit" && v.kind !== "strLit" &&
5087
- v.kind !== "boolLit" && v.kind !== "varRef" && v.kind !== "closure") {
5088
- fields.push({ name, value: v, drop: true });
3646
+ // An ARRAY-LITERAL element under a UNION element slot whose own type
3647
+ // has no lift into the slot builds against the union's single array
3648
+ // arm instead (`['pwd', []]` as (string | string[])[] — tsc pushes no
3649
+ // contextual type through the unannotated chain, so the empty nested
3650
+ // literal types never[], whose f64-element representation re-tags
3651
+ // into nothing). One array arm is unambiguous — the union-slot rule's
3652
+ // stance; several keep the fence.
3653
+ if (type.elem.kind === "union" || type.elem.kind === "array") {
3654
+ let x = el;
3655
+ while (ts.isParenthesizedExpression(x))
3656
+ x = x.expression;
3657
+ if (ts.isArrayLiteralExpression(x)) {
3658
+ const ownT = lowerer.mapTypeOf(lowerer.checker.getContextualType(x) ?? lowerer.typeOf(x));
3659
+ if (ownT === null || lowerer.widthLiftPlan(ownT, type.elem) === null) {
3660
+ // An ARRAY-typed slot takes the literal directly; a union slot
3661
+ // routes through its single array arm and wraps.
3662
+ if (type.elem.kind === "array") {
3663
+ return lowerer.lowerArrayLiteral(x, type.elem);
3664
+ }
3665
+ const def = lowerer.unions.get(type.elem.unionId);
3666
+ const arrayArms = def?.arms.filter((a) => a.kind === "array") ?? [];
3667
+ if (arrayArms.length === 1) {
3668
+ const built = lowerer.lowerArrayLiteral(x, arrayArms[0]);
3669
+ return lowerer.coerceInto(el, built, type.elem);
5089
3670
  }
5090
3671
  }
5091
- continue;
5092
- }
5093
- throw shapeMismatch(prop);
5094
- }
5095
- let value;
5096
- let valueNode = prop;
5097
- const propDiagsBefore = lowerer.diags.length;
5098
- try {
5099
- if (ts.isPropertyAssignment(prop)) {
5100
- valueNode = prop.initializer;
5101
- // ARRAY-LITERAL initializers route through the expected-type
5102
- // lowering: a union field with one array-family arm builds the
5103
- // literal AS that arm (lowerExprExpecting's IR-directed rule —
5104
- // the option-table `default: [{ value: [] }]` shape), where the
5105
- // bare lowering would take the JS dyn fallback and fence.
5106
- let init = prop.initializer;
5107
- while (ts.isParenthesizedExpression(init))
5108
- init = init.expression;
5109
- value =
5110
- fenceClosureProbe(lowerer, prop.initializer, fieldType, () => lowerer.lowerExpr(prop.initializer)) ??
5111
- (fieldType !== undefined && ts.isArrayLiteralExpression(init)
5112
- ? lowerer.lowerExprExpecting(prop.initializer, fieldType)
5113
- : lowerer.lowerExpr(prop.initializer));
5114
- }
5115
- else if (ts.isShorthandPropertyAssignment(prop)) {
5116
- value = lowerer.lowerShorthandValue(prop);
5117
- }
5118
- else if (ts.isMethodDeclaration(prop)) {
5119
- value =
5120
- fenceClosureProbe(lowerer, prop, fieldType, () => lowerer.lowerLambda(prop)) ?? lowerer.lowerLambda(prop);
5121
3672
  }
5122
- else {
5123
- lowerer.unsupported("SC1090", prop, `syntax '${ts.SyntaxKind[prop.kind]}'`);
5124
- }
5125
- }
5126
- catch (err) {
5127
- // A member VALUE a JS file cannot lower (a namespace object in an
5128
- // export aggregate — the sharedWithCli `errors` member): the
5129
- // member NARROWS AWAY like a CJS export-table accessor entry —
5130
- // the diagnostics defer to the runtime-fence ledger, the shape
5131
- // drops the field, and each READ of it meets its own per-site
5132
- // fence. Pure member forms only (identifier/shorthand reads);
5133
- // TypeScript, probe mode, and ICEs keep the poison.
5134
- const pureMember = ts.isShorthandPropertyAssignment(prop) ||
5135
- (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.initializer));
5136
- if (!(err instanceof PoisonError) ||
5137
- !pureMember ||
5138
- !isJsSourceFile(expr.getSourceFile()) ||
5139
- lowerer.diagSink !== null ||
5140
- lowerer.diags.length <= propDiagsBefore ||
5141
- lowerer.diags.slice(propDiagsBefore).some((d) => d.code === "SC9001")) {
5142
- throw err;
5143
- }
5144
- lowerer.runtimeFences.push(...lowerer.diags.splice(propDiagsBefore));
5145
- droppedNames.add(name);
5146
- continue;
5147
- }
5148
- if (!fieldType) {
5149
- // Overflow entry: the value flows into the index signature's value
5150
- // slot — dyn slots take a dyn conversion (dynFrom), typed slots the
5151
- // ordinary coercion path. Later duplicates override (map semantics
5152
- // are last-write-wins already; a duplicate literal key is a tsc
5153
- // error anyway).
5154
- const slotted = lowerer.intoIndexValueSlot(value, shape.indexValue, valueNode);
5155
- fields.push({ name, value: slotted, overflow: true });
5156
- continue;
5157
- }
5158
- value = lowerer.coerceInto(valueNode, value, fieldType); // union-typed fields wrap arm values
5159
- if (!typeEquals(value.type, fieldType))
5160
- lowerer.badType(valueNode, lowerer.typeOf(valueNode));
5161
- // An explicit property overrides a spread-copied field: JS
5162
- // last-write-wins. The entry moves to the END so explicit property
5163
- // values keep their source-order evaluation among themselves. A
5164
- // conditional-spread entry cannot be overridden (its ternary IS the
5165
- // spread's evaluation; splicing it out would drop that).
5166
- if (conditionalNames.has(name)) {
5167
- lowerer.unsupported("SC1090", prop, `'${name}' after a conditional spread of the same name (the desugar keeps one entry per name — restructure so each name has one contributor)`);
5168
- }
5169
- const at = fields.findIndex((x) => x.name === name);
5170
- if (at >= 0)
5171
- fields.splice(at, 1);
5172
- fields.push({ name, value });
5173
- }
5174
- // Optional fields (undefined-armed union slots) may be omitted: the
5175
- // absent field holds the interned undefined arm, exactly like writing
5176
- // `a: undefined` (without exactOptionalPropertyTypes tsc treats the two
5177
- // the same, and only optional fields may be omitted — tsc rejects
5178
- // omission of required fields before lowering, undefined-armed or not).
5179
- // A REQUIRED missing field keeps the shape-mismatch rejection (possible
5180
- // through `as`: the cast smuggles a narrower literal past freshness).
5181
- if (droppedNames.size > 0) {
5182
- const narrowed = shape.fields.filter((f) => !droppedNames.has(f.name));
5183
- const narrowedId = lowerer.shapes.intern(narrowed.map((f) => ({ name: f.name, type: f.type })), false, shape.indexValue);
5184
- type = { kind: "record", shapeId: narrowedId };
5185
- shape = lowerer.shapes.get(narrowedId);
5186
- }
5187
- if (fields.filter((f) => !f.overflow).length !== shape.fields.length) {
5188
- const provided = new Set(fields.filter((f) => !f.overflow).map((f) => f.name));
5189
- for (const f of shape.fields) {
5190
- if (provided.has(f.name))
5191
- continue;
5192
- // 'unknown' fields complete with the dyn undefined — the absent
5193
- // property reads as undefined in Node, and a dyn slot holds
5194
- // exactly that (the options-record call shape against
5195
- // `{ plugins: unknown, ... }` — a JS caller the checker admits).
5196
- const absent = lowerer.wrappedUndefined(f.type, loc) ?? (f.type.kind === "dyn" ? dynUndefinedExpr(loc) : null);
5197
- if (!absent)
5198
- throw shapeMismatch(expr); // only optional (undefined-armed) and 'unknown' fields may be omitted
5199
- fields.push({ name: f.name, value: absent });
5200
3673
  }
5201
- }
5202
- return { kind: "recordLit", fields, type, loc };
5203
- }
5204
- /** Parses the conditional-spread idiom `...(c ? { k: v, ... } : {})`
5205
- * (either orientation). Returns the condition, the non-empty arm's
5206
- * properties, and which arm carries them; "unsupported" for conditional
5207
- * sources OUTSIDE the idiom (both arms non-empty, computed/method
5208
- * members); null when the spread source isn't a conditional at all.
5209
- * Callers slice their own honest subset (the static record path takes
5210
- * exactly one property; the island literal takes any number; spawn's
5211
- * options walk takes a `detached` boolean literal). */
5212
- export function conditionalSpreadOf(expr) {
5213
- let e = expr;
5214
- while (ts.isParenthesizedExpression(e))
5215
- e = e.expression;
5216
- if (!ts.isConditionalExpression(e))
5217
- return null;
5218
- const unwrap = (a) => {
5219
- let x = a;
5220
- while (ts.isParenthesizedExpression(x))
5221
- x = x.expression;
5222
- return x;
5223
- };
5224
- const whenTrue = unwrap(e.whenTrue);
5225
- const whenFalse = unwrap(e.whenFalse);
5226
- if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
5227
- return "unsupported";
5228
- }
5229
- const trueCarries = whenFalse.properties.length === 0 && whenTrue.properties.length > 0;
5230
- const falseCarries = whenTrue.properties.length === 0 && whenFalse.properties.length > 0;
5231
- if (!trueCarries && !falseCarries)
5232
- return "unsupported"; // both empty is tsc-unreachable; both non-empty has no single desugar
5233
- const carrier = trueCarries ? whenTrue : whenFalse;
5234
- const props = [];
5235
- for (const p of carrier.properties) {
5236
- if ((ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
5237
- p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))) {
5238
- props.push(p);
3674
+ // Elements flow into the element slot like an assignment: union
3675
+ // elements wrap plain arm values (`[1, "a"]` as (number | string)[]),
3676
+ // holes reject inside lowerExpr.
3677
+ let lowered;
3678
+ const directOptionalCandidate = runtimeOptionalElements.has(i) ||
3679
+ ts.isElementAccessExpression(el) ||
3680
+ (ts.isCallExpression(el) && ts.isPropertyAccessExpression(el.expression) &&
3681
+ (el.expression.name.text === "pop" || el.expression.name.text === "shift"));
3682
+ if (directOptionalCandidate) {
3683
+ const raw = lowerer.lowerExpr(el);
3684
+ if (!typeEquals(raw.type, type.elem)) {
3685
+ needsStateBuild = true;
3686
+ return raw;
3687
+ }
3688
+ lowered = raw;
5239
3689
  }
5240
3690
  else {
5241
- return "unsupported";
5242
- }
5243
- }
5244
- return { cond: e.condition, props, whenTrue: trueCarries };
5245
- }
5246
- /** `{ ...idx, ...idx2 }` typed a DECLARED shape (no index signature) — the
5247
- * defaults-merge idiom over runtime-keyed sources (`{ ...fromEntries(a),
5248
- * ...fromEntries(b) }` typed AppConfig). Lowers to ONE interned helper
5249
- * call: sources evaluate once each as arguments (source order — computed
5250
- * sources included, JS's evaluate-once), the result starts all-undefined
5251
- * (every target field must be optional — the runtime keys decide
5252
- * presence), and each source's keys apply in JS own-key order with a
5253
- * per-key dispatch onto the declared fields: a matching key writes the
5254
- * field — identity when the source's value slot IS the field type, a
5255
- * validated extraction otherwise (a dyn slot dynChecks; a union slot
5256
- * re-tags arm-by-arm, and a value outside the field's arms throws the
5257
- * catchable TypeError — divergence 34's keyed-write stance, where Node's
5258
- * untyped copy would store the lie) — and a key naming NO declared field
5259
- * is DROPPED (the shape cannot represent it; Node keeps it invisibly —
5260
- * divergence 68). Later contributors overwrite earlier ones
5261
- * (last-write-wins). Sources must be PURE index-signature records. */
5262
- function lowerDeclaredSpreadMerge(lowerer, expr, type, shape, loc) {
5263
- const srcs = [];
5264
- for (const prop of expr.properties) {
5265
- const spread = prop; // caller-checked: all spreads
5266
- const value = lowerer.lowerExpr(spread.expression);
5267
- const srcShape = value.type.kind === "record" ? lowerer.shapes.get(value.type.shapeId) : undefined;
5268
- if (value.type.kind !== "record" || !srcShape?.indexValue || srcShape.tuple || srcShape.fields.length > 0) {
5269
- lowerer.unsupported("SC1090", prop, `object spread of '${lowerer.fmt(value.type)}' into '${lowerer.fmt(type)}' (only PURE index-signature records — Object.fromEntries results, Record<string, T> values — spread into a declared shape)`);
5270
- }
5271
- srcs.push({ value, shapeId: value.type.shapeId, iv: srcShape.indexValue });
5272
- }
5273
- // Every target field must be optional: absent keys leave the undefined
5274
- // arm, exactly the unset-optional representation.
5275
- for (const f of shape.fields) {
5276
- if (f.type.kind !== "union" || lowerer.armTag(f.type.unionId, UNDEFINED_T) < 0) {
5277
- lowerer.unsupported("SC1090", expr, `object spread of runtime-keyed sources onto the required field '${f.name}' (the keys decide presence at runtime — declare the field optional or spell it explicitly)`);
5278
- }
5279
- }
5280
- // Per (source value slot → field) conversion, checked up front so the
5281
- // fence fires at the literal, not inside the interned helper.
5282
- const conv = (iv, f, v) => {
5283
- if (typeEquals(iv, f.type))
5284
- return { value: v };
5285
- if (iv.kind === "dyn") {
5286
- return { value: { kind: "dynCheck", value: v, type: f.type, loc } };
5287
- }
5288
- if (iv.kind === "union" && f.type.kind === "union") {
5289
- const fUnion = f.type;
5290
- const def = lowerer.unions.get(iv.unionId);
5291
- const fDef = lowerer.unions.get(fUnion.unionId);
5292
- if (def && fDef && def.arms.some((a) => lowerer.armTag(fUnion.unionId, a) >= 0)) {
5293
- // Arm-by-arm validated re-tag, inline in the helper: matching
5294
- // arms map by identity (unit arms re-wrap, value arms narrow and
5295
- // wrap); anything else throws the catchable TypeError.
5296
- return {
5297
- stmts: (write) => {
5298
- const chain = (i) => {
5299
- if (i >= def.arms.length) {
5300
- return [{
5301
- kind: "throw",
5302
- value: {
5303
- kind: "libCall",
5304
- fn: "error.new",
5305
- args: [{ kind: "strLit", value: `expected ${lowerer.fmt(fUnion)} at $.${f.name}`, type: STRING, loc }],
5306
- type: { kind: "object", className: "%TypeError" },
5307
- loc,
5308
- },
5309
- loc,
5310
- }];
5311
- }
5312
- const arm = def.arms[i];
5313
- const toTag = lowerer.armTag(fUnion.unionId, arm);
5314
- if (toTag < 0)
5315
- return chain(i + 1);
5316
- const extracted = isUnitType(arm)
5317
- ? { kind: "unionWrap", unionId: fUnion.unionId, tag: toTag, value: { kind: "unitLit", unit: arm.kind === "undefinedT" ? "undefined" : "null", type: arm, loc }, type: fUnion, loc }
5318
- : { kind: "unionWrap", unionId: fUnion.unionId, tag: toTag, value: { kind: "unionNarrow", unionId: iv.unionId, tag: i, value: v, type: arm, loc }, type: fUnion, loc };
5319
- return [{
5320
- kind: "if",
5321
- cond: { kind: "unionIsTag", unionId: iv.unionId, tag: i, negated: false, value: v, type: BOOL, loc },
5322
- then: [write(extracted)],
5323
- else_: chain(i + 1),
5324
- loc,
5325
- }];
5326
- };
5327
- return chain(0);
5328
- },
5329
- };
5330
- }
3691
+ lowered = lowerer.lowerExprExpecting(el, type.elem);
5331
3692
  }
5332
- lowerer.unsupported("SC1090", expr, `object spread into '${lowerer.fmt(type)}' where the source's '${lowerer.fmt(iv)}' values cannot reach the '${lowerer.fmt(f.type)}' field '${f.name}' (the value slot must be the field type, 'unknown', or a union covering the field's arms)`);
5333
- };
5334
- const key = `declmerge:${type.shapeId}:${srcs.map((s) => s.shapeId).join(",")}`;
5335
- let helper = lowerer.widthHelpers.get(key);
5336
- if (!helper) {
5337
- helper = `%rec.declmerge.${lowerer.widthHelpers.size}`;
5338
- const outRef = varRef("out.0", type, loc);
5339
- const ksT = arrayOf(STRING);
5340
- const locals = [{ id: "out.0", name: "out", type, mutable: false }];
5341
- const params = srcs.map((s, j) => {
5342
- locals.push({ id: `s${j}.0`, name: `s${j}`, type: s.value.type, mutable: true });
5343
- return { localId: `s${j}.0`, name: `s${j}`, type: s.value.type };
5344
- });
5345
- const body = [
5346
- {
5347
- kind: "varDecl",
5348
- localId: "out.0",
5349
- init: {
5350
- kind: "recordLit",
5351
- fields: shape.fields.map((f) => ({ name: f.name, value: lowerer.wrappedUndefined(f.type, loc) })),
5352
- type,
5353
- loc,
5354
- },
5355
- loc,
5356
- },
5357
- ];
5358
- srcs.forEach((s, j) => {
5359
- const sRef = varRef(`s${j}.0`, s.value.type, loc);
5360
- const kRef = varRef(`k${j}.0`, STRING, loc);
5361
- const vRef = varRef(`v${j}.0`, s.iv, loc);
5362
- locals.push({ id: `ks${j}.0`, name: `ks${j}`, type: ksT, mutable: false }, { id: `i${j}.0`, name: `i${j}`, type: F64, mutable: true }, { id: `k${j}.0`, name: `k${j}`, type: STRING, mutable: false }, { id: `v${j}.0`, name: `v${j}`, type: s.iv, mutable: false });
5363
- // Per-key dispatch: if (k === "a") { write a } else if ... else drop.
5364
- const dispatch = shape.fields.reduceRight((rest, f) => {
5365
- const write = (value) => ({ kind: "recordSet", obj: outRef, shapeId: type.shapeId, field: f.name, value, loc });
5366
- const c = conv(s.iv, f, vRef);
5367
- const thenBody = "value" in c ? [write(c.value)] : c.stmts(write);
5368
- return [{
5369
- kind: "if",
5370
- cond: { kind: "strEq", negated: false, left: kRef, right: { kind: "strLit", value: f.name, type: STRING, loc }, type: BOOL, loc },
5371
- then: thenBody,
5372
- else_: rest.length > 0 ? rest : null,
5373
- loc,
5374
- }];
5375
- }, []);
5376
- body.push({ kind: "varDecl", localId: `ks${j}.0`, init: { kind: "recordOvfKeys", obj: sRef, shapeId: s.shapeId, type: ksT, loc }, loc }, {
5377
- kind: "for",
5378
- init: { kind: "varDecl", localId: `i${j}.0`, init: numLit(0, loc), loc },
5379
- cond: { kind: "bin", op: "<", left: varRef(`i${j}.0`, F64, loc), right: { kind: "arrIntrinsic", method: "length", receiver: varRef(`ks${j}.0`, ksT, loc), args: [], type: F64, loc }, type: BOOL, loc },
5380
- update: { kind: "assign", localId: `i${j}.0`, value: { kind: "bin", op: "+", left: varRef(`i${j}.0`, F64, loc), right: numLit(1, loc), type: F64, loc }, loc },
5381
- body: [
5382
- { kind: "varDecl", localId: `k${j}.0`, init: { kind: "arrayGet", arr: varRef(`ks${j}.0`, ksT, loc), index: varRef(`i${j}.0`, F64, loc), type: STRING, loc }, loc },
5383
- { kind: "varDecl", localId: `v${j}.0`, init: { kind: "recordKeyGet", obj: sRef, shapeId: s.shapeId, key: kRef, overflowOnly: true, type: s.iv, loc }, loc },
5384
- ...dispatch,
5385
- ],
3693
+ if (!typeEquals(lowered.type, type.elem))
3694
+ lowerer.badType(el, lowerer.typeOf(el));
3695
+ return lowered;
3696
+ });
3697
+ if (!needsStateBuild) {
3698
+ return { kind: "arrayLit", elems, ...(spreads.length > 0 ? { spreads } : {}), type, loc };
3699
+ }
3700
+ const out = lowerer.declareHiddenLocal("%arrayLit", type);
3701
+ const outRef = varRef(out.id, type, loc);
3702
+ const body = [
3703
+ { kind: "varDecl", localId: out.id, init: { kind: "arrayLit", elems: [], type, loc }, loc },
3704
+ ];
3705
+ const spreadSet = new Set(spreads);
3706
+ const currentLength = () => ({
3707
+ kind: "arrIntrinsic",
3708
+ method: "length",
3709
+ receiver: outRef,
3710
+ args: [],
3711
+ type: F64,
3712
+ loc,
3713
+ });
3714
+ const nextLength = () => ({
3715
+ kind: "bin",
3716
+ op: "+",
3717
+ left: currentLength(),
3718
+ right: { kind: "numLit", value: 1, type: F64, loc },
3719
+ type: F64,
3720
+ loc,
3721
+ });
3722
+ for (let i = 0; i < elems.length; i++) {
3723
+ const el = elems[i];
3724
+ if (spreadSet.has(i)) {
3725
+ body.push({
3726
+ kind: "exprStmt",
3727
+ expr: { kind: "arrIntrinsic", method: "pushSpread", receiver: outRef, args: [el], type: F64, loc },
5386
3728
  loc,
5387
3729
  });
5388
- });
5389
- body.push({ kind: "return", value: outRef, loc });
5390
- lowerer.liftedFns.push({
5391
- name: helper,
5392
- params,
5393
- returnType: type,
5394
- locals,
5395
- body,
5396
- loc,
5397
- });
5398
- // Registered only after a fence-free build: a conv() fence mid-build
5399
- // must not leave a phantom helper behind for the next literal.
5400
- lowerer.widthHelpers.set(key, helper);
5401
- }
5402
- return { kind: "call", callee: helper, args: srcs.map((s) => s.value), type, loc };
5403
- }
5404
- /** The value of a shorthand property (`{ x }`): the binding `x` refers to.
5405
- * The property name's own symbol is the PROPERTY, so resolution goes
5406
- * through getShorthandAssignmentValueSymbol. */
5407
- export function lowerShorthandValue(lowerer, prop) {
5408
- // 7 types the shorthand's name as PropertyName; it is always an
5409
- // Identifier (the grammar allows nothing else in shorthand position).
5410
- const propName = prop.name;
5411
- const loc = locOf(propName);
5412
- const symbol = lowerer.checker.getShorthandAssignmentValueSymbol(prop);
5413
- if (symbol) {
5414
- if (lowerer.ctx.selfSymbol === symbol) {
5415
- return { kind: "selfRef", type: lowerer.ctx.selfType, loc };
5416
- }
5417
- const local = lowerer.resolveKey(symbol, propName);
5418
- if (local) {
5419
- return lowerer.maybeNarrow({ kind: "varRef", localId: local.id, type: local.type, loc }, propName);
5420
- }
5421
- const resolved = symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
5422
- lowerer.flushDeferred(resolved);
5423
- const g = lowerer.globalsBySymbol.get(resolved);
5424
- if (g) {
5425
- return lowerer.maybeNarrow({ kind: "varRef", localId: g.id, type: g.type, loc }, propName);
3730
+ continue;
5426
3731
  }
5427
- const sig = lowerer.fnSigsBySymbol.get(resolved);
5428
- const decl = lowerer.checker.declarationsOf(resolved)[0];
5429
- if (sig && decl && ts.isFunctionDeclaration(decl) &&
5430
- (ts.isSourceFile(decl.parent) || lowerer.nsBlocks.get(decl.parent) === "flattened")) {
5431
- lowerer.noteEdge(sig.name);
5432
- const funcType = {
5433
- kind: "func",
5434
- params: sig.params.filter((p) => p.mode !== "dynRest").map((p) => p.type),
5435
- ret: sig.returnType,
5436
- ...(sig.params.some((p) => p.mode === "dynRest") ? { rest: true } : {}),
5437
- };
5438
- lowerer.requireExactArityValue(prop, propName, sig.params, funcType);
5439
- return { kind: "closure", fnName: sig.name, captures: [], type: funcType, loc };
3732
+ if (holes.has(i)) {
3733
+ body.push({ kind: "arraySetLength", arr: outRef, length: nextLength(), loc });
3734
+ continue;
5440
3735
  }
5441
- if (lowerer.genericFnsBySymbol.has(resolved)) {
5442
- lowerer.unsupported("SC1090", prop, `generic functions as values (call '${propName.text}' directly)`);
3736
+ let value = el;
3737
+ if (!typeEquals(value.type, type.elem)) {
3738
+ const temp = lowerer.declareHiddenLocal("%arrayValue", value.type);
3739
+ body.push({ kind: "varDecl", localId: temp.id, init: value, loc });
3740
+ value = varRef(temp.id, temp.type, loc);
5443
3741
  }
3742
+ body.push(arrayValueStore(lowerer, outRef, currentLength(), value, type.elem, loc));
5444
3743
  }
5445
- lowerer.rejectUnresolvedSymbol(symbol ? (symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol) : null, propName.text, prop, `the reference to '${propName.text}' (a binding form with no lowering)`);
5446
- }
5447
- /** Rejects any `this` inside an object-literal method body — including in
5448
- * nested arrows, which inherit the method's `this` (nested function
5449
- * expressions reset it, but their bare `this` is already a tsc error
5450
- * under noImplicitThis, so over-rejecting them here changes nothing). */
5451
- export function rejectThisInObjectMethod(lowerer, node) {
5452
- if (node.kind === ts.SyntaxKind.ThisKeyword) {
5453
- lowerer.unsupported("SC1090", node, "references to 'this' in object literal methods");
5454
- }
5455
- ts.forEachChild(node, (child) => lowerer.rejectThisInObjectMethod(child));
3744
+ return {
3745
+ kind: "seqExpr",
3746
+ stmts: body,
3747
+ result: outRef,
3748
+ type,
3749
+ loc,
3750
+ };
5456
3751
  }
5457
- /** The accessor twin of rejectThisInObjectMethod: a get/set accessor body
5458
- * referencing `this` (`get x() { return this._x }`). The accessor lowers
5459
- * as a closure stored IN the record — passing the record as a receiver
5460
- * would capture the value under construction (an RC cycle), and the
5461
- * generic lexical-this walk would silently bind an ENCLOSING method's
5462
- * `this`; the fence names the fix (capture a binding instead). */
5463
- function rejectThisInObjectAccessor(lowerer, node) {
5464
- if (node.kind === ts.SyntaxKind.ThisKeyword) {
5465
- lowerer.unsupported("SC1090", node, "references to 'this' in object literal get/set accessors (the accessor lowers as a captured closure with no receiver — read a captured binding instead)");
5466
- }
5467
- ts.forEachChild(node, (child) => rejectThisInObjectAccessor(lowerer, child));
3752
+ /** Convert an undefined-armed numeric value at an arithmetic use. Ordinary
3753
+ * reads preserve their tagged value; JavaScript's ToNumber(undefined) is NaN,
3754
+ * so compounds and binary arithmetic use this checked one-time extraction. */
3755
+ export function lowerOptionalNumber(lowerer, operand, loc, narrowedNode) {
3756
+ if (operand.type.kind !== "union" || lowerer.armTag(operand.type.unionId, UNDEFINED_T) < 0)
3757
+ return operand;
3758
+ const directNumber = lowerer.stripUndefinedArm(operand.type).kind === "f64";
3759
+ const checkerNumber = narrowedNode !== undefined && lowerer.mapTypeOf(lowerer.typeOf(narrowedNode))?.kind === "f64";
3760
+ if (!directNumber && (!checkerNumber || lowerer.armTag(operand.type.unionId, F64) < 0))
3761
+ return operand;
3762
+ const undefTag = lowerer.armTag(operand.type.unionId, UNDEFINED_T);
3763
+ const valueTag = lowerer.armTag(operand.type.unionId, F64);
3764
+ if (undefTag < 0 || valueTag < 0)
3765
+ return operand;
3766
+ const checked = directNumber ? null : lowerer.narrowedArmHelper(operand.type.unionId, F64, loc);
3767
+ if (!directNumber && !checked)
3768
+ return operand;
3769
+ let stable = operand;
3770
+ let prefix = [];
3771
+ if (!isSafeToRepeat(operand)) {
3772
+ const tmp = lowerer.declareHiddenLocal("%arith", operand.type);
3773
+ stable = varRef(tmp.id, operand.type, loc);
3774
+ prefix = [{ kind: "varDecl", localId: tmp.id, init: operand, loc }];
3775
+ }
3776
+ const value = {
3777
+ kind: "ternary",
3778
+ cond: { kind: "unionIsTag", unionId: operand.type.unionId, tag: undefTag, negated: false, value: stable, type: BOOL, loc },
3779
+ then: { kind: "bin", op: "/", left: numLit(0, loc), right: numLit(0, loc), type: F64, loc },
3780
+ else_: checked
3781
+ ? { kind: "call", callee: checked, args: [stable], type: F64, loc }
3782
+ : { kind: "unionNarrow", unionId: operand.type.unionId, tag: valueTag, value: stable, type: F64, loc },
3783
+ type: F64,
3784
+ loc,
3785
+ };
3786
+ return prefix.length === 0 ? value : { kind: "seqExpr", stmts: prefix, result: value, type: F64, loc };
5468
3787
  }
5469
3788
  /** `a[i]` reads. Only f64 indices into array receivers are modeled; JS
5470
3789
  * string-key element access (`a["length"]`) and string indexing (`s[0]`
@@ -5477,7 +3796,7 @@ export function lowerElementAccess(lowerer, expr) {
5477
3796
  }
5478
3797
  // `a?.b[i]` — the tail of a chain whose token sits deeper: the whole
5479
3798
  // tail short-circuits with the guard.
5480
- if (!expr.questionDotToken && chainTailClaimed(lowerer, expr)) {
3799
+ if (!expr.questionDotToken && isOptionalChainTail(lowerer, expr)) {
5481
3800
  return lowerer.lowerOptionalChain(expr);
5482
3801
  }
5483
3802
  // Enum accesses in element clothing — `E["A"]` forward reads and
@@ -5488,6 +3807,9 @@ export function lowerElementAccess(lowerer, expr) {
5488
3807
  if (en)
5489
3808
  return en;
5490
3809
  }
3810
+ const cachedModule = lowerRequireCacheElement(lowerer, expr);
3811
+ if (cachedModule)
3812
+ return cachedModule;
5491
3813
  // Native Web handles use the same supported surface for bracket and dot
5492
3814
  // spellings. Fence a statically-known unsupported key before the generic
5493
3815
  // checked-dynamic element-read path can turn it into a runtime missing
@@ -5584,7 +3906,7 @@ export function lowerElementAccess(lowerer, expr) {
5584
3906
  ? { kind: "toString", operand: rawKey, type: STRING, loc: rawKey.loc }
5585
3907
  : null;
5586
3908
  if (key) {
5587
- const opt = chainGuardedByQuestionDot(expr.expression);
3909
+ const opt = hasOptionalChainGuard(expr.expression);
5588
3910
  return lowerer.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: recv, type: DYN, loc: locOf(expr) }, expr);
5589
3911
  }
5590
3912
  }
@@ -5592,7 +3914,16 @@ export function lowerElementAccess(lowerer, expr) {
5592
3914
  // result; any invalid index traps (the array discipline; JS would
5593
3915
  // read undefined — divergence 4's policy).
5594
3916
  if (receiverIr?.kind === "bytes") {
5595
- const recv = lowerer.lowerExpr(expr.expression);
3917
+ let recv = lowerer.lowerExpr(expr.expression);
3918
+ if (recv.type.kind === "union" && lowerer.armTag(recv.type.unionId, UNDEFINED_T) >= 0) {
3919
+ const present = lowerer.stripUndefinedArm(recv.type);
3920
+ const helper = present.kind === "bytes"
3921
+ ? lowerer.narrowedArmHelper(recv.type.unionId, present, locOf(expr.expression))
3922
+ : null;
3923
+ recv = helper
3924
+ ? { kind: "call", callee: helper, args: [recv], type: present, loc: locOf(expr.expression) }
3925
+ : lowerer.maybeNarrow(recv, expr.expression);
3926
+ }
5596
3927
  // A typed-array .d.ts surface whose VALUE is an island handle
5597
3928
  // (`data()[0]` on a declared Uint8Array return — call results only
5598
3929
  // exit primitives eagerly): the engine element read, exiting at the
@@ -5612,7 +3943,7 @@ export function lowerElementAccess(lowerer, expr) {
5612
3943
  if (receiverIr?.kind === "record") {
5613
3944
  const shape = lowerer.shapes.get(receiverIr.shapeId);
5614
3945
  if (shape?.tuple) {
5615
- const obj = lowerer.lowerExpr(expr.expression);
3946
+ let obj = lowerer.lowerExpr(expr.expression);
5616
3947
  // A tuple-typed .d.ts surface whose VALUE is an island handle
5617
3948
  // (`pair()[0]` on a declared `[number, string]` return — the
5618
3949
  // anonymous tuple type has no npm symbol, so the checker maps it
@@ -5621,22 +3952,38 @@ export function lowerElementAccess(lowerer, expr) {
5621
3952
  // like the array path.
5622
3953
  if (obj.type.kind === "jsval")
5623
3954
  return islandElementRead(lowerer, expr, obj);
3955
+ if (obj.type.kind === "union" && lowerer.armTag(obj.type.unionId, UNDEFINED_T) >= 0) {
3956
+ // The checker sees the outer `arrays[i]` as a tuple when
3957
+ // noUncheckedIndexedAccess is disabled, but the lowered read is
3958
+ // still tuple | undefined. A nested tuple field must validate the
3959
+ // outer tag before recordGet; an unchecked unionNarrow would let a
3960
+ // missing outer element reach the record backend as a bad pointer.
3961
+ const present = lowerer.stripUndefinedArm(obj.type);
3962
+ const helper = present.kind === "record"
3963
+ ? lowerer.narrowedArmHelper(obj.type.unionId, present, locOf(expr.expression))
3964
+ : null;
3965
+ obj = helper
3966
+ ? { kind: "call", callee: helper, args: [obj], type: present, loc: locOf(expr.expression) }
3967
+ : lowerer.maybeNarrow(obj, expr.expression);
3968
+ }
5624
3969
  const idx = tupleLiteralIndex(expr.argumentExpression);
5625
3970
  if (idx === null) {
5626
3971
  lowerer.unsupported("SC1090", expr.argumentExpression, "tuple indexing with a non-literal index (tuples are fixed-shape — use t[0], t[1], ...)");
5627
3972
  }
5628
- const fieldType = shape.fields.find((f) => f.name === String(idx))?.type;
3973
+ const shapeId = obj.type.kind === "record" ? obj.type.shapeId : receiverIr.shapeId;
3974
+ const actualShape = lowerer.shapes.get(shapeId) ?? shape;
3975
+ const fieldType = actualShape.fields.find((f) => f.name === String(idx))?.type;
5629
3976
  if (fieldType === undefined)
5630
3977
  lowerer.badType(expr, lowerer.typeOf(expr)); // OOB smuggled past tsc
5631
3978
  const get = {
5632
3979
  kind: "recordGet",
5633
3980
  obj,
5634
- shapeId: receiverIr.shapeId,
3981
+ shapeId,
5635
3982
  field: String(idx),
5636
3983
  type: fieldType,
5637
3984
  loc: locOf(expr),
5638
3985
  };
5639
- return lowerer.maybeNarrow(get, expr);
3986
+ return lowerer.isRuntimeOptionalField(shapeId, String(idx)) ? get : lowerer.maybeNarrow(get, expr);
5640
3987
  }
5641
3988
  if (shape && !shape.tuple) {
5642
3989
  // A record-typed .d.ts surface whose VALUE is an island handle
@@ -5673,7 +4020,7 @@ export function lowerElementAccess(lowerer, expr) {
5673
4020
  ? { kind: "toString", operand: rawKey, type: STRING, loc: rawKey.loc }
5674
4021
  : null;
5675
4022
  if (key) {
5676
- const opt = chainGuardedByQuestionDot(expr.expression);
4023
+ const opt = hasOptionalChainGuard(expr.expression);
5677
4024
  return lowerer.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: obj, type: DYN, loc: locOf(expr) }, expr);
5678
4025
  }
5679
4026
  }
@@ -5695,6 +4042,17 @@ export function lowerElementAccess(lowerer, expr) {
5695
4042
  const value = lowerer.lowerExpr(expr.expression);
5696
4043
  if (value.type.kind === "union") {
5697
4044
  const key = lowerer.lowerExpr(expr.argumentExpression);
4045
+ if (key.type.kind === "f64") {
4046
+ const def = lowerer.unions.get(value.type.unionId);
4047
+ const presentArms = def?.arms.filter((arm) => !isUnitType(arm)) ?? [];
4048
+ if (presentArms.length === 1 && presentArms[0]?.kind === "string") {
4049
+ const property = runtimeOptionalElementKey(expr.argumentExpression);
4050
+ const receiver = property === null ? null : lowerer.runtimeOptionalPropertyReceiver(expr.expression, value, STRING, property);
4051
+ if (receiver !== null) {
4052
+ return { kind: "strIntrinsic", method: "charAt", receiver, args: [key], type: STRING, loc: locOf(expr) };
4053
+ }
4054
+ }
4055
+ }
5698
4056
  if (key.type.kind === "string") {
5699
4057
  const lit = ts.isStringLiteral(expr.argumentExpression)
5700
4058
  ? expr.argumentExpression.text
@@ -5751,6 +4109,65 @@ export function lowerElementAccess(lowerer, expr) {
5751
4109
  lowerer.unsupported("SC1090", expr, "element access on non-array values");
5752
4110
  }
5753
4111
  let arr = lowerer.lowerExpr(expr.expression);
4112
+ const runtimeOptionalSource = lowerer.runtimeOptionalSourceValue(expr.expression, arr);
4113
+ if (runtimeOptionalSource?.kind === "varRef" &&
4114
+ runtimeOptionalSource.type.kind === "union" &&
4115
+ arr.type.kind === "array") {
4116
+ // lowerer.lowerElementAccess has already recognized a captured
4117
+ // array | undefined receiver and supplied its checked extraction as
4118
+ // the expression override. Rebuild that check here so a missing
4119
+ // receiver throws the member-access error Node exposes, rather than
4120
+ // the generic target-union narrowing error. The stored receiver is a
4121
+ // varRef, so both the tag test and extraction read the same value
4122
+ // without repeating an effect.
4123
+ const stored = runtimeOptionalSource;
4124
+ if (stored.type.kind !== "union")
4125
+ throw new InternalCompilerError("runtime-optional array source lost its union storage");
4126
+ const unionId = stored.type.unionId;
4127
+ const undefTag = lowerer.armTag(unionId, UNDEFINED_T);
4128
+ const valueTag = lowerer.armTag(unionId, arr.type);
4129
+ const property = runtimeOptionalElementKey(expr.argumentExpression);
4130
+ if (undefTag >= 0 && valueTag >= 0 && property !== null) {
4131
+ arr = {
4132
+ kind: "ternary",
4133
+ cond: {
4134
+ kind: "unionIsTag",
4135
+ unionId,
4136
+ tag: undefTag,
4137
+ negated: false,
4138
+ value: stored,
4139
+ type: BOOL,
4140
+ loc: locOf(expr.expression),
4141
+ },
4142
+ then: nodeThrowExpr(1, "", `Cannot read properties of undefined (reading '${property}')`, arr.type, locOf(expr.expression)),
4143
+ else_: {
4144
+ kind: "unionNarrow",
4145
+ unionId,
4146
+ tag: valueTag,
4147
+ value: stored,
4148
+ type: arr.type,
4149
+ loc: locOf(expr.expression),
4150
+ },
4151
+ type: arr.type,
4152
+ loc: locOf(expr.expression),
4153
+ };
4154
+ }
4155
+ }
4156
+ if (arr.type.kind === "union" &&
4157
+ lowerer.armTag(arr.type.unionId, UNDEFINED_T) >= 0 &&
4158
+ receiverIr.kind === "array") {
4159
+ // As with tuple receivers above, an unchecked outer read can be
4160
+ // undefined even when the checker reports an array. Extract the
4161
+ // present array arm with a runtime tag check before arrayGet or the
4162
+ // safe indexed-read helper runs.
4163
+ const present = lowerer.stripUndefinedArm(arr.type);
4164
+ const helper = present.kind === "array"
4165
+ ? lowerer.narrowedArmHelper(arr.type.unionId, present, locOf(expr.expression))
4166
+ : null;
4167
+ arr = helper
4168
+ ? { kind: "call", callee: helper, args: [arr], type: present, loc: locOf(expr.expression) }
4169
+ : lowerer.maybeNarrow(arr, expr.expression);
4170
+ }
5754
4171
  // The checker sees an array but the VALUE is an island handle (an
5755
4172
  // array-typed .d.ts surface: `parts()[0]` on a declared `string[]`
5756
4173
  // return, `issue.path[0]` on a declared member — arrays never exit
@@ -5782,12 +4199,22 @@ export function lowerElementAccess(lowerer, expr) {
5782
4199
  // signature where the .d.ts says Function[], which would flatten the
5783
4200
  // element to a zero-parameter closure).
5784
4201
  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)) {
4202
+ // Every ordinary array read is a JavaScript property read: an invalid
4203
+ // index answers undefined, even when the checker was built without
4204
+ // noUncheckedIndexedAccess. Keep the explicit optional union in the IR
4205
+ // so it can flow through templates, locals, calls, returns, operators,
4206
+ // and narrowing. The helper performs one length/proof check and then
4207
+ // uses the dense getter for the proven-present path, so this does not
4208
+ // change the storage representation or the low-level getter itself.
4209
+ if (arr.type.kind === "array") {
4210
+ // A non-null assertion is the explicit proven-present form. Preserve
4211
+ // the dense getter's established bounds trap here; turning `xs[i]!`
4212
+ // into an optional union would change library ABI checks and the
4213
+ // existing catchable RangeError contract for an out-of-bounds assert.
4214
+ const provenPresent = ts.isNonNullExpression(expr.parent) && expr.parent.expression === expr;
4215
+ if (provenPresent) {
4216
+ return lowerer.maybeNarrow({ kind: "arrayGet", arr, index, type: elemT, loc: locOf(expr) }, expr);
4217
+ }
5791
4218
  const safe = lowerSafeIndexRead(lowerer, arr, index, locOf(expr));
5792
4219
  if (safe)
5793
4220
  return safe;
@@ -5807,16 +4234,6 @@ export function lowerElementAccess(lowerer, expr) {
5807
4234
  // narrows literal-index element accesses).
5808
4235
  return lowerer.maybeNarrow({ kind: "arrayGet", arr, index, type: elemT, loc: locOf(expr) }, expr);
5809
4236
  }
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
4237
  /** `o[k]` where the RECEIVER is an island value — a jsval-mapped checker
5821
4238
  * type, or an array/tuple-typed .d.ts surface whose lowered value is a
5822
4239
  * handle. The key marshals in (any JS key kind — the engine does its
@@ -5883,8 +4300,7 @@ export function lowerRecordKeyRead(lowerer, expr, shapeId, shape, includeUndefin
5883
4300
  // Number AND checked-dynamic keys ride the JS-exact formatter —
5884
4301
  // property keys ARE strings (o[k] is o[String(k)] in JS), and a dyn
5885
4302
  // 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);
4303
+ dk = lowerRecordPropertyKey(lowerer, dk, keyNode);
5888
4304
  if (dk.type.kind !== "string") {
5889
4305
  lowerer.unsupported("SC1090", keyNode, "indexing records with non-string or non-number keys");
5890
4306
  }
@@ -5914,11 +4330,23 @@ export function lowerRecordKeyRead(lowerer, expr, shapeId, shape, includeUndefin
5914
4330
  : lowerer.lowerExpr(keyNode);
5915
4331
  // Runtime NUMBER keys ride the JS-exact formatter (o[n] is o[String(n)]
5916
4332
  // in JS — the number-keyed-signature access path).
5917
- if (key.type.kind === "f64")
5918
- key = lowerer.ensureString(key, keyNode);
4333
+ key = lowerRecordPropertyKey(lowerer, key, keyNode);
5919
4334
  if (key.type.kind !== "string") {
5920
4335
  lowerer.unsupported("SC1090", keyNode, "indexing records with non-string or non-number keys");
5921
4336
  }
4337
+ if (shape.fields.length === 0 && shape.indexValue === undefined) {
4338
+ const result = dynUndefinedExpr(loc);
4339
+ return {
4340
+ kind: "seqExpr",
4341
+ stmts: [
4342
+ { kind: "exprStmt", expr: obj, loc },
4343
+ { kind: "exprStmt", expr: key, loc },
4344
+ ],
4345
+ result,
4346
+ type: DYN,
4347
+ loc,
4348
+ };
4349
+ }
5922
4350
  // The DECLARED result type of the access — the index signature's value
5923
4351
  // type (armed with undefined under noUncheckedIndexedAccess), or the
5924
4352
  // declared fields' one common type on signature-free shapes (tsc's
@@ -5958,6 +4386,16 @@ export function lowerRecordKeyRead(lowerer, expr, shapeId, shape, includeUndefin
5958
4386
  };
5959
4387
  return includeUndefined ? read : lowerer.maybeNarrow(read, expr);
5960
4388
  }
4389
+ /** ToPropertyKey for represented record keys. Missing array-derived string
4390
+ * values remain real undefined values, whose JavaScript property spelling
4391
+ * is the string "undefined". */
4392
+ function lowerRecordPropertyKey(lowerer, key, node) {
4393
+ if (key.type.kind === "f64" || key.type.kind === "bool" || key.type.kind === "dyn" ||
4394
+ lowerer.runtimeOptionalWidening(key.type, STRING) !== null) {
4395
+ return lowerer.ensureString(key, node);
4396
+ }
4397
+ return key;
4398
+ }
5961
4399
  /** A keyed read used only to observe absence (`if (map[k])`, or the left
5962
4400
  * side of `||`/`??`). JavaScript answers undefined for missing record keys
5963
4401
  * and array indices even when noUncheckedIndexedAccess is disabled. Keep
@@ -6000,6 +4438,9 @@ export function lowerAbsenceProbe(lowerer, node) {
6000
4438
  }
6001
4439
  if (!ts.isElementAccessExpression(expr))
6002
4440
  return null;
4441
+ const cachedModule = lowerRequireCacheElement(lowerer, expr);
4442
+ if (cachedModule)
4443
+ return cachedModule;
6003
4444
  const header = lowerer.lowerHttpHeadersElement(expr);
6004
4445
  if (header)
6005
4446
  return header;
@@ -6097,6 +4538,7 @@ function tupleLiteralIndex(node) {
6097
4538
  * assignment, produce no value in our subset). */
6098
4539
  export function lowerElementWrite(lowerer, expr) {
6099
4540
  const target = expr.left;
4541
+ fenceNodeModuleMutation(lowerer, target, "assignment");
6100
4542
  // `process.env[key] = v` — the computed twin of the dotted env write:
6101
4543
  // setenv(3), string keys and string values only.
6102
4544
  if (lowerer.isProcessEnv(target.expression) && !target.questionDotToken) {
@@ -6229,8 +4671,7 @@ export function lowerElementWrite(lowerer, expr) {
6229
4671
  let key = litKey !== null
6230
4672
  ? { kind: "strLit", value: litKey, type: STRING, loc: locOf(target.argumentExpression) }
6231
4673
  : lowerer.lowerExpr(target.argumentExpression);
6232
- if (key.type.kind === "f64")
6233
- key = lowerer.ensureString(key, target.argumentExpression);
4674
+ key = lowerRecordPropertyKey(lowerer, key, target.argumentExpression);
6234
4675
  if (key.type.kind !== "string") {
6235
4676
  lowerer.unsupported("SC1090", target.argumentExpression, "indexing records with non-string or non-number keys");
6236
4677
  }
@@ -6308,11 +4749,7 @@ export function lowerElementWrite(lowerer, expr) {
6308
4749
  : lowerer.lowerExpr(target.argumentExpression);
6309
4750
  // Runtime number/boolean/dyn keys stringify — ToPropertyKey,
6310
4751
  // 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
- }
4752
+ key = lowerRecordPropertyKey(lowerer, key, target.argumentExpression);
6316
4753
  if (key.type.kind !== "string") {
6317
4754
  lowerer.unsupported("SC1090", target.argumentExpression, "indexing records with non-string or non-number keys");
6318
4755
  }
@@ -6341,8 +4778,7 @@ export function lowerElementWrite(lowerer, expr) {
6341
4778
  : lowerer.lowerExpr(target.argumentExpression);
6342
4779
  // Runtime NUMBER keys canonicalize through the JS-exact formatter
6343
4780
  // (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);
4781
+ key = lowerRecordPropertyKey(lowerer, key, target.argumentExpression);
6346
4782
  if (key.type.kind !== "string") {
6347
4783
  lowerer.unsupported("SC1090", target.argumentExpression, "indexing records with non-string or non-number keys");
6348
4784
  }
@@ -6369,18 +4805,14 @@ export function lowerElementWrite(lowerer, expr) {
6369
4805
  // probes the receiver's own lowered world: a dyn value takes the same
6370
4806
  // write, anything else falls through to the fences.
6371
4807
  if (receiverIr?.kind === "dyn" || receiverIr === null) {
6372
- const obj = receiverIr !== null ? lowerer.lowerExpr(target.expression) : probeLower(lowerer, target.expression);
4808
+ const obj = receiverIr !== null ? lowerer.lowerExpr(target.expression) : tryLowerExpression(lowerer, target.expression);
6373
4809
  if (obj !== null && obj.type.kind === "dyn") {
6374
4810
  const loc = locOf(expr);
6375
4811
  const litKey = recordKeyLiteralText(target.argumentExpression);
6376
4812
  let key = litKey !== null
6377
4813
  ? { kind: "strLit", value: litKey, type: STRING, loc: locOf(target.argumentExpression) }
6378
4814
  : 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
- }
4815
+ key = lowerRecordPropertyKey(lowerer, key, target.argumentExpression);
6384
4816
  if (key.type.kind !== "string") {
6385
4817
  lowerer.unsupported("SC1090", target.argumentExpression, "indexing checked-dynamic values with non-string or non-number keys");
6386
4818
  }
@@ -6394,7 +4826,7 @@ export function lowerElementWrite(lowerer, expr) {
6394
4826
  if (receiverIr?.kind !== "array") {
6395
4827
  lowerer.unsupported("SC1090", target, "assignment to non-array elements");
6396
4828
  }
6397
- const arr = lowerer.lowerExpr(target.expression);
4829
+ let arr = lowerer.lowerExpr(target.expression);
6398
4830
  // The write twin of the island element read: an array-typed .d.ts
6399
4831
  // surface whose VALUE is a handle takes the engine keyed write, never
6400
4832
  // a static arraySet over a jsval.
@@ -6404,17 +4836,29 @@ export function lowerElementWrite(lowerer, expr) {
6404
4836
  const loc = locOf(expr);
6405
4837
  return { kind: "exprStmt", expr: { kind: "jsOp", op: "setIdx", args: [arr, key, value], type: VOID, loc }, loc };
6406
4838
  }
4839
+ if (arr.type.kind === "union" &&
4840
+ lowerer.armTag(arr.type.unionId, UNDEFINED_T) >= 0 &&
4841
+ receiverIr.kind === "array") {
4842
+ const present = lowerer.stripUndefinedArm(arr.type);
4843
+ const helper = present.kind === "array"
4844
+ ? lowerer.narrowedArmHelper(arr.type.unionId, present, locOf(target.expression))
4845
+ : null;
4846
+ if (helper) {
4847
+ arr = { kind: "call", callee: helper, args: [arr], type: present, loc: locOf(target.expression) };
4848
+ }
4849
+ }
4850
+ if (arr.type.kind !== "array") {
4851
+ lowerer.unsupported("SC1090", target.expression, "assignment through a possibly missing nested array receiver");
4852
+ }
6407
4853
  const index = lowerer.lowerExpr(target.argumentExpression);
6408
4854
  if (index.type.kind !== "f64") {
6409
4855
  lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys");
6410
4856
  }
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) };
4857
+ // Optional array reads carry the source array's payload type plus an
4858
+ // undefined arm. arrayValueStore turns that arm into the runtime's
4859
+ // UNDEFINED state while keeping number[]/string[] payload storage scalar.
4860
+ const value = lowerer.lowerExpr(expr.right);
4861
+ return arrayValueStore(lowerer, arr, index, value, receiverIr.elem, locOf(expr));
6418
4862
  }
6419
4863
  export function ensureString(lowerer, e, node) {
6420
4864
  if (e.type.kind === "string")
@@ -6679,6 +5123,12 @@ export function lowerAsExpression(lowerer, expr) {
6679
5123
  if (target.kind === "bytes" && target.elem === "u8") {
6680
5124
  return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) };
6681
5125
  }
5126
+ // Class instances round-trip only through compiler-owned typed
5127
+ // references. Plain JSON objects fail the runtime class-brand check;
5128
+ // exact capsules unwrap the original object by identity.
5129
+ if (isDynTypedRefType(target)) {
5130
+ return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) };
5131
+ }
6682
5132
  // ADAPTABLE function targets (`u as (x: number) => number` — the
6683
5133
  // checked-dynamic function boundary): kind check, then exact unwrap
6684
5134
  // or the per-target adapter shim. Non-adaptable signatures keep the
@@ -6717,10 +5167,11 @@ export function lowerPrefixUnary(lowerer, expr) {
6717
5167
  const loc = locOf(expr);
6718
5168
  switch (expr.operator) {
6719
5169
  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 };
5170
+ const raw = lowerer.lowerExpr(expr.operand);
5171
+ if (raw.type.kind === "jsval") {
5172
+ return { kind: "jsOp", op: "neg", args: [raw], type: JSVAL, loc };
6723
5173
  }
5174
+ const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand);
6724
5175
  if (operand.type.kind !== "f64")
6725
5176
  lowerer.unsupported("SC1043", expr);
6726
5177
  if (operand.kind === "numLit")
@@ -6728,16 +5179,17 @@ export function lowerPrefixUnary(lowerer, expr) {
6728
5179
  return { kind: "unary", op: "-", operand, type: F64, loc };
6729
5180
  }
6730
5181
  case ts.SyntaxKind.PlusToken: {
6731
- const operand = lowerer.lowerExpr(expr.operand);
5182
+ const raw = lowerer.lowerExpr(expr.operand);
6732
5183
  // Unary + is ToNumber; on an already-number operand it's identity,
6733
5184
  // and a STRING operand runs the runtime's ECMA-exact StringToNumber
6734
5185
  // (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 };
5186
+ if (raw.type.kind === "jsval") {
5187
+ return { kind: "jsOp", op: "plus", args: [raw], type: JSVAL, loc };
6737
5188
  }
6738
- if (operand.type.kind === "string") {
6739
- return { kind: "libCall", fn: "num.fromString", args: [operand], type: F64, loc };
5189
+ if (raw.type.kind === "string") {
5190
+ return { kind: "libCall", fn: "num.fromString", args: [raw], type: F64, loc };
6740
5191
  }
5192
+ const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand);
6741
5193
  if (operand.type.kind !== "f64")
6742
5194
  lowerer.unsupported("SC1043", expr);
6743
5195
  return operand;
@@ -6749,7 +5201,7 @@ export function lowerPrefixUnary(lowerer, expr) {
6749
5201
  }
6750
5202
  case ts.SyntaxKind.TildeToken: {
6751
5203
  // `~x`: ToInt32, complement, back to f64 (JS-exact, incl. NaN → -1).
6752
- const operand = lowerer.lowerExpr(expr.operand);
5204
+ const operand = lowerOptionalNumber(lowerer, lowerer.lowerExpr(expr.operand), loc, expr.operand);
6753
5205
  if (operand.type.kind !== "f64")
6754
5206
  lowerer.unsupported("SC1043", expr);
6755
5207
  return { kind: "unary", op: "~", operand, type: F64, loc };
@@ -6815,9 +5267,9 @@ function lowerIncDec(lowerer, expr, prefix) {
6815
5267
  lowerer.unsupported("SC1043", expr);
6816
5268
  return { kind: "incDec", op, prefix, localId: target.id, type: F64, loc };
6817
5269
  }
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).
5270
+ /** Whether a lowered statement can live inside a seqExpr. These statements
5271
+ * do not leave the expression through a jump; the native emitters can place
5272
+ * their writes and local state branches before evaluating the final result.
6821
5273
  * Blocks check their bodies recursively. */
6822
5274
  function seqExprSafeStmt(s) {
6823
5275
  switch (s.kind) {
@@ -6828,18 +5280,59 @@ function seqExprSafeStmt(s) {
6828
5280
  case "recordSet":
6829
5281
  case "recordKeySet":
6830
5282
  case "arraySet":
5283
+ case "arraySetLength":
6831
5284
  case "bytesSet":
5285
+ case "arraySetUndefined":
5286
+ case "arrayDelete":
6832
5287
  return true;
5288
+ case "if":
5289
+ return s.then.every(seqExprSafeStmt) && (s.else_?.every(seqExprSafeStmt) ?? true);
6833
5290
  case "block":
6834
5291
  return s.body.every(seqExprSafeStmt);
6835
5292
  default:
6836
5293
  return false;
6837
5294
  }
6838
5295
  }
5296
+ /** Run one supported `any` binary operation in the island. Checked-dynamic
5297
+ * operands marshal in only after lowerBinary's identity-preserving native
5298
+ * cases have had their chance (notably dyn strict equality). */
5299
+ function lowerAnyBinaryInIsland(lowerer, expr, left, right, loc) {
5300
+ const op = expr.operatorToken.kind;
5301
+ const JS_BIN = {
5302
+ [ts.SyntaxKind.PlusToken]: "add",
5303
+ [ts.SyntaxKind.MinusToken]: "sub",
5304
+ [ts.SyntaxKind.AsteriskToken]: "mul",
5305
+ [ts.SyntaxKind.SlashToken]: "div",
5306
+ [ts.SyntaxKind.PercentToken]: "mod",
5307
+ [ts.SyntaxKind.AsteriskAsteriskToken]: "pow",
5308
+ [ts.SyntaxKind.LessThanToken]: "lt",
5309
+ [ts.SyntaxKind.LessThanEqualsToken]: "le",
5310
+ [ts.SyntaxKind.GreaterThanToken]: "gt",
5311
+ [ts.SyntaxKind.GreaterThanEqualsToken]: "ge",
5312
+ [ts.SyntaxKind.EqualsEqualsEqualsToken]: "eq",
5313
+ [ts.SyntaxKind.ExclamationEqualsEqualsToken]: "neq",
5314
+ };
5315
+ const jop = JS_BIN[op];
5316
+ if (jop === undefined) {
5317
+ lowerer.unsupported("SC1090", expr, `operator '${ts.tokenToString(op) ?? ts.SyntaxKind[op]}' on 'any' values`);
5318
+ }
5319
+ const type = jsOpResultKind(jop) === "bool" ? BOOL : JSVAL;
5320
+ return {
5321
+ kind: "jsOp", op: jop,
5322
+ args: [lowerer.jsvalIn(left, expr.left), lowerer.jsvalIn(right, expr.right)],
5323
+ type, loc,
5324
+ };
5325
+ }
6839
5326
  export function lowerBinary(lowerer, expr) {
6840
5327
  const loc = locOf(expr);
6841
5328
  const op = expr.operatorToken.kind;
5329
+ const cacheHas = lowerRequireCacheHas(lowerer, expr);
5330
+ if (cacheHas)
5331
+ return cacheHas;
6842
5332
  if (op === ts.SyntaxKind.EqualsToken || (op >= ts.SyntaxKind.FirstCompoundAssignment && op <= ts.SyntaxKind.LastCompoundAssignment)) {
5333
+ if (ts.isPropertyAccessExpression(expr.left) || ts.isElementAccessExpression(expr.left)) {
5334
+ fenceNodeModuleMutation(lowerer, expr.left, "assignment");
5335
+ }
6843
5336
  // `x = e` in EXPRESSION position (`while ((idx = s.indexOf("\n")) !== -1)`,
6844
5337
  // `f(x = v)`): evaluate e once, write the binding, yield the assigned
6845
5338
  // value — JS evaluation order. Variable targets only (locals and module
@@ -6950,7 +5443,7 @@ export function lowerBinary(lowerer, expr) {
6950
5443
  // RHS value is the expression's value — its OWN type, so the
6951
5444
  // consumer sees exactly what the checker typed.
6952
5445
  if (ts.isPropertyAccessExpression(expr.left) && !expr.left.questionDotToken) {
6953
- const recv = probeLower(lowerer, expr.left.expression);
5446
+ const recv = tryLowerExpression(lowerer, expr.left.expression);
6954
5447
  if (recv && recv.type.kind === "dyn") {
6955
5448
  const recvTmp = lowerer.declareHiddenLocal("%setRecv", DYN);
6956
5449
  const rhsVal = lowerer.lowerExpr(expr.right);
@@ -7075,8 +5568,74 @@ export function lowerBinary(lowerer, expr) {
7075
5568
  // Everything else (`u && flag`, whose value would need a wider
7076
5569
  // re-tagged union) stays fenced — in CONDITION position those
7077
5570
  // shapes lower through lowerCondition's bool descent instead.
7078
- const target = lowerer.mapTypeOf(lowerer.typeOf(expr));
5571
+ let target = lowerer.mapTypeOf(lowerer.typeOf(expr));
5572
+ // A broad-JSDoc npm-static body can leave the CHECKER result `any`
5573
+ // even after declaration-backed specialization has recovered both
5574
+ // operands (`helpOption && args.find(...)` in commander). When the
5575
+ // left consists only of always-truthy references and falsy units,
5576
+ // the static result is exactly the right operand plus those units.
5577
+ if (target === null &&
5578
+ lowerer.implicitParamTypes !== null &&
5579
+ npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null &&
5580
+ (lowerer.typeOf(expr).flags & ts.TypeFlags.Any) !== 0 &&
5581
+ left.type.kind === "union") {
5582
+ const leftArms = lowerer.unions.get(left.type.unionId)?.arms;
5583
+ const rightArms = right.type.kind === "union"
5584
+ ? lowerer.unions.get(right.type.unionId)?.arms
5585
+ : [right.type];
5586
+ const rightIsStatic = rightArms?.every((arm) => arm.kind !== "dyn" && arm.kind !== "caught" && arm.kind !== "jsval" && arm.kind !== "void") ?? false;
5587
+ if (leftArms && rightArms && rightIsStatic &&
5588
+ leftArms.some(isUnitType) &&
5589
+ leftArms.every((arm) => isUnitType(arm) || REF_TRUTHY_KINDS.has(arm.kind))) {
5590
+ const byKey = new Map();
5591
+ for (const arm of [...rightArms, ...leftArms.filter(isUnitType)]) {
5592
+ byKey.set(typeKey(arm), arm);
5593
+ }
5594
+ const arms = [...byKey.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
5595
+ if (arms.length > 1) {
5596
+ target = { kind: "union", unionId: lowerer.unions.intern(arms) };
5597
+ }
5598
+ }
5599
+ }
7079
5600
  if (target?.kind === "union") {
5601
+ // `&&` whose left does NOT fit the result union: the checker
5602
+ // built that result by DROPPING left arms which are always
5603
+ // truthy (`Option | undefined && string | undefined` answers
5604
+ // `string | undefined`, not `Option | string | undefined`).
5605
+ // Evaluate the left once, test it in its own union, and retag it
5606
+ // only on the falsy path. Any stranded reference arms are
5607
+ // unreachable there because every JS object is truthy.
5608
+ if (op === ts.SyntaxKind.AmpersandAmpersandToken && left.type.kind === "union" && !typeEquals(left.type, target)) {
5609
+ const def = lowerer.unions.get(left.type.unionId);
5610
+ const trappable = new Set();
5611
+ def?.arms.forEach((arm, tag) => {
5612
+ if (lowerer.armTag(target.unionId, arm) < 0 && REF_TRUTHY_KINDS.has(arm.kind)) {
5613
+ trappable.add(tag);
5614
+ }
5615
+ });
5616
+ const retag = trappable.size === 0
5617
+ ? null
5618
+ : lowerer.unionRetagHelper(left.type.unionId, target.unionId, loc, trappable);
5619
+ if (retag) {
5620
+ lowerer.requireTruthyUnion(left.type.unionId, expr);
5621
+ const stmts = [];
5622
+ let stable = left;
5623
+ if (!isSafeToRepeat(left)) {
5624
+ const local = lowerer.declareHiddenLocal("%and", left.type);
5625
+ stmts.push({ kind: "varDecl", localId: local.id, init: left, loc });
5626
+ stable = varRef(local.id, left.type, loc);
5627
+ }
5628
+ const result = {
5629
+ kind: "ternary",
5630
+ cond: lowerer.ensureBool(stable, expr.left),
5631
+ then: lowerer.coerceInto(expr.right, right, target),
5632
+ else_: { kind: "call", callee: retag, args: [stable], type: target, loc },
5633
+ type: target,
5634
+ loc,
5635
+ };
5636
+ return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: target, loc };
5637
+ }
5638
+ }
7080
5639
  // `||` whose left does NOT fit the result union: the checker built
7081
5640
  // that result by DROPPING the left's falsy arms (`process.env.X ||
7082
5641
  // null` is `string | null`, `|| 3000` is `string | number` — the
@@ -7084,9 +5643,7 @@ export function lowerBinary(lowerer, expr) {
7084
5643
  // the shared shape below does, retags an arm the test is about to
7085
5644
  // rule out and throws where Node yields the default. Single-eval
7086
5645
  // instead: test the left in its OWN union and retag only on the
7087
- // truthy side, where the dropped arms are unreachable. `&&` keeps
7088
- // the eager shape — there the falsy left IS the result, so the
7089
- // checker keeps those arms in the target and they must coerce.
5646
+ // truthy side, where the dropped arms are unreachable.
7090
5647
  if (op === ts.SyntaxKind.BarBarToken && left.type.kind === "union" && !typeEquals(left.type, target)) {
7091
5648
  const retag = lowerer.unionRetagHelper(left.type.unionId, target.unionId, loc);
7092
5649
  if (retag) {
@@ -7299,6 +5856,14 @@ export function lowerBinary(lowerer, expr) {
7299
5856
  // retry lifts the site. Genuine unknown keeps the SC1100 story.
7300
5857
  if ((left.type.kind === "dyn" && lowerer.anyOrigin(expr.left)) ||
7301
5858
  (right.type.kind === "dyn" && lowerer.anyOrigin(expr.right))) {
5859
+ // `JSON.parse` and other checked-dynamic producers deliberately
5860
+ // remain dyn when assigned to an `any` local: eagerly converting
5861
+ // the binding would deep-copy its data and sever aliases. At an
5862
+ // operator the dynamic build can cross only the operands and let
5863
+ // the engine apply JS's exact coercion semantics. Static builds
5864
+ // retain the SC2011 promise that --dynamic lifts this site.
5865
+ if (lowerer.dynamic)
5866
+ return lowerAnyBinaryInIsland(lowerer, expr, left, right, loc);
7302
5867
  lowerer.anyOpFence(`the '${ts.tokenToString(op) ?? ts.SyntaxKind[op]}' operator`, expr);
7303
5868
  }
7304
5869
  // tsc allows ===/!== on unknown (arithmetic/comparisons it rejects
@@ -7310,37 +5875,120 @@ export function lowerBinary(lowerer, expr) {
7310
5875
  // computes. Comparisons come back as static bools; arithmetic stays
7311
5876
  // an island value ('1 as any + "x"' is a string over there).
7312
5877
  if (left.type.kind === "jsval" || right.type.kind === "jsval") {
7313
- const JS_BIN = {
7314
- [ts.SyntaxKind.PlusToken]: "add",
7315
- [ts.SyntaxKind.MinusToken]: "sub",
7316
- [ts.SyntaxKind.AsteriskToken]: "mul",
7317
- [ts.SyntaxKind.SlashToken]: "div",
7318
- [ts.SyntaxKind.PercentToken]: "mod",
7319
- [ts.SyntaxKind.AsteriskAsteriskToken]: "pow",
7320
- [ts.SyntaxKind.LessThanToken]: "lt",
7321
- [ts.SyntaxKind.LessThanEqualsToken]: "le",
7322
- [ts.SyntaxKind.GreaterThanToken]: "gt",
7323
- [ts.SyntaxKind.GreaterThanEqualsToken]: "ge",
7324
- [ts.SyntaxKind.EqualsEqualsEqualsToken]: "eq",
7325
- [ts.SyntaxKind.ExclamationEqualsEqualsToken]: "neq",
5878
+ return lowerAnyBinaryInIsland(lowerer, expr, left, right, loc);
5879
+ }
5880
+ // An unchecked array read keeps its undefined arm in the IR even when
5881
+ // the checker typed the binding as `string`. An optional string is not
5882
+ // itself a string operand for `+`: `undefined + 1` and
5883
+ // `undefined + undefined` are NaN, while a present string concatenates.
5884
+ // Keep this predicate exact so a nullable string does not silently take
5885
+ // the string path either.
5886
+ const optionalStringType = (type) => {
5887
+ if (type.kind !== "union")
5888
+ return false;
5889
+ const arms = lowerer.unions.get(type.unionId)?.arms;
5890
+ return !!arms && arms.length === 2 &&
5891
+ arms.some((arm) => arm.kind === "string") &&
5892
+ arms.some((arm) => arm.kind === "undefinedT");
5893
+ };
5894
+ const arithmeticNumber = (operand, node) => lowerOptionalNumber(lowerer, operand, loc, node);
5895
+ const arithmeticLeft = strictEquality ? left : arithmeticNumber(left, expr.left);
5896
+ const arithmeticRight = strictEquality ? right : arithmeticNumber(right, expr.right);
5897
+ const plainBothNum = left.type.kind === "f64" && right.type.kind === "f64";
5898
+ const bothNum = arithmeticLeft.type.kind === "f64" && arithmeticRight.type.kind === "f64";
5899
+ const bothStr = left.type.kind === "string" && right.type.kind === "string";
5900
+ const lowerOptionalStringPlus = () => {
5901
+ const leftOptional = optionalStringType(left.type);
5902
+ const rightOptional = optionalStringType(right.type);
5903
+ if (!leftOptional && !rightOptional)
5904
+ return null;
5905
+ const allowed = (type) => type.kind === "f64" || type.kind === "string" || optionalStringType(type);
5906
+ if (!allowed(left.type) || !allowed(right.type))
5907
+ return null;
5908
+ // The tag tests and the chosen concatenation branch must see the same
5909
+ // operand values. This matters for `take()[0] + 1` and for two reads
5910
+ // with observable getters/calls.
5911
+ const prefix = [];
5912
+ const stable = (operand) => {
5913
+ if (isSafeToRepeat(operand) || isSafeToDiscard(operand))
5914
+ return operand;
5915
+ const tmp = lowerer.declareHiddenLocal("%plus", operand.type);
5916
+ prefix.push({ kind: "varDecl", localId: tmp.id, init: operand, loc });
5917
+ return varRef(tmp.id, operand.type, loc);
7326
5918
  };
7327
- const jop = JS_BIN[op];
7328
- if (jop === undefined) {
7329
- lowerer.unsupported("SC1090", expr, `operator '${ts.tokenToString(op) ?? ts.SyntaxKind[op]}' on 'any' values`);
5919
+ const stableLeft = stable(left);
5920
+ const stableRight = stable(right);
5921
+ const leftString = lowerer.ensureString(stableLeft, expr.left);
5922
+ const rightString = lowerer.ensureString(stableRight, expr.right);
5923
+ const concat = { kind: "strConcat", left: leftString, right: rightString, type: STRING, loc };
5924
+ const nan = { kind: "bin", op: "/", left: numLit(0, loc), right: numLit(0, loc), type: F64, loc };
5925
+ const resultT = { kind: "union", unionId: lowerer.unions.intern([F64, STRING]) };
5926
+ lowerer.runtimeOptionalArithmeticTypes.set(expr, resultT);
5927
+ const numberTag = lowerer.armTag(resultT.unionId, F64);
5928
+ const stringTag = lowerer.armTag(resultT.unionId, STRING);
5929
+ if (numberTag < 0 || stringTag < 0)
5930
+ throw new InternalCompilerError("lowerer bug: optional string plus result union");
5931
+ const number = () => ({
5932
+ kind: "unionWrap", unionId: resultT.unionId, tag: numberTag, value: nan, type: resultT, loc,
5933
+ });
5934
+ const string = () => ({
5935
+ kind: "unionWrap", unionId: resultT.unionId, tag: stringTag, value: concat, type: resultT, loc,
5936
+ });
5937
+ const undefinedTest = (operand) => ({
5938
+ kind: "unionIsTag",
5939
+ unionId: operand.type.kind === "union" ? operand.type.unionId : "",
5940
+ tag: operand.type.kind === "union" ? lowerer.armTag(operand.type.unionId, UNDEFINED_T) : -1,
5941
+ negated: false,
5942
+ value: operand,
5943
+ type: BOOL,
5944
+ loc,
5945
+ });
5946
+ let result;
5947
+ if (leftOptional && rightOptional) {
5948
+ const rightMissing = undefinedTest(stableRight);
5949
+ const bothMissing = {
5950
+ kind: "ternary",
5951
+ cond: rightMissing,
5952
+ then: number(),
5953
+ else_: string(),
5954
+ type: resultT,
5955
+ loc,
5956
+ };
5957
+ result = {
5958
+ kind: "ternary",
5959
+ cond: undefinedTest(stableLeft),
5960
+ then: bothMissing,
5961
+ else_: string(),
5962
+ type: resultT,
5963
+ loc,
5964
+ };
7330
5965
  }
7331
- const type = jsOpResultKind(jop) === "bool" ? BOOL : JSVAL;
7332
- return {
7333
- kind: "jsOp", op: jop,
7334
- args: [lowerer.jsvalIn(left, expr.left), lowerer.jsvalIn(right, expr.right)],
7335
- type, loc,
7336
- };
7337
- }
7338
- const bothNum = left.type.kind === "f64" && right.type.kind === "f64";
7339
- const bothStr = left.type.kind === "string" && right.type.kind === "string";
5966
+ else if (leftOptional) {
5967
+ result = {
5968
+ kind: "ternary",
5969
+ cond: undefinedTest(stableLeft),
5970
+ then: number(),
5971
+ else_: string(),
5972
+ type: resultT,
5973
+ loc,
5974
+ };
5975
+ }
5976
+ else {
5977
+ result = {
5978
+ kind: "ternary",
5979
+ cond: undefinedTest(stableRight),
5980
+ then: number(),
5981
+ else_: string(),
5982
+ type: resultT,
5983
+ loc,
5984
+ };
5985
+ }
5986
+ return prefix.length === 0 ? result : { kind: "seqExpr", stmts: prefix, result, type: resultT, loc };
5987
+ };
7340
5988
  switch (op) {
7341
5989
  case ts.SyntaxKind.PlusToken:
7342
5990
  if (bothNum)
7343
- return { kind: "bin", op: "+", left, right, type: F64, loc };
5991
+ return { kind: "bin", op: "+", left: arithmeticLeft, right: arithmeticRight, type: F64, loc };
7344
5992
  if (left.type.kind === "string" || right.type.kind === "string") {
7345
5993
  return {
7346
5994
  kind: "strConcat",
@@ -7349,6 +5997,9 @@ export function lowerBinary(lowerer, expr) {
7349
5997
  type: STRING, loc,
7350
5998
  };
7351
5999
  }
6000
+ const optionalStringPlus = lowerOptionalStringPlus();
6001
+ if (optionalStringPlus)
6002
+ return optionalStringPlus;
7352
6003
  lowerer.unsupported("SC1043", expr);
7353
6004
  break;
7354
6005
  case ts.SyntaxKind.MinusToken:
@@ -7362,7 +6013,7 @@ export function lowerBinary(lowerer, expr) {
7362
6013
  : op === ts.SyntaxKind.AsteriskToken ? "*"
7363
6014
  : op === ts.SyntaxKind.SlashToken ? "/"
7364
6015
  : op === ts.SyntaxKind.PercentToken ? "%" : "**";
7365
- return { kind: "bin", op: binOp, left, right, type: F64, loc };
6016
+ return { kind: "bin", op: binOp, left: arithmeticLeft, right: arithmeticRight, type: F64, loc };
7366
6017
  }
7367
6018
  // The bitwise six, JS-exact: operands through ToInt32/ToUint32
7368
6019
  // (NaN/±Infinity → 0, truncate, wrap mod 2^32), the operation in
@@ -7381,12 +6032,12 @@ export function lowerBinary(lowerer, expr) {
7381
6032
  : op === ts.SyntaxKind.CaretToken ? "^"
7382
6033
  : op === ts.SyntaxKind.LessThanLessThanToken ? "<<"
7383
6034
  : op === ts.SyntaxKind.GreaterThanGreaterThanToken ? ">>" : ">>>";
7384
- return { kind: "bin", op: bitOp, left, right, type: F64, loc };
6035
+ return { kind: "bin", op: bitOp, left: arithmeticLeft, right: arithmeticRight, type: F64, loc };
7385
6036
  }
7386
6037
  case ts.SyntaxKind.EqualsEqualsEqualsToken:
7387
6038
  case ts.SyntaxKind.ExclamationEqualsEqualsToken: {
7388
6039
  const negated = op === ts.SyntaxKind.ExclamationEqualsEqualsToken;
7389
- if (bothNum)
6040
+ if (plainBothNum)
7390
6041
  return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc };
7391
6042
  if (bothStr)
7392
6043
  return { kind: "strEq", negated, left, right, type: BOOL, loc };
@@ -7445,6 +6096,38 @@ export function lowerBinary(lowerer, expr) {
7445
6096
  loc,
7446
6097
  };
7447
6098
  }
6099
+ if (left.type.kind === "union" && right.type.kind === "union") {
6100
+ const leftDef = lowerer.unions.get(left.type.unionId);
6101
+ const rightDef = lowerer.unions.get(right.type.unionId);
6102
+ if (leftDef && rightDef) {
6103
+ const leftValues = leftDef.arms.filter((arm) => !isUnitType(arm));
6104
+ const rightValues = rightDef.arms.filter((arm) => !isUnitType(arm));
6105
+ const sameValueArms = leftValues.length === rightValues.length &&
6106
+ leftValues.every((arm) => rightValues.some((candidate) => typeEquals(candidate, arm)));
6107
+ if (sameValueArms) {
6108
+ const arms = [...leftDef.arms];
6109
+ for (const arm of rightDef.arms) {
6110
+ if (!arms.some((candidate) => typeEquals(candidate, arm)))
6111
+ arms.push(arm);
6112
+ }
6113
+ arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
6114
+ const common = {
6115
+ kind: "union",
6116
+ unionId: lowerer.unions.intern(arms),
6117
+ };
6118
+ return {
6119
+ kind: "unionEq",
6120
+ unionId: common.unionId,
6121
+ negated,
6122
+ sameValue: false,
6123
+ left: lowerer.coerceInto(expr.left, left, common),
6124
+ right: lowerer.coerceInto(expr.right, right, common),
6125
+ type: BOOL,
6126
+ loc,
6127
+ };
6128
+ }
6129
+ }
6130
+ }
7448
6131
  lowerer.unsupported("SC1090", expr, `comparisons of union-typed values (${NARROW_FIRST})`);
7449
6132
  }
7450
6133
  // Arrays, maps, functions, class instances and records compare by
@@ -7509,7 +6192,7 @@ export function lowerBinary(lowerer, expr) {
7509
6192
  : op === ts.SyntaxKind.LessThanEqualsToken ? "<="
7510
6193
  : op === ts.SyntaxKind.GreaterThanToken ? ">" : ">=";
7511
6194
  if (bothNum)
7512
- return { kind: "bin", op: cmpOp, left, right, type: BOOL, loc };
6195
+ return { kind: "bin", op: cmpOp, left: arithmeticLeft, right: arithmeticRight, type: BOOL, loc };
7513
6196
  if (bothStr)
7514
6197
  return { kind: "strCmp", op: cmpOp, left, right, type: BOOL, loc };
7515
6198
  lowerer.unsupported("SC1043", expr);
@@ -7885,7 +6568,7 @@ function lowerUnionTypeofTest(lowerer, expr, loc) {
7885
6568
  continue;
7886
6569
  const tags = answers.flatMap((s, i) => (s === b.text ? [i] : []));
7887
6570
  if (tags.length === 0 || tags.length === def.arms.length) {
7888
- if (!pureReemittable(value)) {
6571
+ if (!isSafeToRepeat(value)) {
7889
6572
  lowerer.unsupported("SC1090", expr, "statically-decided 'typeof' tests over effectful operands (bind the value to a const first)");
7890
6573
  }
7891
6574
  return { kind: "boolLit", value: (tags.length !== 0) !== negated, type: BOOL, loc };
@@ -7895,7 +6578,7 @@ function lowerUnionTypeofTest(lowerer, expr, loc) {
7895
6578
  });
7896
6579
  if (tags.length === 1)
7897
6580
  return isTag(tags[0]);
7898
- if (!pureReemittable(value)) {
6581
+ if (!isSafeToRepeat(value)) {
7899
6582
  lowerer.unsupported("SC1090", expr, "'typeof' tests matching several union arms over operands that aren't plain reads (bind the value to a const first)");
7900
6583
  }
7901
6584
  // Tag-in-set as a short-circuit chain (De Morgan for `!==`).
@@ -7950,7 +6633,7 @@ function lowerDynTypeofTest(lowerer, expr, loc) {
7950
6633
  const recv = unparen(pn.expression);
7951
6634
  if (ts.isAsExpression(recv) || ts.isTypeAssertion(recv)) {
7952
6635
  const inner = unparen(recv.expression);
7953
- const obj = probeLower(lowerer, inner);
6636
+ const obj = tryLowerExpression(lowerer, inner);
7954
6637
  if (obj?.type.kind === "dyn") {
7955
6638
  keyed = {
7956
6639
  kind: "dynKeyGet",
@@ -7973,7 +6656,7 @@ function lowerDynTypeofTest(lowerer, expr, loc) {
7973
6656
  // "string") ... }`, the validateConfig idiom). Probe the lowering
7974
6657
  // and claim exactly the dyn results; anything else keeps its
7975
6658
  // sibling lowerings and fences untouched.
7976
- const probed = probeLower(lowerer, a.expression);
6659
+ const probed = tryLowerExpression(lowerer, a.expression);
7977
6660
  if (probed?.type.kind !== "dyn")
7978
6661
  continue;
7979
6662
  value = probed;
@@ -8137,33 +6820,6 @@ function lowerCjsExportTableThisMember(lowerer, expr) {
8137
6820
  return null;
8138
6821
  return cjsAccessorCall(lowerer, sibling, locOf(expr));
8139
6822
  }
8140
- /** Lowers an expression, converting a fence (PoisonError) into a null
8141
- * DECLINE — the probing dispatch for shapes where only the lowered
8142
- * value's type can say whether a lowering applies. unsupported() pushes
8143
- * its diagnostic BEFORE throwing, so the probe runs behind a diagnostic
8144
- * sink: a declined probe's diagnostics vanish (the sibling lowerings
8145
- * own the site), a successful probe's flush through afterwards. */
8146
- export function probeLower(lowerer, node) {
8147
- const saved = lowerer.diagSink;
8148
- const captured = [];
8149
- lowerer.diagSink = captured;
8150
- let result;
8151
- try {
8152
- result = lowerer.lowerExpr(node);
8153
- }
8154
- catch (e) {
8155
- if (e instanceof PoisonError) {
8156
- lowerer.diagSink = saved;
8157
- return null;
8158
- }
8159
- lowerer.diagSink = saved;
8160
- throw e;
8161
- }
8162
- lowerer.diagSink = saved;
8163
- for (const d of captured)
8164
- lowerer.pushDiag(d);
8165
- return result;
8166
- }
8167
6823
  /** A read of a catch binding — the caught analog of maybeNarrow. Where
8168
6824
  * tsc's control-flow narrowing has proven a supported test (`instanceof`
8169
6825
  * over a hierarchy class, `typeof` over a primitive), the read bridges
@@ -8643,7 +7299,7 @@ function lowerInExpression(lowerer, expr, loc) {
8643
7299
  // stringified like every property key (o[k] is o[String(k)] in JS;
8644
7300
  // `in` shares the coercion).
8645
7301
  {
8646
- const probed = probeLower(lowerer, expr.right);
7302
+ const probed = tryLowerExpression(lowerer, expr.right);
8647
7303
  if (probed?.type.kind === "dyn") {
8648
7304
  let k = lowerer.lowerExpr(expr.left); // JS order: the key evaluates first
8649
7305
  if (k.type.kind === "f64" || k.type.kind === "string" || k.type.kind === "dyn") {
@@ -9473,6 +8129,34 @@ export function lowerUnionProperty(lowerer, expr) {
9473
8129
  if (!def)
9474
8130
  throw new InternalCompilerError(`lowerer bug: unknown union ${value.type.unionId}`);
9475
8131
  const field = expr.name.text;
8132
+ if (field === "length") {
8133
+ const presentArms = def.arms.filter((arm) => !isUnitType(arm));
8134
+ const present = presentArms.length === 1 ? presentArms[0] : null;
8135
+ if (present?.kind === "string" || present?.kind === "array") {
8136
+ const receiver = lowerer.runtimeOptionalPropertyReceiver(expr.expression, value, present, field);
8137
+ if (receiver !== null) {
8138
+ return present.kind === "string"
8139
+ ? { kind: "strIntrinsic", method: "length", receiver, args: [], type: F64, loc: locOf(expr) }
8140
+ : { kind: "arrIntrinsic", method: "length", receiver, args: [], type: F64, loc: locOf(expr) };
8141
+ }
8142
+ }
8143
+ }
8144
+ const nonUnitArms = def.arms.filter((arm) => !isUnitType(arm));
8145
+ if (nonUnitArms.length === 1 && nonUnitArms[0]?.kind === "object") {
8146
+ const objectType = nonUnitArms[0];
8147
+ const fieldType = lowerer.classes.get(objectType.className)?.fields.get(field);
8148
+ if (fieldType !== undefined) {
8149
+ let receiver = lowerer.runtimeOptionalPropertyReceiver(expr.expression, value, objectType, field);
8150
+ if (receiver === null) {
8151
+ const helper = lowerer.narrowedArmHelper(value.type.unionId, objectType, locOf(expr.expression));
8152
+ if (helper !== null)
8153
+ receiver = { kind: "call", callee: helper, args: [value], type: objectType, loc: locOf(expr.expression) };
8154
+ }
8155
+ if (receiver !== null) {
8156
+ return { kind: "fieldGet", obj: receiver, className: objectType.className, field, type: fieldType, loc: locOf(expr) };
8157
+ }
8158
+ }
8159
+ }
9476
8160
  let common = null;
9477
8161
  for (const arm of def.arms) {
9478
8162
  let ft;
@@ -9638,8 +8322,21 @@ export function fieldTarget(lowerer, access) {
9638
8322
  return null;
9639
8323
  }
9640
8324
  const fieldType = info.fields.get(access.name.text);
8325
+ const lowerObjectReceiver = () => {
8326
+ let obj = lowerer.lowerExpr(access.expression);
8327
+ if (obj.type.kind === "union" && lowerer.armTag(obj.type.unionId, UNDEFINED_T) >= 0) {
8328
+ const present = lowerer.stripUndefinedArm(obj.type);
8329
+ const helper = present.kind === "object"
8330
+ ? lowerer.narrowedArmHelper(obj.type.unionId, present, locOf(access.expression))
8331
+ : null;
8332
+ obj = helper
8333
+ ? { kind: "call", callee: helper, args: [obj], type: present, loc: locOf(access.expression) }
8334
+ : lowerer.maybeNarrow(obj, access.expression);
8335
+ }
8336
+ return obj;
8337
+ };
9641
8338
  if (fieldType) {
9642
- const obj = lowerer.lowerExpr(access.expression);
8339
+ const obj = lowerObjectReceiver();
9643
8340
  return { container: "class", obj, className: receiverIr.className, field: access.name.text, fieldType };
9644
8341
  }
9645
8342
  // Accessor property: either half declared anywhere on the chain
@@ -9648,7 +8345,7 @@ export function fieldTarget(lowerer, access) {
9648
8345
  const getF = lowerer.findMethodOn(info, `get:${access.name.text}`);
9649
8346
  const setF = lowerer.findMethodOn(info, `set:${access.name.text}`);
9650
8347
  if (getF || setF) {
9651
- const obj = lowerer.lowerExpr(access.expression);
8348
+ const obj = lowerObjectReceiver();
9652
8349
  return {
9653
8350
  container: "accessor",
9654
8351
  obj,
@@ -9663,13 +8360,35 @@ export function fieldTarget(lowerer, access) {
9663
8360
  const shape = lowerer.shapes.get(receiverIr.shapeId);
9664
8361
  const fieldType = shape?.fields.find((f) => f.name === access.name.text)?.type;
9665
8362
  if (fieldType) {
9666
- const obj = lowerer.lowerExpr(access.expression);
8363
+ let obj = lowerer.lowerExpr(access.expression);
9667
8364
  // A checker-record receiver whose VALUE stayed dyn (the erased
9668
8365
  // all-unknown-fields cast — `(err as { code?: unknown }).code`):
9669
8366
  // decline, and the dyn keyed-read fallback answers.
8367
+ if (obj.type.kind === "union" && lowerer.armTag(obj.type.unionId, UNDEFINED_T) >= 0) {
8368
+ // A nested property read such as `rows[i].value` has the same
8369
+ // unchecked outer array-read issue as tuple indexing. Validate
8370
+ // the record arm before fieldGet/recordGet so a missing outer
8371
+ // value raises a catchable TypeError rather than reaching the
8372
+ // backend with a union receiver.
8373
+ const present = lowerer.stripUndefinedArm(obj.type);
8374
+ const helper = present.kind === "record"
8375
+ ? lowerer.narrowedArmHelper(obj.type.unionId, present, locOf(access.expression))
8376
+ : null;
8377
+ obj = helper
8378
+ ? { kind: "call", callee: helper, args: [obj], type: present, loc: locOf(access.expression) }
8379
+ : lowerer.maybeNarrow(obj, access.expression);
8380
+ }
9670
8381
  if (obj.type.kind !== "record")
9671
8382
  return null;
9672
- return { container: "record", obj, shapeId: receiverIr.shapeId, field: access.name.text, fieldType };
8383
+ const actualShape = lowerer.shapes.get(obj.type.shapeId);
8384
+ const actualField = actualShape?.fields.find((f) => f.name === access.name.text);
8385
+ return {
8386
+ container: "record",
8387
+ obj,
8388
+ shapeId: obj.type.shapeId,
8389
+ field: access.name.text,
8390
+ fieldType: actualField?.type ?? fieldType,
8391
+ };
9673
8392
  }
9674
8393
  // A RECORD accessor property: either slot present makes the name an
9675
8394
  // accessor target (get/set share the property namespace — tsc
@@ -10102,7 +8821,7 @@ export function lowerFieldCompound(lowerer, access, op, rhsNode, loc) {
10102
8821
  // receiver is an identifier (checked above), so evaluating it for
10103
8822
  // read and write matches JS's once-evaluation observably.
10104
8823
  if (ts.isPropertyAccessExpression(access)) {
10105
- const probed = probeLower(lowerer, access.expression);
8824
+ const probed = tryLowerExpression(lowerer, access.expression);
10106
8825
  if (probed?.type.kind === "dyn") {
10107
8826
  const key = { kind: "strLit", value: access.name.text, type: STRING, loc: locOf(access.name) };
10108
8827
  const read = { kind: "dynKeyGet", key, value: probed, type: DYN, loc };