@scriptc/compiler 0.0.29 → 0.0.31

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 (49) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +5 -13
  2. package/dist/backend/emission/emit-async.d.ts +1 -0
  3. package/dist/backend/emission/emit-async.js +27 -1
  4. package/dist/backend/emission/emit-async.js.map +1 -1
  5. package/dist/backend/emission/emit-exprs.js +109 -18
  6. package/dist/backend/emission/emit-exprs.js.map +1 -1
  7. package/dist/backend/emission/emitter.d.ts +1 -0
  8. package/dist/backend/emission/emitter.js +25 -3
  9. package/dist/backend/emission/emitter.js.map +1 -1
  10. package/dist/backend/llvm/emitter.js +152 -0
  11. package/dist/backend/llvm/emitter.js.map +1 -1
  12. package/dist/backend/mangle.d.ts +1 -0
  13. package/dist/backend/mangle.js +3 -0
  14. package/dist/backend/mangle.js.map +1 -1
  15. package/dist/diagnostics/diagnostic.d.ts +23 -1
  16. package/dist/diagnostics/diagnostic.js +34 -0
  17. package/dist/diagnostics/diagnostic.js.map +1 -1
  18. package/dist/frontend/lowering/lower-calls.d.ts +3 -1
  19. package/dist/frontend/lowering/lower-calls.js +201 -37
  20. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  21. package/dist/frontend/lowering/lower-containers.d.ts +6 -5
  22. package/dist/frontend/lowering/lower-containers.js +27 -13
  23. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  24. package/dist/frontend/lowering/lower-exprs.d.ts +11 -1
  25. package/dist/frontend/lowering/lower-exprs.js +398 -49
  26. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  27. package/dist/frontend/lowering/lower-server.d.ts +5 -0
  28. package/dist/frontend/lowering/lower-server.js +105 -55
  29. package/dist/frontend/lowering/lower-server.js.map +1 -1
  30. package/dist/frontend/lowering/lower-stmts.js +74 -11
  31. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  32. package/dist/frontend/lowering/lowerer.d.ts +50 -12
  33. package/dist/frontend/lowering/lowerer.js +84 -15
  34. package/dist/frontend/lowering/lowerer.js.map +1 -1
  35. package/dist/index.js +35 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/ir/nodes.d.ts +52 -19
  38. package/dist/ir/nodes.js +2 -2
  39. package/dist/ir/nodes.js.map +1 -1
  40. package/dist/ir/validate.js +67 -9
  41. package/dist/ir/validate.js.map +1 -1
  42. package/dist/library/profile.d.ts +33 -0
  43. package/dist/library/profile.js +161 -3
  44. package/dist/library/profile.js.map +1 -1
  45. package/dist/library/sidecar.d.ts +3 -2
  46. package/dist/library/sidecar.js +7 -3
  47. package/dist/library/sidecar.js.map +1 -1
  48. package/package.json +2 -2
  49. package/surface-manifest.json +1 -1
