parseman 0.44.0 → 0.46.0

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 (111) hide show
  1. package/dist/analysis/choice-cost-gate.d.ts +158 -0
  2. package/dist/analysis/choice-cost-gate.d.ts.map +1 -0
  3. package/dist/analysis/choice-cost-render.d.ts +88 -0
  4. package/dist/analysis/choice-cost-render.d.ts.map +1 -0
  5. package/dist/analysis/choice-cost.d.ts +383 -0
  6. package/dist/analysis/choice-cost.d.ts.map +1 -0
  7. package/dist/analysis/corpus.d.ts +73 -0
  8. package/dist/analysis/corpus.d.ts.map +1 -0
  9. package/dist/analysis/diagnose-render.d.ts +65 -0
  10. package/dist/analysis/diagnose-render.d.ts.map +1 -0
  11. package/dist/analysis/diagnose.d.ts +133 -0
  12. package/dist/analysis/diagnose.d.ts.map +1 -0
  13. package/dist/analysis/diagnostics.cjs +11272 -0
  14. package/dist/analysis/diagnostics.cjs.map +7 -0
  15. package/dist/analysis/diagnostics.d.ts +27 -0
  16. package/dist/analysis/diagnostics.d.ts.map +1 -0
  17. package/dist/analysis/diagnostics.js +11249 -0
  18. package/dist/analysis/diagnostics.js.map +7 -0
  19. package/dist/analysis/fix-render.d.ts +43 -0
  20. package/dist/analysis/fix-render.d.ts.map +1 -0
  21. package/dist/analysis/fix.d.ts +172 -0
  22. package/dist/analysis/fix.d.ts.map +1 -0
  23. package/dist/analysis/gating.d.ts +17 -9
  24. package/dist/analysis/gating.d.ts.map +1 -1
  25. package/dist/analysis/grammar.d.ts.map +1 -1
  26. package/dist/analysis/rebuild.d.ts +61 -0
  27. package/dist/analysis/rebuild.d.ts.map +1 -0
  28. package/dist/analysis/terminal.d.ts +142 -0
  29. package/dist/analysis/terminal.d.ts.map +1 -0
  30. package/dist/cli/index.cjs +12507 -0
  31. package/dist/cli/index.cjs.map +7 -0
  32. package/dist/cli/index.d.ts +3 -0
  33. package/dist/cli/index.d.ts.map +1 -0
  34. package/dist/cli/index.js +12484 -0
  35. package/dist/cli/index.js.map +7 -0
  36. package/dist/combinators/choice.d.ts.map +1 -1
  37. package/dist/combinators/dispatch.d.ts +19 -1
  38. package/dist/combinators/dispatch.d.ts.map +1 -1
  39. package/dist/combinators/expect.d.ts.map +1 -1
  40. package/dist/combinators/first-set.d.ts.map +1 -1
  41. package/dist/combinators/node.d.ts +19 -0
  42. package/dist/combinators/node.d.ts.map +1 -1
  43. package/dist/compiler/build-arity.d.ts +28 -0
  44. package/dist/compiler/build-arity.d.ts.map +1 -1
  45. package/dist/compiler/codegen.d.ts +80 -60
  46. package/dist/compiler/codegen.d.ts.map +1 -1
  47. package/dist/compiler/degradation.d.ts +123 -0
  48. package/dist/compiler/degradation.d.ts.map +1 -0
  49. package/dist/compiler/fields.d.ts.map +1 -1
  50. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  51. package/dist/compiler/inline-build.d.ts.map +1 -1
  52. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  53. package/dist/compiler/linker.d.ts +45 -7
  54. package/dist/compiler/linker.d.ts.map +1 -1
  55. package/dist/compiler/module-hoist.d.ts +106 -0
  56. package/dist/compiler/module-hoist.d.ts.map +1 -0
  57. package/dist/coverage.d.ts +13 -0
  58. package/dist/coverage.d.ts.map +1 -1
  59. package/dist/cst/reflection.d.ts.map +1 -1
  60. package/dist/functional/run.d.ts.map +1 -1
  61. package/dist/index.cjs +1846 -610
  62. package/dist/index.cjs.map +4 -4
  63. package/dist/index.d.ts +11 -7
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +1846 -610
  66. package/dist/index.js.map +4 -4
  67. package/dist/language-service/index.cjs +40 -30
  68. package/dist/language-service/index.cjs.map +3 -3
  69. package/dist/language-service/index.js +40 -30
  70. package/dist/language-service/index.js.map +3 -3
  71. package/dist/oracle/digest.d.ts +132 -6
  72. package/dist/oracle/digest.d.ts.map +1 -1
  73. package/dist/oracle/index.cjs +136 -305
  74. package/dist/oracle/index.cjs.map +4 -4
  75. package/dist/oracle/index.d.ts +43 -31
  76. package/dist/oracle/index.d.ts.map +1 -1
  77. package/dist/oracle/index.js +136 -305
  78. package/dist/oracle/index.js.map +4 -4
  79. package/dist/plugin/evaluator.d.ts +3 -0
  80. package/dist/plugin/evaluator.d.ts.map +1 -1
  81. package/dist/plugin/free-identifiers.d.ts +23 -0
  82. package/dist/plugin/free-identifiers.d.ts.map +1 -0
  83. package/dist/plugin/index.cjs +1529 -763
  84. package/dist/plugin/index.cjs.map +4 -4
  85. package/dist/plugin/index.d.ts +18 -0
  86. package/dist/plugin/index.d.ts.map +1 -1
  87. package/dist/plugin/index.js +1529 -763
  88. package/dist/plugin/index.js.map +4 -4
  89. package/dist/plugin/reducer-resolver.d.ts +57 -0
  90. package/dist/plugin/reducer-resolver.d.ts.map +1 -0
  91. package/dist/run/index.cjs +33 -30
  92. package/dist/run/index.cjs.map +2 -2
  93. package/dist/run/index.js +33 -30
  94. package/dist/run/index.js.map +2 -2
  95. package/dist/spec/index.cjs +222 -34
  96. package/dist/spec/index.cjs.map +4 -4
  97. package/dist/spec/index.js +222 -34
  98. package/dist/spec/index.js.map +4 -4
  99. package/dist/spec/model.d.ts.map +1 -1
  100. package/dist/types.d.ts +6 -1
  101. package/dist/types.d.ts.map +1 -1
  102. package/dist/version.d.ts +1 -1
  103. package/package.json +23 -4
  104. package/dist/combinators/guard.d.ts +0 -8
  105. package/dist/combinators/guard.d.ts.map +0 -1
  106. package/dist/cst/offset-model.d.ts +0 -119
  107. package/dist/cst/offset-model.d.ts.map +0 -1
  108. package/dist/oracle/corpus.d.ts +0 -32
  109. package/dist/oracle/corpus.d.ts.map +0 -1
  110. package/dist/oracle/identity.d.ts +0 -117
  111. package/dist/oracle/identity.d.ts.map +0 -1
package/dist/index.cjs CHANGED
@@ -21,12 +21,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  GRAMMAR_COVERAGE_DEFINITIONS: () => GRAMMAR_COVERAGE_DEFINITIONS,
24
- OffsetIndex: () => OffsetIndex,
25
24
  absoluteSpanAt: () => absoluteSpanAt,
26
25
  absoluteSpanCST: () => absoluteSpanCST,
27
26
  absolutize: () => absolutize,
28
27
  absolutizeCST: () => absolutizeCST,
29
28
  alternationGroups: () => alternationGroups,
29
+ analyzeChoiceInventory: () => analyzeChoiceInventory,
30
30
  analyzeDuplication: () => analyzeDuplication,
31
31
  analyzeDuplicationRules: () => analyzeDuplicationRules,
32
32
  analyzeGating: () => analyzeGating,
