@scriptc/compiler 0.1.1 → 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 (209) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +45 -17
  2. package/ambient/scriptc.d.ts +2 -1
  3. package/dist/backend/build-cache.js.map +1 -1
  4. package/dist/backend/c/async.js +24 -2
  5. package/dist/backend/c/async.js.map +1 -1
  6. package/dist/backend/c/c-emitter.d.ts +22 -10
  7. package/dist/backend/c/c-emitter.js +60 -15
  8. package/dist/backend/c/c-emitter.js.map +1 -1
  9. package/dist/backend/c/exprs.js +220 -10
  10. package/dist/backend/c/exprs.js.map +1 -1
  11. package/dist/backend/c/island.js.map +1 -1
  12. package/dist/backend/c/may-throw.js.map +1 -1
  13. package/dist/backend/c/stmts.d.ts +3 -5
  14. package/dist/backend/c/stmts.js +58 -17
  15. package/dist/backend/c/stmts.js.map +1 -1
  16. package/dist/backend/c/types.js.map +1 -1
  17. package/dist/backend/c/walkers.js +31 -5
  18. package/dist/backend/c/walkers.js.map +1 -1
  19. package/dist/backend/external-c.js.map +1 -1
  20. package/dist/backend/linker.js.map +1 -1
  21. package/dist/backend/llvm/blocks.js.map +1 -1
  22. package/dist/backend/llvm/dyn.js +56 -3
  23. package/dist/backend/llvm/dyn.js.map +1 -1
  24. package/dist/backend/llvm/emitter.js +98 -19
  25. package/dist/backend/llvm/emitter.js.map +1 -1
  26. package/dist/backend/llvm/expr-async.js +85 -3
  27. package/dist/backend/llvm/expr-async.js.map +1 -1
  28. package/dist/backend/llvm/expr-bytes.js.map +1 -1
  29. package/dist/backend/llvm/expr-calls.js.map +1 -1
  30. package/dist/backend/llvm/expr-containers.js.map +1 -1
  31. package/dist/backend/llvm/expr-context.d.ts +1 -0
  32. package/dist/backend/llvm/expr-control.js.map +1 -1
  33. package/dist/backend/llvm/expr-dispatch.js.map +1 -1
  34. package/dist/backend/llvm/expr-dynamic.js +6 -3
  35. package/dist/backend/llvm/expr-dynamic.js.map +1 -1
  36. package/dist/backend/llvm/expr-island.js.map +1 -1
  37. package/dist/backend/llvm/expr-primitives.js.map +1 -1
  38. package/dist/backend/llvm/expr-records.js.map +1 -1
  39. package/dist/backend/llvm/expr-stream-bridges.js +220 -9
  40. package/dist/backend/llvm/expr-stream-bridges.js.map +1 -1
  41. package/dist/backend/llvm/lib-dispatch.js +2 -0
  42. package/dist/backend/llvm/lib-dispatch.js.map +1 -1
  43. package/dist/backend/llvm/lib-shared.js +18 -0
  44. package/dist/backend/llvm/lib-shared.js.map +1 -1
  45. package/dist/backend/llvm/lib-stream.js +6 -3
  46. package/dist/backend/llvm/lib-stream.js.map +1 -1
  47. package/dist/backend/llvm/split.js.map +1 -1
  48. package/dist/backend/llvm/unsupported.js.map +1 -1
  49. package/dist/backend/llvm/walkers.js.map +1 -1
  50. package/dist/backend/native-codegen.js.map +1 -1
  51. package/dist/backend/native-codegen.test.js +2 -0
  52. package/dist/backend/native-codegen.test.js.map +1 -1
  53. package/dist/backend/native-toolchain.js.map +1 -1
  54. package/dist/backend/native-toolchain.test.js.map +1 -1
  55. package/dist/backend/runtime-pack.js.map +1 -1
  56. package/dist/backend/targets.js.map +1 -1
  57. package/dist/backend/vendor-archives.js.map +1 -1
  58. package/dist/compat/fetch-profile.js +4 -2
  59. package/dist/compat/fetch-profile.js.map +1 -1
  60. package/dist/coverage/report.js.map +1 -1
  61. package/dist/coverage/surface-manifest.js +12 -0
  62. package/dist/coverage/surface-manifest.js.map +1 -1
  63. package/dist/diagnostics/diagnostic.d.ts +6 -5
  64. package/dist/diagnostics/diagnostic.js +7 -6
  65. package/dist/diagnostics/diagnostic.js.map +1 -1
  66. package/dist/diagnostics/render.js.map +1 -1
  67. package/dist/ffi/ffi-manifest.js.map +1 -1
  68. package/dist/frontend/cjs-lexer.js.map +1 -1
  69. package/dist/frontend/dts-paths.js.map +1 -1
  70. package/dist/frontend/input-tracker.d.ts +10 -0
  71. package/dist/frontend/input-tracker.js +26 -3
  72. package/dist/frontend/input-tracker.js.map +1 -1
  73. package/dist/frontend/input-tracker.test.js +8 -1
  74. package/dist/frontend/input-tracker.test.js.map +1 -1
  75. package/dist/frontend/lib-contract.js.map +1 -1
  76. package/dist/frontend/lowering/array-values.d.ts +4 -0
  77. package/dist/frontend/lowering/array-values.js +18 -2
  78. package/dist/frontend/lowering/array-values.js.map +1 -1
  79. package/dist/frontend/lowering/callback-arg.js.map +1 -1
  80. package/dist/frontend/lowering/containers/bytes.d.ts +35 -0
  81. package/dist/frontend/lowering/containers/bytes.js +1021 -0
  82. package/dist/frontend/lowering/containers/bytes.js.map +1 -0
  83. package/dist/frontend/lowering/containers/string-and-regexp.d.ts +17 -0
  84. package/dist/frontend/lowering/containers/string-and-regexp.js +345 -0
  85. package/dist/frontend/lowering/containers/string-and-regexp.js.map +1 -0
  86. package/dist/frontend/lowering/expressions/evaluation-safety.d.ts +13 -0
  87. package/dist/frontend/lowering/expressions/evaluation-safety.js +76 -0
  88. package/dist/frontend/lowering/expressions/evaluation-safety.js.map +1 -0
  89. package/dist/frontend/lowering/expressions/object-literals.d.ts +44 -0
  90. package/dist/frontend/lowering/expressions/object-literals.js +1815 -0
  91. package/dist/frontend/lowering/expressions/object-literals.js.map +1 -0
  92. package/dist/frontend/lowering/expressions/optional-chains.d.ts +31 -0
  93. package/dist/frontend/lowering/expressions/optional-chains.js +382 -0
  94. package/dist/frontend/lowering/expressions/optional-chains.js.map +1 -0
  95. package/dist/frontend/lowering/expressions/try-lower-expression.d.ts +7 -0
  96. package/dist/frontend/lowering/expressions/try-lower-expression.js +26 -0
  97. package/dist/frontend/lowering/expressions/try-lower-expression.js.map +1 -0
  98. package/dist/frontend/lowering/lib-boundary.js +7 -2
  99. package/dist/frontend/lowering/lib-boundary.js.map +1 -1
  100. package/dist/frontend/lowering/lower-assert.js.map +1 -1
  101. package/dist/frontend/lowering/lower-async-iteration.d.ts +7 -0
  102. package/dist/frontend/lowering/lower-async-iteration.js +657 -0
  103. package/dist/frontend/lowering/lower-async-iteration.js.map +1 -0
  104. package/dist/frontend/lowering/lower-builtins.d.ts +18 -0
  105. package/dist/frontend/lowering/lower-builtins.js +219 -48
  106. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  107. package/dist/frontend/lowering/lower-calls.d.ts +13 -4
  108. package/dist/frontend/lowering/lower-calls.js +407 -64
  109. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  110. package/dist/frontend/lowering/lower-classes.d.ts +7 -13
  111. package/dist/frontend/lowering/lower-classes.js +128 -35
  112. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  113. package/dist/frontend/lowering/lower-comptime.js.map +1 -1
  114. package/dist/frontend/lowering/lower-containers.d.ts +1 -47
  115. package/dist/frontend/lowering/lower-containers.js +135 -1467
  116. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  117. package/dist/frontend/lowering/lower-event-emitter.js +51 -5
  118. package/dist/frontend/lowering/lower-event-emitter.js.map +1 -1
  119. package/dist/frontend/lowering/lower-exprs.d.ts +0 -78
  120. package/dist/frontend/lowering/lower-exprs.js +402 -2351
  121. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  122. package/dist/frontend/lowering/lower-generators.d.ts +18 -3
  123. package/dist/frontend/lowering/lower-generators.js +145 -9
  124. package/dist/frontend/lowering/lower-generators.js.map +1 -1
  125. package/dist/frontend/lowering/lower-inspect.js +3 -3
  126. package/dist/frontend/lowering/lower-inspect.js.map +1 -1
  127. package/dist/frontend/lowering/lower-island.d.ts +6 -0
  128. package/dist/frontend/lowering/lower-island.js +37 -5
  129. package/dist/frontend/lowering/lower-island.js.map +1 -1
  130. package/dist/frontend/lowering/lower-modules.js +52 -15
  131. package/dist/frontend/lowering/lower-modules.js.map +1 -1
  132. package/dist/frontend/lowering/lower-node-module.d.ts +18 -0
  133. package/dist/frontend/lowering/lower-node-module.js +302 -0
  134. package/dist/frontend/lowering/lower-node-module.js.map +1 -0
  135. package/dist/frontend/lowering/lower-server.js +1 -1
  136. package/dist/frontend/lowering/lower-server.js.map +1 -1
  137. package/dist/frontend/lowering/lower-stmts.d.ts +5 -24
  138. package/dist/frontend/lowering/lower-stmts.js +519 -253
  139. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  140. package/dist/frontend/lowering/lower-stream.js +4 -4
  141. package/dist/frontend/lowering/lower-stream.js.map +1 -1
  142. package/dist/frontend/lowering/lowerer.d.ts +37 -34
  143. package/dist/frontend/lowering/lowerer.js +143 -57
  144. package/dist/frontend/lowering/lowerer.js.map +1 -1
  145. package/dist/frontend/lowering/optional-arguments.d.ts +9 -0
  146. package/dist/frontend/lowering/optional-arguments.js +57 -0
  147. package/dist/frontend/lowering/optional-arguments.js.map +1 -0
  148. package/dist/frontend/npm-static-declarations.d.ts +35 -0
  149. package/dist/frontend/npm-static-declarations.js +506 -0
  150. package/dist/frontend/npm-static-declarations.js.map +1 -0
  151. package/dist/frontend/npm-static-declarations.test.d.ts +1 -0
  152. package/dist/frontend/npm-static-declarations.test.js +122 -0
  153. package/dist/frontend/npm-static-declarations.test.js.map +1 -0
  154. package/dist/frontend/npm-static.d.ts +2 -0
  155. package/dist/frontend/npm-static.js +26 -10
  156. package/dist/frontend/npm-static.js.map +1 -1
  157. package/dist/frontend/npm.d.ts +60 -0
  158. package/dist/frontend/npm.js +38 -4
  159. package/dist/frontend/npm.js.map +1 -1
  160. package/dist/frontend/npm.test.d.ts +1 -0
  161. package/dist/frontend/npm.test.js +50 -0
  162. package/dist/frontend/npm.test.js.map +1 -0
  163. package/dist/frontend/program.d.ts +3 -2
  164. package/dist/frontend/program.js +495 -68
  165. package/dist/frontend/program.js.map +1 -1
  166. package/dist/frontend/resolve.d.ts +10 -2
  167. package/dist/frontend/resolve.js +90 -14
  168. package/dist/frontend/resolve.js.map +1 -1
  169. package/dist/frontend/resolve.test.js +4 -1
  170. package/dist/frontend/resolve.test.js.map +1 -1
  171. package/dist/frontend/runtime-resolve.d.ts +22 -0
  172. package/dist/frontend/runtime-resolve.js +101 -0
  173. package/dist/frontend/runtime-resolve.js.map +1 -0
  174. package/dist/frontend/runtime-resolve.test.d.ts +1 -0
  175. package/dist/frontend/runtime-resolve.test.js +18 -0
  176. package/dist/frontend/runtime-resolve.test.js.map +1 -0
  177. package/dist/frontend/ts7/adapter.js +12 -13
  178. package/dist/frontend/ts7/adapter.js.map +1 -1
  179. package/dist/frontend/ts7/ast.js.map +1 -1
  180. package/dist/frontend/ts7/census-check.js +3 -3
  181. package/dist/frontend/ts7/census-check.js.map +1 -1
  182. package/dist/frontend/ts7/checker.d.ts +3 -0
  183. package/dist/frontend/ts7/checker.js +12 -3
  184. package/dist/frontend/ts7/checker.js.map +1 -1
  185. package/dist/frontend/ts7/enums.js.map +1 -1
  186. package/dist/frontend/ts7/program-adapter.js +42 -3
  187. package/dist/frontend/ts7/program-adapter.js.map +1 -1
  188. package/dist/frontend/type-mapper.js +120 -7
  189. package/dist/frontend/type-mapper.js.map +1 -1
  190. package/dist/index.js +5 -5
  191. package/dist/index.js.map +1 -1
  192. package/dist/ir/analysis.js.map +1 -1
  193. package/dist/ir/ir.d.ts +60 -30
  194. package/dist/ir/ir.js +57 -6
  195. package/dist/ir/ir.js.map +1 -1
  196. package/dist/ir/validate.js +44 -59
  197. package/dist/ir/validate.js.map +1 -1
  198. package/dist/library/cache-primitives.js.map +1 -1
  199. package/dist/library/cache-reset.test.js.map +1 -1
  200. package/dist/library/compiler-self-identity.js.map +1 -1
  201. package/dist/library/int-infer.js.map +1 -1
  202. package/dist/library/int-infer.test.js.map +1 -1
  203. package/dist/library/library-profile.js.map +1 -1
  204. package/dist/library/sidecar-validate.js.map +1 -1
  205. package/dist/library/sidecar.js.map +1 -1
  206. package/dist/startup-cache.js.map +1 -1
  207. package/dist/wasi-paths.js.map +1 -1
  208. package/package.json +18 -18
  209. package/surface-manifest.json +60 -4
@@ -9,12 +9,12 @@ import * as ts from "../ts7/adapter.js";
9
9
  import { dirname, posix } from "node:path";
10
10
  import { pathToFileURL } from "node:url";
11
11
  import { wasiGuestPath } from "../../wasi-paths.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, 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";
13
13
  import { cjsClassExprWholeExportOf, cjsExportAssignmentOf, cjsExportDiscardReason, isCjsExportTableLiteral, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeEsmFile, locOf } from "../program.js";
