@scriptc/compiler 0.2.8 → 0.2.9

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 (189) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +1 -0
  2. package/dist/backend/cycle-analysis.js +50 -17
  3. package/dist/backend/cycle-analysis.js.map +1 -1
  4. package/dist/backend/cycle-analysis.test.js +138 -14
  5. package/dist/backend/cycle-analysis.test.js.map +1 -1
  6. package/dist/backend/cycle-mutability.d.ts +31 -6
  7. package/dist/backend/cycle-mutability.js +120 -18
  8. package/dist/backend/cycle-mutability.js.map +1 -1
  9. package/dist/backend/llvm/alloc.d.ts +13 -3
  10. package/dist/backend/llvm/alloc.js +24 -10
  11. package/dist/backend/llvm/alloc.js.map +1 -1
  12. package/dist/backend/llvm/alloc.test.js +5 -5
  13. package/dist/backend/llvm/alloc.test.js.map +1 -1
  14. package/dist/backend/llvm/byte-numbers.js +2 -3
  15. package/dist/backend/llvm/byte-numbers.js.map +1 -1
  16. package/dist/backend/llvm/classes.js +7 -6
  17. package/dist/backend/llvm/classes.js.map +1 -1
  18. package/dist/backend/llvm/common.d.ts +17 -0
  19. package/dist/backend/llvm/common.js +28 -0
  20. package/dist/backend/llvm/common.js.map +1 -1
  21. package/dist/backend/llvm/dense-array-access.js +1 -1
  22. package/dist/backend/llvm/dense-array-access.test.js +1 -1
  23. package/dist/backend/llvm/dyn.d.ts +4 -4
  24. package/dist/backend/llvm/dyn.js +27 -11
  25. package/dist/backend/llvm/dyn.js.map +1 -1
  26. package/dist/backend/llvm/emitter.d.ts +45 -3
  27. package/dist/backend/llvm/emitter.js +333 -74
  28. package/dist/backend/llvm/emitter.js.map +1 -1
  29. package/dist/backend/llvm/emitter.test.js +36 -3
  30. package/dist/backend/llvm/emitter.test.js.map +1 -1
  31. package/dist/backend/llvm/expr-bytes.js +35 -28
  32. package/dist/backend/llvm/expr-bytes.js.map +1 -1
  33. package/dist/backend/llvm/expr-containers.js +1 -1
  34. package/dist/backend/llvm/expr-context.d.ts +2 -0
  35. package/dist/backend/llvm/expr-control.js +46 -5
  36. package/dist/backend/llvm/expr-control.js.map +1 -1
  37. package/dist/backend/llvm/expr-dynamic.js +3 -0
  38. package/dist/backend/llvm/expr-dynamic.js.map +1 -1
  39. package/dist/backend/llvm/expr-stream-bridges.d.ts +3 -0
  40. package/dist/backend/llvm/expr-stream-bridges.js +73 -11
  41. package/dist/backend/llvm/expr-stream-bridges.js.map +1 -1
  42. package/dist/backend/llvm/lib-filesystem.js +18 -2
  43. package/dist/backend/llvm/lib-filesystem.js.map +1 -1
  44. package/dist/backend/llvm/lib-shared.js +1 -0
  45. package/dist/backend/llvm/lib-shared.js.map +1 -1
  46. package/dist/backend/llvm/local-array-reads.d.ts +17 -6
  47. package/dist/backend/llvm/local-array-reads.js +155 -43
  48. package/dist/backend/llvm/local-array-reads.js.map +1 -1
  49. package/dist/backend/llvm/reference-effects.js +34 -1
  50. package/dist/backend/llvm/reference-effects.js.map +1 -1
  51. package/dist/backend/llvm/shapes.d.ts +14 -3
  52. package/dist/backend/llvm/shapes.js +43 -12
  53. package/dist/backend/llvm/shapes.js.map +1 -1
  54. package/dist/backend/llvm/shapes.test.js +5 -5
  55. package/dist/backend/llvm/shapes.test.js.map +1 -1
  56. package/dist/backend/llvm/string-reads.js +29 -26
  57. package/dist/backend/llvm/string-reads.js.map +1 -1
  58. package/dist/backend/llvm/string-slices.js +1 -1
  59. package/dist/backend/llvm/trunc.d.ts +8 -0
  60. package/dist/backend/llvm/trunc.js +28 -0
  61. package/dist/backend/llvm/trunc.js.map +1 -0
  62. package/dist/backend/loop-polls.d.ts +14 -0
  63. package/dist/backend/loop-polls.js +100 -0
  64. package/dist/backend/loop-polls.js.map +1 -0
  65. package/dist/backend/loop-polls.test.d.ts +1 -0
  66. package/dist/backend/loop-polls.test.js +86 -0
  67. package/dist/backend/loop-polls.test.js.map +1 -0
  68. package/dist/backend/may-throw.js +20 -1
  69. package/dist/backend/may-throw.js.map +1 -1
  70. package/dist/backend/may-throw.test.js +23 -0
  71. package/dist/backend/may-throw.test.js.map +1 -1
  72. package/dist/backend/native-codegen.d.ts +1 -3
  73. package/dist/backend/native-codegen.js +4 -32
  74. package/dist/backend/native-codegen.js.map +1 -1
  75. package/dist/backend/runtime-abi.d.ts +2 -2
  76. package/dist/backend/runtime-abi.js +2 -2
  77. package/dist/backend/runtime-abi.js.map +1 -1
  78. package/dist/backend/runtime-pack-targets.test.js +1 -1
  79. package/dist/backend/runtime-pack-targets.test.js.map +1 -1
  80. package/dist/backend/runtime-pack.test.js +1 -1
  81. package/dist/backend/runtime-pack.test.js.map +1 -1
  82. package/dist/cli/command.js +8 -1
  83. package/dist/cli/command.js.map +1 -1
  84. package/dist/cli/output-options.d.ts +1 -0
  85. package/dist/cli/output-options.js +3 -0
  86. package/dist/cli/output-options.js.map +1 -1
  87. package/dist/cli/usage.d.ts +5 -1
  88. package/dist/cli/usage.js +6 -0
  89. package/dist/cli/usage.js.map +1 -1
  90. package/dist/compile-types.d.ts +5 -0
  91. package/dist/executable/executable-cache.d.ts +2 -0
  92. package/dist/executable/executable-cache.js +3 -0
  93. package/dist/executable/executable-cache.js.map +1 -1
  94. package/dist/executable/executable-cache.test.js +1 -0
  95. package/dist/executable/executable-cache.test.js.map +1 -1
  96. package/dist/frontend/lowering/array-values.js +37 -1
  97. package/dist/frontend/lowering/array-values.js.map +1 -1
  98. package/dist/frontend/lowering/builtins/strings.js +4 -1
  99. package/dist/frontend/lowering/builtins/strings.js.map +1 -1
  100. package/dist/frontend/lowering/call-arguments.js +96 -4
  101. package/dist/frontend/lowering/call-arguments.js.map +1 -1
  102. package/dist/frontend/lowering/class-method-mutations.js +88 -0
  103. package/dist/frontend/lowering/class-method-mutations.js.map +1 -1
  104. package/dist/frontend/lowering/containers/bytes.js +15 -4
  105. package/dist/frontend/lowering/containers/bytes.js.map +1 -1
  106. package/dist/frontend/lowering/expressions/union-fields.d.ts +19 -0
  107. package/dist/frontend/lowering/expressions/union-fields.js +236 -0
  108. package/dist/frontend/lowering/expressions/union-fields.js.map +1 -0
  109. package/dist/frontend/lowering/lower-calls.js +12 -2
  110. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  111. package/dist/frontend/lowering/lower-exprs.d.ts +6 -0
  112. package/dist/frontend/lowering/lower-exprs.js +162 -3
  113. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  114. package/dist/frontend/lowering/lower-island.js +5 -19
  115. package/dist/frontend/lowering/lower-island.js.map +1 -1
  116. package/dist/frontend/lowering/lower-stmts.js +11 -1
  117. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  118. package/dist/frontend/lowering/lowerer.d.ts +5 -0
  119. package/dist/frontend/lowering/lowerer.js +174 -8
  120. package/dist/frontend/lowering/lowerer.js.map +1 -1
  121. package/dist/frontend/lowering/nan-coded-reads.d.ts +161 -0
  122. package/dist/frontend/lowering/nan-coded-reads.js +1342 -0
  123. package/dist/frontend/lowering/nan-coded-reads.js.map +1 -0
  124. package/dist/frontend/lowering/runtime-optional-bounds.d.ts +14 -2
  125. package/dist/frontend/lowering/runtime-optional-bounds.js +17 -11
  126. package/dist/frontend/lowering/runtime-optional-bounds.js.map +1 -1
  127. package/dist/frontend/lowering/runtime-optional-bounds.test.js +28 -0
  128. package/dist/frontend/lowering/runtime-optional-bounds.test.js.map +1 -1
  129. package/dist/frontend/lowering/surfaces.js +8 -0
  130. package/dist/frontend/lowering/surfaces.js.map +1 -1
  131. package/dist/index.d.ts +1 -1
  132. package/dist/index.js +20 -8
  133. package/dist/index.js.map +1 -1
  134. package/dist/ir/builtin-signatures.d.ts +1 -1
  135. package/dist/ir/builtin-signatures.js +2 -0
  136. package/dist/ir/builtin-signatures.js.map +1 -1
  137. package/dist/ir/constant-globals.d.ts +14 -0
  138. package/dist/ir/constant-globals.js +81 -0
  139. package/dist/ir/constant-globals.js.map +1 -0
  140. package/dist/ir/constant-globals.test.d.ts +1 -0
  141. package/dist/ir/constant-globals.test.js +97 -0
  142. package/dist/ir/constant-globals.test.js.map +1 -0
  143. package/dist/ir/final-fields.d.ts +35 -0
  144. package/dist/ir/final-fields.js +187 -0
  145. package/dist/ir/final-fields.js.map +1 -0
  146. package/dist/ir/final-fields.test.d.ts +1 -0
  147. package/dist/ir/final-fields.test.js +106 -0
  148. package/dist/ir/final-fields.test.js.map +1 -0
  149. package/dist/ir/int32-slots.d.ts +9 -0
  150. package/dist/ir/int32-slots.js +70 -57
  151. package/dist/ir/int32-slots.js.map +1 -1
  152. package/dist/ir/int32-slots.test.js +27 -0
  153. package/dist/ir/int32-slots.test.js.map +1 -1
  154. package/dist/ir/integer-loops.d.ts +5 -0
  155. package/dist/ir/integer-loops.js +20 -4
  156. package/dist/ir/integer-loops.js.map +1 -1
  157. package/dist/ir/integer-loops.test.js +17 -0
  158. package/dist/ir/integer-loops.test.js.map +1 -1
  159. package/dist/ir/ir.d.ts +6 -1
  160. package/dist/ir/ir.js +4 -0
  161. package/dist/ir/ir.js.map +1 -1
  162. package/dist/ir/serialize.d.ts +12 -2
  163. package/dist/ir/serialize.js +190 -39
  164. package/dist/ir/serialize.js.map +1 -1
  165. package/dist/ir/serialize.test.js +65 -1
  166. package/dist/ir/serialize.test.js.map +1 -1
  167. package/dist/native/cache.d.ts +8 -1
  168. package/dist/native/cache.js +38 -1
  169. package/dist/native/cache.js.map +1 -1
  170. package/dist/native/cache.test.js +11 -0
  171. package/dist/native/cache.test.js.map +1 -1
  172. package/dist/native/compiler.js +4 -3
  173. package/dist/native/compiler.js.map +1 -1
  174. package/dist/native/prepare.d.ts +3 -1
  175. package/dist/native/prepare.js +63 -28
  176. package/dist/native/prepare.js.map +1 -1
  177. package/dist/native/prepare.test.js +23 -1
  178. package/dist/native/prepare.test.js.map +1 -1
  179. package/dist/native/text-file.d.ts +10 -0
  180. package/dist/native/text-file.js +41 -0
  181. package/dist/native/text-file.js.map +1 -0
  182. package/dist/text-parts.d.ts +5 -0
  183. package/dist/text-parts.js +36 -0
  184. package/dist/text-parts.js.map +1 -0
  185. package/package.json +20 -20
  186. package/surface-manifest.json +7 -1
  187. package/dist/frontend/lowering/expressions/union-field-write.d.ts +0 -8
  188. package/dist/frontend/lowering/expressions/union-field-write.js +0 -108
  189. package/dist/frontend/lowering/expressions/union-field-write.js.map +0 -1