@@ -186,6 +186,8 @@ export declare function finishOptionalChain(L: Lowerer, expr: ts.Expression, id:
186
186
  * short-circuiting, and mixed operand kinds that have no VALUE
187
187
  * representation (`u && flag` — a union and a bool) test fine here. */
188
188
  export declare function lowerCondition(L: Lowerer, expr: ts.Expression): IrExpr;
189
+ export declare function runtimeOptionalTrueIds(L: Lowerer, node: ts.Expression): IrLocal[];
190
+ export declare function withRuntimeOptionalNarrowed<T>(L: Lowerer, locals: readonly IrLocal[], fn: () => T): T;
189
191
  /** JS ToBoolean: bool passes through; f64/string get a `toBool` wrapper
190
192
  * (falsy: 0, -0, NaN, ""); unions get the same wrapper answered by a
191
193
  * per-union interned helper (unit arms falsy; scalar/string arms by
@@ -288,7 +290,15 @@ export declare function lowerElementAccess(L: Lowerer, expr: ts.ElementAccessExp
288
290
  * fields outside that stay fenced. Declared-only shapes support reads
289
291
  * whose key type proves membership (tsc's keyof check): all fields must
290
292
  * share the result type, and a smuggled miss traps. */
291
- export declare function lowerRecordKeyRead(L: Lowerer, expr: ts.ElementAccessExpression, shapeId: string, shape: IrRecordShape): IrExpr;
293
+ export declare function lowerRecordKeyRead(L: Lowerer, expr: ts.ElementAccessExpression, shapeId: string, shape: IrRecordShape, includeUndefined?: boolean): IrExpr;
294
+ /** A keyed read used only to observe absence (`if (map[k])`, or the left
295
+ * side of `||`/`??`). JavaScript answers undefined for missing record keys
296
+ * and array indices even when noUncheckedIndexedAccess is disabled. Keep
297
+ * that value in an explicit union until the surrounding guard/default has
298
+ * consumed it; ordinary unchecked reads retain the typed trap. Assertions
299
+ * around the read are erased here because this context handles the missing
300
+ * arm instead of consuming it as the asserted type. */
301
+ export declare function lowerAbsenceProbe(L: Lowerer, node: ts.Expression): IrExpr | null;
292
302
  /** The compile-time string spelling of a record key literal: a string
293
303
  * literal's text, or a NON-NEGATIVE numeric literal's canonical JS
294
304
  * spelling (`1` → "1", `1e21` → "1e+21" — String(Number(text)), which is
@@ -12,7 +12,7 @@ import { cjsClassExprWholeExportOf, cjsExportAssignmentOf, cjsExportDiscardReaso
12
12
  import { ARRAY_METHODS, builtinConstLit, builtinFenceHintOf, builtinModuleConstOf, builtinModulesArrayLit, builtinModuleFnOf, ISLAND_SURFACE, isChildSurfaceMember, MAP_METHODS, NARROW_FIRST, SET_METHODS, STR_METHODS, UNSUPPORTED_EXPR, sideEffectFreeOptionValue, stdlibGlobalNameOf } from "./surfaces.js";
13
13
  import { blockedBindingUseDiag, recordShapeMismatchDiag, requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
14
14
  import { PoisonError, dynUndefinedExpr, jsFuncNameOf, neverTaintedJsType, nodeThrowExpr, own } from "./lowerer.js";
15
- import { lowerIndexMergeHelper, lowerNpmStaticSafeIndexRead, strCharsCall } from "./lower-containers.js";
15
+ import { lowerIndexMergeHelper, lowerNpmStaticSafeIndexRead, lowerSafeIndexRead, strCharsCall } from "./lower-containers.js";
16
16
  import { npmStaticPackageOfPath } from "../npm-static.js";
17
17
  import { unsupportedModuleFeatureOf } from "../shared.js";
18
18
  import { fenceEnumObjectValue, lowerEnumAccess } from "./lower-enums.js";
@@ -204,6 +204,9 @@ function lowerExprInner(L, expr) {
204
204
  if (ts.isNonNullExpression(expr)) {
205
205
  const inner = L.lowerExpr(expr.expression);
206
206
  if (inner.type.kind === "union") {
207
+ const use = runtimeOptionalUseOf(expr);
208
+ if (runtimeOptionalAssertionErases(L, expr, inner, use))
209
+ return inner;
207
210
  const target = L.mapTypeOf(L.typeOf(expr));
208
211
  if (target && target.kind !== "union" && !typeEquals(target, inner.type)) {
209
212
  const helper = L.narrowedArmHelper(inner.type.unionId, target, loc);
@@ -650,6 +653,37 @@ function lowerExprInner(L, expr) {
650
653
  if (local) {
651
654
  if (local.type.kind === "caught")
652
655
  return L.caughtRead(expr, local, loc);
656
+ const symbol = L.resolveValueSymbol(expr);
657
+ const runtimeOptionalRoot = L.runtimeOptionalRootOf(local);
658
+ const active = L.runtimeOptionalLocals.has(runtimeOptionalRoot);
659
+ const captured = !!symbol &&
660
+ L.runtimeOptionalStorageLocals.has(runtimeOptionalRoot) &&
661
+ L.ctx.captureBySymbol.get(symbol) === local;
662
+ if (active || captured) {
663
+ const use = runtimeOptionalUseOf(expr);
664
+ if (use?.complex) {
665
+ L.unsupported("SC1090", expr, "a logical or conditional receiver containing a runtime-optional capture");
666
+ }
667
+ if (use?.optional)
668
+ return { kind: "varRef", localId: local.id, type: local.type, loc };
669
+ if (use?.kind === "property")
670
+ return runtimeOptionalReceiverRead(L, expr, local, use.access.name.text, loc);
671
+ if (use?.kind === "element") {
672
+ const key = runtimeOptionalElementKey(use.access.argumentExpression);
673
+ if (key === null) {
674
+ L.unsupported("SC1090", use.access, "a computed read from a runtime-optional capture (use a literal key)");
675
+ }
676
+ return runtimeOptionalReceiverRead(L, expr, local, key, loc);
677
+ }
678
+ if (use?.kind === "call") {
679
+ if (use.comma && !use.optional) {
680
+ L.unsupported("SC1090", use.access, "a direct call through a comma-wrapped runtime-optional capture");
681
+ }
682
+ return runtimeOptionalReceiverRead(L, expr, local, null, loc);
683
+ }
684
+ if (active || captured)
685
+ return { kind: "varRef", localId: local.id, type: local.type, loc };
686
+ }
653
687
  return L.maybeNarrow({ kind: "varRef", localId: local.id, type: local.type, loc }, expr);
654
688
  }
655
689
  // `import x = N.y` aliases resolve transparently through globalOf/
@@ -877,11 +911,8 @@ function lowerExprInner(L, expr) {
877
911
  L.noLowering(expr.text, expr, globalHints[expr.text], sym ?? undefined);
878
912
  }
879
913
  }
880
- // `declare const __VERSION__: string` — an ambient global NOTHING
881
- // defines (a bundler define in the real pipeline; scriptc has no
882
- // define mechanism, and neither does Node running the source —
883
- // the oracle). The read compiles to exactly what Node does at the
884
- // access: the catchable ReferenceError "<name> is not defined".
914
+ // An ambient global nothing defines compiles to exactly what Node does
915
+ // at the access: the catchable ReferenceError "<name> is not defined".
885
916
  // Stdlib/@types/node ambients never reach here (their chokepoint
886
917
  // is above); only user-file declares do.
887
918
  {
@@ -1795,9 +1826,7 @@ export function lowerTernary(L, expr, expected) {
1795
1826
  const constituents = t.isUnionType() ? t.getTypes() : [];
1796
1827
  const nonArray = constituents.filter((a) => !L.checker.isArrayType(a) && !L.checker.isTupleType(a));
1797
1828
  const mapped = L.mapTypeOf(t);
1798
- const armCount = mapped?.kind === "union"
1799
- ? (L.unions.get(mapped.unionId)?.arms.filter((a) => a.kind === "array").length ?? 0)
1800
- : 0;
1829
+ const armCount = mapped?.kind === "union" ? L.arrayValueTags(mapped.unionId).length : 0;
1801
1830
  if (sym && nonArray.length === 1 && armCount === 1) {
1802
1831
  falseArmNarrowType = nonArray[0];
1803
1832
  const collect = (n) => {
@@ -1913,12 +1942,14 @@ export function lowerTernary(L, expr, expected) {
1913
1942
  })();
1914
1943
  let thenRaw;
1915
1944
  let elseRaw;
1945
+ const runtimeTrueIds = runtimeOptionalTrueIds(L, expr.condition);
1946
+ const lowerTrueArm = () => withRuntimeOptionalNarrowed(L, runtimeTrueIds, () => lowerArm(expr.whenTrue, ownArrayJoin ?? undefined));
1916
1947
  if (!ctxArray && emptyUntypedArrayArm(expr.whenTrue) && !emptyUntypedArrayArm(expr.whenFalse)) {
1917
1948
  elseRaw = L.lowerExpr(expr.whenFalse);
1918
- thenRaw = lowerArm(expr.whenTrue, elseRaw.type);
1949
+ thenRaw = withRuntimeOptionalNarrowed(L, runtimeTrueIds, () => lowerArm(expr.whenTrue, elseRaw.type));
1919
1950
  }
1920
1951
  else {
1921
- thenRaw = lowerArm(expr.whenTrue, ownArrayJoin ?? undefined);
1952
+ thenRaw = lowerTrueArm();
1922
1953
  elseRaw = lowerArm(expr.whenFalse, thenRaw.type);
1923
1954
  }
1924
1955
  // The ternary's IR type is normally the checker's own: it collapses
@@ -2069,25 +2100,20 @@ export function maybeNarrow(L, expr, node) {
2069
2100
  }
2070
2101
  if (expr.type.kind !== "union")
2071
2102
  return expr;
2072
- const narrowed = L.mapTypeOf(L.typeOf(node));
2073
- // `Array.isArray(u)` on a `string | readonly string[]` union: the lib
2074
- // predicate narrows the READONLY arm to `any[]` (tsc's readonly-array
2075
- // quirk), which maps to the island's array-of-handles (or nothing) —
2076
- // but the tag test proved the union's one array arm, so the bridge is
2077
- // the same trusted unionNarrow the mapped case below builds (the
2078
- // certs configuredTlds shape).
2079
- if (!narrowed || (narrowed.kind === "array" && narrowed.elem.kind === "jsval")) {
2080
- const t = L.typeOf(node);
2081
- const anyElem = L.checker.isArrayType(t) &&
2082
- ((L.checker.getTypeArguments(t)[0]?.flags ?? 0) &
2083
- (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0;
2084
- if (anyElem) {
2085
- const def = L.unions.get(expr.type.unionId);
2086
- const arrayTags = def ? def.arms.flatMap((a, i) => (a.kind === "array" ? [i] : [])) : [];
2087
- if (arrayTags.length === 1) {
2088
- const arm = def.arms[arrayTags[0]];
2089
- return { kind: "unionNarrow", unionId: expr.type.unionId, tag: arrayTags[0], value: expr, type: arm, loc: expr.loc };
2090
- }
2103
+ const narrowedTs = L.typeOf(node);
2104
+ const narrowed = L.mapTypeOf(narrowedTs);
2105
+ // `Array.isArray(u)` on a union with one readonly array/tuple arm: the
2106
+ // lib predicate narrows to `any[]`, either directly or intersected
2107
+ // with the original union. That checker type can map to the island's
2108
+ // array-of-handles, nothing, or a synthetic intersection record — but
2109
+ // the tag test proved the union's one array-valued arm, so bridge to it
2110
+ // with the same trusted unionNarrow the mapped case below builds.
2111
+ if (L.checkerAnyArrayType(narrowedTs)) {
2112
+ const def = L.unions.get(expr.type.unionId);
2113
+ const arrayTags = L.arrayValueTags(expr.type.unionId);
2114
+ if (arrayTags.length === 1) {
2115
+ const arm = def.arms[arrayTags[0]];
2116
+ return { kind: "unionNarrow", unionId: expr.type.unionId, tag: arrayTags[0], value: expr, type: arm, loc: expr.loc };
2091
2117
  }
2092
2118
  }
2093
2119
  // A checker type narrowed to a UNIT arm (the `=== undefined` branch)
@@ -2110,6 +2136,109 @@ export function maybeNarrow(L, expr, node) {
2110
2136
  loc: expr.loc,
2111
2137
  };
2112
2138
  }
2139
+ /** A runtime-optional slot can be assigned while a truthy guard is in
2140
+ * force. TypeScript then types a direct member receiver as the plain arm,
2141
+ * even though an OOB-safe assignment may have restored undefined. Check
2142
+ * that hidden arm and throw the member-read TypeError Node would produce. */
2143
+ function runtimeOptionalReceiverRead(L, expr, local, property, loc) {
2144
+ if (local.type.kind !== "union")
2145
+ throw new Error("runtime-optional local is not union-typed");
2146
+ const narrowed = L.mapTypeOf(L.typeOf(expr));
2147
+ if (!narrowed || narrowed.kind === "union" || isUnitType(narrowed)) {
2148
+ L.unsupported("SC1090", expr, "a runtime-optional capture whose narrowed value type is not representable");
2149
+ }
2150
+ const valueTag = L.armTag(local.type.unionId, narrowed);
2151
+ const undefTag = L.armTag(local.type.unionId, UNDEFINED_T);
2152
+ if (valueTag < 0 || undefTag < 0)
2153
+ throw new Error("runtime-optional local is missing its value or undefined arm");
2154
+ const def = L.unions.get(local.type.unionId);
2155
+ if (!def || def.arms.length !== 2) {
2156
+ L.unsupported("SC1090", expr, "a direct read from a runtime-optional capture with multiple value arms");
2157
+ }
2158
+ const ref = () => ({ kind: "varRef", localId: local.id, type: local.type, loc });
2159
+ const message = property === null
2160
+ ? `${expr.text} is not a function`
2161
+ : `Cannot read properties of undefined (reading '${property}')`;
2162
+ return {
2163
+ kind: "ternary",
2164
+ cond: {
2165
+ kind: "unionIsTag",
2166
+ unionId: local.type.unionId,
2167
+ tag: undefTag,
2168
+ negated: false,
2169
+ value: ref(),
2170
+ type: BOOL,
2171
+ loc,
2172
+ },
2173
+ then: nodeThrowExpr(1, "", message, narrowed, loc),
2174
+ else_: {
2175
+ kind: "unionNarrow",
2176
+ unionId: local.type.unionId,
2177
+ tag: valueTag,
2178
+ value: ref(),
2179
+ type: narrowed,
2180
+ loc,
2181
+ },
2182
+ type: narrowed,
2183
+ loc,
2184
+ };
2185
+ }
2186
+ function runtimeOptionalUseOf(expr) {
2187
+ let receiver = expr;
2188
+ let complex = false;
2189
+ let comma = false;
2190
+ for (;;) {
2191
+ const parent = receiver.parent;
2192
+ if ((ts.isParenthesizedExpression(parent) || ts.isAssertionExpression(parent) ||
2193
+ ts.isSatisfiesExpression(parent) ||
2194
+ ts.isNonNullExpression(parent)) &&
2195
+ parent.expression === receiver) {
2196
+ receiver = parent;
2197
+ continue;
2198
+ }
2199
+ if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.CommaToken && parent.right === receiver) {
2200
+ comma = true;
2201
+ receiver = parent;
2202
+ continue;
2203
+ }
2204
+ if ((ts.isBinaryExpression(parent) &&
2205
+ (parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
2206
+ parent.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
2207
+ parent.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) &&
2208
+ (parent.left === receiver || parent.right === receiver)) ||
2209
+ (ts.isConditionalExpression(parent) &&
2210
+ (parent.condition === receiver || parent.whenTrue === receiver || parent.whenFalse === receiver))) {
2211
+ complex = true;
2212
+ receiver = parent;
2213
+ continue;
2214
+ }
2215
+ break;
2216
+ }
2217
+ const access = receiver.parent;
2218
+ if (ts.isPropertyAccessExpression(access) && access.expression === receiver) {
2219
+ return { kind: "property", access, optional: access.questionDotToken !== undefined, complex, comma };
2220
+ }
2221
+ if (ts.isElementAccessExpression(access) && access.expression === receiver) {
2222
+ return { kind: "element", access, optional: access.questionDotToken !== undefined, complex, comma };
2223
+ }
2224
+ if (ts.isCallExpression(access) && access.expression === receiver) {
2225
+ return { kind: "call", access, optional: access.questionDotToken !== undefined, complex, comma };
2226
+ }
2227
+ return null;
2228
+ }
2229
+ function runtimeOptionalElementKey(expr) {
2230
+ if (ts.isStringLiteral(expr) || ts.isNumericLiteral(expr))
2231
+ return expr.text;
2232
+ if (ts.isNoSubstitutionTemplateLiteral(expr))
2233
+ return expr.text;
2234
+ return null;
2235
+ }
2236
+ function runtimeOptionalAssertionErases(L, expr, inner, use) {
2237
+ if (inner.type.kind !== "union" || !use?.optional || use.complex || L.armTag(inner.type.unionId, UNDEFINED_T) < 0)
2238
+ return false;
2239
+ const target = L.mapTypeOf(L.typeOf(expr));
2240
+ return !!target && target.kind !== "union" && typeEquals(L.stripUndefinedArm(inner.type), target);
2241
+ }
2113
2242
  /** `v === undefined` / `v !== null` — the narrowing tests for unit-armed
2114
2243
  * unions. A union operand compared with a unit literal lowers to a
2115
2244
  * runtime TAG test (unionIsTag); afterwards tsc's control-flow narrowing
@@ -2374,7 +2503,7 @@ function pureCondExpr(e) {
2374
2503
  * results (several non-unit arms) and defaults that change the result
2375
2504
  * type are fenced with narrow-first hints. */
