@scriptc/compiler 0.0.9 → 0.0.11

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 (97) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +54 -9
  2. package/dist/backend/emission/emit-exprs.js +170 -14
  3. package/dist/backend/emission/emit-exprs.js.map +1 -1
  4. package/dist/backend/emission/emit-walkers.js +29 -2
  5. package/dist/backend/emission/emit-walkers.js.map +1 -1
  6. package/dist/backend/emission/emitter.js +25 -1
  7. package/dist/backend/emission/emitter.js.map +1 -1
  8. package/dist/backend/llvm/dyn.d.ts +1 -0
  9. package/dist/backend/llvm/dyn.js +83 -1
  10. package/dist/backend/llvm/dyn.js.map +1 -1
  11. package/dist/backend/llvm/emitter.js +210 -13
  12. package/dist/backend/llvm/emitter.js.map +1 -1
  13. package/dist/coverage/surface-manifest.js +16 -2
  14. package/dist/coverage/surface-manifest.js.map +1 -1
  15. package/dist/diagnostics/diagnostic.d.ts +41 -14
  16. package/dist/diagnostics/diagnostic.js +79 -27
  17. package/dist/diagnostics/diagnostic.js.map +1 -1
  18. package/dist/diagnostics/render.js +8 -0
  19. package/dist/diagnostics/render.js.map +1 -1
  20. package/dist/frontend/lowering/lib-boundary.js +11 -1
  21. package/dist/frontend/lowering/lib-boundary.js.map +1 -1
  22. package/dist/frontend/lowering/lower-builtins.d.ts +69 -0
  23. package/dist/frontend/lowering/lower-builtins.js +380 -4
  24. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  25. package/dist/frontend/lowering/lower-calls.d.ts +41 -0
  26. package/dist/frontend/lowering/lower-calls.js +559 -11
  27. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  28. package/dist/frontend/lowering/lower-classes.d.ts +7 -0
  29. package/dist/frontend/lowering/lower-classes.js +51 -1
  30. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  31. package/dist/frontend/lowering/lower-containers.d.ts +21 -0
  32. package/dist/frontend/lowering/lower-containers.js +262 -17
  33. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  34. package/dist/frontend/lowering/lower-dgram.js +92 -17
  35. package/dist/frontend/lowering/lower-dgram.js.map +1 -1
  36. package/dist/frontend/lowering/lower-emitter.d.ts +70 -3
  37. package/dist/frontend/lowering/lower-emitter.js +436 -29
  38. package/dist/frontend/lowering/lower-emitter.js.map +1 -1
  39. package/dist/frontend/lowering/lower-exprs.d.ts +80 -5
  40. package/dist/frontend/lowering/lower-exprs.js +651 -23
  41. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  42. package/dist/frontend/lowering/lower-modules.d.ts +8 -5
  43. package/dist/frontend/lowering/lower-modules.js +144 -19
  44. package/dist/frontend/lowering/lower-modules.js.map +1 -1
  45. package/dist/frontend/lowering/lower-namespaces.d.ts +39 -11
  46. package/dist/frontend/lowering/lower-namespaces.js +166 -24
  47. package/dist/frontend/lowering/lower-namespaces.js.map +1 -1
  48. package/dist/frontend/lowering/lower-server.js +211 -4
  49. package/dist/frontend/lowering/lower-server.js.map +1 -1
  50. package/dist/frontend/lowering/lower-stmts.d.ts +1 -5
  51. package/dist/frontend/lowering/lower-stmts.js +293 -10
  52. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  53. package/dist/frontend/lowering/lower-stream.js +178 -10
  54. package/dist/frontend/lowering/lower-stream.js.map +1 -1
  55. package/dist/frontend/lowering/lowerer.d.ts +53 -1
  56. package/dist/frontend/lowering/lowerer.js +210 -19
  57. package/dist/frontend/lowering/lowerer.js.map +1 -1
  58. package/dist/frontend/lowering/surfaces.d.ts +44 -0
  59. package/dist/frontend/lowering/surfaces.js +234 -3
  60. package/dist/frontend/lowering/surfaces.js.map +1 -1
  61. package/dist/frontend/npm.d.ts +12 -0
  62. package/dist/frontend/npm.js +30 -0
  63. package/dist/frontend/npm.js.map +1 -1
  64. package/dist/frontend/program.d.ts +25 -0
  65. package/dist/frontend/program.js +144 -135
  66. package/dist/frontend/program.js.map +1 -1
  67. package/dist/frontend/shared.d.ts +1 -1
  68. package/dist/frontend/shared.js +1 -1
  69. package/dist/frontend/shared.js.map +1 -1
  70. package/dist/frontend/types.d.ts +13 -0
  71. package/dist/frontend/types.js +84 -7
  72. package/dist/frontend/types.js.map +1 -1
  73. package/dist/index.d.ts +2 -1
  74. package/dist/index.js +157 -12
  75. package/dist/index.js.map +1 -1
  76. package/dist/ir/nodes.d.ts +163 -13
  77. package/dist/ir/nodes.js +65 -6
  78. package/dist/ir/nodes.js.map +1 -1
  79. package/dist/ir/validate.js +64 -5
  80. package/dist/ir/validate.js.map +1 -1
  81. package/dist/library/fence-eval.d.ts +77 -0
  82. package/dist/library/fence-eval.js +395 -0
  83. package/dist/library/fence-eval.js.map +1 -0
  84. package/dist/library/int-infer.d.ts +119 -0
  85. package/dist/library/int-infer.js +1301 -0
  86. package/dist/library/int-infer.js.map +1 -0
  87. package/dist/library/int-infer.test.d.ts +1 -0
  88. package/dist/library/int-infer.test.js +279 -0
  89. package/dist/library/int-infer.test.js.map +1 -0
  90. package/dist/library/profile.d.ts +43 -6
  91. package/dist/library/profile.js +179 -31
  92. package/dist/library/profile.js.map +1 -1
  93. package/dist/library/sidecar.d.ts +23 -0
  94. package/dist/library/sidecar.js +150 -16
  95. package/dist/library/sidecar.js.map +1 -1
  96. package/package.json +2 -2
  97. package/surface-manifest.json +208 -10
@@ -8,9 +8,9 @@ import * as ts from "../ts7/adapter.js";
8
8
  import { dirname } from "node:path";
9
9
  import { BOOL, CAUGHT, DYN, DYN_HANDLE_KINDS, F64, JSVAL, NULL_T, REF_TRUTHY_KINDS, REGEX, RUNTIME_ERROR_CLASSES, SEARCH_PARAMS_T, STRING, UNDEFINED_T, VOID, arrayOf, canAdaptDynFuncTo, canBoxFuncIntoDyn, canDynCheckTo, funcOf, isJsonSafeType, isUnitType, jsOpResultKind, shapeHasAccessorSlots, typeEquals, typeKey, unionFuncSetArmsOk } from "../../ir/nodes.js";
10
10
  import { cjsClassExprWholeExportOf, cjsExportAssignmentOf, cjsExportDiscardReason, isCjsExportTableLiteral, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeEsmFile, locOf } from "../program.js";