@@ -35,18 +35,18 @@ __export(index_exports, {
35
35
  annotateSpan: () => annotateSpan,
36
36
  annotateTreeSpans: () => annotateTreeSpans,
37
37
  applyEdit: () => applyEdit,
38
+ armLabel: () => armLabel,
38
39
  attempt: () => attempt,
39
40
  balanced: () => balanced,
40
- blankLinesIn: () => blankLinesIn,
41
41
  buildLineIndex: () => buildLineIndex,
42
- buildOffsetIndex: () => buildOffsetIndex,
43
42
  buildRootTriviaIndex: () => buildRootTriviaIndex,
44
43
  buildTriviaIndex: () => buildTriviaIndex,
44
+ buildWastedWorkBaseline: () => buildWastedWorkBaseline,
45
45
  charClassMembers: () => charClassMembers,
46
+ checkWastedWork: () => checkWastedWork,
46
47
  choice: () => choice,
48
+ choiceSiteKey: () => choiceSiteKey,
47
49
  classifiedTrivia: () => classifiedTrivia,
48
- collectLeafSlots: () => collectLeafSlots,
49
- commentsIn: () => commentsIn,
50
50
  compile: () => compile,
51
51
  compiledGrammarCoverageDefinitions: () => compiledGrammarCoverageDefinitions,
52
52
  completionsAt: () => completionsAt,
@@ -59,30 +59,29 @@ __export(index_exports, {
59
59
  createLineIndex: () => createLineIndex,
60
60
  createVisitor: () => createVisitor,
61
61
  cstBuildHost: () => cstBuildHost,
62
+ diagnoseGrammar: () => diagnoseGrammar,
62
63
  dispatch: () => dispatch,
63
64
  duplicationFindingCount: () => duplicationFindingCount,
64
65
  endsWith: () => endsWith,
66
+ examinedNothing: () => examinedNothing,
65
67
  expect: () => expect,
66
68
  extractCharClasses: () => extractCharClasses,
67
69
  field: () => field,
68
70
  firstSetToString: () => firstSetToString,
69
71
  formatDuplicationFindings: () => formatDuplicationFindings,
70
72
  formatGatingWarnings: () => formatGatingWarnings,
71
- gapIsSignificant: () => gapIsSignificant,
72
- gapText: () => gapText,
73
+ formatGrammarDiagnosis: () => formatGrammarDiagnosis,
74
+ fuseInterpreted: () => fuseInterpreted,
73
75
  gate: () => gate,
74
76
  grammarCoverageDefinitions: () => grammarCoverageDefinitions,
75
- guard: () => guard,
76
- indentMixed: () => indentMixed,
77
- indentWidth: () => indentWidth,
77
+ isInterpretedFuse: () => isInterpretedFuse,
78
78
  isParseError: () => isParseError,
79
79
  keywordAlternationHazards: () => keywordAlternationHazards,
80
80
  keywordRegexShape: () => keywordRegexShape,
81
81
  keywords: () => keywords,
82
82
  label: () => label,
83
83
  leaf: () => leaf,
84
- lineBreaksIn: () => lineBreaksIn,
85
- lineStartWithin: () => lineStartWithin,
84
+ leftFactorPreview: () => leftFactorPreview,
86
85
  literal: () => literal,
87
86
  makeWhen: () => makeWhen,
88
87
  makeWord: () => makeWord,
@@ -101,11 +100,14 @@ __export(index_exports, {
101
100
  parseDoc: () => parseDoc,
102
101
  parser: () => parser,
103
102
  peek: () => peek,
103
+ profileWastedWork: () => profileWastedWork,
104
104
  recordLineRange: () => recordLineRange,
105
105
  ref: () => ref,
106
106
  regex: () => regex,
107
107
  relativize: () => relativize,
108
108
  relativizeCST: () => relativizeCST,
109
+ renderChoiceInventory: () => renderChoiceInventory,
110
+ renderWastedWork: () => renderWastedWork,
109
111
  routed: () => routed,
110
112
  rules: () => rules,
111
113
  run: () => run,
@@ -181,8 +183,9 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set(), resolve) {
181
183
  return d.value.length === 0;
182
184
  case "keywords":
183
185
  return false;
186
+ // The routed token itself is the selector's match, never empty; a fallback can be.
184
187
  case "routed":
185
- return false;
188
+ return d.fallback === void 0 ? false : me(d.fallback);
186
189
  case "regex":
187
190
  try {
188
191
  const m = new RegExp(d.source).exec("");
@@ -1944,8 +1947,11 @@ function derive(c, seen) {
1944
1947
  case "not":
1945
1948
  case "peek":
1946
1949
  return deriveExpected2(def.parser);
1950
+ // A bare routed() can only fail as "there is no routed token here". With a
1951
+ // fallback, THAT is the branch that can fail, so its expectations are the useful
1952
+ // ones — the routed path either succeeds or is not taken.
1947
1953
  case "routed":
1948
- return ["routed()"];
1954
+ return def.fallback === void 0 ? ["routed()"] : deriveExpected2(def.fallback);
1949
1955
  case "lazy": {
1950
1956
  const name = c._ruleName;
1951
1957
  if (seen.has(c)) return name ? [name] : [];
@@ -2135,7 +2141,7 @@ function detectStrategy(parsers) {
2135
2141
  function bareLeadingTermKey(term) {
2136
2142
  const d = term._def;
2137
2143
  if (d.tag === "literal" && !d.caseInsensitive) return `L:${d.value}`;
2138
- if (d.tag === "regex") return `R:${d.source}\0${d.flags}`;
2144
+ if (d.tag === "regex") return `R:${JSON.stringify([d.source, d.flags])}`;
2139
2145
  return null;
2140
2146
  }
2141
2147
  function detectSharedPrefix(parsers) {
@@ -2393,17 +2399,21 @@ function makeWhen(opts = {}) {
2393
2399
  function otherwise(parser2) {
2394
2400
  return { kind: "otherwise", parser: parser2, usesRouted: parserUsesRouted(parser2) };
2395
2401
  }
2396
- function routed() {
2402
+ function routed(fallback) {
2397
2403
  return {
2398
2404
  _tag: "routed",
2399
- _meta: { firstSet: { kind: "any" }, canMatchNewline: false, isTrivia: false },
2400
- _def: { tag: "routed" },
2405
+ _meta: {
2406
+ // The routed token is whatever the selector matched, so the first set stays
2407
+ // `any` even with a fallback — `any` already subsumes the fallback's.
2408
+ firstSet: { kind: "any" },
2409
+ canMatchNewline: fallback?._meta.canMatchNewline ?? false,
2410
+ isTrivia: false
2411
+ },
2412
+ _def: fallback === void 0 ? { tag: "routed" } : { tag: "routed", fallback },
2401
2413
  parse(_input, pos, ctx) {
2402
2414
  const item = ctx._routed;
2403
- if (item === void 0) {
2404
- return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2405
- }
2406
- if (pos !== item.span.start) {
2415
+ if (item === void 0 || pos !== item.span.start) {
2416
+ if (fallback !== void 0) return fallback.parse(_input, pos, ctx);
2407
2417
  return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2408
2418
  }
2409
2419
  if (cstCaptureActive(ctx)) {
@@ -2946,13 +2956,13 @@ function markUnusedValues(root) {
2946
2956
  return;
2947
2957
  }
2948
2958
  };
2949
- const resolveRoot = (c) => {
2950
- const guard2 = /* @__PURE__ */ new Set();
2959
+ const resolveRoot2 = (c) => {
2960
+ const guard = /* @__PURE__ */ new Set();
2951
2961
  let cur = c;
2952
- while (!guard2.has(cur)) {
2962
+ while (!guard.has(cur)) {
2953
2963
  const d = cur._def;
2954
2964
  if (d.tag !== "lazy") return cur;
2955
- guard2.add(cur);
2965
+ guard.add(cur);
2956
2966
  try {
2957
2967
  cur = d.thunk();
2958
2968
  } catch {
@@ -2961,7 +2971,7 @@ function markUnusedValues(root) {
2961
2971
  }
2962
2972
  return cur;
2963
2973
  };
2964
- visit(resolveRoot(root), true);
2974
+ visit(resolveRoot2(root), true);
2965
2975
  }
2966
2976
 
2967
2977
  // src/combinators/grammar.ts
@@ -3121,10 +3131,11 @@ function childrenOf(def) {
3121
3131
  return [def.main, def.skipped];
3122
3132
  case "scanTo":
3123
3133
  return [def.sentinel, ...def.skip];
3134
+ case "routed":
3135
+ return def.fallback ? [def.fallback] : [];
3124
3136
  case "literal":
3125
3137
  case "regex":
3126
3138
  case "keywords":
3127
- case "routed":
3128
3139
  case "guard":
3129
3140
  case "lazy":
3130
3141
  case "unknown":
@@ -3350,54 +3361,272 @@ function peek(combinator) {
3350
3361
  };
3351
3362
  }
3352
3363
 
3364
+ // src/compiler/degradation.ts
3365
+ function formatDegradation(d) {
3366
+ return `[parseman] degraded [${d.code}] ${d.where}: ${d.subject} \u2014 ${d.fellBackTo}; otherwise ${d.otherwise}`;
3367
+ }
3368
+ var DETAIL_CAP = 8;
3369
+ function formatDegradations(list) {
3370
+ const byCode = /* @__PURE__ */ new Map();
3371
+ for (const d of list) {
3372
+ const bucket = byCode.get(d.code);
3373
+ if (bucket) bucket.push(d);
3374
+ else byCode.set(d.code, [d]);
3375
+ }
3376
+ const out = [];
3377
+ for (const [code, items] of byCode) {
3378
+ for (const d of items.slice(0, DETAIL_CAP)) out.push(formatDegradation(d));
3379
+ if (items.length > DETAIL_CAP) {
3380
+ out.push(
3381
+ `[parseman] degraded [${code}] +${items.length - DETAIL_CAP} more site(s) not listed (${items.length} total). Set PARSEMAN_DEGRADATION=error to fail the build on these.`
3382
+ );
3383
+ }
3384
+ }
3385
+ return out;
3386
+ }
3387
+ var _sinks = [];
3388
+ function beginDegradationCapture() {
3389
+ _sinks.push(/* @__PURE__ */ new Map());
3390
+ }
3391
+ function degradationCaptureDepth() {
3392
+ return _sinks.length;
3393
+ }
3394
+ function unwindDegradationCapture(depth) {
3395
+ const found = [];
3396
+ while (_sinks.length > depth) {
3397
+ const sink = _sinks.pop();
3398
+ found.unshift(...sink.values());
3399
+ }
3400
+ return found;
3401
+ }
3402
+ function resolveDegradationLevel(explicit) {
3403
+ if (explicit !== void 0) return explicit;
3404
+ const env = typeof process !== "undefined" ? process.env?.PARSEMAN_DEGRADATION : void 0;
3405
+ if (env === "off" || env === "warn" || env === "error") return env;
3406
+ return "warn";
3407
+ }
3408
+ function degradationKey(d) {
3409
+ return JSON.stringify([d.code, d.where, d.subject]);
3410
+ }
3411
+ function recordDegradation(d) {
3412
+ const level = resolveDegradationLevel();
3413
+ if (level === "off") return;
3414
+ const key = degradationKey(d);
3415
+ const sink = _sinks[_sinks.length - 1];
3416
+ if (sink) {
3417
+ if (!sink.has(key)) sink.set(key, d);
3418
+ return;
3419
+ }
3420
+ if (_immediate.has(key)) return;
3421
+ _immediate.add(key);
3422
+ if (level === "error") throw new Error(`parseman: degraded compilation path
3423
+ ${formatDegradation(d)}`);
3424
+ console.warn(formatDegradation(d));
3425
+ }
3426
+ var _immediate = /* @__PURE__ */ new Set();
3427
+ function beginCompileDegradationDrain() {
3428
+ if (resolveDegradationLevel() === "off" || _sinks.length > 0) return () => {
3429
+ };
3430
+ const depth = degradationCaptureDepth();
3431
+ beginDegradationCapture();
3432
+ return (report) => {
3433
+ const found = unwindDegradationCapture(depth);
3434
+ if (!report || found.length === 0) return;
3435
+ const fresh = found.filter((d) => {
3436
+ const k = degradationKey(d);
3437
+ if (_immediate.has(k)) return false;
3438
+ _immediate.add(k);
3439
+ return true;
3440
+ });
3441
+ if (fresh.length === 0) return;
3442
+ const lines = formatDegradations(fresh);
3443
+ if (resolveDegradationLevel() === "error") {
3444
+ throw new Error(`parseman: ${fresh.length} degraded compilation path(s)
3445
+ ${lines.join("\n")}`);
3446
+ }
3447
+ for (const l of lines) console.warn(l);
3448
+ };
3449
+ }
3450
+
3353
3451
  // src/compiler/build-arity.ts
3354
3452
  function cstOutputHost(build) {
3355
3453
  return build?._parsemanCstOutput === true;
3356
3454
  }
3357
- var PARAM_LIST_RE = (
3358
- // (params) => ... | function name?(params) ... | single-ident arrow `x => ...`
3359
- /^(?:function\b[^(]*\(([^)]*)\)|\(([^)]*)\)\s*=>|([A-Za-z_$][\w$]*)\s*=>)/
3360
- );
3361
- var CONFIRMABLE_PARAM_RE = /^[A-Za-z_$][\w$]*\s*\??\s*(?::[^,=]+)?$/;
3455
+ var SINGLE_IDENT_ARROW_RE = /^[A-Za-z_$][\w$]*\s*=>/;
3456
+ var PARAM_LIST_OPEN_RE = /^(?:function\b[^(]*)?\(/;
3457
+ var NATIVE_CODE_RE = /\{\s*\[native code\]\s*\}/;
3458
+ var PARAM_HEAD_RE = /^[A-Za-z_$][\w$]*\s*\??\s*/;
3362
3459
  function confirmedBuildArity(src) {
3363
3460
  const s = src.trim();
3364
- const m = PARAM_LIST_RE.exec(s);
3365
- if (!m) return null;
3366
- if (m[3] !== void 0) return 1;
3367
- const inner = (m[1] ?? m[2] ?? "").trim();
3368
- if (inner === "") return 0;
3369
- const parts = inner.split(",");
3461
+ if (NATIVE_CODE_RE.test(s)) return null;
3462
+ if (/\barguments\b/.test(s)) return null;
3463
+ if (SINGLE_IDENT_ARROW_RE.test(s)) return 1;
3464
+ const inner = paramListText(s);
3465
+ if (inner === null) return null;
3466
+ if (inner.trim() === "") return 0;
3467
+ const parts = splitTopLevel(inner);
3468
+ if (parts === null) return null;
3370
3469
  for (const part of parts) {
3371
- if (!CONFIRMABLE_PARAM_RE.test(part.trim())) return null;
3470
+ if (!isConfirmableParam(part.trim())) return null;
3372
3471
  }
3373
- if (/\barguments\b/.test(s)) return null;
3374
3472
  return parts.length;
3375
3473
  }
3474
+ function paramListText(s) {
3475
+ const open = PARAM_LIST_OPEN_RE.exec(s);
3476
+ if (!open) return null;
3477
+ const start = open[0].length;
3478
+ const end = matchingParen(s, start);
3479
+ if (end === null) return null;
3480
+ if (!s.startsWith("function") && !/^\s*=>/.test(s.slice(end + 1))) return null;
3481
+ return s.slice(start, end);
3482
+ }
3483
+ function matchingParen(s, start) {
3484
+ let depth = 0;
3485
+ for (let i = start; i < s.length; i++) {
3486
+ const c = s[i];
3487
+ if (c === '"' || c === "'" || c === "`") {
3488
+ const close = skipString(s, i);
3489
+ if (close === null) return null;
3490
+ i = close;
3491
+ continue;
3492
+ }
3493
+ if (c === "(" || c === "[" || c === "{") depth++;
3494
+ else if (c === "]" || c === "}") {
3495
+ if (--depth < 0) return null;
3496
+ } else if (c === ")") {
3497
+ if (depth === 0) return i;
3498
+ depth--;
3499
+ }
3500
+ }
3501
+ return null;
3502
+ }
3503
+ function skipString(s, open) {
3504
+ const quote = s[open];
3505
+ for (let i = open + 1; i < s.length; i++) {
3506
+ if (s[i] === "\\") {
3507
+ i++;
3508
+ continue;
3509
+ }
3510
+ if (s[i] === quote) return i;
3511
+ }
3512
+ return null;
3513
+ }
3514
+ function splitTopLevel(inner) {
3515
+ const parts = [];
3516
+ let depth = 0;
3517
+ let angle = 0;
3518
+ let last = 0;
3519
+ for (let i = 0; i < inner.length; i++) {
3520
+ const c = inner[i];
3521
+ if (c === "=" && inner[i + 1] === ">") {
3522
+ i++;
3523
+ continue;
3524
+ }
3525
+ if (c === '"' || c === "'" || c === "`") {
3526
+ const close = skipString(inner, i);
3527
+ if (close === null) return null;
3528
+ i = close;
3529
+ continue;
3530
+ }
3531
+ if (c === "(" || c === "[" || c === "{") depth++;
3532
+ else if (c === ")" || c === "]" || c === "}") {
3533
+ if (--depth < 0) return null;
3534
+ } else if (c === "<") angle++;
3535
+ else if (c === ">") {
3536
+ if (--angle < 0) return null;
3537
+ } else if (c === "," && depth === 0 && angle === 0) {
3538
+ parts.push(inner.slice(last, i));
3539
+ last = i + 1;
3540
+ }
3541
+ }
3542
+ if (depth !== 0 || angle !== 0) return null;
3543
+ parts.push(inner.slice(last));
3544
+ return parts;
3545
+ }
3546
+ function isConfirmableParam(part) {
3547
+ const head = PARAM_HEAD_RE.exec(part);
3548
+ if (!head) return false;
3549
+ const rest = part.slice(head[0].length);
3550
+ if (rest === "") return true;
3551
+ if (rest[0] !== ":") return false;
3552
+ return !hasDefaultAssign(rest.slice(1));
3553
+ }
3554
+ function hasDefaultAssign(annotation) {
3555
+ for (let i = 0; i < annotation.length; i++) {
3556
+ if (annotation[i] !== "=") continue;
3557
+ if (annotation[i + 1] === ">" || annotation[i + 1] === "=") {
3558
+ i++;
3559
+ continue;
3560
+ }
3561
+ const prev = annotation[i - 1];
3562
+ if (prev === "<" || prev === ">" || prev === "!" || prev === "=") continue;
3563
+ return true;
3564
+ }
3565
+ return false;
3566
+ }
3567
+ function buildAnalysisSrc(def) {
3568
+ return def.buildSigSrc ?? def.buildSrc ?? def.build.toString();
3569
+ }
3570
+ var RESOLVER_REASONS = {
3571
+ "rest-parameter": "its parameter list uses a rest parameter, so the declared arity is unbounded",
3572
+ "arguments": "its body references `arguments`",
3573
+ "reassigned": "the binding it names is reassigned, so the function read here is not decidable",
3574
+ "not-a-function": "the binding it names is not a function declaration",
3575
+ "unresolved-import": "its module could not be resolved or parsed",
3576
+ "not-found": "the name did not resolve to any binding",
3577
+ "computed": "it is a computed expression",
3578
+ "foreign-source": "it is named in a module the resolver was never told about, so its offsets index no scope tree the resolver holds",
3579
+ "ambiguous-source": "two distinct registered modules hold byte-identical text, so that text no longer names the module its offsets index"
3580
+ };
3581
+ function unconfirmableReason(def, src) {
3582
+ const declared = def.buildArityUnresolved;
3583
+ if (declared !== void 0 && RESOLVER_REASONS[declared]) return RESOLVER_REASONS[declared];
3584
+ const s = src.trim();
3585
+ const params = s.slice(0, s.indexOf(")") + 1);
3586
+ if (params.includes("...")) return RESOLVER_REASONS["rest-parameter"];
3587
+ if (/\barguments\b/.test(s)) return RESOLVER_REASONS["arguments"];
3588
+ if (/^[A-Za-z_$][\w$]*(\s*\.\s*[A-Za-z_$][\w$]*)?$/.test(s)) {
3589
+ return "it names a binding this build could not resolve to a function declaration";
3590
+ }
3591
+ return "its parameter list could not be parsed";
3592
+ }
3593
+ function confirmedArityForDef(def) {
3594
+ if (def.buildArity !== void 0) return def.buildArity;
3595
+ const src = buildAnalysisSrc(def);
3596
+ const arity = confirmedBuildArity(src);
3597
+ if (arity !== null) return arity;
3598
+ const subject = `build reducer \`${(def.buildSrc ?? "<runtime fn>").trim().split("\n")[0].slice(0, 60)}\``;
3599
+ recordDegradation({
3600
+ code: "build-arity-unconfirmed",
3601
+ severity: "warn",
3602
+ where: `node("${def.type ?? "<inferred>"}")`,
3603
+ subject,
3604
+ fellBackTo: `could not confirm its declared arity \u2014 ${unconfirmableReason(def, src)} \u2014 so this node captures children, fields and raw children, logs trivia, and clones \`_ctx.state\` on every match`,
3605
+ otherwise: "only the tiers the reducer actually declares would be captured (arity >= 1 children, >= 2 fields, >= 4 raw, >= 5 trivia, >= 6 state). Declare it with `node(..., { buildArity: n })` if it cannot be derived"
3606
+ });
3607
+ return null;
3608
+ }
3376
3609
  function buildReadsChildren(def) {
3377
3610
  if (!def.build) return true;
3378
- const src = def.buildSrc ?? def.build.toString();
3379
- const arity = confirmedBuildArity(src);
3611
+ const arity = confirmedArityForDef(def);
3380
3612
  if (arity === null) return true;
3381
3613
  return arity >= 1;
3382
3614
  }
3383
3615
  function buildReadsRaw(def) {
3384
3616
  if (!def.build) return true;
3385
- const src = def.buildSrc ?? def.build.toString();
3386
- const arity = confirmedBuildArity(src);
3617
+ const arity = confirmedArityForDef(def);
3387
3618
  if (arity === null) return true;
3388
3619
  return arity >= 4;
3389
3620
  }
3390
3621
  function buildReadsTrivia(def) {
3391
3622
  if (!def.build) return true;
3392
- const src = def.buildSrc ?? def.build.toString();
3393
- const arity = confirmedBuildArity(src);
3623
+ const arity = confirmedArityForDef(def);
3394
3624
  if (arity === null) return true;
3395
3625
  return arity >= 5;
3396
3626
  }
3397
3627
  function buildReadsState(def) {
3398
3628
  if (!def.build) return true;
3399
- const src = def.buildSrc ?? def.build.toString();
3400
- const arity = confirmedBuildArity(src);
3629
+ const arity = confirmedArityForDef(def);
3401
3630
  if (arity === null) return true;
3402
3631
  return arity >= 6;
3403
3632
  }
@@ -3405,8 +3634,7 @@ function buildReadsState(def) {
3405
3634
  // src/compiler/fields.ts
3406
3635
  function buildReadsFields(def) {
3407
3636
  if (!def.build) return true;
3408
- const src = def.buildSrc ?? def.build.toString();
3409
- const arity = confirmedBuildArity(src);
3637
+ const arity = confirmedArityForDef(def);
3410
3638
  if (arity === null) return true;
3411
3639
  return arity >= 2;
3412
3640
  }
@@ -3587,6 +3815,13 @@ function isCstChild(value) {
3587
3815
  function missingInferredType() {
3588
3816
  throw new Error('node(): inferred node type requires a rules() key; pass node("Type", parser) outside rules()');
3589
3817
  }
3818
+ function normalizeBuildArity(arity) {
3819
+ if (arity === void 0) return void 0;
3820
+ if (!Number.isInteger(arity) || arity < 0 || arity > 6) {
3821
+ throw new Error("node() buildArity must be an integer 0..6 (children, fields, span, rawChildren, triviaLog, state)");
3822
+ }
3823
+ return arity;
3824
+ }
3590
3825
  function normalizeProject(project) {
3591
3826
  if (project === void 0) return void 0;
3592
3827
  if (!Number.isInteger(project) || project < 0) {
@@ -3622,13 +3857,14 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
3622
3857
  const captureTrivia = opts?.captureTrivia === true;
3623
3858
  const trailingTrivia = opts?.trailingTrivia === true;
3624
3859
  const tags = opts?.tags;
3860
+ const buildArity = normalizeBuildArity(opts?.buildArity);
3625
3861
  if (unwrap && collapse) {
3626
3862
  throw new Error("node() options cannot set both unwrap and collapse");
3627
3863
  }
3628
3864
  if (project !== void 0 && (unwrap || collapse)) {
3629
3865
  throw new Error("node() options cannot combine project with unwrap or collapse");
3630
3866
  }
3631
- const def = unwrap || collapse || project !== void 0 || captureTrivia || trailingTrivia || tags !== void 0 && tags.length > 0 ? { ...baseDef, ...unwrap ? { unwrap: true } : {}, ...collapse ? { collapse: true } : {}, ...project !== void 0 ? { project } : {}, ...captureTrivia ? { captureTrivia: true } : {}, ...trailingTrivia ? { trailingTrivia: true } : {}, ...tags !== void 0 && tags.length > 0 ? { tags } : {} } : baseDef;
3867
+ const def = unwrap || collapse || project !== void 0 || captureTrivia || trailingTrivia || buildArity !== void 0 || tags !== void 0 && tags.length > 0 ? { ...baseDef, ...unwrap ? { unwrap: true } : {}, ...collapse ? { collapse: true } : {}, ...project !== void 0 ? { project } : {}, ...captureTrivia ? { captureTrivia: true } : {}, ...trailingTrivia ? { trailingTrivia: true } : {}, ...buildArity !== void 0 ? { buildArity } : {}, ...tags !== void 0 && tags.length > 0 ? { tags } : {} } : baseDef;
3632
3868
  const capturesTrivia = captureTrivia || trailingTrivia || (build ? buildReadsTrivia(def) : project === void 0);
3633
3869
  const clonesState = build ? buildReadsState(def) : project === void 0;
3634
3870
  const hasOwnFields = parserHasOwnFields(combinator);
@@ -3674,7 +3910,7 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
3674
3910
  const cstOutput = cstOutputHost(ctx.build);
3675
3911
  const span = ctx.trackLines ? annotateSpanFromLineContext(r.span, ctx) : r.span;
3676
3912
  const hostState = clonesState ? st : ctx.state !== void 0 ? Object.assign({}, ctx.state) : void 0;
3677
- const built = unwrap && children2.length === 1 ? unwrapChild(children2[0]) : collapse && children2.length === 1 ? children2[0] : !build && project === void 0 && ctx.build?._parsemanCstCollapse && children2.length === 1 && rawChildren.length === 1 && ctx.build._parsemanCstCollapse(nodeType, children2[0], children2, rawChildren) ? children2[0] : project !== void 0 ? cstOutput && ctx.build ? ctx.build(nodeType, children2, fields, span, rawChildren, triviaLog, hostState, tags) : projectChild(children2, project, nodeType) : build ? cstOutput && ctx.build ? ctx.build(nodeType, children2, fields, span, rawChildren, triviaLog, hostState, tags) : build(children2, fields, span, rawChildren, triviaLog, st) : ctx.build ? ctx.build(nodeType, children2, fields, span, rawChildren, triviaLog, st, tags) : { _tag: "node", type: nodeType, span: ctx.trackLines ? { ...span } : { start: r.span.start, end: r.span.end }, state: st ?? null, children: children2 };
3913
+ const built = unwrap && children2.length === 1 ? unwrapChild(children2[0]) : collapse && children2.length === 1 ? children2[0] : (cstOutput || !build && project === void 0) && ctx.build?._parsemanCstCollapse && children2.length === 1 && rawChildren.length === 1 && ctx.build._parsemanCstCollapse(nodeType, children2[0], children2, rawChildren) ? children2[0] : project !== void 0 ? cstOutput && ctx.build ? ctx.build(nodeType, children2, fields, span, rawChildren, triviaLog, hostState, tags) : projectChild(children2, project, nodeType) : build ? cstOutput && ctx.build ? ctx.build(nodeType, children2, fields, span, rawChildren, triviaLog, hostState, tags) : build(children2, fields, span, rawChildren, triviaLog, st) : ctx.build ? ctx.build(nodeType, children2, fields, span, rawChildren, triviaLog, st, tags) : { _tag: "node", type: nodeType, span: ctx.trackLines ? { ...span } : { start: r.span.start, end: r.span.end }, state: st ?? null, children: children2 };
3678
3914
  const rawEntry = isCstChild(built) ? built : { _tag: "leaf", value: typeof built === "string" ? built : typeof built === "object" && built !== null ? input.slice(r.span.start, r.span.end) : "", span };
3679
3915
  if (saved.buf !== void 0 || saved.ch !== void 0) {
3680
3916
  pushCstChild(ctx, built, rawEntry);
@@ -3869,7 +4105,7 @@ function leaf(root, fn) {
3869
4105
  }
3870
4106
 
3871
4107
  // src/version.ts
3872
- var PARSEMAN_VERSION = "0.44.0";
4108
+ var PARSEMAN_VERSION = "0.46.0";
3873
4109
 
3874
4110
  // src/cst/host-mode.ts
3875
4111
  var FUSED_HOST_MODE = /* @__PURE__ */ Symbol.for("parseman.fusedHostMode");
@@ -3924,14 +4160,28 @@ function firstSetToString(fs) {
3924
4160
  return fs.ranges.map((r) => r.lo === r.hi ? ch(r.lo) : `${ch(r.lo)}-${ch(r.hi)}`).join(",");
3925
4161
  }
3926
4162
  var ruleNameOf2 = (p) => p._ruleName;
4163
+ var KEYWORD_REGEX_RE = /^\^?[@#.-]?[A-Za-z][\w-]*(\(\?![^)]*\))?\$?$/;
3927
4164
  var SUGGESTIONS = {
3928
- "broad-recognizer": "if this arm leads with a keyword regex, use word('kw', boundary) / keywords([...]) \u2014 they expose an EXACT, gating first-set and lower to the same charCodeAt scan. A genuine scanTo fallback is fine; accept it in the gating snapshot if intentional.",
3929
- "leading-not": "let the arm lead with its actual consuming terminal (first-sets gate it automatically); keep not(...) only as a TRAILING boundary. Do not hand-roll not(not(...)) to fake gating \u2014 use peek(X), which is zero-width AND carries X first-set.",
3930
- "nullable-prefix": "a leading optional/many lets a later, broad term start the arm. Split the empty case into its own arm, or gate on the prefix first char. A plain sepBy(item, sep) is ALSO nullable \u2014 pass { min: 1 } for a list that cannot be empty.",
3931
- "cross-artifact-ref": "parseman >=0.32.0 resolves a g.Foo ref first-set at fuse time; if still ANY the target rule is itself ungated \u2014 analyze it and give it a concrete non-nullable lead.",
3932
- "opaque-wrapper": "this wrapper (guard/withCtx/recover) contributes no first char; put a concrete leading terminal before it.",
3933
- "ref-cycle": "a recursive ref resolved to ANY; ensure the recursion has a concrete terminal lead on the base case."
4165
+ "broad-recognizer": "This arm can begin with any character, so no single-character test can rule it out. The parser has to enter it \u2014 set up, try, and undo \u2014 at every position it reaches, instead of skipping it for free.\nTo fix: make the arm begin with a fixed character, word or keyword. If it is meant to be a catch-all that matches anything, leave it and add the choice to the accept list at the end.",
4166
+ "leading-not": "This arm begins with a not(...) check, which matches nothing and so says nothing about which character the arm starts with. The parser then cannot rule the arm out and enters it everywhere.\nTo fix: put the term that actually consumes text first and keep not(...) after it as a trailing boundary. If you need to REQUIRE something ahead without consuming it, use peek(X) \u2014 it checks the same thing but still tells parseman that the arm starts with X.",
4167
+ "nullable-prefix": "This arm begins with something optional \u2014 an optional(...) or a repeat that allows zero items \u2014 so the arm can also start with whatever comes AFTER it, and that is broad enough that no character rules the arm out.\nTo fix: give the optional part its own arm so each arm has a definite beginning, or require at least one item. A plain sepBy(item, sep) also matches nothing at all; pass { min: 1 } when the list must not be empty.",
4168
+ "cross-artifact-ref": "This arm hands off to another rule, and that rule has the same problem \u2014 it can begin with any character \u2014 so the cost is inherited rather than caused here.\nTo fix: run this check on the rule it refers to and fix it there. One rule given a definite beginning fixes every choice that uses it.",
4169
+ "opaque-wrapper": "This arm begins with a wrapper that only inspects parser state (a gate/withCtx/recover) and consumes no text, so it says nothing about which character the arm starts with.\nTo fix: put a term that actually matches text in front of the wrapper.",
4170
+ "ref-cycle": "This arm refers back to itself through a cycle of rules, and parseman could not work out a definite first character for any of them.\nTo fix: make sure the base case of the recursion begins with a fixed character or keyword, so the cycle has somewhere definite to start."
3934
4171
  };
4172
+ function refineSuggestion(cause, arm) {
4173
+ if (cause !== "broad-recognizer") return SUGGESTIONS[cause];
4174
+ const lead = peelToLeading(arm);
4175
+ const d = lead._def;
4176
+ if (d.tag === "regex" && KEYWORD_REGEX_RE.test(d.source)) {
4177
+ return `This arm matches the fixed word \`${d.source}\` with a regular expression, and parseman cannot always tell from a regular expression which character it starts with.
4178
+ To fix: write it as word('\u2026') or keywords([\u2026]) instead. They match exactly the same text and compile to the same character scan, but they also tell parseman the first character, which is what lets the parser skip this arm when it cannot match.`;
4179
+ }
4180
+ if (d.tag === "scanTo") {
4181
+ return "This arm ends in a scanTo(...) catch-all, which reads forward until it finds something and so can begin at any character. The parser can never rule it out. That is usually exactly what you want from a fallback.\nTo fix: usually nothing. Add this choice to the accept list at the end so the check keeps flagging the choices that are real problems.";
4182
+ }
4183
+ return SUGGESTIONS[cause];
4184
+ }
3935
4185
  function classifyBroadArm(arm, resolve) {
3936
4186
  const seen = /* @__PURE__ */ new Set();
3937
4187
  const relabel = (inner, cause, detail) => ({ cause, detail, ...inner?.unresolvedExternal ? { unresolvedExternal: true } : {} });
@@ -4047,7 +4297,6 @@ function peelToLeading(arm) {
4047
4297
  if (d.tag === "sequence" && d.parsers.length >= 1) return d.parsers[0];
4048
4298
  return cur;
4049
4299
  }
4050
- var KEYWORD_REGEX_RE = /^\^?[@#.-]?[A-Za-z][\w-]*(\(\?![^)]*\))?\$?$/;
4051
4300
  function detectAntiPatterns(rule, arms) {
4052
4301
  const out = [];
4053
4302
  arms.forEach((arm, i) => {
@@ -4060,14 +4309,14 @@ function detectAntiPatterns(rule, arms) {
4060
4309
  kind: "double-not",
4061
4310
  rule,
4062
4311
  armIndex: i,
4063
- message: "not(not(...)) hand-rolls automatic first-char gating and MISCOMPILES among shared-first-char sibling arms (its first-set is ANY, poisoning dispatch). Prefer letting the arm lead with its consuming terminal; where a real positive lookahead IS needed, use peek(X) \u2014 zero-width like not(not(X)) but it CARRIES X's first-set, so the arm still dispatches."
4312
+ message: 'not(not(X)) is a hand-written way of saying "X must come next, but do not consume it". parseman does that for you, and the hand-written form is worse than nothing here: because not(...) reveals no first character, the arm cannot be skipped, and among sibling arms that share a first character it can select the wrong one.\nTo fix: write peek(X). It checks the same thing without consuming, and it also tells parseman that the arm starts with whatever X starts with.'
4064
4313
  });
4065
4314
  } else {
4066
4315
  out.push({
4067
4316
  kind: "leading-not",
4068
4317
  rule,
4069
4318
  armIndex: i,
4070
- message: "a leading not(...) on a choice arm hand-rolls gating and poisons the choice first-set (not() is ANY). Reorder so a consuming terminal leads; keep not(...) as a TRAILING boundary only."
4319
+ message: "This arm begins with not(...), which matches no text and so reveals no first character. The parser therefore cannot skip the arm and enters it at every position.\nTo fix: put the term that actually consumes text first, and keep not(...) after it as a trailing boundary check."
4071
4320
  });
4072
4321
  }
4073
4322
  }
@@ -4076,7 +4325,7 @@ function detectAntiPatterns(rule, arms) {
4076
4325
  kind: "keyword-regex",
4077
4326
  rule,
4078
4327
  armIndex: i,
4079
- message: `regex(/${ld.source}/) leads this arm but is a keyword \u2014 use word('\u2026', boundary) / keywords([\u2026]) for an EXACT resolvable first-set (they lower to the same charCodeAt scan).`
4328
+ message: `This arm starts with the fixed word \`${ld.source}\` written as a regular expression. Writing it as word('\u2026') or keywords([\u2026]) matches exactly the same text and compiles to the same character scan, but also tells parseman the first character, which is what lets the parser skip the arm when it cannot match.`
4080
4329
  });
4081
4330
  }
4082
4331
  });
@@ -4118,7 +4367,7 @@ function analyzeGatingRules(ruleMap, opts) {
4118
4367
  const d = def;
4119
4368
  const rule = ruleNameOf2(p) ?? enclosingRule;
4120
4369
  if (d.tag === "choice") {
4121
- raw.push({ g: analyzeChoice(p, d, rule, opts?.resolveRef), rule });
4370
+ raw.push({ g: analyzeChoice(p, d, rule, opts?.resolveRef), rule, arms: d.parsers });
4122
4371
  antiPatterns.push(...detectAntiPatterns(rule, d.parsers));
4123
4372
  }
4124
4373
  const rec = d;
@@ -4147,13 +4396,15 @@ function analyzeGatingRules(ruleMap, opts) {
4147
4396
  const seenInRule = /* @__PURE__ */ new Map();
4148
4397
  const accept = new Set(opts?.accept ?? []);
4149
4398
  const usedAccept = /* @__PURE__ */ new Set();
4150
- const choices = raw.map(({ g, rule }) => {
4399
+ const choices = raw.map(({ g, rule, arms }) => {
4151
4400
  const n = seenInRule.get(rule) ?? 0;
4152
4401
  seenInRule.set(rule, n + 1);
4153
4402
  const id = (perRule.get(rule) ?? 1) === 1 ? rule : `${rule}#${n}`;
4154
4403
  const isAccepted = g.gates === "no" && accept.has(id);
4155
4404
  if (isAccepted) usedAccept.add(id);
4156
- return { ...g, id, accepted: isAccepted };
4405
+ const out = { ...g, id, accepted: isAccepted };
4406
+ Object.defineProperty(out, "arms", { value: arms, enumerable: false, writable: false });
4407
+ return out;
4157
4408
  });
4158
4409
  const gated = choices.filter((c) => c.gates === "yes").length;
4159
4410
  const recoverable = choices.filter((c) => c.gates === "recoverable").length;
@@ -4180,7 +4431,14 @@ function analyzeChoice(p, d, rule, resolve) {
4180
4431
  const deep = arms.map((a) => firstSetOf(a, /* @__PURE__ */ new Set(), resolve));
4181
4432
  const anyArms = deep.map((fs, index) => ({ fs, index })).filter((x) => isAny(x.fs)).map(({ index }) => {
4182
4433
  const { cause, detail, unresolvedExternal } = classifyBroadArm(arms[index], resolve);
4183
- return { index, cause, detail, shallowAnyOnly: false, unresolvedExternal: unresolvedExternal === true, suggestion: SUGGESTIONS[cause] };
4434
+ return {
4435
+ index,
4436
+ cause,
4437
+ detail,
4438
+ shallowAnyOnly: false,
4439
+ unresolvedExternal: unresolvedExternal === true,
4440
+ suggestion: refineSuggestion(cause, arms[index])
4441
+ };
4184
4442
  });
4185
4443
  const overlaps = [];
4186
4444
  for (let i = 0; i < deep.length; i++)
@@ -4190,7 +4448,7 @@ function analyzeChoice(p, d, rule, resolve) {
4190
4448
  a: i,
4191
4449
  b: j,
4192
4450
  on: intersection(deep[i], deep[j]),
4193
- suggestion: "arms share a first char \u2014 left-factor. parseman auto-detects the sharedPrefix strategy for bare sequences (choice.ts); make the arms bare sequences with a common leading terminal, or restructure."
4451
+ suggestion: "Two arms of this choice can begin with the same character, so the parser cannot tell from that character which one to try. It tries them in order and undoes the ones that do not match.\nTo fix: pull the shared beginning out in front of the choice so it is matched once \u2014 sequence(shared, choice(rest\u2026)) \u2014 instead of repeating it inside each arm. parseman recognises that shape automatically and turns it back into a single test."
4194
4452
  });
4195
4453
  let gates;
4196
4454
  if (d.disjoint) gates = "yes";
@@ -4744,7 +5002,7 @@ function findDivergentSlot(members, shapeOf) {
4744
5002
  const path = [];
4745
5003
  const defs = [];
4746
5004
  let cur = [...members];
4747
- for (let guard2 = 0; guard2 < 64; guard2++) {
5005
+ for (let guard = 0; guard < 64; guard++) {
4748
5006
  const d0 = cur[0]._def;
4749
5007
  const kidLists = cur.map((c) => childrenOf2(c._def));
4750
5008
  const arity = kidLists[0].length;
@@ -5703,7 +5961,15 @@ function children(def, winners) {
5703
5961
  let resolved;
5704
5962
  try {
5705
5963
  resolved = def.thunk();
5706
- } catch {
5964
+ } catch (e) {
5965
+ recordDegradation({
5966
+ code: "coverage-definitions-unavailable",
5967
+ severity: "warn",
5968
+ where: "<lazy rule reference>",
5969
+ subject: `unresolvable reference (${e.message})`,
5970
+ fellBackTo: "the coverage walk stopped at this reference, so every definition below it is ABSENT from the denominator \u2014 which raises the reported ratio rather than lowering it",
5971
+ otherwise: "the referenced rule and its whole subtree would contribute definitions"
5972
+ });
5707
5973
  return [];
5708
5974
  }
5709
5975
  const name = resolved._ruleName;
@@ -5994,15 +6260,15 @@ function parseStringShape(source) {
5994
6260
  if (source.length < 3 || source[source.length - 1] !== q) return null;
5995
6261
  const inner = source.slice(1, -1);
5996
6262
  const CLS = String.raw`\[\^((?:\\.|[^\]])+)\]`;
5997
- const ESC = String.raw`\\\\(\.|\[\\s\\S\]|\[\\S\\s\])`;
6263
+ const ESC2 = String.raw`\\\\(\.|\[\\s\\S\]|\[\\S\\s\])`;
5998
6264
  let body;
5999
6265
  let esc;
6000
- let m = new RegExp(`^\\((?:\\?:)?${CLS}\\|${ESC}\\)\\*$`).exec(inner);
6266
+ let m = new RegExp(`^\\((?:\\?:)?${CLS}\\|${ESC2}\\)\\*$`).exec(inner);
6001
6267
  if (m) {
6002
6268
  body = m[1];
6003
6269
  esc = m[2];
6004
6270
  } else {
6005
- m = new RegExp(`^\\((?:\\?:)?${ESC}\\|${CLS}\\)\\*$`).exec(inner);
6271
+ m = new RegExp(`^\\((?:\\?:)?${ESC2}\\|${CLS}\\)\\*$`).exec(inner);
6006
6272
  if (m) {
6007
6273
  esc = m[1];
6008
6274
  body = m[2];
@@ -6012,8 +6278,8 @@ function parseStringShape(source) {
6012
6278
  const excluded = parseClassRanges(body);
6013
6279
  if (!excluded) return null;
6014
6280
  const qcp = q.codePointAt(0);
6015
- const inSet = (cp) => excluded.some(([lo, hi]) => cp >= lo && cp <= hi);
6016
- if (!inSet(qcp) || !inSet(BACKSLASH)) return null;
6281
+ const inSet2 = (cp) => excluded.some(([lo, hi]) => cp >= lo && cp <= hi);
6282
+ if (!inSet2(qcp) || !inSet2(BACKSLASH)) return null;
6017
6283
  return { kind: "string", quote: qcp, excluded, escLineTerm: esc !== "." };
6018
6284
  }
6019
6285
  function parseSeqParts(source) {
@@ -7204,27 +7470,53 @@ function emitScannableTerminal(shape, c) {
7204
7470
  }
7205
7471
 
7206
7472
  // src/compiler/inline-build.ts
7207
- var MK_BUILD_RE = /^\(\s*(\w+)\s*,\s*\w+\s*,\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*\)\s*=>\s*mk\s*\(\s*(['"])([^'"]+)\5\s*,\s*\1\s*,\s*\3\s*,\s*\2\s*,\s*\4\s*\)\s*$/;
7473
+ var ANN = String.raw`\s*\??\s*(?::[^,)=]+)?`;
7474
+ var ANY_P = String.raw`\w+${ANN}`;
7475
+ var MK_BUILD_RE = new RegExp(
7476
+ String.raw`^\(\s*(\w+)${ANN}\s*,\s*${ANY_P}\s*,\s*(\w+)${ANN}\s*,\s*(\w+)${ANN}\s*,\s*(\w+)${ANN}\s*\)\s*=>\s*mk\s*\(\s*(['"])([^'"]+)\5\s*,\s*\1\s*,\s*\3\s*,\s*\2\s*,\s*\4\s*\)\s*$`
7477
+ );
7208
7478
  function escapeRegExp(s) {
7209
7479
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7210
7480
  }
7481
+ function identMkBuildRe(typeIdent) {
7482
+ return new RegExp(
7483
+ String.raw`^\(\s*(\w+)${ANN}\s*,\s*${ANY_P}\s*,\s*(\w+)${ANN}\s*,\s*(\w+)${ANN}\s*,\s*(\w+)${ANN}\s*\)\s*=>\s*(?:[\w$.]*\.)?mk\s*\(\s*${escapeRegExp(typeIdent)}\s*,\s*\1\s*,\s*\3\s*,\s*\2\s*,\s*\4\s*\)\s*$`
7484
+ );
7485
+ }
7211
7486
  function looseMkBuildRe(type) {
7212
7487
  return new RegExp(
7213
- String.raw`^\(\s*(\w+)\s*,\s*\w+\s*,\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*\)\s*=>\s*.+\(\s*(['"])${escapeRegExp(type)}\5\s*,\s*\1\s*,\s*\3\s*,\s*\2\s*,\s*\4\s*\)\s*$`
7488
+ String.raw`^\(\s*(\w+)${ANN}\s*,\s*${ANY_P}\s*,\s*(\w+)${ANN}\s*,\s*(\w+)${ANN}\s*,\s*(\w+)${ANN}\s*\)\s*=>\s*.+\(\s*(['"])${escapeRegExp(type)}\5\s*,\s*\1\s*,\s*\3\s*,\s*\2\s*,\s*\4\s*\)\s*$`
7214
7489
  );
7215
7490
  }
7491
+ var LOOKS_LIKE_MK_RE = /=>\s*(?:[\w$.]*\.)?mk\s*\(/;
7216
7492
  function analyzeMkInlineBuild(def) {
7217
7493
  if (!def.build) return null;
7218
7494
  if (def.type === void 0) return null;
7219
- const src = (def.buildSrc ?? def.build.toString()).trim();
7495
+ const src = buildAnalysisSrc(def).trim();
7220
7496
  const strict = src.match(MK_BUILD_RE);
7221
7497
  if (strict) {
7222
7498
  const mkType = strict[6];
7223
- return mkType === def.type ? def.type : null;
7499
+ if (mkType === def.type) return def.type;
7500
+ reportMkMiss(def.type, src, `it builds a "${mkType}" node, not "${def.type}"`);
7501
+ return null;
7224
7502
  }
7225
7503
  if (looseMkBuildRe(def.type).test(src)) return def.type;
7504
+ if (def.typeSrc !== void 0 && identMkBuildRe(def.typeSrc).test(src)) return def.type;
7505
+ if (LOOKS_LIKE_MK_RE.test(src)) {
7506
+ reportMkMiss(def.type, src, "its parameter list or argument order does not match `(children, fields, span, rawChildren, triviaLog) => mk(type, children, rawChildren, span, triviaLog)`");
7507
+ }
7226
7508
  return null;
7227
7509
  }
7510
+ function reportMkMiss(type, src, why) {
7511
+ recordDegradation({
7512
+ code: "mk-inline-missed",
7513
+ severity: "warn",
7514
+ where: `node("${type}")`,
7515
+ subject: `build reducer \`${src.split("\n")[0].slice(0, 80)}\``,
7516
+ fellBackTo: `looks like an inline-\`mk\` builder but ${why}, so every match pays a \`_build[n](...)\` call instead of an inlined object literal`,
7517
+ otherwise: "the node object would be constructed at the call site with no call frame"
7518
+ });
7519
+ }
7228
7520
  function emitInlineMkNodeExpr(type, chV, rawV, spanExpr, tlV) {
7229
7521
  return `{ _tag: 'node', type: ${JSON.stringify(type)}, span: ${spanExpr}, children: ${chV}, rawCount: ${rawV}.length, localTriviaLen: ${tlV}.length }`;
7230
7522
  }
@@ -7407,10 +7699,34 @@ function leadingFirstSetRecipe(p, seen = /* @__PURE__ */ new Set()) {
7407
7699
  seen.delete(p);
7408
7700
  }
7409
7701
  }
7702
+ function tern(cond, whenTrue, whenFalse) {
7703
+ if (cond === "false") return whenFalse;
7704
+ if (cond === "true") return whenTrue;
7705
+ return `${cond} ? ${whenTrue} : ${whenFalse}`;
7706
+ }
7707
+ function notGate(cond) {
7708
+ if (cond === "false") return "true";
7709
+ if (cond === "true") return "false";
7710
+ return `!(${cond})`;
7711
+ }
7712
+ function orGate(a, b) {
7713
+ if (a === "false") return b;
7714
+ if (b === "false") return a;
7715
+ if (a === "true" || b === "true") return "true";
7716
+ return `${a} || ${b}`;
7717
+ }
7718
+ function andGate(a, b) {
7719
+ if (a === "false" || b === "false") return "false";
7720
+ if (a === "true") return b;
7721
+ if (b === "true") return a;
7722
+ return `${a} && ${b}`;
7723
+ }
7410
7724
  var HOST_READS_DECL = "const _hostReads = (b, n) => { if (b === undefined) return false; let s; try { s = Function.prototype.toString.call(b) } catch (e) { return true } if (/\\barguments\\b/.test(s)) return true; const m = /^[^(]*\\(([\\s\\S]*?)\\)/.exec(s); if (m && /\\.\\.\\.|=/.test(m[1])) return true; return b.length > n }";
7725
+ var RAW_ENTRY_DECL = "const _rawEntry = (v, input, s, e, span) => (typeof v === 'object' && v !== null && (v._tag === 'node' || v._tag === 'leaf' || v._tag === 'parseError')) ? v : { _tag: 'leaf', value: typeof v === 'string' ? v : (typeof v === 'object' && v !== null ? input.slice(s, e) : ''), span: span ?? { start: s, end: e } }";
7411
7726
  var LINE_TRACK_DECL = "const _trackLines = (_ctx, input, start, end) => { const from = _ctx._lineScannedTo ?? 0; if (end <= from) return; for (let i = from; i < end; i++) if (input.charCodeAt(i) === 10) _ctx._lineStarts.push(i + 1); _ctx._lineScannedTo = end }";
7412
7727
  var LINE_SPAN_DECL = "const _lineCol = (_ctx, offset) => { const starts = _ctx._lineStarts; let lo = 0, hi = starts.length - 1; while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (starts[mid] <= offset) lo = mid; else hi = mid - 1 } return [lo + 1, offset - starts[lo] + 1] }\nconst _spanLines = (_ctx, start, end) => { const s = _lineCol(_ctx, start), e = _lineCol(_ctx, end); return { start, end, startLine: s[0], startColumn: s[1], endLine: e[0], endColumn: e[1] } }";
7413
7728
  var _loweringSink = null;
7729
+ var _inlineCapSink = null;
7414
7730
  function hasSelectedRootTrivia(ctx) {
7415
7731
  return ctx.rootTriviaClassified === true;
7416
7732
  }
@@ -8159,9 +8475,11 @@ function emitSeqValues(def, ctx, pos) {
8159
8475
  const markLog = v(ctx, "_mklg");
8160
8476
  const markRootLog = hasSelectedRootTrivia(ctx) ? v(ctx, "_mkrlg") : null;
8161
8477
  const scanEndV = v(ctx, "_sne");
8162
- const capArg = ctx.noHoist ? "0" : hasSelectedRootTrivia(ctx) ? "(_ctx._triviaLog !== undefined || _ctx._rootTriviaLog !== undefined) ? 2 : 0" : "_ctx._triviaLog !== undefined ? 2 : 0";
8478
+ const logV = v(ctx, "_tlg");
8479
+ const capArg = ctx.noHoist ? "0" : hasSelectedRootTrivia(ctx) ? `(${logV} !== undefined || _ctx._rootTriviaLog !== undefined) ? 2 : 0` : `${logV} !== undefined ? 2 : 0`;
8163
8480
  stmts.push(
8164
- `${ind(ctx)}const ${markLog} = _ctx._triviaLog ? _ctx._triviaLog.length : 0`,
8481
+ `${ind(ctx)}const ${logV} = _ctx._triviaLog`,
8482
+ `${ind(ctx)}const ${markLog} = ${logV} !== undefined ? ${logV}.length : 0`,
8165
8483
  ...markRootLog ? [`${ind(ctx)}const ${markRootLog} = _ctx._rootTriviaLog ? _ctx._rootTriviaLog.length : 0`] : [],
8166
8484
  `${ind(ctx)}const ${scanEndV} = ${trivFn}(input, ${curV}, _ctx, ${capArg})`,
8167
8485
  ...emitLineTrack(ctx, curV, scanEndV)
@@ -8171,7 +8489,7 @@ function emitSeqValues(def, ctx, pos) {
8171
8489
  const endAfterV = v(ctx, "_sea");
8172
8490
  stmts.push(
8173
8491
  `${ind(ctx)}const ${endAfterV} = ${r2.endVar}`,
8174
- `${ind(ctx)}if (${endAfterV} > ${scanEndV}) ${curV} = ${endAfterV}; else { if (_ctx._triviaLog && _ctx._triviaLog.length !== ${markLog}) _ctx._triviaLog.length = ${markLog};${markRootLog ? ` if (_ctx._rootTriviaLog && _ctx._rootTriviaLog.length !== ${markRootLog}) _ctx._rootTriviaLog.length = ${markRootLog};` : ""} }`
8492
+ `${ind(ctx)}if (${endAfterV} > ${scanEndV}) ${curV} = ${endAfterV}; else { if (${logV} !== undefined && ${logV}.length !== ${markLog}) ${logV}.length = ${markLog};${markRootLog ? ` if (_ctx._rootTriviaLog && _ctx._rootTriviaLog.length !== ${markRootLog}) _ctx._rootTriviaLog.length = ${markRootLog};` : ""} }`
8175
8493
  );
8176
8494
  valueVars.push(r2.valueVar);
8177
8495
  continue;
@@ -9243,9 +9561,18 @@ function emitPeek(def, ctx, pos) {
9243
9561
  endVar: pos
9244
9562
  };
9245
9563
  }
9246
- function emitRouted(ctx, pos) {
9564
+ function emitRouted(ctx, pos, def) {
9247
9565
  const local = ctx.routedLocal;
9566
+ const fallback = def.fallback;
9248
9567
  if (local !== void 0) {
9568
+ if (fallback !== void 0 && local.startVar !== pos) {
9569
+ return emitRoutedWithFallback(ctx, pos, fallback, {
9570
+ test: `${local.startVar} === ${pos}`,
9571
+ valExpr: local.valueVar,
9572
+ startExpr: local.startVar,
9573
+ endExpr: local.endVar
9574
+ });
9575
+ }
9249
9576
  return {
9250
9577
  stmts: [
9251
9578
  ...emitIfFail(ctx, `${local.startVar} !== ${pos}`, failBody(ctx, '"routed()"', pos)),
@@ -9256,9 +9583,19 @@ function emitRouted(ctx, pos) {
9256
9583
  };
9257
9584
  }
9258
9585
  const item = v(ctx, "_rt");
9586
+ const read = `${ind(ctx)}const ${item} = _ctx._routed`;
9587
+ if (fallback !== void 0) {
9588
+ const r = emitRoutedWithFallback(ctx, pos, fallback, {
9589
+ test: `${item} !== undefined && ${item}.span.start === ${pos}`,
9590
+ valExpr: `${item}.value`,
9591
+ startExpr: `${item}.span.start`,
9592
+ endExpr: `${item}.span.end`
9593
+ });
9594
+ return { ...r, stmts: [read, ...r.stmts] };
9595
+ }
9259
9596
  return {
9260
9597
  stmts: [
9261
- `${ind(ctx)}const ${item} = _ctx._routed`,
9598
+ read,
9262
9599
  ...emitIfFail(ctx, `${item} === undefined || ${item}.span.start !== ${pos}`, failBody(ctx, '"routed()"', pos)),
9263
9600
  ...emitLeafCapture(ctx, `${item}.value`, `${item}.span.start`, `${item}.span.end`)
9264
9601
  ],
@@ -9266,6 +9603,30 @@ function emitRouted(ctx, pos) {
9266
9603
  endVar: `${item}.span.end`
9267
9604
  };
9268
9605
  }
9606
+ function emitRoutedWithFallback(ctx, pos, fallback, routed2) {
9607
+ const valV = v(ctx, "_rtv");
9608
+ const endV = v(ctx, "_rte");
9609
+ const i = ind(ctx);
9610
+ ctx.indent++;
9611
+ const capture = emitLeafCapture(ctx, routed2.valExpr, routed2.startExpr, routed2.endExpr);
9612
+ const fb = emit(fallback, ctx, pos);
9613
+ const inner = ind(ctx);
9614
+ ctx.indent--;
9615
+ return {
9616
+ stmts: [
9617
+ `${i}let ${valV}, ${endV}`,
9618
+ `${i}if (${routed2.test}) {`,
9619
+ ...capture,
9620
+ `${inner}${valV} = ${routed2.valExpr}; ${endV} = ${routed2.endExpr}`,
9621
+ `${i}} else {`,
9622
+ ...fb.stmts,
9623
+ `${inner}${valV} = ${fb.valueVar}; ${endV} = ${fb.endVar}`,
9624
+ `${i}}`
9625
+ ],
9626
+ valueVar: valV,
9627
+ endVar: endV
9628
+ };
9629
+ }
9269
9630
  function escapeTokenLiteral(value) {
9270
9631
  return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
9271
9632
  }
@@ -9447,22 +9808,16 @@ function emitNode(def, ctx, pos) {
9447
9808
  const smk = structural ? v(ctx, "_smk") : null;
9448
9809
  if (structural) ctx.needsHostReads = true;
9449
9810
  const hostTriviaGate = `_ctx.build !== undefined && (_ctx.build._parsemanCaptureTrivia !== undefined ? _ctx.build._parsemanCaptureTrivia(${JSON.stringify(def.type)}) : (_ctx._pmCapTL ??= _hostReads(_ctx.build, 5)))`;
9450
- const pmV = v(ctx, "_pm"), recV = v(ctx, "_rec"), capV = v(ctx, "_cap");
9451
- const profHoist = `${i}const ${pmV} = _ctx._pmProfile, ${recV} = ${pmV}?.phase === 'recognizer', ${capV} = ${pmV}?.phase === 'capture'`;
9452
- const profileRecognizer = recV;
9453
- const profileCapture = capV;
9454
- const cstMode = ctx.hostMode === "cst";
9455
- const directCstV = !structural && !cstMode && (!capturesChildren || !capturesRaw) ? v(ctx, "_dcst") : null;
9456
- const directCstGate = directCstV === null ? "false" : profileCapture;
9811
+ const profileRecognizer = "false";
9812
+ const profileCapture = "false";
9457
9813
  const structuralCapturesTrivia = structural && (def.captureTrivia === true || def.trailingTrivia === true);
9458
9814
  const chNeededExpr = def.unwrap || def.collapse ? "true" : `(_ctx._pmReadsCh ??= (_ctx.build === undefined || _ctx.build._parsemanReadsChildren !== false || _ctx.build._parsemanCstCollapse !== undefined))`;
9459
- const chAlloc = def.unwrap || def.collapse ? `${profileRecognizer} ? undefined : []` : `${profileRecognizer} ? undefined : ((${profileCapture} || ${chNeededExpr}) ? [] : undefined)`;
9460
- const allocStmt = structural ? `${i}const ${capTLv} = !(${profileRecognizer}) && (${profileCapture} || ${structuralCapturesTrivia ? "true" : hostTriviaGate}), ${capSTv} = !(${profileRecognizer} || ${profileCapture}) && (_ctx._pmCapST ??= (_ctx.build === undefined || _hostReads(_ctx.build, 6)))${capFv ? `, ${capFv} = !(${profileRecognizer}) && (${profileCapture} || (_ctx.build !== undefined && _hostReads(_ctx.build, 2)))` : ""}
9461
- ${i}const ${chV} = ${chAlloc}, ${rawV} = ${profileRecognizer} ? undefined : [], ${tlV} = ${profileRecognizer} ? undefined : ${innerEnablesTriviaCapture ? "[]" : `${capTLv} ? [] : _EMPTY_TL`}` : capturesTrivia ? `${directCstV ? `${i}const ${directCstV} = ${directCstGate}
9462
- ` : ""}${i}const ${capTLv} = !(${profileRecognizer}), ${chV} = ${profileRecognizer} ? undefined : ${capturesChildren ? "[]" : `${directCstV} ? [] : undefined`}, ${rawV} = ${profileRecognizer} ? undefined : ${capturesRaw ? "[]" : `${directCstV} ? [] : undefined`}, ${tlV} = ${profileRecognizer} ? undefined : []` : `${directCstV ? `${i}const ${directCstV} = ${directCstGate}
9463
- ` : ""}${i}const ${chV} = ${profileRecognizer} ? undefined : ${capturesChildren ? "[]" : `${directCstV} ? [] : undefined`}, ${rawV} = ${profileRecognizer} ? undefined : ${capturesRaw ? "[]" : `${directCstV} ? [] : undefined`}`;
9815
+ const chAlloc = def.unwrap || def.collapse ? tern(profileRecognizer, "undefined", "[]") : tern(profileRecognizer, "undefined", `${tern(orGate(profileCapture, chNeededExpr), "[]", "undefined")}`);
9816
+ const dcstAlloc = "undefined";
9817
+ const allocStmt = structural ? `${i}const ${capTLv} = ${andGate(notGate(profileRecognizer), orGate(profileCapture, structuralCapturesTrivia ? "true" : hostTriviaGate))}, ${capSTv} = ${andGate(notGate(orGate(profileRecognizer, profileCapture)), "(_ctx._pmCapST ??= (_ctx.build === undefined || _hostReads(_ctx.build, 6)))")}${capFv ? `, ${capFv} = ${andGate(notGate(profileRecognizer), orGate(profileCapture, "(_ctx.build !== undefined && _hostReads(_ctx.build, 2))"))}` : ""}
9818
+ ${i}const ${chV} = ${chAlloc}, ${rawV} = ${tern(profileRecognizer, "undefined", "[]")}, ${tlV} = ${tern(profileRecognizer, "undefined", innerEnablesTriviaCapture ? "[]" : `${capTLv} ? [] : _EMPTY_TL`)}` : capturesTrivia ? `${i}const ${capTLv} = ${notGate(profileRecognizer)}, ${chV} = ${tern(profileRecognizer, "undefined", capturesChildren ? "[]" : dcstAlloc)}, ${rawV} = ${tern(profileRecognizer, "undefined", capturesRaw ? "[]" : dcstAlloc)}, ${tlV} = ${tern(profileRecognizer, "undefined", "[]")}` : `${i}const ${chV} = ${tern(profileRecognizer, "undefined", capturesChildren ? "[]" : dcstAlloc)}, ${rawV} = ${tern(profileRecognizer, "undefined", capturesRaw ? "[]" : dcstAlloc)}`;
9464
9819
  const innerTl = structural || capturesTrivia ? structural && !innerEnablesTriviaCapture ? `${capTLv} ? ${tlV} : undefined` : tlV : "undefined";
9465
- const fieldsOn = structural ? capFv ?? "false" : `${profileRecognizer} ? false : ${capturesFields ? "true" : "false"}`;
9820
+ const fieldsOn = structural ? capFv ?? "false" : tern(profileRecognizer, "false", capturesFields ? "true" : "false");
9466
9821
  const sf = hasFields ? v(ctx, "_sf") : null;
9467
9822
  const fArr = hasFields ? v(ctx, "_fa") : null;
9468
9823
  const fObj = hasFields ? v(ctx, "_fields") : "undefined";
@@ -9471,7 +9826,7 @@ ${i}const ${chV} = ${chAlloc}, ${rawV} = ${profileRecognizer} ? undefined : [],
9471
9826
  const installTrivia = needsTriviaFrame ? `; _ctx.captureTrivia = ${capTLv ?? "false"}; _ctx._cstTriviaLog = ${innerTl}${smk ? `; _ctx._triviaCaptureMask = ${capTLv} && _ctx.build !== undefined && _ctx.build._parsemanTriviaKinds !== undefined ? _ctx.build._parsemanTriviaKinds(${JSON.stringify(def.type)}) : ${smk}` : ""}` : "";
9472
9827
  const restoreTrivia = needsTriviaFrame ? `; _ctx.captureTrivia = ${st}; _ctx._cstTriviaLog = ${stl}${smk ? `; _ctx._triviaCaptureMask = ${smk}` : ""}` : "";
9473
9828
  const preGuard = [];
9474
- if (!ctx.recovery && !ctx.coverage && (capturesChildren || structural) && needsFirstSetGuard(def.parser)) {
9829
+ if (!ctx.recovery && !ctx.coverage && needsFirstSetGuard(def.parser)) {
9475
9830
  const gcV = v(ctx, "_ngc");
9476
9831
  const gExp = armStaticExpected(ctx, def.parser);
9477
9832
  const gFail = ctx.failLabel ? ctx.recordFail ? `{ _ctx._fe = ${pos}; _ctx._fx = ${gExp}; break ${ctx.failLabel} }` : `break ${ctx.failLabel}` : failReturnArr(ctx, gExp, pos);
@@ -9482,7 +9837,6 @@ ${i}const ${chV} = ${chAlloc}, ${rawV} = ${profileRecognizer} ? undefined : [],
9482
9837
  }
9483
9838
  const stmts = [
9484
9839
  ...preGuard,
9485
- profHoist,
9486
9840
  allocStmt,
9487
9841
  `${i}const ${sc} = _ctx._cstChildren, ${sl} = _ctx._cstLeaves, ${sr} = _ctx._cstRawChildren${saveTrivia}${sf ? `, ${sf} = _ctx._fields` : ""}`,
9488
9842
  `${i}_ctx._cstChildren = ${chV}; _ctx._cstLeaves = ${chV}; _ctx._cstRawChildren = ${rawV}${installTrivia}${sf ? `; _ctx._fields = ${fieldsOn} ? [] : undefined` : ""}`
@@ -9499,21 +9853,13 @@ ${i}const ${chV} = ${chAlloc}, ${rawV} = ${profileRecognizer} ? undefined : [],
9499
9853
  }
9500
9854
  stmts.push(`${i}_ctx._cstChildren = ${sc}; _ctx._cstLeaves = ${sl}; _ctx._cstRawChildren = ${sr}${restoreTrivia}${sf ? `; _ctx._fields = ${sf}` : ""}`);
9501
9855
  stmts.push(...emitIfFail(ctx, `!${okVar}`, propagateFailBody(ctx)));
9502
- stmts.push(
9503
- `${i}if (_ctx._pmProfile) {`,
9504
- `${i} _ctx._pmProfile.nodes++`,
9505
- `${i} if (${profileCapture}) {`,
9506
- `${i} _ctx._pmProfile.childSlots += ${chV}.length; _ctx._pmProfile.rawSlots += ${rawV}.length; _ctx._pmProfile.triviaSlots += ${tlV}.length${fArr ? `; _ctx._pmProfile.fieldSlots += ${fArr} ? ${fArr}.length : 0` : ""}`,
9507
- `${i} }`,
9508
- `${i}}`
9509
- );
9510
9856
  let stV = "undefined";
9511
9857
  if (structural) {
9512
9858
  stV = v(ctx, "_nst");
9513
9859
  stmts.push(`${i}const ${stV} = ${capSTv} && _ctx.state !== undefined ? Object.assign({}, _ctx.state) : undefined`);
9514
9860
  } else if (clonesState) {
9515
9861
  stV = v(ctx, "_nst");
9516
- stmts.push(`${i}const ${stV} = !(${profileRecognizer} || ${profileCapture}) && _ctx.state !== undefined ? Object.assign({}, _ctx.state) : undefined`);
9862
+ stmts.push(`${i}const ${stV} = ${andGate(notGate(orGate(profileRecognizer, profileCapture)), "_ctx.state !== undefined")} ? Object.assign({}, _ctx.state) : undefined`);
9517
9863
  }
9518
9864
  if (sf && fArr) {
9519
9865
  const fe = v(ctx, "_fe");
@@ -9537,19 +9883,22 @@ ${i}const ${chV} = ${chAlloc}, ${rawV} = ${profileRecognizer} ? undefined : [],
9537
9883
  if (ctx.lineTracking) stmts.push(`${i}const ${nodeSpanV} = ${nodeSpanExpr}`);
9538
9884
  const buildExpr = structural ? `{ _tag: 'node', type: ${JSON.stringify(def.type)}, span: ${nodeSpanV}, state: ${stV} ?? null, children: ${chV} }` : mkType ? emitInlineMkNodeExpr(mkType, chV, rawV, nodeSpanV, tlV) : def.project !== void 0 ? emitNodeProjectExpr(def.type, def.project, chV) : `${buildRef(ctx)}[${buildIdx}](${chV}, ${fObj}, ${nodeSpanV}, ${rawV}, ${tlV}, ${stV})`;
9539
9885
  const hostBuildArgs = `${JSON.stringify(def.type)}, ${chV}, ${fObj}, ${nodeSpanV}, ${rawV}, ${tlV}, ${stV}${def.tags !== void 0 && def.tags.length > 0 ? `, ${JSON.stringify(def.tags)}` : ""}`;
9540
- const hostBuildExpr = `_ctx._pmProfile?.phase === 'host' && _ctx._pmProfile.hostCalls++, _ctx.build(${hostBuildArgs})`;
9886
+ const hostBuildExpr = `_ctx.build(${hostBuildArgs})`;
9541
9887
  const ndExpr = structural ? `_ctx.build !== undefined ? (${hostBuildExpr}) : (${buildExpr})` : cstOut ? hostBuildExpr : buildExpr;
9542
- const hostCollapseExpr = structural ? `_ctx.build !== undefined && _ctx.build._parsemanCstCollapse !== undefined && ${chV}.length === 1 && ${rawV}.length === 1 && _ctx.build._parsemanCstCollapse(${JSON.stringify(def.type)}, ${chV}[0], ${chV}, ${rawV}) ? ${chV}[0] : (${ndExpr})` : ndExpr;
9888
+ const hostCollapses = structural || cstOut;
9889
+ const hostCollapseExpr = hostCollapses ? `_ctx.build !== undefined && _ctx.build._parsemanCstCollapse !== undefined && ${chV}.length === 1 && ${rawV}.length === 1 && _ctx.build._parsemanCstCollapse(${JSON.stringify(def.type)}, ${chV}[0], ${chV}, ${rawV}) ? ${chV}[0] : (${ndExpr})` : ndExpr;
9543
9890
  const unwrapExpr = `${chV}.length === 1 ? (${chV}[0] !== null && typeof ${chV}[0] === 'object' && ${chV}[0]._tag === 'leaf' ? ${chV}[0].value : ${chV}[0]) : (${ndExpr})`;
9544
9891
  const collapseExpr = `${chV}.length === 1 ? ${chV}[0] : (${ndExpr})`;
9545
9892
  const finalExpr = def.unwrap ? unwrapExpr : def.collapse ? collapseExpr : hostCollapseExpr;
9546
- stmts.push(
9547
- `${i}const ${ndV} = (${profileRecognizer} || ${profileCapture}) ? undefined : (${finalExpr})`,
9548
- `${i}if (!(${profileRecognizer})) {`,
9549
- `${i} if (${sc}) ${sc}.push(${ndV})`,
9550
- `${i} if (${sr}) ${sr}.push((typeof ${ndV} === 'object' && ${ndV} !== null && (${ndV}._tag === 'node' || ${ndV}._tag === 'leaf' || ${ndV}._tag === 'parseError')) ? ${ndV} : { _tag: 'leaf', value: typeof ${ndV} === 'string' ? ${ndV} : (typeof ${ndV} === 'object' && ${ndV} !== null ? input.slice(${pos}, ${endVar}) : ''), span: ${nodeSpanV} })`,
9551
- `${i}}`
9552
- );
9893
+ const ndGate = orGate(profileRecognizer, profileCapture);
9894
+ const recGate = notGate(profileRecognizer);
9895
+ const pushIndent = recGate === "true" ? i : `${i} `;
9896
+ const pushCh = `${pushIndent}if (${sc}) ${sc}.push(${ndV})`;
9897
+ ctx.needsRawEntry = true;
9898
+ const pushRaw = `${pushIndent}if (${sr}) ${sr}.push(_rawEntry(${ndV}, input, ${pos}, ${endVar}${ctx.lineTracking ? `, ${nodeSpanV}` : ""}))`;
9899
+ stmts.push(`${i}const ${ndV} = ${tern(ndGate, "undefined", `(${finalExpr})`)}`);
9900
+ if (recGate === "true") stmts.push(pushCh, pushRaw);
9901
+ else stmts.push(`${i}if (${recGate}) {`, pushCh, pushRaw, `${i}}`);
9553
9902
  return { stmts, valueVar: ndV, endVar };
9554
9903
  }
9555
9904
  function emitRuntimeFallback(parser2, ctx, pos) {
@@ -9578,7 +9927,12 @@ function emitLazy(p, def, ctx, pos) {
9578
9927
  } catch {
9579
9928
  return emitRuntimeFallback(p, ctx, pos);
9580
9929
  }
9581
- return emit(resolved, ctx, pos);
9930
+ const cost = usage.sizes.get(resolved) ?? 1;
9931
+ if (cost <= ctx.inlineLeft) {
9932
+ ctx.inlineLeft -= cost;
9933
+ return emit(resolved, ctx, pos);
9934
+ }
9935
+ _inlineCapSink?.push({ fn: ctx.currentFnName ?? "<root>", nodes: cost });
9582
9936
  }
9583
9937
  if (!ctx.namedParsers.has(p)) {
9584
9938
  const fnName2 = canonical2 ?? `${nsp(ctx)}_pf${ctx.namedParsers.size}`;
@@ -9593,6 +9947,9 @@ function emitLazy(p, def, ctx, pos) {
9593
9947
  const savedIndent = ctx.indent;
9594
9948
  const savedFailLabel = ctx.failLabel;
9595
9949
  const savedRecord = ctx.recordFail;
9950
+ const savedInlineLeft = ctx.inlineLeft, savedFnName = ctx.currentFnName;
9951
+ ctx.inlineLeft = ctx.inlineMax;
9952
+ ctx.currentFnName = fnName2;
9596
9953
  const failureRuleId = p === ctx.coverage?.entry ? void 0 : ctx.coverage?.plan.rules.get(p) ?? ctx.coverage?.plan.rules.get(resolved);
9597
9954
  const savedActiveRule = ctx.activeCoverageRuleId;
9598
9955
  const savedSuppressFailure = ctx.suppressCoverageFailure;
@@ -9607,6 +9964,8 @@ function emitLazy(p, def, ctx, pos) {
9607
9964
  ctx.indent = savedIndent;
9608
9965
  ctx.failLabel = savedFailLabel;
9609
9966
  ctx.recordFail = savedRecord;
9967
+ ctx.inlineLeft = savedInlineLeft;
9968
+ ctx.currentFnName = savedFnName;
9610
9969
  ctx.activeCoverageRuleId = savedActiveRule;
9611
9970
  ctx.suppressCoverageFailure = savedSuppressFailure;
9612
9971
  pushNamedFnDecl(ctx, fnName2, r.stmts, r.valueVar, r.endVar, failureRuleId);
@@ -9701,6 +10060,9 @@ function emit(p, ctx, pos) {
9701
10060
  const savedIndent = ctx.indent;
9702
10061
  const savedFailLabel = ctx.failLabel;
9703
10062
  const savedRecord = ctx.recordFail;
10063
+ const savedInlineLeft = ctx.inlineLeft, savedFnName = ctx.currentFnName;
10064
+ ctx.inlineLeft = ctx.inlineMax;
10065
+ ctx.currentFnName = fnName;
9704
10066
  ctx.indent = 1;
9705
10067
  ctx.failLabel = "_pfail";
9706
10068
  ctx.recordFail = true;
@@ -9708,6 +10070,8 @@ function emit(p, ctx, pos) {
9708
10070
  ctx.indent = savedIndent;
9709
10071
  ctx.failLabel = savedFailLabel;
9710
10072
  ctx.recordFail = savedRecord;
10073
+ ctx.inlineLeft = savedInlineLeft;
10074
+ ctx.currentFnName = savedFnName;
9711
10075
  pushNamedFnDecl(ctx, fnName, r.stmts, r.valueVar, r.endVar);
9712
10076
  return instrument(emitNamedFnCall(ctx, fnName, pos));
9713
10077
  }
@@ -9901,7 +10265,7 @@ function emitDispatch(p, ctx, pos) {
9901
10265
  case "peek":
9902
10266
  return emitPeek(def, ctx, pos);
9903
10267
  case "routed":
9904
- return emitRouted(ctx, pos);
10268
+ return emitRouted(ctx, pos, def);
9905
10269
  case "node":
9906
10270
  return emitNode(def, ctx, pos);
9907
10271
  case "scanTo":
@@ -9936,6 +10300,9 @@ function emitDispatch(p, ctx, pos) {
9936
10300
  const savedIndent = ctx.indent;
9937
10301
  const savedFailLabel = ctx.failLabel;
9938
10302
  const savedRecord = ctx.recordFail;
10303
+ const savedInlineLeft = ctx.inlineLeft, savedFnName = ctx.currentFnName;
10304
+ ctx.inlineLeft = ctx.inlineMax;
10305
+ ctx.currentFnName = fnName;
9939
10306
  ctx.indent = 1;
9940
10307
  ctx.failLabel = "_pfail";
9941
10308
  ctx.recordFail = true;
@@ -9943,6 +10310,8 @@ function emitDispatch(p, ctx, pos) {
9943
10310
  ctx.indent = savedIndent;
9944
10311
  ctx.failLabel = savedFailLabel;
9945
10312
  ctx.recordFail = savedRecord;
10313
+ ctx.inlineLeft = savedInlineLeft;
10314
+ ctx.currentFnName = savedFnName;
9946
10315
  pushNamedFnDecl(ctx, fnName, innerR.stmts, innerR.valueVar, innerR.endVar);
9947
10316
  }
9948
10317
  const fn = ctx.namedParsers.get(innerParser);
@@ -10101,12 +10470,16 @@ function childrenOf3(def) {
10101
10470
  return [def.parser, def.sentinel];
10102
10471
  case "scanTo":
10103
10472
  return [def.sentinel, ...def.skip];
10473
+ // A `routed()` fallback IS emitted at this site (emitRouted), so the usage
10474
+ // analysis must see the edge — otherwise a fallback shared by several routed()
10475
+ // sites is miscounted as single-use and inlined at each.
10476
+ case "routed":
10477
+ return def.fallback ? [def.fallback] : [];
10104
10478
  case "lazy":
10105
10479
  case "literal":
10106
10480
  case "regex":
10107
10481
  case "keywords":
10108
10482
  case "guard":
10109
- case "routed":
10110
10483
  case "unknown":
10111
10484
  return [];
10112
10485
  }
@@ -10127,6 +10500,17 @@ function isHoistableTag(tag) {
10127
10500
  return HOISTABLE_TAGS.has(tag);
10128
10501
  }
10129
10502
  var HOIST_MIN_SUBTREE = 3;
10503
+ var INLINE_MAX_NODES = 1e3;
10504
+ function resolveInlineMax(explicit) {
10505
+ if (explicit !== void 0) return explicit;
10506
+ const env = typeof process !== "undefined" ? process.env?.PARSEMAN_MAX_INLINE : void 0;
10507
+ if (env !== void 0 && env !== "") {
10508
+ if (env === "off" || env === "infinity") return Infinity;
10509
+ const n = Number(env);
10510
+ if (Number.isFinite(n) && n >= 0) return n;
10511
+ }
10512
+ return INLINE_MAX_NODES;
10513
+ }
10130
10514
  function analyzeLazyUsage(root) {
10131
10515
  return analyzeLazyUsageMulti([root]);
10132
10516
  }
@@ -10232,102 +10616,6 @@ function ruleDependencies(ruleMap) {
10232
10616
  }
10233
10617
  return deps;
10234
10618
  }
10235
- function resolveGatingLevel(opt) {
10236
- const explicit = typeof opt === "string" ? opt : opt?.level;
10237
- if (explicit !== void 0) return explicit;
10238
- const env = typeof process !== "undefined" ? process.env?.PARSEMAN_GATING : void 0;
10239
- if (env === "off" || env === "warn" || env === "error") return env;
10240
- return "warn";
10241
- }
10242
- function runGatingDiagnostic(combinator, opt) {
10243
- return reportGating(opt, (o) => analyzeGating(combinator, o));
10244
- }
10245
- function runGatingDiagnosticRules(ruleMap, opt) {
10246
- return reportGating(opt, (o) => analyzeGatingRules(ruleMap, o));
10247
- }
10248
- function runFusedGatingDiagnostic(getRuleMaps, opt, getOpaque) {
10249
- if (resolveGatingLevel(opt) === "off") return void 0;
10250
- const ruleMaps = getRuleMaps();
10251
- const opaque = getOpaque?.() ?? [];
10252
- const opaqueFindings = opaque.map((o) => ({
10253
- rule: o.ruleNames.length > 0 ? o.ruleNames.join(", ") : `<artifact ${o.ns}>`,
10254
- kind: "opaque-artifact",
10255
- reason: `precompiled artifact "${o.ns}" carries compiled rule functions, not re-lowerable IR \u2014 its choices were not examined at this fuse.`
10256
- }));
10257
- if (!ruleMaps.some((map) => hasExternalRuleRef(map))) {
10258
- if (opaqueFindings.length === 0) return void 0;
10259
- return reportGating(opt, () => ({
10260
- totalChoices: 0,
10261
- gated: 0,
10262
- recoverable: 0,
10263
- unanalysable: opaqueFindings,
10264
- ungated: [],
10265
- accepted: [],
10266
- deferred: [],
10267
- acceptedUnused: [],
10268
- choices: [],
10269
- antiPatterns: []
10270
- }));
10271
- }
10272
- const winners = /* @__PURE__ */ new Map();
10273
- for (const map of ruleMaps) for (const [name, rule] of map) {
10274
- if (rule._def.tag === "lazy") {
10275
- try {
10276
- rule._def.thunk();
10277
- } catch {
10278
- continue;
10279
- }
10280
- }
10281
- winners.set(name, rule);
10282
- }
10283
- const entries = [...winners];
10284
- return reportGating(opt, (o) => {
10285
- const deferredHere = new Set(analyzeGatingRules(entries, o).deferred.map((c) => c.id));
10286
- const fused = analyzeGatingRules(entries, { ...o, resolveRef: (name) => winners.get(name) });
10287
- return {
10288
- ...fused,
10289
- ungated: fused.ungated.filter((c) => deferredHere.has(c.id)),
10290
- antiPatterns: [],
10291
- unanalysable: [...fused.unanalysable, ...opaqueFindings]
10292
- };
10293
- });
10294
- }
10295
- function reportGating(opt, analyze) {
10296
- const level = resolveGatingLevel(opt);
10297
- if (level === "off") return void 0;
10298
- const obj = opt !== null && typeof opt === "object" ? opt : void 0;
10299
- const analyzeOpts = obj?.accept !== void 0 || obj?.entryName !== void 0 ? { ...obj.accept !== void 0 ? { accept: obj.accept } : {}, ...obj.entryName !== void 0 ? { entryName: obj.entryName } : {} } : void 0;
10300
- let report;
10301
- try {
10302
- report = analyze(analyzeOpts);
10303
- } catch (e) {
10304
- const reason = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
10305
- const failed = {
10306
- totalChoices: 0,
10307
- gated: 0,
10308
- recoverable: 0,
10309
- unanalysable: [{ rule: "<whole grammar>", kind: "not-a-combinator", reason: `gating analysis threw \u2014 ${reason}` }],
10310
- ungated: [],
10311
- accepted: [],
10312
- deferred: [],
10313
- acceptedUnused: [],
10314
- choices: [],
10315
- antiPatterns: []
10316
- };
10317
- const failLines = formatGatingWarnings(failed);
10318
- if (level === "error") throw new Error(`parseman: gating analysis FAILED (no verdict available)
10319
- ${failLines.join("\n")}`);
10320
- for (const l of failLines) console.warn(l);
10321
- return failed;
10322
- }
10323
- const lines = formatGatingWarnings(report);
10324
- if (lines.length > 0) {
10325
- if (level === "error") throw new Error(`parseman: ${report.ungated.length} ungated hot choice(s) / ${report.antiPatterns.length} anti-pattern(s) / ${report.unanalysable.length} unanalysable rule(s)
10326
- ${lines.join("\n")}`);
10327
- for (const l of lines) console.warn(l);
10328
- }
10329
- return report;
10330
- }
10331
10619
  function resolveDuplicationLevel(opt) {
10332
10620
  const explicit = typeof opt === "string" ? opt : opt?.level;
10333
10621
  if (explicit !== void 0) return explicit;
@@ -10370,7 +10658,17 @@ function runDuplicationDiagnosticRules(ruleMap, opt) {
10370
10658
  return reportDuplication(opt, (o) => analyzeDuplicationRules(ruleMap, o));
10371
10659
  }
10372
10660
  function compile(combinator, mapFnSources, opts) {
10373
- const gatingReport = runGatingDiagnostic(combinator, opts?.gating);
10661
+ const drain = beginCompileDegradationDrain();
10662
+ let completed = false;
10663
+ try {
10664
+ const compiled = compileImpl(combinator, mapFnSources, opts);
10665
+ completed = true;
10666
+ return compiled;
10667
+ } finally {
10668
+ drain(completed);
10669
+ }
10670
+ }
10671
+ function compileImpl(combinator, mapFnSources, opts) {
10374
10672
  runDuplicationDiagnostic(combinator, opts?.duplication);
10375
10673
  markUnusedValues(combinator);
10376
10674
  const grammarTrivia = combinator._meta.grammarTrivia;
@@ -10378,9 +10676,12 @@ function compile(combinator, mapFnSources, opts) {
10378
10676
  const grammarScanSkip = combinator._meta.grammarScanSkip;
10379
10677
  const grammarHostMode = combinator._meta.grammarHostMode;
10380
10678
  const grammarTrackLines = combinator._meta.grammarTrackLines;
10679
+ const _inlineMax = resolveInlineMax(opts?.maxInline);
10381
10680
  const ctx = {
10382
10681
  vars: 0,
10383
10682
  indent: 1,
10683
+ inlineMax: _inlineMax,
10684
+ inlineLeft: _inlineMax,
10384
10685
  regexDecls: [],
10385
10686
  regexMap: /* @__PURE__ */ new Map(),
10386
10687
  expectedDecls: [],
@@ -10426,11 +10727,13 @@ function compile(combinator, mapFnSources, opts) {
10426
10727
  const namedPrelude = ctx.namedFnDecls.length > 0 ? [...namedFnPrelude(), ""] : [];
10427
10728
  const emptyTlDecls = ctx.needsEmptyTl ? ["const _EMPTY_TL = Object.freeze([])"] : [];
10428
10729
  const hostReadsDecls = ctx.needsHostReads ? [HOST_READS_DECL] : [];
10730
+ const rawEntryDecls = ctx.needsRawEntry ? [RAW_ENTRY_DECL] : [];
10429
10731
  const lineTrackDecls = ctx.needsLineTrack ? [LINE_TRACK_DECL] : [];
10430
10732
  const lineSpanDecls = ctx.needsLineSpan ? [LINE_SPAN_DECL] : [];
10431
10733
  const source = [
10432
10734
  ...emptyTlDecls,
10433
10735
  ...hostReadsDecls,
10736
+ ...rawEntryDecls,
10434
10737
  ...lineTrackDecls,
10435
10738
  ...lineSpanDecls,
10436
10739
  ...ctx.regexDecls,
@@ -10445,6 +10748,7 @@ function compile(combinator, mapFnSources, opts) {
10445
10748
  const fn = new Function("input", "_pos", "_rp", "_mf", "_build", "_ctx", [
10446
10749
  ...emptyTlDecls,
10447
10750
  ...hostReadsDecls,
10751
+ ...rawEntryDecls,
10448
10752
  ...lineTrackDecls,
10449
10753
  ...lineSpanDecls,
10450
10754
  ...ctx.regexDecls,
@@ -10494,8 +10798,7 @@ function compile(combinator, mapFnSources, opts) {
10494
10798
  const compiledMeta = {
10495
10799
  source,
10496
10800
  inlineExpression,
10497
- ...ctx.coverage === void 0 ? {} : { coverageDefinitions: ctx.coverage.plan.definitions },
10498
- ...gatingReport === void 0 ? {} : { gating: gatingReport }
10801
+ ...ctx.coverage === void 0 ? {} : { coverageDefinitions: ctx.coverage.plan.definitions }
10499
10802
  };
10500
10803
  if (!ctx.lineTracking) {
10501
10804
  return {
@@ -10561,7 +10864,6 @@ function publicRuleWrapperSource(rule, fnSource, ambientTriviaKindLabels, ambien
10561
10864
  }
10562
10865
  function compileLinkable(ruleMapArg, ns, opts) {
10563
10866
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
10564
- if (opts?.gating !== void 0) runGatingDiagnosticRules(ruleMapArg, opts.gating);
10565
10867
  if (opts?.duplication !== void 0) runDuplicationDiagnosticRules(ruleMapArg, opts.duplication);
10566
10868
  for (const [, rule] of ruleMapArg) markUnusedValues(rule);
10567
10869
  const grammarTrivia = opts?.trivia ?? ruleMapArg.map(([, r]) => r._meta.grammarTrivia).find(Boolean);
@@ -10580,9 +10882,12 @@ function compileLinkable(ruleMapArg, ns, opts) {
10580
10882
  }
10581
10883
  });
10582
10884
  const nodeMeta = new Map(ruleMap.map(([name, rule]) => [name, collectGrammarReflection([[name, rule]], { followLazy: false })]));
10885
+ const _inlineMax = resolveInlineMax(opts?.maxInline);
10583
10886
  const ctx = {
10584
10887
  vars: 0,
10585
10888
  indent: 1,
10889
+ inlineMax: _inlineMax,
10890
+ inlineLeft: _inlineMax,
10586
10891
  regexDecls: [],
10587
10892
  regexMap: /* @__PURE__ */ new Map(),
10588
10893
  expectedDecls: [],
@@ -10678,6 +10983,9 @@ function compileLinkable(ruleMapArg, ns, opts) {
10678
10983
  }
10679
10984
  }
10680
10985
  const savedIndent = ctx.indent, savedFail = ctx.failLabel, savedRec = ctx.recordFail;
10986
+ const savedInlineLeft = ctx.inlineLeft, savedFnName = ctx.currentFnName;
10987
+ ctx.inlineLeft = ctx.inlineMax;
10988
+ ctx.currentFnName = fn;
10681
10989
  ctx.indent = 1;
10682
10990
  ctx.failLabel = "_pfail";
10683
10991
  ctx.recordFail = true;
@@ -10693,6 +11001,8 @@ function compileLinkable(ruleMapArg, ns, opts) {
10693
11001
  ctx.indent = savedIndent;
10694
11002
  ctx.failLabel = savedFail;
10695
11003
  ctx.recordFail = savedRec;
11004
+ ctx.inlineLeft = savedInlineLeft;
11005
+ ctx.currentFnName = savedFnName;
10696
11006
  const ruleId = ctx.coverage?.plan.rules.get(rule);
10697
11007
  const stmts = ruleId === void 0 ? body.stmts : [
10698
11008
  `${ind(ctx)}_ctx._grammarCoverage?.(${JSON.stringify(ruleId)})`,
@@ -10777,6 +11087,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
10777
11087
  deps: ruleDependencies(ruleMap),
10778
11088
  needsEmptyTl: !!ctx.needsEmptyTl,
10779
11089
  needsHostReads: !!ctx.needsHostReads,
11090
+ needsRawEntry: !!ctx.needsRawEntry,
10780
11091
  hostMode: ctx.hostMode ?? "ast",
10781
11092
  hostBranchElided: !!ctx.hostBranchElided,
10782
11093
  hasDirectBuilders: ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
@@ -10786,26 +11097,6 @@ function compileLinkable(ruleMapArg, ns, opts) {
10786
11097
  buildFns: buildSrcs ? [] : ctx.buildFns
10787
11098
  };
10788
11099
  }
10789
- function hasExternalRuleRef(ruleMap) {
10790
- const seen = /* @__PURE__ */ new Set();
10791
- const visit = (p) => {
10792
- if (seen.has(p)) return false;
10793
- seen.add(p);
10794
- const def = p._def;
10795
- if (def.tag === "lazy") {
10796
- let resolved;
10797
- try {
10798
- resolved = def.thunk();
10799
- } catch {
10800
- resolved = void 0;
10801
- }
10802
- if (resolved === void 0) return p._ruleName !== void 0;
10803
- return visit(resolved);
10804
- }
10805
- return childrenOf3(def).some(visit);
10806
- };
10807
- return ruleMap.some(([, rule]) => visit(rule));
10808
- }
10809
11100
  function hasSemanticReduction(roots, externalRefs) {
10810
11101
  const seen = /* @__PURE__ */ new Set();
10811
11102
  const visit = (parser2) => {
@@ -10851,14 +11142,16 @@ function buildInlineExpression(ctx, r, mapFnSources, buildSources, coverageRootR
10851
11142
  const buildDecl = buildSources?.length ? ` const _build = [${buildSources.join(", ")}]` : "";
10852
11143
  const emptyTlDecl = ctx.needsEmptyTl ? ` const _EMPTY_TL = Object.freeze([])` : "";
10853
11144
  const hostReadsDecl = ctx.needsHostReads ? ` ${HOST_READS_DECL}` : "";
11145
+ const rawEntryDecl = ctx.needsRawEntry ? ` ${RAW_ENTRY_DECL}` : "";
10854
11146
  const lineTrackDecl = ctx.needsLineTrack ? ` ${LINE_TRACK_DECL}` : "";
10855
11147
  const lineSpanDecl = ctx.needsLineSpan ? ` ${LINE_SPAN_DECL}` : "";
10856
- const needsWrapper = ctx.regexDecls.length > 0 || ctx.expectedDecls.length > 0 || ctx.namedFnDecls.length > 0 || !!mfDecl || !!buildDecl || !!emptyTlDecl || !!hostReadsDecl || !!lineTrackDecl || !!lineSpanDecl;
11148
+ const needsWrapper = ctx.regexDecls.length > 0 || ctx.expectedDecls.length > 0 || ctx.namedFnDecls.length > 0 || !!mfDecl || !!buildDecl || !!emptyTlDecl || !!hostReadsDecl || !!rawEntryDecl || !!lineTrackDecl || !!lineSpanDecl;
10857
11149
  if (!needsWrapper) return innerFn;
10858
11150
  const namedPrelude = ctx.namedFnDecls.length > 0 ? namedFnPrelude() : [];
10859
11151
  const hoistedDecls = [
10860
11152
  emptyTlDecl,
10861
11153
  hostReadsDecl,
11154
+ rawEntryDecl,
10862
11155
  lineTrackDecl,
10863
11156
  lineSpanDecl,
10864
11157
  ...ctx.regexDecls.map((d) => ` ${d}`),
@@ -10928,11 +11221,12 @@ function childrenOf4(def) {
10928
11221
  return [def.main, def.skipped];
10929
11222
  case "scanTo":
10930
11223
  return [def.sentinel, ...def.skip];
11224
+ case "routed":
11225
+ return def.fallback ? [def.fallback] : [];
10931
11226
  case "lazy":
10932
11227
  case "literal":
10933
11228
  case "regex":
10934
11229
  case "keywords":
10935
- case "routed":
10936
11230
  case "guard":
10937
11231
  case "recover":
10938
11232
  case "unknown":
@@ -11014,7 +11308,7 @@ function evalRuleMapIR(ir) {
11014
11308
  l._def.fnSrc = src;
11015
11309
  return l;
11016
11310
  };
11017
- const _nd = (type, child, src, opts, staticError) => {
11311
+ const _nd = (type, child, src, opts, staticError, sigSrc) => {
11018
11312
  if (staticError !== void 0 && staticError.length > 0) {
11019
11313
  throw new Error(`IR direct node builder for ${type} must be macro-static and self-contained; unsupported binding(s): ${staticError.join(", ")}`);
11020
11314
  }
@@ -11023,6 +11317,7 @@ function evalRuleMapIR(ir) {
11023
11317
  }), opts);
11024
11318
  n._def.buildSrc = src;
11025
11319
  if (staticError !== void 0) n._def.buildStaticError = staticError;
11320
+ if (sigSrc !== void 0) n._def.buildSigSrc = sigSrc;
11026
11321
  return n;
11027
11322
  };
11028
11323
  const _gch = (items) => {
@@ -11335,7 +11630,7 @@ ${outerDecls.map((d) => " " + d).join("\n")}
11335
11630
  case "peek":
11336
11631
  return `peek(${kid(def.parser)})`;
11337
11632
  case "routed":
11338
- return "routed()";
11633
+ return def.fallback === void 0 ? "routed()" : `routed(${kid(def.fallback)})`;
11339
11634
  case "trivia":
11340
11635
  return `trivia(${kid(def.parser)})`;
11341
11636
  case "token":
@@ -11368,6 +11663,9 @@ ${outerDecls.map((d) => " " + d).join("\n")}
11368
11663
  ...projectEntry === void 0 ? [] : [projectEntry],
11369
11664
  ...def.captureTrivia ? ["captureTrivia: true"] : [],
11370
11665
  ...def.trailingTrivia ? ["trailingTrivia: true"] : [],
11666
+ // Declared or resolved arity. Carried so a re-lowered composed artifact does
11667
+ // not silently re-acquire the fail-open capture cost this module resolved away.
11668
+ ...def.buildArity === void 0 ? [] : [`buildArity: ${def.buildArity}`],
11371
11669
  ...tagEntry === void 0 ? [] : [tagEntry]
11372
11670
  ];
11373
11671
  const opts = optEntries.length > 0 ? `, { ${optEntries.join(", ")} }` : "";
@@ -11376,8 +11674,14 @@ ${outerDecls.map((d) => " " + d).join("\n")}
11376
11674
  return opts ? `node(${kid(def.parser)}, undefined${opts})` : `node(${kid(def.parser)})`;
11377
11675
  }
11378
11676
  if (def.buildSrc !== void 0) {
11379
- const staticError = def.buildStaticError === void 0 ? "" : `${opts ? "" : ", undefined"}, ${JSON.stringify(def.buildStaticError)}`;
11380
- return `_nd(${JSON.stringify(def.type)}, ${kid(def.parser)}, ${JSON.stringify(def.buildSrc)}${opts}${staticError})`;
11677
+ const tail = [];
11678
+ if (def.buildStaticError !== void 0 || def.buildSigSrc !== void 0) {
11679
+ if (!opts) tail.push("undefined");
11680
+ tail.push(def.buildStaticError === void 0 ? "undefined" : JSON.stringify(def.buildStaticError));
11681
+ }
11682
+ if (def.buildSigSrc !== void 0) tail.push(JSON.stringify(def.buildSigSrc));
11683
+ const trailing = tail.length > 0 ? `, ${tail.join(", ")}` : "";
11684
+ return `_nd(${JSON.stringify(def.type)}, ${kid(def.parser)}, ${JSON.stringify(def.buildSrc)}${opts}${trailing})`;
11381
11685
  }
11382
11686
  return opts ? `node(${JSON.stringify(def.type)}, ${kid(def.parser)}, undefined${opts})` : `node(${JSON.stringify(def.type)}, ${kid(def.parser)})`;
11383
11687
  }
@@ -11434,7 +11738,7 @@ function cstBuildHost(typeOrOptions, children2, _fields, span, rawChildren, triv
11434
11738
  return host;
11435
11739
  }
11436
11740
  cstBuildHost._parsemanCstOutput = true;
11437
- function fusedBody(pieces) {
11741
+ function fusedBody(pieces, hoist) {
11438
11742
  for (const p of pieces) {
11439
11743
  if (p.v === void 0) {
11440
11744
  throw new Error(
@@ -11458,12 +11762,14 @@ function fusedBody(pieces) {
11458
11762
  const contributingPieces = [...contributing];
11459
11763
  const needsEmptyTl = contributingPieces.some((p) => p.needsEmptyTl);
11460
11764
  const needsHostReads = contributingPieces.some((p) => p.needsHostReads);
11765
+ const needsRawEntry = contributingPieces.some((p) => p.needsRawEntry);
11461
11766
  const lines = [
11462
11767
  // Shared sentinel protocol (must match NAMED_FN_FAIL / NAMED_FN_END in codegen).
11463
11768
  "const _pfFail = {}",
11464
11769
  "let _pfEnd",
11465
11770
  ...needsEmptyTl ? ["const _EMPTY_TL = Object.freeze([])"] : [],
11466
11771
  ...needsHostReads ? [HOST_READS_DECL] : [],
11772
+ ...needsRawEntry ? [RAW_ENTRY_DECL] : [],
11467
11773
  // Each contributing artifact's namespaced private prelude (regexes, _pf, …).
11468
11774
  ...new Set(contributingPieces.flatMap((p) => p.prelude)),
11469
11775
  // The winning `_r_<Name>` function for each rule (one per name → no redeclare).
@@ -11472,8 +11778,7 @@ function fusedBody(pieces) {
11472
11778
  const wrapperEntries = [...winner].map(([k, p]) => `${JSON.stringify(k)}: ${p.wrappers.get(k)}`);
11473
11779
  const reflection = mergeGrammarReflections([...winner].map(([k, p]) => p.nodeMeta.get(k) ?? { nodes: [] }));
11474
11780
  const { mode, elided } = hostModeOfPieces(pieces);
11475
- const rawBody = [
11476
- ...lines,
11781
+ const tailSrc = [
11477
11782
  "const _map = {",
11478
11783
  wrapperEntries.join(",\n"),
11479
11784
  "}",
@@ -11489,6 +11794,7 @@ function fusedBody(pieces) {
11489
11794
  "}",
11490
11795
  "return _map"
11491
11796
  ].join("\n");
11797
+ const rawBody = [...lines, tailSrc].join("\n");
11492
11798
  const ANY2 = { kind: "any" };
11493
11799
  const EMPTY3 = { kind: "empty" };
11494
11800
  const recipes = /* @__PURE__ */ new Map();
@@ -11529,7 +11835,7 @@ function fusedBody(pieces) {
11529
11835
  }
11530
11836
  if (!changed) break;
11531
11837
  }
11532
- const body = rawBody.replace(
11838
+ const resolveFS = (src) => src.replace(
11533
11839
  // Rule name + code-point var are both JS identifiers (rule names are validated
11534
11840
  // at compile time — see assertRuleName in codegen), so an identifier class
11535
11841
  // matches every well-formed placeholder.
@@ -11540,6 +11846,7 @@ function fusedBody(pieces) {
11540
11846
  return `(${firstSetCond(codevar, fs)})`;
11541
11847
  }
11542
11848
  );
11849
+ const body = hoist === void 0 ? resolveFS(rawBody) : [...hoist.claim(lines.map(resolveFS)), resolveFS(tailSrc)].join("\n");
11543
11850
  const env = {};
11544
11851
  for (const p of contributingPieces) {
11545
11852
  if (p.mfFns.length) env[`${p.ns}mf`] = p.mfFns;
@@ -11579,17 +11886,6 @@ var COMPOSED_COVERAGE_RULES = /* @__PURE__ */ Symbol.for("parseman.composedCover
11579
11886
  function isIRPiece(p) {
11580
11887
  return !!p && typeof p === "object" && typeof p.ir === "string" && typeof p.ns === "string" && !("ruleFns" in p);
11581
11888
  }
11582
- function once(fn) {
11583
- let done = false;
11584
- let value;
11585
- return () => {
11586
- if (!done) {
11587
- value = fn();
11588
- done = true;
11589
- }
11590
- return value;
11591
- };
11592
- }
11593
11889
  function carriedRuleMapsDetailed(carried) {
11594
11890
  const maps = [];
11595
11891
  const opaque = [];
@@ -11706,12 +12002,6 @@ function compose(items, opts) {
11706
12002
  const used = /* @__PURE__ */ new Set();
11707
12003
  const trivia2 = composingTriviaOf(items);
11708
12004
  const carried = items.flatMap((item) => itemCarried(item, used, trivia2, opts?.hostMode));
11709
- const detailed = once(() => carriedRuleMapsDetailed(carried));
11710
- runFusedGatingDiagnostic(
11711
- () => detailed().maps,
11712
- void 0,
11713
- () => detailed().opaque
11714
- );
11715
12005
  const pieces = carried.map((p) => materializePiece(p, trivia2, false, opts?.hostMode));
11716
12006
  const map = fuseRules(pieces);
11717
12007
  Object.defineProperty(map, COMPOSED_PIECES, { value: carried, enumerable: false });
@@ -11720,8 +12010,168 @@ function compose(items, opts) {
11720
12010
  if (coverageRules) Object.defineProperty(map, COMPOSED_COVERAGE_RULES, { value: coverageRules, enumerable: false });
11721
12011
  return map;
11722
12012
  }
11723
- function composeLeaf(_items) {
11724
- throw new Error("composeLeaf(): requires Parseman macro lowering; runtime composition is forbidden");
12013
+ function composeLeaf(items) {
12014
+ const pieces = items.flatMap(interpretedPieces);
12015
+ let fused;
12016
+ const map = {};
12017
+ for (const name of ruleNamesOf(pieces)) {
12018
+ Object.defineProperty(map, name, {
12019
+ enumerable: true,
12020
+ configurable: true,
12021
+ get: () => (fused ??= fusePieces(pieces))[name]
12022
+ });
12023
+ }
12024
+ Object.defineProperty(map, LEAF_COMPOSED, { value: true, enumerable: false });
12025
+ Object.defineProperty(map, INTERPRETED_PIECES, {
12026
+ value: pieces.filter((p) => p.plain).map((p) => p.entries),
12027
+ enumerable: false
12028
+ });
12029
+ return map;
12030
+ }
12031
+ function isRefSlot(c) {
12032
+ return c._def.tag === "lazy" && typeof c.define === "function";
12033
+ }
12034
+ var FUSE_ORIGINAL = /* @__PURE__ */ Symbol.for("parseman.interpretedFuseOriginal");
12035
+ var FUSE_TARGET = /* @__PURE__ */ Symbol.for("parseman.interpretedFuseTarget");
12036
+ var INTERPRETED_PIECES = /* @__PURE__ */ Symbol.for("parseman.interpretedPieces");
12037
+ function isInterpretedFuse(map) {
12038
+ return Array.isArray(map[INTERPRETED_PIECES]);
12039
+ }
12040
+ function definitionOf(entry) {
12041
+ if (isRefSlot(entry)) {
12042
+ const original = entry[FUSE_ORIGINAL];
12043
+ if (original !== void 0) return original ?? void 0;
12044
+ try {
12045
+ return entry._def.thunk();
12046
+ } catch {
12047
+ return void 0;
12048
+ }
12049
+ }
12050
+ if (entry._def.tag === "lazy") {
12051
+ try {
12052
+ entry._def.thunk();
12053
+ } catch {
12054
+ return void 0;
12055
+ }
12056
+ }
12057
+ return entry;
12058
+ }
12059
+ function repointRef(slot, target, name) {
12060
+ const tagged = slot;
12061
+ const bound = tagged[FUSE_TARGET];
12062
+ const original = FUSE_ORIGINAL in tagged ? tagged[FUSE_ORIGINAL] : (() => {
12063
+ try {
12064
+ return slot._def.thunk();
12065
+ } catch {
12066
+ return null;
12067
+ }
12068
+ })();
12069
+ if ((bound ?? original) === target) return;
12070
+ if (bound !== void 0) {
12071
+ throw new Error(
12072
+ `fuseInterpreted: rule "${name}" is already bound by a DIFFERENT interpreted fusion of the same grammar piece. An interpreted fuse binds the shared placeholder objects in place, so two fusions cannot share a piece \u2014 build a fresh instance of the piece (call its rules() factory again, or import the module under a distinct specifier) for the second fusion.`
12073
+ );
12074
+ }
12075
+ if (!(FUSE_ORIGINAL in tagged)) Object.defineProperty(slot, FUSE_ORIGINAL, { value: original, enumerable: false });
12076
+ Object.defineProperty(slot, FUSE_TARGET, { value: target, enumerable: false });
12077
+ slot._def.thunk = () => target;
12078
+ slot.parse = (input, pos, ctx) => target.parse(input, pos, ctx);
12079
+ const meta = slot._meta;
12080
+ meta.firstSet = target._meta.firstSet;
12081
+ meta.canMatchNewline = target._meta.canMatchNewline;
12082
+ meta.isTrivia = target._meta.isTrivia;
12083
+ if (target._meta.triviaKindLabels !== void 0) meta.triviaKindLabels = target._meta.triviaKindLabels;
12084
+ else delete meta.triviaKindLabels;
12085
+ if (target._meta.disjoint !== void 0) meta.disjoint = target._meta.disjoint;
12086
+ else delete meta.disjoint;
12087
+ }
12088
+ function interpretedPieces(item) {
12089
+ const fused = item[INTERPRETED_PIECES];
12090
+ if (Array.isArray(fused)) return fused.map((entries) => ({ entries, plain: false }));
12091
+ const carried = composedPiecesOf(item);
12092
+ if (carried !== void 0) {
12093
+ const { maps, opaque } = carriedRuleMapsDetailed(carried);
12094
+ if (opaque.length > 0) {
12095
+ throw new Error(
12096
+ `fuseInterpreted: cannot interpret a composed grammar containing precompiled artifact(s) ${opaque.map((o) => `"${o.ns}" (${o.ruleNames.length} rules)`).join(", ")} \u2014 they carry compiled functions, not a combinator graph. Pass the source grammars (the same items you passed to compose()) instead.`
12097
+ );
12098
+ }
12099
+ return maps.map((entries) => ({ entries, plain: false }));
12100
+ }
12101
+ if (item.ruleFns instanceof Map) {
12102
+ throw new Error("fuseInterpreted: a precompiled linkable artifact has no combinator graph to interpret; pass the source grammar (a rules() map) instead");
12103
+ }
12104
+ return [{ entries: Object.entries(item), plain: true }];
12105
+ }
12106
+ function fuseInterpreted(items, opts) {
12107
+ return fusePieces(items.flatMap(interpretedPieces), opts);
12108
+ }
12109
+ function ruleNamesOf(pieces) {
12110
+ const names = /* @__PURE__ */ new Set();
12111
+ for (const piece of pieces) {
12112
+ for (const [name, value] of piece.entries) if (definitionOf(value) !== void 0) names.add(name);
12113
+ }
12114
+ return [...names];
12115
+ }
12116
+ function fusePieces(pieces, opts) {
12117
+ let trivia2;
12118
+ for (let i = pieces.length - 1; i >= 0 && trivia2 === void 0; i--) {
12119
+ if (!pieces[i].plain) continue;
12120
+ for (const [, rule] of pieces[i].entries) {
12121
+ const t = rule._meta.grammarTrivia;
12122
+ if (t) {
12123
+ trivia2 = t;
12124
+ break;
12125
+ }
12126
+ }
12127
+ }
12128
+ const winner = /* @__PURE__ */ new Map();
12129
+ const entry = /* @__PURE__ */ new Map();
12130
+ for (const piece of pieces) {
12131
+ for (const [name, value] of piece.entries) {
12132
+ const def = definitionOf(value);
12133
+ if (def === void 0) continue;
12134
+ winner.set(name, def);
12135
+ entry.set(name, value);
12136
+ }
12137
+ }
12138
+ const missing = /* @__PURE__ */ new Set();
12139
+ for (const piece of pieces) {
12140
+ for (const [name, value] of piece.entries) {
12141
+ if (!isRefSlot(value)) continue;
12142
+ const target = winner.get(name);
12143
+ if (target === void 0) {
12144
+ missing.add(name);
12145
+ continue;
12146
+ }
12147
+ repointRef(value, target, name);
12148
+ }
12149
+ }
12150
+ if (missing.size > 0) throw new Error(missingRuleMessage(pieces, missing));
12151
+ const out = {};
12152
+ for (const [name, value] of entry) {
12153
+ out[name] = value;
12154
+ const meta = value._meta;
12155
+ if (trivia2 !== void 0 && !meta.isTrivia) meta.grammarTrivia = trivia2;
12156
+ if (opts?.hostMode !== void 0 && !meta.isTrivia) {
12157
+ if (opts.hostMode === "cst") meta.grammarHostMode = "cst";
12158
+ else delete meta.grammarHostMode;
12159
+ }
12160
+ }
12161
+ Object.defineProperty(out, INTERPRETED_PIECES, {
12162
+ value: pieces.filter((p) => p.plain).map((p) => p.entries),
12163
+ enumerable: false
12164
+ });
12165
+ return out;
12166
+ }
12167
+ function missingRuleMessage(pieces, missing) {
12168
+ for (const piece of pieces) {
12169
+ const deps = ruleDependencies(piece.entries.filter(([, v2]) => definitionOf(v2) !== void 0));
12170
+ for (const [name, ds] of deps) {
12171
+ for (const d of ds) if (missing.has(d)) return `fuseInterpreted: rule "${name}" references missing rule "${d}"`;
12172
+ }
12173
+ }
12174
+ return `fuseInterpreted: missing rule(s) ${[...missing].map((n) => `"${n}"`).join(", ")}`;
11725
12175
  }
11726
12176
 
11727
12177
  // src/analysis/grammar.ts
@@ -11738,14 +12188,1123 @@ function analyzeGrammarGating(grammar, opts) {
11738
12188
  kind: "opaque-artifact",
11739
12189
  reason: `precompiled artifact "${o.ns}" carries compiled rule functions, not re-lowerable IR \u2014 its choices cannot be examined. Recompile the contributing grammar so it carries IR (the macro emits IR by default) to bring it back into analysis.`
11740
12190
  }));
12191
+ for (const o of opaque) {
12192
+ recordDegradation({
12193
+ code: "opaque-artifact",
12194
+ // `info`, not `warn`: the artifact is usually someone else's package, so the author
12195
+ // of THIS grammar has nothing to act on — but the blind spot still has to be counted.
12196
+ severity: "info",
12197
+ where: o.ruleNames.length > 0 ? o.ruleNames.join(", ") : `<artifact ${o.ns}>`,
12198
+ subject: `precompiled artifact "${o.ns}"`,
12199
+ fellBackTo: `it carries compiled rule functions rather than re-lowerable IR, so its ${o.ruleNames.length > 0 ? `${o.ruleNames.length} rule(s)` : "rules"} were NOT analysed and a clean gating report must not be read as a pass`,
12200
+ otherwise: "its choices would be walked like any other rule. Recompile the contributing grammar so it carries IR (the macro emits IR by default)"
12201
+ });
12202
+ }
11741
12203
  return fromOpaque.length === 0 ? report : { ...report, unanalysable: [...report.unanalysable, ...fromOpaque] };
11742
12204
  }
11743
12205
 
11744
- // src/combinators/gate.ts
11745
- function gate(predicate) {
11746
- const meta = {
11747
- firstSet: { kind: "any" },
11748
- canMatchNewline: false,
12206
+ // src/analysis/diagnose.ts
12207
+ var isCombinator = (g) => typeof g === "object" && g !== null && "_def" in g;
12208
+ var isRuleEntries = (g) => Array.isArray(g);
12209
+ function diagnoseGrammar(grammar, opts) {
12210
+ const depth = degradationCaptureDepth();
12211
+ beginDegradationCapture();
12212
+ let report;
12213
+ let degradations;
12214
+ try {
12215
+ report = runAnalysis(grammar, opts);
12216
+ } catch (e) {
12217
+ const reason = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
12218
+ report = emptyReport(`gating analysis threw \u2014 ${reason}`);
12219
+ } finally {
12220
+ degradations = unwindDegradationCapture(depth);
12221
+ }
12222
+ return assemble(report, degradations);
12223
+ }
12224
+ function examinedNothing(d) {
12225
+ return d.summary.totalChoices === 0 && d.summary.unanalysable > 0;
12226
+ }
12227
+ function formatGrammarDiagnosis(d) {
12228
+ const s = d.summary;
12229
+ const lines = [];
12230
+ lines.push(
12231
+ examinedNothing(d) ? `parseman: COULD NOT ANALYSE \u2014 0 choice(s) examined; ${s.unanalysable} rule(s) unreadable. No verdict about this grammar is available, good or bad.` : d.ok ? `parseman: grammar OK \u2014 ${s.gated}/${s.totalChoices} choice(s) gate on first char${s.recoverable > 0 ? `, ${s.recoverable} recoverable` : ""}${s.accepted > 0 ? `, ${s.accepted} accepted` : ""}${s.deferred > 0 ? `, ${s.deferred} deferred to the fusing artifact` : ""}.` : `parseman: grammar NOT OK \u2014 ${blockingOf(d).length - s.unanalysable} blocking finding(s) over ${s.totalChoices} examined choice(s).`
12232
+ );
12233
+ if (s.unanalysable > 0 && !examinedNothing(d)) {
12234
+ lines.push(
12235
+ ` ${s.unanalysable} rule(s) UNANALYSABLE \u2014 THIS REPORT IS PARTIAL. An empty finding list below does NOT mean the grammar is clean.`
12236
+ );
12237
+ }
12238
+ for (const f of d.findings) {
12239
+ lines.push(`${f.severity === "blocking" ? "\u2717" : "\xB7"} [${f.code}] ${f.id}: ${f.message}`);
12240
+ for (const detail of f.details) lines.push(` ${detail.replace(/\n/g, "\n ")}`);
12241
+ if (f.acceptKey !== void 0) {
12242
+ lines.push(` intentional? add to the gating snapshot: { accept: ['${f.acceptKey}'] }`);
12243
+ }
12244
+ }
12245
+ if (!d.ok && d.acceptSnapshot.length > 0) {
12246
+ lines.push(` accept-all snapshot: { accept: [${d.acceptSnapshot.map((i) => `'${i}'`).join(", ")}] }`);
12247
+ }
12248
+ return lines;
12249
+ }
12250
+ function runAnalysis(grammar, opts) {
12251
+ if (isCombinator(grammar)) return analyzeGating(grammar, opts);
12252
+ if (isRuleEntries(grammar)) return analyzeGatingRules(grammar, opts);
12253
+ return analyzeGrammarGating(grammar, opts);
12254
+ }
12255
+ function emptyReport(reason) {
12256
+ return {
12257
+ totalChoices: 0,
12258
+ gated: 0,
12259
+ recoverable: 0,
12260
+ unanalysable: [{ rule: "<whole grammar>", kind: "not-a-combinator", reason }],
12261
+ ungated: [],
12262
+ accepted: [],
12263
+ deferred: [],
12264
+ acceptedUnused: [],
12265
+ choices: [],
12266
+ antiPatterns: []
12267
+ };
12268
+ }
12269
+ var blockingOf = (d) => d.findings.filter((f) => f.severity === "blocking");
12270
+ var SEVERITY_RANK = { blocking: 0, advisory: 1 };
12271
+ var CODE_RANK = {
12272
+ unanalysable: 0,
12273
+ "ungated-choice": 1,
12274
+ "anti-pattern": 2,
12275
+ degraded: 3,
12276
+ "stale-accept": 4
12277
+ };
12278
+ function assemble(report, degradations) {
12279
+ const findings = [];
12280
+ for (const u of report.unanalysable) {
12281
+ findings.push({
12282
+ id: u.rule,
12283
+ code: "unanalysable",
12284
+ severity: "blocking",
12285
+ rule: u.rule,
12286
+ message: `could not be examined [${u.kind}] \u2014 no verdict about it is available`,
12287
+ details: [u.reason]
12288
+ });
12289
+ }
12290
+ for (const c of report.ungated) {
12291
+ findings.push({
12292
+ id: c.id,
12293
+ code: "ungated-choice",
12294
+ severity: "blocking",
12295
+ rule: c.rule,
12296
+ message: `choice is UNGATED [${c.strategy}] \u2014 no first-char dispatch; every position speculatively enters doomed arms`,
12297
+ details: [
12298
+ ...c.anyArms.map((a) => `arm[${a.index}] first-set ANY (${a.cause}): ${a.detail}
12299
+ fix: ${a.suggestion}`),
12300
+ ...c.overlaps.map((o) => `arm[${o.a}] \u2229 arm[${o.b}] overlap on ${firstSetToString(o.on)}
12301
+ fix: ${o.suggestion}`)
12302
+ ],
12303
+ acceptKey: c.id
12304
+ });
12305
+ }
12306
+ for (const ap of report.antiPatterns) {
12307
+ findings.push({
12308
+ id: `${ap.rule}#arm${ap.armIndex}`,
12309
+ code: "anti-pattern",
12310
+ severity: "blocking",
12311
+ rule: ap.rule,
12312
+ message: `[${ap.kind}] ${ap.message}`,
12313
+ details: []
12314
+ });
12315
+ }
12316
+ for (const d of degradations) {
12317
+ findings.push({
12318
+ // `where` + `code` is the degradation channel's own identity; reuse it so the two
12319
+ // surfaces name the same thing the same way.
12320
+ id: `${d.code}@${d.where}`,
12321
+ code: "degraded",
12322
+ // Mirrors `DegradationSeverity`: 'warn' is a real cost the author can remove,
12323
+ // 'info' is a fact they cannot act on (someone else's package).
12324
+ severity: d.severity === "warn" ? "blocking" : "advisory",
12325
+ rule: d.where,
12326
+ message: formatDegradation(d),
12327
+ details: []
12328
+ });
12329
+ }
12330
+ for (const id of report.acceptedUnused) {
12331
+ findings.push({
12332
+ id,
12333
+ code: "stale-accept",
12334
+ severity: "advisory",
12335
+ rule: id,
12336
+ message: "accept entry matched no ungated choice \u2014 the grammar was fixed; prune this line",
12337
+ details: []
12338
+ });
12339
+ }
12340
+ findings.sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] || CODE_RANK[a.code] - CODE_RANK[b.code] || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
12341
+ return {
12342
+ schema: "parseman.diagnosis/1",
12343
+ ok: !findings.some((f) => f.severity === "blocking"),
12344
+ summary: {
12345
+ totalChoices: report.totalChoices,
12346
+ gated: report.gated,
12347
+ recoverable: report.recoverable,
12348
+ ungated: report.ungated.length,
12349
+ accepted: report.accepted.length,
12350
+ deferred: report.deferred.length,
12351
+ antiPatterns: report.antiPatterns.length,
12352
+ unanalysable: report.unanalysable.length,
12353
+ degraded: degradations.length,
12354
+ staleAccepts: report.acceptedUnused.length
12355
+ },
12356
+ findings,
12357
+ acceptSnapshot: report.ungated.map((c) => c.id).sort(),
12358
+ gating: report,
12359
+ degradations
12360
+ };
12361
+ }
12362
+
12363
+ // src/functional/run.ts
12364
+ var SELECT_EXAMPLE = "run(entry, input, { rootTrivia: { select: ['blockComment', 'lineComment'] } })";
12365
+ var REMOVED_RUN_RESULT_FIELDS = [
12366
+ [
12367
+ "triviaMap",
12368
+ `It was a dense root-trivia index built on every parse; root trivia is now an OPT-IN sparse capture, because most grammars paid for an index they never read. Migration: name the trivia labels your grammar defines \u2014 ${SELECT_EXAMPLE} \u2014 and read \`RunResult.rootTrivia\`, whose \`.index\` carries the labels and gap lookups \`triviaMap\` exposed.`
12369
+ ],
12370
+ [
12371
+ "triviaLog",
12372
+ `It was the flat \`start, end[, kindIndex]\` log every other trivia view was derived from, and it is no longer allocated at all \u2014 not empty, absent. Migration: request the labels you need \u2014 ${SELECT_EXAMPLE} \u2014 and read \`RunResult.rootTrivia.rows\`. Do NOT reuse a stride-2/3 reader: rows are a different width and a different unit (one row per SELECTED marker, not one per trivia chunk), so an old loop reads confidently wrong offsets rather than failing.`
12373
+ ],
12374
+ [
12375
+ "triviaKindLabels",
12376
+ `It was already optional, so nothing ever flagged its disappearance \u2014 and \`triviaKindMask(undefined, \u2026)\` means "capture everything", so reading it now silently WIDENS capture instead of erroring. Migration: request labels \u2014 ${SELECT_EXAMPLE} \u2014 then the set you asked for is \`RunResult.rootTrivia.select\` and the resolved table is \`RunResult.rootTrivia.index.labels\`.`
12377
+ ]
12378
+ ];
12379
+ function guardRemovedFields(result) {
12380
+ for (const [name, detail] of REMOVED_RUN_RESULT_FIELDS) {
12381
+ Object.defineProperty(result, name, {
12382
+ configurable: true,
12383
+ enumerable: false,
12384
+ get() {
12385
+ throw new TypeError(
12386
+ `RunResult.${name} was REMOVED in parseman 0.44.0 and has no default replacement. ${detail}`
12387
+ );
12388
+ }
12389
+ });
12390
+ }
12391
+ return result;
12392
+ }
12393
+ var invoke = (r, input, pos, ctx) => typeof r === "function" ? r(input, pos, ctx) : r.parse(input, pos, ctx);
12394
+ function triviaKindLabelsFromRunnable(r) {
12395
+ if (!r) return void 0;
12396
+ if (typeof r === "function") return r._meta?.triviaKindLabels;
12397
+ const grammarTrivia = r._meta.grammarTrivia;
12398
+ return grammarTrivia?._meta.triviaKindLabels ?? r._meta.triviaKindLabels ?? (r._def.tag === "grammar" ? r._def.triviaParser?._meta.triviaKindLabels : void 0);
12399
+ }
12400
+ function rootTriviaClassifiedFromRunnable(r) {
12401
+ if (!r) return false;
12402
+ if (typeof r === "function") return r._meta?.rootTriviaClassified === true;
12403
+ const grammarTrivia = r._meta.grammarTrivia;
12404
+ return grammarTrivia?._meta.rootTriviaClassified === true || r._meta.rootTriviaClassified === true;
12405
+ }
12406
+ function makeSelectedRootLabelIndex(labels) {
12407
+ const index = /* @__PURE__ */ Object.create(null);
12408
+ for (let i = 0; i < labels.length; i++) {
12409
+ if (index[labels[i]] === void 0) index[labels[i]] = i;
12410
+ }
12411
+ return index;
12412
+ }
12413
+ function runOnce(entry, input, options, phase, profileState) {
12414
+ if (typeof entry !== "function" && typeof entry?.parse !== "function") {
12415
+ throw new TypeError(
12416
+ `run(): start production is ${entry === null ? "null" : typeof entry}, not a rule \u2014 the requested grammar rule does not exist (check the rule name).`
12417
+ );
12418
+ }
12419
+ const rootTriviaSelection = options.rootTrivia?.select;
12420
+ const captureRootTrivia = rootTriviaSelection !== void 0 && rootTriviaSelection.length > 0;
12421
+ const selectedRootLog = captureRootTrivia ? [] : void 0;
12422
+ const selectedRootLabelIndex = captureRootTrivia ? makeSelectedRootLabelIndex(rootTriviaSelection) : void 0;
12423
+ const errors = [];
12424
+ const profile = profileState ?? (phase === void 0 ? void 0 : { phase, nodes: 0, childSlots: 0, rawSlots: 0, triviaSlots: 0, fieldSlots: 0, hostCalls: 0 });
12425
+ const skipGlobalSinks = phase === "recognizer" || phase === "capture";
12426
+ const grammarTrivia = typeof entry !== "function" ? entry._meta.grammarTrivia : void 0;
12427
+ const grammarScanSkip = typeof entry !== "function" ? entry._meta.grammarScanSkip : void 0;
12428
+ const triviaKindLabels = triviaKindLabelsFromRunnable(entry) ?? triviaKindLabelsFromRunnable(options.trivia);
12429
+ if (captureRootTrivia && triviaKindLabels === void 0) {
12430
+ throw new TypeError("run(): rootTrivia.select requires labeled grammar trivia.");
12431
+ }
12432
+ if (captureRootTrivia) {
12433
+ for (const label2 of rootTriviaSelection) {
12434
+ if (!triviaKindLabels.includes(label2)) {
12435
+ throw new TypeError(`run(): rootTrivia.select contains unknown trivia label ${JSON.stringify(label2)}.`);
12436
+ }
12437
+ }
12438
+ }
12439
+ if (captureRootTrivia && !rootTriviaClassifiedFromRunnable(entry) && !rootTriviaClassifiedFromRunnable(options.trivia)) {
12440
+ throw new TypeError(
12441
+ "run(): rootTrivia.select requires classifiedTrivia() at the root."
12442
+ );
12443
+ }
12444
+ const stamped = entry;
12445
+ assertHostModeCompatible(
12446
+ typeof entry === "function" ? stamped[FUSED_HOST_MODE] ?? "ast" : entry._meta.grammarHostMode ?? "ast",
12447
+ options.build,
12448
+ typeof entry === "function" && stamped[FUSED_HOST_ELIDED] === true
12449
+ );
12450
+ const ctx = {
12451
+ trackLines: false,
12452
+ ...skipGlobalSinks ? { _pmProfile: profile } : {
12453
+ ...captureRootTrivia ? {
12454
+ _rootTriviaLog: selectedRootLog,
12455
+ _rootTriviaKindIndex: selectedRootLabelIndex,
12456
+ _rootTriviaStrictScopes: true
12457
+ } : {},
12458
+ _errors: errors,
12459
+ ...profile === void 0 ? {} : { _pmProfile: profile }
12460
+ },
12461
+ build: options.build,
12462
+ state: options.state,
12463
+ ...grammarTrivia !== void 0 ? { trivia: grammarTrivia, ...triviaKindLabels ? { triviaKindLabels } : {} } : triviaKindLabels ? { triviaKindLabels } : {},
12464
+ ...grammarScanSkip !== void 0 ? { scanSkip: grammarScanSkip } : {},
12465
+ ...options.triviaCaptureMask !== void 0 ? { _triviaCaptureMask: options.triviaCaptureMask } : {},
12466
+ ...options.tolerant ? { _tolerant: true, _rec: REC } : {},
12467
+ ...options.instrumentation === void 0 ? {} : options.instrumentation
12468
+ };
12469
+ const r = invoke(entry, input, 0, ctx);
12470
+ let unconsumedFrom = null;
12471
+ if (r.ok) {
12472
+ let pos = r.span?.end ?? 0;
12473
+ if (options.trivia && pos < input.length) {
12474
+ const t = invoke(options.trivia, input, pos, { trackLines: false });
12475
+ if (t.ok && t.span.end > pos) pos = t.span.end;
12476
+ }
12477
+ unconsumedFrom = pos < input.length ? pos : null;
12478
+ }
12479
+ return {
12480
+ ok: r.ok,
12481
+ value: r.ok ? r.value : void 0,
12482
+ span: r.span ?? { start: 0, end: 0 },
12483
+ expected: r.ok ? [] : r.expected ?? [],
12484
+ errors,
12485
+ ...selectedRootLog !== void 0 && selectedRootLog.length > 0 ? {
12486
+ rootTrivia: {
12487
+ rows: selectedRootLog,
12488
+ select: rootTriviaSelection,
12489
+ index: buildRootTriviaIndex(selectedRootLog, rootTriviaSelection)
12490
+ }
12491
+ } : {},
12492
+ unconsumedFrom
12493
+ };
12494
+ }
12495
+ function run(entry, input, options = {}) {
12496
+ if (!options.profile) return guardRemovedFields(runOnce(entry, input, options));
12497
+ throw new TypeError(
12498
+ "run({ profile: true }) is unavailable: profiling counters are no longer compiled into parser artifacts, and the interpreter does not implement them yet. Profiling is moving to interpreted mode."
12499
+ );
12500
+ }
12501
+
12502
+ // src/analysis/choice-cost.ts
12503
+ var choiceSiteKey = (s) => s.path === "" ? s.rule : `${s.rule} \u203A ${s.path}`;
12504
+ function childrenOf5(d) {
12505
+ switch (d.tag) {
12506
+ case "sequence":
12507
+ case "choice":
12508
+ return d.parsers;
12509
+ case "dispatch":
12510
+ return [
12511
+ d.selector,
12512
+ ...d.cases.map((c) => c.parser),
12513
+ ...d.otherwise === void 0 ? [] : [d.otherwise]
12514
+ ];
12515
+ case "skip":
12516
+ return [d.main, d.skipped];
12517
+ case "sepBy":
12518
+ return [d.parser, d.separator];
12519
+ case "recover":
12520
+ return [d.parser, d.sentinel];
12521
+ case "scanTo":
12522
+ return [d.sentinel, ...d.skip];
12523
+ case "grammar":
12524
+ return d.triviaParser ? [d.parser, d.triviaParser] : [d.parser];
12525
+ case "lazy":
12526
+ case "literal":
12527
+ case "regex":
12528
+ case "keywords":
12529
+ case "guard":
12530
+ case "unknown":
12531
+ return [];
12532
+ default: {
12533
+ const rec = d;
12534
+ return rec.parser ? [rec.parser] : [];
12535
+ }
12536
+ }
12537
+ }
12538
+ var ruleNameOf5 = (p) => p._ruleName;
12539
+ function slotLabel(d, index) {
12540
+ switch (d.tag) {
12541
+ case "sequence":
12542
+ return `seq[${index}]`;
12543
+ case "choice":
12544
+ return `choice[${index}]`;
12545
+ case "dispatch":
12546
+ return index === 0 ? "dispatch.selector" : `dispatch[${index - 1}]`;
12547
+ case "skip":
12548
+ return index === 0 ? "skip.main" : "skip.skipped";
12549
+ case "sepBy":
12550
+ return index === 0 ? "sepBy.item" : "sepBy.sep";
12551
+ case "recover":
12552
+ return index === 0 ? "recover.parser" : "recover.sentinel";
12553
+ case "scanTo":
12554
+ return index === 0 ? "scanTo.sentinel" : `scanTo.skip[${index - 1}]`;
12555
+ case "grammar":
12556
+ return index === 0 ? "grammar" : "grammar.trivia";
12557
+ case "node":
12558
+ return `node(${d.type ?? "anonymous"})`;
12559
+ case "label":
12560
+ return `label(${d.label})`;
12561
+ case "field":
12562
+ return `field(${d.name})`;
12563
+ default:
12564
+ return d.tag;
12565
+ }
12566
+ }
12567
+ function resolveRoot(p) {
12568
+ let cur = p;
12569
+ for (let hop = 0; hop < 8; hop++) {
12570
+ const d = cur._def;
12571
+ if (d.tag !== "lazy") return { ok: true, p: cur };
12572
+ let next;
12573
+ try {
12574
+ next = d.thunk();
12575
+ } catch {
12576
+ return { ok: false };
12577
+ }
12578
+ if (next === cur) return { ok: false };
12579
+ cur = next;
12580
+ }
12581
+ return { ok: false };
12582
+ }
12583
+ function walkRules(ruleMap) {
12584
+ assertAnalysable(ruleMap);
12585
+ const out = [];
12586
+ const seen = /* @__PURE__ */ new Set();
12587
+ const unresolvedRoots = [];
12588
+ const visit = (p, rule, path) => {
12589
+ if (seen.has(p)) return;
12590
+ seen.add(p);
12591
+ const d = p._def;
12592
+ const own = ruleNameOf5(p);
12593
+ const here = own !== void 0 && own !== rule ? { rule: own, path: "" } : { rule, path };
12594
+ out.push({ p, d, site: here });
12595
+ const kids = childrenOf5(d);
12596
+ for (let i = 0; i < kids.length; i++) {
12597
+ const seg = slotLabel(d, i);
12598
+ visit(kids[i], here.rule, here.path === "" ? seg : `${here.path} \u203A ${seg}`);
12599
+ }
12600
+ };
12601
+ for (const [name, rule] of ruleMap) {
12602
+ const r = resolveRoot(rule);
12603
+ if (!r.ok) {
12604
+ unresolvedRoots.push(name);
12605
+ continue;
12606
+ }
12607
+ visit(r.p, name, "");
12608
+ }
12609
+ unresolvedRoots.sort();
12610
+ return { walked: out, unresolvedRoots };
12611
+ }
12612
+ function assertAnalysable(ruleMap) {
12613
+ if (ruleMap.length === 0) {
12614
+ throw new TypeError(
12615
+ "choice-cost: the rule map is EMPTY. Analysing nothing and reporting no findings is indistinguishable from analysing a clean grammar, so this is refused rather than answered. Pass the rules() map (or the map given to compileRuleMap/compileLinkable)."
12616
+ );
12617
+ }
12618
+ for (const [name, rule] of ruleMap) {
12619
+ const def = rule?._def;
12620
+ if (def !== null && typeof def === "object" && typeof def.tag === "string") continue;
12621
+ throw new TypeError(
12622
+ `choice-cost: rule '${name}' is not a combinator (no _def). This analysis walks the COMBINATOR TREE; the value returned by compose()/composeLeaf() is a fused artifact whose entries are parse functions. Pass the rules() map itself instead.`
12623
+ );
12624
+ }
12625
+ }
12626
+ function leadingTermOfArm2(arm) {
12627
+ let d = arm._def;
12628
+ for (; ; ) {
12629
+ if (d.tag === "node" || d.tag === "grammar" || d.tag === "transform" || d.tag === "label") {
12630
+ d = d.parser._def;
12631
+ continue;
12632
+ }
12633
+ break;
12634
+ }
12635
+ if (d.tag !== "sequence") {
12636
+ return {
12637
+ reason: "not-a-sequence",
12638
+ detail: `arm is \`${d.tag}\`; only node/grammar/transform/label wrappers are peeled, because they are the ones that consume nothing before the sequence`
12639
+ };
12640
+ }
12641
+ if (d.parsers.length < 2) {
12642
+ return { reason: "sequence-shorter-than-2", detail: `sequence has ${d.parsers.length} term(s); nothing would remain after factoring` };
12643
+ }
12644
+ return { term: d.parsers[0] };
12645
+ }
12646
+ function leadingTermKey(term) {
12647
+ const d = term._def;
12648
+ if (d.tag === "literal") {
12649
+ if (d.caseInsensitive) {
12650
+ return { reason: "lead-case-insensitive-literal", detail: `literal ${JSON.stringify(d.value)} is case-insensitive; the matched text can differ from the literal's own value` };
12651
+ }
12652
+ return { key: `L:${JSON.stringify(d.value)}`, render: JSON.stringify(d.value) };
12653
+ }
12654
+ if (d.tag === "regex") {
12655
+ return { key: `R:${JSON.stringify([d.source, d.flags])}`, render: `/${d.source}/${d.flags.replace(/[gy]/g, "")}` };
12656
+ }
12657
+ return {
12658
+ reason: "lead-not-concrete-terminal",
12659
+ detail: `leading term is \`${d.tag}\`${d.tag === "lazy" ? ` (ref${ruleNameOf5(term) === void 0 ? "" : ` to ${ruleNameOf5(term)}`})` : ""}; factoring through it would change the arm's value or capture shape`
12660
+ };
12661
+ }
12662
+ function armLabel(arm) {
12663
+ const name = ruleNameOf5(arm);
12664
+ if (name !== void 0) return name;
12665
+ let d = arm._def;
12666
+ for (let hop = 0; hop < 8; hop++) {
12667
+ if (d.tag === "node" && d.type !== void 0) return `node(${d.type})`;
12668
+ if (d.tag === "label") return `label(${d.label})`;
12669
+ if (d.tag === "field") return `field(${d.name})`;
12670
+ if (d.tag === "literal") return JSON.stringify(d.value);
12671
+ if (d.tag === "regex") return `/${d.source}/`;
12672
+ if (d.tag === "keywords") return `keywords(${d.words.length})`;
12673
+ if (d.tag === "sequence" || d.tag === "choice") {
12674
+ const first = d.parsers[0];
12675
+ if (first === void 0) return d.tag;
12676
+ const inner = ruleNameOf5(first);
12677
+ if (inner !== void 0) return `${d.tag}(${inner}, \u2026)`;
12678
+ d = first._def;
12679
+ continue;
12680
+ }
12681
+ const rec = d;
12682
+ if (rec.parser === void 0) return d.tag;
12683
+ d = rec.parser._def;
12684
+ }
12685
+ return arm._def.tag;
12686
+ }
12687
+ function analyzeChoiceInventory(ruleMap) {
12688
+ const { walked, unresolvedRoots } = walkRules(ruleMap);
12689
+ const entries = [];
12690
+ for (const w of walked) {
12691
+ if (w.d.tag !== "choice") continue;
12692
+ const d = w.d;
12693
+ const arity = d.parsers.length;
12694
+ const strategy = d.strategy.tag;
12695
+ const gated = d.gates.some((g) => g !== null);
12696
+ const byKey = /* @__PURE__ */ new Map();
12697
+ const armDeclines = [];
12698
+ for (let i = 0; i < arity; i++) {
12699
+ const lead = leadingTermOfArm2(d.parsers[i]);
12700
+ if ("reason" in lead) {
12701
+ armDeclines.push({ arm: i, reason: lead.reason, detail: lead.detail });
12702
+ continue;
12703
+ }
12704
+ const keyed = leadingTermKey(lead.term);
12705
+ if ("reason" in keyed) {
12706
+ armDeclines.push({ arm: i, reason: keyed.reason, detail: keyed.detail });
12707
+ continue;
12708
+ }
12709
+ const slot = byKey.get(keyed.key);
12710
+ if (slot === void 0) byKey.set(keyed.key, { render: keyed.render, members: [i] });
12711
+ else slot.members.push(i);
12712
+ }
12713
+ const groups = [...byKey.entries()].filter(([, v2]) => v2.members.length >= 2).map(([key, v2]) => ({ key, render: v2.render, members: v2.members })).sort((a, b) => b.members.length - a.members.length || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
12714
+ const factored = strategy === "sharedPrefix";
12715
+ let declineReason;
12716
+ if (!factored) {
12717
+ declineReason = arity < 2 ? "fewer-than-two-arms" : d.disjoint ? "disjoint-dispatch" : gated ? "gated-arms" : strategy !== "firstMatch" ? "strategy-preempted" : armDeclines.length > 0 ? "arms-not-factorable" : "leads-differ";
12718
+ }
12719
+ const inGroups = /* @__PURE__ */ new Set();
12720
+ for (const g of groups) for (const m of g.members) inGroups.add(m);
12721
+ entries.push({
12722
+ site: w.site,
12723
+ siteKey: choiceSiteKey(w.site),
12724
+ arity,
12725
+ strategy,
12726
+ disjoint: d.disjoint,
12727
+ gated,
12728
+ groups,
12729
+ armDeclines,
12730
+ factored,
12731
+ ...declineReason === void 0 ? {} : { declineReason },
12732
+ unfactoredArms: factored ? 0 : inGroups.size
12733
+ });
12734
+ }
12735
+ entries.sort((a, b) => a.siteKey < b.siteKey ? -1 : a.siteKey > b.siteKey ? 1 : 0);
12736
+ const backlog = entries.filter((e) => !e.factored && e.groups.length > 0 && e.declineReason !== "disjoint-dispatch" && e.declineReason !== "strategy-preempted");
12737
+ return {
12738
+ schema: "parseman.choice-inventory/1",
12739
+ rules: ruleMap.length,
12740
+ choiceSites: entries.length,
12741
+ factoredSites: entries.filter((e) => e.factored).length,
12742
+ backlogSites: backlog.length,
12743
+ backlogArms: backlog.reduce((n, e) => n + e.unfactoredArms, 0),
12744
+ unresolvedRoots,
12745
+ entries
12746
+ };
12747
+ }
12748
+ function compiledFirstCharGate(p) {
12749
+ const fs = p._meta.firstSet;
12750
+ if (fs.kind !== "any" && !canMatchEmptyAtStart2(p)) return fs;
12751
+ if (fs.kind !== "any" || canMatchEmptyAtStart2(p)) return null;
12752
+ let deep;
12753
+ try {
12754
+ deep = firstSetOf(p);
12755
+ } catch {
12756
+ return null;
12757
+ }
12758
+ return deep.kind === "ranges" ? deep : null;
12759
+ }
12760
+ function canMatchEmptyAtStart2(p) {
12761
+ const d = p._def;
12762
+ switch (d.tag) {
12763
+ case "regex":
12764
+ return regexMatchesEmpty2(d.source, d.flags);
12765
+ case "optional":
12766
+ case "many":
12767
+ return true;
12768
+ case "transform":
12769
+ case "label":
12770
+ case "field":
12771
+ return canMatchEmptyAtStart2(d.parser);
12772
+ case "literal":
12773
+ return d.value.length === 0;
12774
+ default:
12775
+ return false;
12776
+ }
12777
+ }
12778
+ function regexMatchesEmpty2(source, flags) {
12779
+ try {
12780
+ return new RegExp(`^(?:${source})$`, flags.replace(/[gy]/g, "")).test("");
12781
+ } catch {
12782
+ return true;
12783
+ }
12784
+ }
12785
+ function inSet(code, fs) {
12786
+ if (fs.kind === "any") return true;
12787
+ if (fs.kind === "empty") return false;
12788
+ for (const r of fs.ranges) if (code >= r.lo && code <= r.hi) return true;
12789
+ return false;
12790
+ }
12791
+ var TERMINAL_TAGS = /* @__PURE__ */ new Set(["literal", "regex", "keywords", "scanTo"]);
12792
+ var INSTRUMENTABLE = /* @__PURE__ */ new Set(["firstMatch", "sharedPrefix"]);
12793
+ function profileWastedWork(opts) {
12794
+ const { rules: rules2, corpus } = opts;
12795
+ const { walked, unresolvedRoots } = walkRules(rules2);
12796
+ const entryParser = typeof opts.entry === "string" ? rules2.find(([n]) => n === opts.entry)?.[1] : opts.entry;
12797
+ if (entryParser === void 0) {
12798
+ throw new TypeError(
12799
+ `choice-cost: entry rule '${String(opts.entry)}' is not in the rule map (have: ${rules2.map(([n]) => n).slice(0, 12).join(", ")}${rules2.length > 12 ? ", \u2026" : ""}).`
12800
+ );
12801
+ }
12802
+ if (corpus.length === 0) {
12803
+ throw new TypeError("choice-cost: the corpus is EMPTY. A profile over no input measures nothing; refusing to report zero wasted bytes as a result.");
12804
+ }
12805
+ for (const f of corpus) {
12806
+ if (f.text.length === 0) throw new TypeError(`choice-cost: corpus entry '${f.id}' is EMPTY.`);
12807
+ }
12808
+ let highWater = 0;
12809
+ const armRecords = [];
12810
+ const siteTotals = /* @__PURE__ */ new Map();
12811
+ const restore = [];
12812
+ const undoAll = () => {
12813
+ for (let i = restore.length - 1; i >= 0; i--) restore[i]();
12814
+ };
12815
+ let instrumentedSites = 0;
12816
+ let uninstrumentableSites = 0;
12817
+ try {
12818
+ for (const w of walked) {
12819
+ if (w.d.tag !== "choice") continue;
12820
+ const siteKey = choiceSiteKey(w.site);
12821
+ const strategy = w.d.strategy.tag;
12822
+ const canInstrument = INSTRUMENTABLE.has(strategy) && !w.d.disjoint;
12823
+ const rec = {
12824
+ siteKey,
12825
+ site: w.site,
12826
+ strategy,
12827
+ arity: w.d.parsers.length,
12828
+ instrumented: canInstrument,
12829
+ attempts: 0,
12830
+ failures: 0,
12831
+ wastedBytes: 0,
12832
+ gatedAttempts: 0,
12833
+ gatedFailures: 0,
12834
+ gatedWastedBytes: 0
12835
+ };
12836
+ if (siteTotals.has(siteKey)) {
12837
+ uninstrumentableSites++;
12838
+ continue;
12839
+ }
12840
+ siteTotals.set(siteKey, rec);
12841
+ if (!canInstrument) {
12842
+ uninstrumentableSites++;
12843
+ continue;
12844
+ }
12845
+ instrumentedSites++;
12846
+ const slots = w.d.parsers;
12847
+ for (let i = 0; i < slots.length; i++) {
12848
+ const original = slots[i];
12849
+ const gate2 = compiledFirstCharGate(original);
12850
+ const armRec = {
12851
+ site: w.site,
12852
+ siteKey,
12853
+ arm: i,
12854
+ label: armLabel(original),
12855
+ gate: gate2,
12856
+ attempts: 0,
12857
+ failures: 0,
12858
+ wastedBytes: 0,
12859
+ gatedAttempts: 0,
12860
+ gatedFailures: 0,
12861
+ gatedWastedBytes: 0
12862
+ };
12863
+ armRecords.push(armRec);
12864
+ const wrapper = {
12865
+ _tag: original._tag,
12866
+ _meta: original._meta,
12867
+ _def: original._def,
12868
+ parse(input, pos, ctx) {
12869
+ const code = pos < input.length ? input.codePointAt(pos) ?? -1 : -1;
12870
+ const entered = gate2 === null || code >= 0 && inSet(code, gate2);
12871
+ const saved = highWater;
12872
+ highWater = pos;
12873
+ const r = original.parse(input, pos, ctx);
12874
+ const reach = highWater;
12875
+ highWater = saved > reach ? saved : reach;
12876
+ armRec.attempts++;
12877
+ rec.attempts++;
12878
+ if (entered) {
12879
+ armRec.gatedAttempts++;
12880
+ rec.gatedAttempts++;
12881
+ }
12882
+ if (!r.ok) {
12883
+ armRec.failures++;
12884
+ rec.failures++;
12885
+ if (entered) {
12886
+ armRec.gatedFailures++;
12887
+ rec.gatedFailures++;
12888
+ }
12889
+ const wasted = reach - pos;
12890
+ if (wasted > 0) {
12891
+ armRec.wastedBytes += wasted;
12892
+ rec.wastedBytes += wasted;
12893
+ if (entered) {
12894
+ armRec.gatedWastedBytes += wasted;
12895
+ rec.gatedWastedBytes += wasted;
12896
+ }
12897
+ }
12898
+ }
12899
+ return r;
12900
+ }
12901
+ };
12902
+ const rn = ruleNameOf5(original);
12903
+ if (rn !== void 0) wrapper._ruleName = rn;
12904
+ slots[i] = wrapper;
12905
+ restore.push(() => {
12906
+ slots[i] = original;
12907
+ });
12908
+ }
12909
+ }
12910
+ if (instrumentedSites === 0) {
12911
+ throw new TypeError(
12912
+ "choice-cost: the grammar contains NO instrumentable choice site (no `firstMatch` or `sharedPrefix` choice). A wasted-work profile over it would report zero for a reason that has nothing to do with the grammar being efficient, so it is refused."
12913
+ );
12914
+ }
12915
+ for (const w of walked) {
12916
+ if (!TERMINAL_TAGS.has(w.d.tag)) continue;
12917
+ const p = w.p;
12918
+ const original = p.parse;
12919
+ p.parse = ((input, pos, ctx) => {
12920
+ if (pos > highWater) highWater = pos;
12921
+ return original.call(w.p, input, pos, ctx);
12922
+ });
12923
+ restore.push(() => {
12924
+ p.parse = original;
12925
+ });
12926
+ }
12927
+ } catch (e) {
12928
+ undoAll();
12929
+ throw e;
12930
+ }
12931
+ let parsedOk = 0;
12932
+ let parsedFailed = 0;
12933
+ let corpusBytes = 0;
12934
+ try {
12935
+ const runner = opts.runner ?? ((e, input, options) => run(e, input, options));
12936
+ for (const f of corpus) {
12937
+ corpusBytes += f.text.length;
12938
+ highWater = 0;
12939
+ const r = runner(entryParser, f.text, opts.runOptions ?? {});
12940
+ if (r.ok) parsedOk++;
12941
+ else parsedFailed++;
12942
+ }
12943
+ } finally {
12944
+ undoAll();
12945
+ }
12946
+ const arms = armRecords.filter((a) => a.attempts > 0).map((a) => ({
12947
+ siteKey: a.siteKey,
12948
+ site: a.site,
12949
+ arm: a.arm,
12950
+ label: a.label,
12951
+ attempts: a.attempts,
12952
+ failures: a.failures,
12953
+ wastedBytes: a.wastedBytes,
12954
+ firstCharGated: a.gate !== null,
12955
+ gatedAttempts: a.gatedAttempts,
12956
+ gatedFailures: a.gatedFailures,
12957
+ gatedWastedBytes: a.gatedWastedBytes
12958
+ })).sort((x, y) => y.gatedWastedBytes - x.gatedWastedBytes || y.wastedBytes - x.wastedBytes || (x.siteKey < y.siteKey ? -1 : x.siteKey > y.siteKey ? 1 : 0) || x.arm - y.arm);
12959
+ const sites = [...siteTotals.values()].sort((x, y) => y.gatedWastedBytes - x.gatedWastedBytes || y.wastedBytes - x.wastedBytes || (x.siteKey < y.siteKey ? -1 : x.siteKey > y.siteKey ? 1 : 0));
12960
+ const minAttempts = opts.inversionMinAttempts ?? 4;
12961
+ const inversions = arms.filter((a) => a.gatedAttempts >= minAttempts && a.gatedFailures === a.gatedAttempts && arms.some((b) => b.siteKey === a.siteKey && b.arm > a.arm && b.gatedAttempts > b.gatedFailures)).sort((x, y) => y.gatedAttempts - x.gatedAttempts || y.gatedWastedBytes - x.gatedWastedBytes || (x.siteKey < y.siteKey ? -1 : x.siteKey > y.siteKey ? 1 : 0) || x.arm - y.arm);
12962
+ return {
12963
+ schema: "parseman.wasted-work/1",
12964
+ corpusFiles: corpus.length,
12965
+ corpusBytes,
12966
+ parsedOk,
12967
+ parsedFailed,
12968
+ instrumentedSites,
12969
+ uninstrumentableSites,
12970
+ unresolvedRoots,
12971
+ totalWastedBytes: sites.reduce((n, s) => n + s.wastedBytes, 0),
12972
+ totalGatedWastedBytes: sites.reduce((n, s) => n + s.gatedWastedBytes, 0),
12973
+ arms,
12974
+ inversions,
12975
+ sites
12976
+ };
12977
+ }
12978
+
12979
+ // src/analysis/choice-cost-render.ts
12980
+ function groupDigits(n) {
12981
+ const s = String(Math.trunc(Math.abs(n)));
12982
+ let out = "";
12983
+ for (let i = 0; i < s.length; i++) {
12984
+ if (i > 0 && (s.length - i) % 3 === 0) out += ",";
12985
+ out += s[i];
12986
+ }
12987
+ return (n < 0 ? "-" : "") + out;
12988
+ }
12989
+ function bytes(n) {
12990
+ if (n < 1024) return `${groupDigits(n)} B`;
12991
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} kB`;
12992
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
12993
+ }
12994
+ var ESC = "\x1B";
12995
+ var ANSI = {
12996
+ dim: `${ESC}[2m`,
12997
+ bold: `${ESC}[1m`,
12998
+ red: `${ESC}[31m`,
12999
+ yellow: `${ESC}[33m`,
13000
+ cyan: `${ESC}[36m`,
13001
+ reset: `${ESC}[0m`
13002
+ };
13003
+ var paint = (on) => (code, s) => on ? `${ANSI[code]}${s}${ANSI.reset}` : s;
13004
+ var pad = (s, w) => s.length >= w ? s : s + " ".repeat(w - s.length);
13005
+ var padStart = (s, w) => s.length >= w ? s : " ".repeat(w - s.length) + s;
13006
+ var ARM_DECLINE_LINE = {
13007
+ "not-a-sequence": "arm is not a sequence, so there is no leading term to lift out",
13008
+ "sequence-shorter-than-2": "arm is a one-term sequence; lifting its only term would leave an empty arm",
13009
+ "lead-case-insensitive-literal": "leading literal is case-insensitive, so the matched text can differ from the literal \u2014 a lifted prefix would carry the wrong string",
13010
+ "lead-not-concrete-terminal": "leading term is not a bare literal or regex; lifting it would change the arm\u2019s value or capture shape"
13011
+ };
13012
+ var SITE_DECLINE_LINE = {
13013
+ "disjoint-dispatch": "first-char dispatch already selects one arm \u2014 nothing is re-scanned",
13014
+ "gated-arms": "an arm carries a runtime gate; per-arm predicates are incompatible with factoring",
13015
+ "strategy-preempted": "a stronger non-backtracking strategy already applies",
13016
+ "fewer-than-two-arms": "fewer than two arms",
13017
+ "arms-not-factorable": "at least one arm cannot contribute a leading term (see below)",
13018
+ "leads-differ": "the arms do not all begin with the same term"
13019
+ };
13020
+ function leftFactorPreview(e) {
13021
+ if (e.factored) return null;
13022
+ if (e.groups.length !== 1) return null;
13023
+ const g = e.groups[0];
13024
+ if (g.members.length !== e.arity) return null;
13025
+ const tails = g.members.map((i) => `sequence(\u2026arm[${i}] tail\u2026)`).join(",\n ");
13026
+ return `sequence(
13027
+ ${g.render === "" ? "<prefix>" : g.render.startsWith("/") ? `regex(${g.render})` : `literal(${g.render})`},
13028
+ choice(
13029
+ ${tails},
13030
+ ),
13031
+ )`;
13032
+ }
13033
+ function renderChoiceInventory(r, opts = {}) {
13034
+ const c = paint(opts.color === true);
13035
+ const limit = opts.limit ?? 20;
13036
+ const out = [];
13037
+ out.push(c("bold", "shared-prefix inventory"));
13038
+ out.push(c("dim", " Static grammar shape only. A site listed here may already cost nothing at runtime:"));
13039
+ out.push(c("dim", " compiled output gates each arm on its first character. Rank with profileWastedWork()."));
13040
+ out.push(` ${groupDigits(r.rules)} rules, ${groupDigits(r.choiceSites)} choice sites`);
13041
+ out.push(` ${groupDigits(r.factoredSites)} left-factored by the compiler`);
13042
+ out.push(` ${groupDigits(r.backlogSites)} sites where alternatives share a leading term and the compiler DECLINED (${groupDigits(r.backlogArms)} arms)`);
13043
+ const backlog = r.entries.filter((e) => !e.factored && e.groups.length > 0 && e.declineReason !== "disjoint-dispatch" && e.declineReason !== "strategy-preempted").sort((a, b) => b.unfactoredArms - a.unfactoredArms || (a.siteKey < b.siteKey ? -1 : 1));
13044
+ if (backlog.length === 0) {
13045
+ out.push("");
13046
+ out.push(" no declined shared prefixes.");
13047
+ return out.join("\n");
13048
+ }
13049
+ out.push("");
13050
+ for (const e of backlog.slice(0, limit)) {
13051
+ out.push(` ${c("cyan", e.siteKey)} ${c("dim", `${e.arity} arms \xB7 ${e.strategy}`)}`);
13052
+ for (const g of e.groups) {
13053
+ out.push(` arms ${g.members.join(", ")} all begin with ${c("bold", g.render)}`);
13054
+ }
13055
+ out.push(` ${c("yellow", "declined")}: ${SITE_DECLINE_LINE[e.declineReason]}`);
13056
+ for (const a of e.armDeclines) {
13057
+ out.push(` arm[${a.arm}] \u2014 ${ARM_DECLINE_LINE[a.reason]}`);
13058
+ }
13059
+ const fix = leftFactorPreview(e);
13060
+ if (fix !== null) {
13061
+ out.push(` ${c("dim", "candidate rewrite (preview \u2014 not applied, not yet digest-verified):")}`);
13062
+ for (const line of fix.split("\n")) out.push(` ${line}`);
13063
+ }
13064
+ out.push("");
13065
+ }
13066
+ if (backlog.length > limit) out.push(` \u2026 and ${groupDigits(backlog.length - limit)} more (the report holds all of them)`);
13067
+ return out.join("\n");
13068
+ }
13069
+ function renderWastedWork(r, opts = {}) {
13070
+ const c = paint(opts.color === true);
13071
+ const limit = opts.limit ?? 20;
13072
+ const out = [];
13073
+ const removed = r.arms.reduce((n, a) => n + (a.attempts - a.gatedAttempts), 0);
13074
+ const totalAttempts = r.arms.reduce((n, a) => n + a.attempts, 0);
13075
+ out.push(c("bold", "wasted work \u2014 input bytes re-scanned after a failed alternative"));
13076
+ out.push(...interpretedBanner(c, removed, totalAttempts));
13077
+ out.push(` corpus: ${groupDigits(r.corpusFiles)} files, ${bytes(r.corpusBytes)} (${groupDigits(r.parsedOk)} parsed, ${groupDigits(r.parsedFailed)} failed)`);
13078
+ out.push(` sites: ${groupDigits(r.instrumentedSites)} instrumented, ${groupDigits(r.uninstrumentableSites)} not instrumentable`);
13079
+ if (r.unresolvedRoots.length > 0) {
13080
+ out.push(` ${c("yellow", "PARTIAL")}: ${groupDigits(r.unresolvedRoots.length)} rule(s) could not be resolved and were NOT walked \u2014 the total below is a lower bound`);
13081
+ }
13082
+ out.push(` total: ${c("bold", bytes(r.totalGatedWastedBytes))} re-scanned` + (r.corpusBytes > 0 ? ` \u2014 ${(r.totalGatedWastedBytes / r.corpusBytes).toFixed(2)}x the corpus` : "") + (r.totalWastedBytes === r.totalGatedWastedBytes ? "" : ` (interpreted: ${bytes(r.totalWastedBytes)})`));
13083
+ const ranked = r.sites.filter((s) => s.gatedWastedBytes > 0 || s.wastedBytes > 0);
13084
+ if (ranked.length === 0) {
13085
+ out.push("");
13086
+ out.push(" no alternative failed on this corpus.");
13087
+ return out.join("\n");
13088
+ }
13089
+ out.push("");
13090
+ out.push(c("dim", ' per arm: "entered" counts what the COMPILED parser reaches; the parenthesised'));
13091
+ out.push(c("dim", " number is the interpreted count, shown only where the first-char gate differs."));
13092
+ out.push("");
13093
+ const w = Math.min(52, Math.max(...ranked.slice(0, limit).map((s) => s.siteKey.length)));
13094
+ for (const s of ranked.slice(0, limit)) {
13095
+ const share = r.totalGatedWastedBytes > 0 ? ` ${padStart((100 * s.gatedWastedBytes / r.totalGatedWastedBytes).toFixed(1) + "%", 7)}` : "";
13096
+ out.push(` ${c("cyan", pad(s.siteKey.slice(0, w), w))} ${padStart(bytes(s.gatedWastedBytes), 10)}${c("dim", share)}`);
13097
+ const arms = r.arms.filter((a) => a.siteKey === s.siteKey).sort((a, b) => a.arm - b.arm);
13098
+ for (const a of arms) {
13099
+ const entered = a.gatedAttempts === a.attempts ? groupDigits(a.attempts) : `${groupDigits(a.gatedAttempts)} (${groupDigits(a.attempts)} interp)`;
13100
+ const verdict = a.gatedAttempts === 0 ? { text: "never entered when compiled", color: "dim" } : a.gatedFailures === a.gatedAttempts ? { text: `failed ALL ${entered}`, color: "red" } : a.gatedFailures === 0 ? { text: `matched ${entered}`, color: "dim" } : { text: `failed ${groupDigits(a.gatedFailures)} / ${entered}`, color: null };
13101
+ const cell = verdict.color === null ? pad(verdict.text, 34) : c(verdict.color, pad(verdict.text, 34));
13102
+ out.push(` ${padStart(String(a.arm), 2)} ${pad(a.label.slice(0, 30), 30)} ${cell} ${a.gatedWastedBytes > 0 ? padStart(bytes(a.gatedWastedBytes), 10) : ""}`);
13103
+ }
13104
+ out.push("");
13105
+ }
13106
+ if (ranked.length > limit) out.push(` \u2026 and ${groupDigits(ranked.length - limit)} more sites (the report holds all of them)`);
13107
+ if (r.inversions.length > 0) {
13108
+ out.push(c("bold", " ordering inversions \u2014 arm failed EVERY compiled entry while a later arm matched"));
13109
+ out.push(c("dim", " ranked by entries, not bytes: an arm can fail 100% of the time and re-scan nothing."));
13110
+ for (const a of r.inversions.slice(0, limit)) {
13111
+ out.push(` ${c("cyan", pad(a.siteKey.slice(0, w), w))} arm ${padStart(String(a.arm), 2)} ${pad(a.label.slice(0, 26), 26)} ${padStart(groupDigits(a.gatedAttempts), 8)} entries ${a.gatedWastedBytes > 0 ? padStart(bytes(a.gatedWastedBytes), 10) : padStart("0 B", 10)}`);
13112
+ }
13113
+ if (r.inversions.length > limit) out.push(` \u2026 and ${groupDigits(r.inversions.length - limit)} more`);
13114
+ }
13115
+ return out.join("\n");
13116
+ }
13117
+ function interpretedBanner(c, removed, total) {
13118
+ const share = total > 0 ? ` \u2014 ${(100 * removed / total).toFixed(0)}% of interpreted arm entries` : "";
13119
+ return [
13120
+ c("yellow", " MEASURED IN THE INTERPRETER. Compiled output gates each arm on its first"),
13121
+ c("yellow", ` character; ${groupDigits(removed)} entries below never happen in the shipped parser${share}.`),
13122
+ c("dim", " Counts shown are the modelled COMPILED ones. Byte totals are unaffected by the"),
13123
+ c("dim", " gate (a gated-out arm consumes nothing); entry counts and failure RATES are not.")
13124
+ ];
13125
+ }
13126
+
13127
+ // src/analysis/choice-cost-gate.ts
13128
+ var KIND_ORDER = [
13129
+ "invalid-baseline",
13130
+ "unmeasurable",
13131
+ "ceiling",
13132
+ "drift",
13133
+ "shrank",
13134
+ "inversion",
13135
+ "unbaselined",
13136
+ "stale"
13137
+ ];
13138
+ var pct = (actual, base) => base === 0 ? actual === 0 ? 0 : Infinity : (actual / base - 1) * 100;
13139
+ function checkWastedWork(reports, baseline, policy = {}) {
13140
+ const breaches = [];
13141
+ const tolerance = policy.driftTolerancePct ?? 1;
13142
+ const add = (kind, key, detail) => {
13143
+ breaches.push({ kind, key, detail });
13144
+ };
13145
+ const finish = (checkedCorpora, checkedSites2) => {
13146
+ breaches.sort((a, b2) => KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b2.kind) || (a.key < b2.key ? -1 : a.key > b2.key ? 1 : 0));
13147
+ return {
13148
+ schema: "parseman.wasted-work-verdict/1",
13149
+ ok: breaches.length === 0,
13150
+ breaches,
13151
+ checkedCorpora,
13152
+ checkedSites: checkedSites2
13153
+ };
13154
+ };
13155
+ const b = baseline;
13156
+ if (b === null || b === void 0 || typeof b !== "object") {
13157
+ add("invalid-baseline", "<baseline>", "no baseline supplied \u2014 a gate with no baseline measures nothing, and passing here is how a budget stops being enforced");
13158
+ return finish(0, 0);
13159
+ }
13160
+ if (b.schema !== "parseman.wasted-work-baseline/1") {
13161
+ add("invalid-baseline", "<baseline>", `baseline schema is ${JSON.stringify(b.schema)}, expected "parseman.wasted-work-baseline/1"`);
13162
+ return finish(0, 0);
13163
+ }
13164
+ const isRecord = (v2) => typeof v2 === "object" && v2 !== null && !Array.isArray(v2);
13165
+ if (!isRecord(b.totals) || !isRecord(b.sites)) {
13166
+ add("invalid-baseline", "<baseline>", "baseline `totals` or `sites` is missing or is not an object");
13167
+ return finish(0, 0);
13168
+ }
13169
+ if (Object.keys(b.totals).length === 0) {
13170
+ add("invalid-baseline", "<baseline>", "baseline records ZERO corpora");
13171
+ return finish(0, 0);
13172
+ }
13173
+ const finite = (v2) => typeof v2 === "number" && Number.isFinite(v2);
13174
+ for (const [id, t] of Object.entries(b.totals).sort(([x], [y]) => x < y ? -1 : 1)) {
13175
+ if (!isRecord(t) || !finite(t.corpusBytes) || !finite(t.totalWastedBytes)) {
13176
+ add("invalid-baseline", id, "baseline entry has no finite numeric `corpusBytes` / `totalWastedBytes` \u2014 the comparison against it would silently produce NaN and no breach");
13177
+ }
13178
+ }
13179
+ for (const [key, v2] of Object.entries(b.sites).sort(([x], [y]) => x < y ? -1 : 1)) {
13180
+ if (!finite(v2)) add("invalid-baseline", key, "baseline site value is not a finite number \u2014 the comparison against it would silently produce NaN and no breach");
13181
+ }
13182
+ if (breaches.length > 0) return finish(0, 0);
13183
+ if (policy.ceilingRatio !== void 0) {
13184
+ for (const [id, t] of Object.entries(b.totals).sort(([x], [y]) => x < y ? -1 : 1)) {
13185
+ if (t.corpusBytes > 0 && t.totalWastedBytes / t.corpusBytes > policy.ceilingRatio) {
13186
+ add(
13187
+ "invalid-baseline",
13188
+ id,
13189
+ `baseline records ${t.totalWastedBytes} wasted / ${t.corpusBytes} corpus bytes = ${(t.totalWastedBytes / t.corpusBytes).toFixed(3)}x, above the ${policy.ceilingRatio}x ceiling \u2014 a rebaseline cannot accept an over-ceiling number`
13190
+ );
13191
+ }
13192
+ }
13193
+ if (breaches.length > 0) return finish(0, 0);
13194
+ }
13195
+ if (Object.keys(reports).length === 0) {
13196
+ add("unmeasurable", "<reports>", "no corpus was measured \u2014 refusing to report a pass on an empty measurement");
13197
+ return finish(0, 0);
13198
+ }
13199
+ let checkedSites = 0;
13200
+ for (const id of Object.keys(reports).sort()) {
13201
+ const r = reports[id];
13202
+ if (r.corpusFiles === 0) {
13203
+ add("unmeasurable", id, "report covers ZERO corpus files");
13204
+ continue;
13205
+ }
13206
+ if (r.instrumentedSites === 0) {
13207
+ add("unmeasurable", id, "report instrumented ZERO choice sites \u2014 the total below would be zero for a reason unrelated to the grammar");
13208
+ continue;
13209
+ }
13210
+ if (r.unresolvedRoots.length > 0) {
13211
+ add(
13212
+ "unmeasurable",
13213
+ id,
13214
+ `${r.unresolvedRoots.length} rule(s) could not be resolved and were NOT walked (${r.unresolvedRoots.slice(0, 4).join(", ")}${r.unresolvedRoots.length > 4 ? ", \u2026" : ""}) \u2014 the total is a lower bound over an unknown fraction of the grammar`
13215
+ );
13216
+ continue;
13217
+ }
13218
+ const base = b.totals[id];
13219
+ if (base === void 0) {
13220
+ add("unbaselined", id, `measured but absent from the baseline (${r.totalWastedBytes} wasted bytes over ${r.corpusBytes})`);
13221
+ continue;
13222
+ }
13223
+ if (policy.ceilingRatio !== void 0 && r.corpusBytes > 0) {
13224
+ const ratio = r.totalWastedBytes / r.corpusBytes;
13225
+ if (ratio > policy.ceilingRatio) {
13226
+ add("ceiling", id, `${r.totalWastedBytes} wasted / ${r.corpusBytes} corpus bytes = ${ratio.toFixed(3)}x, over the ${policy.ceilingRatio}x ceiling`);
13227
+ }
13228
+ }
13229
+ if (r.corpusBytes !== base.corpusBytes) {
13230
+ add("unmeasurable", id, `corpus changed: baseline ${base.corpusBytes} bytes, measured ${r.corpusBytes} \u2014 totals are not comparable until the baseline is refreshed`);
13231
+ continue;
13232
+ }
13233
+ const d = pct(r.totalWastedBytes, base.totalWastedBytes);
13234
+ if (d > tolerance) {
13235
+ add("drift", id, `total wasted ${base.totalWastedBytes} -> ${r.totalWastedBytes} bytes (${d >= 0 ? "+" : ""}${d.toFixed(2)}%, band \xB1${tolerance}%)`);
13236
+ } else if (d < -tolerance) {
13237
+ add("shrank", id, `total wasted ${base.totalWastedBytes} -> ${r.totalWastedBytes} bytes (${d.toFixed(2)}%, band \xB1${tolerance}%) \u2014 BANK THE WIN: rebaseline so the ${base.totalWastedBytes - r.totalWastedBytes} bytes you just saved cannot become headroom for the next regression`);
13238
+ }
13239
+ if (r.totalGatedWastedBytes !== r.totalWastedBytes) {
13240
+ const dg = pct(r.totalGatedWastedBytes, base.totalWastedBytes);
13241
+ if (dg > tolerance) {
13242
+ add("drift", id, `compiled-model total wasted ${base.totalWastedBytes} -> ${r.totalGatedWastedBytes} bytes (${dg >= 0 ? "+" : ""}${dg.toFixed(2)}%, band \xB1${tolerance}%) \u2014 the compiled and interpreted columns have PARTED (${r.totalGatedWastedBytes} vs ${r.totalWastedBytes}); the baseline records the interpreted one`);
13243
+ } else if (dg < -tolerance) {
13244
+ add("shrank", id, `compiled-model total wasted ${base.totalWastedBytes} -> ${r.totalGatedWastedBytes} bytes (${dg.toFixed(2)}%, band \xB1${tolerance}%) \u2014 the compiled and interpreted columns have PARTED (${r.totalGatedWastedBytes} vs ${r.totalWastedBytes}); the baseline records the interpreted one`);
13245
+ }
13246
+ }
13247
+ for (const s of r.sites) {
13248
+ const key = `${id}::${s.siteKey}`;
13249
+ const bs = b.sites[key];
13250
+ if (bs === void 0) {
13251
+ if (s.wastedBytes > 0) add("unbaselined", key, `site measured ${s.wastedBytes} wasted bytes but has no baseline entry`);
13252
+ continue;
13253
+ }
13254
+ checkedSites++;
13255
+ const sd = pct(s.wastedBytes, bs);
13256
+ if (sd > tolerance) add("drift", key, `${bs} -> ${s.wastedBytes} bytes (${sd >= 0 ? "+" : ""}${sd.toFixed(2)}%, band \xB1${tolerance}%)`);
13257
+ else if (sd < -tolerance) add("shrank", key, `${bs} -> ${s.wastedBytes} bytes (${sd.toFixed(2)}%, band \xB1${tolerance}%) \u2014 BANK THE WIN: rebaseline, or this site's ${bs - s.wastedBytes} recovered bytes become budget`);
13258
+ if (s.gatedWastedBytes !== s.wastedBytes) {
13259
+ const sg = pct(s.gatedWastedBytes, bs);
13260
+ if (sg > tolerance) add("drift", key, `compiled-model ${bs} -> ${s.gatedWastedBytes} bytes (${sg >= 0 ? "+" : ""}${sg.toFixed(2)}%, band \xB1${tolerance}%) \u2014 columns PARTED (${s.gatedWastedBytes} vs ${s.wastedBytes})`);
13261
+ else if (sg < -tolerance) add("shrank", key, `compiled-model ${bs} -> ${s.gatedWastedBytes} bytes (${sg.toFixed(2)}%, band \xB1${tolerance}%) \u2014 columns PARTED (${s.gatedWastedBytes} vs ${s.wastedBytes})`);
13262
+ }
13263
+ }
13264
+ if (policy.failOnInversions === true) {
13265
+ for (const inv of r.inversions) {
13266
+ add(
13267
+ "inversion",
13268
+ `${id}::${inv.siteKey}#${inv.arm}`,
13269
+ // The GATED columns, because that is what `inversions` was computed from
13270
+ // (choice-cost.ts filters on `gatedAttempts`/`gatedFailures`). Printing the
13271
+ // interpreted counts here would state "failed all N" with an N the condition
13272
+ // never tested — the two differ wherever `firstCharGated` is true.
13273
+ `arm ${inv.arm} (${inv.label}) failed all ${inv.gatedAttempts} compiled entries while a later arm matched; ${inv.gatedWastedBytes} bytes re-scanned`
13274
+ );
13275
+ }
13276
+ }
13277
+ }
13278
+ const measuredKeys = /* @__PURE__ */ new Set();
13279
+ for (const id of Object.keys(reports)) for (const s of reports[id].sites) measuredKeys.add(`${id}::${s.siteKey}`);
13280
+ for (const key of Object.keys(b.sites).sort()) {
13281
+ if (!measuredKeys.has(key)) {
13282
+ add("stale", key, "baselined site matches no measured site \u2014 a rule was renamed or removed, and ignoring it silently shrinks the gated set");
13283
+ }
13284
+ }
13285
+ for (const id of Object.keys(b.totals).sort()) {
13286
+ if (reports[id] === void 0) add("stale", id, "baselined corpus was not measured");
13287
+ }
13288
+ return finish(Object.keys(reports).length, checkedSites);
13289
+ }
13290
+ function buildWastedWorkBaseline(reports, meta) {
13291
+ const totals = {};
13292
+ const sites = {};
13293
+ for (const id of Object.keys(reports).sort()) {
13294
+ const r = reports[id];
13295
+ totals[id] = { corpusBytes: r.corpusBytes, totalWastedBytes: r.totalWastedBytes, instrumentedSites: r.instrumentedSites };
13296
+ for (const s of [...r.sites].sort((a, c) => a.siteKey < c.siteKey ? -1 : 1)) {
13297
+ if (s.wastedBytes > 0) sites[`${id}::${s.siteKey}`] = s.wastedBytes;
13298
+ }
13299
+ }
13300
+ return { schema: "parseman.wasted-work-baseline/1", gitRev: meta.gitRev, updatedAt: meta.updatedAt, totals, sites };
13301
+ }
13302
+
13303
+ // src/combinators/gate.ts
13304
+ function gate(predicate) {
13305
+ const meta = {
13306
+ firstSet: { kind: "any" },
13307
+ canMatchNewline: false,
11749
13308
  isTrivia: false
11750
13309
  };
11751
13310
  return {
@@ -11760,9 +13319,6 @@ function gate(predicate) {
11760
13319
  };
11761
13320
  }
11762
13321
 
11763
- // src/combinators/guard.ts
11764
- var guard = gate;
11765
-
11766
13322
  // src/combinators/completions.ts
11767
13323
  function completionsAt(target, input, offset, options = {}) {
11768
13324
  const probe = { offset, best: null };
@@ -11989,9 +13545,9 @@ function parseMiddle(input, reStart, reEnd, cStart, ruleFn, separator, build, to
11989
13545
  const ctx = mkCtx(null, build, tolerant);
11990
13546
  const out = [];
11991
13547
  let pos = reStart;
11992
- let guard2 = 0;
13548
+ let guard = 0;
11993
13549
  while (pos < reEnd) {
11994
- if (++guard2 > reEnd - reStart + 2) return null;
13550
+ if (++guard > reEnd - reStart + 2) return null;
11995
13551
  if (separator && out.length > 0 && out[out.length - 1]._tag === "node") {
11996
13552
  if (pos + separator.length > reEnd || !input.startsWith(separator, pos)) return null;
11997
13553
  out.push({ _tag: "leaf", value: separator, span: { start: pos - cStart, end: pos + separator.length - cStart } });
@@ -12283,142 +13839,6 @@ function parseDoc(registry, rootRule, input, opts = {}) {
12283
13839
  return new ParseDocImpl(registry, rootRule, opts, { abs: null }, [fail], null, input);
12284
13840
  }
12285
13841
 
12286
- // src/functional/run.ts
12287
- var invoke = (r, input, pos, ctx) => typeof r === "function" ? r(input, pos, ctx) : r.parse(input, pos, ctx);
12288
- function triviaKindLabelsFromRunnable(r) {
12289
- if (!r) return void 0;
12290
- if (typeof r === "function") return r._meta?.triviaKindLabels;
12291
- const grammarTrivia = r._meta.grammarTrivia;
12292
- return grammarTrivia?._meta.triviaKindLabels ?? r._meta.triviaKindLabels ?? (r._def.tag === "grammar" ? r._def.triviaParser?._meta.triviaKindLabels : void 0);
12293
- }
12294
- function rootTriviaClassifiedFromRunnable(r) {
12295
- if (!r) return false;
12296
- if (typeof r === "function") return r._meta?.rootTriviaClassified === true;
12297
- const grammarTrivia = r._meta.grammarTrivia;
12298
- return grammarTrivia?._meta.rootTriviaClassified === true || r._meta.rootTriviaClassified === true;
12299
- }
12300
- function makeSelectedRootLabelIndex(labels) {
12301
- const index = /* @__PURE__ */ Object.create(null);
12302
- for (let i = 0; i < labels.length; i++) {
12303
- if (index[labels[i]] === void 0) index[labels[i]] = i;
12304
- }
12305
- return index;
12306
- }
12307
- function runOnce(entry, input, options, phase, profileState) {
12308
- if (typeof entry !== "function" && typeof entry?.parse !== "function") {
12309
- throw new TypeError(
12310
- `run(): start production is ${entry === null ? "null" : typeof entry}, not a rule \u2014 the requested grammar rule does not exist (check the rule name).`
12311
- );
12312
- }
12313
- const rootTriviaSelection = options.rootTrivia?.select;
12314
- const captureRootTrivia = rootTriviaSelection !== void 0 && rootTriviaSelection.length > 0;
12315
- const selectedRootLog = captureRootTrivia ? [] : void 0;
12316
- const selectedRootLabelIndex = captureRootTrivia ? makeSelectedRootLabelIndex(rootTriviaSelection) : void 0;
12317
- const errors = [];
12318
- const profile = profileState ?? (phase === void 0 ? void 0 : { phase, nodes: 0, childSlots: 0, rawSlots: 0, triviaSlots: 0, fieldSlots: 0, hostCalls: 0 });
12319
- const skipGlobalSinks = phase === "recognizer" || phase === "capture";
12320
- const grammarTrivia = typeof entry !== "function" ? entry._meta.grammarTrivia : void 0;
12321
- const grammarScanSkip = typeof entry !== "function" ? entry._meta.grammarScanSkip : void 0;
12322
- const triviaKindLabels = triviaKindLabelsFromRunnable(entry) ?? triviaKindLabelsFromRunnable(options.trivia);
12323
- if (captureRootTrivia && triviaKindLabels === void 0) {
12324
- throw new TypeError("run(): rootTrivia.select requires labeled grammar trivia.");
12325
- }
12326
- if (captureRootTrivia) {
12327
- for (const label2 of rootTriviaSelection) {
12328
- if (!triviaKindLabels.includes(label2)) {
12329
- throw new TypeError(`run(): rootTrivia.select contains unknown trivia label ${JSON.stringify(label2)}.`);
12330
- }
12331
- }
12332
- }
12333
- if (captureRootTrivia && !rootTriviaClassifiedFromRunnable(entry) && !rootTriviaClassifiedFromRunnable(options.trivia)) {
12334
- throw new TypeError(
12335
- "run(): rootTrivia.select requires classifiedTrivia() at the root."
12336
- );
12337
- }
12338
- const stamped = entry;
12339
- assertHostModeCompatible(
12340
- typeof entry === "function" ? stamped[FUSED_HOST_MODE] ?? "ast" : entry._meta.grammarHostMode ?? "ast",
12341
- options.build,
12342
- typeof entry === "function" && stamped[FUSED_HOST_ELIDED] === true
12343
- );
12344
- const ctx = {
12345
- trackLines: false,
12346
- ...skipGlobalSinks ? { _pmProfile: profile } : {
12347
- ...captureRootTrivia ? {
12348
- _rootTriviaLog: selectedRootLog,
12349
- _rootTriviaKindIndex: selectedRootLabelIndex,
12350
- _rootTriviaStrictScopes: true
12351
- } : {},
12352
- _errors: errors,
12353
- ...profile === void 0 ? {} : { _pmProfile: profile }
12354
- },
12355
- build: options.build,
12356
- state: options.state,
12357
- ...grammarTrivia !== void 0 ? { trivia: grammarTrivia, ...triviaKindLabels ? { triviaKindLabels } : {} } : triviaKindLabels ? { triviaKindLabels } : {},
12358
- ...grammarScanSkip !== void 0 ? { scanSkip: grammarScanSkip } : {},
12359
- ...options.triviaCaptureMask !== void 0 ? { _triviaCaptureMask: options.triviaCaptureMask } : {},
12360
- ...options.tolerant ? { _tolerant: true, _rec: REC } : {},
12361
- ...options.instrumentation === void 0 ? {} : options.instrumentation
12362
- };
12363
- const r = invoke(entry, input, 0, ctx);
12364
- let unconsumedFrom = null;
12365
- if (r.ok) {
12366
- let pos = r.span?.end ?? 0;
12367
- if (options.trivia && pos < input.length) {
12368
- const t = invoke(options.trivia, input, pos, { trackLines: false });
12369
- if (t.ok && t.span.end > pos) pos = t.span.end;
12370
- }
12371
- unconsumedFrom = pos < input.length ? pos : null;
12372
- }
12373
- return {
12374
- ok: r.ok,
12375
- value: r.ok ? r.value : void 0,
12376
- span: r.span ?? { start: 0, end: 0 },
12377
- expected: r.ok ? [] : r.expected ?? [],
12378
- errors,
12379
- ...selectedRootLog !== void 0 && selectedRootLog.length > 0 ? {
12380
- rootTrivia: {
12381
- rows: selectedRootLog,
12382
- select: rootTriviaSelection,
12383
- index: buildRootTriviaIndex(selectedRootLog, rootTriviaSelection)
12384
- }
12385
- } : {},
12386
- unconsumedFrom
12387
- };
12388
- }
12389
- function clonePassState(state) {
12390
- if (state === null || typeof state !== "object") return state;
12391
- return Array.isArray(state) ? [...state] : { ...state };
12392
- }
12393
- function profilePass(entry, input, options, phase) {
12394
- const state = { phase, nodes: 0, childSlots: 0, rawSlots: 0, triviaSlots: 0, fieldSlots: 0, hostCalls: 0 };
12395
- const passOptions = options.state === void 0 ? options : { ...options, state: clonePassState(options.state) };
12396
- const start = performance.now();
12397
- const result = runOnce(entry, input, passOptions, phase, state);
12398
- const { phase: _phase, ...counts } = state;
12399
- return { result, profile: { ms: performance.now() - start, ...counts } };
12400
- }
12401
- function run(entry, input, options = {}) {
12402
- if (!options.profile) return runOnce(entry, input, options);
12403
- if (typeof entry !== "function") {
12404
- throw new TypeError("run({ profile: true }) requires a compiled parser entry");
12405
- }
12406
- const recognizer = profilePass(entry, input, options, "recognizer");
12407
- const capture = profilePass(entry, input, options, "capture");
12408
- const host = profilePass(entry, input, options, "host");
12409
- if (recognizer.result.ok !== host.result.ok || capture.result.ok !== host.result.ok || recognizer.result.unconsumedFrom !== host.result.unconsumedFrom || capture.result.unconsumedFrom !== host.result.unconsumedFrom) {
12410
- throw new Error("run({ profile: true }) changed recognition; the grammar is not profile-safe");
12411
- }
12412
- return {
12413
- ...host.result,
12414
- profile: {
12415
- recognizer: recognizer.profile,
12416
- structuralCapture: capture.profile,
12417
- hostConstruction: host.profile
12418
- }
12419
- };
12420
- }
12421
-
12422
13842
  // src/coverage.ts
12423
13843
  function dispatchMatcher(entry) {
12424
13844
  switch (entry.kind) {
@@ -12433,7 +13853,7 @@ function dispatchMatcher(entry) {
12433
13853
  var GRAMMAR_COVERAGE_DEFINITIONS = /* @__PURE__ */ Symbol.for("parseman.grammarCoverageDefinitions");
12434
13854
  function compiledGrammarCoverageDefinitions(grammar) {
12435
13855
  const definitions = grammar[GRAMMAR_COVERAGE_DEFINITIONS];
12436
- if (!Array.isArray(definitions) || !definitions.every((definition) => typeof definition === "object" && definition !== null && typeof definition.id === "string" && (definition.kind === "rule" || definition.kind === "choice-arm" || definition.kind === "dispatch-arm" || definition.kind === "label"))) {
13856
+ if (!Array.isArray(definitions) || definitions.length === 0 || !definitions.every((definition) => typeof definition === "object" && definition !== null && typeof definition.id === "string" && (definition.kind === "rule" || definition.kind === "choice-arm" || definition.kind === "dispatch-arm" || definition.kind === "label"))) {
12437
13857
  throw new TypeError("grammar has no coverage definitions; enable grammarCoverage for this macro build");
12438
13858
  }
12439
13859
  return definitions;
@@ -12487,7 +13907,13 @@ function createGrammarCoverageCollector(definitions) {
12487
13907
  snapshot() {
12488
13908
  const hitList = [...hits].sort();
12489
13909
  const unhit = ordered.map((definition) => definition.id).filter((id) => !hits.has(id));
12490
- return { definitions: ordered, hits: hitList, unhit, ratio: ordered.length === 0 ? 1 : hitList.length / ordered.length };
13910
+ return {
13911
+ definitions: ordered,
13912
+ hits: hitList,
13913
+ unhit,
13914
+ ratio: hitList.length / ordered.length,
13915
+ measurable: ordered.length > 0
13916
+ };
12491
13917
  },
12492
13918
  reset() {
12493
13919
  hits.clear();
@@ -12807,13 +14233,13 @@ function buildTriviaIndex(root, input = "", opts) {
12807
14233
  }
12808
14234
 
12809
14235
  // src/cst/walk.ts
12810
- function isCombinator(value) {
14236
+ function isCombinator2(value) {
12811
14237
  return !!value && typeof value === "object" && "_def" in value && "_meta" in value && typeof value.parse === "function";
12812
14238
  }
12813
14239
  function reflectionFor(grammar) {
12814
14240
  const stamped = grammarReflectionOf(grammar);
12815
14241
  if (stamped) return stamped;
12816
- const entries = Object.entries(grammar).filter((entry) => isCombinator(entry[1]));
14242
+ const entries = Object.entries(grammar).filter((entry) => isCombinator2(entry[1]));
12817
14243
  return collectGrammarReflection(entries);
12818
14244
  }
12819
14245
  function createVisitor(grammar, spec) {
@@ -12851,196 +14277,6 @@ function createVisitor(grammar, spec) {
12851
14277
  };
12852
14278
  }
12853
14279
 
12854
- // src/cst/offset-model.ts
12855
- var { isFinite } = Number;
12856
- function floorIndex(arr, len, x) {
12857
- let lo = 0;
12858
- let hi = len - 1;
12859
- let ans = -1;
12860
- while (lo <= hi) {
12861
- const mid = lo + hi >>> 1;
12862
- if (arr[mid] <= x) {
12863
- ans = mid;
12864
- lo = mid + 1;
12865
- } else {
12866
- hi = mid - 1;
12867
- }
12868
- }
12869
- return ans;
12870
- }
12871
- var OffsetIndex = class {
12872
- /** Slot starts, sorted ascending. Parallel to `_ends`. */
12873
- _starts;
12874
- _ends;
12875
- count;
12876
- docStart;
12877
- docEnd;
12878
- constructor(slots, docStart = 0, docEnd) {
12879
- const sorted = [...slots].sort((a, b) => a.start - b.start);
12880
- const starts = [];
12881
- const ends = [];
12882
- let prevEnd = docStart;
12883
- for (const s of sorted) {
12884
- if (!isFinite(s.start) || !isFinite(s.end) || s.end < s.start) {
12885
- throw new RangeError(`OffsetIndex: invalid slot [${s.start}, ${s.end}]`);
12886
- }
12887
- if (s.start < prevEnd) {
12888
- throw new RangeError(`OffsetIndex: overlapping/out-of-order slot [${s.start}, ${s.end}] after end ${prevEnd}`);
12889
- }
12890
- starts.push(s.start);
12891
- ends.push(s.end);
12892
- prevEnd = s.end;
12893
- }
12894
- this._starts = starts;
12895
- this._ends = ends;
12896
- this.count = starts.length;
12897
- this.docStart = docStart;
12898
- this.docEnd = docEnd ?? (this.count ? Math.max(prevEnd, ends[ends.length - 1]) : docStart);
12899
- }
12900
- /** The slot at index `i` (0-based, source order), or undefined. */
12901
- slot(i) {
12902
- if (i < 0 || i >= this.count) return void 0;
12903
- return { start: this._starts[i], end: this._ends[i] };
12904
- }
12905
- /**
12906
- * Index of the gap that CONTAINS `offset`, in `[0, count]`. Gap `i` spans
12907
- * `[endOf(i-1), startOf(i))` where `endOf(-1)=docStart` and `startOf(count)=docEnd`.
12908
- * An offset that lands inside a slot returns that slot's TRAILING gap index
12909
- * (i.e. the gap after it) — callers querying at slot boundaries get the
12910
- * intuitive answer via `gapBefore`/`gapAfter`.
12911
- */
12912
- gapIndexAt(offset) {
12913
- const i = floorIndex(this._starts, this.count, offset);
12914
- if (i < 0) return 0;
12915
- if (offset < this._ends[i]) return i + 1;
12916
- return i + 1;
12917
- }
12918
- /** The gap at index `i` in `[0, count]`. */
12919
- gap(i) {
12920
- if (i < 0 || i > this.count) return void 0;
12921
- const start = i === 0 ? this.docStart : this._ends[i - 1];
12922
- const end = i === this.count ? this.docEnd : this._starts[i];
12923
- return { start, end };
12924
- }
12925
- /**
12926
- * The trivia gap immediately BEFORE `offset` (`[prevSlotEnd, offset)`), where
12927
- * `prevSlotEnd` is the end of the last slot ending at or before `offset` (or
12928
- * `docStart`). For a real slot start this is the gap preceding that slot; for an
12929
- * adjacent slot the gap is empty and agrees with `gapAfter` on the same seam.
12930
- */
12931
- gapBefore(offset) {
12932
- const e = floorIndex(this._ends, this.count, offset);
12933
- const start = e >= 0 ? this._ends[e] : this.docStart;
12934
- return { start, end: offset };
12935
- }
12936
- /**
12937
- * The trivia gap immediately AFTER `offset` (`[offset, nextSlotStart)`), where
12938
- * `nextSlotStart` is the least slot start at or after `offset` (or `docEnd`).
12939
- */
12940
- gapAfter(offset) {
12941
- return { start: offset, end: this._ceilStart(offset) };
12942
- }
12943
- /** Least slot start `>= offset`, else `docEnd`. */
12944
- _ceilStart(offset) {
12945
- let lo = 0;
12946
- let hi = this.count - 1;
12947
- let ans = this.docEnd;
12948
- while (lo <= hi) {
12949
- const mid = lo + hi >>> 1;
12950
- if (this._starts[mid] >= offset) {
12951
- ans = this._starts[mid];
12952
- hi = mid - 1;
12953
- } else {
12954
- lo = mid + 1;
12955
- }
12956
- }
12957
- return ans;
12958
- }
12959
- /** Is this gap empty (adjacent slots, no trivia)? */
12960
- isEmpty(gap) {
12961
- return gap.end <= gap.start;
12962
- }
12963
- };
12964
- var NL = 10;
12965
- var CR = 13;
12966
- var SP = 32;
12967
- var TAB = 9;
12968
- function gapText(source, gap) {
12969
- return gap.end > gap.start ? source.slice(gap.start, gap.end) : "";
12970
- }
12971
- function lineBreaksIn(source, gap) {
12972
- let n = 0;
12973
- for (let i = gap.start; i < gap.end; i++) {
12974
- const c = source.charCodeAt(i);
12975
- if (c === NL) n++;
12976
- else if (c === CR && source.charCodeAt(i + 1) !== NL) n++;
12977
- }
12978
- return n;
12979
- }
12980
- function blankLinesIn(source, gap) {
12981
- return Math.max(0, lineBreaksIn(source, gap) - 1);
12982
- }
12983
- function lineStartWithin(source, gap) {
12984
- for (let i = gap.end - 1; i >= gap.start; i--) {
12985
- const c = source.charCodeAt(i);
12986
- if (c === NL || c === CR) return i + 1;
12987
- }
12988
- return gap.start;
12989
- }
12990
- function indentWidth(source, gap) {
12991
- const ls = lineStartWithin(source, gap);
12992
- if (ls === gap.start) return 0;
12993
- return gap.end - ls;
12994
- }
12995
- function indentMixed(source, gap) {
12996
- const ls = lineStartWithin(source, gap);
12997
- let sawSpace = false;
12998
- let sawTab = false;
12999
- for (let i = ls; i < gap.end; i++) {
13000
- const c = source.charCodeAt(i);
13001
- if (c === SP) sawSpace = true;
13002
- else if (c === TAB) sawTab = true;
13003
- }
13004
- return sawSpace && sawTab;
13005
- }
13006
- function commentsIn(source, gap, comment) {
13007
- const re = new RegExp(comment.source, comment.flags.replace(/[gy]/g, "") + "g");
13008
- re.lastIndex = gap.start;
13009
- const out = [];
13010
- let m;
13011
- while ((m = re.exec(source)) && m.index < gap.end) {
13012
- if (m[0].length === 0) {
13013
- re.lastIndex++;
13014
- continue;
13015
- }
13016
- if (m.index + m[0].length > gap.end) break;
13017
- out.push({ start: m.index, end: m.index + m[0].length });
13018
- }
13019
- return out;
13020
- }
13021
- function gapIsSignificant(source, gap, comment) {
13022
- if (gap.end <= gap.start) return false;
13023
- if (lineBreaksIn(source, gap) > 0) return true;
13024
- if (comment && commentsIn(source, gap, comment).length > 0) return true;
13025
- return false;
13026
- }
13027
- function collectLeafSlots(root) {
13028
- const slots = [];
13029
- const visit = (n) => {
13030
- const kids = n.children;
13031
- if (kids && kids.length) {
13032
- for (const c of kids) visit(c);
13033
- } else if (n.span) {
13034
- slots.push({ start: n.span.start, end: n.span.end });
13035
- }
13036
- };
13037
- if (root) visit(root);
13038
- return slots;
13039
- }
13040
- function buildOffsetIndex(root, input = "") {
13041
- return new OffsetIndex(collectLeafSlots(root), 0, input.length);
13042
- }
13043
-
13044
14280
  // src/index.ts
13045
14281
  registerRegexAnalyzer(firstSetFromRegex);
13046
14282
  //# sourceMappingURL=index.cjs.map