@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
@@ -0,0 +1,1815 @@
1
+ import { InternalCompilerError } from "../../../errors.js";
2
+ import * as ts from "../../ts7/adapter.js";
3
+ import { BOOL, DYN, F64, STRING, UNDEFINED_T, VOID, arrayOf, canDynCheckTo, isUnitType, shapeHasAccessorSlots, typeEquals, } from "../../../ir/ir.js";
4
+ import { isCjsExportTableLiteral, isJsSourceFile, locOf } from "../../program.js";
5
+ import { NARROW_FIRST } from "../surfaces.js";
6
+ import { recordShapeMismatchDiag } from "../../../diagnostics/diagnostic.js";
7
+ import { PoisonError, dynUndefinedExpr } from "../lowerer.js";
8
+ import { lowerIndexMergeHelper } from "../lower-containers.js";
9
+ import { isGenericCallableMemberType } from "../../type-mapper.js";
10
+ import { numLit, varRef } from "../../../ir/build.js";
11
+ import { isSafeToRepeat } from "./evaluation-safety.js";
12
+ import { tryLowerExpression } from "./try-lower-expression.js";
13
+ /** `{ a: 1, b: "x" }` → recordLit. The record type comes from the
14
+ * contextual type when tsc has one (annotated declarations, arguments,
15
+ * nested literals) and from the literal's own type otherwise — both intern
16
+ * to the same shapeId unless width subtyping is in play (an `as` cast can
17
+ * smuggle a wider/narrower literal past tsc's freshness check), which the
18
+ * exact-shape checks below reject with SC2002. Fields lower IN SOURCE
19
+ * ORDER: JS evaluates property values in source order. */
20
+ /** The dropped-field names of the PromiseSettledResult honest subset
21
+ * (SEMANTICS.md 46): when the literal's target type is (a union
22
+ * containing) the lib's PromiseFulfilledResult / PromiseRejectedResult,
23
+ * the record mapping kept only `status` — `value` and `reason` evaluate
24
+ * for effect and are not stored. Null for every other target. */
25
+ function settledDropNames(lowerer, tsType) {
26
+ let names = null;
27
+ const parts = tsType.isUnionType() ? ts.constituentTypes(tsType) : [tsType];
28
+ for (const part of parts) {
29
+ const sym = part.getSymbol();
30
+ if (!sym ||
31
+ !lowerer.checker.declarationsOf(sym).some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()))) {
32
+ continue;
33
+ }
34
+ if (sym.name === "PromiseFulfilledResult")
35
+ (names ??= new Set()).add("value");
36
+ if (sym.name === "PromiseRejectedResult")
37
+ (names ??= new Set()).add("reason");
38
+ }
39
+ return names;
40
+ }
41
+ /** `{ [KEY]: v }` where KEY is a compile-time-known string or number:
42
+ * the key folds into an ordinary (spelled) property name — the record
43
+ * shape is exactly what tsc computed for the literal (the late-bound
44
+ * name IS the checker's literal type), so the fold is just spelling.
45
+ * PURE key forms only, so skipping the key's evaluation is exact (JS
46
+ * evaluates the key before the value; effectful expressions keep the
47
+ * fence): any side-effect-free expression — identifiers, property
48
+ * chains with no accessor on them (enum members, `other.name` reads),
49
+ * literals, parenthesized/as-wrapped forms, operators over those —
50
+ * whose checker type is ONE string or number literal (or a union whose
51
+ * arms all spell the same name — `E1.x || E2.x` where both are 0).
52
+ * Number keys take JS's canonical spelling (`{ [E.member]: v }` stores
53
+ * "0" — ToPropertyKey), exactly the name tsc late-bound. Templates
54
+ * fold structurally span by span as well (a template of literals whose
55
+ * checker type widened still spells one string). Symbol-typed and
56
+ * runtime-valued keys stay out. */
57
+ function literalComputedKey(lowerer, name) {
58
+ return foldedStringKeyOf(lowerer, name.expression);
59
+ }
60
+ export function foldedStringKeyOf(lowerer, expr) {
61
+ if (ts.isParenthesizedExpression(expr))
62
+ return foldedStringKeyOf(lowerer, expr.expression);
63
+ if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr))
64
+ return expr.text;
65
+ if (ts.isTemplateExpression(expr)) {
66
+ // Structural fold first — exact even where the checker widened the
67
+ // template's own type; the general type-directed fold below is the
68
+ // fallback (a template the checker DID type as one literal).
69
+ let out = expr.head.text;
70
+ let folded = true;
71
+ for (const span of expr.templateSpans) {
72
+ const part = foldedStringKeyOf(lowerer, span.expression);
73
+ if (part === null) {
74
+ folded = false;
75
+ break;
76
+ }
77
+ out += part + span.literal.text;
78
+ }
79
+ if (folded)
80
+ return out;
81
+ }
82
+ // The general fold: a PURE expression whose checker type spells one
83
+ // property name. An as-cast folds by its OWN checker type — that is
84
+ // the name tsc late-bound the property under, so shape and storage
85
+ // stay one name (the standard trust-the-checker bet); a cast that
86
+ // WIDENS to string un-late-binds the property, its type is no literal,
87
+ // and the fence stays.
88
+ if (!pureKeyExpr(lowerer, expr))
89
+ return null;
90
+ return literalKeySpellingOf(lowerer.typeOf(expr));
91
+ }
92
+ /** The property-name spelling of a single-literal checker type: string
93
+ * literals directly, number literals in JS's canonical ToString spelling
94
+ * (enum members included — their literal flags carry the value), and
95
+ * unions whose arms all spell the SAME name (`E1.x || E2.x` — two enum
96
+ * literal types, one value). Null for everything else. */
97
+ function literalKeySpellingOf(t) {
98
+ if (t.isStringLiteralType())
99
+ return t.value;
100
+ if (t.isNumberLiteralType())
101
+ return String(t.value);
102
+ if (t.isUnionType()) {
103
+ let out = null;
104
+ for (const arm of ts.constituentTypes(t)) {
105
+ const s = arm.isStringLiteralType() ? arm.value : arm.isNumberLiteralType() ? String(arm.value) : null;
106
+ if (s === null || (out !== null && s !== out))
107
+ return null;
108
+ out = s;
109
+ }
110
+ return out;
111
+ }
112
+ return null;
113
+ }
114
+ /** True iff evaluating `expr` can have no observable effect, so a fold
115
+ * may skip it: literals, identifier reads, property chains whose every
116
+ * member is a plain field/enum member (an accessor anywhere on the chain
117
+ * is a call), operators and casts over those. Calls, `new`, assignments,
118
+ * and element accesses stay impure. */
119
+ function pureKeyExpr(lowerer, expr) {
120
+ if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr))
121
+ return pureKeyExpr(lowerer, expr.expression);
122
+ if (ts.isAsExpression(expr) || ts.isTypeAssertion(expr))
123
+ return pureKeyExpr(lowerer, expr.expression);
124
+ if (ts.isIdentifier(expr))
125
+ return true;
126
+ if (ts.isStringLiteralLike(expr) || ts.isNumericLiteral(expr))
127
+ return true;
128
+ if (expr.kind === ts.SyntaxKind.TrueKeyword || expr.kind === ts.SyntaxKind.FalseKeyword ||
129
+ expr.kind === ts.SyntaxKind.NullKeyword)
130
+ return true;
131
+ if (ts.isPropertyAccessExpression(expr)) {
132
+ const sym = lowerer.checker.getSymbolAtLocation(expr.name);
133
+ if (sym && sym.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor))
134
+ return false;
135
+ return pureKeyExpr(lowerer, expr.expression);
136
+ }
137
+ if (ts.isPrefixUnaryExpression(expr) &&
138
+ (expr.operator === ts.SyntaxKind.MinusToken || expr.operator === ts.SyntaxKind.PlusToken ||
139
+ expr.operator === ts.SyntaxKind.ExclamationToken || expr.operator === ts.SyntaxKind.TildeToken)) {
140
+ return pureKeyExpr(lowerer, expr.operand);
141
+ }
142
+ if (ts.isBinaryExpression(expr)) {
143
+ const op = expr.operatorToken.kind;
144
+ const pureOp = op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken ||
145
+ op === ts.SyntaxKind.QuestionQuestionToken ||
146
+ (op >= ts.SyntaxKind.LessThanToken && op <= ts.SyntaxKind.CaretToken &&
147
+ op !== ts.SyntaxKind.InstanceOfKeyword && op !== ts.SyntaxKind.InKeyword);
148
+ return pureOp && pureKeyExpr(lowerer, expr.left) && pureKeyExpr(lowerer, expr.right);
149
+ }
150
+ if (ts.isConditionalExpression(expr)) {
151
+ return pureKeyExpr(lowerer, expr.condition) && pureKeyExpr(lowerer, expr.whenTrue) && pureKeyExpr(lowerer, expr.whenFalse);
152
+ }
153
+ if (ts.isTemplateExpression(expr))
154
+ return expr.templateSpans.every((s) => pureKeyExpr(lowerer, s.expression));
155
+ return false;
156
+ }
157
+ /** A property's field name: identifier/string-literal keys directly,
158
+ * foldable computed keys through literalComputedKey (the syntax pass
159
+ * fenced everything else). */
160
+ function propNameText(lowerer, name) {
161
+ if (ts.isComputedPropertyName(name)) {
162
+ const k = literalComputedKey(lowerer, name);
163
+ if (k === null)
164
+ throw new InternalCompilerError("lowerer bug: unfoldable computed key past the fence");
165
+ return k;
166
+ }
167
+ // Numeric literals name their canonical string key (JS's ToPropertyKey
168
+ // — the same name the checker gives the property symbol).
169
+ if (ts.isNumericLiteral(name))
170
+ return String(Number(name.text));
171
+ return name.text;
172
+ }
173
+ /** The runtime-keyed JS object literal (a computed key that doesn't fold):
174
+ * builds a dyn object member-by-member. Keys evaluate before their values,
175
+ * properties in source order — JS's object-literal evaluation exactly.
176
+ * Identifier/string keys are compile-time strings; numeric keys take
177
+ * their canonical number string (`{ 0x10: v }` stores "16" — JS's
178
+ * ToPropertyKey); computed keys evaluate and pass through ToString (the
179
+ * dyn's String() for dyn operands — `{ [field]: v }` where field is a
180
+ * checked-dynamic param). Values convert through the usual dyn boundary
181
+ * (dynFrom's domain, functions box); a value with no dyn representation
182
+ * fences per property. Spreads copy through dyn.assign in source order;
183
+ * accessors stay fenced. */
184
+ export function lowerDynObjectLiteral(lowerer, expr, boxValue) {
185
+ const loc = locOf(expr);
186
+ let fields = [];
187
+ let acc = null;
188
+ const flushFields = () => {
189
+ if (fields.length === 0)
190
+ return;
191
+ const chunk = { kind: "dynObjLit", fields, type: DYN, loc };
192
+ acc = acc === null
193
+ ? chunk
194
+ : { kind: "libCall", fn: "dyn.assign", args: [acc, chunk], type: DYN, loc };
195
+ fields = [];
196
+ };
197
+ for (const prop of expr.properties) {
198
+ if (ts.isSpreadAssignment(prop)) {
199
+ flushFields();
200
+ const raw = lowerer.lowerExpr(prop.expression);
201
+ const source = boxValue
202
+ ? boxValue(prop.expression, raw)
203
+ : lowerer.coerceToExpected(raw, DYN);
204
+ if (source.type.kind !== "dyn") {
205
+ lowerer.unsupported("SC1101", prop.expression, `spreading '${lowerer.fmt(source.type)}' into a checked-dynamic object literal`);
206
+ }
207
+ acc ??= { kind: "dynObjLit", fields: [], type: DYN, loc };
208
+ acc = { kind: "libCall", fn: "dyn.assign", args: [acc, source], type: DYN, loc: locOf(prop) };
209
+ continue;
210
+ }
211
+ if (!ts.isPropertyAssignment(prop) &&
212
+ !ts.isShorthandPropertyAssignment(prop) &&
213
+ !ts.isMethodDeclaration(prop)) {
214
+ lowerer.unsupported("SC1090", prop, "accessors in a runtime-keyed (computed-key) object literal");
215
+ }
216
+ const name = prop.name;
217
+ let key;
218
+ if (ts.isComputedPropertyName(name)) {
219
+ const folded = literalComputedKey(lowerer, name);
220
+ if (folded !== null) {
221
+ key = { kind: "strLit", value: folded, type: STRING, loc: locOf(name) };
222
+ }
223
+ else {
224
+ let k = lowerer.lowerExpr(name.expression);
225
+ if (k.type.kind === "f64" || k.type.kind === "bool" || k.type.kind === "dyn") {
226
+ k = { kind: "toString", operand: k, type: STRING, loc: locOf(name) };
227
+ }
228
+ if (k.type.kind !== "string") {
229
+ lowerer.unsupported("SC1090", name, `'${lowerer.fmt(k.type)}'-typed computed property keys (string, number, boolean, and unknown keys stringify)`);
230
+ }
231
+ key = k;
232
+ }
233
+ }
234
+ else if (ts.isIdentifier(name) || ts.isStringLiteral(name)) {
235
+ key = { kind: "strLit", value: name.text, type: STRING, loc: locOf(name) };
236
+ }
237
+ else if (ts.isNumericLiteral(name)) {
238
+ key = { kind: "strLit", value: String(Number(name.text)), type: STRING, loc: locOf(name) };
239
+ }
240
+ else {
241
+ lowerer.unsupported("SC1090", prop, "non-identifier property names");
242
+ }
243
+ const valueExpr = ts.isMethodDeclaration(prop)
244
+ ? prop
245
+ : ts.isShorthandPropertyAssignment(prop)
246
+ ? prop.name
247
+ : prop.initializer;
248
+ // Lambda values that fail to lower become trap closures (the
249
+ // per-call fence granularity — fenceClosureProbe) so the object
250
+ // still builds; everything else keeps the per-property fence below.
251
+ let raw;
252
+ const propDiagsBefore = lowerer.diags.length;
253
+ try {
254
+ const lowerValue = () => ts.isMethodDeclaration(prop)
255
+ ? (lowerer.rejectThisInObjectMethod(prop.body ?? prop), lowerer.lowerLambda(prop))
256
+ : lowerer.lowerExpr(valueExpr);
257
+ raw =
258
+ fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ??
259
+ lowerValue();
260
+ }
261
+ catch (err) {
262
+ // A PURE member read a JS file cannot lower (a namespace object
263
+ // in an export aggregate): the slot takes a boxed fence closure —
264
+ // the diagnostics defer to the runtime-fence ledger, the object
265
+ // builds, and only USING the member stops the run.
266
+ const pureMember = ts.isIdentifier(valueExpr);
267
+ if (!(err instanceof PoisonError) || !pureMember)
268
+ throw err;
269
+ const fence = lowerer.deferToRuntimeFence(propDiagsBefore, prop, {
270
+ kind: "closure",
271
+ name: () => `%fn${lowerer.lambdaCounter++}_dynfence`,
272
+ returnType: VOID,
273
+ type: { kind: "func", params: [], ret: VOID },
274
+ });
275
+ if (!fence)
276
+ throw err;
277
+ raw = fence;
278
+ }
279
+ let v = boxValue
280
+ ? boxValue(valueExpr, raw)
281
+ : lowerer.coerceToExpected(raw, DYN);
282
+ if (v.type.kind !== "dyn") {
283
+ const convDiagsBefore = lowerer.diags.length;
284
+ try {
285
+ lowerer.unsupported("SC1101", valueExpr, `holding '${lowerer.fmt(v.type)}' values in a runtime-keyed (computed-key) object literal`);
286
+ }
287
+ catch (err) {
288
+ // A member value the checked-dynamic tree cannot hold — a func whose signature
289
+ // cannot box, an island ('any') handle, a record carrying func
290
+ // fields (the export aggregate's typed utilities and npm-handle
291
+ // members): the same call-time fence deferral as an unlowerable
292
+ // member — the slot takes a boxed fence closure; only USING it
293
+ // stops the run. Probe mode and ICEs keep the poison.
294
+ if (!(err instanceof PoisonError))
295
+ throw err;
296
+ const fence = lowerer.deferToRuntimeFence(convDiagsBefore, prop, {
297
+ kind: "closure",
298
+ name: () => `%fn${lowerer.lambdaCounter++}_dynfence`,
299
+ returnType: VOID,
300
+ type: { kind: "func", params: [], ret: VOID },
301
+ });
302
+ if (!fence)
303
+ throw err;
304
+ v = lowerer.coerceToExpected(fence, DYN);
305
+ }
306
+ }
307
+ fields.push({ key, value: v });
308
+ }
309
+ flushFields();
310
+ return acc ?? { kind: "dynObjLit", fields: [], type: DYN, loc };
311
+ }
312
+ /** Spreading a source whose shape carries accessor slots: Node's copy
313
+ * invokes each getter exactly once in insertion order — even for keys a
314
+ * later contributor overrides — while the field-copy desugar assumes pure
315
+ * reads it may drop or reorder. Getter-call counts would silently diverge,
316
+ * so accessor-carrying sources fence by name. */
317
+ function fenceAccessorSpreadSource(lowerer, prop, srcShape) {
318
+ if (srcShape && shapeHasAccessorSlots(srcShape)) {
319
+ 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)");
320
+ }
321
+ }
322
+ /** The JS trap-closure fallback for FUNCTION-VALUED object properties: a
323
+ * lambda whose body fails to lower inside a JS object literal becomes a
324
+ * closure of the field's exact func type whose body is the runtime fence —
325
+ * the object still BUILDS (commander's `_outputConfiguration` table: the
326
+ * driven writeOut/writeErr entries work; an unloweraable color probe traps
327
+ * only if something CALLS it). The failed lambda's diagnostics move to the
328
+ * runtime-fence inventory, exactly like a fenced JS statement. Null when
329
+ * the fallback does not apply (not a JS file, not a lambda, no func slot)
330
+ * — the caller lowers normally. ICEs (SC9001) never convert. */
331
+ function fenceClosureProbe(lowerer, node, slotType, attempt) {
332
+ if (!isJsSourceFile(node.getSourceFile()))
333
+ return null;
334
+ let inner = node;
335
+ while (ts.isParenthesizedExpression(inner))
336
+ inner = inner.expression;
337
+ if (!ts.isArrowFunction(inner) && !ts.isFunctionExpression(inner) && !ts.isMethodDeclaration(inner)) {
338
+ return null;
339
+ }
340
+ // The trap's ABI: the field's func slot when one exists; otherwise (the
341
+ // dyn-object literal path — the value boxes into dyn) the all-dyn
342
+ // signature of the lambda's own arity, which canBoxFuncIntoDyn always
343
+ // admits.
344
+ const fieldType = slotType !== undefined && slotType.kind === "func"
345
+ ? slotType
346
+ : { kind: "func", params: inner.parameters.map(() => DYN), ret: DYN };
347
+ if (slotType !== undefined && slotType.kind !== "func")
348
+ return null;
349
+ const diagsBefore = lowerer.diags.length;
350
+ try {
351
+ return attempt();
352
+ }
353
+ catch (e) {
354
+ if (!(e instanceof PoisonError))
355
+ throw e;
356
+ const params = fieldType.params.map((t, i) => ({ localId: `p.${i}`, name: `p${i}`, type: t }));
357
+ const fence = lowerer.deferToRuntimeFence(diagsBefore, node, {
358
+ kind: "closure",
359
+ name: () => `%fence.fn.${lowerer.liftedFns.length}`,
360
+ params,
361
+ returnType: fieldType.ret,
362
+ paramsMutable: true,
363
+ type: fieldType,
364
+ fallback: { code: "SC1090", message: "this function's body has no static lowering" },
365
+ bareMessage: true,
366
+ allowDiagSink: true,
367
+ });
368
+ if (!fence)
369
+ throw e;
370
+ return fence;
371
+ }
372
+ }
373
+ /** The union arm an object LITERAL inhabits when its fields must widen
374
+ * PER FIELD into exactly one record arm — the reducer-action pattern
375
+ * (`{ kind: "a", parsed: 1 }` into `{ kind: "a"; parsed: number | null }
376
+ * | { kind: "b" }`). The IR shapes erased the literal types tsc
377
+ * discriminated on, so the probe runs against the contextual union's
378
+ * CHECKER members: every literal field must exist on the member
379
+ * (excess-property freshness — tsc already enforced it), every arm field
380
+ * missing from the literal must be optional-flavored (an undefined-armed
381
+ * union, or a dyn slot — the absent-completion rule), and each present
382
+ * field's LITERAL type must fit the member's field type — literal against
383
+ * literal decides by VALUE (the discriminant), everything else by the
384
+ * widened IR pair under the width-lift relation. Exactly ONE fitting arm
385
+ * answers it; zero or several answer null and the caller keeps its
386
+ * fences. Plain property-assignment/shorthand literals only — spreads,
387
+ * accessors, methods, and unfoldable computed keys keep their own paths. */
388
+ function literalUnionArmOf(lowerer, expr, tsType, recordArms) {
389
+ if (!tsType.isUnionType())
390
+ return null;
391
+ const props = [];
392
+ for (const p of expr.properties) {
393
+ if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) {
394
+ props.push({ name: propNameText(lowerer, p.name), node: p.initializer });
395
+ }
396
+ else if (ts.isShorthandPropertyAssignment(p) && ts.isIdentifier(p.name)) {
397
+ props.push({ name: p.name.text, node: p.name });
398
+ }
399
+ else {
400
+ return null;
401
+ }
402
+ }
403
+ /** litT fits ftT: unions per arm; literal-vs-literal by value; unit
404
+ * types only into their own unit; otherwise the widened IR pair must be
405
+ * equal or width-liftable. */
406
+ const fits = (litT, ftT) => {
407
+ if (ftT.isUnionType())
408
+ return ts.constituentTypes(ftT).some((a) => fits(litT, a));
409
+ if (ftT.isStringLiteralType())
410
+ return litT.isStringLiteralType() && litT.value === ftT.value;
411
+ if (ftT.isNumberLiteralType())
412
+ return litT.isNumberLiteralType() && litT.value === ftT.value;
413
+ if (ftT.flags & ts.TypeFlags.BooleanLiteral) {
414
+ return (litT.flags & ts.TypeFlags.BooleanLiteral) !== 0 && lowerer.checker.typeToString(litT) === lowerer.checker.typeToString(ftT);
415
+ }
416
+ if (ftT.flags & ts.TypeFlags.Null)
417
+ return (litT.flags & ts.TypeFlags.Null) !== 0;
418
+ if (ftT.flags & ts.TypeFlags.Undefined)
419
+ return (litT.flags & ts.TypeFlags.Undefined) !== 0;
420
+ if (litT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined))
421
+ return false;
422
+ const li = lowerer.mapTypeOf(lowerer.checker.getBaseTypeOfLiteralType(litT));
423
+ const fi = lowerer.mapTypeOf(ftT);
424
+ if (!li || !fi)
425
+ return false;
426
+ return typeEquals(li, fi) || lowerer.widthLiftPlan(li, fi) !== null;
427
+ };
428
+ const armShapeIds = new Set(recordArms.map((a) => a.shapeId));
429
+ const candidates = new Set();
430
+ for (const member of ts.constituentTypes(tsType)) {
431
+ const mMapped = lowerer.mapTypeOf(member);
432
+ if (mMapped?.kind !== "record" || !armShapeIds.has(mMapped.shapeId) || candidates.has(mMapped.shapeId))
433
+ continue;
434
+ const shape = lowerer.shapes.get(mMapped.shapeId);
435
+ if (!shape || shape.tuple || shape.indexValue)
436
+ continue;
437
+ // Excess-property freshness: every literal field must exist on the
438
+ // member (tsc rejected the others for a fresh literal).
439
+ if (!props.every((p) => lowerer.checker.getPropertyOfType(member, p.name) !== undefined))
440
+ continue;
441
+ // Arm fields the literal leaves unset must be optional-flavored (the
442
+ // absent-completion rule: an undefined-armed union or a dyn slot).
443
+ const names = new Set(props.map((p) => p.name));
444
+ const absentOk = shape.fields.every((f) => {
445
+ if (names.has(f.name))
446
+ return true;
447
+ if (f.type.kind === "dyn")
448
+ return true;
449
+ if (f.type.kind !== "union")
450
+ return false;
451
+ return lowerer.unions.get(f.type.unionId)?.arms.some((a) => a.kind === "undefinedT") ?? false;
452
+ });
453
+ if (!absentOk)
454
+ continue;
455
+ const fieldsFit = props.every((p) => {
456
+ const sym = lowerer.checker.getPropertyOfType(member, p.name);
457
+ if (!sym)
458
+ return false;
459
+ return fits(lowerer.typeOf(p.node), lowerer.checker.getTypeOfSymbol(sym));
460
+ });
461
+ if (fieldsFit)
462
+ candidates.add(mMapped.shapeId);
463
+ }
464
+ if (candidates.size !== 1)
465
+ return null;
466
+ const [only] = candidates;
467
+ return recordArms.find((a) => a.shapeId === only) ?? null;
468
+ }
469
+ /** Object spread treats nullish sources as empty. Preserve evaluation once,
470
+ * then turn `Record<K, V> | undefined` into either that record or a fresh
471
+ * empty record of the same represented shape. */
472
+ function lowerOptionalIndexSpreadSource(lowerer, source, loc) {
473
+ if (source.type.kind !== "union")
474
+ return null;
475
+ const def = lowerer.unions.get(source.type.unionId);
476
+ if (!def || def.arms.length !== 2)
477
+ return null;
478
+ const undefinedTag = def.arms.findIndex((arm) => arm.kind === "undefinedT");
479
+ const recordTag = def.arms.findIndex((arm) => arm.kind === "record");
480
+ if (undefinedTag < 0 || recordTag < 0)
481
+ return null;
482
+ const recordType = def.arms[recordTag];
483
+ if (recordType.kind !== "record")
484
+ return null;
485
+ const prefix = [];
486
+ let stable = source;
487
+ if (!isSafeToRepeat(source)) {
488
+ const local = lowerer.declareHiddenLocal("%spread", source.type);
489
+ prefix.push({ kind: "varDecl", localId: local.id, init: source, loc });
490
+ stable = varRef(local.id, source.type, loc);
491
+ }
492
+ const selected = {
493
+ kind: "ternary",
494
+ cond: { kind: "unionIsTag", unionId: source.type.unionId, tag: undefinedTag, negated: false, value: stable, type: BOOL, loc },
495
+ then: { kind: "recordLit", fields: [], type: recordType, loc },
496
+ else_: { kind: "unionNarrow", unionId: source.type.unionId, tag: recordTag, value: stable, type: recordType, loc },
497
+ type: recordType,
498
+ loc,
499
+ };
500
+ return prefix.length === 0 ? selected : { kind: "seqExpr", stmts: prefix, result: selected, type: recordType, loc };
501
+ }
502
+ export function lowerObjectLiteral(lowerer, expr) {
503
+ const loc = locOf(expr);
504
+ // The RUNTIME-KEYED literal (JS): a computed key that doesn't fold to a
505
+ // compile-time string means the literal's shape is not a compile-time
506
+ // fact — no record shape can hold it. The whole literal builds as a dyn
507
+ // object instead (`{ [field]: criteria, actual: 0, ... }` —
508
+ // test/common's _mustCallInner context), where keys are runtime string
509
+ // values. TypeScript keeps the record world and its fence: this shape
510
+ // only arises in checked-dynamic JS.
511
+ if (isJsSourceFile(expr.getSourceFile()) &&
512
+ expr.properties.some((p) => {
513
+ const n = ts.isSpreadAssignment(p) ? undefined : p.name;
514
+ return n !== undefined && ts.isComputedPropertyName(n) && literalComputedKey(lowerer, n) === null;
515
+ })) {
516
+ return lowerDynObjectLiteral(lowerer, expr);
517
+ }
518
+ // Syntax fence FIRST: unsupported member forms get their specific
519
+ // message even when they also make the literal's type unmappable
520
+ // (a getter or a `this`-returning method would otherwise surface as an
521
+ // opaque type fence on the whole literal).
522
+ for (const prop of expr.properties) {
523
+ if (ts.isSpreadAssignment(prop)) {
524
+ // Supported shapes: FULL spreads of known record shapes read as
525
+ // identifiers or other side-effect-free reads, all BEFORE any
526
+ // explicit property (the field-copy desugar reads spread fields
527
+ // first, exactly JS's eager copy order); and CONDITIONAL spreads
528
+ // `...(c ? {k: v} : {})` — the optional-field idiom tsc types as
529
+ // `k?: ...` — which desugar to one conditional field at the
530
+ // spread's own position (any position: they introduce one fresh
531
+ // name, collision-fenced below). Everything else stays fenced.
532
+ const cs = conditionalSpreadOf(prop.expression);
533
+ if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
534
+ lowerer.unsupported("SC1090", prop, "conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm — spell other shapes as optional fields)");
535
+ }
536
+ // Spread ORDER is fenced in the field-by-field desugar below, not
537
+ // here: the index-signature merge path supports any order (keyed
538
+ // last-write-wins writes), so `{ K: v, ...extra }` into a pure
539
+ // Record shape compiles — the buildServiceEnv pattern.
540
+ continue;
541
+ }
542
+ if (ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop)) {
543
+ // JavaScript CJS-export tables reach reads through the lifted-
544
+ // accessor path (cjsExportAccessorRead) — the literal VALUE
545
+ // narrows to its plain fields below (accessor names are not
546
+ // record storage; SEMANTICS.md documents the enumeration
547
+ // divergence). TypeScript accessors lower into the shape's
548
+ // reserved closure slots (%get:/%set: — see accessorSlotProp);
549
+ // `this` in the body is fenced up front: the closure slot has no
550
+ // receiver (capturing the record under construction would be an
551
+ // RC cycle), and the generic lexical-this walk would silently
552
+ // capture an ENCLOSING method's `this` — the object-method rule.
553
+ if (!isJsSourceFile(expr.getSourceFile())) {
554
+ rejectThisInObjectAccessor(lowerer, prop.body ?? prop);
555
+ }
556
+ }
557
+ // Unfoldable computed keys are NOT fenced here: an index-signature
558
+ // target lowers them as runtime keyed writes (the merge path below);
559
+ // every other target re-fences them after the merge path declined
560
+ // (fenceUnfoldableComputedKeys).
561
+ // Identifier keys, STRING-LITERAL keys (`"content-type": v` —
562
+ // record field names are data, never C identifiers; the mangler
563
+ // encodes what C can't spell), NUMERIC-LITERAL keys in their
564
+ // canonical string spelling (`{ 0: v }` stores "0", `{ 0x10: v }`
565
+ // stores "16" — JS's ToPropertyKey; the checker names the property
566
+ // symbol the same way), and FOLDABLE computed keys (above).
567
+ if (!prop.name ||
568
+ !(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ||
569
+ ts.isNumericLiteral(prop.name) || ts.isComputedPropertyName(prop.name))) {
570
+ lowerer.unsupported("SC1090", prop, "non-identifier property names");
571
+ }
572
+ // Shorthand methods are just closure-valued fields — but their `this`
573
+ // is dynamically bound (typed as the literal by tsc, so noImplicitThis
574
+ // lets it through) and records don't model it; the generic
575
+ // lexical-this walk would silently capture an ENCLOSING method's
576
+ // `this`, so any `this` in the body is rejected up front.
577
+ if (ts.isMethodDeclaration(prop))
578
+ lowerer.rejectThisInObjectMethod(prop.body ?? prop);
579
+ }
580
+ let tsType = lowerer.checker.getContextualType(expr) ?? lowerer.typeOf(expr);
581
+ // `lit satisfies T` is TYPE-LEVEL only: the expression's checker type —
582
+ // and therefore the shape every downstream consumer sees — is the
583
+ // literal's OWN type (T still contextually types members, so inferred
584
+ // parameter types flow). Building at T would reshape the value tsc
585
+ // says has the literal's type: `{...} satisfies Movable &
586
+ // Record<string, unknown>` must NOT become an index-signature record.
587
+ // Own type wins whenever it maps to a record; an unmappable own type
588
+ // keeps the contextual fallback (a bare-null field whose satisfies
589
+ // target names the wider slot type).
590
+ {
591
+ let p = expr.parent;
592
+ while (ts.isParenthesizedExpression(p))
593
+ p = p.parent;
594
+ if (ts.isSatisfiesExpression(p)) {
595
+ const own = lowerer.typeOf(expr);
596
+ if (lowerer.mapTypeOf(own)?.kind === "record")
597
+ tsType = own;
598
+ }
599
+ }
600
+ // An async function's return position types the literal
601
+ // `T | PromiseLike<T>` (the lib's await-unwrapping contract). The
602
+ // PromiseLike arm never maps, and the record the return slot actually
603
+ // holds is exactly the checker's awaited type — strip to it BEFORE the
604
+ // own-type fallback below: the awaited contextual type carries the
605
+ // slot's field types (`lanIp: string | null`), which the literal's own
606
+ // type narrows away (a field written as `lanIp: null` types as bare
607
+ // `null`, which maps to nothing on its own).
608
+ if (tsType.isUnionType() && ts.constituentTypes(tsType).some((t) => t.getSymbol()?.name === "PromiseLike")) {
609
+ tsType = lowerer.checker.getAwaitedType(tsType) ?? tsType;
610
+ }
611
+ let mapped = lowerer.mapTypeOf(tsType);
612
+ // An EMPTY-record context under a NON-empty literal (`Object.keys({
613
+ // ...process.env })` — the lib's `{}`-typed parameters admit every
614
+ // object): `{}` carries no shape information, so the literal builds as
615
+ // its OWN type, exactly the unmappable-context fallback below. An
616
+ // empty LITERAL keeps the context (the shapes agree).
617
+ if (mapped?.kind === "record" && expr.properties.length > 0) {
618
+ const ctxShape = lowerer.shapes.get(mapped.shapeId);
619
+ if (ctxShape && ctxShape.fields.length === 0 && !ctxShape.indexValue && !ctxShape.tuple) {
620
+ mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped;
621
+ }
622
+ }
623
+ // A literal with a property the contextual TYPE ITSELF lacks — only
624
+ // reachable through type assertions and satisfies (fresh-literal
625
+ // excess-property checks reject the direct spelling): the context
626
+ // cannot hold the value, so the literal builds at its OWN type and the
627
+ // slot's width coercion narrows it (divergence 36's copy stance — the
628
+ // extra fields drop in the copy). The probe is against the CHECKER's
629
+ // contextual type, not the mapped shape: a property the contextual
630
+ // type carries but the shape dropped (settled value/reason,
631
+ // generic-callable members) belongs to the drop paths below, and
632
+ // index-signature contexts keep every key (overflow capture).
633
+ if (mapped?.kind === "record" && expr.properties.length > 0) {
634
+ const ctxShape = lowerer.shapes.get(mapped.shapeId);
635
+ if (ctxShape && !ctxShape.indexValue && !ctxShape.tuple) {
636
+ const names = new Set(ctxShape.fields.map((f) => f.name));
637
+ const ctxHasProp = (name) => {
638
+ const members = tsType.isUnionType() ? ts.constituentTypes(tsType) : [tsType];
639
+ return members.some((m) => lowerer.checker.getPropertyOfType(m, name) !== undefined);
640
+ };
641
+ const extraOf = (text) => !names.has(text) && !ctxHasProp(text);
642
+ const extra = expr.properties.some((p) => {
643
+ if (ts.isSpreadAssignment(p) || !p.name)
644
+ return false;
645
+ if (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))
646
+ return extraOf(p.name.text);
647
+ if (ts.isNumericLiteral(p.name))
648
+ return extraOf(String(Number(p.name.text)));
649
+ return false; // computed keys keep their existing paths
650
+ });
651
+ if (extra)
652
+ mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped;
653
+ }
654
+ }
655
+ // A union-typed slot (`const r: Res = { kind: "ok", ... }`) contextually
656
+ // types the literal as the WHOLE union; build the literal as its own
657
+ // (arm) shape and let the slot's coercion wrap it into the union. An
658
+ // unknown-typed slot (`JSON.stringify({ a: 1 })`) likewise. An
659
+ // UNMAPPABLE context falls back the same way — the literal's own type
660
+ // is the plain record the slot coerces. A CLASS-INSTANCE context
661
+ // (`const p: Point = { x, y }` — tsc's structural view of data
662
+ // classes) falls back too: the literal builds as its own record and
663
+ // the slot's width coercion constructs through the trivial
664
+ // parameter-property constructor (recordToClassPlan), or fences with
665
+ // the record-shape story.
666
+ // A jsval-mapped context reaches here only when lowerExpr's island
667
+ // gate DECLINED it (a project-declared typedef that absorbed to the
668
+ // island through checker-`any` field residue): the literal builds at
669
+ // its own type like every unmappable context.
670
+ if (mapped === null || mapped.kind === "union" || mapped.kind === "dyn" || mapped.kind === "object" || mapped.kind === "jsval") {
671
+ const ctxUnion = mapped?.kind === "union" ? mapped : null;
672
+ mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)) ?? mapped;
673
+ // A literal whose own shape re-tags into NO arm of the contextual
674
+ // union, where the union has exactly ONE record arm: build AS that
675
+ // arm — there is no ambiguity (tsc already checked the literal
676
+ // against the union, and the record arm is the only shape it can
677
+ // inhabit), and the arm's field types drive every property's
678
+ // coercion (`{ env: {...spread...}, onCleanup }` against an
679
+ // optional options param — the env value builds by ITS contextual
680
+ // index-signature type and wraps into the arm's `| undefined`
681
+ // field, where the literal's own inferred width would mismatch).
682
+ // Empty literals (`_env = {}` — the fieldless own shape) and
683
+ // literals against PURE index-signature arms (`{ OPENSSL_CONF:
684
+ // candidate }` — keys become overflow entries) are the same rule's
685
+ // simplest cases. The empty-array-in-union rule, record form.
686
+ if (ctxUnion) {
687
+ const def = lowerer.unions.get(ctxUnion.unionId);
688
+ const recordArms = def?.arms.filter((a) => a.kind === "record") ?? [];
689
+ if (recordArms.length === 1) {
690
+ const armShape = lowerer.shapes.get(recordArms[0].shapeId);
691
+ if ((mapped?.kind !== "record" || mapped.shapeId !== recordArms[0].shapeId) &&
692
+ !armShape?.tuple) {
693
+ mapped = recordArms[0];
694
+ }
695
+ }
696
+ else if (recordArms.length > 1) {
697
+ const ownShapeId = mapped?.kind === "record" ? mapped.shapeId : null;
698
+ // SEVERAL record arms (the reducer-action / discriminated-message
699
+ // pattern): the literal's own inferred shape re-tags into no arm
700
+ // — its field types widened per field (`parsed: 1` against
701
+ // `parsed: number | null`), so the IR-level candidate probe is
702
+ // ambiguous (a narrower arm also admits the literal by dropping
703
+ // fields). The LITERAL types tsc checked carry the discriminant
704
+ // the shapes erased: match the literal's fields against each
705
+ // union member's CHECKER types (literal-vs-literal field pairs
706
+ // decide by value — the `kind: "a"` discriminant), and when
707
+ // exactly ONE member fits, build AS that arm — its field types
708
+ // drive every property's coercion, exactly the single-record-arm
709
+ // rule above. Ambiguous literals keep the SC2003 fence.
710
+ if (ownShapeId === null || !recordArms.some((a) => a.shapeId === ownShapeId)) {
711
+ const arm = literalUnionArmOf(lowerer, expr, tsType, recordArms);
712
+ if (arm)
713
+ mapped = arm;
714
+ }
715
+ }
716
+ }
717
+ }
718
+ // The CJS EXPORT-TABLE literal in VALUE position (JS): importers reach
719
+ // every member through alias plumbing and accessor lifts — the record
720
+ // VALUE exists for the module's own reads (Object.keys, internal
721
+ // member reads), so it keeps exactly the plain fields whose values
722
+ // lower; accessor entries and members with no value representation
723
+ // (rest-param function types) narrow away. SEMANTICS.md documents the
724
+ // enumeration divergence.
725
+ if (isJsSourceFile(expr.getSourceFile()) &&
726
+ (!mapped || expr.properties.some((p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p))) &&
727
+ isCjsExportTableLiteral(expr)) {
728
+ const fields = [];
729
+ for (const prop of expr.properties) {
730
+ if (ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop))
731
+ continue;
732
+ const propName = ts.isSpreadAssignment(prop) ? undefined : prop.name;
733
+ if (!propName || !(ts.isIdentifier(propName) || ts.isStringLiteral(propName)))
734
+ continue;
735
+ const name = propName.text;
736
+ let v = null;
737
+ if (ts.isShorthandPropertyAssignment(prop)) {
738
+ v = tryLowerExpression(lowerer, propName);
739
+ }
740
+ else if (ts.isPropertyAssignment(prop)) {
741
+ v = tryLowerExpression(lowerer, prop.initializer);
742
+ }
743
+ if (!v || v.type.kind === "void" || v.type.kind === "caught" || v.type.kind === "jsval")
744
+ continue;
745
+ if (isUnitType(v.type))
746
+ continue;
747
+ fields.push({ name, value: v });
748
+ }
749
+ // Canonical (sorted) field order — the shape registry's invariant;
750
+ // the dropped reads are all pure, so reordering loses nothing.
751
+ fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
752
+ const shapeId = lowerer.shapes.intern(fields.map((f) => ({ name: f.name, type: f.value.type })));
753
+ return { kind: "recordLit", fields, type: { kind: "record", shapeId }, loc };
754
+ }
755
+ // The JS declaration fallback, literal-side (the checked-dynamic tree-array rule's
756
+ // object form): a literal whose shape has no static home — an
757
+ // unmappable contextual type over unmappable own fields (the
758
+ // PropertyDescriptorMap argument of Object.defineProperties, nested
759
+ // descriptor records with `any` values) — builds as a dyn OBJECT:
760
+ // each field converts through the usual dyn boundary, and dynamic
761
+ // consumers ride the keyed-dyn paths. TypeScript keeps the fence.
762
+ if ((!mapped || mapped.kind === "dyn") && isJsSourceFile(expr.getSourceFile())) {
763
+ return lowerDynObjectLiteral(lowerer, expr);
764
+ }
765
+ if (!mapped || mapped.kind !== "record")
766
+ lowerer.badType(expr, tsType);
767
+ let type = mapped;
768
+ let shape = lowerer.shapes.get(type.shapeId);
769
+ // ACCESSOR properties, JS literals only (TS accessors fill the shape's
770
+ // %get:/%set: closure slots below): no record storage exists for them,
771
+ // so the literal's shape NARROWS to its plain fields (reads resolve
772
+ // through the lifted accessors; Object.keys over the value omits
773
+ // accessor names — the documented divergence).
774
+ if (isJsSourceFile(expr.getSourceFile())) {
775
+ const accessorNames = new Set(expr.properties
776
+ .filter((p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p))
777
+ .map((p) => (p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) ? p.name.text : ""))
778
+ .filter((n) => n !== ""));
779
+ if (accessorNames.size > 0) {
780
+ const narrowed = shape.fields.filter((f) => !accessorNames.has(f.name));
781
+ const narrowedId = lowerer.shapes.intern(narrowed.map((f) => ({ name: f.name, type: f.type })), false, shape.indexValue);
782
+ type = { kind: "record", shapeId: narrowedId };
783
+ shape = lowerer.shapes.get(narrowedId);
784
+ }
785
+ }
786
+ const fieldTypes = new Map(shape.fields.map((f) => [f.name, f.type]));
787
+ // File-scope JavaScript object bindings live in the checked-dynamic tree
788
+ // to preserve open writes and identity. Build an optional-field literal
789
+ // from its actually written keys so Object.hasOwn can distinguish an
790
+ // omitted property from a present property whose value is undefined.
791
+ const topLevelJsDecl = ts.isVariableDeclaration(expr.parent) &&
792
+ expr.parent.initializer === expr &&
793
+ ts.isVariableStatement(expr.parent.parent.parent) &&
794
+ ts.isSourceFile(expr.parent.parent.parent.parent);
795
+ const assignedProperties = expr.properties.filter((prop) => ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop));
796
+ if (topLevelJsDecl && isJsSourceFile(expr.getSourceFile()) &&
797
+ assignedProperties.length === expr.properties.length) {
798
+ const provided = new Set(assignedProperties.map((prop) => propNameText(lowerer, prop.name)));
799
+ const omittedOptional = shape.fields.some((field) => !provided.has(field.name) && field.type.kind === "union" &&
800
+ lowerer.armTag(field.type.unionId, UNDEFINED_T) >= 0);
801
+ if (omittedOptional)
802
+ return lowerDynObjectLiteral(lowerer, expr);
803
+ }
804
+ // The clone probe may lower the leading spread before declining (a
805
+ // static `as` cast can make the checker-visible shape match while its
806
+ // erased IR value keeps a wider shape). Reuse that exact value in the
807
+ // ordinary spread path so lowering still happens once.
808
+ let leadingSpreadLowered = null;
809
+ // The overwhelmingly common immutable-update form over a record:
810
+ // `{ ...model, changed, other: value }`. The historic lowering expanded
811
+ // the spread into one recordGet per untouched field at EVERY site, then
812
+ // the backends inlined all of those retains and stores into the caller.
813
+ // A 178-field model updated hundreds of times consequently produced a
814
+ // half-million-line LLVM function. Keep the same evaluation/ownership
815
+ // semantics in one compact IR node: source first, explicit overrides in
816
+ // source order, and one backend clone helper per shape.
817
+ //
818
+ // Stay deliberately narrow. Tuple/index/accessor shapes, conditional or
819
+ // multiple spreads, spread-after-explicit order, and shape-changing width
820
+ // copies retain their existing lowering and diagnostics.
821
+ if (!isJsSourceFile(expr.getSourceFile()) &&
822
+ !shape.indexValue &&
823
+ !shape.tuple &&
824
+ !shapeHasAccessorSlots(shape) &&
825
+ expr.properties.length >= 2 &&
826
+ ts.isSpreadAssignment(expr.properties[0]) &&
827
+ !conditionalSpreadOf(expr.properties[0].expression) &&
828
+ expr.properties.slice(1).every((p) => (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
829
+ p.name !== undefined &&
830
+ (ts.isIdentifier(p.name) ||
831
+ ts.isStringLiteral(p.name) ||
832
+ ts.isNumericLiteral(p.name) ||
833
+ (ts.isComputedPropertyName(p.name) && literalComputedKey(lowerer, p.name) !== null)))) {
834
+ const spread = expr.properties[0];
835
+ const props = expr.properties.slice(1);
836
+ const names = props.map((p) => propNameText(lowerer, p.name));
837
+ const unique = new Set(names);
838
+ const sourceType = lowerer.mapTypeOf(lowerer.typeOf(spread.expression));
839
+ if (sourceType?.kind === "record" &&
840
+ sourceType.shapeId === type.shapeId &&
841
+ unique.size === names.length &&
842
+ names.every((name) => fieldTypes.has(name))) {
843
+ const source = lowerer.lowerExpr(spread.expression);
844
+ if (source.type.kind === "record" && source.type.shapeId === type.shapeId) {
845
+ const overrides = [];
846
+ for (let i = 0; i < props.length; i++) {
847
+ const p = props[i];
848
+ const name = names[i];
849
+ const fieldType = fieldTypes.get(name);
850
+ const valueNode = ts.isPropertyAssignment(p) ? p.initializer : p;
851
+ let value = ts.isPropertyAssignment(p)
852
+ ? lowerer.lowerExpr(p.initializer)
853
+ : lowerer.lowerShorthandValue(p);
854
+ value = lowerer.coerceInto(valueNode, value, fieldType);
855
+ if (!typeEquals(value.type, fieldType))
856
+ lowerer.badType(valueNode, lowerer.typeOf(valueNode));
857
+ overrides.push({ name, value });
858
+ }
859
+ return { kind: "recordClone", source, overrides, type, loc };
860
+ }
861
+ leadingSpreadLowered = source;
862
+ }
863
+ }
864
+ // A PURE index-signature target with spreads — `{ ...process.env }`,
865
+ // `{ ...process.env, ...extraEnv }`, `{ ...env, PATH: p }` (the
866
+ // spawn-env pattern): the field-by-field desugar below cannot
867
+ // enumerate runtime overflow keys, so the literal lowers as ONE
868
+ // interned merge-helper call — contributors apply in literal order
869
+ // with keyed writes (JS last-write-wins), sources and values evaluate
870
+ // once each in source order (the call's argument order). A CONDITIONAL
871
+ // spread `...(c ? { k: v } : {})` contributes its one key as a ternary
872
+ // — cond once, v lazily, the empty arm holding the value slot's
873
+ // undefined arm (the explicit-undefined-is-absent stance: JSON and
874
+ // child-env builders drop it, exactly Node's absent key); targets
875
+ // with declared fields keep the historic desugar below.
876
+ const isRuntimeComputedKey = (p) => !ts.isSpreadAssignment(p) &&
877
+ p.name !== undefined &&
878
+ ts.isComputedPropertyName(p.name) &&
879
+ literalComputedKey(lowerer, p.name) === null;
880
+ if (shape.indexValue &&
881
+ shape.fields.length === 0 &&
882
+ !shape.tuple &&
883
+ (expr.properties.some((p) => ts.isSpreadAssignment(p)) || expr.properties.some(isRuntimeComputedKey))) {
884
+ const contributors = [];
885
+ let mergeable = true;
886
+ for (const prop of expr.properties) {
887
+ if (ts.isSpreadAssignment(prop)) {
888
+ const cs = conditionalSpreadOf(prop.expression);
889
+ if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
890
+ lowerer.unsupported("SC1090", prop, "conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm)");
891
+ }
892
+ if (cs) {
893
+ const absent = lowerer.wrappedUndefined(shape.indexValue, locOf(prop));
894
+ if (!absent) {
895
+ 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)`);
896
+ }
897
+ const csProp = cs.props[0];
898
+ const cond = lowerer.lowerCondition(cs.cond);
899
+ const vNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp;
900
+ const v = lowerer.intoIndexValueSlot(ts.isPropertyAssignment(csProp) ? lowerer.lowerExpr(csProp.initializer) : lowerer.lowerShorthandValue(csProp), shape.indexValue, vNode);
901
+ contributors.push({
902
+ kind: "field",
903
+ name: csProp.name.text,
904
+ value: {
905
+ kind: "ternary",
906
+ cond,
907
+ then: cs.whenTrue ? v : absent,
908
+ else_: cs.whenTrue ? absent : v,
909
+ type: shape.indexValue,
910
+ loc: locOf(prop),
911
+ },
912
+ });
913
+ continue;
914
+ }
915
+ let src = lowerer.lowerExpr(prop.expression);
916
+ src = lowerOptionalIndexSpreadSource(lowerer, src, locOf(prop)) ?? src;
917
+ if (src.type.kind !== "record") {
918
+ lowerer.unsupported("SC1090", prop, `object spread of '${lowerer.fmt(src.type)}' sources into an index-signature shape (only index-signature records merge — ${NARROW_FIRST})`);
919
+ }
920
+ fenceAccessorSpreadSource(lowerer, prop, lowerer.shapes.get(src.type.shapeId));
921
+ contributors.push({ kind: "spread", shapeId: src.type.shapeId, value: src });
922
+ continue;
923
+ }
924
+ if ((ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) &&
925
+ prop.name &&
926
+ (ts.isIdentifier(prop.name) ||
927
+ ts.isStringLiteral(prop.name) ||
928
+ ts.isNumericLiteral(prop.name) ||
929
+ (ts.isComputedPropertyName(prop.name) && literalComputedKey(lowerer, prop.name) !== null))) {
930
+ const value = ts.isPropertyAssignment(prop)
931
+ ? lowerer.intoIndexValueSlot(lowerer.lowerExpr(prop.initializer), shape.indexValue, prop.initializer)
932
+ : lowerer.intoIndexValueSlot(lowerer.lowerShorthandValue(prop), shape.indexValue, prop);
933
+ contributors.push({ kind: "field", name: propNameText(lowerer, prop.name), value });
934
+ continue;
935
+ }
936
+ // A RUNTIME-keyed property (`{ ...m, ["a" + "b"]: "" }`, `{ [K]:
937
+ // v }` where K is a runtime string): the key evaluates before its
938
+ // value (JS's per-property order — both are helper arguments) and
939
+ // writes through the signature exactly like a spread's keys.
940
+ // Number/boolean/unknown keys stringify (ToPropertyKey).
941
+ if (ts.isPropertyAssignment(prop) && ts.isComputedPropertyName(prop.name)) {
942
+ let k = lowerer.lowerExpr(prop.name.expression);
943
+ if (k.type.kind === "f64" || k.type.kind === "bool" || k.type.kind === "dyn") {
944
+ k = { kind: "toString", operand: k, type: STRING, loc: locOf(prop.name) };
945
+ }
946
+ if (k.type.kind !== "string") {
947
+ lowerer.unsupported("SC1090", prop.name, `'${lowerer.fmt(k.type)}'-typed computed property keys (string, number, boolean, and unknown keys stringify)`);
948
+ }
949
+ const value = lowerer.intoIndexValueSlot(lowerer.lowerExpr(prop.initializer), shape.indexValue, prop.initializer);
950
+ contributors.push({ kind: "keyedField", key: k, value });
951
+ continue;
952
+ }
953
+ mergeable = false;
954
+ break;
955
+ }
956
+ if (mergeable) {
957
+ const helper = lowerIndexMergeHelper(lowerer, type.shapeId, contributors, loc);
958
+ if (helper === null) {
959
+ 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)`);
960
+ }
961
+ return {
962
+ kind: "call",
963
+ callee: helper,
964
+ args: contributors.flatMap((c) => (c.kind === "keyedField" ? [c.key, c.value] : [c.value])),
965
+ type,
966
+ loc,
967
+ };
968
+ }
969
+ }
970
+ // Unfoldable computed keys outside the index-signature merge path:
971
+ // no record shape can hold a runtime-decided name.
972
+ for (const prop of expr.properties) {
973
+ if (isRuntimeComputedKey(prop)) {
974
+ 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)");
975
+ }
976
+ }
977
+ // A DECLARED-fields target built ENTIRELY from index-signature spreads —
978
+ // `{ ...Object.fromEntries(...), ...Object.fromEntries(...) }` typed
979
+ // AppConfig (the defaults-merge idiom over runtime-keyed sources): the
980
+ // field-by-field desugar cannot enumerate runtime keys, so the literal
981
+ // lowers as ONE interned merge-helper call — sources evaluate once each
982
+ // in source order (the call's argument order — computed sources
983
+ // included, no re-read), contributors apply in order with per-key
984
+ // dispatch onto the declared fields (JS last-write-wins). Divergence 68
985
+ // has the runtime rules (validated collisions, extra keys dropped).
986
+ if (!shape.indexValue &&
987
+ !shape.tuple &&
988
+ shape.fields.length > 0 &&
989
+ expr.properties.length > 0 &&
990
+ expr.properties.every((p) => ts.isSpreadAssignment(p) && !conditionalSpreadOf(p.expression)) &&
991
+ expr.properties.some((p) => {
992
+ const t = lowerer.mapTypeOf(lowerer.typeOf(p.expression));
993
+ return t?.kind === "record" && !!lowerer.shapes.get(t.shapeId)?.indexValue;
994
+ })) {
995
+ return lowerDeclaredSpreadMerge(lowerer, expr, type, shape, loc);
996
+ }
997
+ const shapeMismatch = (node) => {
998
+ const own = lowerer.mapTypeOf(lowerer.typeOf(expr));
999
+ lowerer.pushDiag(recordShapeMismatchDiag(lowerer.fmt(type), own ? lowerer.fmt(own) : lowerer.checker.typeToString(lowerer.typeOf(expr)), locOf(node), own?.kind === "record" && type.kind === "record"
1000
+ ? (lowerer.describeRecordWidthBlocker(own.shapeId, type.shapeId) ?? undefined)
1001
+ : undefined));
1002
+ throw new PoisonError();
1003
+ };
1004
+ const fields = [];
1005
+ // Field names introduced by conditional spreads: their ternary carries
1006
+ // the spread's whole evaluation (cond once, value lazily), so a LATER
1007
+ // contributor overriding one would silently drop that evaluation —
1008
+ // collisions fence in both directions.
1009
+ const conditionalNames = new Set();
1010
+ // Member names DROPPED from the shape (JS deferral — unlowerable pure
1011
+ // member reads narrow away; see the catch in the property loop).
1012
+ const droppedNames = new Set();
1013
+ for (const prop of expr.properties) {
1014
+ if (ts.isSpreadAssignment(prop)) {
1015
+ const cs = conditionalSpreadOf(prop.expression);
1016
+ if (cs && cs !== "unsupported") {
1017
+ // `...(c ? { k: v } : {})` — ONE conditional field at the
1018
+ // spread's own position: cond evaluates exactly once, `v` only
1019
+ // when the non-empty arm is taken (ternary arms are lazy), and
1020
+ // the empty arm holds the interned undefined arm — exactly the
1021
+ // omitted-optional-field representation, which is also why the
1022
+ // target field must be optional (tsc types the idiom that way).
1023
+ const csProp = cs.props[0]; // single-prop-checked in the fence pass
1024
+ const name = csProp.name.text;
1025
+ const fieldType = fieldTypes.get(name);
1026
+ if (!fieldType) {
1027
+ if (shape.indexValue) {
1028
+ lowerer.unsupported("SC1090", prop, "conditional spreads into index-signature keys (overflow entries model presence — write the key in an if statement instead)");
1029
+ }
1030
+ throw shapeMismatch(prop);
1031
+ }
1032
+ const absent = lowerer.wrappedUndefined(fieldType, locOf(prop));
1033
+ if (!absent) {
1034
+ lowerer.unsupported("SC1090", prop, `conditional spreads onto the required field '${name}' (the empty arm leaves it undefined — declare the field optional)`);
1035
+ }
1036
+ if (fields.some((f) => f.name === name)) {
1037
+ 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)`);
1038
+ }
1039
+ const cond = lowerer.lowerCondition(cs.cond);
1040
+ const valueNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp;
1041
+ let v = ts.isPropertyAssignment(csProp)
1042
+ ? lowerer.lowerExpr(csProp.initializer)
1043
+ : lowerer.lowerShorthandValue(csProp);
1044
+ v = lowerer.coerceInto(valueNode, v, fieldType);
1045
+ if (!typeEquals(v.type, fieldType))
1046
+ lowerer.badType(valueNode, lowerer.typeOf(valueNode));
1047
+ conditionalNames.add(name);
1048
+ fields.push({
1049
+ name,
1050
+ value: {
1051
+ kind: "ternary",
1052
+ cond,
1053
+ then: cs.whenTrue ? v : absent,
1054
+ else_: cs.whenTrue ? absent : v,
1055
+ type: fieldType,
1056
+ loc: locOf(prop),
1057
+ },
1058
+ });
1059
+ continue;
1060
+ }
1061
+ // `{ ...base, ... }` — field-by-field copy of a known record
1062
+ // shape. Every source field must land on the target shape with an
1063
+ // equal type (a wider source would silently DROP fields JS keeps —
1064
+ // the width fence, same as literals). Later contributors override
1065
+ // earlier ones (JS last-write-wins; the reads are side-effect-free,
1066
+ // so dropping the earlier read is exact). Identifier sources
1067
+ // re-read per field (historic path); any OTHER source must be a
1068
+ // re-emittable pure read, sharing one lowered node per field.
1069
+ // The desugar's one-entry-per-name list reads spread fields
1070
+ // EAGERLY at the spread's position, so an explicit property
1071
+ // BEFORE a spread would need JS's overwrite — order-fenced (the
1072
+ // index-signature merge path above takes any order).
1073
+ if (expr.properties
1074
+ .slice(0, expr.properties.indexOf(prop))
1075
+ .some((p) => !ts.isSpreadAssignment(p))) {
1076
+ 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)");
1077
+ }
1078
+ let srcNode = prop.expression;
1079
+ while (ts.isParenthesizedExpression(srcNode))
1080
+ srcNode = srcNode.expression;
1081
+ const srcLowered = prop === expr.properties[0] && leadingSpreadLowered !== null
1082
+ ? leadingSpreadLowered
1083
+ : ts.isIdentifier(srcNode)
1084
+ ? null
1085
+ : lowerer.lowerExpr(srcNode);
1086
+ const srcType = srcLowered ? srcLowered.type : lowerer.mapTypeOf(lowerer.typeOf(srcNode));
1087
+ // `...options.installConfig` — a spread of `Partial<X> | undefined`
1088
+ // (the optional-options merge idiom `{ ...DEFAULTS, ...overrides }`):
1089
+ // JS spreads nothing for the unit arm and copies present keys
1090
+ // otherwise. Per target field the desugar builds
1091
+ // `present ? extracted : earlier` — present tests the source's
1092
+ // record tag AND (optional source fields) the field's own value
1093
+ // arm; absent keeps the earlier contributor's value (both reads
1094
+ // are pure, so the reorder into the ternary is unobservable).
1095
+ if (srcType?.kind === "union") {
1096
+ const def = lowerer.unions.get(srcType.unionId);
1097
+ const recArms = def?.arms.filter((a) => a.kind === "record") ?? [];
1098
+ if (!def ||
1099
+ recArms.length !== 1 ||
1100
+ !def.arms.every((a) => a.kind === "record" || isUnitType(a))) {
1101
+ lowerer.unsupported("SC1090", prop, `object spread of '${lowerer.fmt(srcType)}' sources (only known record shapes spread — ${NARROW_FIRST})`);
1102
+ }
1103
+ if (srcLowered && !isSafeToRepeat(srcLowered)) {
1104
+ lowerer.unsupported("SC1090", prop, "object spread of computed sources (the field copies re-read the source — bind it to a const first)");
1105
+ }
1106
+ const recArm = recArms[0];
1107
+ const recTag = def.arms.indexOf(recArm);
1108
+ // A checked-dynamic VALUE under the union-mapped checker type
1109
+ // (a JS dyn-holding binding): the present/absent desugar tests
1110
+ // union tags a dyn box does not carry — fence honestly instead
1111
+ // of the validator's ICE.
1112
+ const probedSrc = srcLowered ?? tryLowerExpression(lowerer, srcNode);
1113
+ if (probedSrc?.type.kind === "dyn") {
1114
+ lowerer.unsupported("SC1090", prop, `object spread of a checked-dynamic '${lowerer.fmt(srcType)}' source (${NARROW_FIRST})`);
1115
+ }
1116
+ const srcShape = lowerer.shapes.get(recArm.shapeId);
1117
+ if (!srcShape)
1118
+ throw new InternalCompilerError(`lowerer bug: spread of unknown shape ${recArm.shapeId}`);
1119
+ fenceAccessorSpreadSource(lowerer, prop, srcShape);
1120
+ if (srcShape.indexValue || shape.indexValue) {
1121
+ lowerer.unsupported("SC1090", prop, "object spread involving index-signature shapes (overflow keys are runtime state — copy the fields you need explicitly)");
1122
+ }
1123
+ const laterNames = new Set();
1124
+ for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
1125
+ if (ts.isSpreadAssignment(later)) {
1126
+ if (conditionalSpreadOf(later.expression))
1127
+ continue;
1128
+ const lt = lowerer.mapTypeOf(lowerer.typeOf(later.expression));
1129
+ if (lt?.kind === "record") {
1130
+ for (const lf of lowerer.shapes.get(lt.shapeId)?.fields ?? [])
1131
+ laterNames.add(lf.name);
1132
+ }
1133
+ continue;
1134
+ }
1135
+ if (later.name &&
1136
+ (ts.isIdentifier(later.name) ||
1137
+ ts.isStringLiteral(later.name) ||
1138
+ ts.isNumericLiteral(later.name) ||
1139
+ (ts.isComputedPropertyName(later.name) && literalComputedKey(lowerer, later.name) !== null))) {
1140
+ laterNames.add(propNameText(lowerer, later.name));
1141
+ }
1142
+ }
1143
+ const srcRef = () => srcLowered ?? lowerer.lowerExpr(srcNode);
1144
+ for (const f of srcShape.fields) {
1145
+ if (laterNames.has(f.name))
1146
+ continue;
1147
+ const targetType = fieldTypes.get(f.name);
1148
+ // No slot on the target shape: the copy DROPS the field
1149
+ // (divergence 36's stance, same as the plain-record spread).
1150
+ if (!targetType)
1151
+ continue;
1152
+ if (conditionalNames.has(f.name)) {
1153
+ 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)`);
1154
+ }
1155
+ const fRead = () => ({
1156
+ kind: "recordGet",
1157
+ obj: { kind: "unionNarrow", unionId: srcType.unionId, tag: recTag, value: srcRef(), type: recArm, loc: locOf(prop) },
1158
+ shapeId: recArm.shapeId,
1159
+ field: f.name,
1160
+ type: f.type,
1161
+ loc: locOf(prop),
1162
+ });
1163
+ let cond = { kind: "unionIsTag", unionId: srcType.unionId, tag: recTag, negated: false, value: srcRef(), type: BOOL, loc: locOf(prop) };
1164
+ let thenVal;
1165
+ if (typeEquals(f.type, targetType)) {
1166
+ thenVal = fRead();
1167
+ }
1168
+ else if (f.type.kind === "union" &&
1169
+ lowerer.armTag(f.type.unionId, UNDEFINED_T) >= 0 &&
1170
+ typeEquals(lowerer.stripUndefinedArm(f.type), targetType)) {
1171
+ // Optional source field into a required target slot: present
1172
+ // means the source holds the record AND the field its value
1173
+ // arm — the spread-override completion, union-source form.
1174
+ const ftUndef = lowerer.armTag(f.type.unionId, UNDEFINED_T);
1175
+ cond = {
1176
+ kind: "logical",
1177
+ op: "&&",
1178
+ left: cond,
1179
+ right: { kind: "unionIsTag", unionId: f.type.unionId, tag: ftUndef, negated: true, value: fRead(), type: BOOL, loc: locOf(prop) },
1180
+ type: BOOL,
1181
+ loc: locOf(prop),
1182
+ };
1183
+ const ftDef = lowerer.unions.get(f.type.unionId);
1184
+ if (targetType.kind === "union") {
1185
+ const retag = lowerer.unionRetagHelper(f.type.unionId, targetType.unionId, locOf(prop));
1186
+ if (!retag) {
1187
+ 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`));
1188
+ throw new PoisonError();
1189
+ }
1190
+ thenVal = { kind: "call", callee: retag, args: [fRead()], type: targetType, loc: locOf(prop) };
1191
+ }
1192
+ else if (ftDef && ftDef.arms.length === 2 && lowerer.armTag(f.type.unionId, targetType) >= 0) {
1193
+ thenVal = { kind: "unionNarrow", unionId: f.type.unionId, tag: lowerer.armTag(f.type.unionId, targetType), value: fRead(), type: targetType, loc: locOf(prop) };
1194
+ }
1195
+ else {
1196
+ 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`));
1197
+ throw new PoisonError();
1198
+ }
1199
+ }
1200
+ else {
1201
+ // The width-lift fallback (arm wrap, re-tag, nested
1202
+ // reshape) — the same per-field rule the slot coercion
1203
+ // applies. Runs AFTER the optional-completion branch: a
1204
+ // present-test has its own semantics a re-tag's stranded
1205
+ // undefined trap must not shadow.
1206
+ const lift = lowerer.widthLiftPlan(f.type, targetType);
1207
+ if (!lift) {
1208
+ 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)}'`));
1209
+ throw new PoisonError();
1210
+ }
1211
+ thenVal = lowerer.applyWidthLift(lift, fRead(), targetType, locOf(prop));
1212
+ }
1213
+ const at = fields.findIndex((x) => x.name === f.name && !x.drop);
1214
+ const elseVal = at >= 0 ? fields[at].value : lowerer.wrappedUndefined(targetType, locOf(prop));
1215
+ if (!elseVal) {
1216
+ 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 })`);
1217
+ }
1218
+ const merged = { kind: "ternary", cond, then: thenVal, else_: elseVal, type: targetType, loc: locOf(prop) };
1219
+ if (at >= 0)
1220
+ fields[at] = { name: f.name, value: merged };
1221
+ else
1222
+ fields.push({ name: f.name, value: merged });
1223
+ }
1224
+ continue;
1225
+ }
1226
+ if (srcType?.kind !== "record") {
1227
+ 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})`);
1228
+ }
1229
+ if (srcLowered && !isSafeToRepeat(srcLowered)) {
1230
+ lowerer.unsupported("SC1090", prop, "object spread of computed sources (the field copies re-read the source — bind it to a const first)");
1231
+ }
1232
+ const srcShape = lowerer.shapes.get(srcType.shapeId);
1233
+ if (!srcShape)
1234
+ throw new InternalCompilerError(`lowerer bug: spread of unknown shape ${srcType.shapeId}`);
1235
+ fenceAccessorSpreadSource(lowerer, prop, srcShape);
1236
+ // Index-signature shapes carry runtime-keyed overflow entries the
1237
+ // field-by-field desugar cannot enumerate — fenced on either side.
1238
+ if (srcShape.indexValue || shape.indexValue) {
1239
+ lowerer.unsupported("SC1090", prop, "object spread involving index-signature shapes (overflow keys are runtime state — copy the fields you need explicitly)");
1240
+ }
1241
+ // Names a LATER contributor unconditionally defines: copying such a
1242
+ // source field is dead under JS last-write-wins (and spread reads
1243
+ // are side-effect-free), so it neither lowers nor width-checks —
1244
+ // the spread-then-override completion `{ ...config, stateDir }`
1245
+ // narrows an optional source field into a required target slot
1246
+ // exactly like Node does. Conditional spreads don't count (their
1247
+ // empty arm defines nothing) — their own collision fences hold.
1248
+ const laterNames = new Set();
1249
+ for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
1250
+ if (ts.isSpreadAssignment(later)) {
1251
+ if (conditionalSpreadOf(later.expression))
1252
+ continue;
1253
+ const lt = lowerer.mapTypeOf(lowerer.typeOf(later.expression));
1254
+ if (lt?.kind === "record") {
1255
+ for (const lf of lowerer.shapes.get(lt.shapeId)?.fields ?? [])
1256
+ laterNames.add(lf.name);
1257
+ }
1258
+ continue;
1259
+ }
1260
+ if (later.name &&
1261
+ (ts.isIdentifier(later.name) ||
1262
+ ts.isStringLiteral(later.name) ||
1263
+ ts.isNumericLiteral(later.name) ||
1264
+ (ts.isComputedPropertyName(later.name) && literalComputedKey(lowerer, later.name) !== null))) {
1265
+ laterNames.add(propNameText(lowerer, later.name));
1266
+ }
1267
+ }
1268
+ for (const f of srcShape.fields) {
1269
+ if (laterNames.has(f.name))
1270
+ continue;
1271
+ const targetType = fieldTypes.get(f.name);
1272
+ // A source field with NO slot on the target shape: the copy
1273
+ // DROPS it — a spread of a wider record into a narrower literal
1274
+ // is width subtyping in spread clothing, divergence 36's stance
1275
+ // (Node's object would keep the key); the read is pure, so
1276
+ // skipping evaluates nothing.
1277
+ if (!targetType)
1278
+ continue;
1279
+ const lift = typeEquals(f.type, targetType)
1280
+ ? null
1281
+ : lowerer.widthLiftPlan(f.type, targetType);
1282
+ if (!typeEquals(f.type, targetType) && !lift) {
1283
+ // Print the SOURCE shape, not the literal's own type: the
1284
+ // checker's own type already has later overrides applied, so
1285
+ // it can render identically to the target while the spread
1286
+ // source (the thing that actually mismatches) differs — an
1287
+ // invisible difference is a diagnostics bug.
1288
+ 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)}'`));
1289
+ throw new PoisonError();
1290
+ }
1291
+ const obj = srcLowered ?? lowerer.lowerExpr(srcNode);
1292
+ // A record-mapped CHECKER type whose VALUE lives in the checked-dynamic tree (a
1293
+ // JS file-scope object-literal global): read each field from
1294
+ // the checked-dynamic tree (dynKeyGet) and VALIDATE it into the source shape's
1295
+ // field type (dynCheck) — the checked-dynamic member-read
1296
+ // discipline. A missing key answers the dyn undefined, exactly
1297
+ // the undefined-armed optional's absent case; a mismatched
1298
+ // runtime value throws the catchable TypeError, never a silent
1299
+ // wrong copy. Runtime-ADDED keys drop — width subtyping in
1300
+ // spread clothing, divergence 36's stance.
1301
+ let value;
1302
+ if (obj.type.kind === "dyn") {
1303
+ if (!canDynCheckTo(f.type, (id) => lowerer.shapes.get(id), (id) => lowerer.unions.get(id))) {
1304
+ 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)`);
1305
+ }
1306
+ value = {
1307
+ kind: "dynCheck",
1308
+ value: {
1309
+ kind: "dynKeyGet",
1310
+ key: { kind: "strLit", value: f.name, type: STRING, loc: locOf(prop) },
1311
+ value: obj,
1312
+ type: DYN,
1313
+ loc: locOf(prop),
1314
+ },
1315
+ type: f.type,
1316
+ loc: locOf(prop),
1317
+ };
1318
+ }
1319
+ else {
1320
+ value = {
1321
+ kind: "recordGet",
1322
+ obj,
1323
+ shapeId: srcType.shapeId,
1324
+ field: f.name,
1325
+ type: f.type,
1326
+ loc: locOf(prop),
1327
+ };
1328
+ }
1329
+ // A liftable field widens into the target slot (arm wrap,
1330
+ // re-tag, nested reshape) — the same per-field rule the slot
1331
+ // coercion applies.
1332
+ if (lift)
1333
+ value = lowerer.applyWidthLift(lift, value, targetType, locOf(prop));
1334
+ if (conditionalNames.has(f.name)) {
1335
+ 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)`);
1336
+ }
1337
+ const at = fields.findIndex((x) => x.name === f.name);
1338
+ if (at >= 0)
1339
+ fields[at] = { name: f.name, value };
1340
+ else
1341
+ fields.push({ name: f.name, value });
1342
+ }
1343
+ continue;
1344
+ }
1345
+ if (ts.isGetAccessorDeclaration(prop) || ts.isSetAccessorDeclaration(prop)) {
1346
+ // JS accessor entries carry no storage (narrowed away above); TS
1347
+ // accessors fill the shape's reserved closure slots — the getter
1348
+ // body lowers as an ordinary zero-arg closure invoked per property
1349
+ // READ, the setter as a one-arg closure invoked per WRITE
1350
+ // (fieldGetExpr/fieldSetStmt dispatch on the slots). Creating the
1351
+ // closures is side-effect-free, so their position among the data
1352
+ // fields' source-order evaluation is unobservable — exactly JS,
1353
+ // where accessor definitions evaluate nothing.
1354
+ if (isJsSourceFile(expr.getSourceFile()))
1355
+ continue;
1356
+ const name = propNameText(lowerer, prop.name); // key-form-checked above
1357
+ const slotName = `${ts.isGetAccessorDeclaration(prop) ? "%get" : "%set"}:${name}`;
1358
+ const slotT = fieldTypes.get(slotName);
1359
+ if (!slotT || slotT.kind !== "func") {
1360
+ // The contextual shape stores a DATA value under this name
1361
+ // (`const p: { x: number } = { get x() {...} }` — tsc lets a
1362
+ // live accessor satisfy a data member): the slot would freeze
1363
+ // one getter answer where Node keeps the accessor live.
1364
+ if (fieldTypes.has(name)) {
1365
+ 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)`);
1366
+ }
1367
+ throw shapeMismatch(prop);
1368
+ }
1369
+ const closure = lowerer.coerceInto(prop, lowerer.lowerLambda(prop), slotT);
1370
+ if (!typeEquals(closure.type, slotT))
1371
+ lowerer.badType(prop, lowerer.typeOf(prop));
1372
+ fields.push({ name: slotName, value: closure });
1373
+ continue;
1374
+ }
1375
+ const name = propNameText(lowerer, prop.name); // key-form-checked above
1376
+ let fieldType = fieldTypes.get(name);
1377
+ // An undeclared name against an index-signature shape is an OVERFLOW
1378
+ // entry (tsc typechecked it against the signature's value type);
1379
+ // against a plain shape it is the width mismatch it always was —
1380
+ // EXCEPT the PromiseSettledResult honest subset's dropped fields
1381
+ // (value/reason — SEMANTICS.md 46): those evaluate for effect in
1382
+ // their source-order slot and store nothing.
1383
+ // A GENERIC-callable member (a generic method `m<T>(x: T) {...}` or a
1384
+ // generic arrow/function-expression property): excluded from the
1385
+ // record shape (isGenericCallableMemberType — no single closure slot
1386
+ // can hold it), so the literal stores nothing for it. Pure
1387
+ // function-creating forms skip outright (creating a closure has no
1388
+ // side effects; calls resolve statically against this declaration);
1389
+ // a computed initializer would need its evaluation kept — fenced.
1390
+ if (!fieldType && !ts.isSpreadAssignment(prop)) {
1391
+ const memberSym = prop.name && lowerer.checker.getSymbolAtLocation(prop.name);
1392
+ const memberT = memberSym ? lowerer.checker.getTypeOfSymbol(memberSym) : undefined;
1393
+ if (memberT && isGenericCallableMemberType(memberT, lowerer.checker)) {
1394
+ const pureInit = (() => {
1395
+ if (ts.isMethodDeclaration(prop))
1396
+ return true;
1397
+ if (ts.isShorthandPropertyAssignment(prop))
1398
+ return true; // a pure read
1399
+ if (!ts.isPropertyAssignment(prop))
1400
+ return false;
1401
+ let init = prop.initializer;
1402
+ while (ts.isParenthesizedExpression(init))
1403
+ init = init.expression;
1404
+ return ts.isArrowFunction(init) || ts.isFunctionExpression(init) || ts.isIdentifier(init);
1405
+ })();
1406
+ if (!pureInit) {
1407
+ 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)`);
1408
+ }
1409
+ continue;
1410
+ }
1411
+ }
1412
+ if (!fieldType && !shape.indexValue) {
1413
+ if (settledDropNames(lowerer, tsType)?.has(name)) {
1414
+ // Identifier and shorthand initializers are effect-free reads —
1415
+ // skipped outright. The caught `reason` MUST skip: lowering the
1416
+ // read would hit the catch-binding narrowness fence, and the
1417
+ // snapshot it names needs no evaluation.
1418
+ if (ts.isPropertyAssignment(prop) && !ts.isIdentifier(prop.initializer)) {
1419
+ const v = lowerer.lowerExpr(prop.initializer);
1420
+ // Effect-free lowerings (literals, plain reads) drop at
1421
+ // compile time; anything else — the awaited mapper — runs
1422
+ // (and may throw into the enclosing catch) with its result
1423
+ // released by the statement frame.
1424
+ if (v.kind !== "unitLit" && v.kind !== "numLit" && v.kind !== "strLit" &&
1425
+ v.kind !== "boolLit" && v.kind !== "varRef" && v.kind !== "closure") {
1426
+ fields.push({ name, value: v, drop: true });
1427
+ }
1428
+ }
1429
+ continue;
1430
+ }
1431
+ throw shapeMismatch(prop);
1432
+ }
1433
+ let value;
1434
+ let valueNode = prop;
1435
+ const propDiagsBefore = lowerer.diags.length;
1436
+ try {
1437
+ if (ts.isPropertyAssignment(prop)) {
1438
+ valueNode = prop.initializer;
1439
+ // ARRAY-LITERAL initializers route through the expected-type
1440
+ // lowering: a union field with one array-family arm builds the
1441
+ // literal AS that arm (lowerExprExpecting's IR-directed rule —
1442
+ // the option-table `default: [{ value: [] }]` shape), where the
1443
+ // bare lowering would take the JS dyn fallback and fence.
1444
+ let init = prop.initializer;
1445
+ while (ts.isParenthesizedExpression(init))
1446
+ init = init.expression;
1447
+ value =
1448
+ fenceClosureProbe(lowerer, prop.initializer, fieldType, () => lowerer.lowerExpr(prop.initializer)) ??
1449
+ (fieldType !== undefined && ts.isArrayLiteralExpression(init)
1450
+ ? lowerer.lowerExprExpecting(prop.initializer, fieldType)
1451
+ : lowerer.lowerExpr(prop.initializer));
1452
+ }
1453
+ else if (ts.isShorthandPropertyAssignment(prop)) {
1454
+ value = lowerer.lowerShorthandValue(prop);
1455
+ }
1456
+ else if (ts.isMethodDeclaration(prop)) {
1457
+ value =
1458
+ fenceClosureProbe(lowerer, prop, fieldType, () => lowerer.lowerLambda(prop)) ?? lowerer.lowerLambda(prop);
1459
+ }
1460
+ else {
1461
+ lowerer.unsupported("SC1090", prop, `syntax '${ts.SyntaxKind[prop.kind]}'`);
1462
+ }
1463
+ }
1464
+ catch (err) {
1465
+ // A member VALUE a JS file cannot lower (a namespace object in an
1466
+ // export aggregate — the sharedWithCli `errors` member): the
1467
+ // member NARROWS AWAY like a CJS export-table accessor entry —
1468
+ // the diagnostics defer to the runtime-fence ledger, the shape
1469
+ // drops the field, and each READ of it meets its own per-site
1470
+ // fence. Pure member forms only (identifier/shorthand reads);
1471
+ // TypeScript, probe mode, and ICEs keep the poison.
1472
+ const pureMember = ts.isShorthandPropertyAssignment(prop) ||
1473
+ (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.initializer));
1474
+ if (!(err instanceof PoisonError) ||
1475
+ !pureMember ||
1476
+ !isJsSourceFile(expr.getSourceFile()) ||
1477
+ lowerer.diagSink !== null ||
1478
+ lowerer.diags.length <= propDiagsBefore ||
1479
+ lowerer.diags.slice(propDiagsBefore).some((d) => d.code === "SC9001")) {
1480
+ throw err;
1481
+ }
1482
+ lowerer.runtimeFences.push(...lowerer.diags.splice(propDiagsBefore));
1483
+ droppedNames.add(name);
1484
+ continue;
1485
+ }
1486
+ if (!fieldType) {
1487
+ // Overflow entry: the value flows into the index signature's value
1488
+ // slot — dyn slots take a dyn conversion (dynFrom), typed slots the
1489
+ // ordinary coercion path. Later duplicates override (map semantics
1490
+ // are last-write-wins already; a duplicate literal key is a tsc
1491
+ // error anyway).
1492
+ const slotted = lowerer.intoIndexValueSlot(value, shape.indexValue, valueNode);
1493
+ fields.push({ name, value: slotted, overflow: true });
1494
+ continue;
1495
+ }
1496
+ const promotedField = fieldType ? lowerer.runtimeOptionalWidening(value.type, fieldType) : null;
1497
+ if (promotedField && fieldType) {
1498
+ type = lowerer.runtimeOptionalRecordField(type, name, promotedField);
1499
+ if (type.kind === "record")
1500
+ shape = lowerer.shapes.get(type.shapeId);
1501
+ fieldTypes.set(name, promotedField);
1502
+ fieldType = promotedField;
1503
+ }
1504
+ value = lowerer.coerceInto(valueNode, value, fieldType); // union-typed fields wrap arm values
1505
+ if (!typeEquals(value.type, fieldType))
1506
+ lowerer.badType(valueNode, lowerer.typeOf(valueNode));
1507
+ // An explicit property overrides a spread-copied field: JS
1508
+ // last-write-wins. The entry moves to the END so explicit property
1509
+ // values keep their source-order evaluation among themselves. A
1510
+ // conditional-spread entry cannot be overridden (its ternary IS the
1511
+ // spread's evaluation; splicing it out would drop that).
1512
+ if (conditionalNames.has(name)) {
1513
+ 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)`);
1514
+ }
1515
+ const at = fields.findIndex((x) => x.name === name);
1516
+ if (at >= 0)
1517
+ fields.splice(at, 1);
1518
+ fields.push({ name, value });
1519
+ }
1520
+ // Optional fields (undefined-armed union slots) may be omitted: the
1521
+ // absent field holds the interned undefined arm, exactly like writing
1522
+ // `a: undefined` (without exactOptionalPropertyTypes tsc treats the two
1523
+ // the same, and only optional fields may be omitted — tsc rejects
1524
+ // omission of required fields before lowering, undefined-armed or not).
1525
+ // A REQUIRED missing field keeps the shape-mismatch rejection (possible
1526
+ // through `as`: the cast smuggles a narrower literal past freshness).
1527
+ if (droppedNames.size > 0) {
1528
+ const narrowed = shape.fields.filter((f) => !droppedNames.has(f.name));
1529
+ const narrowedId = lowerer.shapes.intern(narrowed.map((f) => ({ name: f.name, type: f.type })), false, shape.indexValue);
1530
+ type = { kind: "record", shapeId: narrowedId };
1531
+ shape = lowerer.shapes.get(narrowedId);
1532
+ }
1533
+ if (fields.filter((f) => !f.overflow).length !== shape.fields.length) {
1534
+ const provided = new Set(fields.filter((f) => !f.overflow).map((f) => f.name));
1535
+ for (const f of shape.fields) {
1536
+ if (provided.has(f.name))
1537
+ continue;
1538
+ // 'unknown' fields complete with the dyn undefined — the absent
1539
+ // property reads as undefined in Node, and a dyn slot holds
1540
+ // exactly that (the options-record call shape against
1541
+ // `{ plugins: unknown, ... }` — a JS caller the checker admits).
1542
+ const absent = lowerer.wrappedUndefined(f.type, loc) ?? (f.type.kind === "dyn" ? dynUndefinedExpr(loc) : null);
1543
+ if (!absent)
1544
+ throw shapeMismatch(expr); // only optional (undefined-armed) and 'unknown' fields may be omitted
1545
+ fields.push({ name: f.name, value: absent });
1546
+ }
1547
+ }
1548
+ return { kind: "recordLit", fields, type, loc };
1549
+ }
1550
+ /** Parses the conditional-spread idiom `...(c ? { k: v, ... } : {})`
1551
+ * (either orientation). Returns the condition, the non-empty arm's
1552
+ * properties, and which arm carries them; "unsupported" for conditional
1553
+ * sources OUTSIDE the idiom (both arms non-empty, computed/method
1554
+ * members); null when the spread source isn't a conditional at all.
1555
+ * Callers slice their own honest subset (the static record path takes
1556
+ * exactly one property; the island literal takes any number; spawn's
1557
+ * options walk takes a `detached` boolean literal). */
1558
+ export function conditionalSpreadOf(expr) {
1559
+ let e = expr;
1560
+ while (ts.isParenthesizedExpression(e))
1561
+ e = e.expression;
1562
+ if (!ts.isConditionalExpression(e))
1563
+ return null;
1564
+ const unwrap = (a) => {
1565
+ let x = a;
1566
+ while (ts.isParenthesizedExpression(x))
1567
+ x = x.expression;
1568
+ return x;
1569
+ };
1570
+ const whenTrue = unwrap(e.whenTrue);
1571
+ const whenFalse = unwrap(e.whenFalse);
1572
+ if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
1573
+ return "unsupported";
1574
+ }
1575
+ const trueCarries = whenFalse.properties.length === 0 && whenTrue.properties.length > 0;
1576
+ const falseCarries = whenTrue.properties.length === 0 && whenFalse.properties.length > 0;
1577
+ if (!trueCarries && !falseCarries)
1578
+ return "unsupported"; // both empty is tsc-unreachable; both non-empty has no single desugar
1579
+ const carrier = trueCarries ? whenTrue : whenFalse;
1580
+ const props = [];
1581
+ for (const p of carrier.properties) {
1582
+ if ((ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
1583
+ p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))) {
1584
+ props.push(p);
1585
+ }
1586
+ else {
1587
+ return "unsupported";
1588
+ }
1589
+ }
1590
+ return { cond: e.condition, props, whenTrue: trueCarries };
1591
+ }
1592
+ /** `{ ...idx, ...idx2 }` typed a DECLARED shape (no index signature) — the
1593
+ * defaults-merge idiom over runtime-keyed sources (`{ ...fromEntries(a),
1594
+ * ...fromEntries(b) }` typed AppConfig). Lowers to ONE interned helper
1595
+ * call: sources evaluate once each as arguments (source order — computed
1596
+ * sources included, JS's evaluate-once), the result starts all-undefined
1597
+ * (every target field must be optional — the runtime keys decide
1598
+ * presence), and each source's keys apply in JS own-key order with a
1599
+ * per-key dispatch onto the declared fields: a matching key writes the
1600
+ * field — identity when the source's value slot IS the field type, a
1601
+ * validated extraction otherwise (a dyn slot dynChecks; a union slot
1602
+ * re-tags arm-by-arm, and a value outside the field's arms throws the
1603
+ * catchable TypeError — divergence 34's keyed-write stance, where Node's
1604
+ * untyped copy would store the lie) — and a key naming NO declared field
1605
+ * is DROPPED (the shape cannot represent it; Node keeps it invisibly —
1606
+ * divergence 68). Later contributors overwrite earlier ones
1607
+ * (last-write-wins). Sources must be PURE index-signature records. */
1608
+ function lowerDeclaredSpreadMerge(lowerer, expr, type, shape, loc) {
1609
+ const srcs = [];
1610
+ for (const prop of expr.properties) {
1611
+ const spread = prop; // caller-checked: all spreads
1612
+ const value = lowerer.lowerExpr(spread.expression);
1613
+ const srcShape = value.type.kind === "record" ? lowerer.shapes.get(value.type.shapeId) : undefined;
1614
+ if (value.type.kind !== "record" || !srcShape?.indexValue || srcShape.tuple || srcShape.fields.length > 0) {
1615
+ 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)`);
1616
+ }
1617
+ srcs.push({ value, shapeId: value.type.shapeId, iv: srcShape.indexValue });
1618
+ }
1619
+ // Every target field must be optional: absent keys leave the undefined
1620
+ // arm, exactly the unset-optional representation.
1621
+ for (const f of shape.fields) {
1622
+ if (f.type.kind !== "union" || lowerer.armTag(f.type.unionId, UNDEFINED_T) < 0) {
1623
+ 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)`);
1624
+ }
1625
+ }
1626
+ // Per (source value slot → field) conversion, checked up front so the
1627
+ // fence fires at the literal, not inside the interned helper.
1628
+ const conv = (iv, f, v) => {
1629
+ if (typeEquals(iv, f.type))
1630
+ return { value: v };
1631
+ if (iv.kind === "dyn") {
1632
+ return { value: { kind: "dynCheck", value: v, type: f.type, loc } };
1633
+ }
1634
+ if (iv.kind === "union" && f.type.kind === "union") {
1635
+ const fUnion = f.type;
1636
+ const def = lowerer.unions.get(iv.unionId);
1637
+ const fDef = lowerer.unions.get(fUnion.unionId);
1638
+ if (def && fDef && def.arms.some((a) => lowerer.armTag(fUnion.unionId, a) >= 0)) {
1639
+ // Arm-by-arm validated re-tag, inline in the helper: matching
1640
+ // arms map by identity (unit arms re-wrap, value arms narrow and
1641
+ // wrap); anything else throws the catchable TypeError.
1642
+ return {
1643
+ stmts: (write) => {
1644
+ const chain = (i) => {
1645
+ if (i >= def.arms.length) {
1646
+ return [{
1647
+ kind: "throw",
1648
+ value: {
1649
+ kind: "libCall",
1650
+ fn: "error.new",
1651
+ args: [{ kind: "strLit", value: `expected ${lowerer.fmt(fUnion)} at $.${f.name}`, type: STRING, loc }],
1652
+ type: { kind: "object", className: "%TypeError" },
1653
+ loc,
1654
+ },
1655
+ loc,
1656
+ }];
1657
+ }
1658
+ const arm = def.arms[i];
1659
+ const toTag = lowerer.armTag(fUnion.unionId, arm);
1660
+ if (toTag < 0)
1661
+ return chain(i + 1);
1662
+ const extracted = isUnitType(arm)
1663
+ ? { kind: "unionWrap", unionId: fUnion.unionId, tag: toTag, value: { kind: "unitLit", unit: arm.kind === "undefinedT" ? "undefined" : "null", type: arm, loc }, type: fUnion, loc }
1664
+ : { kind: "unionWrap", unionId: fUnion.unionId, tag: toTag, value: { kind: "unionNarrow", unionId: iv.unionId, tag: i, value: v, type: arm, loc }, type: fUnion, loc };
1665
+ return [{
1666
+ kind: "if",
1667
+ cond: { kind: "unionIsTag", unionId: iv.unionId, tag: i, negated: false, value: v, type: BOOL, loc },
1668
+ then: [write(extracted)],
1669
+ else_: chain(i + 1),
1670
+ loc,
1671
+ }];
1672
+ };
1673
+ return chain(0);
1674
+ },
1675
+ };
1676
+ }
1677
+ }
1678
+ 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)`);
1679
+ };
1680
+ const key = `declmerge:${type.shapeId}:${srcs.map((s) => s.shapeId).join(",")}`;
1681
+ let helper = lowerer.widthHelpers.get(key);
1682
+ if (!helper) {
1683
+ helper = `%rec.declmerge.${lowerer.widthHelpers.size}`;
1684
+ const outRef = varRef("out.0", type, loc);
1685
+ const ksT = arrayOf(STRING);
1686
+ const locals = [{ id: "out.0", name: "out", type, mutable: false }];
1687
+ const params = srcs.map((s, j) => {
1688
+ locals.push({ id: `s${j}.0`, name: `s${j}`, type: s.value.type, mutable: true });
1689
+ return { localId: `s${j}.0`, name: `s${j}`, type: s.value.type };
1690
+ });
1691
+ const body = [
1692
+ {
1693
+ kind: "varDecl",
1694
+ localId: "out.0",
1695
+ init: {
1696
+ kind: "recordLit",
1697
+ fields: shape.fields.map((f) => ({ name: f.name, value: lowerer.wrappedUndefined(f.type, loc) })),
1698
+ type,
1699
+ loc,
1700
+ },
1701
+ loc,
1702
+ },
1703
+ ];
1704
+ srcs.forEach((s, j) => {
1705
+ const sRef = varRef(`s${j}.0`, s.value.type, loc);
1706
+ const kRef = varRef(`k${j}.0`, STRING, loc);
1707
+ const vRef = varRef(`v${j}.0`, s.iv, loc);
1708
+ 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 });
1709
+ // Per-key dispatch: if (k === "a") { write a } else if ... else drop.
1710
+ const dispatch = shape.fields.reduceRight((rest, f) => {
1711
+ const write = (value) => ({ kind: "recordSet", obj: outRef, shapeId: type.shapeId, field: f.name, value, loc });
1712
+ const c = conv(s.iv, f, vRef);
1713
+ const thenBody = "value" in c ? [write(c.value)] : c.stmts(write);
1714
+ return [{
1715
+ kind: "if",
1716
+ cond: { kind: "strEq", negated: false, left: kRef, right: { kind: "strLit", value: f.name, type: STRING, loc }, type: BOOL, loc },
1717
+ then: thenBody,
1718
+ else_: rest.length > 0 ? rest : null,
1719
+ loc,
1720
+ }];
1721
+ }, []);
1722
+ body.push({ kind: "varDecl", localId: `ks${j}.0`, init: { kind: "recordOvfKeys", obj: sRef, shapeId: s.shapeId, type: ksT, loc }, loc }, {
1723
+ kind: "for",
1724
+ init: { kind: "varDecl", localId: `i${j}.0`, init: numLit(0, loc), loc },
1725
+ 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 },
1726
+ 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 },
1727
+ body: [
1728
+ { 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 },
1729
+ { kind: "varDecl", localId: `v${j}.0`, init: { kind: "recordKeyGet", obj: sRef, shapeId: s.shapeId, key: kRef, overflowOnly: true, type: s.iv, loc }, loc },
1730
+ ...dispatch,
1731
+ ],
1732
+ loc,
1733
+ });
1734
+ });
1735
+ body.push({ kind: "return", value: outRef, loc });
1736
+ lowerer.liftedFns.push({
1737
+ name: helper,
1738
+ params,
1739
+ returnType: type,
1740
+ locals,
1741
+ body,
1742
+ loc,
1743
+ });
1744
+ // Registered only after a fence-free build: a conv() fence mid-build
1745
+ // must not leave a phantom helper behind for the next literal.
1746
+ lowerer.widthHelpers.set(key, helper);
1747
+ }
1748
+ return { kind: "call", callee: helper, args: srcs.map((s) => s.value), type, loc };
1749
+ }
1750
+ /** The value of a shorthand property (`{ x }`): the binding `x` refers to.
1751
+ * The property name's own symbol is the PROPERTY, so resolution goes
1752
+ * through getShorthandAssignmentValueSymbol. */
1753
+ export function lowerShorthandValue(lowerer, prop) {
1754
+ // 7 types the shorthand's name as PropertyName; it is always an
1755
+ // Identifier (the grammar allows nothing else in shorthand position).
1756
+ const propName = prop.name;
1757
+ const loc = locOf(propName);
1758
+ const symbol = lowerer.checker.getShorthandAssignmentValueSymbol(prop);
1759
+ if (symbol) {
1760
+ if (lowerer.ctx.selfSymbol === symbol) {
1761
+ return { kind: "selfRef", type: lowerer.ctx.selfType, loc };
1762
+ }
1763
+ const local = lowerer.resolveKey(symbol, propName);
1764
+ if (local) {
1765
+ return lowerer.maybeNarrow({ kind: "varRef", localId: local.id, type: local.type, loc }, propName);
1766
+ }
1767
+ const resolved = symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
1768
+ lowerer.flushDeferred(resolved);
1769
+ const g = lowerer.globalsBySymbol.get(resolved);
1770
+ if (g) {
1771
+ return lowerer.maybeNarrow({ kind: "varRef", localId: g.id, type: g.type, loc }, propName);
1772
+ }
1773
+ const sig = lowerer.fnSigsBySymbol.get(resolved);
1774
+ const decl = lowerer.checker.declarationsOf(resolved)[0];
1775
+ if (sig && decl && ts.isFunctionDeclaration(decl) &&
1776
+ (ts.isSourceFile(decl.parent) || lowerer.nsBlocks.get(decl.parent) === "flattened")) {
1777
+ lowerer.noteEdge(sig.name);
1778
+ const funcType = {
1779
+ kind: "func",
1780
+ params: sig.params.filter((p) => p.mode !== "dynRest").map((p) => p.type),
1781
+ ret: sig.returnType,
1782
+ ...(sig.params.some((p) => p.mode === "dynRest") ? { rest: true } : {}),
1783
+ };
1784
+ lowerer.requireExactArityValue(prop, propName, sig.params, funcType);
1785
+ return { kind: "closure", fnName: sig.name, captures: [], type: funcType, loc };
1786
+ }
1787
+ if (lowerer.genericFnsBySymbol.has(resolved)) {
1788
+ lowerer.unsupported("SC1090", prop, `generic functions as values (call '${propName.text}' directly)`);
1789
+ }
1790
+ }
1791
+ 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)`);
1792
+ }
1793
+ /** Rejects any `this` inside an object-literal method body — including in
1794
+ * nested arrows, which inherit the method's `this` (nested function
1795
+ * expressions reset it, but their bare `this` is already a tsc error
1796
+ * under noImplicitThis, so over-rejecting them here changes nothing). */
1797
+ export function rejectThisInObjectMethod(lowerer, node) {
1798
+ if (node.kind === ts.SyntaxKind.ThisKeyword) {
1799
+ lowerer.unsupported("SC1090", node, "references to 'this' in object literal methods");
1800
+ }
1801
+ ts.forEachChild(node, (child) => lowerer.rejectThisInObjectMethod(child));
1802
+ }
1803
+ /** The accessor twin of rejectThisInObjectMethod: a get/set accessor body
1804
+ * referencing `this` (`get x() { return this._x }`). The accessor lowers
1805
+ * as a closure stored IN the record — passing the record as a receiver
1806
+ * would capture the value under construction (an RC cycle), and the
1807
+ * generic lexical-this walk would silently bind an ENCLOSING method's
1808
+ * `this`; the fence names the fix (capture a binding instead). */
1809
+ function rejectThisInObjectAccessor(lowerer, node) {
1810
+ if (node.kind === ts.SyntaxKind.ThisKeyword) {
1811
+ 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)");
1812
+ }
1813
+ ts.forEachChild(node, (child) => rejectThisInObjectAccessor(lowerer, child));
1814
+ }
1815
+ //# sourceMappingURL=object-literals.js.map