11
- import { ARRAY_METHODS, builtinConstLit, builtinFenceHintOf, builtinModuleConstOf, builtinModuleFnOf, ISLAND_SURFACE, isChildSurfaceMember, MAP_METHODS, NARROW_FIRST, SET_METHODS, STR_METHODS, UNSUPPORTED_EXPR, sideEffectFreeOptionValue, stdlibGlobalNameOf } from "./surfaces.js";
11
+ 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";
12
12
  import { blockedBindingUseDiag, recordShapeMismatchDiag, requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
13
- import { PoisonError, dynUndefinedExpr, jsFuncNameOf, neverTaintedJsType, own } from "./lowerer.js";
13
+ import { PoisonError, dynUndefinedExpr, jsFuncNameOf, neverTaintedJsType, nodeThrowExpr, own } from "./lowerer.js";
14
14
  import { lowerIndexMergeHelper, lowerNpmStaticSafeIndexRead, strCharsCall } from "./lower-containers.js";
15
15
  import { npmStaticPackageOfPath } from "../npm-static.js";
16
16
  import { unsupportedModuleFeatureOf } from "../shared.js";
@@ -19,7 +19,7 @@ import { ambientNsRootOf, ambientUndefReadType, ambientUndefVarRootOf, ambientUn
19
19
  import { expandoMemberRead, expandoWritableTarget } from "./lower-expando.js";
20
20
  import { lowerSocketInstanceOf } from "./lower-server.js";
21
21
  import { findGenericMethodOn, lowerStaticFieldRead } from "./lower-classes.js";
22
- import { bindingNeverReassigned, implicitMonoFile, lowerTaggedTemplate, objLitGenericFnInfoOf, objLitGenericFnNodeOf, requireObjLitGenericReceiver } from "./lower-calls.js";
22
+ import { bindingNeverReassigned, implicitMonoFile, lowerTaggedTemplate, nullishGenericBindingUnitOf, objLitGenericFnInfoOf, objLitGenericFnNodeOf, requireObjLitGenericReceiver } from "./lower-calls.js";
23
23
  import { mixinFnOfCallee } from "./lower-mixins.js";
24
24
  import { isConstAssertionTypeNode, isGenericCallableMemberType, underConstAssertion, unitOnlyUnion } from "../types.js";
25
25
  import { lowerYield } from "./lower-generators.js";
@@ -83,7 +83,22 @@ function lowerExprInner(L, expr) {
83
83
  if (chainRecv)
84
84
  return { ...chainRecv, loc };
85
85
  if (ts.isNumericLiteral(expr)) {
86
- return { kind: "numLit", value: Number(expr.text.replace(/_/g, "")), type: F64, loc };
86
+ const value = Number(expr.text.replace(/_/g, ""));
87
+ // Ask 4's representability input: a DECIMAL INTEGER source spelling
88
+ // that does not survive the trip through f64 (parse, format back,
89
+ // compare) rides the literal so the library integer-boundary check
90
+ // can refuse on the author's source text. `expr.text` is the
91
+ // scanner's COOKED value (already the nearest double), so the
92
+ // source spelling comes from the file text; numeric separators are
93
+ // spelling sugar and strip first. Round-tripping literals (every
94
+ // integer within ±(2^53−1)) and non-integer spellings carry
95
+ // nothing, so the IR is unchanged for programs that held their
96
+ // numbers.
97
+ const spelled = expr.getText().replace(/_/g, "");
98
+ if (/^\d+$/.test(spelled) && String(Number(spelled)) !== spelled) {
99
+ return { kind: "numLit", value: Number(spelled), spelling: spelled, type: F64, loc };
100
+ }
101
+ return { kind: "numLit", value, type: F64, loc };
87
102
  }
88
103
  if (expr.kind === ts.SyntaxKind.TrueKeyword) {
89
104
  return { kind: "boolLit", value: true, type: BOOL, loc };
@@ -649,6 +664,11 @@ function lowerExprInner(L, expr) {
649
664
  const c = builtinModuleConstOf(L, bi.module, bi.member);
650
665
  if (c !== undefined)
651
666
  return builtinConstLit(c, loc);
667
+ // module.builtinModules — the baked Node v24 list, a fresh
668
+ // string[] per read.
669
+ if (bi.module === "module" && bi.member === "builtinModules") {
670
+ return builtinModulesArrayLit(loc);
671
+ }
652
672
  // JavaScript sources: a builtin member taken as a bare VALUE is
653
673
  // the same identity-token story as stdlib globals above (the
654
674
  // harness adds worker_threads.Worker to its identity Set).
@@ -796,6 +816,18 @@ function lowerExprInner(L, expr) {
796
816
  return nsUndefRead(L, expr.text, expr, t);
797
817
  }
798
818
  }
819
+ // A read of a TRAP binding — a declaration whose own initializer
820
+ // provably threw (module init unwound there), so this reference can
821
+ // never execute: any lowering is sound, and the trap keeps the
822
+ // shape honest. Typed by the use site when it maps, the F64 dummy
823
+ // otherwise (never observed — never even reached).
824
+ {
825
+ const sym = L.resolveValueSymbol(expr);
826
+ if (sym !== null && L.trapBindings.has(sym)) {
827
+ const t = ambientUndefReadType(L, expr) ?? contextualUndefReadType(L, expr) ?? F64;
828
+ return nsUndefRead(L, expr.text, expr, t);
829
+ }
830
+ }
799
831
  // A program CLASS NAME as a value: the classRef over the class's
800
832
  // immortal class object (member accesses and construction never
801
833
  // reach here — their hooks claim the property/new forms first).
@@ -1165,6 +1197,15 @@ function lowerExprInner(L, expr) {
1165
1197
  }
1166
1198
  return read;
1167
1199
  }
1200
+ // `m.groups` on a match result whose regex is statically known:
1201
+ // the compile-time record projection over the honest slice (or
1202
+ // Node's undefined when the pattern has no named groups) — see
1203
+ // lowerMatchGroupsRead.
1204
+ {
1205
+ const g = lowerMatchGroupsRead(L, expr);
1206
+ if (g !== null)
1207
+ return g;
1208
+ }
1168
1209
  // `m.index` on a for-of-over-matchAll binding reads the companion-
1169
1210
  // index array at this iteration's cursor — always a number: every
1170
1211
  // drained row matched, so the lib's `.index` is never undefined
@@ -1406,12 +1447,25 @@ function lowerExprInner(L, expr) {
1406
1447
  // read (ARR answers length; everything else JS's own-property
1407
1448
  // answer). Mapped receivers keep the fence-first order: their
1408
1449
  // members' gaps are real surface gaps ([1,2].entries), not
1409
- // representation artifacts.
1410
- if (isJsSourceFile(expr.getSourceFile()) &&
1450
+ // representation artifacts. `.length` on a receiver whose checker
1451
+ // ARRAY type lowers to the checked-dynamic representation —
1452
+ // `unknown[]`, the collapsed `(string | object)[]`, and the `any[]`
1453
+ // an Array.isArray guard narrows a collapsed union to — is the DOM
1454
+ // array's OWN length (a keyed read the ARR kind answers exactly),
1455
+ // never an `Array.prototype` surface gap; both source languages.
1456
+ // Prototype-method VALUE reads (`ps.map` unparenthesized) keep the
1457
+ // fence-first order: a stored-member undefined would mis-answer
1458
+ // them, and calls dispatch through the dyn method machinery
1459
+ // instead.
1460
+ if ((isJsSourceFile(expr.getSourceFile()) &&
1411
1461
  (L.mapTypeOf(L.typeOf(expr.expression)) === null ||
1412
1462
  // A never-tainted receiver type maps (never rides as f64) but
1413
1463
  // its VALUE lowered checked-dynamic — same DOM read.
1414
- neverTaintedJsType(L, expr.expression, L.typeOf(expr.expression)))) {
1464
+ neverTaintedJsType(L, expr.expression, L.typeOf(expr.expression)))) ||
1465
+ (expr.name.text === "length" &&
1466
+ (L.checkerAnyArray(expr.expression) ||
1467
+ (L.checker.isArrayType(L.typeOf(expr.expression)) &&
1468
+ L.mapTypeOf(L.typeOf(expr.expression))?.kind === "dyn")))) {
1415
1469
  const recv = L.lowerExpr(expr.expression);
1416
1470
  if (recv.type.kind === "dyn") {
1417
1471
  const key = { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) };
@@ -1479,6 +1533,17 @@ function lowerExprInner(L, expr) {
1479
1533
  return nsUndefRead(L, ambientRoot.text, expr, t);
1480
1534
  }
1481
1535
  }
1536
+ // A member read through a NULLISH generic binding (`const i: I<A &
1537
+ // B> = null as any; const _i: I<A> = i.something` — the receiver
1538
+ // provably holds null/undefined forever): the read throws Node's
1539
+ // exact TypeError at the access.
1540
+ if (ts.isIdentifier(expr.expression) && expr.questionDotToken === undefined) {
1541
+ const unit = nullishGenericBindingUnitOf(L, L.resolveValueSymbol(expr.expression));
1542
+ if (unit !== null) {
1543
+ const t = ambientUndefReadType(L, expr) ?? contextualUndefReadType(L, expr) ?? F64;
1544
+ return nodeThrowExpr(1, "", `Cannot read properties of ${unit} (reading '${expr.name.text}')`, t, loc);
1545
+ }
1546
+ }
1482
1547
  // The lib fence's PROPERTY chokepoint: a stdlib-declared member that
1483
1548
  // no lowering above claimed ([1,2].entries, Math.SQRT2, Promise.all,
1484
1549
  // re.exec as a value, ...) reports SC2020 here.
@@ -1516,6 +1581,31 @@ function lowerExprInner(L, expr) {
1516
1581
  const opt = chainGuardedByQuestionDot(expr.expression);
1517
1582
  return L.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: recvLowered, type: DYN, loc }, expr);
1518
1583
  }
1584
+ // The lowered receiver is a RECORD the checker spelled wider —
1585
+ // `s.match(re).groups.key` in a JS file: the checker says
1586
+ // `{ [key: string]: string } | undefined`, but the groups
1587
+ // projection already answered the record arm (its null trap
1588
+ // included). Declared fields read directly; an index-signature
1589
+ // shape serves undeclared keys through the overflow read, exactly
1590
+ // the dot-access rule on checker-spelled hybrids.
1591
+ if (recvLowered.type.kind === "record") {
1592
+ const recvShape = L.shapes.get(recvLowered.type.shapeId);
1593
+ const field = recvShape?.fields.find((f) => f.name === expr.name.text);
1594
+ if (field) {
1595
+ return L.maybeNarrow({ kind: "recordGet", obj: recvLowered, shapeId: recvLowered.type.shapeId, field: field.name, type: field.type, loc }, expr);
1596
+ }
1597
+ if (recvShape?.indexValue !== undefined && !recvShape.tuple) {
1598
+ return L.maybeNarrow({
1599
+ kind: "recordKeyGet",
1600
+ obj: recvLowered,
1601
+ shapeId: recvLowered.type.shapeId,
1602
+ key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) },
1603
+ overflowOnly: true,
1604
+ type: recvShape.indexValue,
1605
+ loc,
1606
+ }, expr);
1607
+ }
1608
+ }
1519
1609
  // An ABSTRACT property through an abstract-typed receiver: the