@@ -2,6 +2,7 @@ import { type CycleInitFlag } from "./cycle-initialization.js";
2
2
  import type { FieldLift } from "./coercions/structural-plans.js";
3
3
  import { CoercionState } from "./coercions/state.js";
4
4
  import { StringIndexBounds } from "./string-index-bounds.js";
5
+ import { NanCodedReads } from "./nan-coded-reads.js";
5
6
  import { RuntimeOptionalLocals } from "./runtime-optional-locals.js";
6
7
  import type { WidthLift } from "./width-lift.js";
7
8
  import { type FnCtx } from "./function-context.js";
@@ -361,6 +362,10 @@ export declare class Lowerer {
361
362
  /** Per-symbol result of the never-reassigned file scan
362
363
  * (bindingNeverReassigned — object-literal generic-method receivers). */
363
364
  readonly neverReassignedCache: Map<ts.Symbol, boolean>;
365
+ /** Typed-array element reads whose every consumer treats undefined like
366
+ * NaN, which lower to plain doubles (see nan-coded-reads.ts). Settled by
367
+ * analyzeRuntimeOptionalArrayReads before any body lowers. */
368
+ nanCodedReads: NanCodedReads | null;
364
369
  private stringIndexBoundsCache;
365
370
  /** Proofs that a string element read names an existing code unit. The
366
371
  * indexed-read analysis and expression lowering share one instance, so a
@@ -23,6 +23,7 @@ import { ArrayElementStates } from "./runtime-optional-elements.js";
23
23
  import { indexReadInBounds } from "./runtime-optional-bounds.js";
24
24
  import { ArrayOwnership } from "./runtime-optional-ownership.js";
25
25
  import { StringIndexBounds } from "./string-index-bounds.js";
26
+ import { NanCodedReads } from "./nan-coded-reads.js";
26
27
  import { RuntimeOptionalLocals } from "./runtime-optional-locals.js";
27
28
  import { sanitizeUnregisteredClassTypes } from "./sanitize-class-types.js";
28
29
  import { UnregisteredClassTypes } from "./unregistered-class-types.js";
@@ -976,6 +977,10 @@ export class Lowerer {
976
977
  /** Per-symbol result of the never-reassigned file scan
977
978
  * (bindingNeverReassigned — object-literal generic-method receivers). */
978
979
  neverReassignedCache = new Map();
980
+ /** Typed-array element reads whose every consumer treats undefined like
981
+ * NaN, which lower to plain doubles (see nan-coded-reads.ts). Settled by
982
+ * analyzeRuntimeOptionalArrayReads before any body lowers. */
983
+ nanCodedReads = null;
979
984
  stringIndexBoundsCache = null;
980
985
  /** Proofs that a string element read names an existing code unit. The
981
986
  * indexed-read analysis and expression lowering share one instance, so a
@@ -3133,6 +3138,32 @@ export class Lowerer {
3133
3138
  }
3134
3139
  }
3135
3140
  }
3141
+ const mappedOrNull = (node) => panicSafe(() => this.mapTypeOf(this.typeOf(node)), null);
3142
+ const nanCoded = new NanCodedReads({
3143
+ symbolOf,
3144
+ declarationOf: (symbol) => this.checker.valueDeclarationOf(symbol),
3145
+ bytesElemOf: (receiver) => {
3146
+ const t = mappedOrNull(receiver);
3147
+ return t?.kind === "bytes" ? t.elem : null;
3148
+ },
3149
+ isNumber: (node) => mappedOrNull(node)?.kind === "f64",
3150
+ isString: (node) => mappedOrNull(node)?.kind === "string",
3151
+ hasLength: (node) => {
3152
+ const kind = mappedOrNull(node)?.kind;
3153
+ return kind === "array" || kind === "bytes";
3154
+ },
3155
+ isStdlibGlobal: (node, name) => this.isStdlibGlobal(node, name),
3156
+ paramIsNumber: (fn, index) => {
3157
+ const param = signatureBySymbol.get(fn)?.params[index];
3158
+ return (param !== undefined &&
3159
+ param.type.kind === "f64" &&
3160
+ (param.mode === "required" || param.mode === "omittable"));
3161
+ },
3162
+ returnIsNumber: (fn) => signatureBySymbol.get(fn)?.returnType.kind === "f64",
3163
+ methodOverridden: (fn) => (familyBySymbol.get(fn)?.length ?? 1) > 1,
3164
+ }, sourceFiles);
3165
+ nanCoded.analyze();
3166
+ this.nanCodedReads = nanCoded;
3136
3167
  const peel = (node) => {
3137
3168
  let e = node;
3138
3169
  while (ts.isParenthesizedExpression(e) ||
@@ -3252,8 +3283,11 @@ export class Lowerer {
3252
3283
  const kind = this.mapTypeOf(this.typeOf(e.expression))?.kind;
3253
3284
  if (kind === "string")
3254
3285
  return !this.stringIndexBounds.inBounds(e);
3255
- // Typed-array reads answer undefined for an invalid index too.
3256
- return kind === "array" || kind === "bytes";
3286
+ // Typed-array reads answer undefined for an invalid index too, unless
3287
+ // every consumer treats undefined like NaN.
3288
+ if (kind === "bytes")
3289
+ return !nanCoded.isNumericRead(e);
3290
+ return kind === "array";
3257
3291
  };
3258
3292
  const isDynamicObjectEntryRead = (node) => {
3259
3293
  const read = peel(node);
@@ -3337,6 +3371,13 @@ export class Lowerer {
3337
3371
  this.mapTypeOf(this.typeOf(e))?.kind === "union" &&
3338
3372
  this.armTag(this.mapTypeOf(this.typeOf(e)).unionId, UNDEFINED_T) >= 0)
3339
3373
  return true;
3374
+ // A NaN-coded value that reaches a consumer able to observe undefined
3375
+ // is rebuilt as the ordinary optional value there; every other read
3376
+ // of a NaN-coded slot is a plain number.
3377
+ if (nanCoded.reboxes(e))
3378
+ return true;
3379
+ if (nanCoded.readsNanCodedSlot(e))
3380
+ return false;
3340
3381
  if (ts.isIdentifier(e)) {
3341
3382
  const symbol = symbolOf(e);
3342
3383
  return (symbol !== null &&
@@ -3429,6 +3470,25 @@ export class Lowerer {
3429
3470
  return false;
3430
3471
  return noteFieldSymbol(symbol, name);
3431
3472
  };
3473
+ /** An instance field of a TypeScript class whose declared storage is
3474
+ * exactly `T | undefined`: an assignment from a missing read stores the
3475
+ * undefined arm while the checker narrows later reads to `T`. */
3476
+ const optionalInstanceField = (symbol) => {
3477
+ const decl = this.checker.valueDeclarationOf(symbol);
3478
+ if (!decl || !ts.isPropertyDeclaration(decl) || !ts.isIdentifier(decl.name))
3479
+ return false;
3480
+ if (ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Static)
3481
+ return false;
3482
+ const sf = decl.getSourceFile();
3483
+ if (isJsSourceFile(sf) || sf.isDeclarationFile)
3484
+ return false;
3485
+ const declared = typeOrNull(decl.name);
3486
+ const mapped = declared ? this.mapTypeOf(declared) : null;
3487
+ if (mapped?.kind !== "union" || this.armTag(mapped.unionId, UNDEFINED_T) < 0)
3488
+ return false;
3489
+ const arms = this.unions.get(mapped.unionId)?.arms ?? [];
3490
+ return (arms.length === 2 && arms.every((arm) => arm.kind === "undefinedT" || !isUnitType(arm)));
3491
+ };
3432
3492
  const noteFieldSymbol = (symbol, name) => {
3433
3493
  const set = optionalFields.get(symbol) ?? new Set();
3434
3494
  const before = set.size;
@@ -3567,14 +3627,31 @@ export class Lowerer {
3567
3627
  return found;
3568
3628
  };
3569
3629
  const jsLambdas = [];
3630
+ // TypeScript lambdas keep the checker's inferred return type, which
3631
+ // spells a missing read as its bare element type and `read ?? "text"`
3632
+ // as a number. Their closures return the runtime result instead.
3633
+ const tsLambdas = [];
3570
3634
  for (const sf of sourceFiles) {
3571
- if (!isJsSourceFile(sf))
3635
+ const js = isJsSourceFile(sf);
3636
+ if (!js && sf.isDeclarationFile)
3572
3637
  continue;
3573
3638
  ts.walkPreorder(sf, (node) => {
3574
3639
  if (ts.isArrowFunction(node) || ts.isFunctionExpression(node))
3575
- jsLambdas.push(node);
3640
+ (js ? jsLambdas : tsLambdas).push(node);
3576
3641
  });
3577
3642
  }
3643
+ /** The primitive result union of a lambda's returns (see
3644
+ * optionalPrimitiveResultType), as declared functions record it. */
3645
+ const lambdaArithmeticReturn = (fn) => {
3646
+ if (fn.body && !ts.isBlock(fn.body))
3647
+ return optionalPrimitiveResultType(fn.body);
3648
+ for (const expression of returnsOf(fn)) {
3649
+ const result = optionalPrimitiveResultType(expression);
3650
+ if (result)
3651
+ return result;
3652
+ }
3653
+ return null;
3654
+ };
3578
3655
  for (const [symbol, signature] of signatureBySymbol) {
3579
3656
  const declaration = functionDeclBySymbol.get(symbol);
3580
3657
  if (!declaration || !isJsSourceFile(declaration.getSourceFile()))
@@ -4262,6 +4339,14 @@ export class Lowerer {
4262
4339
  provenance?.note("static-field", node.left, node.right);
4263
4340
  changed = true;
4264
4341
  }
4342
+ else if (field && !optionalSymbols.has(field) && optionalInstanceField(field)) {
4343
+ // The checker narrows the field to the assigned read's bare
4344
+ // type, but the stored value can be undefined: reads keep the
4345
+ // stored union (runtimeOptionalClassField).
4346
+ optionalSymbols.add(field);
4347
+ provenance?.note("assigned-field", node.left, node.right);
4348
+ changed = true;
4349
+ }
4265
4350
  else if (ts.isIdentifier(node.left.expression)) {
4266
4351
  const symbol = symbolOf(node.left.expression);
4267
4352
  if (symbol && noteFieldSymbol(symbol, node.left.name.text)) {
@@ -4575,6 +4660,52 @@ export class Lowerer {
4575
4660
  for (const [symbol, decl] of functionDeclBySymbol)
4576
4661
  if (scanReturns(symbol, decl))
4577
4662
  changed = true;
4663
+ for (const fn of tsLambdas) {
4664
+ // An annotated return is the author's contract, as for parameters.
4665
+ if (fn.type)
4666
+ continue;
4667
+ const mapped = this.mapTypeOf(this.typeOf(fn));
4668
+ if (mapped?.kind !== "func")
4669
+ continue;
4670
+ const previous = this.runtimeOptionalFunctionReturnType(fn, mapped.ret);
4671
+ // Async, generator and void bodies keep their own result shapes.
4672
+ if (previous.kind === "promise" ||
4673
+ previous.kind === "generator" ||
4674
+ previous.kind === "void")
4675
+ continue;
4676
+ const arithmetic = lambdaArithmeticReturn(fn);
4677
+ const next = arithmetic &&
4678
+ (previous.kind === "f64" || previous.kind === "string" || previous.kind === "bool")
4679
+ ? arithmetic
4680
+ : bodyReturnsOptional(fn)
4681
+ ? this.runtimeOptionalType(previous)
4682
+ : previous;
4683
+ if (!typeEquals(previous, next)) {
4684
+ this.runtimeOptionalFunctionReturns.set(fn, next);
4685
+ provenance?.note("lambda-return", fn, fn.body);
4686
+ changed = true;
4687
+ }
4688
+ // Calls through the lambda's own binding (`const get = () => xs[i];
4689
+ // get()`) produce the same result as calls of a declared function.
4690
+ const binding = fn.parent;
4691
+ if (next !== previous &&
4692
+ ts.isVariableDeclaration(binding) &&
4693
+ binding.initializer === fn &&
4694
+ !binding.type &&
4695
+ ts.isIdentifier(binding.name)) {
4696
+ const symbol = symbolOf(binding.name);
4697
+ if (symbol && next.kind === "union" && this.armTag(next.unionId, UNDEFINED_T) >= 0) {
4698
+ if (!optionalReturns.has(symbol)) {
4699
+ optionalReturns.add(symbol);
4700
+ changed = true;
4701
+ }
4702
+ }
4703
+ else if (symbol && !arithmeticReturns.has(symbol)) {
4704
+ arithmeticReturns.set(symbol, next);
4705
+ changed = true;
4706
+ }
4707
+ }
4708
+ }
4578
4709
  for (const fn of jsLambdas) {
4579
4710
  if (!bodyReturnsOptional(fn))
4580
4711
  continue;
@@ -4606,6 +4737,25 @@ export class Lowerer {
4606
4737
  }
4607
4738
  }
4608
4739
  }
4740
+ // NaN-coded slots convert optional values on entry instead of holding
4741
+ // the union, so promotion never applies to them.
4742
+ for (const symbol of [...optionalSymbols])
4743
+ if (nanCoded.nanCodedBinding(symbol))
4744
+ optionalSymbols.delete(symbol);
4745
+ for (const symbol of [...optionalReturns])
4746
+ if (nanCoded.nanCodedReturn(symbol))
4747
+ optionalReturns.delete(symbol);
4748
+ for (const [symbol, params] of optionalParams) {
4749
+ const decl = functionDeclBySymbol.get(symbol);
4750
+ for (const index of [...params]) {
4751
+ const name = decl?.parameters[index]?.name;
4752
+ const param = name && ts.isIdentifier(name) ? symbolOf(name) : null;
4753
+ if (param && nanCoded.nanCodedBinding(param))
4754
+ params.delete(index);
4755
+ }
4756
+ if (params.size === 0)
4757
+ optionalParams.delete(symbol);
4758
+ }
4609
4759
  for (const [site, present] of presentSites)
4610
4760
  if (present)
4611
4761
  this.presentElementCalls.add(site);
@@ -4678,11 +4828,11 @@ export class Lowerer {
4678
4828
  const symbol = symbolOf(node.name);
4679
4829
  if (!symbol)
4680
4830
  return;
4681
- if (isJsSourceFile(sf) &&
4682
- !node.type &&
4831
+ if (!node.type &&
4683
4832
  !hasJsTypeAnnotation(node) &&
4684
4833
  node.initializer &&
4685
- (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
4834
+ (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) &&
4835
+ (isJsSourceFile(sf) || this.runtimeOptionalFunctionReturns.has(node.initializer))) {
4686
4836
  const global = this.globalsBySymbol.get(symbol);
4687
4837
  if (global?.type.kind === "func")
4688
4838
  global.type = {
@@ -8224,7 +8374,23 @@ export class Lowerer {
8224
8374
  }
8225
8375
  /* ── scoping and captures ─────────────────────────────────────────── */
8226
8376
  declareLocal(nameNode, name, type, mutable) {
8227
- return declareContextLocal(this.ctx, name, type, mutable, this.checker.getSymbolAtLocation(nameNode), bindingSource(nameNode));
8377
+ const symbol = this.checker.getSymbolAtLocation(nameNode);
8378
+ const local = declareContextLocal(this.ctx, name, type, mutable, symbol, bindingSource(nameNode));
8379
+ // A binding declared `T | undefined` that receives a missing read: the
8380
+ // checker narrows it to `T` after the assignment, but the stored value
8381
+ // can be undefined. Track it like widened storage so bare reads keep
8382
+ // the union until a guard proves presence.
8383
+ if (symbol &&
8384
+ this.runtimeOptionalAssignedSymbols.has(symbol) &&
8385
+ type.kind === "union" &&
8386
+ this.armTag(type.unionId, UNDEFINED_T) >= 0 &&
8387
+ this.unions.get(type.unionId)?.arms.length === 2 &&
8388
+ !isUnitType(this.stripUndefinedArm(type))) {
8389
+ const root = this.runtimeOptionalRootOf(local);
8390
+ this.runtimeOptionalStorageLocals.add(root);
8391
+ this.runtimeOptionalLocals.add(root);
8392
+ }
8393
+ return local;
8228
8394
  }
8229
8395
  /** A never-returning call in a value position of type `expected`: the
8230
8396
  * call runs for its effects (it throws or exits), and the expression ends