14
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";
15
- import { blockedBindingUseDiag, recordShapeMismatchDiag, requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
15
+ import { blockedBindingUseDiag, requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
16
16
  import { PoisonError, dynUndefinedExpr, jsFuncNameOf, neverTaintedJsType, nodeThrowExpr, own } from "./lowerer.js";
17
- import { lowerIndexMergeHelper, lowerNpmStaticSafeIndexRead, lowerSafeIndexRead, strCharsCall } from "./lower-containers.js";
17
+ import { lowerNpmStaticSafeIndexRead, lowerSafeIndexRead, strCharsCall } from "./lower-containers.js";
18
18
  import { arrayValueStore } from "./array-values.js";
19
19
  import { npmStaticPackageOfPath } from "../npm-static.js";
20
20
  import { unsupportedModuleFeatureOf } from "../builtin-modules.js";
@@ -29,6 +29,11 @@ import { isConstAssertionTypeNode, isGenericCallableMemberType, isParseArgsDynTy
29
29
  import { lowerYield } from "./lower-generators.js";
30
30
  import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "./lower-stream.js";
31
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";
32
37
  /** A template piece's RAW text (String.raw's contract: escapes stay
33
38
  * characters). 7's client AST ships no rawText at runtime (the typing
34
39
  * declares it; the serialized node data omits it), so the raw span comes
@@ -405,7 +410,7 @@ function lowerExprInner(lowerer, expr) {
405
410
  lowerer.unsupported("SC1090", prop, "a conditional spread mixed with plain spreads in an 'any'-typed object literal");
406
411
  }
407
412
  const cond = lowerer.lowerCondition(cs.cond);
408
- if (!pureCondExpr(cond)) {
413
+ if (!isSafeToMoveConditionEarlier(cond)) {
409
414
  lowerer.unsupported("SC1090", prop, "conditional spreads with effectful conditions in an 'any'-typed object literal (bind the condition to a const first)");
410
415
  }
411
416
  spread = { cond, whenTrue: cs.whenTrue };
@@ -533,6 +538,14 @@ function lowerExprInner(lowerer, expr) {
533
538
  return { kind: "jsOp", op: "arrLit", args, type: JSVAL, loc };
534
539
  }
535
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
+ }
536
549
  // `typeof queueMicrotask` / `typeof DOMException` on a STDLIB global
537
550
  // whose declared type is callable or constructable: folds to
538
551
  // "function" BEFORE the operand lowers — the identity-token story
@@ -578,7 +591,7 @@ function lowerExprInner(lowerer, expr) {
578
591
  undefinedT: "undefined", nullT: "object",
579
592
  };
580
593
  const folded = FOLD[operand.type.kind];
581
- if (folded && droppableStatic(operand)) {
594
+ if (folded && isSafeToDiscard(operand)) {
582
595
  return { kind: "strLit", value: folded, type: STRING, loc };
583
596
  }
584
597
  // A union operand: every arm's typeof answer is static, so the value
@@ -587,7 +600,7 @@ function lowerExprInner(lowerer, expr) {
587
600
  // tests, so only side-effect-free reads compose — and when every arm
588
601
  // agrees the whole expression folds to that one string (dropping only
589
602
  // the pure read, the trust-the-checker bet lowerUnitComparison makes).
590
- if (operand.type.kind === "union" && pureReemittable(operand)) {
603
+ if (operand.type.kind === "union" && isSafeToRepeat(operand)) {
591
604
  const def = lowerer.unions.get(operand.type.unionId);
592
605
  const answers = def?.arms.map(typeofAnswer);
593
606
  if (def && answers && answers.every((s) => s !== null)) {
@@ -614,6 +627,9 @@ function lowerExprInner(lowerer, expr) {
614
627
  lowerer.unsupported("SC1090", expr, "typeof expressions on statically-typed values");
615
628
  }
616
629
  if (ts.isIdentifier(expr)) {
630
+ const moduleValue = lowerNodeModuleIdentifier(lowerer, expr);
631
+ if (moduleValue)
632
+ return moduleValue;
617
633
  if (lowerer.isSelfReference(expr)) {
618
634
  return { kind: "selfRef", type: lowerer.ctx.selfType, loc };
619
635
  }
@@ -1276,8 +1292,8 @@ function lowerExprInner(lowerer, expr) {
1276
1292
  // The tail of an optional chain whose `?.` sits deeper
1277
1293
  // (`x?.trim().toLowerCase()`): the whole tail short-circuits with
1278
1294
  // the guard, so it lowers as one chain (dyn/island receivers keep
1279
- // their own undefined-propagating reads — see chainTailClaimed).
1280
- if (chainTailClaimed(lowerer, expr))
1295
+ // their own undefined-propagating reads — see isOptionalChainTail).
1296
+ if (isOptionalChainTail(lowerer, expr))
1281
1297
  return lowerer.lowerOptionalChain(expr);
1282
1298
  return lowerer.lowerCall(expr);
1283
1299
  }
@@ -1325,6 +1341,17 @@ function lowerExprInner(lowerer, expr) {
1325
1341
  if (isRequireMainFilename(lowerer, expr)) {
1326
1342
  return { kind: "strLit", value: lowerer.entry.fileName, type: STRING, loc };
1327
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
+ }
1328
1355
  // Optional chaining `a?.b`: the guard lowers here (a tag test around
1329
1356
  // the plain property lowering below); the handled marker keeps this
1330
1357
  // re-entrant dispatch from looping.
@@ -1333,7 +1360,7 @@ function lowerExprInner(lowerer, expr) {
1333
1360
  }
1334
1361
  // `a?.b.c` — the tail of a chain whose token sits deeper: the whole
1335
1362
  // tail short-circuits with the guard.
1336
- if (!expr.questionDotToken && chainTailClaimed(lowerer, expr)) {
1363
+ if (!expr.questionDotToken && isOptionalChainTail(lowerer, expr)) {
1337
1364
  return lowerer.lowerOptionalChain(expr);
1338
1365
  }
1339
1366
  // Island receiver: o.x is an engine property read (getProp may throw
@@ -1662,7 +1689,7 @@ function lowerExprInner(lowerer, expr) {
1662
1689
  const recv = lowerer.lowerExpr(expr.expression);
1663
1690
  if (recv.type.kind === "dyn") {
1664
1691
  const key = { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) };
1665
- const opt = chainGuardedByQuestionDot(expr.expression);
1692
+ const opt = hasOptionalChainGuard(expr.expression);
1666
1693
  return lowerer.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: recv, type: DYN, loc }, expr);
1667
1694
  }
1668
1695
  }
@@ -1676,7 +1703,7 @@ function lowerExprInner(lowerer, expr) {
1676
1703
  // `f.call` as a value). JS files only; TS keeps the fence.
1677
1704
  if (!["apply", "bind", "call", "toString", "constructor", "prototype", "caller", "arguments"].includes(expr.name.text) &&
1678
1705
  isJsSourceFile(expr.getSourceFile())) {
1679
- const probed = probeLower(lowerer, expr.expression);
1706
+ const probed = tryLowerExpression(lowerer, expr.expression);
1680
1707
  if (probed?.type.kind === "func" &&
1681
1708
  canBoxFuncIntoDyn(probed.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) {
1682
1709
  const fnName = jsFuncNameOf(expr.expression);
@@ -1741,7 +1768,7 @@ function lowerExprInner(lowerer, expr) {
1741
1768
  type: STRING,
1742
1769
  loc: locOf(expr.name),
1743
1770
  };
1744
- const opt = chainGuardedByQuestionDot(expr.expression);
1771
+ const opt = hasOptionalChainGuard(expr.expression);
1745
1772
  return lowerer.maybeNarrow({
1746
1773
  kind: "dynKeyGet",
1747
1774
  key,
@@ -1801,7 +1828,7 @@ function lowerExprInner(lowerer, expr) {
1801
1828
  // validated dynCheck as usual.
1802
1829
  if (recvLowered.type.kind === "dyn") {
1803
1830
  const key = { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) };
1804
- const opt = chainGuardedByQuestionDot(expr.expression);
1831
+ const opt = hasOptionalChainGuard(expr.expression);
1805
1832
  return lowerer.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: recvLowered, type: DYN, loc }, expr);
1806
1833
  }
1807
1834
  // The lowered receiver is a RECORD the checker spelled wider —
@@ -1868,9 +1895,6 @@ function importMetaProperty(lowerer, expr) {
1868
1895
  return null;
1869
1896
  const sf = expr.getSourceFile();
1870
1897
  const fileName = moduleFileName(lowerer, sf);
1871
- if (lowerer.dynamic) {
1872
- lowerer.unsupported("SC1090", expr, `'import.meta.${expr.name.text}' in a dynamic island (module-loader metadata is supported only in static ESM)`);
1873
- }
1874
1898
  switch (expr.name.text) {
1875
1899
  case "url":
1876
1900
  return {
@@ -2437,7 +2461,7 @@ export function lowerUnitComparison(lowerer, left, right, negated, loc) {
2437
2461
  // callback's effects are observable) rides one throwaway tag
2438
2462
  // test; droppable reads fold to the bare literal.
2439
2463
  const answer = { kind: "boolLit", value: negated, type: BOOL, loc };
2440
- if (droppableStatic(other))
2464
+ if (isSafeToDiscard(other))
2441
2465
  return answer;
2442
2466
  const evalOnce = {
2443
2467
  kind: "unionIsTag", unionId: other.type.unionId, tag: 0, negated: false, value: other, type: BOOL, loc,
@@ -2545,7 +2569,7 @@ function lowerLooseNullCompare(lowerer, expr, loc) {
2545
2569
  // backend emits it per test — restricted to re-emittable pure reads;
2546
2570
  // anything effectful gets its own actionable fence (the generic
2547
2571
  // SC1040 hint would wrongly suggest `== null` as the fix).
2548
- if (!pureReemittable(other)) {
2572
+ if (!isSafeToRepeat(other)) {
2549
2573
  lowerer.unsupported("SC1090", expr, `'== null' / '!= null' on a '${lowerer.fmt(ut)}' value that isn't a plain read ` +
2550
2574
  `(both unit arms need the operand twice — bind it to a const first)`);
2551
2575
  }
@@ -2575,88 +2599,6 @@ function lowerLooseNullCompare(lowerer, expr, loc) {
2575
2599
  // `== null` is statically false, `!= null` statically true.
2576
2600
  return { kind: "boolLit", value: negated, type: BOOL, loc };
2577
2601
  }
2578
- /** Safe to EMIT twice: plain reads with no side effects — local/global
2579
- * reads and record/class field reads over such (accessor properties
2580
- * lower to getter CALLS, so they never appear as field reads). Used by
2581
- * the two-unit-arm `== null` composition, the union `typeof` forms, and
2582
- * the union-switch desugar, whose operands ride several tests. */
2583
- /** A static expression whose evaluation is unobservable — safe to DROP
2584
- * (typeof's constant fold: JS evaluates the operand, but when nothing in
2585
- * it can throw, call, write, or allocate observably, folding it away
2586
- * changes nothing). Strictly wider than pureReemittable: numeric
2587
- * arithmetic (f64 ops never throw — division by zero is Infinity),
2588
- * string concatenation, logical/ternary composition, and unit/literal
2589
- * leaves compose; anything with a call, an island op, an index read (can
2590
- * trap), or an unknown kind answers false. */
2591
- export function droppableStatic(e) {
2592
- switch (e.kind) {
2593
- case "numLit":
2594
- case "strLit":
2595
- case "boolLit":
2596
- case "unitLit":
2597
- return true;
2598
- case "bin":
2599
- return droppableStatic(e.left) && droppableStatic(e.right);
2600
- case "unary":
2601
- return droppableStatic(e.operand);
2602
- case "logical":
2603
- return droppableStatic(e.left) && droppableStatic(e.right);
2604
- case "ternary":
2605
- return droppableStatic(e.cond) && droppableStatic(e.then) && droppableStatic(e.else_);
2606
- case "strConcat":
2607
- return droppableStatic(e.left) && droppableStatic(e.right);
2608
- case "toBool":
2609
- return droppableStatic(e.operand);
2610
- case "unionIsTag":
2611
- return droppableStatic(e.value);
2612
- case "unionWrap":
2613
- return droppableStatic(e.value);
2614
- // Fresh allocations are unobservable too — only their PIECES can
2615
- // carry effects (a spread re-reads its source: pure; element and
2616
- // field initializers recurse).
2617
- case "arrayLit":
2618
- return e.elems.every(droppableStatic);
2619
- case "recordLit":
2620
- return e.fields.every((f) => droppableStatic(f.value));
2621
- case "recordClone":
2622
- return droppableStatic(e.source) && e.overrides.every((f) => droppableStatic(f.value));
2623
- case "closure":
2624
- return true;
2625
- default:
2626
- return pureReemittable(e);
2627
- }
2628
- }
2629
- export function pureReemittable(e) {
2630
- if (e.kind === "varRef")
2631
- return true;
2632
- if (e.kind === "recordGet" || e.kind === "fieldGet")
2633
- return pureReemittable(e.obj);
2634
- // The trust-the-checker narrowing bridges over a pure read: extraction
2635
- // and downcast are reads too (the +1 on ref payloads is RC bookkeeping,
2636
- // not an observable effect — each emission owns its own copy).
2637
- if (e.kind === "unionNarrow" || e.kind === "downcast")
2638
- return pureReemittable(e.value);
2639
- return false;
2640
- }
2641
- /** A side-effect-free CONDITION: the ToBoolean/test wrappers lowerCondition
2642
- * builds over pure reads (evaluating one early or twice changes nothing
2643
- * observable). Used by the island-literal conditional spread, whose
2644
- * desugar hoists the condition ahead of the properties. */
2645
- function pureCondExpr(e) {
2646
- if (e.kind === "boolLit")
2647
- return true;
2648
- if (e.kind === "toBool")
2649
- return pureReemittable(e.operand);
2650
- if (e.kind === "unionIsTag" || e.kind === "dynTest")
2651
- return pureReemittable(e.value);
2652
- if (e.kind === "unary" && e.op === "!")
2653
- return pureCondExpr(e.operand);
2654
- if (e.kind === "logical")
2655
- return pureCondExpr(e.left) && pureCondExpr(e.right);
2656
- if (e.kind === "jsOp" && (e.op === "truthy" || e.op === "not"))
2657
- return e.args.every(pureReemittable);
2658
- return pureReemittable(e);
2659
- }
2660
2602
  /** `a ?? b` — JS-exact nullish coalescing: ONLY null/undefined take the
2661
2603
  * default (0, "", and false do not), and the right side evaluates lazily.
2662
2604
  * On a unit-armed union left this is the `nullish` node (a runtime tag
@@ -2705,6 +2647,42 @@ export function lowerNullishCoalesce(lowerer, expr, loc) {
2705
2647
  const right = lowerer.lowerExprExpecting(expr.right, type);
2706
2648
  return { kind: "nullish", left, right, type, loc };
2707
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
+ }
2708
2686
  // The RETAGGED shape — the default changes the result union (`s ??
2709
2687
  // null` over `string | undefined` answers `null | string`): every
2710
2688
  // non-unit arm of the left has a home in the result, so an interned
@@ -2715,9 +2693,72 @@ export function lowerNullishCoalesce(lowerer, expr, loc) {
2715
2693
  if (type.kind === "union") {
2716
2694
  const leftT = left.type;
2717
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
+ }
2718
2759
  if (armPairs.every((p) => p.src >= 0 && p.dst >= 0)) {
2719
2760
  const right = lowerer.lowerExprExpecting(expr.right, type);
2720
- if (droppableStatic(right)) {
2761
+ if (isSafeToDiscard(right)) {
2721
2762
  const helper = nullishRetagHelper(lowerer, leftT, type, loc);
2722
2763
  return { kind: "call", callee: helper, args: [left, right], type, loc };
2723
2764
  }
@@ -2784,384 +2825,6 @@ function nullishRetagHelper(lowerer, leftT, resT, loc) {
2784
2825
  });
2785
2826
  return name;
2786
2827
  }
2787
- /** One optional-chain STEP: `a?.b`, `a?.m(...)`, `a?.[i]` (the token on
2788
- * the member access) and `f?.()` (the token on the call). The receiver
2789
- * lowers once; when it is a unit-armed union with ONE non-unit arm, the
2790
- * member/call lowers exactly as its non-optional spelling would — the
2791
- * receiver node reads back as the chain's bound narrowed value
2792
- * (chainRecv) and types as its non-nullish type — and the whole thing
2793
- * becomes an optChain node: tag test, undefined on the unit path (JS:
2794
- * null receivers still yield undefined), the body lazily otherwise,
2795
- * argument side effects included. A receiver the checker types
2796
- * never-nullish makes `?.` behave exactly like `.` — the guard folds
2797
- * away and the plain lowering is the value (trust-the-checker, like
2798
- * ??'s fold). Multi-step TAILS (`a?.b.c`, `x?.trim().toLowerCase()`)
2799
- * short-circuit whole: the guarded member step is the chain's dot and
2800
- * every later step lowers inside the guard, checker-narrowed non-nullish
2801
- * (see chainTailDot); sub-union receivers are fenced with rewrite
2802
- * hints. */
2803
- /** The unhandled `?.`-carrying MEMBER step in this node's receiver
2804
- * spine, when the node is the TAIL of an optional chain whose token sits
2805
- * deeper — `x?.trim().toLowerCase()` reads nothing past the guard when x
2806
- * is nullish, so the whole tail must lower inside it. Walks
2807
- * property/element accesses and call steps only (parens and `!` break
2808
- * the chain per the grammar: `(a?.b).c` throws on undefined in JS);
2809
- * the node's OWN token — and a call's immediate callee token — are the
2810
- * plain entries' cases, not tails, and handled markers make the chain
2811
- * lowering's re-dispatch walk past its own step. `f?.()` steps stay out
2812
- * (a call carrying the token has no member step to re-enter — the
2813
- * split-it fence keeps them). */
2814
- function chainTailDot(lowerer, expr) {
2815
- let cur = expr;
2816
- for (;;) {
2817
- // An active chain's bound receiver read is a LEAF — its own token
2818
- // was consumed by the chain that bound it.
2819
- if (lowerer.chainRecvByNode.has(cur))
2820
- return null;
2821
- if (ts.isCallExpression(cur)) {
2822
- // `f?.()` steps route through their own entry (or are being
2823
- // handled); a deeper token under one is that chain's business.
2824
- if (cur.questionDotToken)
2825
- return null;
2826
- cur = cur.expression;
2827
- continue;
2828
- }
2829
- if (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur)) {
2830
- if (cur.questionDotToken) {
2831
- // The node's own token and a call's immediate callee token are
2832
- // the plain entries' cases; a handled one is mid-re-dispatch
2833
- // (its receiver read, further down, ends the walk). The walk
2834
- // NEVER continues past an unhandled deeper token: that guard is
2835
- // where this tail's chain enters — anything below it belongs to
2836
- // the receiver's own (nested) chain.
2837
- if (cur === expr || lowerer.chainHandled.has(cur)) {
2838
- cur = cur.expression;
2839
- continue;
2840
- }
2841
- if (ts.isCallExpression(expr) && expr.expression === cur)
2842
- return null;
2843
- return cur;
2844
- }
2845
- cur = cur.expression;
2846
- continue;
2847
- }
2848
- return null;
2849
- }
2850
- }
2851
- /** True when `expr` is `require.main.filename` (either dot optional) on
2852
- * the AMBIENT CommonJS require — the entry-module identity read. The
2853
- * value is the ENTRY file's path, a compile-time constant like
2854
- * __filename; ESM files stay out (Node never defines require there —
2855
- * the ambient symbol would not resolve anyway). Shared by the property
2856
- * lowering (the fold) and lowerStringMethodCall's receiver gate (the
2857
- * checker types the chain `string | undefined`, but the folded receiver
2858
- * IS a string). */
2859
- export function isRequireMainFilename(lowerer, expr) {
2860
- if (!ts.isPropertyAccessExpression(expr) || expr.name.text !== "filename")
2861
- return false;
2862
- const main = expr.expression;
2863
- if (!ts.isPropertyAccessExpression(main) || main.name.text !== "main")
2864
- return false;
2865
- if (!lowerer.isStdlibGlobal(main.expression, "require"))
2866
- return false;
2867
- return !isNodeEsmFile(expr.getSourceFile());
2868
- }
2869
- /** True when `expr` is the tail of an optional chain the STATIC chain
2870
- * machinery should short-circuit whole: an unhandled deeper `?.` whose
2871
- * guarded receiver lowers as a unit-armed union. Dyn ('unknown') and
2872
- * island ('any') receivers answer their tails through their own
2873
- * undefined-propagating reads and stay out; never-nullish receivers fold
2874
- * at the token's own entry. */
2875
- function chainTailClaimed(lowerer, expr) {
2876
- const tail = chainTailDot(lowerer, expr);
2877
- if (!tail)
2878
- return false;
2879
- const recvT = lowerer.mapTypeOf(lowerer.typeOf(tail.expression));
2880
- if (recvT?.kind !== "union")
2881
- return false;
2882
- const def = lowerer.unions.get(recvT.unionId);
2883
- return !!def && def.arms.some(isUnitType);
2884
- }
2885
- /** True when an EARLIER step of this access chain carries `?.` — JS
2886
- * short-circuits the whole tail (`a?.b.c` reads nothing when a is
2887
- * nullish), so a dyn tail read must answer undefined instead of
2888
- * throwing. Walks the receiver spine only; the argument of the access
2889
- * itself is not part of the guard. */
2890
- function chainGuardedByQuestionDot(expr) {
2891
- let cur = expr;
2892
- for (;;) {
2893
- if (ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur)) {
2894
- cur = cur.expression;
2895
- continue;
2896
- }
2897
- if (ts.isPropertyAccessExpression(cur) ||
2898
- ts.isElementAccessExpression(cur) ||
2899
- ts.isCallExpression(cur)) {
2900
- if (cur.questionDotToken)
2901
- return true;
2902
- cur = cur.expression;
2903
- continue;
2904
- }
2905
- return false;
2906
- }
2907
- }
2908
- export function lowerOptionalChain(lowerer, expr) {
2909
- const loc = locOf(expr);
2910
- // The node CARRYING the ?. token and the receiver expression it guards.
2911
- let dotNode;
2912
- let recvNode;
2913
- if (ts.isCallExpression(expr) && expr.questionDotToken) {
2914
- dotNode = expr; // f?.()
2915
- recvNode = expr.expression;
2916
- }
2917
- else if (ts.isCallExpression(expr) &&
2918
- ts.isPropertyAccessExpression(expr.expression) &&
2919
- expr.expression.questionDotToken) {
2920
- dotNode = expr.expression; // a?.m()
2921
- recvNode = expr.expression.expression;
2922
- }
2923
- else if ((ts.isPropertyAccessExpression(expr) || ts.isElementAccessExpression(expr)) &&
2924
- expr.questionDotToken) {
2925
- dotNode = expr; // a?.b / a?.[i]
2926
- recvNode = expr.expression;
2927
- }
2928
- else {
2929
- // TAIL entry: the token sits deeper in the receiver spine and JS
2930
- // short-circuits the WHOLE tail with it (`x?.trim().toLowerCase()`
2931
- // reads nothing when x is nullish). The guarded member step becomes
2932
- // the chain's dot; every step above it lowers inside the guard, its
2933
- // checker type narrowed non-nullish below (the tag test proved it).
2934
- const tail = chainTailDot(lowerer, expr);
2935
- if (!tail) {
2936
- // A `f?.()` step under a member tail, or no token at all (a
2937
- // dispatch bug): short-circuiting those is not modeled.
2938
- lowerer.unsupported("SC1090", expr, "multi-step optional chains (split them: const v = a?.b; then use v?.c)");
2939
- }
2940
- dotNode = tail;
2941
- recvNode = tail.expression;
2942
- }
2943
- const loweredReceiver = lowerer.lowerExpr(recvNode);
2944
- const receiver = lowerer.runtimeOptionalSourceValue(recvNode, loweredReceiver) ?? loweredReceiver;
2945
- if (receiver.type.kind === "dyn") {
2946
- // `pkg?.name` / `pkg?.scripts?.[k]` on a JSON.parse result: dyn
2947
- // represents undefined directly, so the chain step IS the keyed
2948
- // read with the optional (unit-answers-undefined) policy — no
2949
- // optChain wrapper needed; nested steps compose the same way.
2950
- if (dotNode === expr && ts.isPropertyAccessExpression(expr)) {
2951
- const key = { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) };
2952
- return lowerer.maybeNarrow({ kind: "dynKeyGet", key, optional: true, value: receiver, type: DYN, loc }, expr);
2953
- }
2954
- if (dotNode === expr && ts.isElementAccessExpression(expr)) {
2955
- const key = lowerer.lowerExpr(expr.argumentExpression);
2956
- if (key.type.kind === "string") {
2957
- return lowerer.maybeNarrow({ kind: "dynKeyGet", key, optional: true, value: receiver, type: DYN, loc }, expr);
2958
- }
2959
- // NUMBER-typed indices (`entries?.[0]`): the property key is
2960
- // ToString(i), exactly JS — the canonical number text answers
2961
- // array indices in the dyn helper, anything else (fractions,
2962
- // negatives, NaN) reads as an absent key.
2963
- if (key.type.kind === "f64") {
2964
- const skey = { kind: "toString", operand: key, type: STRING, loc: key.loc };
2965
- return lowerer.maybeNarrow({ kind: "dynKeyGet", key: skey, optional: true, value: receiver, type: DYN, loc }, expr);
2966
- }
2967
- }
2968
- // The METHOD-call step (`rawName?.match(re)` on a dyn value): a
2969
- // nullish receiver short-circuits to the undefined dyn singleton
2970
- // (dyn represents undefined directly); anything else runs the
2971
- // VALIDATED dynamic dispatch — the same one the truthy-guarded
2972
- // spelling (`v ? v.match(...) : null`) compiles to, Node-shaped
2973
- // TypeError on a kind mismatch included. The dispatch's static
2974
- // result converts back into the checked-dynamic tree (the chain's checker type is
2975
- // the error-any world's `any`, so dyn IS its representation).
2976
- if (ts.isCallExpression(expr) &&
2977
- ts.isPropertyAccessExpression(dotNode) &&
2978
- dotNode === expr.expression) {
2979
- const m = dotNode.name.text;
2980
- const id = `chain.${lowerer.chainCounter++}`;
2981
- const recvRef = { kind: "chainRecv", id, type: DYN, loc: locOf(recvNode) };
2982
- lowerer.chainRecvByNode.set(recvNode, recvRef);
2983
- lowerer.chainHandled.add(dotNode);
2984
- let body;
2985
- try {
2986
- body = lowerer.lowerExpr(expr);
2987
- }
2988
- finally {
2989
- lowerer.chainRecvByNode.delete(recvNode);
2990
- lowerer.chainHandled.delete(dotNode);
2991
- }
2992
- if (body.type.kind !== "dyn") {
2993
- if (body.type.kind !== "void" && !lowerer.dynConvertible(body.type)) {
2994
- lowerer.unsupported("SC1100", expr, `optional METHOD calls on 'unknown' values where the result ('${lowerer.fmt(body.type)}' ` +
2995
- `from '.${m}(...)') has no dynamic representation`);
2996
- }
2997
- if (body.type.kind === "void") {
2998
- return { kind: "optChain", id, receiver, body, type: VOID, loc };
2999
- }
3000
- body = { kind: "dynFrom", value: body, type: DYN, loc };
3001
- }
3002
- return { kind: "optChain", id, receiver, body, type: DYN, loc };
3003
- }
3004
- lowerer.unsupported("SC1100", expr, "optional chaining on 'unknown' values");
3005
- }
3006
- // An 'any' (island-handle) receiver: the nullish test asks the ENGINE
3007
- // value at runtime — null/undefined short-circuit to the engine's
3008
- // undefined (JS: null receivers yield undefined too), anything else
3009
- // proceeds as the plain island operation with the receiver evaluated
3010
- // once (argument side effects stay lazy, like every optChain). The
3011
- // result is an island value again ('any' in, 'any' out).
3012
- if (receiver.type.kind === "jsval") {
3013
- const id = `chain.${lowerer.chainCounter++}`;
3014
- const recvRef = { kind: "chainRecv", id, type: JSVAL, loc: locOf(recvNode) };
3015
- if (dotNode === expr && ts.isCallExpression(expr)) {
3016
- // `a.b?.(...)` — a MEMBER callee: JS calls it with `this` bound to
3017
- // `a` and short-circuits a nullish member — exactly the engine's
3018
- // own `o.name?.()` (optCallMethod: the RECEIVER is `a`, evaluated
3019
- // once through the chain; a nullish `a` itself short-circuits
3020
- // earlier like any optional chain). Computed members keep a fence.
3021
- if (ts.isPropertyAccessExpression(recvNode) || ts.isElementAccessExpression(recvNode)) {
3022
- if (ts.isPropertyAccessExpression(recvNode) &&
3023
- !recvNode.questionDotToken) {
3024
- const obj = lowerer.lowerExpr(recvNode.expression);
3025
- if (obj.type.kind !== "jsval")
3026
- lowerer.badType(recvNode.expression, lowerer.typeOf(recvNode.expression));
3027
- const args = expr.arguments.map((a) => lowerer.jsvalIn(lowerer.lowerExpr(a), a));
3028
- return { kind: "jsOp", op: "optCallMethod", name: recvNode.name.text, args: [obj, ...args], type: JSVAL, loc };
3029
- }
3030
- lowerer.unsupported("SC1090", expr, "optional calls of computed 'any' member values (a[k]?.() — bind the member to a const first)");
3031
- }
3032
- const args = expr.arguments.map((a) => lowerer.jsvalIn(lowerer.lowerExpr(a), a));
3033
- const body = { kind: "jsOp", op: "callFn", args: [recvRef, ...args], type: JSVAL, loc };
3034
- return { kind: "optChain", id, receiver, body, type: JSVAL, loc };
3035
- }
3036
- // Member forms (`x?.y`, `x?.y(...)`, `x?.[i]`): re-dispatch the plain
3037
- // island lowering with the receiver node bound to the chain.
3038
- lowerer.chainRecvByNode.set(recvNode, recvRef);
3039
- lowerer.chainHandled.add(dotNode);
3040
- let body;
3041
- try {
3042
- body = lowerer.lowerExpr(expr);
3043
- }
3044
- finally {
3045
- lowerer.chainRecvByNode.delete(recvNode);
3046
- lowerer.chainHandled.delete(dotNode);
3047
- }
3048
- if (body.type.kind !== "jsval")
3049
- lowerer.badType(expr, lowerer.typeOf(expr));
3050
- return { kind: "optChain", id, receiver, body, type: JSVAL, loc };
3051
- }
3052
- const def = receiver.type.kind === "union" ? lowerer.unions.get(receiver.type.unionId) : undefined;
3053
- if (!def || !def.arms.some(isUnitType)) {
3054
- // Never nullish: `?.` IS `.` — re-dispatch the plain lowering. The
3055
- // receiver subtree above is discarded (lowering is pure IR
3056
- // construction); the fresh dispatch lowers it again in place. The
3057
- // checker may still SPELL the receiver nullable while the lowering
3058
- // answers the plain value (`process.getuid?.()` is the POSIX
3059
- // number), so narrow the node too — downstream receiver-typed
3060
- // dispatch must agree with the lowered value, not the spelling.
3061
- lowerer.chainHandled.add(dotNode);
3062
- const hadNarrow = lowerer.chainNarrowedType.has(recvNode);
3063
- if (!hadNarrow) {
3064
- lowerer.chainNarrowedType.set(recvNode, lowerer.checker.getNonNullableType(lowerer.checker.getTypeAtLocation(recvNode)));
3065
- }
3066
- try {
3067
- return lowerer.lowerExpr(expr);
3068
- }
3069
- finally {
3070
- lowerer.chainHandled.delete(dotNode);
3071
- if (!hadNarrow)
3072
- lowerer.chainNarrowedType.delete(recvNode);
3073
- }
3074
- }
3075
- const rest = def.arms.filter((a) => !isUnitType(a));
3076
- if (rest.length !== 1) {
3077
- lowerer.unsupported("SC1090", expr, `'?.' on '${lowerer.fmt(receiver.type)}' (the guarded receiver is a sub-union; check a discriminant field first)`);
3078
- }
3079
- const narrowed = rest[0];
3080
- const id = `chain.${lowerer.chainCounter++}`;
3081
- const recvRef = { kind: "chainRecv", id, type: narrowed, loc: locOf(recvNode) };
3082
- // `f?.()`: the callee IS the guarded value — build the indirect call
3083
- // directly (no member dispatch exists to re-enter).
3084
- if (dotNode === expr && ts.isCallExpression(expr)) {
3085
- if (narrowed.kind !== "func")
3086
- lowerer.badType(recvNode, lowerer.typeOf(recvNode));
3087
- const params = narrowed.params;
3088
- const args = expr.arguments.map((a, i) => lowerer.lowerExprExpecting(a, params[i]));
3089
- const body = { kind: "callValue", callee: recvRef, args, type: narrowed.ret, loc };
3090
- return lowerer.finishOptionalChain(expr, id, receiver, body, loc);
3091
- }
3092
- // Member forms: re-dispatch the normal lowering with the receiver node
3093
- // bound to the chain (reads as chainRecv, types as non-nullish).
3094
- const narrowedTs = lowerer.checker.getNonNullableType(lowerer.checker.getTypeAtLocation(recvNode));
3095
- lowerer.chainRecvByNode.set(recvNode, recvRef);
3096
- lowerer.chainNarrowedType.set(recvNode, narrowedTs);
3097
- lowerer.chainHandled.add(dotNode);
3098
- // A TAIL entry's intermediate steps (`x?.trim()` inside
3099
- // `x?.trim().toLowerCase()`) checker-type with the chain's `|
3100
- // undefined` even though inside the guard they are proven non-nullish
3101
- // — narrow each so the downstream method/member dispatch rides the
3102
- // real receiver kind. Only nodes this chain registers are cleaned up.
3103
- const tailSteps = [];
3104
- if (dotNode !== expr && !(ts.isCallExpression(expr) && expr.expression === dotNode)) {
3105
- for (let cur = expr; cur !== dotNode;) {
3106
- const next = ts.isCallExpression(cur)
3107
- ? cur.expression
3108
- : cur.expression;
3109
- if (next === dotNode)
3110
- break;
3111
- if (!lowerer.chainNarrowedType.has(next)) {
3112
- lowerer.chainNarrowedType.set(next, lowerer.checker.getNonNullableType(lowerer.checker.getTypeAtLocation(next)));
3113
- tailSteps.push(next);
3114
- }
3115
- cur = next;
3116
- }
3117
- if (!lowerer.chainNarrowedType.has(dotNode)) {
3118
- lowerer.chainNarrowedType.set(dotNode, lowerer.checker.getNonNullableType(lowerer.checker.getTypeAtLocation(dotNode)));
3119
- tailSteps.push(dotNode);
3120
- }
3121
- }
3122
- let body;
3123
- try {
3124
- body = lowerer.lowerExpr(expr);
3125
- }
3126
- finally {
3127
- lowerer.chainRecvByNode.delete(recvNode);
3128
- lowerer.chainNarrowedType.delete(recvNode);
3129
- lowerer.chainHandled.delete(dotNode);
3130
- for (const n of tailSteps)
3131
- lowerer.chainNarrowedType.delete(n);
3132
- }
3133
- return lowerer.finishOptionalChain(expr, id, receiver, body, loc);
3134
- }
3135
- /** The optChain node around a lowered chain body: void bodies keep the
3136
- * statement form (`cb?.();` — the checker's `void | undefined` result IS
3137
- * void here); value bodies wrap into the checker's undefined-armed
3138
- * result union. */
3139
- export function finishOptionalChain(lowerer, expr, id, receiver, body, loc) {
3140
- if (body.type.kind === "void") {
3141
- return { kind: "optChain", id, receiver, body, type: VOID, loc };
3142
- }
3143
- // A dyn body (`pricing?.[key]` — an unknown-valued index-signature
3144
- // read) stays dyn: `unknown | undefined` IS unknown, and dyn represents
3145
- // undefined directly (the unit path yields the undefined dyn value).
3146
- if (body.type.kind === "dyn") {
3147
- return { kind: "optChain", id, receiver, body, type: DYN, loc };
3148
- }
3149
- let type = lowerer.irTypeOf(expr);
3150
- const hiddenOptionalResult = (type.kind !== "union" || lowerer.armTag(type.unionId, UNDEFINED_T) < 0) &&
3151
- receiver.type.kind === "union" && lowerer.armTag(receiver.type.unionId, UNDEFINED_T) >= 0 &&
3152
- body.type.kind !== "generator" && body.type.kind !== "jsval"
3153
- ? lowerer.withUndefinedArmOf(body.type)
3154
- : null;
3155
- if (hiddenOptionalResult)
3156
- type = hiddenOptionalResult;
3157
- if (type.kind !== "union" || lowerer.armTag(type.unionId, UNDEFINED_T) < 0) {
3158
- lowerer.unsupported("SC1090", expr, `'?.' where the result '${lowerer.fmt(type)}' has no undefined arm (narrow the receiver with 'if (x !== undefined)' instead)`);
3159
- }
3160
- const wrapped = lowerer.coerceToExpected(body, type);
3161
- if (!typeEquals(wrapped.type, type))
3162
- lowerer.badType(expr, lowerer.typeOf(expr));
3163
- return { kind: "optChain", id, receiver, body: wrapped, type, loc };
3164
- }
3165
2828
  /** A CONDITION-position expression: the result is consumed as a bool
3166
2829
  * only, so `&&`/`||` descend recursively over ToBoolean'd operands —
3167
2830
  * JS-exact (`ToBoolean(a && b)` ≡ `ToBoolean(a) && ToBoolean(b)`), still
@@ -3171,6 +2834,11 @@ export function lowerCondition(lowerer, expr) {
3171
2834
  let e = expr;
3172
2835
  while (ts.isParenthesizedExpression(e))
3173
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
+ }
3174
2842
  if (ts.isBinaryExpression(e)) {
3175
2843
  const op = e.operatorToken.kind;
3176
2844
  if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) {
@@ -3204,6 +2872,47 @@ export function lowerCondition(lowerer, expr) {
3204
2872
  }
3205
2873
  return lowerer.ensureBool(lowerAbsenceProbe(lowerer, expr) ?? lowerer.lowerExpr(expr), expr);
3206
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
+ }
3207
2916
  function runtimeOptionalLocalOf(lowerer, node) {
3208
2917
  let expr = node;
3209
2918
  while (ts.isParenthesizedExpression(expr))
@@ -3729,6 +3438,27 @@ export function lowerArrayLiteral(lowerer, expr, expected) {
3729
3438
  }
3730
3439
  }
3731
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
+ }
3732
3462
  // An EMPTY literal under a CONST ASSERTION whose type queries panicked
3733
3463
  // (tsgo's readonly-[] TupleType conversion — the facade answers `any`,
3734
3464
  // which maps to nothing statically and to an island value under
@@ -3810,7 +3540,7 @@ export function lowerArrayLiteral(lowerer, expr, expected) {
3810
3540
  }
3811
3541
  lowerer.badType(expr, tsType);
3812
3542
  }
3813
- let type = mapped;
3543
+ const type = mapped;
3814
3544
  // Keep the array payload type fixed. Optional reads are stored through
3815
3545
  // arrayValueStore, which records UNDEFINED in the state byte while the
3816
3546
  // payload remains number/string/etc.; widening the array element here
@@ -3823,7 +3553,7 @@ export function lowerArrayLiteral(lowerer, expr, expected) {
3823
3553
  const candidate = expr.elements[i];
3824
3554
  if (ts.isSpreadElement(candidate) || ts.isOmittedExpression(candidate))
3825
3555
  continue;
3826
- const probe = probeLower(lowerer, candidate);
3556
+ const probe = tryLowerExpression(lowerer, candidate);
3827
3557
  if (probe && lowerer.runtimeOptionalWidening(probe.type, type.elem) !== null) {
3828
3558
  runtimeOptionalElements.add(i);
3829
3559
  needsStateBuild = true;
@@ -4019,315 +3749,6 @@ export function lowerArrayLiteral(lowerer, expr, expected) {
4019
3749
  loc,
4020
3750
  };
4021
3751
  }
4022
- /** `{ a: 1, b: "x" }` → recordLit. The record type comes from the
4023
- * contextual type when tsc has one (annotated declarations, arguments,
4024
- * nested literals) and from the literal's own type otherwise — both intern
4025
- * to the same shapeId unless width subtyping is in play (an `as` cast can
4026
- * smuggle a wider/narrower literal past tsc's freshness check), which the
4027
- * exact-shape checks below reject with SC2002. Fields lower IN SOURCE
4028
- * ORDER: JS evaluates property values in source order. */
4029
- /** The dropped-field names of the PromiseSettledResult honest subset
4030
- * (SEMANTICS.md 46): when the literal's target type is (a union
4031
- * containing) the lib's PromiseFulfilledResult / PromiseRejectedResult,
4032
- * the record mapping kept only `status` — `value` and `reason` evaluate
4033
- * for effect and are not stored. Null for every other target. */
4034
- function settledDropNames(lowerer, tsType) {
4035
- let names = null;
4036
- const parts = tsType.isUnionType() ? ts.constituentTypes(tsType) : [tsType];
4037
- for (const part of parts) {
4038
- const sym = part.getSymbol();
4039
- if (!sym ||
4040
- !lowerer.checker.declarationsOf(sym).some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()))) {
4041
- continue;
4042
- }
4043
- if (sym.name === "PromiseFulfilledResult")
4044
- (names ??= new Set()).add("value");
4045
- if (sym.name === "PromiseRejectedResult")
4046
- (names ??= new Set()).add("reason");
4047
- }
4048
- return names;
4049
- }
4050
- /** `{ [KEY]: v }` where KEY is a compile-time-known string or number:
4051
- * the key folds into an ordinary (spelled) property name — the record
4052
- * shape is exactly what tsc computed for the literal (the late-bound
4053
- * name IS the checker's literal type), so the fold is just spelling.
4054
- * PURE key forms only, so skipping the key's evaluation is exact (JS
4055
- * evaluates the key before the value; effectful expressions keep the
4056
- * fence): any side-effect-free expression — identifiers, property
4057
- * chains with no accessor on them (enum members, `other.name` reads),
4058
- * literals, parenthesized/as-wrapped forms, operators over those —
4059
- * whose checker type is ONE string or number literal (or a union whose
4060
- * arms all spell the same name — `E1.x || E2.x` where both are 0).
4061
- * Number keys take JS's canonical spelling (`{ [E.member]: v }` stores
4062
- * "0" — ToPropertyKey), exactly the name tsc late-bound. Templates
4063
- * fold structurally span by span as well (a template of literals whose
4064
- * checker type widened still spells one string). Symbol-typed and
4065
- * runtime-valued keys stay out. */
4066
- function literalComputedKey(lowerer, name) {
4067
- return foldedStringKeyOf(lowerer, name.expression);
4068
- }
4069
- export function foldedStringKeyOf(lowerer, expr) {
4070
- if (ts.isParenthesizedExpression(expr))
4071
- return foldedStringKeyOf(lowerer, expr.expression);
4072
- if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr))
4073
- return expr.text;
4074
- if (ts.isTemplateExpression(expr)) {
4075
- // Structural fold first — exact even where the checker widened the
4076
- // template's own type; the general type-directed fold below is the
4077
- // fallback (a template the checker DID type as one literal).
4078
- let out = expr.head.text;
4079
- let folded = true;
4080
- for (const span of expr.templateSpans) {
4081
- const part = foldedStringKeyOf(lowerer, span.expression);
4082
- if (part === null) {
4083
- folded = false;
4084
- break;
4085
- }
4086
- out += part + span.literal.text;
4087
- }
4088
- if (folded)
4089
- return out;
4090
- }
4091
- // The general fold: a PURE expression whose checker type spells one
4092
- // property name. An as-cast folds by its OWN checker type — that is
4093
- // the name tsc late-bound the property under, so shape and storage
4094
- // stay one name (the standard trust-the-checker bet); a cast that
4095
- // WIDENS to string un-late-binds the property, its type is no literal,
4096
- // and the fence stays.
4097
- if (!pureKeyExpr(lowerer, expr))
4098
- return null;
4099
- return literalKeySpellingOf(lowerer.typeOf(expr));
4100
- }
4101
- /** The property-name spelling of a single-literal checker type: string
4102
- * literals directly, number literals in JS's canonical ToString spelling
4103
- * (enum members included — their literal flags carry the value), and
4104
- * unions whose arms all spell the SAME name (`E1.x || E2.x` — two enum
4105
- * literal types, one value). Null for everything else. */
4106
- function literalKeySpellingOf(t) {
4107
- if (t.isStringLiteralType())
4108
- return t.value;
4109
- if (t.isNumberLiteralType())
4110
- return String(t.value);
4111
- if (t.isUnionType()) {
4112
- let out = null;
4113
- for (const arm of ts.constituentTypes(t)) {
4114
- const s = arm.isStringLiteralType() ? arm.value : arm.isNumberLiteralType() ? String(arm.value) : null;
4115
- if (s === null || (out !== null && s !== out))
4116
- return null;
4117
- out = s;
4118
- }
4119
- return out;
4120
- }
4121
- return null;
4122
- }
4123
- /** True iff evaluating `expr` can have no observable effect, so a fold
4124
- * may skip it: literals, identifier reads, property chains whose every
4125
- * member is a plain field/enum member (an accessor anywhere on the chain
4126
- * is a call), operators and casts over those. Calls, `new`, assignments,
4127
- * and element accesses stay impure. */
4128
- function pureKeyExpr(lowerer, expr) {
4129
- if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr))
4130
- return pureKeyExpr(lowerer, expr.expression);
4131
- if (ts.isAsExpression(expr) || ts.isTypeAssertion(expr))
4132
- return pureKeyExpr(lowerer, expr.expression);
4133
- if (ts.isIdentifier(expr))
4134
- return true;
4135
- if (ts.isStringLiteralLike(expr) || ts.isNumericLiteral(expr))
4136
- return true;
4137
- if (expr.kind === ts.SyntaxKind.TrueKeyword || expr.kind === ts.SyntaxKind.FalseKeyword ||
4138
- expr.kind === ts.SyntaxKind.NullKeyword)
4139
- return true;
4140
- if (ts.isPropertyAccessExpression(expr)) {
4141
- const sym = lowerer.checker.getSymbolAtLocation(expr.name);
4142
- if (sym && sym.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor))
4143
- return false;
4144
- return pureKeyExpr(lowerer, expr.expression);
4145
- }
4146
- if (ts.isPrefixUnaryExpression(expr) &&
4147
- (expr.operator === ts.SyntaxKind.MinusToken || expr.operator === ts.SyntaxKind.PlusToken ||
4148
- expr.operator === ts.SyntaxKind.ExclamationToken || expr.operator === ts.SyntaxKind.TildeToken)) {
4149
- return pureKeyExpr(lowerer, expr.operand);
4150
- }
4151
- if (ts.isBinaryExpression(expr)) {
4152
- const op = expr.operatorToken.kind;
4153
- const pureOp = op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken ||
4154
- op === ts.SyntaxKind.QuestionQuestionToken ||
4155
- (op >= ts.SyntaxKind.LessThanToken && op <= ts.SyntaxKind.CaretToken &&
4156
- op !== ts.SyntaxKind.InstanceOfKeyword && op !== ts.SyntaxKind.InKeyword);
4157
- return pureOp && pureKeyExpr(lowerer, expr.left) && pureKeyExpr(lowerer, expr.right);
4158
- }
4159
- if (ts.isConditionalExpression(expr)) {
4160
- return pureKeyExpr(lowerer, expr.condition) && pureKeyExpr(lowerer, expr.whenTrue) && pureKeyExpr(lowerer, expr.whenFalse);
4161
- }
4162
- if (ts.isTemplateExpression(expr))
4163
- return expr.templateSpans.every((s) => pureKeyExpr(lowerer, s.expression));
4164
- return false;
4165
- }
4166
- /** A property's field name: identifier/string-literal keys directly,
4167
- * foldable computed keys through literalComputedKey (the syntax pass
4168
- * fenced everything else). */
4169
- function propNameText(lowerer, name) {
4170
- if (ts.isComputedPropertyName(name)) {
4171
- const k = literalComputedKey(lowerer, name);
4172
- if (k === null)
4173
- throw new InternalCompilerError("lowerer bug: unfoldable computed key past the fence");
4174
- return k;
4175
- }
4176
- // Numeric literals name their canonical string key (JS's ToPropertyKey
4177
- // — the same name the checker gives the property symbol).
4178
- if (ts.isNumericLiteral(name))
4179
- return String(Number(name.text));
4180
- return name.text;
4181
- }
4182
- /** The runtime-keyed JS object literal (a computed key that doesn't fold):
4183
- * builds a dyn object member-by-member. Keys evaluate before their values,
4184
- * properties in source order — JS's object-literal evaluation exactly.
4185
- * Identifier/string keys are compile-time strings; numeric keys take
4186
- * their canonical number string (`{ 0x10: v }` stores "16" — JS's
4187
- * ToPropertyKey); computed keys evaluate and pass through ToString (the
4188
- * dyn's String() for dyn operands — `{ [field]: v }` where field is a
4189
- * checked-dynamic param). Values convert through the usual dyn boundary
4190
- * (dynFrom's domain, functions box); a value with no dyn representation
4191
- * fences per property. Spreads copy through dyn.assign in source order;
4192
- * accessors stay fenced. */
4193
- export function lowerDynObjectLiteral(lowerer, expr, boxValue) {
4194
- const loc = locOf(expr);
4195
- let fields = [];
4196
- let acc = null;
4197
- const flushFields = () => {
4198
- if (fields.length === 0)
4199
- return;
4200
- const chunk = { kind: "dynObjLit", fields, type: DYN, loc };
4201
- acc = acc === null
4202
- ? chunk
4203
- : { kind: "libCall", fn: "dyn.assign", args: [acc, chunk], type: DYN, loc };
4204
- fields = [];
4205
- };
4206
- for (const prop of expr.properties) {
4207
- if (ts.isSpreadAssignment(prop)) {
4208
- flushFields();
4209
- const raw = lowerer.lowerExpr(prop.expression);
4210
- const source = boxValue
4211
- ? boxValue(prop.expression, raw)
4212
- : lowerer.coerceToExpected(raw, DYN);
4213
- if (source.type.kind !== "dyn") {
4214
- lowerer.unsupported("SC1101", prop.expression, `spreading '${lowerer.fmt(source.type)}' into a checked-dynamic object literal`);
4215
- }
4216
- acc ??= { kind: "dynObjLit", fields: [], type: DYN, loc };
4217
- acc = { kind: "libCall", fn: "dyn.assign", args: [acc, source], type: DYN, loc: locOf(prop) };
4218
- continue;
4219
- }
4220
- if (!ts.isPropertyAssignment(prop) &&
4221
- !ts.isShorthandPropertyAssignment(prop) &&
4222
- !ts.isMethodDeclaration(prop)) {
4223
- lowerer.unsupported("SC1090", prop, "accessors in a runtime-keyed (computed-key) object literal");
4224
- }
4225
- const name = prop.name;
4226
- let key;
4227
- if (ts.isComputedPropertyName(name)) {
4228
- const folded = literalComputedKey(lowerer, name);
4229
- if (folded !== null) {
4230
- key = { kind: "strLit", value: folded, type: STRING, loc: locOf(name) };
4231
- }
4232
- else {
4233
- let k = lowerer.lowerExpr(name.expression);
4234
- if (k.type.kind === "f64" || k.type.kind === "bool" || k.type.kind === "dyn") {
4235
- k = { kind: "toString", operand: k, type: STRING, loc: locOf(name) };
4236
- }
4237
- if (k.type.kind !== "string") {
4238
- lowerer.unsupported("SC1090", name, `'${lowerer.fmt(k.type)}'-typed computed property keys (string, number, boolean, and unknown keys stringify)`);
4239
- }
4240
- key = k;
4241
- }
4242
- }
4243
- else if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
4244
- key = { kind: "strLit", value: name.text, type: STRING, loc: locOf(name) };
4245
- }
4246
- else if (ts.isNumericLiteral(name)) {
4247
- key = { kind: "strLit", value: String(Number(name.text)), type: STRING, loc: locOf(name) };
4248
- }
4249
- else {
4250
- lowerer.unsupported("SC1090", prop, "non-identifier property names");
4251
- }
4252
- const valueExpr = ts.isMethodDeclaration(prop)
4253
- ? prop
4254
- : ts.isShorthandPropertyAssignment(prop)
4255
- ? prop.name
4256
- : prop.initializer;
4257
- // Lambda values that fail to lower become trap closures (the
4258
- // per-call fence granularity — fenceClosureProbe) so the object
4259
- // still builds; everything else keeps the per-property fence below.
4260
- let raw;
4261
- const propDiagsBefore = lowerer.diags.length;
4262
- try {
4263
- const lowerValue = () => ts.isMethodDeclaration(prop)
4264
- ? (lowerer.rejectThisInObjectMethod(prop.body ?? prop), lowerer.lowerLambda(prop))
4265
- : lowerer.lowerExpr(valueExpr);
4266
- raw =
4267
- fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ??
4268
- lowerValue();
4269
- }
4270
- catch (err) {
4271
- // A PURE member read a JS file cannot lower (a namespace object
4272
- // in an export aggregate): the slot takes a boxed fence closure —
4273
- // the diagnostics defer to the runtime-fence ledger, the object
4274
- // builds, and only USING the member stops the run.
4275
- const pureMember = ts.isIdentifier(valueExpr);
4276
- if (!(err instanceof PoisonError) || !pureMember)
4277
- throw err;
4278
- const fence = lowerer.deferToRuntimeFence(propDiagsBefore, prop, {
4279
- kind: "closure",
4280
- name: () => `%fn${lowerer.lambdaCounter++}_dynfence`,
4281
- returnType: VOID,
4282
- type: { kind: "func", params: [], ret: VOID },
4283
- });
4284
- if (!fence)
4285
- throw err;
4286
- raw = fence;
4287
- }
4288
- let v = boxValue
4289
- ? boxValue(valueExpr, raw)
4290
- : lowerer.coerceToExpected(raw, DYN);
4291
- if (v.type.kind !== "dyn") {
4292
- const convDiagsBefore = lowerer.diags.length;
4293
- try {
4294
- lowerer.unsupported("SC1101", valueExpr, `holding '${lowerer.fmt(v.type)}' values in a runtime-keyed (computed-key) object literal`);
4295
- }
4296
- catch (err) {
4297
- // A member value the checked-dynamic tree cannot hold — a func whose signature
4298
- // cannot box, an island ('any') handle, a record carrying func
4299
- // fields (the export aggregate's typed utilities and npm-handle
4300
- // members): the same call-time fence deferral as an unlowerable
4301
- // member — the slot takes a boxed fence closure; only USING it
4302
- // stops the run. Probe mode and ICEs keep the poison.
4303
- if (!(err instanceof PoisonError))
4304
- throw err;
4305
- const fence = lowerer.deferToRuntimeFence(convDiagsBefore, prop, {
4306
- kind: "closure",
4307
- name: () => `%fn${lowerer.lambdaCounter++}_dynfence`,
4308
- returnType: VOID,
4309
- type: { kind: "func", params: [], ret: VOID },
4310
- });
4311
- if (!fence)
4312
- throw err;
4313
- v = lowerer.coerceToExpected(fence, DYN);
4314
- }
4315
- }
4316
- fields.push({ key, value: v });
4317
- }
4318
- flushFields();
4319
- return acc ?? { kind: "dynObjLit", fields: [], type: DYN, loc };
4320
- }
4321
- /** Spreading a source whose shape carries accessor slots: Node's copy
4322
- * invokes each getter exactly once in insertion order — even for keys a
4323
- * later contributor overrides — while the field-copy desugar assumes pure
4324
- * reads it may drop or reorder. Getter-call counts would silently diverge,
4325
- * so accessor-carrying sources fence by name. */
4326
- function fenceAccessorSpreadSource(lowerer, prop, srcShape) {
4327
- if (srcShape && shapeHasAccessorSlots(srcShape)) {
4328
- lowerer.unsupported("SC1090", prop, "object spread of sources carrying get/set accessor properties (Node invokes each getter once during the copy — the field-copy desugar cannot model the calls; bind the reads to consts first)");
4329
- }
4330
- }
4331
3752
  /** Convert an undefined-armed numeric value at an arithmetic use. Ordinary
4332
3753
  * reads preserve their tagged value; JavaScript's ToNumber(undefined) is NaN,
4333
3754
  * so compounds and binary arithmetic use this checked one-time extraction. */
