@scriptc/compiler 0.0.10 → 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 (78) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +54 -9
  2. package/dist/backend/emission/emit-exprs.js +34 -2
  3. package/dist/backend/emission/emit-exprs.js.map +1 -1
  4. package/dist/backend/emission/emitter.js +25 -1
  5. package/dist/backend/emission/emitter.js.map +1 -1
  6. package/dist/backend/llvm/emitter.js +59 -4
  7. package/dist/backend/llvm/emitter.js.map +1 -1
  8. package/dist/coverage/surface-manifest.js +16 -2
  9. package/dist/coverage/surface-manifest.js.map +1 -1
  10. package/dist/diagnostics/diagnostic.d.ts +20 -7
  11. package/dist/diagnostics/diagnostic.js +44 -11
  12. package/dist/diagnostics/diagnostic.js.map +1 -1
  13. package/dist/frontend/lowering/lower-builtins.d.ts +56 -0
  14. package/dist/frontend/lowering/lower-builtins.js +235 -4
  15. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  16. package/dist/frontend/lowering/lower-calls.js +38 -4
  17. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  18. package/dist/frontend/lowering/lower-classes.d.ts +7 -0
  19. package/dist/frontend/lowering/lower-classes.js +51 -1
  20. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  21. package/dist/frontend/lowering/lower-containers.d.ts +4 -0
  22. package/dist/frontend/lowering/lower-containers.js +69 -7
  23. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  24. package/dist/frontend/lowering/lower-emitter.d.ts +70 -3
  25. package/dist/frontend/lowering/lower-emitter.js +436 -29
  26. package/dist/frontend/lowering/lower-emitter.js.map +1 -1
  27. package/dist/frontend/lowering/lower-exprs.d.ts +80 -5
  28. package/dist/frontend/lowering/lower-exprs.js +472 -13
  29. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  30. package/dist/frontend/lowering/lower-modules.d.ts +8 -5
  31. package/dist/frontend/lowering/lower-modules.js +141 -47
  32. package/dist/frontend/lowering/lower-modules.js.map +1 -1
  33. package/dist/frontend/lowering/lower-namespaces.d.ts +14 -0
  34. package/dist/frontend/lowering/lower-namespaces.js +122 -15
  35. package/dist/frontend/lowering/lower-namespaces.js.map +1 -1
  36. package/dist/frontend/lowering/lower-stmts.js +80 -6
  37. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  38. package/dist/frontend/lowering/lower-stream.js +113 -6
  39. package/dist/frontend/lowering/lower-stream.js.map +1 -1
  40. package/dist/frontend/lowering/lowerer.d.ts +17 -0
  41. package/dist/frontend/lowering/lowerer.js +105 -17
  42. package/dist/frontend/lowering/lowerer.js.map +1 -1
  43. package/dist/frontend/lowering/surfaces.d.ts +44 -0
  44. package/dist/frontend/lowering/surfaces.js +234 -3
  45. package/dist/frontend/lowering/surfaces.js.map +1 -1
  46. package/dist/frontend/npm.d.ts +12 -0
  47. package/dist/frontend/npm.js +30 -0
  48. package/dist/frontend/npm.js.map +1 -1
  49. package/dist/frontend/program.d.ts +25 -0
  50. package/dist/frontend/program.js +144 -135
  51. package/dist/frontend/program.js.map +1 -1
  52. package/dist/frontend/shared.d.ts +1 -1
  53. package/dist/frontend/shared.js +1 -1
  54. package/dist/frontend/shared.js.map +1 -1
  55. package/dist/index.d.ts +2 -1
  56. package/dist/index.js +137 -4
  57. package/dist/index.js.map +1 -1
  58. package/dist/ir/nodes.d.ts +64 -8
  59. package/dist/ir/nodes.js +21 -3
  60. package/dist/ir/nodes.js.map +1 -1
  61. package/dist/ir/validate.js +11 -2
  62. package/dist/ir/validate.js.map +1 -1
  63. package/dist/library/fence-eval.js +12 -2
  64. package/dist/library/fence-eval.js.map +1 -1
  65. package/dist/library/int-infer.d.ts +119 -0
  66. package/dist/library/int-infer.js +1301 -0
  67. package/dist/library/int-infer.js.map +1 -0
  68. package/dist/library/int-infer.test.d.ts +1 -0
  69. package/dist/library/int-infer.test.js +279 -0
  70. package/dist/library/int-infer.test.js.map +1 -0
  71. package/dist/library/profile.d.ts +32 -5
  72. package/dist/library/profile.js +77 -13
  73. package/dist/library/profile.js.map +1 -1
  74. package/dist/library/sidecar.d.ts +23 -0
  75. package/dist/library/sidecar.js +150 -16
  76. package/dist/library/sidecar.js.map +1 -1
  77. package/package.json +2 -2
  78. package/surface-manifest.json +208 -10