2376
2505
  export function lowerNullishCoalesce(L, expr, loc) {
2377
- const left = L.lowerExpr(expr.left);
2506
+ const left = lowerAbsenceProbe(L, expr.left) ?? L.lowerExpr(expr.left);
2378
2507
  if (left.type.kind === "dyn") {
2379
2508
  // `a ?? b` on a CHECKED-DYNAMIC left: the deciding test is the
2380
2509
  // runtime kind (scr_dyn_is_nullish — UNDEF/NULL take the default;
@@ -2850,7 +2979,14 @@ export function finishOptionalChain(L, expr, id, receiver, body, loc) {
2850
2979
  if (body.type.kind === "dyn") {
2851
2980
  return { kind: "optChain", id, receiver, body, type: DYN, loc };
2852
2981
  }
2853
- const type = L.irTypeOf(expr);
2982
+ let type = L.irTypeOf(expr);
2983
+ const hiddenOptionalResult = (type.kind !== "union" || L.armTag(type.unionId, UNDEFINED_T) < 0) &&
2984
+ receiver.type.kind === "union" && L.armTag(receiver.type.unionId, UNDEFINED_T) >= 0 &&
2985
+ body.type.kind !== "generator" && body.type.kind !== "jsval"
2986
+ ? L.withUndefinedArmOf(body.type)
2987
+ : null;
2988
+ if (hiddenOptionalResult)
2989
+ type = hiddenOptionalResult;
2854
2990
  if (type.kind !== "union" || L.armTag(type.unionId, UNDEFINED_T) < 0) {
2855
2991
  L.unsupported("SC1090", expr, `'?.' where the result '${L.fmt(type)}' has no undefined arm (narrow the receiver with 'if (x !== undefined)' instead)`);
2856
2992
  }
@@ -2877,7 +3013,18 @@ export function lowerCondition(L, expr) {
2877
3013
  // (true for &&, false for ||) — aliased-typeof narrows the left
2878
3014
  // PROVES under that polarity hold while it lowers (`type ===
2879
3015
  // 'string' && val.length > 0`, the ms entry shape).
2880
- const right = L.narrowingAliases(aliasTypeofNarrows(L, e.left, isAnd), () => L.lowerCondition(e.right));
3016
+ const runtimeOptionalLocal = isAnd ? runtimeOptionalLocalOf(L, e.left) : null;
3017
+ const right = L.narrowingAliases(aliasTypeofNarrows(L, e.left, isAnd), () => {
3018
+ if (runtimeOptionalLocal === null)
3019
+ return L.lowerCondition(e.right);
3020
+ L.runtimeOptionalLocals.delete(L.runtimeOptionalRootOf(runtimeOptionalLocal));
3021
+ try {
3022
+ return L.lowerCondition(e.right);
3023
+ }
3024
+ finally {
3025
+ L.runtimeOptionalLocals.add(L.runtimeOptionalRootOf(runtimeOptionalLocal));
3026
+ }
3027
+ });
2881
3028
  return {
2882
3029
  kind: "logical",
2883
3030
  op: isAnd ? "&&" : "||",
@@ -2888,7 +3035,40 @@ export function lowerCondition(L, expr) {
2888
3035
  };
2889
3036
  }
2890
3037
  }
2891
- return L.ensureBool(L.lowerExpr(expr), expr);
3038
+ return L.ensureBool(lowerAbsenceProbe(L, expr) ?? L.lowerExpr(expr), expr);
3039
+ }
3040
+ function runtimeOptionalLocalOf(L, node) {
3041
+ let expr = node;
3042
+ while (ts.isParenthesizedExpression(expr))
3043
+ expr = expr.expression;
3044
+ if (!ts.isIdentifier(expr))
3045
+ return null;
3046
+ const local = L.resolveLocal(expr);
3047
+ return local && L.runtimeOptionalLocals.has(L.runtimeOptionalRootOf(local)) ? local : null;
3048
+ }
3049
+ export function runtimeOptionalTrueIds(L, node) {
3050
+ let expr = node;
3051
+ while (ts.isParenthesizedExpression(expr))
3052
+ expr = expr.expression;
3053
+ if (ts.isBinaryExpression(expr) &&
3054
+ expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
3055
+ return [...runtimeOptionalTrueIds(L, expr.left), ...runtimeOptionalTrueIds(L, expr.right)];
3056
+ }
3057
+ const local = runtimeOptionalLocalOf(L, expr);
3058
+ return local === null ? [] : [local];
3059
+ }
3060
+ export function withRuntimeOptionalNarrowed(L, locals, fn) {
3061
+ if (locals.length === 0)
3062
+ return fn();
3063
+ for (const local of locals)
3064
+ L.runtimeOptionalLocals.delete(L.runtimeOptionalRootOf(local));
3065
+ try {
3066
+ return fn();
3067
+ }
3068
+ finally {
3069
+ for (const local of locals)
3070
+ L.runtimeOptionalLocals.add(L.runtimeOptionalRootOf(local));
3071
+ }
2892
3072
  }
2893
3073
  /** JS ToBoolean: bool passes through; f64/string get a `toBool` wrapper
2894
3074
  * (falsy: 0, -0, NaN, ""); unions get the same wrapper answered by a
@@ -5378,9 +5558,17 @@ export function lowerElementAccess(L, expr) {
5378
5558
  // `const cmd = ['pwd', []]`, whose binding lowered checked-dynamic)
5379
5559
  // dispatches as unmapped so the dyn-receiver branch below reads
5380
5560
  // through the checked-dynamic tree instead of dynChecking into never's f64 residue.
5381
- const receiverIr = neverTaintedJsType(L, expr.expression, L.typeOf(expr.expression))
5561
+ let receiverIr = neverTaintedJsType(L, expr.expression, L.typeOf(expr.expression))
5382
5562
  ? null
5383
5563
  : L.mapTypeOf(L.typeOf(expr.expression));
5564
+ // A readonly tuple union under Array.isArray is checker-typed as an
5565
+ // intersection with any[], whose structural mapping is not the tuple
5566
+ // shape. maybeNarrow on the receiver uses the runtime tag proof to
5567
+ // extract the one array-valued arm; dispatch element access from that
5568
+ // lowered representation, like the existing any[] array-method path.
5569
+ const checkerArray = L.checkerArrayValue(expr.expression);
5570
+ if (checkerArray)
5571
+ receiverIr = checkerArray.type;
5384
5572
  if (receiverIr?.kind === "jsval") {
5385
5573
  // Dispatch follows the RUNTIME world (383(d)): a checker-'any'
5386
5574
  // receiver whose value LOWERED checked-dynamic (`bag.list[0]` where
@@ -5595,13 +5783,18 @@ export function lowerElementAccess(L, expr) {
5595
5783
  // signature where the .d.ts says Function[], which would flatten the
5596
5784
  // element to a zero-parameter closure).
5597
5785
  const elemT = arr.type.kind === "array" ? arr.type.elem : receiverIr.elem;
5598
- // --npm-static package files: the OOB-SAFE read (elem | undefined)
5599
- // for the LAST-ELEMENT idiom — indexing a fresh slice() result
5600
- // (commander's `registeredArguments.slice(-1)[0]` probe), where the
5601
- // trap divergence would fire on working Node code and the fresh
5602
- // array's read can only be a probe. General indexed reads keep the
5603
- // documented trap (their results feed writes and member reads whose
5604
- // lowerings need the plain element).
5786
+ // OOB-SAFE reads over fresh/probe array results. These expressions are
5787
+ // commonly used to ask for a first/last match (`filter(...).sort(...)[0]`,
5788
+ // `slice(-1)[0]`); an empty result is ordinary undefined, not a bounds
5789
+ // failure. Downstream lowering keeps the explicit optional union, so an
5790
+ // unchecked consumer still cannot place it into a plain element slot.
5791
+ if (arr.type.kind === "array" && isFreshArrayProbe(expr.expression)) {
5792
+ const safe = lowerSafeIndexRead(L, arr, index, locOf(expr));
5793
+ if (safe)
5794
+ return safe;
5795
+ }
5796
+ // --npm-static package files retain the original slice-specific route
5797
+ // for compatibility with package-source inference.
5605
5798
  if (arr.type.kind === "array" &&
5606
5799
  arr.kind === "arrIntrinsic" &&
5607
5800
  arr.method === "slice" &&
@@ -5615,6 +5808,16 @@ export function lowerElementAccess(L, expr) {
5615
5808
  // narrows literal-index element accesses).
5616
5809
  return L.maybeNarrow({ kind: "arrayGet", arr, index, type: elemT, loc: locOf(expr) }, expr);
5617
5810
  }
5811
+ function isFreshArrayProbe(node) {
5812
+ let current = node;
5813
+ while (ts.isParenthesizedExpression(current))
5814
+ current = current.expression;
5815
+ return (ts.isCallExpression(current) &&
5816
+ ts.isPropertyAccessExpression(current.expression) &&
5817
+ (current.expression.name.text === "filter" ||
5818
+ current.expression.name.text === "slice" ||
5819
+ current.expression.name.text === "sort"));
5820
+ }
5618
5821
  /** `o[k]` where the RECEIVER is an island value — a jsval-mapped checker
5619
5822
  * type, or an array/tuple-typed .d.ts surface whose lowered value is a
5620
5823
  * handle. The key marshals in (any JS key kind — the engine does its
@@ -5654,7 +5857,7 @@ function islandElementRead(L, expr, obj) {
5654
5857
  * fields outside that stay fenced. Declared-only shapes support reads
5655
5858
  * whose key type proves membership (tsc's keyof check): all fields must
5656
5859
  * share the result type, and a smuggled miss traps. */
5657
- export function lowerRecordKeyRead(L, expr, shapeId, shape) {
5860
+ export function lowerRecordKeyRead(L, expr, shapeId, shape, includeUndefined = false) {
5658
5861
  const loc = locOf(expr);
5659
5862
  const keyNode = expr.argumentExpression;
5660
5863
  // Literal declared keys are plain field reads (narrowing included).
@@ -5721,7 +5924,7 @@ export function lowerRecordKeyRead(L, expr, shapeId, shape) {
5721
5924
  let declared = null;
5722
5925
  if (shape.indexValue) {
5723
5926
  declared = shape.indexValue;
5724
- if (L.program.getCompilerOptions().noUncheckedIndexedAccess) {
5927
+ if (includeUndefined || L.program.getCompilerOptions().noUncheckedIndexedAccess) {
5725
5928
  declared = L.withUndefinedArmOf(declared);
5726
5929
  if (!declared)
5727
5930
  L.badType(expr, L.typeOf(expr));
@@ -5740,7 +5943,72 @@ export function lowerRecordKeyRead(L, expr, shapeId, shape) {
5740
5943
  if (!recordKeyResultOk(L, overflowOnly ? { ...shape, fields: [] } : shape, declared)) {
5741
5944
  L.unsupported("SC1090", expr, `dynamic keyed reads of '${L.fmt({ kind: "record", shapeId })}' as '${L.fmt(declared)}' (every declared field must be readable at that type)`);
5742
5945
  }
5743
- return L.maybeNarrow({ kind: "recordKeyGet", obj, shapeId, key, ...(overflowOnly ? { overflowOnly: true } : {}), type: declared, loc }, expr);
5946
+ const read = {
5947
+ kind: "recordKeyGet",
5948
+ obj,
5949
+ shapeId,
5950
+ key,
5951
+ ...(overflowOnly ? { overflowOnly: true } : {}),
5952
+ type: declared,
5953
+ loc,
5954
+ };
5955
+ return includeUndefined ? read : L.maybeNarrow(read, expr);
5956
+ }
5957
+ /** A keyed read used only to observe absence (`if (map[k])`, or the left
5958
+ * side of `||`/`??`). JavaScript answers undefined for missing record keys
5959
+ * and array indices even when noUncheckedIndexedAccess is disabled. Keep
5960
+ * that value in an explicit union until the surrounding guard/default has
5961
+ * consumed it; ordinary unchecked reads retain the typed trap. Assertions
5962
+ * around the read are erased here because this context handles the missing
5963
+ * arm instead of consuming it as the asserted type. */
5964
+ export function lowerAbsenceProbe(L, node) {
5965
+ let expr = node;
5966
+ while (ts.isParenthesizedExpression(expr) || ts.isAsExpression(expr) || ts.isTypeAssertion(expr)) {
5967
+ expr = expr.expression;
5968
+ }
5969
+ if (ts.isPropertyAccessExpression(expr)) {
5970
+ const target = L.fieldTarget(expr);
5971
+ if (target?.container !== "recordOvf")
5972
+ return null;
5973
+ if (target.fieldType.kind === "union" &&
5974
+ L.armTag(target.fieldType.unionId, UNDEFINED_T) >= 0) {
5975
+ return null;
5976
+ }
5977
+ const type = L.withUndefinedArmOf(target.fieldType);
5978
+ if (!type)
5979
+ return null;
5980
+ return {
5981
+ kind: "recordKeyGet",
5982
+ obj: target.obj,
5983
+ shapeId: target.shapeId,
5984
+ key: { kind: "strLit", value: target.field, type: STRING, loc: locOf(expr) },
5985
+ overflowOnly: true,
5986
+ type,
5987
+ loc: locOf(expr),
5988
+ };
5989
+ }
5990
+ if (!ts.isElementAccessExpression(expr))
5991
+ return null;
5992
+ const header = L.lowerHttpHeadersElement(expr);
5993
+ if (header)
5994
+ return header;
5995
+ const receiverT = L.mapTypeOf(L.typeOf(expr.expression));
5996
+ if (receiverT?.kind === "record") {
5997
+ const shape = L.shapes.get(receiverT.shapeId);
5998
+ if (shape && !shape.tuple && shape.indexValue && shape.indexValue.kind !== "dyn") {
5999
+ return L.lowerRecordKeyRead(expr, receiverT.shapeId, shape, true);
6000
+ }
6001
+ return null;
6002
+ }
6003
+ if (receiverT?.kind !== "array")
6004
+ return null;
6005
+ const arr = L.lowerExpr(expr.expression);
6006
+ if (arr.type.kind !== "array")
6007
+ return null;
6008
+ const index = L.lowerExpr(expr.argumentExpression);
6009
+ if (index.type.kind !== "f64")
6010
+ return null;
6011
+ return lowerSafeIndexRead(L, arr, index, locOf(expr));
5744
6012
  }
5745
6013
  /** The compile-time string spelling of a record key literal: a string
5746
6014
  * literal's text, or a NON-NEGATIVE numeric literal's canonical JS
@@ -6320,6 +6588,9 @@ export function lowerAsExpression(L, expr) {
6320
6588
  L.unsupported("SC1063", expr);
6321
6589
  }
6322
6590
  const inner = L.lowerExpr(expr.expression);
6591
+ const use = runtimeOptionalUseOf(expr);
6592
+ if (inner.type.kind === "union" && runtimeOptionalAssertionErases(L, expr, inner, use))
6593
+ return inner;
6323
6594
  if (inner.type.kind !== "dyn" && inner.type.kind !== "jsval") {
6324
6595
  // A STATIC value cast `as any` is the explicit island entrance.
6325
6596
  const targetTs0 = L.checker.getTypeFromTypeNode(expr.type);
@@ -6560,6 +6831,9 @@ export function lowerBinary(L, expr) {
6560
6831
  L.rejectUnresolved(expr.left, `assignment to '${expr.left.text}' (not a writable local or module global)`);
6561
6832
  }
6562
6833
  const value = L.lowerExprExpecting(expr.right, target.type);
6834
+ const runtimeOptionalRoot = L.runtimeOptionalRootOf(target);
6835
+ if (L.runtimeOptionalStorageLocals.has(runtimeOptionalRoot))
6836
+ L.runtimeOptionalLocals.add(runtimeOptionalRoot);
6563
6837
  return { kind: "assignExpr", localId: target.id, value, type: target.type, loc };
6564
6838
  }
6565
6839
  // `events.defaultMaxListeners = v` — the module-property write
@@ -6719,11 +6993,16 @@ export function lowerBinary(L, expr) {
6719
6993
  L.unsupported("SC1040", expr);
6720
6994
  }
6721
6995
  if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) {
6996
+ if (op === ts.SyntaxKind.BarBarToken) {
6997
+ const fallback = lowerAbsenceAwareOrChain(L, expr);
6998
+ if (fallback)
6999
+ return fallback;
7000
+ }
6722
7001
  // JS value semantics: `a && b` is `toBool(a) ? b : a` — the result is
6723
7002
  // an operand value, so both operands must share one IR kind (which is
6724
7003
  // also what tsc's type says, modulo literal-union collapsing that
6725
7004
  // mapType already handles). Mixed kinds (`n && s`) stay rejected.
6726
- const left = L.lowerExpr(expr.left);
7005
+ const left = lowerAbsenceProbe(L, expr.left) ?? L.lowerExpr(expr.left);
6727
7006
  const right = L.lowerExpr(expr.right);
6728
7007
  // A LITERAL-unit left operand (a compile-time undefined/null — the
6729
7008
  // capability-probe members: `process.features.inspector ||
@@ -6894,8 +7173,10 @@ export function lowerBinary(L, expr) {
6894
7173
  if (test)
6895
7174
  return test;
6896
7175
  }
6897
- const left = L.lowerExpr(expr.left);
6898
- const right = L.lowerExpr(expr.right);
7176
+ const strictEquality = op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
7177
+ op === ts.SyntaxKind.ExclamationEqualsEqualsToken;
7178
+ const left = strictEquality ? lowerAbsenceProbe(L, expr.left) ?? L.lowerExpr(expr.left) : L.lowerExpr(expr.left);
7179
+ const right = strictEquality ? lowerAbsenceProbe(L, expr.right) ?? L.lowerExpr(expr.right) : L.lowerExpr(expr.right);
6899
7180
  if (left.type.kind === "dyn" || right.type.kind === "dyn") {
6900
7181
  // The narrowing unit comparisons ARE answerable on unknown: `v ===
6901
7182
  // undefined` / `v !== null` test the dyn node's kind directly, and
@@ -7106,6 +7387,29 @@ export function lowerBinary(L, expr) {
7106
7387
  const ut = left.type.kind === "union" ? left.type : right.type;
7107
7388
  const bothUnion = left.type.kind === "union" && right.type.kind === "union";
7108
7389
  const sameUnion = bothUnion && typeEquals(left.type, right.type);
7390
+ const plainFunc = left.type.kind === "func" ? left : right.type.kind === "func" ? right : null;
7391
+ const unionArms = L.unions.get(ut.unionId)?.arms ?? [];
7392
+ const funcArm = unionArms.find((arm) => arm.kind === "func");
7393
+ // `listeners()` reports original prefix callbacks, but the
7394
+ // checker views an indexed result as function | undefined. For
7395
+ // identity comparison, retain the original closure pointer rather
7396
+ // than adapting it to the tuple signature.
7397
+ if (!bothUnion &&
7398
+ plainFunc !== null &&
7399
+ funcArm?.kind === "func" &&
7400
+ unionArms.filter((arm) => arm.kind === "func").length === 1 &&
7401
+ unionArms.some(isUnitType)) {
7402
+ return {
7403
+ kind: "unionFuncEq",
7404
+ unionId: ut.unionId,
7405
+ tag: L.armTag(ut.unionId, funcArm),
7406
+ union: left.type.kind === "union" ? left : right,
7407
+ func: plainFunc,
7408
+ negated,
7409
+ type: BOOL,
7410
+ loc,
7411
+ };
7412
+ }
7109
7413
  if ((sameUnion || !bothUnion) && L.eqComparableUnion(ut.unionId)) {
7110
7414
  return {
7111
7415
  kind: "unionEq",
@@ -7193,6 +7497,44 @@ export function lowerBinary(L, expr) {
7193
7497
  }
7194
7498
  L.unsupported("SC1090", expr, `operator '${ts.tokenToString(op) ?? ts.SyntaxKind[op]}'`);
7195
7499
  }
7500
+ function lowerAbsenceAwareOrChain(L, expr) {
7501
+ const nodes = [];
7502
+ const collect = (node) => {
7503
+ if (ts.isBinaryExpression(node) &&
7504
+ node.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
7505
+ collect(node.left);
7506
+ collect(node.right);
7507
+ }
7508
+ else {
7509
+ nodes.push(node);
7510
+ }
7511
+ };
7512
+ collect(expr);
7513
+ if (nodes.length < 3)
7514
+ return null;
7515
+ const lowered = nodes.map((node) => lowerAbsenceProbe(L, node) ?? L.lowerExpr(node));
7516
+ if (!lowered.some((value) => value.type.kind === "union" && L.armTag(value.type.unionId, UNDEFINED_T) >= 0)) {
7517
+ return null;
7518
+ }
7519
+ let result = lowered[lowered.length - 1];
7520
+ for (let i = lowered.length - 2; i >= 0; i--) {
7521
+ const left = lowered[i];
7522
+ const loc = locOf(nodes[i]);
7523
+ if (left.type.kind === "union") {
7524
+ const def = L.unions.get(left.type.unionId);
7525
+ const nonUnit = def?.arms.filter((arm) => !isUnitType(arm)) ?? [];
7526
+ if (nonUnit.length !== 1 || !typeEquals(nonUnit[0], result.type))
7527
+ return null;
7528
+ L.requireTruthyUnion(left.type.unionId, expr);
7529
+ result = { kind: "orDefault", left, right: result, type: result.type, loc };
7530
+ continue;
7531
+ }
7532
+ if (!typeEquals(left.type, result.type))
7533
+ return null;
7534
+ result = { kind: "logical", op: "||", left, right: result, type: result.type, loc };
7535
+ }
7536
+ return result;
7537
+ }
7196
7538
  /** `typeof e === "lit"` / `typeof e !== "lit"` where e is a catch
7197
7539
  * binding: the runtime kind test over the snapshot's tag. Only the
7198
7540
  * primitive literals narrow ("string"/"number"/"boolean" — exactly what
@@ -9009,7 +9351,14 @@ export function lowerFieldRead(L, expr) {
9009
9351
  return L.fieldGetExpr(target, locOf(expr), expr);
9010
9352
  if (expr.questionDotToken)
9011
9353
  return null;
9012
- const receiverIr = L.mapTypeOf(L.typeOf(expr.expression));
9354
+ let receiverIr = L.mapTypeOf(L.typeOf(expr.expression));
9355
+ // The readonly-tuple Array.isArray narrow has no directly mappable
9356
+ // checker type (`Result & any[]`), but maybeNarrow extracted the tuple
9357
+ // record behind it. Route tuple properties such as `.length` from that
9358
+ // lowered value, the element-access bridge's twin.
9359
+ const checkerArray = L.checkerArrayValue(expr.expression);
9360
+ if (checkerArray?.type.kind === "record")
9361
+ receiverIr = checkerArray.type;
9013
9362
  if (receiverIr?.kind === "object" &&
9014
9363
  (L.findMethodOn(L.classes.get(receiverIr.className) ?? null, expr.name.text) ||
9015
9364
  findGenericMethodOn(L, L.classes.get(receiverIr.className) ?? null, expr.name.text))) {