@@ -4347,7 +3768,7 @@ export function lowerOptionalNumber(lowerer, operand, loc, narrowedNode) {
4347
3768
  return operand;
4348
3769
  let stable = operand;
4349
3770
  let prefix = [];
4350
- if (!pureReemittable(operand)) {
3771
+ if (!isSafeToRepeat(operand)) {
4351
3772
  const tmp = lowerer.declareHiddenLocal("%arith", operand.type);
4352
3773
  stable = varRef(tmp.id, operand.type, loc);
4353
3774
  prefix = [{ kind: "varDecl", localId: tmp.id, init: operand, loc }];
@@ -4364,1499 +3785,6 @@ export function lowerOptionalNumber(lowerer, operand, loc, narrowedNode) {
4364
3785
  };
4365
3786
  return prefix.length === 0 ? value : { kind: "seqExpr", stmts: prefix, result: value, type: F64, loc };
4366
3787
  }
4367
- /** The JS trap-closure fallback for FUNCTION-VALUED object properties: a
4368
- * lambda whose body fails to lower inside a JS object literal becomes a
4369
- * closure of the field's exact func type whose body is the runtime fence —
4370
- * the object still BUILDS (commander's `_outputConfiguration` table: the
4371
- * driven writeOut/writeErr entries work; an unloweraable color probe traps
4372
- * only if something CALLS it). The failed lambda's diagnostics move to the
4373
- * runtime-fence inventory, exactly like a fenced JS statement. Null when
4374
- * the fallback does not apply (not a JS file, not a lambda, no func slot)
4375
- * — the caller lowers normally. ICEs (SC9001) never convert. */
4376
- function fenceClosureProbe(lowerer, node, slotType, attempt) {
4377
- if (!isJsSourceFile(node.getSourceFile()))
4378
- return null;
4379
- let inner = node;
4380
- while (ts.isParenthesizedExpression(inner))
4381
- inner = inner.expression;
4382
- if (!ts.isArrowFunction(inner) && !ts.isFunctionExpression(inner) && !ts.isMethodDeclaration(inner)) {
4383
- return null;
4384
- }
4385
- // The trap's ABI: the field's func slot when one exists; otherwise (the
4386
- // dyn-object literal path — the value boxes into dyn) the all-dyn
4387
- // signature of the lambda's own arity, which canBoxFuncIntoDyn always
4388
- // admits.
4389
- const fieldType = slotType !== undefined && slotType.kind === "func"
4390
- ? slotType
4391
- : { kind: "func", params: inner.parameters.map(() => DYN), ret: DYN };
4392
- if (slotType !== undefined && slotType.kind !== "func")
4393
- return null;
4394
- const diagsBefore = lowerer.diags.length;
4395
- try {
4396
- return attempt();
4397
- }
4398
- catch (e) {
4399
- if (!(e instanceof PoisonError))
4400
- throw e;
4401
- const params = fieldType.params.map((t, i) => ({ localId: `p.${i}`, name: `p${i}`, type: t }));
4402
- const fence = lowerer.deferToRuntimeFence(diagsBefore, node, {
4403
- kind: "closure",
4404
- name: () => `%fence.fn.${lowerer.liftedFns.length}`,
4405
- params,
4406
- returnType: fieldType.ret,
4407
- paramsMutable: true,
4408
- type: fieldType,
4409
- fallback: { code: "SC1090", message: "this function's body has no static lowering" },
4410
- bareMessage: true,
4411
- allowDiagSink: true,
4412
- });
4413
- if (!fence)
4414
- throw e;
4415
- return fence;
4416
- }
4417
- }
4418
- /** The union arm an object LITERAL inhabits when its fields must widen
4419
- * PER FIELD into exactly one record arm — the reducer-action pattern
4420
- * (`{ kind: "a", parsed: 1 }` into `{ kind: "a"; parsed: number | null }
4421
- * | { kind: "b" }`). The IR shapes erased the literal types tsc
4422
- * discriminated on, so the probe runs against the contextual union's
4423
- * CHECKER members: every literal field must exist on the member
4424
- * (excess-property freshness — tsc already enforced it), every arm field
4425
- * missing from the literal must be optional-flavored (an undefined-armed
4426
- * union, or a dyn slot — the absent-completion rule), and each present
4427
- * field's LITERAL type must fit the member's field type — literal against
4428
- * literal decides by VALUE (the discriminant), everything else by the
4429
- * widened IR pair under the width-lift relation. Exactly ONE fitting arm
4430
- * answers it; zero or several answer null and the caller keeps its
4431
- * fences. Plain property-assignment/shorthand literals only — spreads,
4432
- * accessors, methods, and unfoldable computed keys keep their own paths. */
4433
- function literalUnionArmOf(lowerer, expr, tsType, recordArms) {
4434
- if (!tsType.isUnionType())
4435
- return null;
4436
- const props = [];
4437
- for (const p of expr.properties) {
4438
- if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) {
4439
- props.push({ name: propNameText(lowerer, p.name), node: p.initializer });
4440
- }
4441
- else if (ts.isShorthandPropertyAssignment(p) && ts.isIdentifier(p.name)) {
4442
- props.push({ name: p.name.text, node: p.name });
4443
- }
4444
- else {
4445
- return null;
4446
- }
4447
- }
4448
- /** litT fits ftT: unions per arm; literal-vs-literal by value; unit
4449
- * types only into their own unit; otherwise the widened IR pair must be
4450
- * equal or width-liftable. */
4451
- const fits = (litT, ftT) => {
4452
- if (ftT.isUnionType())
4453
- return ts.constituentTypes(ftT).some((a) => fits(litT, a));
4454
- if (ftT.isStringLiteralType())
4455
- return litT.isStringLiteralType() && litT.value === ftT.value;
4456
- if (ftT.isNumberLiteralType())
4457
- return litT.isNumberLiteralType() && litT.value === ftT.value;
4458
- if (ftT.flags & ts.TypeFlags.BooleanLiteral) {
4459
- return (litT.flags & ts.TypeFlags.BooleanLiteral) !== 0 && lowerer.checker.typeToString(litT) === lowerer.checker.typeToString(ftT);
4460
- }
4461
- if (ftT.flags & ts.TypeFlags.Null)
4462
- return (litT.flags & ts.TypeFlags.Null) !== 0;
4463
- if (ftT.flags & ts.TypeFlags.Undefined)
4464
- return (litT.flags & ts.TypeFlags.Undefined) !== 0;
4465
- if (litT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined))
4466
- return false;
4467
- const li = lowerer.mapTypeOf(lowerer.checker.getBaseTypeOfLiteralType(litT));
4468
- const fi = lowerer.mapTypeOf(ftT);
4469
- if (!li || !fi)
4470
- return false;
4471
- return typeEquals(li, fi) || lowerer.widthLiftPlan(li, fi) !== null;
4472
- };
4473
- const armShapeIds = new Set(recordArms.map((a) => a.shapeId));
4474
- const candidates = new Set();
4475
- for (const member of ts.constituentTypes(tsType)) {
4476
- const mMapped = lowerer.mapTypeOf(member);
4477
- if (mMapped?.kind !== "record" || !armShapeIds.has(mMapped.shapeId) || candidates.has(mMapped.shapeId))
4478
- continue;
4479
- const shape = lowerer.shapes.get(mMapped.shapeId);
4480
- if (!shape || shape.tuple || shape.indexValue)
4481
- continue;
4482
- // Excess-property freshness: every literal field must exist on the
4483
- // member (tsc rejected the others for a fresh literal).
4484
- if (!props.every((p) => lowerer.checker.getPropertyOfType(member, p.name) !== undefined))
4485
- continue;
4486
- // Arm fields the literal leaves unset must be optional-flavored (the
4487
- // absent-completion rule: an undefined-armed union or a dyn slot).
4488
- const names = new Set(props.map((p) => p.name));
4489
- const absentOk = shape.fields.every((f) => {
4490
- if (names.has(f.name))
4491
- return true;
4492
- if (f.type.kind === "dyn")
4493
- return true;
4494
- if (f.type.kind !== "union")
4495
- return false;
4496
- return lowerer.unions.get(f.type.unionId)?.arms.some((a) => a.kind === "undefinedT") ?? false;
4497
- });
4498
- if (!absentOk)
4499
- continue;
4500
- const fieldsFit = props.every((p) => {
4501
- const sym = lowerer.checker.getPropertyOfType(member, p.name);
4502
- if (!sym)
4503
- return false;
4504
- return fits(lowerer.typeOf(p.node), lowerer.checker.getTypeOfSymbol(sym));
4505
- });
4506
- if (fieldsFit)
4507
- candidates.add(mMapped.shapeId);
4508
- }
4509
- if (candidates.size !== 1)
4510
- return null;
4511
- const [only] = candidates;
4512
- return recordArms.find((a) => a.shapeId === only) ?? null;
4513
- }
4514
- /** Object spread treats nullish sources as empty. Preserve evaluation once,
4515
- * then turn `Record<K, V> | undefined` into either that record or a fresh
4516
- * empty record of the same represented shape. */
4517
- function lowerOptionalIndexSpreadSource(lowerer, source, loc) {
4518
- if (source.type.kind !== "union")
4519
- return null;
4520
- const def = lowerer.unions.get(source.type.unionId);
4521
- if (!def || def.arms.length !== 2)
4522
- return null;
4523
- const undefinedTag = def.arms.findIndex((arm) => arm.kind === "undefinedT");
4524
- const recordTag = def.arms.findIndex((arm) => arm.kind === "record");
4525
- if (undefinedTag < 0 || recordTag < 0)
4526
- return null;
4527
- const recordType = def.arms[recordTag];
4528
- if (recordType.kind !== "record")
4529
- return null;
4530
- const prefix = [];
4531
- let stable = source;
4532
- if (!pureReemittable(source)) {
4533
- const local = lowerer.declareHiddenLocal("%spread", source.type);
4534
- prefix.push({ kind: "varDecl", localId: local.id, init: source, loc });
4535
- stable = varRef(local.id, source.type, loc);
4536
- }
4537
- const selected = {
4538
- kind: "ternary",
4539
- cond: { kind: "unionIsTag", unionId: source.type.unionId, tag: undefinedTag, negated: false, value: stable, type: BOOL, loc },
4540
- then: { kind: "recordLit", fields: [], type: recordType, loc },
4541
- else_: { kind: "unionNarrow", unionId: source.type.unionId, tag: recordTag, value: stable, type: recordType, loc },
4542
- type: recordType,
4543
- loc,
4544
- };
4545
- return prefix.length === 0 ? selected : { kind: "seqExpr", stmts: prefix, result: selected, type: recordType, loc };
4546
- }
4547
- export function lowerObjectLiteral(lowerer, expr) {
4548
- const loc = locOf(expr);
4549
- // The RUNTIME-KEYED literal (JS): a computed key that doesn't fold to a
4550
- // compile-time string means the literal's shape is not a compile-time
4551
- // fact — no record shape can hold it. The whole literal builds as a dyn
4552
- // object instead (`{ [field]: criteria, actual: 0, ... }` —
4553
- // test/common's _mustCallInner context), where keys are runtime string
4554
- // values. TypeScript keeps the record world and its fence: this shape
4555
- // only arises in checked-dynamic JS.
4556
- if (isJsSourceFile(expr.getSourceFile()) &&
4557
- expr.properties.some((p) => {
4558
- const n = ts.isSpreadAssignment(p) ? undefined : p.name;
4559
- return n !== undefined && ts.isComputedPropertyName(n) && literalComputedKey(lowerer, n) === null;
4560
- })) {
4561
- return lowerDynObjectLiteral(lowerer, expr);
4562
- }
4563
- // Syntax fence FIRST: unsupported member forms get their specific
4564
- // message even when they also make the literal's type unmappable
4565
- // (a getter or a `this`-returning method would otherwise surface as an
4566
- // opaque type fence on the whole literal).
4567
- for (const prop of expr.properties) {
4568
- if (ts.isSpreadAssignment(prop)) {
4569
- // Supported shapes: FULL spreads of known record shapes read as
4570
- // identifiers or other side-effect-free reads, all BEFORE any
4571
- // explicit property (the field-copy desugar reads spread fields
4572
- // first, exactly JS's eager copy order); and CONDITIONAL spreads
4573
- // `...(c ? {k: v} : {})` — the optional-field idiom tsc types as
4574
- // `k?: ...` — which desugar to one conditional field at the
4575
- // spread's own position (any position: they introduce one fresh
4576
- // name, collision-fenced below). Everything else stays fenced.
4577
- const cs = conditionalSpreadOf(prop.expression);
4578
- if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
4579
- lowerer.unsupported("SC1090", prop, "conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm — spell other shapes as optional fields)");
4580
- }
4581
- // Spread ORDER is fenced in the field-by-field desugar below, not
4582
- // here: the index-signature merge path supports any order (keyed
4583
- // last-write-wins writes), so `{ K: v, ...extra }` into a pure
4584
- // Record shape compiles — the buildServiceEnv pattern.
4585
- continue;
4586
- }
4587
- if (ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop)) {
4588
- // JavaScript CJS-export tables reach reads through the lifted-
4589
- // accessor path (cjsExportAccessorRead) — the literal VALUE
4590
- // narrows to its plain fields below (accessor names are not
4591
- // record storage; SEMANTICS.md documents the enumeration
4592
- // divergence). TypeScript accessors lower into the shape's
4593
- // reserved closure slots (%get:/%set: — see accessorSlotProp);
4594
- // `this` in the body is fenced up front: the closure slot has no
4595
- // receiver (capturing the record under construction would be an
4596
- // RC cycle), and the generic lexical-this walk would silently
4597
- // capture an ENCLOSING method's `this` — the object-method rule.
4598
- if (!isJsSourceFile(expr.getSourceFile())) {
4599
- rejectThisInObjectAccessor(lowerer, prop.body ?? prop);
4600
- }
4601
- }
4602
- // Unfoldable computed keys are NOT fenced here: an index-signature
4603
- // target lowers them as runtime keyed writes (the merge path below);
4604
- // every other target re-fences them after the merge path declined
4605
- // (fenceUnfoldableComputedKeys).
4606
- // Identifier keys, STRING-LITERAL keys (`"content-type": v` —
4607
- // record field names are data, never C identifiers; the mangler
4608
- // encodes what C can't spell), NUMERIC-LITERAL keys in their
4609
- // canonical string spelling (`{ 0: v }` stores "0", `{ 0x10: v }`
4610
- // stores "16" — JS's ToPropertyKey; the checker names the property
4611
- // symbol the same way), and FOLDABLE computed keys (above).
4612
- if (!prop.name ||
4613
- !(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ||
4614
- ts.isNumericLiteral(prop.name) || ts.isComputedPropertyName(prop.name))) {
4615
- lowerer.unsupported("SC1090", prop, "non-identifier property names");
4616
- }
4617
- // Shorthand methods are just closure-valued fields — but their `this`
4618
- // is dynamically bound (typed as the literal by tsc, so noImplicitThis
4619
- // lets it through) and records don't model it; the generic
4620
- // lexical-this walk would silently capture an ENCLOSING method's
4621
- // `this`, so any `this` in the body is rejected up front.
4622
- if (ts.isMethodDeclaration(prop))
4623
- lowerer.rejectThisInObjectMethod(prop.body ?? prop);
4624
- }
4625
- let tsType = lowerer.checker.getContextualType(expr) ?? lowerer.typeOf(expr);
4626
- // `lit satisfies T` is TYPE-LEVEL only: the expression's checker type —
4627
- // and therefore the shape every downstream consumer sees — is the
4628
- // literal's OWN type (T still contextually types members, so inferred
4629
- // parameter types flow). Building at T would reshape the value tsc
4630
- // says has the literal's type: `{...} satisfies Movable &
4631
- // Record<string, unknown>` must NOT become an index-signature record.
4632
- // Own type wins whenever it maps to a record; an unmappable own type
4633
- // keeps the contextual fallback (a bare-null field whose satisfies
4634
- // target names the wider slot type).
4635
- {
4636
- let p = expr.parent;
4637
- while (ts.isParenthesizedExpression(p))
4638
- p = p.parent;
4639
- if (ts.isSatisfiesExpression(p)) {
4640
- const own = lowerer.typeOf(expr);
4641
- if (lowerer.mapTypeOf(own)?.kind === "record")
4642
- tsType = own;
4643
- }
4644
- }
4645
- // An async function's return position types the literal
4646
- // `T | PromiseLike<T>` (the lib's await-unwrapping contract). The
4647
- // PromiseLike arm never maps, and the record the return slot actually
4648
- // holds is exactly the checker's awaited type — strip to it BEFORE the
4649
- // own-type fallback below: the awaited contextual type carries the
4650
- // slot's field types (`lanIp: string | null`), which the literal's own
4651
- // type narrows away (a field written as `lanIp: null` types as bare
4652
- // `null`, which maps to nothing on its own).
4653
- if (tsType.isUnionType() && ts.constituentTypes(tsType).some((t) => t.getSymbol()?.name === "PromiseLike")) {
4654
- tsType = lowerer.checker.getAwaitedType(tsType) ?? tsType;
4655
- }
4656
- let mapped = lowerer.mapTypeOf(tsType);
4657
- // An EMPTY-record context under a NON-empty literal (`Object.keys({
4658
- // ...process.env })` — the lib's `{}`-typed parameters admit every
4659
- // object): `{}` carries no shape information, so the literal builds as
4660
- // its OWN type, exactly the unmappable-context fallback below. An
4661
- // empty LITERAL keeps the context (the shapes agree).
4662
- if (mapped?.kind === "record" && expr.properties.length > 0) {
4663
- const ctxShape = lowerer.shapes.get(mapped.shapeId);
4664
- if (ctxShape && ctxShape.fields.length === 0 && !ctxShape.indexValue && !ctxShape.tuple) {
4665
- mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped;
4666
- }
4667
- }
4668
- // A literal with a property the contextual TYPE ITSELF lacks — only
4669
- // reachable through type assertions and satisfies (fresh-literal
4670
- // excess-property checks reject the direct spelling): the context
4671
- // cannot hold the value, so the literal builds at its OWN type and the
4672
- // slot's width coercion narrows it (divergence 36's copy stance — the
4673
- // extra fields drop in the copy). The probe is against the CHECKER's
4674
- // contextual type, not the mapped shape: a property the contextual
4675
- // type carries but the shape dropped (settled value/reason,
4676
- // generic-callable members) belongs to the drop paths below, and
4677
- // index-signature contexts keep every key (overflow capture).
4678
- if (mapped?.kind === "record" && expr.properties.length > 0) {
4679
- const ctxShape = lowerer.shapes.get(mapped.shapeId);
4680
- if (ctxShape && !ctxShape.indexValue && !ctxShape.tuple) {
4681
- const names = new Set(ctxShape.fields.map((f) => f.name));
4682
- const ctxHasProp = (name) => {
4683
- const members = tsType.isUnionType() ? ts.constituentTypes(tsType) : [tsType];
4684
- return members.some((m) => lowerer.checker.getPropertyOfType(m, name) !== undefined);
4685
- };
4686
- const extraOf = (text) => !names.has(text) && !ctxHasProp(text);
4687
- const extra = expr.properties.some((p) => {
4688
- if (ts.isSpreadAssignment(p) || !p.name)
4689
- return false;
4690
- if (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))
4691
- return extraOf(p.name.text);
4692
- if (ts.isNumericLiteral(p.name))
4693
- return extraOf(String(Number(p.name.text)));
4694
- return false; // computed keys keep their existing paths
4695
- });
4696
- if (extra)
4697
- mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped;
4698
- }
4699
- }
4700
- // A union-typed slot (`const r: Res = { kind: "ok", ... }`) contextually
4701
- // types the literal as the WHOLE union; build the literal as its own
4702
- // (arm) shape and let the slot's coercion wrap it into the union. An
4703
- // unknown-typed slot (`JSON.stringify({ a: 1 })`) likewise. An
4704
- // UNMAPPABLE context falls back the same way — the literal's own type
4705
- // is the plain record the slot coerces. A CLASS-INSTANCE context
4706
- // (`const p: Point = { x, y }` — tsc's structural view of data
4707
- // classes) falls back too: the literal builds as its own record and
4708
- // the slot's width coercion constructs through the trivial
4709
- // parameter-property constructor (recordToClassPlan), or fences with
4710
- // the record-shape story.
4711
- // A jsval-mapped context reaches here only when lowerExpr's island
4712
- // gate DECLINED it (a project-declared typedef that absorbed to the
4713
- // island through checker-`any` field residue): the literal builds at
4714
- // its own type like every unmappable context.
4715
- if (mapped === null || mapped.kind === "union" || mapped.kind === "dyn" || mapped.kind === "object" || mapped.kind === "jsval") {
4716
- const ctxUnion = mapped?.kind === "union" ? mapped : null;
4717
- mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped;
4718
- // A literal whose own shape re-tags into NO arm of the contextual
4719
- // union, where the union has exactly ONE record arm: build AS that
4720
- // arm — there is no ambiguity (tsc already checked the literal
4721
- // against the union, and the record arm is the only shape it can
4722
- // inhabit), and the arm's field types drive every property's
4723
- // coercion (`{ env: {...spread...}, onCleanup }` against an
4724
- // optional options param — the env value builds by ITS contextual
4725
- // index-signature type and wraps into the arm's `| undefined`
4726
- // field, where the literal's own inferred width would mismatch).
4727
- // Empty literals (`_env = {}` — the fieldless own shape) and
4728
- // literals against PURE index-signature arms (`{ OPENSSL_CONF:
4729
- // candidate }` — keys become overflow entries) are the same rule's
4730
- // simplest cases. The empty-array-in-union rule, record form.
4731
- if (ctxUnion) {
4732
- const def = lowerer.unions.get(ctxUnion.unionId);
4733
- const recordArms = def?.arms.filter((a) => a.kind === "record") ?? [];
4734
- if (recordArms.length === 1) {
4735
- const armShape = lowerer.shapes.get(recordArms[0].shapeId);
4736
- if ((mapped?.kind !== "record" || mapped.shapeId !== recordArms[0].shapeId) &&
4737
- !armShape?.tuple) {
4738
- mapped = recordArms[0];
4739
- }
4740
- }
4741
- else if (recordArms.length > 1) {
4742
- const ownShapeId = mapped?.kind === "record" ? mapped.shapeId : null;
4743
- // SEVERAL record arms (the reducer-action / discriminated-message
4744
- // pattern): the literal's own inferred shape re-tags into no arm
4745
- // — its field types widened per field (`parsed: 1` against
4746
- // `parsed: number | null`), so the IR-level candidate probe is
4747
- // ambiguous (a narrower arm also admits the literal by dropping
4748
- // fields). The LITERAL types tsc checked carry the discriminant
4749
- // the shapes erased: match the literal's fields against each
4750
- // union member's CHECKER types (literal-vs-literal field pairs
4751
- // decide by value — the `kind: "a"` discriminant), and when
4752
- // exactly ONE member fits, build AS that arm — its field types
4753
- // drive every property's coercion, exactly the single-record-arm
4754
- // rule above. Ambiguous literals keep the SC2003 fence.
4755
- if (ownShapeId === null || !recordArms.some((a) => a.shapeId === ownShapeId)) {
4756
- const arm = literalUnionArmOf(lowerer, expr, tsType, recordArms);
4757
- if (arm)
4758
- mapped = arm;
4759
- }
4760
- }
4761
- }
4762
- }
4763
- // The CJS EXPORT-TABLE literal in VALUE position (JS): importers reach
4764
- // every member through alias plumbing and accessor lifts — the record
4765
- // VALUE exists for the module's own reads (Object.keys, internal
4766
- // member reads), so it keeps exactly the plain fields whose values
4767
- // lower; accessor entries and members with no value representation
4768
- // (rest-param function types) narrow away. SEMANTICS.md documents the
4769
- // enumeration divergence.
4770
- if (isJsSourceFile(expr.getSourceFile()) &&
4771
- (!mapped || expr.properties.some((p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p))) &&
4772
- isCjsExportTableLiteral(expr)) {
4773
- const fields = [];
4774
- for (const prop of expr.properties) {
4775
- if (ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop))
4776
- continue;
4777
- const propName = ts.isSpreadAssignment(prop) ? undefined : prop.name;
4778
- if (!propName || !(ts.isIdentifier(propName) || ts.isStringLiteral(propName)))
4779
- continue;
4780
- const name = propName.text;
4781
- let v = null;
4782
- if (ts.isShorthandPropertyAssignment(prop)) {
4783
- v = probeLower(lowerer, propName);
4784
- }
4785
- else if (ts.isPropertyAssignment(prop)) {
4786
- v = probeLower(lowerer, prop.initializer);
4787
- }
4788
- if (!v || v.type.kind === "void" || v.type.kind === "caught" || v.type.kind === "jsval")
4789
- continue;
4790
- if (isUnitType(v.type))
4791
- continue;
4792
- fields.push({ name, value: v });
4793
- }
4794
- // Canonical (sorted) field order — the shape registry's invariant;
4795
- // the dropped reads are all pure, so reordering loses nothing.
4796
- fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
4797
- const shapeId = lowerer.shapes.intern(fields.map((f) => ({ name: f.name, type: f.value.type })));
4798
- return { kind: "recordLit", fields, type: { kind: "record", shapeId }, loc };
4799
- }
4800
- // The JS declaration fallback, literal-side (the checked-dynamic tree-array rule's
4801
- // object form): a literal whose shape has no static home — an
4802
- // unmappable contextual type over unmappable own fields (the
4803
- // PropertyDescriptorMap argument of Object.defineProperties, nested
4804
- // descriptor records with `any` values) — builds as a dyn OBJECT:
4805
- // each field converts through the usual dyn boundary, and dynamic
4806
- // consumers ride the keyed-dyn paths. TypeScript keeps the fence.
4807
- if ((!mapped || mapped.kind === "dyn") && isJsSourceFile(expr.getSourceFile())) {
4808
- return lowerDynObjectLiteral(lowerer, expr);
4809
- }
4810
- if (!mapped || mapped.kind !== "record")
4811
- lowerer.badType(expr, tsType);
4812
- let type = mapped;
4813
- let shape = lowerer.shapes.get(type.shapeId);
4814
- // ACCESSOR properties, JS literals only (TS accessors fill the shape's
4815
- // %get:/%set: closure slots below): no record storage exists for them,
4816
- // so the literal's shape NARROWS to its plain fields (reads resolve
4817
- // through the lifted accessors; Object.keys over the value omits
4818
- // accessor names — the documented divergence).
4819
- if (isJsSourceFile(expr.getSourceFile())) {
4820
- const accessorNames = new Set(expr.properties
4821
- .filter((p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p))
4822
- .map((p) => (p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) ? p.name.text : ""))
4823
- .filter((n) => n !== ""));
4824
- if (accessorNames.size > 0) {
4825
- const narrowed = shape.fields.filter((f) => !accessorNames.has(f.name));
4826
- const narrowedId = lowerer.shapes.intern(narrowed.map((f) => ({ name: f.name, type: f.type })), false, shape.indexValue);
4827
- type = { kind: "record", shapeId: narrowedId };
4828
- shape = lowerer.shapes.get(narrowedId);
4829
- }
4830
- }
4831
- const fieldTypes = new Map(shape.fields.map((f) => [f.name, f.type]));
4832
- // File-scope JavaScript object bindings live in the checked-dynamic tree
4833
- // to preserve open writes and identity. Build an optional-field literal
4834
- // from its actually written keys so Object.hasOwn can distinguish an
4835
- // omitted property from a present property whose value is undefined.
4836
- const topLevelJsDecl = ts.isVariableDeclaration(expr.parent) &&
4837
- expr.parent.initializer === expr &&
4838
- ts.isVariableStatement(expr.parent.parent.parent) &&
4839
- ts.isSourceFile(expr.parent.parent.parent.parent);
4840
- const assignedProperties = expr.properties.filter((prop) => ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop));
4841
- if (topLevelJsDecl && isJsSourceFile(expr.getSourceFile()) &&
4842
- assignedProperties.length === expr.properties.length) {
4843
- const provided = new Set(assignedProperties.map((prop) => propNameText(lowerer, prop.name)));
4844
- const omittedOptional = shape.fields.some((field) => !provided.has(field.name) && field.type.kind === "union" &&
4845
- lowerer.armTag(field.type.unionId, UNDEFINED_T) >= 0);
4846
- if (omittedOptional)
4847
- return lowerDynObjectLiteral(lowerer, expr);
4848
- }
4849
- // The clone probe may lower the leading spread before declining (a
4850
- // static `as` cast can make the checker-visible shape match while its
4851
- // erased IR value keeps a wider shape). Reuse that exact value in the
4852
- // ordinary spread path so lowering still happens once.
4853
- let leadingSpreadLowered = null;
4854
- // The overwhelmingly common immutable-update form over a record:
4855
- // `{ ...model, changed, other: value }`. The historic lowering expanded
4856
- // the spread into one recordGet per untouched field at EVERY site, then
4857
- // the backends inlined all of those retains and stores into the caller.
4858
- // A 178-field model updated hundreds of times consequently produced a
4859
- // half-million-line LLVM function. Keep the same evaluation/ownership
4860
- // semantics in one compact IR node: source first, explicit overrides in
4861
- // source order, and one backend clone helper per shape.
4862
- //
4863
- // Stay deliberately narrow. Tuple/index/accessor shapes, conditional or
4864
- // multiple spreads, spread-after-explicit order, and shape-changing width
4865
- // copies retain their existing lowering and diagnostics.
4866
- if (!isJsSourceFile(expr.getSourceFile()) &&
4867
- !shape.indexValue &&
4868
- !shape.tuple &&
4869
- !shapeHasAccessorSlots(shape) &&
4870
- expr.properties.length >= 2 &&
4871
- ts.isSpreadAssignment(expr.properties[0]) &&
4872
- !conditionalSpreadOf(expr.properties[0].expression) &&
4873
- expr.properties.slice(1).every((p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
4874
- p.name !== undefined &&
4875
- (ts.isIdentifier(p.name) ||
4876
- ts.isStringLiteral(p.name) ||
4877
- ts.isNumericLiteral(p.name) ||
4878
- (ts.isComputedPropertyName(p.name) && literalComputedKey(lowerer, p.name) !== null)))) {
4879
- const spread = expr.properties[0];
4880
- const props = expr.properties.slice(1);
4881
- const names = props.map((p) => propNameText(lowerer, p.name));
4882
- const unique = new Set(names);
4883
- const sourceType = lowerer.mapTypeOf(lowerer.typeOf(spread.expression));
4884
- if (sourceType?.kind === "record" &&
4885
- sourceType.shapeId === type.shapeId &&
4886
- unique.size === names.length &&
4887
- names.every((name) => fieldTypes.has(name))) {
4888
- const source = lowerer.lowerExpr(spread.expression);
4889
- if (source.type.kind === "record" && source.type.shapeId === type.shapeId) {
4890
- const overrides = [];
4891
- for (let i = 0; i < props.length; i++) {
4892
- const p = props[i];
4893
- const name = names[i];
4894
- const fieldType = fieldTypes.get(name);
4895
- const valueNode = ts.isPropertyAssignment(p) ? p.initializer : p;
4896
- let value = ts.isPropertyAssignment(p)
4897
- ? lowerer.lowerExpr(p.initializer)
4898
- : lowerer.lowerShorthandValue(p);
4899
- value = lowerer.coerceInto(valueNode, value, fieldType);
4900
- if (!typeEquals(value.type, fieldType))
4901
- lowerer.badType(valueNode, lowerer.typeOf(valueNode));
4902
- overrides.push({ name, value });
4903
- }
4904
- return { kind: "recordClone", source, overrides, type, loc };
4905
- }
4906
- leadingSpreadLowered = source;
4907
- }
4908
- }
4909
- // A PURE index-signature target with spreads — `{ ...process.env }`,
4910
- // `{ ...process.env, ...extraEnv }`, `{ ...env, PATH: p }` (the
4911
- // spawn-env pattern): the field-by-field desugar below cannot
4912
- // enumerate runtime overflow keys, so the literal lowers as ONE
4913
- // interned merge-helper call — contributors apply in literal order
4914
- // with keyed writes (JS last-write-wins), sources and values evaluate
4915
- // once each in source order (the call's argument order). A CONDITIONAL
4916
- // spread `...(c ? { k: v } : {})` contributes its one key as a ternary
4917
- // — cond once, v lazily, the empty arm holding the value slot's
4918
- // undefined arm (the explicit-undefined-is-absent stance: JSON and
4919
- // child-env builders drop it, exactly Node's absent key); targets
4920
- // with declared fields keep the historic desugar below.
4921
- const isRuntimeComputedKey = (p) => !ts.isSpreadAssignment(p) &&
4922
- p.name !== undefined &&
4923
- ts.isComputedPropertyName(p.name) &&
4924
- literalComputedKey(lowerer, p.name) === null;
4925
- if (shape.indexValue &&
4926
- shape.fields.length === 0 &&
4927
- !shape.tuple &&
4928
- (expr.properties.some((p) => ts.isSpreadAssignment(p)) || expr.properties.some(isRuntimeComputedKey))) {
4929
- const contributors = [];
4930
- let mergeable = true;
4931
- for (const prop of expr.properties) {
4932
- if (ts.isSpreadAssignment(prop)) {
4933
- const cs = conditionalSpreadOf(prop.expression);
4934
- if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
4935
- lowerer.unsupported("SC1090", prop, "conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm)");
4936
- }
4937
- if (cs) {
4938
- const absent = lowerer.wrappedUndefined(shape.indexValue, locOf(prop));
4939
- if (!absent) {
4940
- 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)`);
4941
- }
4942
- const csProp = cs.props[0];
4943
- const cond = lowerer.lowerCondition(cs.cond);
4944
- const vNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp;
4945
- const v = lowerer.intoIndexValueSlot(ts.isPropertyAssignment(csProp) ? lowerer.lowerExpr(csProp.initializer) : lowerer.lowerShorthandValue(csProp), shape.indexValue, vNode);
4946
- contributors.push({
4947
- kind: "field",
4948
- name: csProp.name.text,
4949
- value: {
4950
- kind: "ternary",
4951
- cond,
4952
- then: cs.whenTrue ? v : absent,
4953
- else_: cs.whenTrue ? absent : v,
4954
- type: shape.indexValue,
4955
- loc: locOf(prop),
4956
- },
4957
- });
4958
- continue;
4959
- }
4960
- let src = lowerer.lowerExpr(prop.expression);
4961
- src = lowerOptionalIndexSpreadSource(lowerer, src, locOf(prop)) ?? src;
4962
- if (src.type.kind !== "record") {
4963
- lowerer.unsupported("SC1090", prop, `object spread of '${lowerer.fmt(src.type)}' sources into an index-signature shape (only index-signature records merge — ${NARROW_FIRST})`);
4964
- }
4965
- fenceAccessorSpreadSource(lowerer, prop, lowerer.shapes.get(src.type.shapeId));
4966
- contributors.push({ kind: "spread", shapeId: src.type.shapeId, value: src });
4967
- continue;
4968
- }
4969
- if ((ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) &&
4970
- prop.name &&
4971
- (ts.isIdentifier(prop.name) ||
4972
- ts.isStringLiteral(prop.name) ||
4973
- ts.isNumericLiteral(prop.name) ||
4974
- (ts.isComputedPropertyName(prop.name) && literalComputedKey(lowerer, prop.name) !== null))) {
4975
- const value = ts.isPropertyAssignment(prop)
4976
- ? lowerer.intoIndexValueSlot(lowerer.lowerExpr(prop.initializer), shape.indexValue, prop.initializer)
4977
- : lowerer.intoIndexValueSlot(lowerer.lowerShorthandValue(prop), shape.indexValue, prop);
4978
- contributors.push({ kind: "field", name: propNameText(lowerer, prop.name), value });
4979
- continue;
4980
- }
4981
- // A RUNTIME-keyed property (`{ ...m, ["a" + "b"]: "" }`, `{ [K]:
4982
- // v }` where K is a runtime string): the key evaluates before its
4983
- // value (JS's per-property order — both are helper arguments) and
4984
- // writes through the signature exactly like a spread's keys.
4985
- // Number/boolean/unknown keys stringify (ToPropertyKey).
4986
- if (ts.isPropertyAssignment(prop) && ts.isComputedPropertyName(prop.name)) {
4987
- let k = lowerer.lowerExpr(prop.name.expression);
4988
- if (k.type.kind === "f64" || k.type.kind === "bool" || k.type.kind === "dyn") {
4989
- k = { kind: "toString", operand: k, type: STRING, loc: locOf(prop.name) };
4990
- }
4991
- if (k.type.kind !== "string") {
4992
- lowerer.unsupported("SC1090", prop.name, `'${lowerer.fmt(k.type)}'-typed computed property keys (string, number, boolean, and unknown keys stringify)`);
4993
- }
4994
- const value = lowerer.intoIndexValueSlot(lowerer.lowerExpr(prop.initializer), shape.indexValue, prop.initializer);
4995
- contributors.push({ kind: "keyedField", key: k, value });
4996
- continue;
4997
- }
4998
- mergeable = false;
4999
- break;
5000
- }
5001
- if (mergeable) {
5002
- const helper = lowerIndexMergeHelper(lowerer, type.shapeId, contributors, loc);
5003
- if (helper === null) {
5004
- 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)`);
5005
- }
5006
- return {
5007
- kind: "call",
5008
- callee: helper,
5009
- args: contributors.flatMap((c) => (c.kind === "keyedField" ? [c.key, c.value] : [c.value])),
5010
- type,
5011
- loc,
5012
- };
5013
- }
5014
- }
5015
- // Unfoldable computed keys outside the index-signature merge path:
5016
- // no record shape can hold a runtime-decided name.
5017
- for (const prop of expr.properties) {
5018
- if (isRuntimeComputedKey(prop)) {
5019
- 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)");
5020
- }
5021
- }
5022
- // A DECLARED-fields target built ENTIRELY from index-signature spreads —
5023
- // `{ ...Object.fromEntries(...), ...Object.fromEntries(...) }` typed
5024
- // AppConfig (the defaults-merge idiom over runtime-keyed sources): the
5025
- // field-by-field desugar cannot enumerate runtime keys, so the literal
5026
- // lowers as ONE interned merge-helper call — sources evaluate once each
5027
- // in source order (the call's argument order — computed sources
5028
- // included, no re-read), contributors apply in order with per-key
5029
- // dispatch onto the declared fields (JS last-write-wins). Divergence 68
5030
- // has the runtime rules (validated collisions, extra keys dropped).
5031
- if (!shape.indexValue &&
5032
- !shape.tuple &&
5033
- shape.fields.length > 0 &&
5034
- expr.properties.length > 0 &&
5035
- expr.properties.every((p) => ts.isSpreadAssignment(p) && !conditionalSpreadOf(p.expression)) &&
5036
- expr.properties.some((p) => {
5037
- const t = lowerer.mapTypeOf(lowerer.typeOf(p.expression));
5038
- return t?.kind === "record" && !!lowerer.shapes.get(t.shapeId)?.indexValue;
5039
- })) {
5040
- return lowerDeclaredSpreadMerge(lowerer, expr, type, shape, loc);
5041
- }
5042
- const shapeMismatch = (node) => {
5043
- const own = lowerer.mapTypeOf(lowerer.typeOf(expr));
5044
- lowerer.pushDiag(recordShapeMismatchDiag(lowerer.fmt(type), own ? lowerer.fmt(own) : lowerer.checker.typeToString(lowerer.typeOf(expr)), locOf(node), own?.kind === "record" && type.kind === "record"
5045
- ? (lowerer.describeRecordWidthBlocker(own.shapeId, type.shapeId) ?? undefined)
5046
- : undefined));
5047
- throw new PoisonError();
5048
- };
5049
- const fields = [];
5050
- // Field names introduced by conditional spreads: their ternary carries
5051
- // the spread's whole evaluation (cond once, value lazily), so a LATER
5052
- // contributor overriding one would silently drop that evaluation —
5053
- // collisions fence in both directions.
5054
- const conditionalNames = new Set();
5055
- // Member names DROPPED from the shape (JS deferral — unlowerable pure
5056
- // member reads narrow away; see the catch in the property loop).
5057
- const droppedNames = new Set();
5058
- for (const prop of expr.properties) {
5059
- if (ts.isSpreadAssignment(prop)) {
5060
- const cs = conditionalSpreadOf(prop.expression);
5061
- if (cs && cs !== "unsupported") {
5062
- // `...(c ? { k: v } : {})` — ONE conditional field at the
5063
- // spread's own position: cond evaluates exactly once, `v` only
5064
- // when the non-empty arm is taken (ternary arms are lazy), and
5065
- // the empty arm holds the interned undefined arm — exactly the
5066
- // omitted-optional-field representation, which is also why the
5067
- // target field must be optional (tsc types the idiom that way).
5068
- const csProp = cs.props[0]; // single-prop-checked in the fence pass
5069
- const name = csProp.name.text;
5070
- const fieldType = fieldTypes.get(name);
5071
- if (!fieldType) {
5072
- if (shape.indexValue) {
5073
- lowerer.unsupported("SC1090", prop, "conditional spreads into index-signature keys (overflow entries model presence — write the key in an if statement instead)");
5074
- }
5075
- throw shapeMismatch(prop);
5076
- }
5077
- const absent = lowerer.wrappedUndefined(fieldType, locOf(prop));
5078
- if (!absent) {
5079
- lowerer.unsupported("SC1090", prop, `conditional spreads onto the required field '${name}' (the empty arm leaves it undefined — declare the field optional)`);
5080
- }
5081
- if (fields.some((f) => f.name === name)) {
5082
- 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)`);
5083
- }
5084
- const cond = lowerer.lowerCondition(cs.cond);
5085
- const valueNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp;
5086
- let v = ts.isPropertyAssignment(csProp)
5087
- ? lowerer.lowerExpr(csProp.initializer)
5088
- : lowerer.lowerShorthandValue(csProp);
5089
- v = lowerer.coerceInto(valueNode, v, fieldType);
5090
- if (!typeEquals(v.type, fieldType))
5091
- lowerer.badType(valueNode, lowerer.typeOf(valueNode));
5092
- conditionalNames.add(name);
5093
- fields.push({
5094
- name,
5095
- value: {
5096
- kind: "ternary",
5097
- cond,
5098
- then: cs.whenTrue ? v : absent,
5099
- else_: cs.whenTrue ? absent : v,
5100
- type: fieldType,
5101
- loc: locOf(prop),
5102
- },
5103
- });
5104
- continue;
5105
- }
5106
- // `{ ...base, ... }` — field-by-field copy of a known record
5107
- // shape. Every source field must land on the target shape with an
5108
- // equal type (a wider source would silently DROP fields JS keeps —
5109
- // the width fence, same as literals). Later contributors override
5110
- // earlier ones (JS last-write-wins; the reads are side-effect-free,
5111
- // so dropping the earlier read is exact). Identifier sources
5112
- // re-read per field (historic path); any OTHER source must be a
5113
- // re-emittable pure read, sharing one lowered node per field.
5114
- // The desugar's one-entry-per-name list reads spread fields
5115
- // EAGERLY at the spread's position, so an explicit property
5116
- // BEFORE a spread would need JS's overwrite — order-fenced (the
5117
- // index-signature merge path above takes any order).
5118
- if (expr.properties
5119
- .slice(0, expr.properties.indexOf(prop))
5120
- .some((p) => !ts.isSpreadAssignment(p))) {
5121
- 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)");
5122
- }
5123
- let srcNode = prop.expression;
5124
- while (ts.isParenthesizedExpression(srcNode))
5125
- srcNode = srcNode.expression;
5126
- const srcLowered = prop === expr.properties[0] && leadingSpreadLowered !== null
5127
- ? leadingSpreadLowered
5128
- : ts.isIdentifier(srcNode)
5129
- ? null
5130
- : lowerer.lowerExpr(srcNode);
5131
- const srcType = srcLowered ? srcLowered.type : lowerer.mapTypeOf(lowerer.typeOf(srcNode));
5132
- // `...options.installConfig` — a spread of `Partial<X> | undefined`
5133
- // (the optional-options merge idiom `{ ...DEFAULTS, ...overrides }`):
5134
- // JS spreads nothing for the unit arm and copies present keys
5135
- // otherwise. Per target field the desugar builds
5136
- // `present ? extracted : earlier` — present tests the source's
5137
- // record tag AND (optional source fields) the field's own value
5138
- // arm; absent keeps the earlier contributor's value (both reads
5139
- // are pure, so the reorder into the ternary is unobservable).
5140
- if (srcType?.kind === "union") {
5141
- const def = lowerer.unions.get(srcType.unionId);
5142
- const recArms = def?.arms.filter((a) => a.kind === "record") ?? [];
5143
- if (!def ||
5144
- recArms.length !== 1 ||
5145
- !def.arms.every((a) => a.kind === "record" || isUnitType(a))) {
5146
- lowerer.unsupported("SC1090", prop, `object spread of '${lowerer.fmt(srcType)}' sources (only known record shapes spread — ${NARROW_FIRST})`);
5147
- }
5148
- if (srcLowered && !pureReemittable(srcLowered)) {
5149
- lowerer.unsupported("SC1090", prop, "object spread of computed sources (the field copies re-read the source — bind it to a const first)");
5150
- }
5151
- const recArm = recArms[0];
5152
- const recTag = def.arms.indexOf(recArm);
5153
- // A checked-dynamic VALUE under the union-mapped checker type
5154
- // (a JS dyn-holding binding): the present/absent desugar tests
5155
- // union tags a dyn box does not carry — fence honestly instead
5156
- // of the validator's ICE.
5157
- const probedSrc = srcLowered ?? probeLower(lowerer, srcNode);
5158
- if (probedSrc?.type.kind === "dyn") {
5159
- lowerer.unsupported("SC1090", prop, `object spread of a checked-dynamic '${lowerer.fmt(srcType)}' source (${NARROW_FIRST})`);
5160
- }
5161
- const srcShape = lowerer.shapes.get(recArm.shapeId);
5162
- if (!srcShape)
5163
- throw new InternalCompilerError(`lowerer bug: spread of unknown shape ${recArm.shapeId}`);
5164
- fenceAccessorSpreadSource(lowerer, prop, srcShape);
5165
- if (srcShape.indexValue || shape.indexValue) {
5166
- lowerer.unsupported("SC1090", prop, "object spread involving index-signature shapes (overflow keys are runtime state — copy the fields you need explicitly)");
5167
- }
5168
- const laterNames = new Set();
5169
- for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
5170
- if (ts.isSpreadAssignment(later)) {
5171
- if (conditionalSpreadOf(later.expression))
5172
- continue;
5173
- const lt = lowerer.mapTypeOf(lowerer.typeOf(later.expression));
5174
- if (lt?.kind === "record") {
5175
- for (const lf of lowerer.shapes.get(lt.shapeId)?.fields ?? [])
5176
- laterNames.add(lf.name);
5177
- }
5178
- continue;
5179
- }
5180
- if (later.name &&
5181
- (ts.isIdentifier(later.name) ||
5182
- ts.isStringLiteral(later.name) ||
5183
- ts.isNumericLiteral(later.name) ||
5184
- (ts.isComputedPropertyName(later.name) && literalComputedKey(lowerer, later.name) !== null))) {
5185
- laterNames.add(propNameText(lowerer, later.name));
5186
- }
5187
- }
5188
- const srcRef = () => srcLowered ?? lowerer.lowerExpr(srcNode);
5189
- for (const f of srcShape.fields) {
5190
- if (laterNames.has(f.name))
5191
- continue;
5192
- const targetType = fieldTypes.get(f.name);
5193
- // No slot on the target shape: the copy DROPS the field
5194
- // (divergence 36's stance, same as the plain-record spread).
5195
- if (!targetType)
5196
- continue;
5197
- if (conditionalNames.has(f.name)) {
5198
- 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)`);
5199
- }
5200
- const fRead = () => ({
5201
- kind: "recordGet",
5202
- obj: { kind: "unionNarrow", unionId: srcType.unionId, tag: recTag, value: srcRef(), type: recArm, loc: locOf(prop) },
5203
- shapeId: recArm.shapeId,
5204
- field: f.name,
5205
- type: f.type,
5206
- loc: locOf(prop),
5207
- });
5208
- let cond = { kind: "unionIsTag", unionId: srcType.unionId, tag: recTag, negated: false, value: srcRef(), type: BOOL, loc: locOf(prop) };
5209
- let thenVal;
5210
- if (typeEquals(f.type, targetType)) {
5211
- thenVal = fRead();
5212
- }
5213
- else if (f.type.kind === "union" &&
5214
- lowerer.armTag(f.type.unionId, UNDEFINED_T) >= 0 &&
5215
- typeEquals(lowerer.stripUndefinedArm(f.type), targetType)) {
5216
- // Optional source field into a required target slot: present
5217
- // means the source holds the record AND the field its value
5218
- // arm — the spread-override completion, union-source form.
5219
- const ftUndef = lowerer.armTag(f.type.unionId, UNDEFINED_T);
5220
- cond = {
5221
- kind: "logical",
5222
- op: "&&",
5223
- left: cond,
5224
- right: { kind: "unionIsTag", unionId: f.type.unionId, tag: ftUndef, negated: true, value: fRead(), type: BOOL, loc: locOf(prop) },
5225
- type: BOOL,
5226
- loc: locOf(prop),
5227
- };
5228
- const ftDef = lowerer.unions.get(f.type.unionId);
5229
- if (targetType.kind === "union") {
5230
- const retag = lowerer.unionRetagHelper(f.type.unionId, targetType.unionId, locOf(prop));
5231
- if (!retag) {
5232
- 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`));
5233
- throw new PoisonError();
5234
- }
5235
- thenVal = { kind: "call", callee: retag, args: [fRead()], type: targetType, loc: locOf(prop) };
5236
- }
5237
- else if (ftDef && ftDef.arms.length === 2 && lowerer.armTag(f.type.unionId, targetType) >= 0) {
5238
- thenVal = { kind: "unionNarrow", unionId: f.type.unionId, tag: lowerer.armTag(f.type.unionId, targetType), value: fRead(), type: targetType, loc: locOf(prop) };
5239
- }
5240
- else {
5241
- 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`));
5242
- throw new PoisonError();
5243
- }
5244
- }
5245
- else {
5246
- // The width-lift fallback (arm wrap, re-tag, nested
5247
- // reshape) — the same per-field rule the slot coercion
5248
- // applies. Runs AFTER the optional-completion branch: a
5249
- // present-test has its own semantics a re-tag's stranded
5250
- // undefined trap must not shadow.
5251
- const lift = lowerer.widthLiftPlan(f.type, targetType);
5252
- if (!lift) {
5253
- 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)}'`));
5254
- throw new PoisonError();
5255
- }
5256
- thenVal = lowerer.applyWidthLift(lift, fRead(), targetType, locOf(prop));
5257
- }
5258
- const at = fields.findIndex((x) => x.name === f.name && !x.drop);
5259
- const elseVal = at >= 0 ? fields[at].value : lowerer.wrappedUndefined(targetType, locOf(prop));
5260
- if (!elseVal) {
5261
- 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 })`);
5262
- }
5263
- const merged = { kind: "ternary", cond, then: thenVal, else_: elseVal, type: targetType, loc: locOf(prop) };
5264
- if (at >= 0)
5265
- fields[at] = { name: f.name, value: merged };
5266
- else
5267
- fields.push({ name: f.name, value: merged });
5268
- }
5269
- continue;
5270
- }
5271
- if (srcType?.kind !== "record") {
5272
- 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})`);
5273
- }
5274
- if (srcLowered && !pureReemittable(srcLowered)) {
5275
- lowerer.unsupported("SC1090", prop, "object spread of computed sources (the field copies re-read the source — bind it to a const first)");
5276
- }
5277
- const srcShape = lowerer.shapes.get(srcType.shapeId);
5278
- if (!srcShape)
5279
- throw new InternalCompilerError(`lowerer bug: spread of unknown shape ${srcType.shapeId}`);
5280
- fenceAccessorSpreadSource(lowerer, prop, srcShape);
5281
- // Index-signature shapes carry runtime-keyed overflow entries the
5282
- // field-by-field desugar cannot enumerate — fenced on either side.
5283
- if (srcShape.indexValue || shape.indexValue) {
5284
- lowerer.unsupported("SC1090", prop, "object spread involving index-signature shapes (overflow keys are runtime state — copy the fields you need explicitly)");
5285
- }
5286
- // Names a LATER contributor unconditionally defines: copying such a
5287
- // source field is dead under JS last-write-wins (and spread reads
5288
- // are side-effect-free), so it neither lowers nor width-checks —
5289
- // the spread-then-override completion `{ ...config, stateDir }`
5290
- // narrows an optional source field into a required target slot
5291
- // exactly like Node does. Conditional spreads don't count (their
5292
- // empty arm defines nothing) — their own collision fences hold.
5293
- const laterNames = new Set();
5294
- for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
5295
- if (ts.isSpreadAssignment(later)) {
5296
- if (conditionalSpreadOf(later.expression))
5297
- continue;
5298
- const lt = lowerer.mapTypeOf(lowerer.typeOf(later.expression));
5299
- if (lt?.kind === "record") {
5300
- for (const lf of lowerer.shapes.get(lt.shapeId)?.fields ?? [])
5301
- laterNames.add(lf.name);
5302
- }
5303
- continue;
5304
- }
5305
- if (later.name &&
5306
- (ts.isIdentifier(later.name) ||
5307
- ts.isStringLiteral(later.name) ||
5308
- ts.isNumericLiteral(later.name) ||
5309
- (ts.isComputedPropertyName(later.name) && literalComputedKey(lowerer, later.name) !== null))) {
5310
- laterNames.add(propNameText(lowerer, later.name));
5311
- }
5312
- }
5313
- for (const f of srcShape.fields) {
5314
- if (laterNames.has(f.name))
5315
- continue;
5316
- const targetType = fieldTypes.get(f.name);
5317
- // A source field with NO slot on the target shape: the copy
5318
- // DROPS it — a spread of a wider record into a narrower literal
5319
- // is width subtyping in spread clothing, divergence 36's stance
5320
- // (Node's object would keep the key); the read is pure, so
5321
- // skipping evaluates nothing.
5322
- if (!targetType)
5323
- continue;
5324
- const lift = typeEquals(f.type, targetType)
5325
- ? null
5326
- : lowerer.widthLiftPlan(f.type, targetType);
5327
- if (!typeEquals(f.type, targetType) && !lift) {
5328
- // Print the SOURCE shape, not the literal's own type: the
5329
- // checker's own type already has later overrides applied, so
5330
- // it can render identically to the target while the spread
5331
- // source (the thing that actually mismatches) differs — an
5332
- // invisible difference is a diagnostics bug.
5333
- 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)}'`));
5334
- throw new PoisonError();
5335
- }
5336
- const obj = srcLowered ?? lowerer.lowerExpr(srcNode);
5337
- // A record-mapped CHECKER type whose VALUE lives in the checked-dynamic tree (a
5338
- // JS file-scope object-literal global): read each field from
5339
- // the checked-dynamic tree (dynKeyGet) and VALIDATE it into the source shape's
5340
- // field type (dynCheck) — the checked-dynamic member-read
5341
- // discipline. A missing key answers the dyn undefined, exactly
5342
- // the undefined-armed optional's absent case; a mismatched
5343
- // runtime value throws the catchable TypeError, never a silent
5344
- // wrong copy. Runtime-ADDED keys drop — width subtyping in
5345
- // spread clothing, divergence 36's stance.
5346
- let value;
5347
- if (obj.type.kind === "dyn") {
5348
- if (!canDynCheckTo(f.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) {
5349
- 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)`);
5350
- }
5351
- value = {
5352
- kind: "dynCheck",
5353
- value: {
5354
- kind: "dynKeyGet",
5355
- key: { kind: "strLit", value: f.name, type: STRING, loc: locOf(prop) },
5356
- value: obj,
5357
- type: DYN,
5358
- loc: locOf(prop),
5359
- },
5360
- type: f.type,
5361
- loc: locOf(prop),
5362
- };
5363
- }
5364
- else {
5365
- value = {
5366
- kind: "recordGet",
5367
- obj,
5368
- shapeId: srcType.shapeId,
5369
- field: f.name,
5370
- type: f.type,
5371
- loc: locOf(prop),
5372
- };
5373
- }
5374
- // A liftable field widens into the target slot (arm wrap,
5375
- // re-tag, nested reshape) — the same per-field rule the slot
5376
- // coercion applies.
5377
- if (lift)
5378
- value = lowerer.applyWidthLift(lift, value, targetType, locOf(prop));
5379
- if (conditionalNames.has(f.name)) {
5380
- 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)`);
5381
- }
5382
- const at = fields.findIndex((x) => x.name === f.name);
5383
- if (at >= 0)
5384
- fields[at] = { name: f.name, value };
5385
- else
5386
- fields.push({ name: f.name, value });
5387
- }
5388
- continue;
5389
- }
5390
- if (ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop)) {
5391
- // JS accessor entries carry no storage (narrowed away above); TS
5392
- // accessors fill the shape's reserved closure slots — the getter
5393
- // body lowers as an ordinary zero-arg closure invoked per property
5394
- // READ, the setter as a one-arg closure invoked per WRITE
5395
- // (fieldGetExpr/fieldSetStmt dispatch on the slots). Creating the
5396
- // closures is side-effect-free, so their position among the data
5397
- // fields' source-order evaluation is unobservable — exactly JS,
5398
- // where accessor definitions evaluate nothing.
5399
- if (isJsSourceFile(expr.getSourceFile()))
5400
- continue;
5401
- const name = propNameText(lowerer, prop.name); // key-form-checked above
5402
- const slotName = `${ts.isGetAccessorDeclaration(prop) ? "%get" : "%set"}:${name}`;
5403
- const slotT = fieldTypes.get(slotName);
5404
- if (!slotT || slotT.kind !== "func") {
5405
- // The contextual shape stores a DATA value under this name
5406
- // (`const p: { x: number } = { get x() {...} }` — tsc lets a
5407
- // live accessor satisfy a data member): the slot would freeze
5408
- // one getter answer where Node keeps the accessor live.
5409
- if (fieldTypes.has(name)) {
5410
- 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)`);
5411
- }
5412
- throw shapeMismatch(prop);
5413
- }
5414
- const closure = lowerer.coerceInto(prop, lowerer.lowerLambda(prop), slotT);
5415
- if (!typeEquals(closure.type, slotT))
5416
- lowerer.badType(prop, lowerer.typeOf(prop));
5417
- fields.push({ name: slotName, value: closure });
5418
- continue;
5419
- }
5420
- const name = propNameText(lowerer, prop.name); // key-form-checked above
5421
- let fieldType = fieldTypes.get(name);
5422
- // An undeclared name against an index-signature shape is an OVERFLOW
5423
- // entry (tsc typechecked it against the signature's value type);
5424
- // against a plain shape it is the width mismatch it always was —
5425
- // EXCEPT the PromiseSettledResult honest subset's dropped fields
5426
- // (value/reason — SEMANTICS.md 46): those evaluate for effect in
5427
- // their source-order slot and store nothing.
5428
- // A GENERIC-callable member (a generic method `m<T>(x: T) {...}` or a
5429
- // generic arrow/function-expression property): excluded from the
5430
- // record shape (isGenericCallableMemberType — no single closure slot
5431
- // can hold it), so the literal stores nothing for it. Pure
5432
- // function-creating forms skip outright (creating a closure has no
5433
- // side effects; calls resolve statically against this declaration);
5434
- // a computed initializer would need its evaluation kept — fenced.
5435
- if (!fieldType && !ts.isSpreadAssignment(prop)) {
5436
- const memberSym = prop.name && lowerer.checker.getSymbolAtLocation(prop.name);
5437
- const memberT = memberSym ? lowerer.checker.getTypeOfSymbol(memberSym) : undefined;
5438
- if (memberT && isGenericCallableMemberType(memberT, lowerer.checker)) {
5439
- const pureInit = (() => {
5440
- if (ts.isMethodDeclaration(prop))
5441
- return true;
5442
- if (ts.isShorthandPropertyAssignment(prop))
5443
- return true; // a pure read
5444
- if (!ts.isPropertyAssignment(prop))
5445
- return false;
5446
- let init = prop.initializer;
5447
- while (ts.isParenthesizedExpression(init))
5448
- init = init.expression;
5449
- return ts.isArrowFunction(init) || ts.isFunctionExpression(init) || ts.isIdentifier(init);
5450
- })();
5451
- if (!pureInit) {
5452
- 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)`);
5453
- }
5454
- continue;
5455
- }
5456
- }
5457
- if (!fieldType && !shape.indexValue) {
5458
- if (settledDropNames(lowerer, tsType)?.has(name)) {
5459
- // Identifier and shorthand initializers are effect-free reads —
5460
- // skipped outright. The caught `reason` MUST skip: lowering the
5461
- // read would hit the catch-binding narrowness fence, and the
5462
- // snapshot it names needs no evaluation.
5463
- if (ts.isPropertyAssignment(prop) && !ts.isIdentifier(prop.initializer)) {
5464
- const v = lowerer.lowerExpr(prop.initializer);
5465
- // Effect-free lowerings (literals, plain reads) drop at
5466
- // compile time; anything else — the awaited mapper — runs
5467
- // (and may throw into the enclosing catch) with its result
5468
- // released by the statement frame.
5469
- if (v.kind !== "unitLit" && v.kind !== "numLit" && v.kind !== "strLit" &&
5470
- v.kind !== "boolLit" && v.kind !== "varRef" && v.kind !== "closure") {
5471
- fields.push({ name, value: v, drop: true });
5472
- }
5473
- }
5474
- continue;
5475
- }
5476
- throw shapeMismatch(prop);
5477
- }
5478
- let value;
5479
- let valueNode = prop;
5480
- const propDiagsBefore = lowerer.diags.length;
5481
- try {
5482
- if (ts.isPropertyAssignment(prop)) {
5483
- valueNode = prop.initializer;
5484
- // ARRAY-LITERAL initializers route through the expected-type
5485
- // lowering: a union field with one array-family arm builds the
5486
- // literal AS that arm (lowerExprExpecting's IR-directed rule —
5487
- // the option-table `default: [{ value: [] }]` shape), where the
5488
- // bare lowering would take the JS dyn fallback and fence.
5489
- let init = prop.initializer;
5490
- while (ts.isParenthesizedExpression(init))
5491
- init = init.expression;
5492
- value =
5493
- fenceClosureProbe(lowerer, prop.initializer, fieldType, () => lowerer.lowerExpr(prop.initializer)) ??
5494
- (fieldType !== undefined && ts.isArrayLiteralExpression(init)
5495
- ? lowerer.lowerExprExpecting(prop.initializer, fieldType)
5496
- : lowerer.lowerExpr(prop.initializer));
5497
- }
5498
- else if (ts.isShorthandPropertyAssignment(prop)) {
5499
- value = lowerer.lowerShorthandValue(prop);
5500
- }
5501
- else if (ts.isMethodDeclaration(prop)) {
5502
- value =
5503
- fenceClosureProbe(lowerer, prop, fieldType, () => lowerer.lowerLambda(prop)) ?? lowerer.lowerLambda(prop);
5504
- }
5505
- else {
5506
- lowerer.unsupported("SC1090", prop, `syntax '${ts.SyntaxKind[prop.kind]}'`);
5507
- }
5508
- }
5509
- catch (err) {
5510
- // A member VALUE a JS file cannot lower (a namespace object in an
5511
- // export aggregate — the sharedWithCli `errors` member): the
5512
- // member NARROWS AWAY like a CJS export-table accessor entry —
5513
- // the diagnostics defer to the runtime-fence ledger, the shape
5514
- // drops the field, and each READ of it meets its own per-site
5515
- // fence. Pure member forms only (identifier/shorthand reads);
5516
- // TypeScript, probe mode, and ICEs keep the poison.
5517
- const pureMember = ts.isShorthandPropertyAssignment(prop) ||
5518
- (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.initializer));
5519
- if (!(err instanceof PoisonError) ||
5520
- !pureMember ||
5521
- !isJsSourceFile(expr.getSourceFile()) ||
5522
- lowerer.diagSink !== null ||
5523
- lowerer.diags.length <= propDiagsBefore ||
5524
- lowerer.diags.slice(propDiagsBefore).some((d) => d.code === "SC9001")) {
5525
- throw err;
5526
- }
5527
- lowerer.runtimeFences.push(...lowerer.diags.splice(propDiagsBefore));
5528
- droppedNames.add(name);
5529
- continue;
5530
- }
5531
- if (!fieldType) {
5532
- // Overflow entry: the value flows into the index signature's value
5533
- // slot — dyn slots take a dyn conversion (dynFrom), typed slots the
5534
- // ordinary coercion path. Later duplicates override (map semantics
5535
- // are last-write-wins already; a duplicate literal key is a tsc
5536
- // error anyway).
5537
- const slotted = lowerer.intoIndexValueSlot(value, shape.indexValue, valueNode);
5538
- fields.push({ name, value: slotted, overflow: true });
5539
- continue;
5540
- }
5541
- const promotedField = fieldType ? lowerer.runtimeOptionalWidening(value.type, fieldType) : null;
5542
- if (promotedField && fieldType) {
5543
- type = lowerer.runtimeOptionalRecordField(type, name, promotedField);
5544
- if (type.kind === "record")
5545
- shape = lowerer.shapes.get(type.shapeId);
5546
- fieldTypes.set(name, promotedField);
5547
- fieldType = promotedField;
5548
- }
5549
- value = lowerer.coerceInto(valueNode, value, fieldType); // union-typed fields wrap arm values
5550
- if (!typeEquals(value.type, fieldType))
5551
- lowerer.badType(valueNode, lowerer.typeOf(valueNode));
5552
- // An explicit property overrides a spread-copied field: JS
5553
- // last-write-wins. The entry moves to the END so explicit property
5554
- // values keep their source-order evaluation among themselves. A
5555
- // conditional-spread entry cannot be overridden (its ternary IS the
5556
- // spread's evaluation; splicing it out would drop that).
5557
- if (conditionalNames.has(name)) {
5558
- 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)`);
5559
- }
5560
- const at = fields.findIndex((x) => x.name === name);
5561
- if (at >= 0)
5562
- fields.splice(at, 1);
5563
- fields.push({ name, value });
5564
- }
5565
- // Optional fields (undefined-armed union slots) may be omitted: the
5566
- // absent field holds the interned undefined arm, exactly like writing
5567
- // `a: undefined` (without exactOptionalPropertyTypes tsc treats the two
5568
- // the same, and only optional fields may be omitted — tsc rejects
5569
- // omission of required fields before lowering, undefined-armed or not).
5570
- // A REQUIRED missing field keeps the shape-mismatch rejection (possible
5571
- // through `as`: the cast smuggles a narrower literal past freshness).
5572
- if (droppedNames.size > 0) {
5573
- const narrowed = shape.fields.filter((f) => !droppedNames.has(f.name));
5574
- const narrowedId = lowerer.shapes.intern(narrowed.map((f) => ({ name: f.name, type: f.type })), false, shape.indexValue);
5575
- type = { kind: "record", shapeId: narrowedId };
5576
- shape = lowerer.shapes.get(narrowedId);
5577
- }
5578
- if (fields.filter((f) => !f.overflow).length !== shape.fields.length) {
5579
- const provided = new Set(fields.filter((f) => !f.overflow).map((f) => f.name));
5580
- for (const f of shape.fields) {
5581
- if (provided.has(f.name))
5582
- continue;
5583
- // 'unknown' fields complete with the dyn undefined — the absent
5584
- // property reads as undefined in Node, and a dyn slot holds
5585
- // exactly that (the options-record call shape against
5586
- // `{ plugins: unknown, ... }` — a JS caller the checker admits).
5587
- const absent = lowerer.wrappedUndefined(f.type, loc) ?? (f.type.kind === "dyn" ? dynUndefinedExpr(loc) : null);
5588
- if (!absent)
5589
- throw shapeMismatch(expr); // only optional (undefined-armed) and 'unknown' fields may be omitted
5590
- fields.push({ name: f.name, value: absent });
5591
- }
5592
- }
5593
- return { kind: "recordLit", fields, type, loc };
5594
- }
5595
- /** Parses the conditional-spread idiom `...(c ? { k: v, ... } : {})`
5596
- * (either orientation). Returns the condition, the non-empty arm's
5597
- * properties, and which arm carries them; "unsupported" for conditional
5598
- * sources OUTSIDE the idiom (both arms non-empty, computed/method
5599
- * members); null when the spread source isn't a conditional at all.
5600
- * Callers slice their own honest subset (the static record path takes
5601
- * exactly one property; the island literal takes any number; spawn's
5602
- * options walk takes a `detached` boolean literal). */
5603
- export function conditionalSpreadOf(expr) {
5604
- let e = expr;
5605
- while (ts.isParenthesizedExpression(e))
5606
- e = e.expression;
5607
- if (!ts.isConditionalExpression(e))
5608
- return null;
5609
- const unwrap = (a) => {
5610
- let x = a;
5611
- while (ts.isParenthesizedExpression(x))
5612
- x = x.expression;
5613
- return x;
5614
- };
5615
- const whenTrue = unwrap(e.whenTrue);
5616
- const whenFalse = unwrap(e.whenFalse);
5617
- if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
5618
- return "unsupported";
5619
- }
5620
- const trueCarries = whenFalse.properties.length === 0 && whenTrue.properties.length > 0;
5621
- const falseCarries = whenTrue.properties.length === 0 && whenFalse.properties.length > 0;
5622
- if (!trueCarries && !falseCarries)
5623
- return "unsupported"; // both empty is tsc-unreachable; both non-empty has no single desugar
5624
- const carrier = trueCarries ? whenTrue : whenFalse;
5625
- const props = [];
5626
- for (const p of carrier.properties) {
5627
- if ((ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
5628
- p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))) {
5629
- props.push(p);
5630
- }
5631
- else {
5632
- return "unsupported";
5633
- }
5634
- }
5635
- return { cond: e.condition, props, whenTrue: trueCarries };
5636
- }
5637
- /** `{ ...idx, ...idx2 }` typed a DECLARED shape (no index signature) — the
5638
- * defaults-merge idiom over runtime-keyed sources (`{ ...fromEntries(a),
5639
- * ...fromEntries(b) }` typed AppConfig). Lowers to ONE interned helper
5640
- * call: sources evaluate once each as arguments (source order — computed
5641
- * sources included, JS's evaluate-once), the result starts all-undefined
5642
- * (every target field must be optional — the runtime keys decide
5643
- * presence), and each source's keys apply in JS own-key order with a
5644
- * per-key dispatch onto the declared fields: a matching key writes the
5645
- * field — identity when the source's value slot IS the field type, a
5646
- * validated extraction otherwise (a dyn slot dynChecks; a union slot
5647
- * re-tags arm-by-arm, and a value outside the field's arms throws the
5648
- * catchable TypeError — divergence 34's keyed-write stance, where Node's
5649
- * untyped copy would store the lie) — and a key naming NO declared field
5650
- * is DROPPED (the shape cannot represent it; Node keeps it invisibly —
5651
- * divergence 68). Later contributors overwrite earlier ones
5652
- * (last-write-wins). Sources must be PURE index-signature records. */
5653
- function lowerDeclaredSpreadMerge(lowerer, expr, type, shape, loc) {
5654
- const srcs = [];
5655
- for (const prop of expr.properties) {
5656
- const spread = prop; // caller-checked: all spreads
5657
- const value = lowerer.lowerExpr(spread.expression);
5658
- const srcShape = value.type.kind === "record" ? lowerer.shapes.get(value.type.shapeId) : undefined;
5659
- if (value.type.kind !== "record" || !srcShape?.indexValue || srcShape.tuple || srcShape.fields.length > 0) {
5660
- 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)`);
5661
- }
5662
- srcs.push({ value, shapeId: value.type.shapeId, iv: srcShape.indexValue });
5663
- }
5664
- // Every target field must be optional: absent keys leave the undefined
5665
- // arm, exactly the unset-optional representation.
5666
- for (const f of shape.fields) {
5667
- if (f.type.kind !== "union" || lowerer.armTag(f.type.unionId, UNDEFINED_T) < 0) {
5668
- 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)`);
5669
- }
5670
- }
5671
- // Per (source value slot → field) conversion, checked up front so the
5672
- // fence fires at the literal, not inside the interned helper.
5673
- const conv = (iv, f, v) => {
5674
- if (typeEquals(iv, f.type))
5675
- return { value: v };
5676
- if (iv.kind === "dyn") {
5677
- return { value: { kind: "dynCheck", value: v, type: f.type, loc } };
5678
- }
5679
- if (iv.kind === "union" && f.type.kind === "union") {
5680
- const fUnion = f.type;
5681
- const def = lowerer.unions.get(iv.unionId);
5682
- const fDef = lowerer.unions.get(fUnion.unionId);
5683
- if (def && fDef && def.arms.some((a) => lowerer.armTag(fUnion.unionId, a) >= 0)) {
5684
- // Arm-by-arm validated re-tag, inline in the helper: matching
5685
- // arms map by identity (unit arms re-wrap, value arms narrow and
5686
- // wrap); anything else throws the catchable TypeError.
5687
- return {
5688
- stmts: (write) => {
5689
- const chain = (i) => {
5690
- if (i >= def.arms.length) {
5691
- return [{
5692
- kind: "throw",
5693
- value: {
5694
- kind: "libCall",
5695
- fn: "error.new",
5696
- args: [{ kind: "strLit", value: `expected ${lowerer.fmt(fUnion)} at $.${f.name}`, type: STRING, loc }],
5697
- type: { kind: "object", className: "%TypeError" },
5698
- loc,
5699
- },
5700
- loc,
5701
- }];
5702
- }
5703
- const arm = def.arms[i];
5704
- const toTag = lowerer.armTag(fUnion.unionId, arm);
5705
- if (toTag < 0)
5706
- return chain(i + 1);
5707
- const extracted = isUnitType(arm)
5708
- ? { kind: "unionWrap", unionId: fUnion.unionId, tag: toTag, value: { kind: "unitLit", unit: arm.kind === "undefinedT" ? "undefined" : "null", type: arm, loc }, type: fUnion, loc }
5709
- : { kind: "unionWrap", unionId: fUnion.unionId, tag: toTag, value: { kind: "unionNarrow", unionId: iv.unionId, tag: i, value: v, type: arm, loc }, type: fUnion, loc };
5710
- return [{
5711
- kind: "if",
5712
- cond: { kind: "unionIsTag", unionId: iv.unionId, tag: i, negated: false, value: v, type: BOOL, loc },
5713
- then: [write(extracted)],
5714
- else_: chain(i + 1),
5715
- loc,
5716
- }];
5717
- };
5718
- return chain(0);
5719
- },
5720
- };
5721
- }
5722
- }
5723
- 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)`);
5724
- };
5725
- const key = `declmerge:${type.shapeId}:${srcs.map((s) => s.shapeId).join(",")}`;
5726
- let helper = lowerer.widthHelpers.get(key);
5727
- if (!helper) {
5728
- helper = `%rec.declmerge.${lowerer.widthHelpers.size}`;
5729
- const outRef = varRef("out.0", type, loc);
5730
- const ksT = arrayOf(STRING);
5731
- const locals = [{ id: "out.0", name: "out", type, mutable: false }];
5732
- const params = srcs.map((s, j) => {
5733
- locals.push({ id: `s${j}.0`, name: `s${j}`, type: s.value.type, mutable: true });
5734
- return { localId: `s${j}.0`, name: `s${j}`, type: s.value.type };
5735
- });
5736
- const body = [
5737
- {
5738
- kind: "varDecl",
5739
- localId: "out.0",
5740
- init: {
5741
- kind: "recordLit",
5742
- fields: shape.fields.map((f) => ({ name: f.name, value: lowerer.wrappedUndefined(f.type, loc) })),
5743
- type,
5744
- loc,
5745
- },
5746
- loc,
5747
- },
5748
- ];
5749
- srcs.forEach((s, j) => {
5750
- const sRef = varRef(`s${j}.0`, s.value.type, loc);
5751
- const kRef = varRef(`k${j}.0`, STRING, loc);
5752
- const vRef = varRef(`v${j}.0`, s.iv, loc);
5753
- 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 });
5754
- // Per-key dispatch: if (k === "a") { write a } else if ... else drop.
5755
- const dispatch = shape.fields.reduceRight((rest, f) => {
5756
- const write = (value) => ({ kind: "recordSet", obj: outRef, shapeId: type.shapeId, field: f.name, value, loc });
5757
- const c = conv(s.iv, f, vRef);
5758
- const thenBody = "value" in c ? [write(c.value)] : c.stmts(write);
5759
- return [{
5760
- kind: "if",
5761
- cond: { kind: "strEq", negated: false, left: kRef, right: { kind: "strLit", value: f.name, type: STRING, loc }, type: BOOL, loc },
5762
- then: thenBody,
5763
- else_: rest.length > 0 ? rest : null,
5764
- loc,
5765
- }];
5766
- }, []);
5767
- body.push({ kind: "varDecl", localId: `ks${j}.0`, init: { kind: "recordOvfKeys", obj: sRef, shapeId: s.shapeId, type: ksT, loc }, loc }, {
5768
- kind: "for",
5769
- init: { kind: "varDecl", localId: `i${j}.0`, init: numLit(0, loc), loc },
5770
- 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 },
5771
- 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 },
5772
- body: [
5773
- { 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 },
5774
- { kind: "varDecl", localId: `v${j}.0`, init: { kind: "recordKeyGet", obj: sRef, shapeId: s.shapeId, key: kRef, overflowOnly: true, type: s.iv, loc }, loc },
5775
- ...dispatch,
5776
- ],
5777
- loc,
5778
- });
5779
- });
5780
- body.push({ kind: "return", value: outRef, loc });
5781
- lowerer.liftedFns.push({
5782
- name: helper,
5783
- params,
5784
- returnType: type,
5785
- locals,
5786
- body,
5787
- loc,
5788
- });
5789
- // Registered only after a fence-free build: a conv() fence mid-build
5790
- // must not leave a phantom helper behind for the next literal.
5791
- lowerer.widthHelpers.set(key, helper);
5792
- }
5793
- return { kind: "call", callee: helper, args: srcs.map((s) => s.value), type, loc };
5794
- }
5795
- /** The value of a shorthand property (`{ x }`): the binding `x` refers to.
5796
- * The property name's own symbol is the PROPERTY, so resolution goes
5797
- * through getShorthandAssignmentValueSymbol. */
5798
- export function lowerShorthandValue(lowerer, prop) {
5799
- // 7 types the shorthand's name as PropertyName; it is always an
5800
- // Identifier (the grammar allows nothing else in shorthand position).
5801
- const propName = prop.name;
5802
- const loc = locOf(propName);
5803
- const symbol = lowerer.checker.getShorthandAssignmentValueSymbol(prop);
5804
- if (symbol) {
5805
- if (lowerer.ctx.selfSymbol === symbol) {
5806
- return { kind: "selfRef", type: lowerer.ctx.selfType, loc };
5807
- }
5808
- const local = lowerer.resolveKey(symbol, propName);
5809
- if (local) {
5810
- return lowerer.maybeNarrow({ kind: "varRef", localId: local.id, type: local.type, loc }, propName);
5811
- }
5812
- const resolved = symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
5813
- lowerer.flushDeferred(resolved);
5814
- const g = lowerer.globalsBySymbol.get(resolved);
5815
- if (g) {
5816
- return lowerer.maybeNarrow({ kind: "varRef", localId: g.id, type: g.type, loc }, propName);
5817
- }
5818
- const sig = lowerer.fnSigsBySymbol.get(resolved);
5819
- const decl = lowerer.checker.declarationsOf(resolved)[0];
5820
- if (sig && decl && ts.isFunctionDeclaration(decl) &&
5821
- (ts.isSourceFile(decl.parent) || lowerer.nsBlocks.get(decl.parent) === "flattened")) {
5822
- lowerer.noteEdge(sig.name);
5823
- const funcType = {
5824
- kind: "func",
5825
- params: sig.params.filter((p) => p.mode !== "dynRest").map((p) => p.type),
5826
- ret: sig.returnType,
5827
- ...(sig.params.some((p) => p.mode === "dynRest") ? { rest: true } : {}),
5828
- };
5829
- lowerer.requireExactArityValue(prop, propName, sig.params, funcType);
5830
- return { kind: "closure", fnName: sig.name, captures: [], type: funcType, loc };
5831
- }
5832
- if (lowerer.genericFnsBySymbol.has(resolved)) {
5833
- lowerer.unsupported("SC1090", prop, `generic functions as values (call '${propName.text}' directly)`);
5834
- }
5835
- }
5836
- 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)`);
5837
- }
5838
- /** Rejects any `this` inside an object-literal method body — including in
5839
- * nested arrows, which inherit the method's `this` (nested function
5840
- * expressions reset it, but their bare `this` is already a tsc error
5841
- * under noImplicitThis, so over-rejecting them here changes nothing). */
5842
- export function rejectThisInObjectMethod(lowerer, node) {
5843
- if (node.kind === ts.SyntaxKind.ThisKeyword) {
5844
- lowerer.unsupported("SC1090", node, "references to 'this' in object literal methods");
5845
- }
5846
- ts.forEachChild(node, (child) => lowerer.rejectThisInObjectMethod(child));
5847
- }
5848
- /** The accessor twin of rejectThisInObjectMethod: a get/set accessor body
5849
- * referencing `this` (`get x() { return this._x }`). The accessor lowers
5850
- * as a closure stored IN the record — passing the record as a receiver
5851
- * would capture the value under construction (an RC cycle), and the
5852
- * generic lexical-this walk would silently bind an ENCLOSING method's
5853
- * `this`; the fence names the fix (capture a binding instead). */
5854
- function rejectThisInObjectAccessor(lowerer, node) {
5855
- if (node.kind === ts.SyntaxKind.ThisKeyword) {
5856
- 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)");
5857
- }
5858
- ts.forEachChild(node, (child) => rejectThisInObjectAccessor(lowerer, child));
5859
- }
5860
3788
  /** `a[i]` reads. Only f64 indices into array receivers are modeled; JS
5861
3789
  * string-key element access (`a["length"]`) and string indexing (`s[0]`
5862
3790
  * typechecks against the lib's index signature; use .charAt) stay out. */
@@ -5868,7 +3796,7 @@ export function lowerElementAccess(lowerer, expr) {
5868
3796
  }
5869
3797
  // `a?.b[i]` — the tail of a chain whose token sits deeper: the whole
5870
3798
  // tail short-circuits with the guard.
5871
- if (!expr.questionDotToken && chainTailClaimed(lowerer, expr)) {
3799
+ if (!expr.questionDotToken && isOptionalChainTail(lowerer, expr)) {
5872
3800
  return lowerer.lowerOptionalChain(expr);
5873
3801
  }
5874
3802
  // Enum accesses in element clothing — `E["A"]` forward reads and
@@ -5879,6 +3807,9 @@ export function lowerElementAccess(lowerer, expr) {
5879
3807
  if (en)
5880
3808
  return en;
5881
3809
  }
3810
+ const cachedModule = lowerRequireCacheElement(lowerer, expr);
3811
+ if (cachedModule)
3812
+ return cachedModule;
5882
3813
  // Native Web handles use the same supported surface for bracket and dot
5883
3814
  // spellings. Fence a statically-known unsupported key before the generic
5884
3815
  // checked-dynamic element-read path can turn it into a runtime missing
@@ -5975,7 +3906,7 @@ export function lowerElementAccess(lowerer, expr) {
5975
3906
  ? { kind: "toString", operand: rawKey, type: STRING, loc: rawKey.loc }
5976
3907
  : null;
5977
3908
  if (key) {
5978
- const opt = chainGuardedByQuestionDot(expr.expression);
3909
+ const opt = hasOptionalChainGuard(expr.expression);
5979
3910
  return lowerer.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: recv, type: DYN, loc: locOf(expr) }, expr);
5980
3911
  }
5981
3912
  }
@@ -6089,7 +4020,7 @@ export function lowerElementAccess(lowerer, expr) {
6089
4020
  ? { kind: "toString", operand: rawKey, type: STRING, loc: rawKey.loc }
6090
4021
  : null;
6091
4022
  if (key) {
6092
- const opt = chainGuardedByQuestionDot(expr.expression);
4023
+ const opt = hasOptionalChainGuard(expr.expression);
6093
4024
  return lowerer.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: obj, type: DYN, loc: locOf(expr) }, expr);
6094
4025
  }
6095
4026
  }
@@ -6111,6 +4042,17 @@ export function lowerElementAccess(lowerer, expr) {
6111
4042
  const value = lowerer.lowerExpr(expr.expression);
6112
4043
  if (value.type.kind === "union") {
6113
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
+ }
6114
4056
  if (key.type.kind === "string") {
6115
4057
  const lit = ts.isStringLiteral(expr.argumentExpression)
6116
4058
  ? expr.argumentExpression.text
@@ -6392,6 +4334,19 @@ export function lowerRecordKeyRead(lowerer, expr, shapeId, shape, includeUndefin
6392
4334
  if (key.type.kind !== "string") {
6393
4335
  lowerer.unsupported("SC1090", keyNode, "indexing records with non-string or non-number keys");
6394
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
+ }
6395
4350
  // The DECLARED result type of the access — the index signature's value
6396
4351
  // type (armed with undefined under noUncheckedIndexedAccess), or the
6397
4352
  // declared fields' one common type on signature-free shapes (tsc's
@@ -6483,6 +4438,9 @@ export function lowerAbsenceProbe(lowerer, node) {
6483
4438
  }
6484
4439
  if (!ts.isElementAccessExpression(expr))
6485
4440
  return null;
4441
+ const cachedModule = lowerRequireCacheElement(lowerer, expr);
4442
+ if (cachedModule)
4443
+ return cachedModule;
6486
4444
  const header = lowerer.lowerHttpHeadersElement(expr);
6487
4445
  if (header)
6488
4446
  return header;
@@ -6580,6 +4538,7 @@ function tupleLiteralIndex(node) {
6580
4538
  * assignment, produce no value in our subset). */
6581
4539
  export function lowerElementWrite(lowerer, expr) {
6582
4540
  const target = expr.left;
4541
+ fenceNodeModuleMutation(lowerer, target, "assignment");
6583
4542
  // `process.env[key] = v` — the computed twin of the dotted env write:
6584
4543
  // setenv(3), string keys and string values only.
6585
4544
  if (lowerer.isProcessEnv(target.expression) && !target.questionDotToken) {
@@ -6846,7 +4805,7 @@ export function lowerElementWrite(lowerer, expr) {
6846
4805
  // probes the receiver's own lowered world: a dyn value takes the same
6847
4806
  // write, anything else falls through to the fences.
6848
4807
  if (receiverIr?.kind === "dyn" || receiverIr === null) {
6849
- 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);
6850
4809
  if (obj !== null && obj.type.kind === "dyn") {
6851
4810
  const loc = locOf(expr);
6852
4811
  const litKey = recordKeyLiteralText(target.argumentExpression);
@@ -7164,6 +5123,12 @@ export function lowerAsExpression(lowerer, expr) {
7164
5123
  if (target.kind === "bytes" && target.elem === "u8") {
7165
5124
  return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) };
7166
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
+ }
7167
5132
  // ADAPTABLE function targets (`u as (x: number) => number` — the
7168
5133
  // checked-dynamic function boundary): kind check, then exact unwrap
7169
5134
  // or the per-target adapter shim. Non-adaptable signatures keep the
@@ -7328,10 +5293,46 @@ function seqExprSafeStmt(s) {
7328
5293
  return false;
7329
5294
  }
7330
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
+ }
7331
5326
  export function lowerBinary(lowerer, expr) {
7332
5327
  const loc = locOf(expr);
7333
5328
  const op = expr.operatorToken.kind;
5329
+ const cacheHas = lowerRequireCacheHas(lowerer, expr);
5330
+ if (cacheHas)
5331
+ return cacheHas;
7334
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
+ }
7335
5336
  // `x = e` in EXPRESSION position (`while ((idx = s.indexOf("\n")) !== -1)`,
7336
5337
  // `f(x = v)`): evaluate e once, write the binding, yield the assigned
7337
5338
  // value — JS evaluation order. Variable targets only (locals and module
@@ -7442,7 +5443,7 @@ export function lowerBinary(lowerer, expr) {
7442
5443
  // RHS value is the expression's value — its OWN type, so the
7443
5444
  // consumer sees exactly what the checker typed.
7444
5445
  if (ts.isPropertyAccessExpression(expr.left) && !expr.left.questionDotToken) {
7445
- const recv = probeLower(lowerer, expr.left.expression);
5446
+ const recv = tryLowerExpression(lowerer, expr.left.expression);
7446
5447
  if (recv && recv.type.kind === "dyn") {
7447
5448
  const recvTmp = lowerer.declareHiddenLocal("%setRecv", DYN);
7448
5449
  const rhsVal = lowerer.lowerExpr(expr.right);
@@ -7567,8 +5568,74 @@ export function lowerBinary(lowerer, expr) {
7567
5568
  // Everything else (`u && flag`, whose value would need a wider
7568
5569
  // re-tagged union) stays fenced — in CONDITION position those
7569
5570
  // shapes lower through lowerCondition's bool descent instead.
7570
- 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
+ }
7571
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
+ }
7572
5639
  // `||` whose left does NOT fit the result union: the checker built
7573
5640
  // that result by DROPPING the left's falsy arms (`process.env.X ||
