parseman 0.45.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 (64) 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 +15 -0
  12. package/dist/analysis/diagnose.d.ts.map +1 -1
  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 +8 -0
  24. package/dist/analysis/gating.d.ts.map +1 -1
  25. package/dist/analysis/rebuild.d.ts +61 -0
  26. package/dist/analysis/rebuild.d.ts.map +1 -0
  27. package/dist/analysis/terminal.d.ts +142 -0
  28. package/dist/analysis/terminal.d.ts.map +1 -0
  29. package/dist/cli/index.cjs +12507 -0
  30. package/dist/cli/index.cjs.map +7 -0
  31. package/dist/cli/index.d.ts +3 -0
  32. package/dist/cli/index.d.ts.map +1 -0
  33. package/dist/cli/index.js +12484 -0
  34. package/dist/cli/index.js.map +7 -0
  35. package/dist/compiler/build-arity.d.ts.map +1 -1
  36. package/dist/compiler/linker.d.ts +44 -6
  37. package/dist/compiler/linker.d.ts.map +1 -1
  38. package/dist/compiler/module-hoist.d.ts +106 -0
  39. package/dist/compiler/module-hoist.d.ts.map +1 -0
  40. package/dist/index.cjs +1297 -297
  41. package/dist/index.cjs.map +4 -4
  42. package/dist/index.d.ts +8 -2
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +1297 -297
  45. package/dist/index.js.map +4 -4
  46. package/dist/language-service/index.cjs.map +2 -2
  47. package/dist/language-service/index.js.map +2 -2
  48. package/dist/plugin/evaluator.d.ts +1 -1
  49. package/dist/plugin/evaluator.d.ts.map +1 -1
  50. package/dist/plugin/free-identifiers.d.ts +23 -0
  51. package/dist/plugin/free-identifiers.d.ts.map +1 -0
  52. package/dist/plugin/index.cjs +429 -43
  53. package/dist/plugin/index.cjs.map +4 -4
  54. package/dist/plugin/index.d.ts.map +1 -1
  55. package/dist/plugin/index.js +429 -43
  56. package/dist/plugin/index.js.map +4 -4
  57. package/dist/plugin/reducer-resolver.d.ts +17 -1
  58. package/dist/plugin/reducer-resolver.d.ts.map +1 -1
  59. package/dist/spec/index.cjs +2 -1
  60. package/dist/spec/index.cjs.map +2 -2
  61. package/dist/spec/index.js +2 -1
  62. package/dist/spec/index.js.map +2 -2
  63. package/dist/version.d.ts +1 -1
  64. package/package.json +18 -3
package/dist/index.js CHANGED
@@ -2824,7 +2824,7 @@ function markUnusedValues(root) {
2824
2824
  return;
2825
2825
  }
2826
2826
  };