1520
1610
  // declaration is erased at runtime — Node defines no field for it,
1521
1611
  // each concrete subclass declares its OWN (at its own layout
@@ -2154,7 +2244,18 @@ function pureCondExpr(e) {
2154
2244
  export function lowerNullishCoalesce(L, expr, loc) {
2155
2245
  const left = L.lowerExpr(expr.left);
2156
2246
  if (left.type.kind === "dyn") {
2157
- L.unsupported("SC1100", expr, "nullish coalescing on 'unknown' values");
2247
+ // `a ?? b` on a CHECKED-DYNAMIC left: the deciding test is the
2248
+ // runtime kind (scr_dyn_is_nullish — UNDEF/NULL take the default;
2249
+ // a wrapped island value routes to the engine's test, defensively:
2250
+ // the wrap constructor scalar-normalizes engine null/undefined
2251
+ // away). Both sides live in the DOM — the right converts through
2252
+ // the usual boundary and evaluates lazily in its branch; a default
2253
+ // with no dyn representation keeps the fence.
2254
+ const right = L.coerceToExpected(L.lowerExpr(expr.right), DYN);
2255
+ if (right.type.kind !== "dyn") {
2256
+ L.unsupported("SC1100", expr, "nullish coalescing on 'unknown' values against defaults with no dynamic representation");
2257
+ }
2258
+ return { kind: "nullish", left, right, type: DYN, loc };
2158
2259
  }
2159
2260
  if (left.type.kind === "jsval") {
2160
2261
  // `a ?? b` on an ISLAND value: the engine's own nullish test — the
@@ -2947,8 +3048,8 @@ export function lowerIntrinsicProperty(L, expr) {
2947
3048
  // No ArrayBuffer VALUE exists — `.buffer` compiles only in the
2948
3049
  // one composed position that consumes it, the DataView
2949
3050
  // constructor's first argument (lowerDataViewNew peels it).
2950
- L.unsupported("SC1090", expr, "'.buffer' outside new DataView(x.buffer, ...) — scriptc typed arrays own their storage: " +
2951
- "no free-standing ArrayBuffer value exists, and slice()/subarray() copy");
3051
+ L.unsupported("SC1090", expr, "'.buffer' outside new DataView(x.buffer, ...) / Buffer.from(x.buffer, ...) " +
3052
+ "(no free-standing ArrayBuffer value exists; subarray() answers an aliasing view directly)");
2952
3053
  }
2953
3054
  if (name === "slice" || name === "subarray" || name === "set" || name === "toString") {
2954
3055
  L.unsupported("SC1090", expr, `typed-array methods as values (call '${name}' directly)`);
@@ -2988,6 +3089,24 @@ export function lowerIntrinsicProperty(L, expr) {
2988
3089
  return read;
2989
3090
  return { kind: "jsExit", value: read, type: F64, loc: locOf(expr) };
2990
3091
  }
3092
+ // A CHECKED-DYNAMIC value behind an array-typed checker spelling
3093
+ // (`Object.keys(u).length` — the dyn walk answers a DOM array while
3094
+ // tsc spells string[]): the runtime-world dispatch — a DOM keyed
3095
+ // read validated into the declared number (a lying length throws
3096
+ // the catchable TypeError, the dynCheck stance).
3097
+ if (receiver.type.kind === "dyn") {
3098
+ const read = {
3099
+ kind: "dynKeyGet",
3100
+ key: { kind: "strLit", value: "length", type: STRING, loc: locOf(expr) },
3101
+ ...(expr.questionDotToken ? { optional: true } : {}),
3102
+ value: receiver,
3103
+ type: DYN,
3104
+ loc: locOf(expr),
3105
+ };
3106
+ if (expr.questionDotToken)
3107
+ return read;
3108
+ return { kind: "dynCheck", value: read, type: F64, loc: locOf(expr) };
3109
+ }
2991
3110
  return kind === "string"
2992
3111
  ? { kind: "strIntrinsic", method: "length", receiver, args: [], type: F64, loc: locOf(expr) }
2993
3112
  : { kind: "arrIntrinsic", method: "length", receiver, args: [], type: F64, loc: locOf(expr) };
@@ -3148,8 +3267,12 @@ export function lowerArrayLiteral(L, expr, expected) {
3148
3267
  // []) builds as a DOM ARRAY — one dyn value whose elements each
3149
3268
  // convert through the usual boundary (dynFrom's JSON-safe domain);
3150
3269
  // an element that cannot convert fences per element. length/index
3151
- // reads and dynamic consumers ride the keyed-DOM paths.
3152
- if (isJsSourceFile(expr.getSourceFile())) {
3270
+ // reads and dynamic consumers ride the keyed-DOM paths. TS literals
3271
+ // take the same build when the slot ITSELF is checked-dynamic — an
3272
+ // `unknown[]` annotation or a collapsed `(string | object)[]` maps
3273
+ // to DYN wholesale now (mapType's dyn-element array rule), so the
3274
+ // literal IS the DOM array.
3275
+ if (isJsSourceFile(expr.getSourceFile()) || mapped?.kind === "dyn") {
3153
3276
  const elems = expr.elements.map((el) => {
3154
3277
  if (ts.isSpreadElement(el)) {
3155
3278
  L.unsupported("SC1090", el, "spread elements in a dynamic (unknown[]) array literal");
@@ -5053,7 +5176,24 @@ export function lowerElementAccess(L, expr) {
5053
5176
  ? null
5054
5177
  : L.mapTypeOf(L.typeOf(expr.expression));
5055
5178
  if (receiverIr?.kind === "jsval") {
5056
- return islandElementRead(L, expr, L.lowerExpr(expr.expression));
5179
+ // Dispatch follows the RUNTIME world (383(d)): a checker-'any'
5180
+ // receiver whose value LOWERED checked-dynamic (`bag.list[0]` where
5181
+ // bag.list is a routed keyed read off a wrapped island value) takes
5182
+ // the DOM keyed read below — the routed JSVAL arm reads the real
5183
+ // engine element; a jsval-lowered receiver keeps the island read.
5184
+ const recv = L.lowerExpr(expr.expression);
5185
+ if (recv.type.kind !== "dyn")
5186
+ return islandElementRead(L, expr, recv);
5187
+ const rawKey = L.lowerExpr(expr.argumentExpression);
5188
+ const key = rawKey.type.kind === "string"
5189
+ ? rawKey
5190
+ : rawKey.type.kind === "f64"
5191
+ ? { kind: "toString", operand: rawKey, type: STRING, loc: rawKey.loc }
5192
+ : null;
5193
+ if (key) {
5194
+ const opt = chainGuardedByQuestionDot(expr.expression);
5195
+ return L.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: recv, type: DYN, loc: locOf(expr) }, expr);
5196
+ }
5057
5197
  }
5058
5198
  // Typed-array element read `b[i]` — like arrayGet with a scalar
5059
5199
  // result; any invalid index traps (the array discipline; JS would
@@ -5131,9 +5271,12 @@ export function lowerElementAccess(L, expr) {
5131
5271
  const obj = L.lowerExpr(expr.expression);
5132
5272
  if (obj.type.kind === "dyn") {
5133
5273
  const rawKey = L.lowerExpr(expr.argumentExpression);
5274
+ // Number, bool, and DYN keys stringify (ToPropertyKey) — the
5275
+ // dyn-keyed read `catchWarning[warning.name]` where the property
5276
+ // chain itself lowered dyn.
5134
5277
  const key = rawKey.type.kind === "string"
5135
5278
  ? rawKey
5136
- : rawKey.type.kind === "f64"
5279
+ : rawKey.type.kind === "f64" || rawKey.type.kind === "bool" || rawKey.type.kind === "dyn"
5137
5280
  ? { kind: "toString", operand: rawKey, type: STRING, loc: rawKey.loc }
5138
5281
  : null;
5139
5282
  if (key) {
@@ -5499,11 +5642,58 @@ export function lowerElementWrite(L, expr) {
5499
5642
  const receiverIr = L.mapTypeOf(L.typeOf(target.expression));
5500
5643
  if (receiverIr?.kind === "jsval") {
5501
5644
  const obj = L.lowerExpr(target.expression);
5645
+ // Dispatch follows the RUNTIME world (383(d)): a checker-'any'
5646
+ // receiver whose value LOWERED checked-dynamic takes the DOM keyed
5647
+ // write — the routed JSVAL arm lands it on the real engine object.
5648
+ if (obj.type.kind === "dyn") {
5649
+ const loc = locOf(expr);
5650
+ const rawKey = L.lowerExpr(target.argumentExpression);
5651
+ const key = rawKey.type.kind === "f64" || rawKey.type.kind === "bool" || rawKey.type.kind === "dyn"
5652
+ ? { kind: "toString", operand: rawKey, type: STRING, loc: rawKey.loc }
5653
+ : rawKey;
5654
+ if (key.type.kind !== "string") {
5655
+ L.unsupported("SC1090", target.argumentExpression, `'${L.fmt(key.type)}'-typed keys in keyed writes through 'unknown' values`);
5656
+ }
5657
+ const value = L.coerceToExpected(L.lowerExpr(expr.right), DYN);
5658
+ if (value.type.kind !== "dyn") {
5659
+ L.unsupported("SC1100", expr.right, `assigning '${L.fmt(value.type)}' values through 'unknown' receivers`);
5660
+ }
5661
+ return {
5662
+ kind: "exprStmt",
5663
+ expr: { kind: "libCall", fn: "dyn.keySet", args: [obj, key, value], type: VOID, loc },
5664
+ loc,
5665
+ };
5666
+ }
5502
5667
  const key = L.jsvalIn(L.lowerExpr(target.argumentExpression), target.argumentExpression);
5503
5668
  const value = L.jsvalIn(L.lowerExpr(expr.right), expr.right);
5504
5669
  const loc = locOf(expr);
5505
5670
  return { kind: "exprStmt", expr: { kind: "jsOp", op: "setIdx", args: [obj, key, value], type: VOID, loc }, loc };
5506
5671
  }
5672
+ // A checked-dynamic receiver TYPE (`unknown[]` and the collapsed
5673
+ // `(string | object)[]` map to DYN wholesale now): the DOM keyed
5674
+ // write — dyn.keySet. Number keys canonicalize through the JS-exact
5675
+ // formatter; ARR receivers take canonical index keys as element
5676
+ // set/extend (undefined-hole padding, JS's length growth exactly),
5677
+ // OBJ receivers set the member, and the runtime throws Node's
5678
+ // TypeErrors on every other kind. Values convert into the DOM;
5679
+ // unconvertible ones fence per site (the dynFrom stance).
5680
+ if (receiverIr?.kind === "dyn") {
5681
+ const obj = L.lowerExpr(target.expression);
5682
+ if (obj.type.kind === "dyn") {
5683
+ const loc = locOf(expr);
5684
+ let key = L.lowerExpr(target.argumentExpression);
5685
+ if (key.type.kind === "f64")
5686
+ key = L.ensureString(key, target.argumentExpression);
5687
+ if (key.type.kind !== "string") {
5688
+ L.unsupported("SC1090", target.argumentExpression, "indexing with non-string or non-number keys");
5689
+ }
5690
+ const value = L.coerceToExpected(L.lowerExpr(expr.right), DYN);
5691
+ if (value.type.kind !== "dyn") {
5692
+ L.unsupported("SC1101", expr.right, `storing '${L.fmt(value.type)}' values in a checked-dynamic array (the value cannot convert into the DOM)`);
5693
+ }
5694
+ return { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [obj, key, value], type: VOID, loc }, loc };
5695
+ }
5696
+ }
5507
5697
  // Typed-array element write `b[i] = v` — bytesSet: the value is an f64
5508
5698
  // the runtime coerces JS-exactly; invalid indices trap (no appends —
5509
5699
  // typed arrays are fixed-length; JS would ignore the write).
@@ -5680,6 +5870,39 @@ export function lowerElementWrite(L, expr) {
5680
5870
  };
5681
5871
  }
5682
5872
  }
5873
+ // Keyed write `d[k] = v` on a CHECKED-DYNAMIC receiver (a JS `any`
5874
+ // dictionary — the Object.create(null) memo-table idiom writes
5875
+ // `result[key] = value`): dyn.keySet, exactly the record-mapped dyn
5876
+ // arm above — later writes win in insertion order, number keys
5877
+ // canonicalize through the JS-exact formatter (ToPropertyKey's string
5878
+ // side), index keys on DOM arrays set/extend elements, and non-object
5879
+ // receivers throw Node's TypeErrors at runtime. An UNMAPPED checker
5880
+ // type (static `any` — mapTypeOf answers null without --dynamic)
5881
+ // probes the receiver's own lowered world: a dyn value takes the same
5882
+ // write, anything else falls through to the fences.
5883
+ if (receiverIr?.kind === "dyn" || receiverIr === null) {
5884
+ const obj = receiverIr !== null ? L.lowerExpr(target.expression) : probeLower(L, target.expression);
5885
+ if (obj !== null && obj.type.kind === "dyn") {
5886
+ const loc = locOf(expr);
5887
+ const litKey = recordKeyLiteralText(target.argumentExpression);
5888
+ let key = litKey !== null
5889
+ ? { kind: "strLit", value: litKey, type: STRING, loc: locOf(target.argumentExpression) }
5890
+ : L.lowerExpr(target.argumentExpression);
5891
+ if (key.type.kind === "f64")
5892
+ key = L.ensureString(key, target.argumentExpression);
5893
+ if (key.type.kind === "bool" || key.type.kind === "dyn") {
5894
+ key = { kind: "toString", operand: key, type: STRING, loc: locOf(target.argumentExpression) };
5895
+ }
5896
+ if (key.type.kind !== "string") {
5897
+ L.unsupported("SC1090", target.argumentExpression, "indexing checked-dynamic values with non-string or non-number keys");
5898
+ }
5899
+ const value = L.coerceToExpected(L.lowerExpr(expr.right), DYN);
5900
+ if (value.type.kind !== "dyn") {
5901
+ L.unsupported("SC1101", expr.right, `storing '${L.fmt(value.type)}' values in a checked-dynamic object (the value cannot convert into the DOM)`);
5902
+ }
5903
+ return { kind: "exprStmt", expr: { kind: "libCall", fn: "dyn.keySet", args: [obj, key, value], type: VOID, loc }, loc };
5904
+ }
5905
+ }
5683
5906
  if (receiverIr?.kind !== "array") {
5684
5907
  L.unsupported("SC1090", target, "assignment to non-array elements");
5685
5908
  }
@@ -8086,11 +8309,14 @@ function lowerRuntimeKeyIn(L, expr, loc) {
8086
8309
  }
8087
8310
  /** A regex literal `/ab+c/gi` → regexLit (interned per (pattern, flags)
8088
8311
  * by the backend). The TS parser has already syntax-checked the literal;
8089
- * what remains here are the slice fences: the flag alphabet (d and v are
8090
- * declared-valid TS flags outside this slice) and named capture groups
8091
- * (their results are unreachable without exec/match). The engine
8092
- * validates the pattern itself lazily at first use (SEMANTICS.md
8093
- * documents the divergence from Node's parse-time SyntaxError). */
8312
+ * what remains here is the flag-alphabet fence (d and v are
8313
+ * declared-valid TS flags outside this slice). Named capture groups
8314
+ * `(?<name>...)` and `\k<name>` backreferences compile — libregexp
8315
+ * executes them natively, replace templates resolve `$<name>` at
8316
+ * runtime, and `.groups` reads desugar at their access sites
8317
+ * (matchResultNamedGroupsOf). The engine validates the pattern itself
8318
+ * lazily at first use (SEMANTICS.md documents the divergence from
8319
+ * Node's parse-time SyntaxError). */
8094
8320
  export function lowerRegexLiteral(L, expr) {
8095
8321
  const text = expr.text;
8096
8322
  const lastSlash = text.lastIndexOf("/");
@@ -8105,12 +8331,414 @@ export function lowerRegexLiteral(L, expr) {
8105
8331
  : `the regex '${f}' flag`);
8106
8332
  }
8107
8333
  }
8108
- // (?<name>...) — named groups; (?<= / (?<! are lookbehinds and fine.
8109
- if (/\(\?<(?![=!])/.test(pattern)) {
8110
- L.unsupported("SC1120", expr, "named capture groups in regex patterns");
8111
- }
8112
8334
  return { kind: "regexLit", pattern, flags, type: REGEX, loc: locOf(expr) };
8113
8335
  }
8336
+ /** The pattern's named capture groups, in source order: each `(?<name>`
8337
+ * with its 1-based capture index (numbered and named groups share the
8338
+ * numbering — the index IS the match slice's element position). Null
8339
+ * when the scan can't answer confidently (an unterminated construct, a
8340
+ * `\u`-escaped group name) — callers fence rather than guess. Duplicate
8341
+ * names (ES2025 — valid across alternatives) appear once per
8342
+ * declaration; consumers pick the participating occurrence. The scan
8343
+ * only needs to track escapes, character classes, and `(` kinds; tsc
8344
+ * has already syntax-checked the literal, so a malformed pattern here
8345
+ * answers null and the engine's lazy compile reports it. */
8346
+ export function namedCaptureGroupsOfPattern(pattern) {
8347
+ const groups = [];
8348
+ let captureIndex = 0;
8349
+ let inClass = false;
8350
+ let i = 0;
8351
+ while (i < pattern.length) {
8352
+ const c = pattern[i];
8353
+ if (c === "\\") {
8354
+ i += 2;
8355
+ continue;
8356
+ }
8357
+ if (inClass) {
8358
+ if (c === "]")
8359
+ inClass = false;
8360
+ i++;
8361
+ continue;
8362
+ }
8363
+ if (c === "[") {
8364
+ inClass = true;
8365
+ i++;
8366
+ continue;
8367
+ }
8368
+ if (c !== "(") {
8369
+ i++;
8370
+ continue;
8371
+ }
8372
+ if (pattern[i + 1] !== "?") {
8373
+ captureIndex++;
8374
+ i++;
8375
+ continue;
8376
+ }
8377
+ // (?<name> captures; (?<= and (?<! are lookbehinds; every other
8378
+ // (?… form — (?:, (?=, (?!, modifier groups (?i: — is non-capturing.
8379
+ if (pattern[i + 2] === "<" && pattern[i + 3] !== "=" && pattern[i + 3] !== "!") {
8380
+ const gt = pattern.indexOf(">", i + 3);
8381
+ if (gt < 0)
8382
+ return null;
8383
+ const name = pattern.slice(i + 3, gt);
8384
+ if (name.includes("\\"))
8385
+ return null; // \u-escaped names: bytecode spells them decoded
8386
+ captureIndex++;
8387
+ groups.push({ name, index: captureIndex });
8388
+ i = gt + 1;
8389
+ continue;
8390
+ }
8391
+ i += 2;
8392
+ }
8393
+ return groups;
8394
+ }
8395
+ /** The statically-known regex PATTERN behind an expression: a regex
8396
+ * literal, a const local initialized with one (the crypto.js
8397
+ * `const regexp = /(?<m>\d+)/` shape), or `new RegExp("...")` over a
8398
+ * string literal (the cooked text IS the pattern). Null when the regex
8399
+ * only exists at runtime — .groups consumers fence there. */
8400
+ export function staticRegexPatternOf(L, e) {
8401
+ let expr = e;
8402
+ for (;;) {
8403
+ if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr)) {
8404
+ expr = expr.expression;
8405
+ continue;
8406
+ }
8407
+ break;
8408
+ }
8409
+ if (ts.isRegularExpressionLiteral(expr)) {
8410
+ const text = expr.text;
8411
+ return text.slice(1, text.lastIndexOf("/"));
8412
+ }
8413
+ if (ts.isNewExpression(expr) &&
8414
+ ts.isIdentifier(expr.expression) &&
8415
+ expr.expression.text === "RegExp" &&
8416
+ L.isStdlibGlobal(expr.expression, "RegExp") &&
8417
+ expr.arguments !== undefined &&
8418
+ expr.arguments.length >= 1 &&
8419
+ (ts.isStringLiteral(expr.arguments[0]) || ts.isNoSubstitutionTemplateLiteral(expr.arguments[0]))) {
8420
+ return expr.arguments[0].text;
8421
+ }
8422
+ if (ts.isIdentifier(expr)) {
8423
+ const init = constInitializerOf(L, expr);
8424
+ if (init !== null)
8425
+ return staticRegexPatternOf(L, init);
8426
+ }
8427
+ return null;
8428
+ }
8429
+ /** The initializer behind a CONST identifier binding (a plain
8430
+ * VariableDeclaration under a const list) — the one-hop provenance step
8431
+ * the regex traces walk. Null for let/var (reassignable), params,
8432
+ * destructured bindings, and unresolvable names. */
8433
+ function constInitializerOf(L, ident) {
8434
+ const sym = L.resolveValueSymbol(ident);
8435
+ const decl = sym ? L.checker.valueDeclarationOf(sym) : undefined;
8436
+ if (decl === undefined || !ts.isVariableDeclaration(decl) || decl.initializer === undefined)
8437
+ return null;
8438
+ if (!ts.isVariableDeclarationList(decl.parent) || (decl.parent.flags & ts.NodeFlags.Const) === 0)
8439
+ return null;
8440
+ return decl.initializer;
8441
+ }
8442
+ /** The named-group table of the regex that PRODUCED a match-result
8443
+ * expression — the `.groups` desugar's provenance question. Traces
8444
+ * (through parens, `!`, and const bindings):
8445
+ * - `re.exec(s)` / `s.match(re)` → re's static pattern
8446
+ * - a matchAll ROW: `rows[i]`, the for-of binding over a direct
8447
+ * `s.matchAll(re)` or a stored const drain → re's static pattern
8448
+ * Answers null when the producing regex isn't statically known (the
8449
+ * caller keeps its fence), or the groups list (possibly empty — a
8450
+ * traced regex WITHOUT named groups is the Node-undefined case). */
8451
+ export function matchResultNamedGroupsOf(L, e) {
8452
+ const reExpr = matchProducerRegexOf(L, e);
8453
+ if (reExpr === null)
8454
+ return null;
8455
+ const pattern = staticRegexPatternOf(L, reExpr);
8456
+ if (pattern === null)
8457
+ return null;
8458
+ return namedCaptureGroupsOfPattern(pattern);
8459
+ }
8460
+ /** The regex EXPRESSION whose match produced `e` (see
8461
+ * matchResultNamedGroupsOf for the traced shapes). */
8462
+ function matchProducerRegexOf(L, e) {
8463
+ let expr = e;
8464
+ for (;;) {
8465
+ if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr)) {
8466
+ expr = expr.expression;
8467
+ continue;
8468
+ }
8469
+ break;
8470
+ }
8471
+ // The direct producers: re.exec(s) / s.match(re) — the receiver's
8472
+ // mapped type pins the STDLIB operation (a regex for exec, a string
8473
+ // or its nullable spelling for match — the claim lower-containers
8474
+ // makes), so a user method that happens to be named `match` with a
8475
+ // regex argument never traces.
8476
+ if (ts.isCallExpression(expr) && ts.isPropertyAccessExpression(expr.expression)) {
8477
+ const name = expr.expression.name.text;
8478
+ const recvT = L.mapTypeOf(L.typeOf(expr.expression.expression));
8479
+ if (name === "exec" && expr.arguments.length === 1 && recvT?.kind === "regex") {
8480
+ return expr.expression.expression;
8481
+ }
8482
+ if (name === "match" && expr.arguments.length === 1 &&
8483
+ (recvT?.kind === "string" || (recvT !== null && recvT !== undefined && nullableStringType(L, recvT))) &&
8484
+ L.mapTypeOf(L.typeOf(expr.arguments[0]))?.kind === "regex") {
8485
+ return expr.arguments[0];
8486
+ }
8487
+ }
8488
+ // A matchAll ROW by element access: rows[i] with rows tracing to the
8489
+ // drain call — or the direct s.matchAll(re)[i] spelling.
8490
+ if (ts.isElementAccessExpression(expr)) {
8491
+ return matchAllRegexOf(L, expr.expression);
8492
+ }
8493
+ if (ts.isIdentifier(expr)) {
8494
+ // The for-of binding over a matchAll drain: `for (const m of
8495
+ // s.matchAll(re))` (or over a stored const rows).
8496
+ const sym = L.resolveValueSymbol(expr);
8497
+ const decl = sym ? L.checker.valueDeclarationOf(sym) : undefined;
8498
+ if (decl !== undefined && ts.isVariableDeclaration(decl) &&
8499
+ ts.isVariableDeclarationList(decl.parent) && ts.isForOfStatement(decl.parent.parent)) {
8500
+ return matchAllRegexOf(L, decl.parent.parent.expression);
8501
+ }
8502
+ // A stored const match result: `const m = re.exec(s)`.
8503
+ const init = constInitializerOf(L, expr);
8504
+ if (init !== null)
8505
+ return matchProducerRegexOf(L, init);
8506
+ }
8507
+ return null;
8508
+ }
8509
+ /** The regex argument of a (possibly const-stored) `s.matchAll(re)`. */
8510
+ function matchAllRegexOf(L, e) {
8511
+ let expr = e;
8512
+ for (;;) {
8513
+ if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr)) {
8514
+ expr = expr.expression;
8515
+ continue;
8516
+ }
8517
+ break;
8518
+ }
8519
+ if (ts.isCallExpression(expr) && ts.isPropertyAccessExpression(expr.expression) &&
8520
+ expr.expression.name.text === "matchAll" && expr.arguments.length === 1 &&
8521
+ L.mapTypeOf(L.typeOf(expr.expression.expression))?.kind === "string" &&
8522
+ L.mapTypeOf(L.typeOf(expr.arguments[0]))?.kind === "regex") {
8523
+ return expr.arguments[0];
8524
+ }
8525
+ // The eager-spread spelling: `[...s.matchAll(re)]`.
8526
+ if (ts.isArrayLiteralExpression(expr) && expr.elements.length === 1) {
8527
+ const only = expr.elements[0];
8528
+ if (ts.isSpreadElement(only))
8529
+ return matchAllRegexOf(L, only.expression);
8530
+ }
8531
+ if (ts.isIdentifier(expr)) {
8532
+ const init = constInitializerOf(L, expr);
8533
+ if (init !== null)
8534
+ return matchAllRegexOf(L, init);
8535
+ }
8536
+ return null;
8537
+ }
8538
+ /** `m.groups` on a match result whose producing regex is statically
8539
+ * known: the honest slice already HOLDS every named group's value at
8540
+ * its capture index, so the groups object is a compile-time record
8541
+ * projection — `{ year: m[1], month: m[2] }` — built by one interned
8542
+ * helper per (shape, index list, receiver type). Node's exact shape
8543
+ * decisions:
8544
+ * - no named groups → `undefined` (the identifier-receiver shapes —
8545
+ * a call receiver would lose its own null trap, so those keep the
8546
+ * member fence);
8547
+ * - a nullable receiver (the JS `s.match(re).groups` idiom) throws
8548
+ * Node's exact TypeError on the unit arms;
8549
+ * - every declared name is a key (nonparticipating groups hold "" —
8550
+ * divergence 51's rule, exactly the slice elements they project);
8551
+ * - ES2025 duplicate names (distinct alternatives) project the first
8552
+ * participating occurrence: at most one participates, and a
8553
+ * participating-empty "" falls through to slots that are "" anyway;
8554
+ * - key order is group declaration order (declaredOrder), Node's own.
8555
+ * The record has no prototype — Node's groups object is null-prototype,
8556
+ * observable only through util.inspect's "[Object: null prototype]"
8557
+ * prefix (the Object.groupBy stance, ledgered). Null when the receiver
8558
+ * isn't a match slice or the regex isn't traceable — the caller's
8559
+ * member fence (with the groups hint) names the gap. */
8560
+ export function lowerMatchGroupsRead(L, expr) {
8561
+ if (expr.name.text !== "groups" || expr.questionDotToken !== undefined)
8562
+ return null;
8563
+ const loc = locOf(expr);
8564
+ const strArr = arrayOf(STRING);
8565
+ const recvT = L.mapTypeOf(L.typeOf(expr.expression));
8566
+ if (!recvT || !isMatchSliceType(L, recvT) || !L.isStdlibMember(expr))
8567
+ return null;
8568
+ const groups = matchResultNamedGroupsOf(L, expr.expression);
8569
+ if (groups === null)
8570
+ return null;
8571
+ if (groups.length === 0) {
8572
+ // Node: a match result of a group-less regex answers undefined —
8573
+ // the unit literal, exactly the `undefined` identifier's lowering.
8574
+ // Identifier receivers only (evaluation-free): a CALL receiver's
8575
+ // exec/match must still run and null-trap, which a folded unit
8576
+ // cannot carry — those keep the fence.
8577
+ let r = expr.expression;
8578
+ while (ts.isParenthesizedExpression(r) || ts.isNonNullExpression(r))
8579
+ r = r.expression;
8580
+ if (ts.isIdentifier(r) && typeEquals(recvT, strArr)) {
8581
+ return { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc };
8582
+ }
8583
+ return null;
8584
+ }
8585
+ const recv = L.lowerExpr(expr.expression);
8586
+ if (!isMatchSliceType(L, recv.type))
8587
+ return null;
8588
+ return lowerGroupsProjection(L, recv, groups, loc);
8589
+ }
8590
+ /** True when the IR type is a string-or-units union — the Dict<string>
8591
+ * member spelling (`process.versions.openssl`), match's nullable
8592
+ * receiver claim. */
8593
+ function nullableStringType(L, t) {
8594
+ if (t.kind !== "union")
8595
+ return false;
8596
+ const arms = L.unions.get(t.unionId)?.arms ?? [];
8597
+ return arms.some((a) => a.kind === "string") && arms.every((a) => a.kind === "string" || isUnitType(a));
8598
+ }
8599
+ /** True when the IR type is the honest match slice — `string[]`, or a
8600
+ * union of it with unit arms only (`string[] | null`). The groups
8601
+ * projection's receiver gate, shared with the destructure interception. */
8602
+ export function isMatchSliceType(L, t) {
8603
+ if (typeEquals(t, arrayOf(STRING)))
8604
+ return true;
8605
+ return t.kind === "union" && matchSliceUnionArms(L, t.unionId) !== null;
8606
+ }
8607
+ /** The union-arm layout of a nullable match slice (`string[] | null`,
8608
+ * matchAll's checker spellings with undefined arms included): the
8609
+ * string[] arm's tag plus each unit arm's tag and spelling. Null when
8610
+ * the union holds anything else. */
8611
+ function matchSliceUnionArms(L, unionId) {
8612
+ const def = L.unions.get(unionId);
8613
+ if (!def)
8614
+ return null;
8615
+ const strArr = arrayOf(STRING);
8616
+ const arrTag = def.arms.findIndex((a) => typeEquals(a, strArr));
8617
+ if (arrTag < 0)
8618
+ return null;
8619
+ const units = [];
8620
+ for (let i = 0; i < def.arms.length; i++) {
8621
+ if (i === arrTag)
8622
+ continue;
8623
+ const a = def.arms[i];
8624
+ if (!isUnitType(a))
8625
+ return null;
8626
+ units.push({ tag: i, unit: a.kind === "nullT" ? "null" : "undefined" });
8627
+ }
8628
+ return { arrTag, units };
8629
+ }
8630
+ /** The groups-record projection call: the result is the CHECKER'S OWN
8631
+ * shape — `{ [key: string]: string }`, the hybrid index-signature
8632
+ * record with no declared fields — holding one OVERFLOW entry per
8633
+ * group name in declaration order, so every downstream consumer
8634
+ * (recordKeyGet reads, Object.keys, JSON, the `| undefined` union
8635
+ * wrap) meets exactly the type tsc reported. One lifted helper per
8636
+ * (index list, receiver representation) builds it; see
8637
+ * lowerMatchGroupsRead for the semantics the body encodes. */
8638
+ export function lowerGroupsProjection(L, recv, groups, loc) {
8639
+ // Name → its capture indices, in declaration order (duplicates keep
8640
+ // every occurrence — the projection picks the participating one).
8641
+ const order = [];
8642
+ const indicesByName = new Map();
8643
+ for (const g of groups) {
8644
+ const got = indicesByName.get(g.name);
8645
+ if (got) {
8646
+ got.push(g.index);
8647
+ }
8648
+ else {
8649
+ indicesByName.set(g.name, [g.index]);
8650
+ order.push(g.name);
8651
+ }
8652
+ }
8653
+ const shapeId = L.shapes.intern([], false, STRING);
8654
+ const recordT = { kind: "record", shapeId };
8655
+ const recvKey = recv.type.kind === "union" ? recv.type.unionId : "arr";
8656
+ const key = `regexgroups:${shapeId}:${groups.map((g) => `${g.name}=${g.index}`).join(",")}:${recvKey}`;
8657
+ let helper = L.widthHelpers.get(key);
8658
+ if (!helper) {
8659
+ helper = `%regex.groups.${L.widthHelpers.size}`;
8660
+ L.widthHelpers.set(key, helper);
8661
+ const body = [];
8662
+ const locals = [{ id: "m.0", name: "m", type: recv.type, mutable: true }];
8663
+ let mRef = { kind: "varRef", localId: "m.0", type: recv.type, loc };
8664
+ if (recv.type.kind === "union") {
8665
+ // Node's exact TypeError per unit arm, then the checked narrow.
8666
+ const arms = matchSliceUnionArms(L, recv.type.unionId);
8667
+ for (const u of arms.units) {
8668
+ body.push({
8669
+ kind: "if",
8670
+ cond: { kind: "unionIsTag", unionId: recv.type.unionId, tag: u.tag, negated: false, value: mRef, type: BOOL, loc },
8671
+ then: [
8672
+ {
8673
+ kind: "throw",
8674
+ value: {
8675
+ kind: "libCall",
8676
+ fn: "error.new",
8677
+ args: [
8678
+ { kind: "strLit", value: `Cannot read properties of ${u.unit} (reading 'groups')`, type: STRING, loc },
8679
+ ],
8680
+ type: { kind: "object", className: "%TypeError" },
8681
+ loc,
8682
+ },
8683
+ loc,
8684
+ },
8685
+ ],
8686
+ else_: null,
8687
+ loc,
8688
+ });
8689
+ }
8690
+ const narrowed = { kind: "unionNarrow", unionId: recv.type.unionId, tag: arms.arrTag, value: mRef, type: arrayOf(STRING), loc };
8691
+ locals.push({ id: "arr.0", name: "arr", type: arrayOf(STRING), mutable: false });
8692
+ body.push({ kind: "varDecl", localId: "arr.0", init: narrowed, loc });
8693
+ mRef = { kind: "varRef", localId: "arr.0", type: arrayOf(STRING), loc };
8694
+ }
8695
+ const num = (value) => ({ kind: "numLit", value, type: F64, loc });
8696
+ const elem = (index) => ({ kind: "arrayGet", arr: mRef, index: num(index), type: STRING, loc });
8697
+ const values = new Map();
8698
+ order.forEach((name, i) => {
8699
+ const idxs = indicesByName.get(name);
8700
+ if (idxs.length === 1) {
8701
+ values.set(name, elem(idxs[0]));
8702
+ return;
8703
+ }
8704
+ // Duplicates: the first non-"" occurrence (at most one
8705
+ // participates; all-"" answers "" like any nonparticipating slot).
8706
+ const id = `g${i}.0`;
8707
+ locals.push({ id, name: `g${i}`, type: STRING, mutable: true });
8708
+ body.push({ kind: "varDecl", localId: id, init: elem(idxs[0]), loc });
8709
+ const gRef = { kind: "varRef", localId: id, type: STRING, loc };
8710
+ for (const idx of idxs.slice(1)) {
8711
+ body.push({
8712
+ kind: "if",
8713
+ cond: { kind: "strEq", negated: false, left: gRef, right: { kind: "strLit", value: "", type: STRING, loc }, type: BOOL, loc },
8714
+ then: [{ kind: "assign", localId: id, value: elem(idx), loc }],
8715
+ else_: null,
8716
+ loc,
8717
+ });
8718
+ }
8719
+ values.set(name, gRef);
8720
+ });
8721
+ body.push({
8722
+ kind: "return",
8723
+ value: {
8724
+ kind: "recordLit",
8725
+ fields: order.map((name) => ({ name, value: values.get(name), overflow: true })),
8726
+ type: recordT,
8727
+ loc,
8728
+ },
8729
+ loc,
8730
+ });
8731
+ L.liftedFns.push({
8732
+ name: helper,
8733
+ params: [{ localId: "m.0", name: "m", type: recv.type }],
8734
+ returnType: recordT,
8735
+ locals,
8736
+ body,
8737
+ loc,
8738
+ });
8739
+ }
8740
+ return { kind: "call", callee: helper, args: [recv], type: recordT, loc };
8741
+ }
8114
8742
  /** Field read `obj.f` on class-instance and record receivers, through the
8115
8743
  * shared FieldTarget union (fieldGet / recordGet). Bound method references
8116
8744
  * on classes are rejected specifically; func-typed record fields are