7574
5641
  // null` is `string | null`, `|| 3000` is `string | number` — the
@@ -7576,9 +5643,7 @@ export function lowerBinary(lowerer, expr) {
7576
5643
  // the shared shape below does, retags an arm the test is about to
7577
5644
  // rule out and throws where Node yields the default. Single-eval
7578
5645
  // instead: test the left in its OWN union and retag only on the
7579
- // truthy side, where the dropped arms are unreachable. `&&` keeps
7580
- // the eager shape — there the falsy left IS the result, so the
7581
- // checker keeps those arms in the target and they must coerce.
5646
+ // truthy side, where the dropped arms are unreachable.
7582
5647
  if (op === ts.SyntaxKind.BarBarToken && left.type.kind === "union" && !typeEquals(left.type, target)) {
7583
5648
  const retag = lowerer.unionRetagHelper(left.type.unionId, target.unionId, loc);
7584
5649
  if (retag) {
@@ -7791,6 +5856,14 @@ export function lowerBinary(lowerer, expr) {
7791
5856
  // retry lifts the site. Genuine unknown keeps the SC1100 story.
7792
5857
  if ((left.type.kind === "dyn" && lowerer.anyOrigin(expr.left)) ||
7793
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);
7794
5867
  lowerer.anyOpFence(`the '${ts.tokenToString(op) ?? ts.SyntaxKind[op]}' operator`, expr);
7795
5868
  }
7796
5869
  // tsc allows ===/!== on unknown (arithmetic/comparisons it rejects
@@ -7802,30 +5875,7 @@ export function lowerBinary(lowerer, expr) {
7802
5875
  // computes. Comparisons come back as static bools; arithmetic stays
7803
5876
  // an island value ('1 as any + "x"' is a string over there).
7804
5877
  if (left.type.kind === "jsval" || right.type.kind === "jsval") {
7805
- const JS_BIN = {
7806
- [ts.SyntaxKind.PlusToken]: "add",
7807
- [ts.SyntaxKind.MinusToken]: "sub",
7808
- [ts.SyntaxKind.AsteriskToken]: "mul",
7809
- [ts.SyntaxKind.SlashToken]: "div",
7810
- [ts.SyntaxKind.PercentToken]: "mod",
7811
- [ts.SyntaxKind.AsteriskAsteriskToken]: "pow",
7812
- [ts.SyntaxKind.LessThanToken]: "lt",
7813
- [ts.SyntaxKind.LessThanEqualsToken]: "le",
7814
- [ts.SyntaxKind.GreaterThanToken]: "gt",
7815
- [ts.SyntaxKind.GreaterThanEqualsToken]: "ge",
7816
- [ts.SyntaxKind.EqualsEqualsEqualsToken]: "eq",
7817
- [ts.SyntaxKind.ExclamationEqualsEqualsToken]: "neq",
7818
- };
7819
- const jop = JS_BIN[op];
7820
- if (jop === undefined) {
7821
- lowerer.unsupported("SC1090", expr, `operator '${ts.tokenToString(op) ?? ts.SyntaxKind[op]}' on 'any' values`);
7822
- }
7823
- const type = jsOpResultKind(jop) === "bool" ? BOOL : JSVAL;
7824
- return {
7825
- kind: "jsOp", op: jop,
7826
- args: [lowerer.jsvalIn(left, expr.left), lowerer.jsvalIn(right, expr.right)],
7827
- type, loc,
7828
- };
5878
+ return lowerAnyBinaryInIsland(lowerer, expr, left, right, loc);
7829
5879
  }
7830
5880
  // An unchecked array read keeps its undefined arm in the IR even when
7831
5881
  // the checker typed the binding as `string`. An optional string is not
@@ -7860,7 +5910,7 @@ export function lowerBinary(lowerer, expr) {
7860
5910
  // with observable getters/calls.
7861
5911
  const prefix = [];
7862
5912
  const stable = (operand) => {
7863
- if (pureReemittable(operand) || droppableStatic(operand))
5913
+ if (isSafeToRepeat(operand) || isSafeToDiscard(operand))
7864
5914
  return operand;
7865
5915
  const tmp = lowerer.declareHiddenLocal("%plus", operand.type);
7866
5916
  prefix.push({ kind: "varDecl", localId: tmp.id, init: operand, loc });
@@ -8518,7 +6568,7 @@ function lowerUnionTypeofTest(lowerer, expr, loc) {
8518
6568
  continue;
8519
6569
  const tags = answers.flatMap((s, i) => (s === b.text ? [i] : []));
8520
6570
  if (tags.length === 0 || tags.length === def.arms.length) {
8521
- if (!pureReemittable(value)) {
6571
+ if (!isSafeToRepeat(value)) {
8522
6572
  lowerer.unsupported("SC1090", expr, "statically-decided 'typeof' tests over effectful operands (bind the value to a const first)");
8523
6573
  }
8524
6574
  return { kind: "boolLit", value: (tags.length !== 0) !== negated, type: BOOL, loc };
@@ -8528,7 +6578,7 @@ function lowerUnionTypeofTest(lowerer, expr, loc) {
8528
6578
  });
8529
6579
  if (tags.length === 1)
8530
6580
  return isTag(tags[0]);
8531
- if (!pureReemittable(value)) {
6581
+ if (!isSafeToRepeat(value)) {
8532
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)");
8533
6583
  }
8534
6584
  // Tag-in-set as a short-circuit chain (De Morgan for `!==`).
@@ -8583,7 +6633,7 @@ function lowerDynTypeofTest(lowerer, expr, loc) {
8583
6633
  const recv = unparen(pn.expression);
8584
6634
  if (ts.isAsExpression(recv) || ts.isTypeAssertion(recv)) {
8585
6635
  const inner = unparen(recv.expression);
8586
- const obj = probeLower(lowerer, inner);
6636
+ const obj = tryLowerExpression(lowerer, inner);
8587
6637
  if (obj?.type.kind === "dyn") {
8588
6638
  keyed = {
8589
6639
  kind: "dynKeyGet",
@@ -8606,7 +6656,7 @@ function lowerDynTypeofTest(lowerer, expr, loc) {
8606
6656
  // "string") ... }`, the validateConfig idiom). Probe the lowering
8607
6657
  // and claim exactly the dyn results; anything else keeps its
8608
6658
  // sibling lowerings and fences untouched.
8609
- const probed = probeLower(lowerer, a.expression);
6659
+ const probed = tryLowerExpression(lowerer, a.expression);
8610
6660
  if (probed?.type.kind !== "dyn")
8611
6661
  continue;
8612
6662
  value = probed;
@@ -8770,33 +6820,6 @@ function lowerCjsExportTableThisMember(lowerer, expr) {
8770
6820
  return null;
8771
6821
  return cjsAccessorCall(lowerer, sibling, locOf(expr));
8772
6822
  }
8773
- /** Lowers an expression, converting a fence (PoisonError) into a null
8774
- * DECLINE — the probing dispatch for shapes where only the lowered
8775
- * value's type can say whether a lowering applies. unsupported() pushes
8776
- * its diagnostic BEFORE throwing, so the probe runs behind a diagnostic
8777
- * sink: a declined probe's diagnostics vanish (the sibling lowerings
8778
- * own the site), a successful probe's flush through afterwards. */
8779
- export function probeLower(lowerer, node) {
8780
- const saved = lowerer.diagSink;
8781
- const captured = [];
8782
- lowerer.diagSink = captured;
8783
- let result;
8784
- try {
8785
- result = lowerer.lowerExpr(node);
8786
- }
8787
- catch (e) {
8788
- if (e instanceof PoisonError) {
8789
- lowerer.diagSink = saved;
8790
- return null;
8791
- }
8792
- lowerer.diagSink = saved;
8793
- throw e;
8794
- }
8795
- lowerer.diagSink = saved;
8796
- for (const d of captured)
8797
- lowerer.pushDiag(d);
8798
- return result;
8799
- }
8800
6823
  /** A read of a catch binding — the caught analog of maybeNarrow. Where
8801
6824
  * tsc's control-flow narrowing has proven a supported test (`instanceof`
8802
6825
  * over a hierarchy class, `typeof` over a primitive), the read bridges
@@ -9276,7 +7299,7 @@ function lowerInExpression(lowerer, expr, loc) {
9276
7299
  // stringified like every property key (o[k] is o[String(k)] in JS;
9277
7300
  // `in` shares the coercion).
9278
7301
  {
9279
- const probed = probeLower(lowerer, expr.right);
7302
+ const probed = tryLowerExpression(lowerer, expr.right);
9280
7303
  if (probed?.type.kind === "dyn") {
9281
7304
  let k = lowerer.lowerExpr(expr.left); // JS order: the key evaluates first
9282
7305
  if (k.type.kind === "f64" || k.type.kind === "string" || k.type.kind === "dyn") {
@@ -10106,6 +8129,34 @@ export function lowerUnionProperty(lowerer, expr) {
10106
8129
  if (!def)
10107
8130
  throw new InternalCompilerError(`lowerer bug: unknown union ${value.type.unionId}`);
10108
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
+ }
10109
8160
  let common = null;
10110
8161
  for (const arm of def.arms) {
10111
8162
  let ft;
@@ -10770,7 +8821,7 @@ export function lowerFieldCompound(lowerer, access, op, rhsNode, loc) {
10770
8821
  // receiver is an identifier (checked above), so evaluating it for
10771
8822
  // read and write matches JS's once-evaluation observably.
10772
8823
  if (ts.isPropertyAccessExpression(access)) {
10773
- const probed = probeLower(lowerer, access.expression);
8824
+ const probed = tryLowerExpression(lowerer, access.expression);
10774
8825
  if (probed?.type.kind === "dyn") {
10775
8826
  const key = { kind: "strLit", value: access.name.text, type: STRING, loc: locOf(access.name) };
10776
8827
  const read = { kind: "dynKeyGet", key, value: probed, type: DYN, loc };