2827
- const resolveRoot = (c) => {
2827
+ const resolveRoot2 = (c) => {
2828
2828
  const guard = /* @__PURE__ */ new Set();
2829
2829
  let cur = c;
2830
2830
  while (!guard.has(cur)) {
@@ -2839,7 +2839,7 @@ function markUnusedValues(root) {
2839
2839
  }
2840
2840
  return cur;
2841
2841
  };
2842
- visit(resolveRoot(root), true);
2842
+ visit(resolveRoot2(root), true);
2843
2843
  }
2844
2844
 
2845
2845
  // src/combinators/grammar.ts
@@ -3443,7 +3443,8 @@ var RESOLVER_REASONS = {
3443
3443
  "unresolved-import": "its module could not be resolved or parsed",
3444
3444
  "not-found": "the name did not resolve to any binding",
3445
3445
  "computed": "it is a computed expression",
3446
- "foreign-source": "it is named inside an imported factory, whose offsets the entry-file resolver cannot index"
3446
+ "foreign-source": "it is named in a module the resolver was never told about, so its offsets index no scope tree the resolver holds",
3447
+ "ambiguous-source": "two distinct registered modules hold byte-identical text, so that text no longer names the module its offsets index"
3447
3448
  };
3448
3449
  function unconfirmableReason(def, src) {
3449
3450
  const declared = def.buildArityUnresolved;
@@ -3972,7 +3973,7 @@ function leaf(root, fn) {
3972
3973
  }
3973
3974
 
3974
3975
  // src/version.ts
3975
- var PARSEMAN_VERSION = "0.45.0";
3976
+ var PARSEMAN_VERSION = "0.46.0";
3976
3977
 
3977
3978
  // src/cst/host-mode.ts
3978
3979
  var FUSED_HOST_MODE = /* @__PURE__ */ Symbol.for("parseman.fusedHostMode");
@@ -4027,14 +4028,28 @@ function firstSetToString(fs) {
4027
4028
  return fs.ranges.map((r) => r.lo === r.hi ? ch(r.lo) : `${ch(r.lo)}-${ch(r.hi)}`).join(",");
4028
4029
  }
4029
4030
  var ruleNameOf2 = (p) => p._ruleName;
4031
+ var KEYWORD_REGEX_RE = /^\^?[@#.-]?[A-Za-z][\w-]*(\(\?![^)]*\))?\$?$/;
4030
4032
  var SUGGESTIONS = {
4031
- "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.",
4032
- "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.",
4033
- "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.",
4034
- "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.",
4035
- "opaque-wrapper": "this wrapper (guard/withCtx/recover) contributes no first char; put a concrete leading terminal before it.",
4036
- "ref-cycle": "a recursive ref resolved to ANY; ensure the recursion has a concrete terminal lead on the base case."
4033
+ "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.",
4034
+ "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.",
4035
+ "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.",
4036
+ "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.",
4037
+ "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.",
4038
+ "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."
4037
4039
  };
4040
+ function refineSuggestion(cause, arm) {
4041
+ if (cause !== "broad-recognizer") return SUGGESTIONS[cause];
4042
+ const lead = peelToLeading(arm);
4043
+ const d = lead._def;
4044
+ if (d.tag === "regex" && KEYWORD_REGEX_RE.test(d.source)) {
4045
+ 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.
4046
+ 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.`;
4047
+ }
4048
+ if (d.tag === "scanTo") {
4049
+ 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.";
4050
+ }
4051
+ return SUGGESTIONS[cause];
4052
+ }
4038
4053
  function classifyBroadArm(arm, resolve) {
4039
4054
  const seen = /* @__PURE__ */ new Set();
4040
4055
  const relabel = (inner, cause, detail) => ({ cause, detail, ...inner?.unresolvedExternal ? { unresolvedExternal: true } : {} });
@@ -4150,7 +4165,6 @@ function peelToLeading(arm) {
4150
4165
  if (d.tag === "sequence" && d.parsers.length >= 1) return d.parsers[0];
4151
4166
  return cur;
4152
4167
  }
4153
- var KEYWORD_REGEX_RE = /^\^?[@#.-]?[A-Za-z][\w-]*(\(\?![^)]*\))?\$?$/;
4154
4168
  function detectAntiPatterns(rule, arms) {
4155
4169
  const out = [];
4156
4170
  arms.forEach((arm, i) => {
@@ -4163,14 +4177,14 @@ function detectAntiPatterns(rule, arms) {
4163
4177
  kind: "double-not",
4164
4178
  rule,
4165
4179
  armIndex: i,
4166
- 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."
4180
+ 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.'
4167
4181
  });
4168
4182
  } else {
4169
4183
  out.push({
4170
4184
  kind: "leading-not",
4171
4185
  rule,
4172
4186
  armIndex: i,
4173
- 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."
4187
+ 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."
4174
4188
  });
4175
4189
  }
4176
4190
  }
@@ -4179,7 +4193,7 @@ function detectAntiPatterns(rule, arms) {
4179
4193
  kind: "keyword-regex",
4180
4194
  rule,
4181
4195
  armIndex: i,
4182
- 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).`
4196
+ 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.`
4183
4197
  });
4184
4198
  }
4185
4199
  });
@@ -4221,7 +4235,7 @@ function analyzeGatingRules(ruleMap, opts) {
4221
4235
  const d = def;
4222
4236
  const rule = ruleNameOf2(p) ?? enclosingRule;
4223
4237
  if (d.tag === "choice") {
4224
- raw.push({ g: analyzeChoice(p, d, rule, opts?.resolveRef), rule });
4238
+ raw.push({ g: analyzeChoice(p, d, rule, opts?.resolveRef), rule, arms: d.parsers });
4225
4239
  antiPatterns.push(...detectAntiPatterns(rule, d.parsers));
4226
4240
  }
4227
4241
  const rec = d;
@@ -4250,13 +4264,15 @@ function analyzeGatingRules(ruleMap, opts) {
4250
4264
  const seenInRule = /* @__PURE__ */ new Map();
4251
4265
  const accept = new Set(opts?.accept ?? []);
4252
4266
  const usedAccept = /* @__PURE__ */ new Set();
4253
- const choices = raw.map(({ g, rule }) => {
4267
+ const choices = raw.map(({ g, rule, arms }) => {
4254
4268
  const n = seenInRule.get(rule) ?? 0;
4255
4269
  seenInRule.set(rule, n + 1);
4256
4270
  const id = (perRule.get(rule) ?? 1) === 1 ? rule : `${rule}#${n}`;
4257
4271
  const isAccepted = g.gates === "no" && accept.has(id);
4258
4272
  if (isAccepted) usedAccept.add(id);
4259
- return { ...g, id, accepted: isAccepted };
4273
+ const out = { ...g, id, accepted: isAccepted };
4274
+ Object.defineProperty(out, "arms", { value: arms, enumerable: false, writable: false });
4275
+ return out;
4260
4276
  });
4261
4277
  const gated = choices.filter((c) => c.gates === "yes").length;
4262
4278
  const recoverable = choices.filter((c) => c.gates === "recoverable").length;
@@ -4283,7 +4299,14 @@ function analyzeChoice(p, d, rule, resolve) {
4283
4299
  const deep = arms.map((a) => firstSetOf(a, /* @__PURE__ */ new Set(), resolve));
4284
4300
  const anyArms = deep.map((fs, index) => ({ fs, index })).filter((x) => isAny(x.fs)).map(({ index }) => {
4285
4301
  const { cause, detail, unresolvedExternal } = classifyBroadArm(arms[index], resolve);
4286
- return { index, cause, detail, shallowAnyOnly: false, unresolvedExternal: unresolvedExternal === true, suggestion: SUGGESTIONS[cause] };
4302
+ return {
4303
+ index,
4304
+ cause,
4305
+ detail,
4306
+ shallowAnyOnly: false,
4307
+ unresolvedExternal: unresolvedExternal === true,
4308
+ suggestion: refineSuggestion(cause, arms[index])
4309
+ };
4287
4310
  });
4288
4311
  const overlaps = [];
4289
4312
  for (let i = 0; i < deep.length; i++)
@@ -4293,7 +4316,7 @@ function analyzeChoice(p, d, rule, resolve) {
4293
4316
  a: i,
4294
4317
  b: j,
4295
4318
  on: intersection(deep[i], deep[j]),
4296
- 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."
4319
+ 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."
4297
4320
  });
4298
4321
  let gates;
4299
4322
  if (d.disjoint) gates = "yes";
@@ -6105,15 +6128,15 @@ function parseStringShape(source) {
6105
6128
  if (source.length < 3 || source[source.length - 1] !== q) return null;
6106
6129
  const inner = source.slice(1, -1);
6107
6130
  const CLS = String.raw`\[\^((?:\\.|[^\]])+)\]`;
6108
- const ESC = String.raw`\\\\(\.|\[\\s\\S\]|\[\\S\\s\])`;
6131
+ const ESC2 = String.raw`\\\\(\.|\[\\s\\S\]|\[\\S\\s\])`;
6109
6132
  let body;
6110
6133
  let esc;
6111
- let m = new RegExp(`^\\((?:\\?:)?${CLS}\\|${ESC}\\)\\*$`).exec(inner);
6134
+ let m = new RegExp(`^\\((?:\\?:)?${CLS}\\|${ESC2}\\)\\*$`).exec(inner);
6112
6135
  if (m) {
6113
6136
  body = m[1];
6114
6137
  esc = m[2];
6115
6138
  } else {
6116
- m = new RegExp(`^\\((?:\\?:)?${ESC}\\|${CLS}\\)\\*$`).exec(inner);
6139
+ m = new RegExp(`^\\((?:\\?:)?${ESC2}\\|${CLS}\\)\\*$`).exec(inner);
6117
6140
  if (m) {
6118
6141
  esc = m[1];
6119
6142
  body = m[2];
@@ -6123,8 +6146,8 @@ function parseStringShape(source) {
6123
6146
  const excluded = parseClassRanges(body);
6124
6147
  if (!excluded) return null;
6125
6148
  const qcp = q.codePointAt(0);
6126
- const inSet = (cp) => excluded.some(([lo, hi]) => cp >= lo && cp <= hi);
6127
- if (!inSet(qcp) || !inSet(BACKSLASH)) return null;
6149
+ const inSet2 = (cp) => excluded.some(([lo, hi]) => cp >= lo && cp <= hi);
6150
+ if (!inSet2(qcp) || !inSet2(BACKSLASH)) return null;
6128
6151
  return { kind: "string", quote: qcp, excluded, escLineTerm: esc !== "." };
6129
6152
  }
6130
6153
  function parseSeqParts(source) {
@@ -11583,7 +11606,7 @@ function cstBuildHost(typeOrOptions, children2, _fields, span, rawChildren, triv
11583
11606
  return host;
11584
11607
  }
11585
11608
  cstBuildHost._parsemanCstOutput = true;
11586
- function fusedBody(pieces) {
11609
+ function fusedBody(pieces, hoist) {
11587
11610
  for (const p of pieces) {
11588
11611
  if (p.v === void 0) {
11589
11612
  throw new Error(
@@ -11623,8 +11646,7 @@ function fusedBody(pieces) {
11623
11646
  const wrapperEntries = [...winner].map(([k, p]) => `${JSON.stringify(k)}: ${p.wrappers.get(k)}`);
11624
11647
  const reflection = mergeGrammarReflections([...winner].map(([k, p]) => p.nodeMeta.get(k) ?? { nodes: [] }));
11625
11648
  const { mode, elided } = hostModeOfPieces(pieces);
11626
- const rawBody = [
11627
- ...lines,
11649
+ const tailSrc = [
11628
11650
  "const _map = {",
11629
11651
  wrapperEntries.join(",\n"),
11630
11652
  "}",
@@ -11640,6 +11662,7 @@ function fusedBody(pieces) {
11640
11662
  "}",
11641
11663
  "return _map"
11642
11664
  ].join("\n");
11665
+ const rawBody = [...lines, tailSrc].join("\n");
11643
11666
  const ANY2 = { kind: "any" };
11644
11667
  const EMPTY3 = { kind: "empty" };
11645
11668
  const recipes = /* @__PURE__ */ new Map();
@@ -11680,7 +11703,7 @@ function fusedBody(pieces) {
11680
11703
  }
11681
11704
  if (!changed) break;
11682
11705
  }
11683
- const body = rawBody.replace(
11706
+ const resolveFS = (src) => src.replace(
11684
11707
  // Rule name + code-point var are both JS identifiers (rule names are validated
11685
11708
  // at compile time — see assertRuleName in codegen), so an identifier class
11686
11709
  // matches every well-formed placeholder.
@@ -11691,6 +11714,7 @@ function fusedBody(pieces) {
11691
11714
  return `(${firstSetCond(codevar, fs)})`;
11692
11715
  }
11693
11716
  );
11717
+ const body = hoist === void 0 ? resolveFS(rawBody) : [...hoist.claim(lines.map(resolveFS)), resolveFS(tailSrc)].join("\n");
11694
11718
  const env = {};
11695
11719
  for (const p of contributingPieces) {
11696
11720
  if (p.mfFns.length) env[`${p.ns}mf`] = p.mfFns;
@@ -11854,8 +11878,168 @@ function compose(items, opts) {
11854
11878
  if (coverageRules) Object.defineProperty(map, COMPOSED_COVERAGE_RULES, { value: coverageRules, enumerable: false });
11855
11879
  return map;
11856
11880
  }
11857
- function composeLeaf(_items) {
11858
- throw new Error("composeLeaf(): requires Parseman macro lowering; runtime composition is forbidden");
11881
+ function composeLeaf(items) {
11882
+ const pieces = items.flatMap(interpretedPieces);
11883
+ let fused;
11884
+ const map = {};
11885
+ for (const name of ruleNamesOf(pieces)) {
11886
+ Object.defineProperty(map, name, {
11887
+ enumerable: true,
11888
+ configurable: true,
11889
+ get: () => (fused ??= fusePieces(pieces))[name]
11890
+ });
11891
+ }
11892
+ Object.defineProperty(map, LEAF_COMPOSED, { value: true, enumerable: false });
11893
+ Object.defineProperty(map, INTERPRETED_PIECES, {
11894
+ value: pieces.filter((p) => p.plain).map((p) => p.entries),
11895
+ enumerable: false
11896
+ });
11897
+ return map;
11898
+ }
11899
+ function isRefSlot(c) {
11900
+ return c._def.tag === "lazy" && typeof c.define === "function";
11901
+ }
11902
+ var FUSE_ORIGINAL = /* @__PURE__ */ Symbol.for("parseman.interpretedFuseOriginal");
11903
+ var FUSE_TARGET = /* @__PURE__ */ Symbol.for("parseman.interpretedFuseTarget");
11904
+ var INTERPRETED_PIECES = /* @__PURE__ */ Symbol.for("parseman.interpretedPieces");
11905
+ function isInterpretedFuse(map) {
11906
+ return Array.isArray(map[INTERPRETED_PIECES]);
11907
+ }
11908
+ function definitionOf(entry) {
11909
+ if (isRefSlot(entry)) {
11910
+ const original = entry[FUSE_ORIGINAL];
11911
+ if (original !== void 0) return original ?? void 0;
11912
+ try {
11913
+ return entry._def.thunk();
11914
+ } catch {
11915
+ return void 0;
11916
+ }
11917
+ }
11918
+ if (entry._def.tag === "lazy") {
11919
+ try {
11920
+ entry._def.thunk();
11921
+ } catch {
11922
+ return void 0;
11923
+ }
11924
+ }
11925
+ return entry;
11926
+ }
11927
+ function repointRef(slot, target, name) {
11928
+ const tagged = slot;
11929
+ const bound = tagged[FUSE_TARGET];
11930
+ const original = FUSE_ORIGINAL in tagged ? tagged[FUSE_ORIGINAL] : (() => {
11931
+ try {
11932
+ return slot._def.thunk();
11933
+ } catch {
11934
+ return null;
11935
+ }
11936
+ })();
11937
+ if ((bound ?? original) === target) return;
11938
+ if (bound !== void 0) {
11939
+ throw new Error(
11940
+ `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.`
11941
+ );
11942
+ }
11943
+ if (!(FUSE_ORIGINAL in tagged)) Object.defineProperty(slot, FUSE_ORIGINAL, { value: original, enumerable: false });
11944
+ Object.defineProperty(slot, FUSE_TARGET, { value: target, enumerable: false });
11945
+ slot._def.thunk = () => target;
11946
+ slot.parse = (input, pos, ctx) => target.parse(input, pos, ctx);
11947
+ const meta = slot._meta;
11948
+ meta.firstSet = target._meta.firstSet;
11949
+ meta.canMatchNewline = target._meta.canMatchNewline;
11950
+ meta.isTrivia = target._meta.isTrivia;
11951
+ if (target._meta.triviaKindLabels !== void 0) meta.triviaKindLabels = target._meta.triviaKindLabels;
11952
+ else delete meta.triviaKindLabels;
11953
+ if (target._meta.disjoint !== void 0) meta.disjoint = target._meta.disjoint;
11954
+ else delete meta.disjoint;
11955
+ }
11956
+ function interpretedPieces(item) {
11957
+ const fused = item[INTERPRETED_PIECES];
11958
+ if (Array.isArray(fused)) return fused.map((entries) => ({ entries, plain: false }));
11959
+ const carried = composedPiecesOf(item);
11960
+ if (carried !== void 0) {
11961
+ const { maps, opaque } = carriedRuleMapsDetailed(carried);
11962
+ if (opaque.length > 0) {
11963
+ throw new Error(
11964
+ `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.`
11965
+ );
11966
+ }
11967
+ return maps.map((entries) => ({ entries, plain: false }));
11968
+ }
11969
+ if (item.ruleFns instanceof Map) {
11970
+ throw new Error("fuseInterpreted: a precompiled linkable artifact has no combinator graph to interpret; pass the source grammar (a rules() map) instead");
11971
+ }
11972
+ return [{ entries: Object.entries(item), plain: true }];
11973
+ }
11974
+ function fuseInterpreted(items, opts) {
11975
+ return fusePieces(items.flatMap(interpretedPieces), opts);
11976
+ }
11977
+ function ruleNamesOf(pieces) {
11978
+ const names = /* @__PURE__ */ new Set();
11979
+ for (const piece of pieces) {
11980
+ for (const [name, value] of piece.entries) if (definitionOf(value) !== void 0) names.add(name);
11981
+ }
11982
+ return [...names];
11983
+ }
11984
+ function fusePieces(pieces, opts) {
11985
+ let trivia2;
11986
+ for (let i = pieces.length - 1; i >= 0 && trivia2 === void 0; i--) {
11987
+ if (!pieces[i].plain) continue;
11988
+ for (const [, rule] of pieces[i].entries) {
11989
+ const t = rule._meta.grammarTrivia;
11990
+ if (t) {
11991
+ trivia2 = t;
11992
+ break;
11993
+ }
11994
+ }
11995
+ }
11996
+ const winner = /* @__PURE__ */ new Map();
11997
+ const entry = /* @__PURE__ */ new Map();
11998
+ for (const piece of pieces) {
11999
+ for (const [name, value] of piece.entries) {
12000
+ const def = definitionOf(value);
12001
+ if (def === void 0) continue;
12002
+ winner.set(name, def);
12003
+ entry.set(name, value);
12004
+ }
12005
+ }
12006
+ const missing = /* @__PURE__ */ new Set();
12007
+ for (const piece of pieces) {
12008
+ for (const [name, value] of piece.entries) {
12009
+ if (!isRefSlot(value)) continue;
12010
+ const target = winner.get(name);
12011
+ if (target === void 0) {
12012
+ missing.add(name);
12013
+ continue;
12014
+ }
12015
+ repointRef(value, target, name);
12016
+ }
12017
+ }
12018
+ if (missing.size > 0) throw new Error(missingRuleMessage(pieces, missing));
12019
+ const out = {};
12020
+ for (const [name, value] of entry) {
12021
+ out[name] = value;
12022
+ const meta = value._meta;
12023
+ if (trivia2 !== void 0 && !meta.isTrivia) meta.grammarTrivia = trivia2;
12024
+ if (opts?.hostMode !== void 0 && !meta.isTrivia) {
12025
+ if (opts.hostMode === "cst") meta.grammarHostMode = "cst";
12026
+ else delete meta.grammarHostMode;
12027
+ }
12028
+ }
12029
+ Object.defineProperty(out, INTERPRETED_PIECES, {
12030
+ value: pieces.filter((p) => p.plain).map((p) => p.entries),
12031
+ enumerable: false
12032
+ });
12033
+ return out;
12034
+ }
12035
+ function missingRuleMessage(pieces, missing) {
12036
+ for (const piece of pieces) {
12037
+ const deps = ruleDependencies(piece.entries.filter(([, v2]) => definitionOf(v2) !== void 0));
12038
+ for (const [name, ds] of deps) {
12039
+ for (const d of ds) if (missing.has(d)) return `fuseInterpreted: rule "${name}" references missing rule "${d}"`;
12040
+ }
12041
+ }
12042
+ return `fuseInterpreted: missing rule(s) ${[...missing].map((n) => `"${n}"`).join(", ")}`;
11859
12043
  }
11860
12044
 
11861
12045
  // src/analysis/grammar.ts
@@ -11905,13 +12089,16 @@ function diagnoseGrammar(grammar, opts) {
11905
12089
  }
11906
12090
  return assemble(report, degradations);
11907
12091
  }
12092
+ function examinedNothing(d) {
12093
+ return d.summary.totalChoices === 0 && d.summary.unanalysable > 0;
12094
+ }
11908
12095
  function formatGrammarDiagnosis(d) {
11909
12096
  const s = d.summary;
11910
12097
  const lines = [];
11911
12098
  lines.push(
11912
- 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} blocking finding(s) over ${s.totalChoices} examined choice(s).`
12099
+ 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).`
11913
12100
  );
11914
- if (s.unanalysable > 0) {
12101
+ if (s.unanalysable > 0 && !examinedNothing(d)) {
11915
12102
  lines.push(
11916
12103
  ` ${s.unanalysable} rule(s) UNANALYSABLE \u2014 THIS REPORT IS PARTIAL. An empty finding list below does NOT mean the grammar is clean.`
11917
12104
  );
@@ -12041,141 +12228,1081 @@ fix: ${o.suggestion}`)
12041
12228
  };
12042
12229
  }
12043
12230
 
12044
- // src/combinators/gate.ts
12045
- function gate(predicate) {
12046
- const meta = {
12047
- firstSet: { kind: "any" },
12048
- canMatchNewline: false,
12049
- isTrivia: false
12050
- };
12051
- return {
12052
- _tag: "guard",
12053
- _meta: meta,
12054
- _def: { tag: "guard", predicate },
12055
- parse(_input, pos, ctx) {
12056
- if (predicate(ctx.state))
12057
- return { ok: true, value: null, span: { start: pos, end: pos } };
12058
- return { ok: false, expected: ["guard"], span: { start: pos, end: pos } };
12059
- }
12060
- };
12061
- }
12062
-
12063
- // src/combinators/completions.ts
12064
- function completionsAt(target, input, offset, options = {}) {
12065
- const probe = { offset, best: null };
12066
- const ctx = { trackLines: false, _probe: probe, ...options.tolerant ? { _tolerant: true, _rec: REC } : {} };
12067
- const sliced = input.slice(0, offset);
12068
- const result = "parseWithContext" in target ? target.parseWithContext(sliced, ctx, 0) : target.parse(sliced, 0, ctx);
12069
- if (result.ok) return options.tolerant ? probe.best?.expected ?? [] : [];
12070
- const best = deeperFail(probe.best, result);
12071
- return best?.expected ?? [];
12072
- }
12073
- function deeperFail(a, b) {
12074
- if (!a) return b;
12075
- if (!b) return a;
12076
- return a.span.start >= b.span.start ? a : b;
12231
+ // src/functional/run.ts
12232
+ var SELECT_EXAMPLE = "run(entry, input, { rootTrivia: { select: ['blockComment', 'lineComment'] } })";
12233
+ var REMOVED_RUN_RESULT_FIELDS = [
12234
+ [
12235
+ "triviaMap",
12236
+ `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.`
12237
+ ],
12238
+ [
12239
+ "triviaLog",
12240
+ `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.`
12241
+ ],
12242
+ [
12243
+ "triviaKindLabels",
12244
+ `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\`.`
12245
+ ]
12246
+ ];
12247
+ function guardRemovedFields(result) {
12248
+ for (const [name, detail] of REMOVED_RUN_RESULT_FIELDS) {
12249
+ Object.defineProperty(result, name, {
12250
+ configurable: true,
12251
+ enumerable: false,
12252
+ get() {
12253
+ throw new TypeError(
12254
+ `RunResult.${name} was REMOVED in parseman 0.44.0 and has no default replacement. ${detail}`
12255
+ );
12256
+ }
12257
+ });
12258
+ }
12259
+ return result;
12077
12260
  }
12078
-
12079
- // src/cst/relative-spans.ts
12080
- function relativize(node2, parentBase = 0) {
12081
- return {
12082
- start: node2.start - parentBase,
12083
- end: node2.end - parentBase,
12084
- children: node2.children.map((c) => relativize(c, node2.start))
12085
- };
12261
+ var invoke = (r, input, pos, ctx) => typeof r === "function" ? r(input, pos, ctx) : r.parse(input, pos, ctx);
12262
+ function triviaKindLabelsFromRunnable(r) {
12263
+ if (!r) return void 0;
12264
+ if (typeof r === "function") return r._meta?.triviaKindLabels;
12265
+ const grammarTrivia = r._meta.grammarTrivia;
12266
+ return grammarTrivia?._meta.triviaKindLabels ?? r._meta.triviaKindLabels ?? (r._def.tag === "grammar" ? r._def.triviaParser?._meta.triviaKindLabels : void 0);
12086
12267
  }
12087
- function absolutize(node2, parentBase = 0) {
12088
- const start = parentBase + node2.start;
12089
- return {
12090
- start,
12091
- end: parentBase + node2.end,
12092
- children: node2.children.map((c) => absolutize(c, start))
12093
- };
12268
+ function rootTriviaClassifiedFromRunnable(r) {
12269
+ if (!r) return false;
12270
+ if (typeof r === "function") return r._meta?.rootTriviaClassified === true;
12271
+ const grammarTrivia = r._meta.grammarTrivia;
12272
+ return grammarTrivia?._meta.rootTriviaClassified === true || r._meta.rootTriviaClassified === true;
12094
12273
  }
12095
- function absoluteSpanAt(root, path) {
12096
- let base = 0;
12097
- let node2 = root;
12098
- for (const idx of path) {
12099
- base += node2.start;
12100
- const child = node2.children[idx];
12101
- if (!child) throw new RangeError(`absoluteSpanAt: no child ${idx} on path ${path.join(".")}`);
12102
- node2 = child;
12274
+ function makeSelectedRootLabelIndex(labels) {
12275
+ const index = /* @__PURE__ */ Object.create(null);
12276
+ for (let i = 0; i < labels.length; i++) {
12277
+ if (index[labels[i]] === void 0) index[labels[i]] = i;
12103
12278
  }
12104
- return { start: base + node2.start, end: base + node2.end };
12279
+ return index;
12105
12280
  }
12106
- function relativizeCST(node2, parentBase = 0) {
12107
- const span = { start: node2.span.start - parentBase, end: node2.span.end - parentBase };
12108
- if (node2.children) {
12109
- const base = node2.span.start;
12110
- return { ...node2, span, children: node2.children.map((c) => relativizeCST(c, base)) };
12281
+ function runOnce(entry, input, options, phase, profileState) {
12282
+ if (typeof entry !== "function" && typeof entry?.parse !== "function") {
12283
+ throw new TypeError(
12284
+ `run(): start production is ${entry === null ? "null" : typeof entry}, not a rule \u2014 the requested grammar rule does not exist (check the rule name).`
12285
+ );
12111
12286
  }
12112
- return { ...node2, span };
12113
- }
12114
- function absolutizeCST(node2, parentBase = 0) {
12115
- const start = parentBase + node2.span.start;
12116
- const span = { start, end: parentBase + node2.span.end };
12117
- if (node2.children) {
12118
- return { ...node2, span, children: node2.children.map((c) => absolutizeCST(c, start)) };
12287
+ const rootTriviaSelection = options.rootTrivia?.select;
12288
+ const captureRootTrivia = rootTriviaSelection !== void 0 && rootTriviaSelection.length > 0;
12289
+ const selectedRootLog = captureRootTrivia ? [] : void 0;
12290
+ const selectedRootLabelIndex = captureRootTrivia ? makeSelectedRootLabelIndex(rootTriviaSelection) : void 0;
12291
+ const errors = [];
12292
+ const profile = profileState ?? (phase === void 0 ? void 0 : { phase, nodes: 0, childSlots: 0, rawSlots: 0, triviaSlots: 0, fieldSlots: 0, hostCalls: 0 });
12293
+ const skipGlobalSinks = phase === "recognizer" || phase === "capture";
12294
+ const grammarTrivia = typeof entry !== "function" ? entry._meta.grammarTrivia : void 0;
12295
+ const grammarScanSkip = typeof entry !== "function" ? entry._meta.grammarScanSkip : void 0;
12296
+ const triviaKindLabels = triviaKindLabelsFromRunnable(entry) ?? triviaKindLabelsFromRunnable(options.trivia);
12297
+ if (captureRootTrivia && triviaKindLabels === void 0) {
12298
+ throw new TypeError("run(): rootTrivia.select requires labeled grammar trivia.");
12119
12299
  }
12120
- return { ...node2, span };
12121
- }
12122
- function absoluteSpanCST(root, path) {
12123
- let base = 0;
12124
- let node2 = root;
12125
- for (const idx of path) {
12126
- base += node2.span.start;
12127
- const child = node2.children?.[idx];
12128
- if (!child) throw new RangeError(`absoluteSpanCST: no child ${idx} on path ${path.join(".")}`);
12129
- node2 = child;
12300
+ if (captureRootTrivia) {
12301
+ for (const label2 of rootTriviaSelection) {
12302
+ if (!triviaKindLabels.includes(label2)) {
12303
+ throw new TypeError(`run(): rootTrivia.select contains unknown trivia label ${JSON.stringify(label2)}.`);
12304
+ }
12305
+ }
12130
12306
  }
12131
- return { start: base + node2.span.start, end: base + node2.span.end };
12132
- }
12133
- function shiftAbsolute(node2, at, delta) {
12134
- const s = node2.start >= at ? node2.start + delta : node2.start;
12135
- const e = node2.end >= at ? node2.end + delta : node2.end;
12136
- return {
12137
- start: s,
12138
- end: e,
12139
- children: node2.children.map((c) => shiftAbsolute(c, at, delta))
12140
- };
12141
- }
12142
- function applyEdit(root, at, delta, stats) {
12143
- return editNode(root, at, delta, 0, 0, stats);
12144
- }
12145
- function editNode(node2, at, delta, parentBase, parentShift, stats) {
12146
- const absStart = parentBase + node2.start;
12147
- const absEnd = parentBase + node2.end;
12148
- if (absEnd < at) return node2;
12149
- const startShifts = absStart >= at;
12150
- if (startShifts && parentShift === delta) return node2;
12151
- if (stats) stats.allocated++;
12152
- const newAbsStart = startShifts ? absStart + delta : absStart;
12153
- const newAbsEnd = absEnd + delta;
12154
- const newParentBase = parentBase + parentShift;
12155
- const newStart = newAbsStart - newParentBase;
12156
- const newEnd = newAbsEnd - newParentBase;
12157
- const ownShift = newAbsStart - absStart;
12158
- let changed = false;
12159
- const newChildren = [];
12160
- for (const c of node2.children) {
12161
- const nc = editNode(c, at, delta, absStart, ownShift, stats);
12162
- if (nc !== c) changed = true;
12163
- newChildren.push(nc);
12307
+ if (captureRootTrivia && !rootTriviaClassifiedFromRunnable(entry) && !rootTriviaClassifiedFromRunnable(options.trivia)) {
12308
+ throw new TypeError(
12309
+ "run(): rootTrivia.select requires classifiedTrivia() at the root."
12310
+ );
12164
12311
  }
12165
- return {
12166
- start: newStart,
12167
- end: newEnd,
12168
- children: changed ? newChildren : node2.children
12169
- };
12170
- }
12171
-
12172
- // src/functional/doc.ts
12173
- function mkCtx(rawState, build, tolerant) {
12174
- const state = rawState ?? void 0;
12175
- return tolerant ? { trackLines: false, state, build, _tolerant: true, _rec: REC, _errors: [] } : { trackLines: false, state, build };
12176
- }
12177
- function asRuleFn(entry) {
12178
- return typeof entry === "function" ? entry : (input, pos, ctx) => entry.parse(input, pos, ctx);
12312
+ const stamped = entry;
12313
+ assertHostModeCompatible(
12314
+ typeof entry === "function" ? stamped[FUSED_HOST_MODE] ?? "ast" : entry._meta.grammarHostMode ?? "ast",
12315
+ options.build,
12316
+ typeof entry === "function" && stamped[FUSED_HOST_ELIDED] === true
12317
+ );
12318
+ const ctx = {
12319
+ trackLines: false,
12320
+ ...skipGlobalSinks ? { _pmProfile: profile } : {
12321
+ ...captureRootTrivia ? {
12322
+ _rootTriviaLog: selectedRootLog,
12323
+ _rootTriviaKindIndex: selectedRootLabelIndex,
12324
+ _rootTriviaStrictScopes: true
12325
+ } : {},
12326
+ _errors: errors,
12327
+ ...profile === void 0 ? {} : { _pmProfile: profile }
12328
+ },
12329
+ build: options.build,
12330
+ state: options.state,
12331
+ ...grammarTrivia !== void 0 ? { trivia: grammarTrivia, ...triviaKindLabels ? { triviaKindLabels } : {} } : triviaKindLabels ? { triviaKindLabels } : {},
12332
+ ...grammarScanSkip !== void 0 ? { scanSkip: grammarScanSkip } : {},
12333
+ ...options.triviaCaptureMask !== void 0 ? { _triviaCaptureMask: options.triviaCaptureMask } : {},
12334
+ ...options.tolerant ? { _tolerant: true, _rec: REC } : {},
12335
+ ...options.instrumentation === void 0 ? {} : options.instrumentation
12336
+ };
12337
+ const r = invoke(entry, input, 0, ctx);
12338
+ let unconsumedFrom = null;
12339
+ if (r.ok) {
12340
+ let pos = r.span?.end ?? 0;
12341
+ if (options.trivia && pos < input.length) {
12342
+ const t = invoke(options.trivia, input, pos, { trackLines: false });
12343
+ if (t.ok && t.span.end > pos) pos = t.span.end;
12344
+ }
12345
+ unconsumedFrom = pos < input.length ? pos : null;
12346
+ }
12347
+ return {
12348
+ ok: r.ok,
12349
+ value: r.ok ? r.value : void 0,
12350
+ span: r.span ?? { start: 0, end: 0 },
12351
+ expected: r.ok ? [] : r.expected ?? [],
12352
+ errors,
12353
+ ...selectedRootLog !== void 0 && selectedRootLog.length > 0 ? {
12354
+ rootTrivia: {
12355
+ rows: selectedRootLog,
12356
+ select: rootTriviaSelection,
12357
+ index: buildRootTriviaIndex(selectedRootLog, rootTriviaSelection)
12358
+ }
12359
+ } : {},
12360
+ unconsumedFrom
12361
+ };
12362
+ }
12363
+ function run(entry, input, options = {}) {
12364
+ if (!options.profile) return guardRemovedFields(runOnce(entry, input, options));
12365
+ throw new TypeError(
12366
+ "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."
12367
+ );
12368
+ }
12369
+
12370
+ // src/analysis/choice-cost.ts
12371
+ var choiceSiteKey = (s) => s.path === "" ? s.rule : `${s.rule} \u203A ${s.path}`;
12372
+ function childrenOf5(d) {
12373
+ switch (d.tag) {
12374
+ case "sequence":
12375
+ case "choice":
12376
+ return d.parsers;
12377
+ case "dispatch":
12378
+ return [
12379
+ d.selector,
12380
+ ...d.cases.map((c) => c.parser),
12381
+ ...d.otherwise === void 0 ? [] : [d.otherwise]
12382
+ ];
12383
+ case "skip":
12384
+ return [d.main, d.skipped];
12385
+ case "sepBy":
12386
+ return [d.parser, d.separator];
12387
+ case "recover":
12388
+ return [d.parser, d.sentinel];
12389
+ case "scanTo":
12390
+ return [d.sentinel, ...d.skip];
12391
+ case "grammar":
12392
+ return d.triviaParser ? [d.parser, d.triviaParser] : [d.parser];
12393
+ case "lazy":
12394
+ case "literal":
12395
+ case "regex":
12396
+ case "keywords":
12397
+ case "guard":
12398
+ case "unknown":
12399
+ return [];
12400
+ default: {
12401
+ const rec = d;
12402
+ return rec.parser ? [rec.parser] : [];
12403
+ }
12404
+ }
12405
+ }
12406
+ var ruleNameOf5 = (p) => p._ruleName;
12407
+ function slotLabel(d, index) {
12408
+ switch (d.tag) {
12409
+ case "sequence":
12410
+ return `seq[${index}]`;
12411
+ case "choice":
12412
+ return `choice[${index}]`;
12413
+ case "dispatch":
12414
+ return index === 0 ? "dispatch.selector" : `dispatch[${index - 1}]`;
12415
+ case "skip":
12416
+ return index === 0 ? "skip.main" : "skip.skipped";
12417
+ case "sepBy":
12418
+ return index === 0 ? "sepBy.item" : "sepBy.sep";
12419
+ case "recover":
12420
+ return index === 0 ? "recover.parser" : "recover.sentinel";
12421
+ case "scanTo":
12422
+ return index === 0 ? "scanTo.sentinel" : `scanTo.skip[${index - 1}]`;
12423
+ case "grammar":
12424
+ return index === 0 ? "grammar" : "grammar.trivia";
12425
+ case "node":
12426
+ return `node(${d.type ?? "anonymous"})`;
12427
+ case "label":
12428
+ return `label(${d.label})`;
12429
+ case "field":
12430
+ return `field(${d.name})`;
12431
+ default:
12432
+ return d.tag;
12433
+ }
12434
+ }
12435
+ function resolveRoot(p) {
12436
+ let cur = p;
12437
+ for (let hop = 0; hop < 8; hop++) {
12438
+ const d = cur._def;
12439
+ if (d.tag !== "lazy") return { ok: true, p: cur };
12440
+ let next;
12441
+ try {
12442
+ next = d.thunk();
12443
+ } catch {
12444
+ return { ok: false };
12445
+ }
12446
+ if (next === cur) return { ok: false };
12447
+ cur = next;
12448
+ }
12449
+ return { ok: false };
12450
+ }
12451
+ function walkRules(ruleMap) {
12452
+ assertAnalysable(ruleMap);
12453
+ const out = [];
12454
+ const seen = /* @__PURE__ */ new Set();
12455
+ const unresolvedRoots = [];
12456
+ const visit = (p, rule, path) => {
12457
+ if (seen.has(p)) return;
12458
+ seen.add(p);
12459
+ const d = p._def;
12460
+ const own = ruleNameOf5(p);
12461
+ const here = own !== void 0 && own !== rule ? { rule: own, path: "" } : { rule, path };
12462
+ out.push({ p, d, site: here });
12463
+ const kids = childrenOf5(d);
12464
+ for (let i = 0; i < kids.length; i++) {
12465
+ const seg = slotLabel(d, i);
12466
+ visit(kids[i], here.rule, here.path === "" ? seg : `${here.path} \u203A ${seg}`);
12467
+ }
12468
+ };
12469
+ for (const [name, rule] of ruleMap) {
12470
+ const r = resolveRoot(rule);
12471
+ if (!r.ok) {
12472
+ unresolvedRoots.push(name);
12473
+ continue;
12474
+ }
12475
+ visit(r.p, name, "");
12476
+ }
12477
+ unresolvedRoots.sort();
12478
+ return { walked: out, unresolvedRoots };
12479
+ }
12480
+ function assertAnalysable(ruleMap) {
12481
+ if (ruleMap.length === 0) {
12482
+ throw new TypeError(
12483
+ "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)."
12484
+ );
12485
+ }
12486
+ for (const [name, rule] of ruleMap) {
12487
+ const def = rule?._def;
12488
+ if (def !== null && typeof def === "object" && typeof def.tag === "string") continue;
12489
+ throw new TypeError(
12490
+ `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.`
12491
+ );
12492
+ }
12493
+ }
12494
+ function leadingTermOfArm2(arm) {
12495
+ let d = arm._def;
12496
+ for (; ; ) {
12497
+ if (d.tag === "node" || d.tag === "grammar" || d.tag === "transform" || d.tag === "label") {
12498
+ d = d.parser._def;
12499
+ continue;
12500
+ }
12501
+ break;
12502
+ }
12503
+ if (d.tag !== "sequence") {
12504
+ return {
12505
+ reason: "not-a-sequence",
12506
+ detail: `arm is \`${d.tag}\`; only node/grammar/transform/label wrappers are peeled, because they are the ones that consume nothing before the sequence`
12507
+ };
12508
+ }
12509
+ if (d.parsers.length < 2) {
12510
+ return { reason: "sequence-shorter-than-2", detail: `sequence has ${d.parsers.length} term(s); nothing would remain after factoring` };
12511
+ }
12512
+ return { term: d.parsers[0] };
12513
+ }
12514
+ function leadingTermKey(term) {
12515
+ const d = term._def;
12516
+ if (d.tag === "literal") {
12517
+ if (d.caseInsensitive) {
12518
+ 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` };
12519
+ }
12520
+ return { key: `L:${JSON.stringify(d.value)}`, render: JSON.stringify(d.value) };
12521
+ }
12522
+ if (d.tag === "regex") {
12523
+ return { key: `R:${JSON.stringify([d.source, d.flags])}`, render: `/${d.source}/${d.flags.replace(/[gy]/g, "")}` };
12524
+ }
12525
+ return {
12526
+ reason: "lead-not-concrete-terminal",
12527
+ 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`
12528
+ };
12529
+ }
12530
+ function armLabel(arm) {
12531
+ const name = ruleNameOf5(arm);
12532
+ if (name !== void 0) return name;
12533
+ let d = arm._def;
12534
+ for (let hop = 0; hop < 8; hop++) {
12535
+ if (d.tag === "node" && d.type !== void 0) return `node(${d.type})`;
12536
+ if (d.tag === "label") return `label(${d.label})`;
12537
+ if (d.tag === "field") return `field(${d.name})`;
12538
+ if (d.tag === "literal") return JSON.stringify(d.value);
12539
+ if (d.tag === "regex") return `/${d.source}/`;
12540
+ if (d.tag === "keywords") return `keywords(${d.words.length})`;
12541
+ if (d.tag === "sequence" || d.tag === "choice") {
12542
+ const first = d.parsers[0];
12543
+ if (first === void 0) return d.tag;
12544
+ const inner = ruleNameOf5(first);
12545
+ if (inner !== void 0) return `${d.tag}(${inner}, \u2026)`;
12546
+ d = first._def;
12547
+ continue;
12548
+ }
12549
+ const rec = d;
12550
+ if (rec.parser === void 0) return d.tag;
12551
+ d = rec.parser._def;
12552
+ }
12553
+ return arm._def.tag;
12554
+ }
12555
+ function analyzeChoiceInventory(ruleMap) {
12556
+ const { walked, unresolvedRoots } = walkRules(ruleMap);
12557
+ const entries = [];
12558
+ for (const w of walked) {
12559
+ if (w.d.tag !== "choice") continue;
12560
+ const d = w.d;
12561
+ const arity = d.parsers.length;
12562
+ const strategy = d.strategy.tag;
12563
+ const gated = d.gates.some((g) => g !== null);
12564
+ const byKey = /* @__PURE__ */ new Map();
12565
+ const armDeclines = [];
12566
+ for (let i = 0; i < arity; i++) {
12567
+ const lead = leadingTermOfArm2(d.parsers[i]);
12568
+ if ("reason" in lead) {
12569
+ armDeclines.push({ arm: i, reason: lead.reason, detail: lead.detail });
12570
+ continue;
12571
+ }
12572
+ const keyed = leadingTermKey(lead.term);
12573
+ if ("reason" in keyed) {
12574
+ armDeclines.push({ arm: i, reason: keyed.reason, detail: keyed.detail });
12575
+ continue;
12576
+ }
12577
+ const slot = byKey.get(keyed.key);
12578
+ if (slot === void 0) byKey.set(keyed.key, { render: keyed.render, members: [i] });
12579
+ else slot.members.push(i);
12580
+ }
12581
+ 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));
12582
+ const factored = strategy === "sharedPrefix";
12583
+ let declineReason;
12584
+ if (!factored) {
12585
+ 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";
12586
+ }
12587
+ const inGroups = /* @__PURE__ */ new Set();
12588
+ for (const g of groups) for (const m of g.members) inGroups.add(m);
12589
+ entries.push({
12590
+ site: w.site,
12591
+ siteKey: choiceSiteKey(w.site),
12592
+ arity,
12593
+ strategy,
12594
+ disjoint: d.disjoint,
12595
+ gated,
12596
+ groups,
12597
+ armDeclines,
12598
+ factored,
12599
+ ...declineReason === void 0 ? {} : { declineReason },
12600
+ unfactoredArms: factored ? 0 : inGroups.size
12601
+ });
12602
+ }
12603
+ entries.sort((a, b) => a.siteKey < b.siteKey ? -1 : a.siteKey > b.siteKey ? 1 : 0);
12604
+ const backlog = entries.filter((e) => !e.factored && e.groups.length > 0 && e.declineReason !== "disjoint-dispatch" && e.declineReason !== "strategy-preempted");
12605
+ return {
12606
+ schema: "parseman.choice-inventory/1",
12607
+ rules: ruleMap.length,
12608
+ choiceSites: entries.length,
12609
+ factoredSites: entries.filter((e) => e.factored).length,
12610
+ backlogSites: backlog.length,
12611
+ backlogArms: backlog.reduce((n, e) => n + e.unfactoredArms, 0),
12612
+ unresolvedRoots,
12613
+ entries
12614
+ };
12615
+ }
12616
+ function compiledFirstCharGate(p) {
12617
+ const fs = p._meta.firstSet;
12618
+ if (fs.kind !== "any" && !canMatchEmptyAtStart2(p)) return fs;
12619
+ if (fs.kind !== "any" || canMatchEmptyAtStart2(p)) return null;
12620
+ let deep;
12621
+ try {
12622
+ deep = firstSetOf(p);
12623
+ } catch {
12624
+ return null;
12625
+ }
12626
+ return deep.kind === "ranges" ? deep : null;
12627
+ }
12628
+ function canMatchEmptyAtStart2(p) {
12629
+ const d = p._def;
12630
+ switch (d.tag) {
12631
+ case "regex":
12632
+ return regexMatchesEmpty2(d.source, d.flags);
12633
+ case "optional":
12634
+ case "many":
12635
+ return true;
12636
+ case "transform":
12637
+ case "label":
12638
+ case "field":
12639
+ return canMatchEmptyAtStart2(d.parser);
12640
+ case "literal":
12641
+ return d.value.length === 0;
12642
+ default:
12643
+ return false;
12644
+ }
12645
+ }
12646
+ function regexMatchesEmpty2(source, flags) {
12647
+ try {
12648
+ return new RegExp(`^(?:${source})$`, flags.replace(/[gy]/g, "")).test("");
12649
+ } catch {
12650
+ return true;
12651
+ }
12652
+ }
12653
+ function inSet(code, fs) {
12654
+ if (fs.kind === "any") return true;
12655
+ if (fs.kind === "empty") return false;
12656
+ for (const r of fs.ranges) if (code >= r.lo && code <= r.hi) return true;
12657
+ return false;
12658
+ }
12659
+ var TERMINAL_TAGS = /* @__PURE__ */ new Set(["literal", "regex", "keywords", "scanTo"]);
12660
+ var INSTRUMENTABLE = /* @__PURE__ */ new Set(["firstMatch", "sharedPrefix"]);
12661
+ function profileWastedWork(opts) {
12662
+ const { rules: rules2, corpus } = opts;
12663
+ const { walked, unresolvedRoots } = walkRules(rules2);
12664
+ const entryParser = typeof opts.entry === "string" ? rules2.find(([n]) => n === opts.entry)?.[1] : opts.entry;
12665
+ if (entryParser === void 0) {
12666
+ throw new TypeError(
12667
+ `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" : ""}).`
12668
+ );
12669
+ }
12670
+ if (corpus.length === 0) {
12671
+ 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.");
12672
+ }
12673
+ for (const f of corpus) {
12674
+ if (f.text.length === 0) throw new TypeError(`choice-cost: corpus entry '${f.id}' is EMPTY.`);
12675
+ }
12676
+ let highWater = 0;
12677
+ const armRecords = [];
12678
+ const siteTotals = /* @__PURE__ */ new Map();
12679
+ const restore = [];
12680
+ const undoAll = () => {
12681
+ for (let i = restore.length - 1; i >= 0; i--) restore[i]();
12682
+ };
12683
+ let instrumentedSites = 0;
12684
+ let uninstrumentableSites = 0;
12685
+ try {
12686
+ for (const w of walked) {
12687
+ if (w.d.tag !== "choice") continue;
12688
+ const siteKey = choiceSiteKey(w.site);
12689
+ const strategy = w.d.strategy.tag;
12690
+ const canInstrument = INSTRUMENTABLE.has(strategy) && !w.d.disjoint;
12691
+ const rec = {
12692
+ siteKey,
12693
+ site: w.site,
12694
+ strategy,
12695
+ arity: w.d.parsers.length,
12696
+ instrumented: canInstrument,
12697
+ attempts: 0,
12698
+ failures: 0,
12699
+ wastedBytes: 0,
12700
+ gatedAttempts: 0,
12701
+ gatedFailures: 0,
12702
+ gatedWastedBytes: 0
12703
+ };
12704
+ if (siteTotals.has(siteKey)) {
12705
+ uninstrumentableSites++;
12706
+ continue;
12707
+ }
12708
+ siteTotals.set(siteKey, rec);
12709
+ if (!canInstrument) {
12710
+ uninstrumentableSites++;
12711
+ continue;
12712
+ }
12713
+ instrumentedSites++;
12714
+ const slots = w.d.parsers;
12715
+ for (let i = 0; i < slots.length; i++) {
12716
+ const original = slots[i];
12717
+ const gate2 = compiledFirstCharGate(original);
12718
+ const armRec = {
12719
+ site: w.site,
12720
+ siteKey,
12721
+ arm: i,
12722
+ label: armLabel(original),
12723
+ gate: gate2,
12724
+ attempts: 0,
12725
+ failures: 0,
12726
+ wastedBytes: 0,
12727
+ gatedAttempts: 0,
12728
+ gatedFailures: 0,
12729
+ gatedWastedBytes: 0
12730
+ };
12731
+ armRecords.push(armRec);
12732
+ const wrapper = {
12733
+ _tag: original._tag,
12734
+ _meta: original._meta,
12735
+ _def: original._def,
12736
+ parse(input, pos, ctx) {
12737
+ const code = pos < input.length ? input.codePointAt(pos) ?? -1 : -1;
12738
+ const entered = gate2 === null || code >= 0 && inSet(code, gate2);
12739
+ const saved = highWater;
12740
+ highWater = pos;
12741
+ const r = original.parse(input, pos, ctx);
12742
+ const reach = highWater;
12743
+ highWater = saved > reach ? saved : reach;
12744
+ armRec.attempts++;
12745
+ rec.attempts++;
12746
+ if (entered) {
12747
+ armRec.gatedAttempts++;
12748
+ rec.gatedAttempts++;
12749
+ }
12750
+ if (!r.ok) {
12751
+ armRec.failures++;
12752
+ rec.failures++;
12753
+ if (entered) {
12754
+ armRec.gatedFailures++;
12755
+ rec.gatedFailures++;
12756
+ }
12757
+ const wasted = reach - pos;
12758
+ if (wasted > 0) {
12759
+ armRec.wastedBytes += wasted;
12760
+ rec.wastedBytes += wasted;
12761
+ if (entered) {
12762
+ armRec.gatedWastedBytes += wasted;
12763
+ rec.gatedWastedBytes += wasted;
12764
+ }
12765
+ }
12766
+ }
12767
+ return r;
12768
+ }
12769
+ };
12770
+ const rn = ruleNameOf5(original);
12771
+ if (rn !== void 0) wrapper._ruleName = rn;
12772
+ slots[i] = wrapper;
12773
+ restore.push(() => {
12774
+ slots[i] = original;
12775
+ });
12776
+ }
12777
+ }
12778
+ if (instrumentedSites === 0) {
12779
+ throw new TypeError(
12780
+ "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."
12781
+ );
12782
+ }
12783
+ for (const w of walked) {
12784
+ if (!TERMINAL_TAGS.has(w.d.tag)) continue;
12785
+ const p = w.p;
12786
+ const original = p.parse;
12787
+ p.parse = ((input, pos, ctx) => {
12788
+ if (pos > highWater) highWater = pos;
12789
+ return original.call(w.p, input, pos, ctx);
12790
+ });
12791
+ restore.push(() => {
12792
+ p.parse = original;
12793
+ });
12794
+ }
12795
+ } catch (e) {
12796
+ undoAll();
12797
+ throw e;
12798
+ }
12799
+ let parsedOk = 0;
12800
+ let parsedFailed = 0;
12801
+ let corpusBytes = 0;
12802
+ try {
12803
+ const runner = opts.runner ?? ((e, input, options) => run(e, input, options));
12804
+ for (const f of corpus) {
12805
+ corpusBytes += f.text.length;
12806
+ highWater = 0;
12807
+ const r = runner(entryParser, f.text, opts.runOptions ?? {});
12808
+ if (r.ok) parsedOk++;
12809
+ else parsedFailed++;
12810
+ }
12811
+ } finally {
12812
+ undoAll();
12813
+ }
12814
+ const arms = armRecords.filter((a) => a.attempts > 0).map((a) => ({
12815
+ siteKey: a.siteKey,
12816
+ site: a.site,
12817
+ arm: a.arm,
12818
+ label: a.label,
12819
+ attempts: a.attempts,
12820
+ failures: a.failures,
12821
+ wastedBytes: a.wastedBytes,
12822
+ firstCharGated: a.gate !== null,
12823
+ gatedAttempts: a.gatedAttempts,
12824
+ gatedFailures: a.gatedFailures,
12825
+ gatedWastedBytes: a.gatedWastedBytes
12826
+ })).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);
12827
+ 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));
12828
+ const minAttempts = opts.inversionMinAttempts ?? 4;
12829
+ 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);
12830
+ return {
12831
+ schema: "parseman.wasted-work/1",
12832
+ corpusFiles: corpus.length,
12833
+ corpusBytes,
12834
+ parsedOk,
12835
+ parsedFailed,
12836
+ instrumentedSites,
12837
+ uninstrumentableSites,
12838
+ unresolvedRoots,
12839
+ totalWastedBytes: sites.reduce((n, s) => n + s.wastedBytes, 0),
12840
+ totalGatedWastedBytes: sites.reduce((n, s) => n + s.gatedWastedBytes, 0),
12841
+ arms,
12842
+ inversions,
12843
+ sites
12844
+ };
12845
+ }
12846
+
12847
+ // src/analysis/choice-cost-render.ts
12848
+ function groupDigits(n) {
12849
+ const s = String(Math.trunc(Math.abs(n)));
12850
+ let out = "";
12851
+ for (let i = 0; i < s.length; i++) {
12852
+ if (i > 0 && (s.length - i) % 3 === 0) out += ",";
12853
+ out += s[i];
12854
+ }
12855
+ return (n < 0 ? "-" : "") + out;
12856
+ }
12857
+ function bytes(n) {
12858
+ if (n < 1024) return `${groupDigits(n)} B`;
12859
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} kB`;
12860
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
12861
+ }
12862
+ var ESC = "\x1B";
12863
+ var ANSI = {
12864
+ dim: `${ESC}[2m`,
12865
+ bold: `${ESC}[1m`,
12866
+ red: `${ESC}[31m`,
12867
+ yellow: `${ESC}[33m`,
12868
+ cyan: `${ESC}[36m`,
12869
+ reset: `${ESC}[0m`
12870
+ };
12871
+ var paint = (on) => (code, s) => on ? `${ANSI[code]}${s}${ANSI.reset}` : s;
12872
+ var pad = (s, w) => s.length >= w ? s : s + " ".repeat(w - s.length);
12873
+ var padStart = (s, w) => s.length >= w ? s : " ".repeat(w - s.length) + s;
12874
+ var ARM_DECLINE_LINE = {
12875
+ "not-a-sequence": "arm is not a sequence, so there is no leading term to lift out",
12876
+ "sequence-shorter-than-2": "arm is a one-term sequence; lifting its only term would leave an empty arm",
12877
+ "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",
12878
+ "lead-not-concrete-terminal": "leading term is not a bare literal or regex; lifting it would change the arm\u2019s value or capture shape"
12879
+ };
12880
+ var SITE_DECLINE_LINE = {
12881
+ "disjoint-dispatch": "first-char dispatch already selects one arm \u2014 nothing is re-scanned",
12882
+ "gated-arms": "an arm carries a runtime gate; per-arm predicates are incompatible with factoring",
12883
+ "strategy-preempted": "a stronger non-backtracking strategy already applies",
12884
+ "fewer-than-two-arms": "fewer than two arms",
12885
+ "arms-not-factorable": "at least one arm cannot contribute a leading term (see below)",
12886
+ "leads-differ": "the arms do not all begin with the same term"
12887
+ };
12888
+ function leftFactorPreview(e) {
12889
+ if (e.factored) return null;
12890
+ if (e.groups.length !== 1) return null;
12891
+ const g = e.groups[0];
12892
+ if (g.members.length !== e.arity) return null;
12893
+ const tails = g.members.map((i) => `sequence(\u2026arm[${i}] tail\u2026)`).join(",\n ");
12894
+ return `sequence(
12895
+ ${g.render === "" ? "<prefix>" : g.render.startsWith("/") ? `regex(${g.render})` : `literal(${g.render})`},
12896
+ choice(
12897
+ ${tails},
12898
+ ),
12899
+ )`;
12900
+ }
12901
+ function renderChoiceInventory(r, opts = {}) {
12902
+ const c = paint(opts.color === true);
12903
+ const limit = opts.limit ?? 20;
12904
+ const out = [];
12905
+ out.push(c("bold", "shared-prefix inventory"));
12906
+ out.push(c("dim", " Static grammar shape only. A site listed here may already cost nothing at runtime:"));
12907
+ out.push(c("dim", " compiled output gates each arm on its first character. Rank with profileWastedWork()."));
12908
+ out.push(` ${groupDigits(r.rules)} rules, ${groupDigits(r.choiceSites)} choice sites`);
12909
+ out.push(` ${groupDigits(r.factoredSites)} left-factored by the compiler`);
12910
+ out.push(` ${groupDigits(r.backlogSites)} sites where alternatives share a leading term and the compiler DECLINED (${groupDigits(r.backlogArms)} arms)`);
12911
+ 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));
12912
+ if (backlog.length === 0) {
12913
+ out.push("");
12914
+ out.push(" no declined shared prefixes.");
12915
+ return out.join("\n");
12916
+ }
12917
+ out.push("");
12918
+ for (const e of backlog.slice(0, limit)) {
12919
+ out.push(` ${c("cyan", e.siteKey)} ${c("dim", `${e.arity} arms \xB7 ${e.strategy}`)}`);
12920
+ for (const g of e.groups) {
12921
+ out.push(` arms ${g.members.join(", ")} all begin with ${c("bold", g.render)}`);
12922
+ }
12923
+ out.push(` ${c("yellow", "declined")}: ${SITE_DECLINE_LINE[e.declineReason]}`);
12924
+ for (const a of e.armDeclines) {
12925
+ out.push(` arm[${a.arm}] \u2014 ${ARM_DECLINE_LINE[a.reason]}`);
12926
+ }
12927
+ const fix = leftFactorPreview(e);
12928
+ if (fix !== null) {
12929
+ out.push(` ${c("dim", "candidate rewrite (preview \u2014 not applied, not yet digest-verified):")}`);
12930
+ for (const line of fix.split("\n")) out.push(` ${line}`);
12931
+ }
12932
+ out.push("");
12933
+ }
12934
+ if (backlog.length > limit) out.push(` \u2026 and ${groupDigits(backlog.length - limit)} more (the report holds all of them)`);
12935
+ return out.join("\n");
12936
+ }
12937
+ function renderWastedWork(r, opts = {}) {
12938
+ const c = paint(opts.color === true);
12939
+ const limit = opts.limit ?? 20;
12940
+ const out = [];
12941
+ const removed = r.arms.reduce((n, a) => n + (a.attempts - a.gatedAttempts), 0);
12942
+ const totalAttempts = r.arms.reduce((n, a) => n + a.attempts, 0);
12943
+ out.push(c("bold", "wasted work \u2014 input bytes re-scanned after a failed alternative"));
12944
+ out.push(...interpretedBanner(c, removed, totalAttempts));
12945
+ out.push(` corpus: ${groupDigits(r.corpusFiles)} files, ${bytes(r.corpusBytes)} (${groupDigits(r.parsedOk)} parsed, ${groupDigits(r.parsedFailed)} failed)`);
12946
+ out.push(` sites: ${groupDigits(r.instrumentedSites)} instrumented, ${groupDigits(r.uninstrumentableSites)} not instrumentable`);
12947
+ if (r.unresolvedRoots.length > 0) {
12948
+ 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`);
12949
+ }
12950
+ 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)})`));
12951
+ const ranked = r.sites.filter((s) => s.gatedWastedBytes > 0 || s.wastedBytes > 0);
12952
+ if (ranked.length === 0) {
12953
+ out.push("");
12954
+ out.push(" no alternative failed on this corpus.");
12955
+ return out.join("\n");
12956
+ }
12957
+ out.push("");
12958
+ out.push(c("dim", ' per arm: "entered" counts what the COMPILED parser reaches; the parenthesised'));
12959
+ out.push(c("dim", " number is the interpreted count, shown only where the first-char gate differs."));
12960
+ out.push("");
12961
+ const w = Math.min(52, Math.max(...ranked.slice(0, limit).map((s) => s.siteKey.length)));
12962
+ for (const s of ranked.slice(0, limit)) {
12963
+ const share = r.totalGatedWastedBytes > 0 ? ` ${padStart((100 * s.gatedWastedBytes / r.totalGatedWastedBytes).toFixed(1) + "%", 7)}` : "";
12964
+ out.push(` ${c("cyan", pad(s.siteKey.slice(0, w), w))} ${padStart(bytes(s.gatedWastedBytes), 10)}${c("dim", share)}`);
12965
+ const arms = r.arms.filter((a) => a.siteKey === s.siteKey).sort((a, b) => a.arm - b.arm);
12966
+ for (const a of arms) {
12967
+ const entered = a.gatedAttempts === a.attempts ? groupDigits(a.attempts) : `${groupDigits(a.gatedAttempts)} (${groupDigits(a.attempts)} interp)`;
12968
+ 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 };
12969
+ const cell = verdict.color === null ? pad(verdict.text, 34) : c(verdict.color, pad(verdict.text, 34));
12970
+ out.push(` ${padStart(String(a.arm), 2)} ${pad(a.label.slice(0, 30), 30)} ${cell} ${a.gatedWastedBytes > 0 ? padStart(bytes(a.gatedWastedBytes), 10) : ""}`);
12971
+ }
12972
+ out.push("");
12973
+ }
12974
+ if (ranked.length > limit) out.push(` \u2026 and ${groupDigits(ranked.length - limit)} more sites (the report holds all of them)`);
12975
+ if (r.inversions.length > 0) {
12976
+ out.push(c("bold", " ordering inversions \u2014 arm failed EVERY compiled entry while a later arm matched"));
12977
+ out.push(c("dim", " ranked by entries, not bytes: an arm can fail 100% of the time and re-scan nothing."));
12978
+ for (const a of r.inversions.slice(0, limit)) {
12979
+ 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)}`);
12980
+ }
12981
+ if (r.inversions.length > limit) out.push(` \u2026 and ${groupDigits(r.inversions.length - limit)} more`);
12982
+ }
12983
+ return out.join("\n");
12984
+ }
12985
+ function interpretedBanner(c, removed, total) {
12986
+ const share = total > 0 ? ` \u2014 ${(100 * removed / total).toFixed(0)}% of interpreted arm entries` : "";
12987
+ return [
12988
+ c("yellow", " MEASURED IN THE INTERPRETER. Compiled output gates each arm on its first"),
12989
+ c("yellow", ` character; ${groupDigits(removed)} entries below never happen in the shipped parser${share}.`),
12990
+ c("dim", " Counts shown are the modelled COMPILED ones. Byte totals are unaffected by the"),
12991
+ c("dim", " gate (a gated-out arm consumes nothing); entry counts and failure RATES are not.")
12992
+ ];
12993
+ }
12994
+
12995
+ // src/analysis/choice-cost-gate.ts
12996
+ var KIND_ORDER = [
12997
+ "invalid-baseline",
12998
+ "unmeasurable",
12999
+ "ceiling",
13000
+ "drift",
13001
+ "shrank",
13002
+ "inversion",
13003
+ "unbaselined",
13004
+ "stale"
13005
+ ];
13006
+ var pct = (actual, base) => base === 0 ? actual === 0 ? 0 : Infinity : (actual / base - 1) * 100;
13007
+ function checkWastedWork(reports, baseline, policy = {}) {
13008
+ const breaches = [];
13009
+ const tolerance = policy.driftTolerancePct ?? 1;
13010
+ const add = (kind, key, detail) => {
13011
+ breaches.push({ kind, key, detail });
13012
+ };
13013
+ const finish = (checkedCorpora, checkedSites2) => {
13014
+ 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));
13015
+ return {
13016
+ schema: "parseman.wasted-work-verdict/1",
13017
+ ok: breaches.length === 0,
13018
+ breaches,
13019
+ checkedCorpora,
13020
+ checkedSites: checkedSites2
13021
+ };
13022
+ };
13023
+ const b = baseline;
13024
+ if (b === null || b === void 0 || typeof b !== "object") {
13025
+ 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");
13026
+ return finish(0, 0);
13027
+ }
13028
+ if (b.schema !== "parseman.wasted-work-baseline/1") {
13029
+ add("invalid-baseline", "<baseline>", `baseline schema is ${JSON.stringify(b.schema)}, expected "parseman.wasted-work-baseline/1"`);
13030
+ return finish(0, 0);
13031
+ }
13032
+ const isRecord = (v2) => typeof v2 === "object" && v2 !== null && !Array.isArray(v2);
13033
+ if (!isRecord(b.totals) || !isRecord(b.sites)) {
13034
+ add("invalid-baseline", "<baseline>", "baseline `totals` or `sites` is missing or is not an object");
13035
+ return finish(0, 0);
13036
+ }
13037
+ if (Object.keys(b.totals).length === 0) {
13038
+ add("invalid-baseline", "<baseline>", "baseline records ZERO corpora");
13039
+ return finish(0, 0);
13040
+ }
13041
+ const finite = (v2) => typeof v2 === "number" && Number.isFinite(v2);
13042
+ for (const [id, t] of Object.entries(b.totals).sort(([x], [y]) => x < y ? -1 : 1)) {
13043
+ if (!isRecord(t) || !finite(t.corpusBytes) || !finite(t.totalWastedBytes)) {
13044
+ add("invalid-baseline", id, "baseline entry has no finite numeric `corpusBytes` / `totalWastedBytes` \u2014 the comparison against it would silently produce NaN and no breach");
13045
+ }
13046
+ }
13047
+ for (const [key, v2] of Object.entries(b.sites).sort(([x], [y]) => x < y ? -1 : 1)) {
13048
+ 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");
13049
+ }
13050
+ if (breaches.length > 0) return finish(0, 0);
13051
+ if (policy.ceilingRatio !== void 0) {
13052
+ for (const [id, t] of Object.entries(b.totals).sort(([x], [y]) => x < y ? -1 : 1)) {
13053
+ if (t.corpusBytes > 0 && t.totalWastedBytes / t.corpusBytes > policy.ceilingRatio) {
13054
+ add(
13055
+ "invalid-baseline",
13056
+ id,
13057
+ `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`
13058
+ );
13059
+ }
13060
+ }
13061
+ if (breaches.length > 0) return finish(0, 0);
13062
+ }
13063
+ if (Object.keys(reports).length === 0) {
13064
+ add("unmeasurable", "<reports>", "no corpus was measured \u2014 refusing to report a pass on an empty measurement");
13065
+ return finish(0, 0);
13066
+ }
13067
+ let checkedSites = 0;
13068
+ for (const id of Object.keys(reports).sort()) {
13069
+ const r = reports[id];
13070
+ if (r.corpusFiles === 0) {
13071
+ add("unmeasurable", id, "report covers ZERO corpus files");
13072
+ continue;
13073
+ }
13074
+ if (r.instrumentedSites === 0) {
13075
+ add("unmeasurable", id, "report instrumented ZERO choice sites \u2014 the total below would be zero for a reason unrelated to the grammar");
13076
+ continue;
13077
+ }
13078
+ if (r.unresolvedRoots.length > 0) {
13079
+ add(
13080
+ "unmeasurable",
13081
+ id,
13082
+ `${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`
13083
+ );
13084
+ continue;
13085
+ }
13086
+ const base = b.totals[id];
13087
+ if (base === void 0) {
13088
+ add("unbaselined", id, `measured but absent from the baseline (${r.totalWastedBytes} wasted bytes over ${r.corpusBytes})`);
13089
+ continue;
13090
+ }
13091
+ if (policy.ceilingRatio !== void 0 && r.corpusBytes > 0) {
13092
+ const ratio = r.totalWastedBytes / r.corpusBytes;
13093
+ if (ratio > policy.ceilingRatio) {
13094
+ add("ceiling", id, `${r.totalWastedBytes} wasted / ${r.corpusBytes} corpus bytes = ${ratio.toFixed(3)}x, over the ${policy.ceilingRatio}x ceiling`);
13095
+ }
13096
+ }
13097
+ if (r.corpusBytes !== base.corpusBytes) {
13098
+ add("unmeasurable", id, `corpus changed: baseline ${base.corpusBytes} bytes, measured ${r.corpusBytes} \u2014 totals are not comparable until the baseline is refreshed`);
13099
+ continue;
13100
+ }
13101
+ const d = pct(r.totalWastedBytes, base.totalWastedBytes);
13102
+ if (d > tolerance) {
13103
+ add("drift", id, `total wasted ${base.totalWastedBytes} -> ${r.totalWastedBytes} bytes (${d >= 0 ? "+" : ""}${d.toFixed(2)}%, band \xB1${tolerance}%)`);
13104
+ } else if (d < -tolerance) {
13105
+ 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`);
13106
+ }
13107
+ if (r.totalGatedWastedBytes !== r.totalWastedBytes) {
13108
+ const dg = pct(r.totalGatedWastedBytes, base.totalWastedBytes);
13109
+ if (dg > tolerance) {
13110
+ 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`);
13111
+ } else if (dg < -tolerance) {
13112
+ 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`);
13113
+ }
13114
+ }
13115
+ for (const s of r.sites) {
13116
+ const key = `${id}::${s.siteKey}`;
13117
+ const bs = b.sites[key];
13118
+ if (bs === void 0) {
13119
+ if (s.wastedBytes > 0) add("unbaselined", key, `site measured ${s.wastedBytes} wasted bytes but has no baseline entry`);
13120
+ continue;
13121
+ }
13122
+ checkedSites++;
13123
+ const sd = pct(s.wastedBytes, bs);
13124
+ if (sd > tolerance) add("drift", key, `${bs} -> ${s.wastedBytes} bytes (${sd >= 0 ? "+" : ""}${sd.toFixed(2)}%, band \xB1${tolerance}%)`);
13125
+ 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`);
13126
+ if (s.gatedWastedBytes !== s.wastedBytes) {
13127
+ const sg = pct(s.gatedWastedBytes, bs);
13128
+ 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})`);
13129
+ 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})`);
13130
+ }
13131
+ }
13132
+ if (policy.failOnInversions === true) {
13133
+ for (const inv of r.inversions) {
13134
+ add(
13135
+ "inversion",
13136
+ `${id}::${inv.siteKey}#${inv.arm}`,
13137
+ // The GATED columns, because that is what `inversions` was computed from
13138
+ // (choice-cost.ts filters on `gatedAttempts`/`gatedFailures`). Printing the
13139
+ // interpreted counts here would state "failed all N" with an N the condition
13140
+ // never tested — the two differ wherever `firstCharGated` is true.
13141
+ `arm ${inv.arm} (${inv.label}) failed all ${inv.gatedAttempts} compiled entries while a later arm matched; ${inv.gatedWastedBytes} bytes re-scanned`
13142
+ );
13143
+ }
13144
+ }
13145
+ }
13146
+ const measuredKeys = /* @__PURE__ */ new Set();
13147
+ for (const id of Object.keys(reports)) for (const s of reports[id].sites) measuredKeys.add(`${id}::${s.siteKey}`);
13148
+ for (const key of Object.keys(b.sites).sort()) {
13149
+ if (!measuredKeys.has(key)) {
13150
+ add("stale", key, "baselined site matches no measured site \u2014 a rule was renamed or removed, and ignoring it silently shrinks the gated set");
13151
+ }
13152
+ }
13153
+ for (const id of Object.keys(b.totals).sort()) {
13154
+ if (reports[id] === void 0) add("stale", id, "baselined corpus was not measured");
13155
+ }
13156
+ return finish(Object.keys(reports).length, checkedSites);
13157
+ }
13158
+ function buildWastedWorkBaseline(reports, meta) {
13159
+ const totals = {};
13160
+ const sites = {};
13161
+ for (const id of Object.keys(reports).sort()) {
13162
+ const r = reports[id];
13163
+ totals[id] = { corpusBytes: r.corpusBytes, totalWastedBytes: r.totalWastedBytes, instrumentedSites: r.instrumentedSites };
13164
+ for (const s of [...r.sites].sort((a, c) => a.siteKey < c.siteKey ? -1 : 1)) {
13165
+ if (s.wastedBytes > 0) sites[`${id}::${s.siteKey}`] = s.wastedBytes;
13166
+ }
13167
+ }
13168
+ return { schema: "parseman.wasted-work-baseline/1", gitRev: meta.gitRev, updatedAt: meta.updatedAt, totals, sites };
13169
+ }
13170
+
13171
+ // src/combinators/gate.ts
13172
+ function gate(predicate) {
13173
+ const meta = {
13174
+ firstSet: { kind: "any" },
13175
+ canMatchNewline: false,
13176
+ isTrivia: false
13177
+ };
13178
+ return {
13179
+ _tag: "guard",
13180
+ _meta: meta,
13181
+ _def: { tag: "guard", predicate },
13182
+ parse(_input, pos, ctx) {
13183
+ if (predicate(ctx.state))
13184
+ return { ok: true, value: null, span: { start: pos, end: pos } };
13185
+ return { ok: false, expected: ["guard"], span: { start: pos, end: pos } };
13186
+ }
13187
+ };
13188
+ }
13189
+
13190
+ // src/combinators/completions.ts
13191
+ function completionsAt(target, input, offset, options = {}) {
13192
+ const probe = { offset, best: null };
13193
+ const ctx = { trackLines: false, _probe: probe, ...options.tolerant ? { _tolerant: true, _rec: REC } : {} };
13194
+ const sliced = input.slice(0, offset);
13195
+ const result = "parseWithContext" in target ? target.parseWithContext(sliced, ctx, 0) : target.parse(sliced, 0, ctx);
13196
+ if (result.ok) return options.tolerant ? probe.best?.expected ?? [] : [];
13197
+ const best = deeperFail(probe.best, result);
13198
+ return best?.expected ?? [];
13199
+ }
13200
+ function deeperFail(a, b) {
13201
+ if (!a) return b;
13202
+ if (!b) return a;
13203
+ return a.span.start >= b.span.start ? a : b;
13204
+ }
13205
+
13206
+ // src/cst/relative-spans.ts
13207
+ function relativize(node2, parentBase = 0) {
13208
+ return {
13209
+ start: node2.start - parentBase,
13210
+ end: node2.end - parentBase,
13211
+ children: node2.children.map((c) => relativize(c, node2.start))
13212
+ };
13213
+ }
13214
+ function absolutize(node2, parentBase = 0) {
13215
+ const start = parentBase + node2.start;
13216
+ return {
13217
+ start,
13218
+ end: parentBase + node2.end,
13219
+ children: node2.children.map((c) => absolutize(c, start))
13220
+ };
13221
+ }
13222
+ function absoluteSpanAt(root, path) {
13223
+ let base = 0;
13224
+ let node2 = root;
13225
+ for (const idx of path) {
13226
+ base += node2.start;
13227
+ const child = node2.children[idx];
13228
+ if (!child) throw new RangeError(`absoluteSpanAt: no child ${idx} on path ${path.join(".")}`);
13229
+ node2 = child;
13230
+ }
13231
+ return { start: base + node2.start, end: base + node2.end };
13232
+ }
13233
+ function relativizeCST(node2, parentBase = 0) {
13234
+ const span = { start: node2.span.start - parentBase, end: node2.span.end - parentBase };
13235
+ if (node2.children) {
13236
+ const base = node2.span.start;
13237
+ return { ...node2, span, children: node2.children.map((c) => relativizeCST(c, base)) };
13238
+ }
13239
+ return { ...node2, span };
13240
+ }
13241
+ function absolutizeCST(node2, parentBase = 0) {
13242
+ const start = parentBase + node2.span.start;
13243
+ const span = { start, end: parentBase + node2.span.end };
13244
+ if (node2.children) {
13245
+ return { ...node2, span, children: node2.children.map((c) => absolutizeCST(c, start)) };
13246
+ }
13247
+ return { ...node2, span };
13248
+ }
13249
+ function absoluteSpanCST(root, path) {
13250
+ let base = 0;
13251
+ let node2 = root;
13252
+ for (const idx of path) {
13253
+ base += node2.span.start;
13254
+ const child = node2.children?.[idx];
13255
+ if (!child) throw new RangeError(`absoluteSpanCST: no child ${idx} on path ${path.join(".")}`);
13256
+ node2 = child;
13257
+ }
13258
+ return { start: base + node2.span.start, end: base + node2.span.end };
13259
+ }
13260
+ function shiftAbsolute(node2, at, delta) {
13261
+ const s = node2.start >= at ? node2.start + delta : node2.start;
13262
+ const e = node2.end >= at ? node2.end + delta : node2.end;
13263
+ return {
13264
+ start: s,
13265
+ end: e,
13266
+ children: node2.children.map((c) => shiftAbsolute(c, at, delta))
13267
+ };
13268
+ }
13269
+ function applyEdit(root, at, delta, stats) {
13270
+ return editNode(root, at, delta, 0, 0, stats);
13271
+ }
13272
+ function editNode(node2, at, delta, parentBase, parentShift, stats) {
13273
+ const absStart = parentBase + node2.start;
13274
+ const absEnd = parentBase + node2.end;
13275
+ if (absEnd < at) return node2;
13276
+ const startShifts = absStart >= at;
13277
+ if (startShifts && parentShift === delta) return node2;
13278
+ if (stats) stats.allocated++;
13279
+ const newAbsStart = startShifts ? absStart + delta : absStart;
13280
+ const newAbsEnd = absEnd + delta;
13281
+ const newParentBase = parentBase + parentShift;
13282
+ const newStart = newAbsStart - newParentBase;
13283
+ const newEnd = newAbsEnd - newParentBase;
13284
+ const ownShift = newAbsStart - absStart;
13285
+ let changed = false;
13286
+ const newChildren = [];
13287
+ for (const c of node2.children) {
13288
+ const nc = editNode(c, at, delta, absStart, ownShift, stats);
13289
+ if (nc !== c) changed = true;
13290
+ newChildren.push(nc);
13291
+ }
13292
+ return {
13293
+ start: newStart,
13294
+ end: newEnd,
13295
+ children: changed ? newChildren : node2.children
13296
+ };
13297
+ }
13298
+
13299
+ // src/functional/doc.ts
13300
+ function mkCtx(rawState, build, tolerant) {
13301
+ const state = rawState ?? void 0;
13302
+ return tolerant ? { trackLines: false, state, build, _tolerant: true, _rec: REC, _errors: [] } : { trackLines: false, state, build };
13303
+ }
13304
+ function asRuleFn(entry) {
13305
+ return typeof entry === "function" ? entry : (input, pos, ctx) => entry.parse(input, pos, ctx);
12179
13306
  }
12180
13307
  function defOf(entry) {
12181
13308
  return typeof entry === "object" && entry !== null && "_def" in entry ? entry._def : void 0;
@@ -12580,145 +13707,6 @@ function parseDoc(registry, rootRule, input, opts = {}) {
12580
13707
  return new ParseDocImpl(registry, rootRule, opts, { abs: null }, [fail], null, input);
12581
13708
  }
12582
13709
 
12583
- // src/functional/run.ts
12584
- var SELECT_EXAMPLE = "run(entry, input, { rootTrivia: { select: ['blockComment', 'lineComment'] } })";
12585
- var REMOVED_RUN_RESULT_FIELDS = [
12586
- [
12587
- "triviaMap",
12588
- `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.`
12589
- ],
12590
- [
12591
- "triviaLog",
12592
- `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.`
12593
- ],
12594
- [
12595
- "triviaKindLabels",
12596
- `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\`.`
12597
- ]
12598
- ];
12599
- function guardRemovedFields(result) {
12600
- for (const [name, detail] of REMOVED_RUN_RESULT_FIELDS) {
12601
- Object.defineProperty(result, name, {
12602
- configurable: true,
12603
- enumerable: false,
12604
- get() {
12605
- throw new TypeError(
12606
- `RunResult.${name} was REMOVED in parseman 0.44.0 and has no default replacement. ${detail}`
12607
- );
12608
- }
12609
- });
12610
- }
12611
- return result;
12612
- }
12613
- var invoke = (r, input, pos, ctx) => typeof r === "function" ? r(input, pos, ctx) : r.parse(input, pos, ctx);
12614
- function triviaKindLabelsFromRunnable(r) {
12615
- if (!r) return void 0;
12616
- if (typeof r === "function") return r._meta?.triviaKindLabels;
12617
- const grammarTrivia = r._meta.grammarTrivia;
12618
- return grammarTrivia?._meta.triviaKindLabels ?? r._meta.triviaKindLabels ?? (r._def.tag === "grammar" ? r._def.triviaParser?._meta.triviaKindLabels : void 0);
12619
- }
12620
- function rootTriviaClassifiedFromRunnable(r) {
12621
- if (!r) return false;
12622
- if (typeof r === "function") return r._meta?.rootTriviaClassified === true;
12623
- const grammarTrivia = r._meta.grammarTrivia;
12624
- return grammarTrivia?._meta.rootTriviaClassified === true || r._meta.rootTriviaClassified === true;
12625
- }
12626
- function makeSelectedRootLabelIndex(labels) {
12627
- const index = /* @__PURE__ */ Object.create(null);
12628
- for (let i = 0; i < labels.length; i++) {
12629
- if (index[labels[i]] === void 0) index[labels[i]] = i;
12630
- }
12631
- return index;
12632
- }
12633
- function runOnce(entry, input, options, phase, profileState) {
12634
- if (typeof entry !== "function" && typeof entry?.parse !== "function") {
12635
- throw new TypeError(
12636
- `run(): start production is ${entry === null ? "null" : typeof entry}, not a rule \u2014 the requested grammar rule does not exist (check the rule name).`
12637
- );
12638
- }
12639
- const rootTriviaSelection = options.rootTrivia?.select;
12640
- const captureRootTrivia = rootTriviaSelection !== void 0 && rootTriviaSelection.length > 0;
12641
- const selectedRootLog = captureRootTrivia ? [] : void 0;
12642
- const selectedRootLabelIndex = captureRootTrivia ? makeSelectedRootLabelIndex(rootTriviaSelection) : void 0;
12643
- const errors = [];
12644
- const profile = profileState ?? (phase === void 0 ? void 0 : { phase, nodes: 0, childSlots: 0, rawSlots: 0, triviaSlots: 0, fieldSlots: 0, hostCalls: 0 });
12645
- const skipGlobalSinks = phase === "recognizer" || phase === "capture";
12646
- const grammarTrivia = typeof entry !== "function" ? entry._meta.grammarTrivia : void 0;
12647
- const grammarScanSkip = typeof entry !== "function" ? entry._meta.grammarScanSkip : void 0;
12648
- const triviaKindLabels = triviaKindLabelsFromRunnable(entry) ?? triviaKindLabelsFromRunnable(options.trivia);
12649
- if (captureRootTrivia && triviaKindLabels === void 0) {
12650
- throw new TypeError("run(): rootTrivia.select requires labeled grammar trivia.");
12651
- }
12652
- if (captureRootTrivia) {
12653
- for (const label2 of rootTriviaSelection) {
12654
- if (!triviaKindLabels.includes(label2)) {
12655
- throw new TypeError(`run(): rootTrivia.select contains unknown trivia label ${JSON.stringify(label2)}.`);
12656
- }
12657
- }
12658
- }
12659
- if (captureRootTrivia && !rootTriviaClassifiedFromRunnable(entry) && !rootTriviaClassifiedFromRunnable(options.trivia)) {
12660
- throw new TypeError(
12661
- "run(): rootTrivia.select requires classifiedTrivia() at the root."
12662
- );
12663
- }
12664
- const stamped = entry;
12665
- assertHostModeCompatible(
12666
- typeof entry === "function" ? stamped[FUSED_HOST_MODE] ?? "ast" : entry._meta.grammarHostMode ?? "ast",
12667
- options.build,
12668
- typeof entry === "function" && stamped[FUSED_HOST_ELIDED] === true
12669
- );
12670
- const ctx = {
12671
- trackLines: false,
12672
- ...skipGlobalSinks ? { _pmProfile: profile } : {
12673
- ...captureRootTrivia ? {
12674
- _rootTriviaLog: selectedRootLog,
12675
- _rootTriviaKindIndex: selectedRootLabelIndex,
12676
- _rootTriviaStrictScopes: true
12677
- } : {},
12678
- _errors: errors,
12679
- ...profile === void 0 ? {} : { _pmProfile: profile }
12680
- },
12681
- build: options.build,
12682
- state: options.state,
12683
- ...grammarTrivia !== void 0 ? { trivia: grammarTrivia, ...triviaKindLabels ? { triviaKindLabels } : {} } : triviaKindLabels ? { triviaKindLabels } : {},
12684
- ...grammarScanSkip !== void 0 ? { scanSkip: grammarScanSkip } : {},
12685
- ...options.triviaCaptureMask !== void 0 ? { _triviaCaptureMask: options.triviaCaptureMask } : {},
12686
- ...options.tolerant ? { _tolerant: true, _rec: REC } : {},
12687
- ...options.instrumentation === void 0 ? {} : options.instrumentation
12688
- };
12689
- const r = invoke(entry, input, 0, ctx);
12690
- let unconsumedFrom = null;
12691
- if (r.ok) {
12692
- let pos = r.span?.end ?? 0;
12693
- if (options.trivia && pos < input.length) {
12694
- const t = invoke(options.trivia, input, pos, { trackLines: false });
12695
- if (t.ok && t.span.end > pos) pos = t.span.end;
12696
- }
12697
- unconsumedFrom = pos < input.length ? pos : null;
12698
- }
12699
- return {
12700
- ok: r.ok,
12701
- value: r.ok ? r.value : void 0,
12702
- span: r.span ?? { start: 0, end: 0 },
12703
- expected: r.ok ? [] : r.expected ?? [],
12704
- errors,
12705
- ...selectedRootLog !== void 0 && selectedRootLog.length > 0 ? {
12706
- rootTrivia: {
12707
- rows: selectedRootLog,
12708
- select: rootTriviaSelection,
12709
- index: buildRootTriviaIndex(selectedRootLog, rootTriviaSelection)
12710
- }
12711
- } : {},
12712
- unconsumedFrom
12713
- };
12714
- }
12715
- function run(entry, input, options = {}) {
12716
- if (!options.profile) return guardRemovedFields(runOnce(entry, input, options));
12717
- throw new TypeError(
12718
- "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."
12719
- );
12720
- }
12721
-
12722
13710
  // src/coverage.ts
12723
13711
  function dispatchMatcher(entry) {
12724
13712
  switch (entry.kind) {
@@ -13166,6 +14154,7 @@ export {
13166
14154
  absolutize,
13167
14155
  absolutizeCST,
13168
14156
  alternationGroups,
14157
+ analyzeChoiceInventory,
13169
14158
  analyzeDuplication,
13170
14159
  analyzeDuplicationRules,
13171
14160
  analyzeGating,
@@ -13174,13 +14163,17 @@ export {
13174
14163
  annotateSpan,
13175
14164
  annotateTreeSpans,
13176
14165
  applyEdit,
14166
+ armLabel,
13177
14167
  attempt,
13178
14168
  balanced,
13179
14169
  buildLineIndex,
13180
14170
  buildRootTriviaIndex,
13181
14171
  buildTriviaIndex,
14172
+ buildWastedWorkBaseline,
13182
14173
  charClassMembers,
14174
+ checkWastedWork,
13183
14175
  choice,
14176
+ choiceSiteKey,
13184
14177
  classifiedTrivia,
13185
14178
  compile,
13186
14179
  compiledGrammarCoverageDefinitions,
@@ -13198,6 +14191,7 @@ export {
13198
14191
  dispatch,
13199
14192
  duplicationFindingCount,
13200
14193
  endsWith,
14194
+ examinedNothing,
13201
14195
  expect,
13202
14196
  extractCharClasses,
13203
14197
  field,
@@ -13205,14 +14199,17 @@ export {
13205
14199
  formatDuplicationFindings,
13206
14200
  formatGatingWarnings,
13207
14201
  formatGrammarDiagnosis,
14202
+ fuseInterpreted,
13208
14203
  gate,
13209
14204
  grammarCoverageDefinitions,
14205
+ isInterpretedFuse,
13210
14206
  isParseError,
13211
14207
  keywordAlternationHazards,
13212
14208
  keywordRegexShape,
13213
14209
  keywords,
13214
14210
  label,
13215
14211
  leaf,
14212
+ leftFactorPreview,
13216
14213
  literal,
13217
14214
  makeWhen,
13218
14215
  makeWord,
@@ -13231,11 +14228,14 @@ export {
13231
14228
  parseDoc,
13232
14229
  parser,
13233
14230
  peek,
14231
+ profileWastedWork,
13234
14232
  recordLineRange,
13235
14233
  ref,
13236
14234
  regex,
13237
14235
  relativize,
13238
14236
  relativizeCST,
14237
+ renderChoiceInventory,
14238
+ renderWastedWork,
13239
14239
  routed,
13240
14240
  rules,
13241
14241
  run,