@@ -8,7 +8,7 @@ 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
13
  import { PoisonError, dynUndefinedExpr, jsFuncNameOf, neverTaintedJsType, nodeThrowExpr, own } from "./lowerer.js";
14
14
  import { lowerIndexMergeHelper, lowerNpmStaticSafeIndexRead, strCharsCall } from "./lower-containers.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).
@@ -1177,6 +1197,15 @@ function lowerExprInner(L, expr) {
1177
1197
  }
1178
1198
  return read;
1179
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
+ }
1180
1209
  // `m.index` on a for-of-over-matchAll binding reads the companion-
1181
1210
  // index array at this iteration's cursor — always a number: every
1182
1211
  // drained row matched, so the lib's `.index` is never undefined
@@ -1552,6 +1581,31 @@ function lowerExprInner(L, expr) {
1552
1581
  const opt = chainGuardedByQuestionDot(expr.expression);
1553
1582
  return L.maybeNarrow({ kind: "dynKeyGet", key, ...(opt ? { optional: true } : {}), value: recvLowered, type: DYN, loc }, expr);
1554
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
+ }
1555
1609
  // An ABSTRACT property through an abstract-typed receiver: the
1556
1610
  // declaration is erased at runtime — Node defines no field for it,
1557
1611
  // each concrete subclass declares its OWN (at its own layout
@@ -2994,8 +3048,8 @@ export function lowerIntrinsicProperty(L, expr) {
2994
3048
  // No ArrayBuffer VALUE exists — `.buffer` compiles only in the
2995
3049
  // one composed position that consumes it, the DataView
2996
3050
  // constructor's first argument (lowerDataViewNew peels it).
2997
- L.unsupported("SC1090", expr, "'.buffer' outside new DataView(x.buffer, ...) — scriptc typed arrays own their storage: " +
2998
- "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)");
2999
3053
  }
3000
3054
  if (name === "slice" || name === "subarray" || name === "set" || name === "toString") {
3001
3055
  L.unsupported("SC1090", expr, `typed-array methods as values (call '${name}' directly)`);
@@ -8255,11 +8309,14 @@ function lowerRuntimeKeyIn(L, expr, loc) {
8255
8309
  }
8256
8310
  /** A regex literal `/ab+c/gi` → regexLit (interned per (pattern, flags)
8257
8311
  * by the backend). The TS parser has already syntax-checked the literal;
8258
- * what remains here are the slice fences: the flag alphabet (d and v are
8259
- * declared-valid TS flags outside this slice) and named capture groups
8260
- * (their results are unreachable without exec/match). The engine
8261
- * validates the pattern itself lazily at first use (SEMANTICS.md
8262
- * 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). */
8263
8320
  export function lowerRegexLiteral(L, expr) {
8264
8321
  const text = expr.text;
8265
8322
  const lastSlash = text.lastIndexOf("/");
@@ -8274,12 +8331,414 @@ export function lowerRegexLiteral(L, expr) {
8274
8331
  : `the regex '${f}' flag`);
8275
8332
  }
8276
8333
  }
8277
- // (?<name>...) — named groups; (?<= / (?<! are lookbehinds and fine.
8278
- if (/\(\?<(?![=!])/.test(pattern)) {
8279
- L.unsupported("SC1120", expr, "named capture groups in regex patterns");
8280
- }
8281
8334
  return { kind: "regexLit", pattern, flags, type: REGEX, loc: locOf(expr) };
8282
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
+ }
8283
8742
  /** Field read `obj.f` on class-instance and record receivers, through the
8284
8743
  * shared FieldTarget union (fieldGet / recordGet). Bound method references
8285
8744
  * on classes are rejected specifically; func-typed record fields are