parseman 0.44.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/analysis/choice-cost-gate.d.ts +158 -0
  2. package/dist/analysis/choice-cost-gate.d.ts.map +1 -0
  3. package/dist/analysis/choice-cost-render.d.ts +88 -0
  4. package/dist/analysis/choice-cost-render.d.ts.map +1 -0
  5. package/dist/analysis/choice-cost.d.ts +383 -0
  6. package/dist/analysis/choice-cost.d.ts.map +1 -0
  7. package/dist/analysis/corpus.d.ts +73 -0
  8. package/dist/analysis/corpus.d.ts.map +1 -0
  9. package/dist/analysis/diagnose-render.d.ts +65 -0
  10. package/dist/analysis/diagnose-render.d.ts.map +1 -0
  11. package/dist/analysis/diagnose.d.ts +133 -0
  12. package/dist/analysis/diagnose.d.ts.map +1 -0
  13. package/dist/analysis/diagnostics.cjs +11272 -0
  14. package/dist/analysis/diagnostics.cjs.map +7 -0
  15. package/dist/analysis/diagnostics.d.ts +27 -0
  16. package/dist/analysis/diagnostics.d.ts.map +1 -0
  17. package/dist/analysis/diagnostics.js +11249 -0
  18. package/dist/analysis/diagnostics.js.map +7 -0
  19. package/dist/analysis/fix-render.d.ts +43 -0
  20. package/dist/analysis/fix-render.d.ts.map +1 -0
  21. package/dist/analysis/fix.d.ts +172 -0
  22. package/dist/analysis/fix.d.ts.map +1 -0
  23. package/dist/analysis/gating.d.ts +17 -9
  24. package/dist/analysis/gating.d.ts.map +1 -1
  25. package/dist/analysis/grammar.d.ts.map +1 -1
  26. package/dist/analysis/rebuild.d.ts +61 -0
  27. package/dist/analysis/rebuild.d.ts.map +1 -0
  28. package/dist/analysis/terminal.d.ts +142 -0
  29. package/dist/analysis/terminal.d.ts.map +1 -0
  30. package/dist/cli/index.cjs +12507 -0
  31. package/dist/cli/index.cjs.map +7 -0
  32. package/dist/cli/index.d.ts +3 -0
  33. package/dist/cli/index.d.ts.map +1 -0
  34. package/dist/cli/index.js +12484 -0
  35. package/dist/cli/index.js.map +7 -0
  36. package/dist/combinators/choice.d.ts.map +1 -1
  37. package/dist/combinators/dispatch.d.ts +19 -1
  38. package/dist/combinators/dispatch.d.ts.map +1 -1
  39. package/dist/combinators/expect.d.ts.map +1 -1
  40. package/dist/combinators/first-set.d.ts.map +1 -1
  41. package/dist/combinators/node.d.ts +19 -0
  42. package/dist/combinators/node.d.ts.map +1 -1
  43. package/dist/compiler/build-arity.d.ts +28 -0
  44. package/dist/compiler/build-arity.d.ts.map +1 -1
  45. package/dist/compiler/codegen.d.ts +80 -60
  46. package/dist/compiler/codegen.d.ts.map +1 -1
  47. package/dist/compiler/degradation.d.ts +123 -0
  48. package/dist/compiler/degradation.d.ts.map +1 -0
  49. package/dist/compiler/fields.d.ts.map +1 -1
  50. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  51. package/dist/compiler/inline-build.d.ts.map +1 -1
  52. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  53. package/dist/compiler/linker.d.ts +45 -7
  54. package/dist/compiler/linker.d.ts.map +1 -1
  55. package/dist/compiler/module-hoist.d.ts +106 -0
  56. package/dist/compiler/module-hoist.d.ts.map +1 -0
  57. package/dist/coverage.d.ts +13 -0
  58. package/dist/coverage.d.ts.map +1 -1
  59. package/dist/cst/reflection.d.ts.map +1 -1
  60. package/dist/functional/run.d.ts.map +1 -1
  61. package/dist/index.cjs +1846 -610
  62. package/dist/index.cjs.map +4 -4
  63. package/dist/index.d.ts +11 -7
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +1846 -610
  66. package/dist/index.js.map +4 -4
  67. package/dist/language-service/index.cjs +40 -30
  68. package/dist/language-service/index.cjs.map +3 -3
  69. package/dist/language-service/index.js +40 -30
  70. package/dist/language-service/index.js.map +3 -3
  71. package/dist/oracle/digest.d.ts +132 -6
  72. package/dist/oracle/digest.d.ts.map +1 -1
  73. package/dist/oracle/index.cjs +136 -305
  74. package/dist/oracle/index.cjs.map +4 -4
  75. package/dist/oracle/index.d.ts +43 -31
  76. package/dist/oracle/index.d.ts.map +1 -1
  77. package/dist/oracle/index.js +136 -305
  78. package/dist/oracle/index.js.map +4 -4
  79. package/dist/plugin/evaluator.d.ts +3 -0
  80. package/dist/plugin/evaluator.d.ts.map +1 -1
  81. package/dist/plugin/free-identifiers.d.ts +23 -0
  82. package/dist/plugin/free-identifiers.d.ts.map +1 -0
  83. package/dist/plugin/index.cjs +1529 -763
  84. package/dist/plugin/index.cjs.map +4 -4
  85. package/dist/plugin/index.d.ts +18 -0
  86. package/dist/plugin/index.d.ts.map +1 -1
  87. package/dist/plugin/index.js +1529 -763
  88. package/dist/plugin/index.js.map +4 -4
  89. package/dist/plugin/reducer-resolver.d.ts +57 -0
  90. package/dist/plugin/reducer-resolver.d.ts.map +1 -0
  91. package/dist/run/index.cjs +33 -30
  92. package/dist/run/index.cjs.map +2 -2
  93. package/dist/run/index.js +33 -30
  94. package/dist/run/index.js.map +2 -2
  95. package/dist/spec/index.cjs +222 -34
  96. package/dist/spec/index.cjs.map +4 -4
  97. package/dist/spec/index.js +222 -34
  98. package/dist/spec/index.js.map +4 -4
  99. package/dist/spec/model.d.ts.map +1 -1
  100. package/dist/types.d.ts +6 -1
  101. package/dist/types.d.ts.map +1 -1
  102. package/dist/version.d.ts +1 -1
  103. package/package.json +23 -4
  104. package/dist/combinators/guard.d.ts +0 -8
  105. package/dist/combinators/guard.d.ts.map +0 -1
  106. package/dist/cst/offset-model.d.ts +0 -119
  107. package/dist/cst/offset-model.d.ts.map +0 -1
  108. package/dist/oracle/corpus.d.ts +0 -32
  109. package/dist/oracle/corpus.d.ts.map +0 -1
  110. package/dist/oracle/identity.d.ts +0 -117
  111. package/dist/oracle/identity.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -51,8 +51,9 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set(), resolve) {
51
51
  return d.value.length === 0;
52
52
  case "keywords":
53
53
  return false;
54
+ // The routed token itself is the selector's match, never empty; a fallback can be.
54
55
  case "routed":
55
- return false;
56
+ return d.fallback === void 0 ? false : me(d.fallback);
56
57
  case "regex":
57
58
  try {
58
59
  const m = new RegExp(d.source).exec("");
@@ -1814,8 +1815,11 @@ function derive(c, seen) {
1814
1815
  case "not":
1815
1816
  case "peek":
1816
1817
  return deriveExpected2(def.parser);
1818
+ // A bare routed() can only fail as "there is no routed token here". With a
1819
+ // fallback, THAT is the branch that can fail, so its expectations are the useful
1820
+ // ones — the routed path either succeeds or is not taken.
1817
1821
  case "routed":
1818
- return ["routed()"];
1822
+ return def.fallback === void 0 ? ["routed()"] : deriveExpected2(def.fallback);
1819
1823
  case "lazy": {
1820
1824
  const name = c._ruleName;
1821
1825
  if (seen.has(c)) return name ? [name] : [];
@@ -2005,7 +2009,7 @@ function detectStrategy(parsers) {
2005
2009
  function bareLeadingTermKey(term) {
2006
2010
  const d = term._def;
2007
2011
  if (d.tag === "literal" && !d.caseInsensitive) return `L:${d.value}`;
2008
- if (d.tag === "regex") return `R:${d.source}\0${d.flags}`;
2012
+ if (d.tag === "regex") return `R:${JSON.stringify([d.source, d.flags])}`;
2009
2013
  return null;
2010
2014
  }
2011
2015
  function detectSharedPrefix(parsers) {
@@ -2263,17 +2267,21 @@ function makeWhen(opts = {}) {
2263
2267
  function otherwise(parser2) {
2264
2268
  return { kind: "otherwise", parser: parser2, usesRouted: parserUsesRouted(parser2) };
2265
2269
  }
2266
- function routed() {
2270
+ function routed(fallback) {
2267
2271
  return {
2268
2272
  _tag: "routed",
2269
- _meta: { firstSet: { kind: "any" }, canMatchNewline: false, isTrivia: false },
2270
- _def: { tag: "routed" },
2273
+ _meta: {
2274
+ // The routed token is whatever the selector matched, so the first set stays
2275
+ // `any` even with a fallback — `any` already subsumes the fallback's.
2276
+ firstSet: { kind: "any" },
2277
+ canMatchNewline: fallback?._meta.canMatchNewline ?? false,
2278
+ isTrivia: false
2279
+ },
2280
+ _def: fallback === void 0 ? { tag: "routed" } : { tag: "routed", fallback },
2271
2281
  parse(_input, pos, ctx) {
2272
2282
  const item = ctx._routed;
2273
- if (item === void 0) {
2274
- return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2275
- }
2276
- if (pos !== item.span.start) {
2283
+ if (item === void 0 || pos !== item.span.start) {
2284
+ if (fallback !== void 0) return fallback.parse(_input, pos, ctx);
2277
2285
  return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2278
2286
  }
2279
2287
  if (cstCaptureActive(ctx)) {
@@ -2816,13 +2824,13 @@ function markUnusedValues(root) {
2816
2824
  return;
2817
2825
  }
2818
2826
  };
2819
- const resolveRoot = (c) => {
2820
- const guard2 = /* @__PURE__ */ new Set();
2827
+ const resolveRoot2 = (c) => {
2828
+ const guard = /* @__PURE__ */ new Set();
2821
2829
  let cur = c;
2822
- while (!guard2.has(cur)) {
2830
+ while (!guard.has(cur)) {
2823
2831
  const d = cur._def;
2824
2832
  if (d.tag !== "lazy") return cur;
2825
- guard2.add(cur);
2833
+ guard.add(cur);
2826
2834
  try {
2827
2835
  cur = d.thunk();
2828
2836
  } catch {
@@ -2831,7 +2839,7 @@ function markUnusedValues(root) {
2831
2839
  }
2832
2840
  return cur;
2833
2841
  };
2834
- visit(resolveRoot(root), true);
2842
+ visit(resolveRoot2(root), true);
2835
2843
  }
2836
2844
 
2837
2845
  // src/combinators/grammar.ts
@@ -2991,10 +2999,11 @@ function childrenOf(def) {
2991
2999
  return [def.main, def.skipped];
2992
3000
  case "scanTo":
2993
3001
  return [def.sentinel, ...def.skip];
3002
+ case "routed":
3003
+ return def.fallback ? [def.fallback] : [];
2994
3004
  case "literal":
2995
3005
  case "regex":
2996
3006
  case "keywords":
2997
- case "routed":
2998
3007
  case "guard":
2999
3008
  case "lazy":
3000
3009
  case "unknown":
@@ -3220,54 +3229,272 @@ function peek(combinator) {
3220
3229
  };
3221
3230
  }
3222
3231
 
3232
+ // src/compiler/degradation.ts
3233
+ function formatDegradation(d) {
3234
+ return `[parseman] degraded [${d.code}] ${d.where}: ${d.subject} \u2014 ${d.fellBackTo}; otherwise ${d.otherwise}`;
3235
+ }
3236
+ var DETAIL_CAP = 8;
3237
+ function formatDegradations(list) {
3238
+ const byCode = /* @__PURE__ */ new Map();
3239
+ for (const d of list) {
3240
+ const bucket = byCode.get(d.code);
3241
+ if (bucket) bucket.push(d);
3242
+ else byCode.set(d.code, [d]);
3243
+ }
3244
+ const out = [];
3245
+ for (const [code, items] of byCode) {
3246
+ for (const d of items.slice(0, DETAIL_CAP)) out.push(formatDegradation(d));
3247
+ if (items.length > DETAIL_CAP) {
3248
+ out.push(
3249
+ `[parseman] degraded [${code}] +${items.length - DETAIL_CAP} more site(s) not listed (${items.length} total). Set PARSEMAN_DEGRADATION=error to fail the build on these.`
3250
+ );
3251
+ }
3252
+ }
3253
+ return out;
3254
+ }
3255
+ var _sinks = [];
3256
+ function beginDegradationCapture() {
3257
+ _sinks.push(/* @__PURE__ */ new Map());
3258
+ }
3259
+ function degradationCaptureDepth() {
3260
+ return _sinks.length;
3261
+ }
3262
+ function unwindDegradationCapture(depth) {
3263
+ const found = [];
3264
+ while (_sinks.length > depth) {
3265
+ const sink = _sinks.pop();
3266
+ found.unshift(...sink.values());
3267
+ }
3268
+ return found;
3269
+ }
3270
+ function resolveDegradationLevel(explicit) {
3271
+ if (explicit !== void 0) return explicit;
3272
+ const env = typeof process !== "undefined" ? process.env?.PARSEMAN_DEGRADATION : void 0;
3273
+ if (env === "off" || env === "warn" || env === "error") return env;
3274
+ return "warn";
3275
+ }
3276
+ function degradationKey(d) {
3277
+ return JSON.stringify([d.code, d.where, d.subject]);
3278
+ }
3279
+ function recordDegradation(d) {
3280
+ const level = resolveDegradationLevel();
3281
+ if (level === "off") return;
3282
+ const key = degradationKey(d);
3283
+ const sink = _sinks[_sinks.length - 1];
3284
+ if (sink) {
3285
+ if (!sink.has(key)) sink.set(key, d);
3286
+ return;
3287
+ }
3288
+ if (_immediate.has(key)) return;
3289
+ _immediate.add(key);
3290
+ if (level === "error") throw new Error(`parseman: degraded compilation path
3291
+ ${formatDegradation(d)}`);
3292
+ console.warn(formatDegradation(d));
3293
+ }
3294
+ var _immediate = /* @__PURE__ */ new Set();
3295
+ function beginCompileDegradationDrain() {
3296
+ if (resolveDegradationLevel() === "off" || _sinks.length > 0) return () => {
3297
+ };
3298
+ const depth = degradationCaptureDepth();
3299
+ beginDegradationCapture();
3300
+ return (report) => {
3301
+ const found = unwindDegradationCapture(depth);
3302
+ if (!report || found.length === 0) return;
3303
+ const fresh = found.filter((d) => {
3304
+ const k = degradationKey(d);
3305
+ if (_immediate.has(k)) return false;
3306
+ _immediate.add(k);
3307
+ return true;
3308
+ });
3309
+ if (fresh.length === 0) return;
3310
+ const lines = formatDegradations(fresh);
3311
+ if (resolveDegradationLevel() === "error") {
3312
+ throw new Error(`parseman: ${fresh.length} degraded compilation path(s)
3313
+ ${lines.join("\n")}`);
3314
+ }
3315
+ for (const l of lines) console.warn(l);
3316
+ };
3317
+ }
3318
+
3223
3319
  // src/compiler/build-arity.ts
3224
3320
  function cstOutputHost(build) {
3225
3321
  return build?._parsemanCstOutput === true;
3226
3322
  }
3227
- var PARAM_LIST_RE = (
3228
- // (params) => ... | function name?(params) ... | single-ident arrow `x => ...`
3229
- /^(?:function\b[^(]*\(([^)]*)\)|\(([^)]*)\)\s*=>|([A-Za-z_$][\w$]*)\s*=>)/
3230
- );
3231
- var CONFIRMABLE_PARAM_RE = /^[A-Za-z_$][\w$]*\s*\??\s*(?::[^,=]+)?$/;
3323
+ var SINGLE_IDENT_ARROW_RE = /^[A-Za-z_$][\w$]*\s*=>/;
3324
+ var PARAM_LIST_OPEN_RE = /^(?:function\b[^(]*)?\(/;
3325
+ var NATIVE_CODE_RE = /\{\s*\[native code\]\s*\}/;
3326
+ var PARAM_HEAD_RE = /^[A-Za-z_$][\w$]*\s*\??\s*/;
3232
3327
  function confirmedBuildArity(src) {
3233
3328
  const s = src.trim();
3234
- const m = PARAM_LIST_RE.exec(s);
3235
- if (!m) return null;
3236
- if (m[3] !== void 0) return 1;
3237
- const inner = (m[1] ?? m[2] ?? "").trim();
3238
- if (inner === "") return 0;
3239
- const parts = inner.split(",");
3329
+ if (NATIVE_CODE_RE.test(s)) return null;
3330
+ if (/\barguments\b/.test(s)) return null;
3331
+ if (SINGLE_IDENT_ARROW_RE.test(s)) return 1;
3332
+ const inner = paramListText(s);
3333
+ if (inner === null) return null;
3334
+ if (inner.trim() === "") return 0;
3335
+ const parts = splitTopLevel(inner);
3336
+ if (parts === null) return null;
3240
3337
  for (const part of parts) {
3241
- if (!CONFIRMABLE_PARAM_RE.test(part.trim())) return null;
3338
+ if (!isConfirmableParam(part.trim())) return null;
3242
3339
  }
3243
- if (/\barguments\b/.test(s)) return null;
3244
3340
  return parts.length;
3245
3341
  }
3342
+ function paramListText(s) {
3343
+ const open = PARAM_LIST_OPEN_RE.exec(s);
3344
+ if (!open) return null;
3345
+ const start = open[0].length;
3346
+ const end = matchingParen(s, start);
3347
+ if (end === null) return null;
3348
+ if (!s.startsWith("function") && !/^\s*=>/.test(s.slice(end + 1))) return null;
3349
+ return s.slice(start, end);
3350
+ }
3351
+ function matchingParen(s, start) {
3352
+ let depth = 0;
3353
+ for (let i = start; i < s.length; i++) {
3354
+ const c = s[i];
3355
+ if (c === '"' || c === "'" || c === "`") {
3356
+ const close = skipString(s, i);
3357
+ if (close === null) return null;
3358
+ i = close;
3359
+ continue;
3360
+ }
3361
+ if (c === "(" || c === "[" || c === "{") depth++;
3362
+ else if (c === "]" || c === "}") {
3363
+ if (--depth < 0) return null;
3364
+ } else if (c === ")") {
3365
+ if (depth === 0) return i;
3366
+ depth--;
3367
+ }
3368
+ }
3369
+ return null;
3370
+ }
3371
+ function skipString(s, open) {
3372
+ const quote = s[open];
3373
+ for (let i = open + 1; i < s.length; i++) {
3374
+ if (s[i] === "\\") {
3375
+ i++;
3376
+ continue;
3377
+ }
3378
+ if (s[i] === quote) return i;
3379
+ }
3380
+ return null;
3381
+ }
3382
+ function splitTopLevel(inner) {
3383
+ const parts = [];
3384
+ let depth = 0;
3385
+ let angle = 0;
3386
+ let last = 0;
3387
+ for (let i = 0; i < inner.length; i++) {
3388
+ const c = inner[i];
3389
+ if (c === "=" && inner[i + 1] === ">") {
3390
+ i++;
3391
+ continue;
3392
+ }
3393
+ if (c === '"' || c === "'" || c === "`") {
3394
+ const close = skipString(inner, i);
3395
+ if (close === null) return null;
3396
+ i = close;
3397
+ continue;
3398
+ }
3399
+ if (c === "(" || c === "[" || c === "{") depth++;
3400
+ else if (c === ")" || c === "]" || c === "}") {
3401
+ if (--depth < 0) return null;
3402
+ } else if (c === "<") angle++;
3403
+ else if (c === ">") {
3404
+ if (--angle < 0) return null;
3405
+ } else if (c === "," && depth === 0 && angle === 0) {
3406
+ parts.push(inner.slice(last, i));
3407
+ last = i + 1;
3408
+ }
3409
+ }
3410
+ if (depth !== 0 || angle !== 0) return null;
3411
+ parts.push(inner.slice(last));
3412
+ return parts;
3413
+ }
3414
+ function isConfirmableParam(part) {
3415
+ const head = PARAM_HEAD_RE.exec(part);
3416
+ if (!head) return false;
3417
+ const rest = part.slice(head[0].length);
3418
+ if (rest === "") return true;
3419
+ if (rest[0] !== ":") return false;
3420
+ return !hasDefaultAssign(rest.slice(1));
3421
+ }
3422
+ function hasDefaultAssign(annotation) {
3423
+ for (let i = 0; i < annotation.length; i++) {
3424
+ if (annotation[i] !== "=") continue;
3425
+ if (annotation[i + 1] === ">" || annotation[i + 1] === "=") {
3426
+ i++;
3427
+ continue;
3428
+ }
3429
+ const prev = annotation[i - 1];
3430
+ if (prev === "<" || prev === ">" || prev === "!" || prev === "=") continue;
3431
+ return true;
3432
+ }
3433
+ return false;
3434
+ }
3435
+ function buildAnalysisSrc(def) {
3436
+ return def.buildSigSrc ?? def.buildSrc ?? def.build.toString();
3437
+ }
3438
+ var RESOLVER_REASONS = {
3439
+ "rest-parameter": "its parameter list uses a rest parameter, so the declared arity is unbounded",
3440
+ "arguments": "its body references `arguments`",
3441
+ "reassigned": "the binding it names is reassigned, so the function read here is not decidable",
3442
+ "not-a-function": "the binding it names is not a function declaration",
3443
+ "unresolved-import": "its module could not be resolved or parsed",
3444
+ "not-found": "the name did not resolve to any binding",
3445
+ "computed": "it is a computed expression",
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"
3448
+ };
3449
+ function unconfirmableReason(def, src) {
3450
+ const declared = def.buildArityUnresolved;
3451
+ if (declared !== void 0 && RESOLVER_REASONS[declared]) return RESOLVER_REASONS[declared];
3452
+ const s = src.trim();
3453
+ const params = s.slice(0, s.indexOf(")") + 1);
3454
+ if (params.includes("...")) return RESOLVER_REASONS["rest-parameter"];
3455
+ if (/\barguments\b/.test(s)) return RESOLVER_REASONS["arguments"];
3456
+ if (/^[A-Za-z_$][\w$]*(\s*\.\s*[A-Za-z_$][\w$]*)?$/.test(s)) {
3457
+ return "it names a binding this build could not resolve to a function declaration";
3458
+ }
3459
+ return "its parameter list could not be parsed";
3460
+ }
3461
+ function confirmedArityForDef(def) {
3462
+ if (def.buildArity !== void 0) return def.buildArity;
3463
+ const src = buildAnalysisSrc(def);
3464
+ const arity = confirmedBuildArity(src);
3465
+ if (arity !== null) return arity;
3466
+ const subject = `build reducer \`${(def.buildSrc ?? "<runtime fn>").trim().split("\n")[0].slice(0, 60)}\``;
3467
+ recordDegradation({
3468
+ code: "build-arity-unconfirmed",
3469
+ severity: "warn",
3470
+ where: `node("${def.type ?? "<inferred>"}")`,
3471
+ subject,
3472
+ fellBackTo: `could not confirm its declared arity \u2014 ${unconfirmableReason(def, src)} \u2014 so this node captures children, fields and raw children, logs trivia, and clones \`_ctx.state\` on every match`,
3473
+ otherwise: "only the tiers the reducer actually declares would be captured (arity >= 1 children, >= 2 fields, >= 4 raw, >= 5 trivia, >= 6 state). Declare it with `node(..., { buildArity: n })` if it cannot be derived"
3474
+ });
3475
+ return null;
3476
+ }
3246
3477
  function buildReadsChildren(def) {
3247
3478
  if (!def.build) return true;
3248
- const src = def.buildSrc ?? def.build.toString();
3249
- const arity = confirmedBuildArity(src);
3479
+ const arity = confirmedArityForDef(def);
3250
3480
  if (arity === null) return true;
3251
3481
  return arity >= 1;
3252
3482
  }
3253
3483
  function buildReadsRaw(def) {
3254
3484
  if (!def.build) return true;
3255
- const src = def.buildSrc ?? def.build.toString();
3256
- const arity = confirmedBuildArity(src);
3485
+ const arity = confirmedArityForDef(def);
3257
3486
  if (arity === null) return true;
3258
3487
  return arity >= 4;
3259
3488
  }
3260
3489
  function buildReadsTrivia(def) {
3261
3490
  if (!def.build) return true;
3262
- const src = def.buildSrc ?? def.build.toString();
3263
- const arity = confirmedBuildArity(src);
3491
+ const arity = confirmedArityForDef(def);
3264
3492
  if (arity === null) return true;
3265
3493
  return arity >= 5;
3266
3494
  }
3267
3495
  function buildReadsState(def) {
3268
3496
  if (!def.build) return true;
3269
- const src = def.buildSrc ?? def.build.toString();
3270
- const arity = confirmedBuildArity(src);
3497
+ const arity = confirmedArityForDef(def);
3271
3498
  if (arity === null) return true;
3272
3499
  return arity >= 6;
3273
3500
  }
@@ -3275,8 +3502,7 @@ function buildReadsState(def) {
3275
3502
  // src/compiler/fields.ts
3276
3503
  function buildReadsFields(def) {
3277
3504
  if (!def.build) return true;
3278
- const src = def.buildSrc ?? def.build.toString();
3279
- const arity = confirmedBuildArity(src);
3505
+ const arity = confirmedArityForDef(def);
3280
3506
  if (arity === null) return true;
3281
3507
  return arity >= 2;
3282
3508
  }
@@ -3457,6 +3683,13 @@ function isCstChild(value) {
3457
3683
  function missingInferredType() {
3458
3684
  throw new Error('node(): inferred node type requires a rules() key; pass node("Type", parser) outside rules()');
3459
3685
  }
3686
+ function normalizeBuildArity(arity) {
3687
+ if (arity === void 0) return void 0;
3688
+ if (!Number.isInteger(arity) || arity < 0 || arity > 6) {
3689
+ throw new Error("node() buildArity must be an integer 0..6 (children, fields, span, rawChildren, triviaLog, state)");
3690
+ }
3691
+ return arity;
3692
+ }
3460
3693
  function normalizeProject(project) {
3461
3694
  if (project === void 0) return void 0;
3462
3695
  if (!Number.isInteger(project) || project < 0) {
@@ -3492,13 +3725,14 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
3492
3725
  const captureTrivia = opts?.captureTrivia === true;
3493
3726
  const trailingTrivia = opts?.trailingTrivia === true;
3494
3727
  const tags = opts?.tags;
3728
+ const buildArity = normalizeBuildArity(opts?.buildArity);
3495
3729
  if (unwrap && collapse) {
3496
3730
  throw new Error("node() options cannot set both unwrap and collapse");
3497
3731
  }
3498
3732
  if (project !== void 0 && (unwrap || collapse)) {
3499
3733
  throw new Error("node() options cannot combine project with unwrap or collapse");
3500
3734
  }
3501
- const def = unwrap || collapse || project !== void 0 || captureTrivia || trailingTrivia || tags !== void 0 && tags.length > 0 ? { ...baseDef, ...unwrap ? { unwrap: true } : {}, ...collapse ? { collapse: true } : {}, ...project !== void 0 ? { project } : {}, ...captureTrivia ? { captureTrivia: true } : {}, ...trailingTrivia ? { trailingTrivia: true } : {}, ...tags !== void 0 && tags.length > 0 ? { tags } : {} } : baseDef;
3735
+ const def = unwrap || collapse || project !== void 0 || captureTrivia || trailingTrivia || buildArity !== void 0 || tags !== void 0 && tags.length > 0 ? { ...baseDef, ...unwrap ? { unwrap: true } : {}, ...collapse ? { collapse: true } : {}, ...project !== void 0 ? { project } : {}, ...captureTrivia ? { captureTrivia: true } : {}, ...trailingTrivia ? { trailingTrivia: true } : {}, ...buildArity !== void 0 ? { buildArity } : {}, ...tags !== void 0 && tags.length > 0 ? { tags } : {} } : baseDef;
3502
3736
  const capturesTrivia = captureTrivia || trailingTrivia || (build ? buildReadsTrivia(def) : project === void 0);
3503
3737
  const clonesState = build ? buildReadsState(def) : project === void 0;
3504
3738
  const hasOwnFields = parserHasOwnFields(combinator);
@@ -3544,7 +3778,7 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
3544
3778
  const cstOutput = cstOutputHost(ctx.build);
3545
3779
  const span = ctx.trackLines ? annotateSpanFromLineContext(r.span, ctx) : r.span;
3546
3780
  const hostState = clonesState ? st : ctx.state !== void 0 ? Object.assign({}, ctx.state) : void 0;
3547
- const built = unwrap && children2.length === 1 ? unwrapChild(children2[0]) : collapse && children2.length === 1 ? children2[0] : !build && project === void 0 && ctx.build?._parsemanCstCollapse && children2.length === 1 && rawChildren.length === 1 && ctx.build._parsemanCstCollapse(nodeType, children2[0], children2, rawChildren) ? children2[0] : project !== void 0 ? cstOutput && ctx.build ? ctx.build(nodeType, children2, fields, span, rawChildren, triviaLog, hostState, tags) : projectChild(children2, project, nodeType) : build ? cstOutput && ctx.build ? ctx.build(nodeType, children2, fields, span, rawChildren, triviaLog, hostState, tags) : build(children2, fields, span, rawChildren, triviaLog, st) : ctx.build ? ctx.build(nodeType, children2, fields, span, rawChildren, triviaLog, st, tags) : { _tag: "node", type: nodeType, span: ctx.trackLines ? { ...span } : { start: r.span.start, end: r.span.end }, state: st ?? null, children: children2 };
3781
+ const built = unwrap && children2.length === 1 ? unwrapChild(children2[0]) : collapse && children2.length === 1 ? children2[0] : (cstOutput || !build && project === void 0) && ctx.build?._parsemanCstCollapse && children2.length === 1 && rawChildren.length === 1 && ctx.build._parsemanCstCollapse(nodeType, children2[0], children2, rawChildren) ? children2[0] : project !== void 0 ? cstOutput && ctx.build ? ctx.build(nodeType, children2, fields, span, rawChildren, triviaLog, hostState, tags) : projectChild(children2, project, nodeType) : build ? cstOutput && ctx.build ? ctx.build(nodeType, children2, fields, span, rawChildren, triviaLog, hostState, tags) : build(children2, fields, span, rawChildren, triviaLog, st) : ctx.build ? ctx.build(nodeType, children2, fields, span, rawChildren, triviaLog, st, tags) : { _tag: "node", type: nodeType, span: ctx.trackLines ? { ...span } : { start: r.span.start, end: r.span.end }, state: st ?? null, children: children2 };
3548
3782
  const rawEntry = isCstChild(built) ? built : { _tag: "leaf", value: typeof built === "string" ? built : typeof built === "object" && built !== null ? input.slice(r.span.start, r.span.end) : "", span };
3549
3783
  if (saved.buf !== void 0 || saved.ch !== void 0) {
3550
3784
  pushCstChild(ctx, built, rawEntry);
@@ -3739,7 +3973,7 @@ function leaf(root, fn) {
3739
3973
  }
3740
3974
 
3741
3975
  // src/version.ts
3742
- var PARSEMAN_VERSION = "0.44.0";
3976
+ var PARSEMAN_VERSION = "0.46.0";
3743
3977
 
3744
3978
  // src/cst/host-mode.ts
3745
3979
  var FUSED_HOST_MODE = /* @__PURE__ */ Symbol.for("parseman.fusedHostMode");
@@ -3794,14 +4028,28 @@ function firstSetToString(fs) {
3794
4028
  return fs.ranges.map((r) => r.lo === r.hi ? ch(r.lo) : `${ch(r.lo)}-${ch(r.hi)}`).join(",");
3795
4029
  }
3796
4030
  var ruleNameOf2 = (p) => p._ruleName;
4031
+ var KEYWORD_REGEX_RE = /^\^?[@#.-]?[A-Za-z][\w-]*(\(\?![^)]*\))?\$?$/;
3797
4032
  var SUGGESTIONS = {
3798
- "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.",
3799
- "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.",
3800
- "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.",
3801
- "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.",
3802
- "opaque-wrapper": "this wrapper (guard/withCtx/recover) contributes no first char; put a concrete leading terminal before it.",
3803
- "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."
3804
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
+ }
3805
4053
  function classifyBroadArm(arm, resolve) {
3806
4054
  const seen = /* @__PURE__ */ new Set();
3807
4055
  const relabel = (inner, cause, detail) => ({ cause, detail, ...inner?.unresolvedExternal ? { unresolvedExternal: true } : {} });
@@ -3917,7 +4165,6 @@ function peelToLeading(arm) {
3917
4165
  if (d.tag === "sequence" && d.parsers.length >= 1) return d.parsers[0];
3918
4166
  return cur;
3919
4167
  }
3920
- var KEYWORD_REGEX_RE = /^\^?[@#.-]?[A-Za-z][\w-]*(\(\?![^)]*\))?\$?$/;
3921
4168
  function detectAntiPatterns(rule, arms) {
3922
4169
  const out = [];
3923
4170
  arms.forEach((arm, i) => {
@@ -3930,14 +4177,14 @@ function detectAntiPatterns(rule, arms) {
3930
4177
  kind: "double-not",
3931
4178
  rule,
3932
4179
  armIndex: i,
3933
- 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.'
3934
4181
  });
3935
4182
  } else {
3936
4183
  out.push({
3937
4184
  kind: "leading-not",
3938
4185
  rule,
3939
4186
  armIndex: i,
3940
- 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."
3941
4188
  });
3942
4189
  }
3943
4190
  }
@@ -3946,7 +4193,7 @@ function detectAntiPatterns(rule, arms) {
3946
4193
  kind: "keyword-regex",
3947
4194
  rule,
3948
4195
  armIndex: i,
3949
- 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.`
3950
4197
  });
3951
4198
  }
3952
4199
  });
@@ -3988,7 +4235,7 @@ function analyzeGatingRules(ruleMap, opts) {
3988
4235
  const d = def;
3989
4236
  const rule = ruleNameOf2(p) ?? enclosingRule;
3990
4237
  if (d.tag === "choice") {
3991
- raw.push({ g: analyzeChoice(p, d, rule, opts?.resolveRef), rule });
4238
+ raw.push({ g: analyzeChoice(p, d, rule, opts?.resolveRef), rule, arms: d.parsers });
3992
4239
  antiPatterns.push(...detectAntiPatterns(rule, d.parsers));
3993
4240
  }
3994
4241
  const rec = d;
@@ -4017,13 +4264,15 @@ function analyzeGatingRules(ruleMap, opts) {
4017
4264
  const seenInRule = /* @__PURE__ */ new Map();
4018
4265
  const accept = new Set(opts?.accept ?? []);
4019
4266
  const usedAccept = /* @__PURE__ */ new Set();
4020
- const choices = raw.map(({ g, rule }) => {
4267
+ const choices = raw.map(({ g, rule, arms }) => {
4021
4268
  const n = seenInRule.get(rule) ?? 0;
4022
4269
  seenInRule.set(rule, n + 1);
4023
4270
  const id = (perRule.get(rule) ?? 1) === 1 ? rule : `${rule}#${n}`;
4024
4271
  const isAccepted = g.gates === "no" && accept.has(id);
4025
4272
  if (isAccepted) usedAccept.add(id);
4026
- 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;
4027
4276
  });
4028
4277
  const gated = choices.filter((c) => c.gates === "yes").length;
4029
4278
  const recoverable = choices.filter((c) => c.gates === "recoverable").length;
@@ -4050,7 +4299,14 @@ function analyzeChoice(p, d, rule, resolve) {
4050
4299
  const deep = arms.map((a) => firstSetOf(a, /* @__PURE__ */ new Set(), resolve));
4051
4300
  const anyArms = deep.map((fs, index) => ({ fs, index })).filter((x) => isAny(x.fs)).map(({ index }) => {
4052
4301
  const { cause, detail, unresolvedExternal } = classifyBroadArm(arms[index], resolve);
4053
- 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
+ };
4054
4310
  });
4055
4311
  const overlaps = [];
4056
4312
  for (let i = 0; i < deep.length; i++)
@@ -4060,7 +4316,7 @@ function analyzeChoice(p, d, rule, resolve) {
4060
4316
  a: i,
4061
4317
  b: j,
4062
4318
  on: intersection(deep[i], deep[j]),
4063
- 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."
4064
4320
  });
4065
4321
  let gates;
4066
4322
  if (d.disjoint) gates = "yes";
@@ -4614,7 +4870,7 @@ function findDivergentSlot(members, shapeOf) {
4614
4870
  const path = [];
4615
4871
  const defs = [];
4616
4872
  let cur = [...members];
4617
- for (let guard2 = 0; guard2 < 64; guard2++) {
4873
+ for (let guard = 0; guard < 64; guard++) {
4618
4874
  const d0 = cur[0]._def;
4619
4875
  const kidLists = cur.map((c) => childrenOf2(c._def));
4620
4876
  const arity = kidLists[0].length;
@@ -5573,7 +5829,15 @@ function children(def, winners) {
5573
5829
  let resolved;
5574
5830
  try {
5575
5831
  resolved = def.thunk();
5576
- } catch {
5832
+ } catch (e) {
5833
+ recordDegradation({
5834
+ code: "coverage-definitions-unavailable",
5835
+ severity: "warn",
5836
+ where: "<lazy rule reference>",
5837
+ subject: `unresolvable reference (${e.message})`,
5838
+ fellBackTo: "the coverage walk stopped at this reference, so every definition below it is ABSENT from the denominator \u2014 which raises the reported ratio rather than lowering it",
5839
+ otherwise: "the referenced rule and its whole subtree would contribute definitions"
5840
+ });
5577
5841
  return [];
5578
5842
  }
5579
5843
  const name = resolved._ruleName;
@@ -5864,15 +6128,15 @@ function parseStringShape(source) {
5864
6128
  if (source.length < 3 || source[source.length - 1] !== q) return null;
5865
6129
  const inner = source.slice(1, -1);
5866
6130
  const CLS = String.raw`\[\^((?:\\.|[^\]])+)\]`;
5867
- const ESC = String.raw`\\\\(\.|\[\\s\\S\]|\[\\S\\s\])`;
6131
+ const ESC2 = String.raw`\\\\(\.|\[\\s\\S\]|\[\\S\\s\])`;
5868
6132
  let body;
5869
6133
  let esc;
5870
- let m = new RegExp(`^\\((?:\\?:)?${CLS}\\|${ESC}\\)\\*$`).exec(inner);
6134
+ let m = new RegExp(`^\\((?:\\?:)?${CLS}\\|${ESC2}\\)\\*$`).exec(inner);
5871
6135
  if (m) {
5872
6136
  body = m[1];
5873
6137
  esc = m[2];
5874
6138
  } else {
5875
- m = new RegExp(`^\\((?:\\?:)?${ESC}\\|${CLS}\\)\\*$`).exec(inner);
6139
+ m = new RegExp(`^\\((?:\\?:)?${ESC2}\\|${CLS}\\)\\*$`).exec(inner);
5876
6140
  if (m) {
5877
6141
  esc = m[1];
5878
6142
  body = m[2];
@@ -5882,8 +6146,8 @@ function parseStringShape(source) {
5882
6146
  const excluded = parseClassRanges(body);
5883
6147
  if (!excluded) return null;
5884
6148
  const qcp = q.codePointAt(0);
5885
- const inSet = (cp) => excluded.some(([lo, hi]) => cp >= lo && cp <= hi);
5886
- 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;
5887
6151
  return { kind: "string", quote: qcp, excluded, escLineTerm: esc !== "." };
5888
6152
  }
5889
6153
  function parseSeqParts(source) {
@@ -7074,27 +7338,53 @@ function emitScannableTerminal(shape, c) {
7074
7338
  }
7075
7339
 
7076
7340
  // src/compiler/inline-build.ts
7077
- var MK_BUILD_RE = /^\(\s*(\w+)\s*,\s*\w+\s*,\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*\)\s*=>\s*mk\s*\(\s*(['"])([^'"]+)\5\s*,\s*\1\s*,\s*\3\s*,\s*\2\s*,\s*\4\s*\)\s*$/;
7341
+ var ANN = String.raw`\s*\??\s*(?::[^,)=]+)?`;
7342
+ var ANY_P = String.raw`\w+${ANN}`;
7343
+ var MK_BUILD_RE = new RegExp(
7344
+ String.raw`^\(\s*(\w+)${ANN}\s*,\s*${ANY_P}\s*,\s*(\w+)${ANN}\s*,\s*(\w+)${ANN}\s*,\s*(\w+)${ANN}\s*\)\s*=>\s*mk\s*\(\s*(['"])([^'"]+)\5\s*,\s*\1\s*,\s*\3\s*,\s*\2\s*,\s*\4\s*\)\s*$`
7345
+ );
7078
7346
  function escapeRegExp(s) {
7079
7347
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7080
7348
  }
7349
+ function identMkBuildRe(typeIdent) {
7350
+ return new RegExp(
7351
+ String.raw`^\(\s*(\w+)${ANN}\s*,\s*${ANY_P}\s*,\s*(\w+)${ANN}\s*,\s*(\w+)${ANN}\s*,\s*(\w+)${ANN}\s*\)\s*=>\s*(?:[\w$.]*\.)?mk\s*\(\s*${escapeRegExp(typeIdent)}\s*,\s*\1\s*,\s*\3\s*,\s*\2\s*,\s*\4\s*\)\s*$`
7352
+ );
7353
+ }
7081
7354
  function looseMkBuildRe(type) {
7082
7355
  return new RegExp(
7083
- String.raw`^\(\s*(\w+)\s*,\s*\w+\s*,\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*\)\s*=>\s*.+\(\s*(['"])${escapeRegExp(type)}\5\s*,\s*\1\s*,\s*\3\s*,\s*\2\s*,\s*\4\s*\)\s*$`
7356
+ String.raw`^\(\s*(\w+)${ANN}\s*,\s*${ANY_P}\s*,\s*(\w+)${ANN}\s*,\s*(\w+)${ANN}\s*,\s*(\w+)${ANN}\s*\)\s*=>\s*.+\(\s*(['"])${escapeRegExp(type)}\5\s*,\s*\1\s*,\s*\3\s*,\s*\2\s*,\s*\4\s*\)\s*$`
7084
7357
  );
7085
7358
  }
7359
+ var LOOKS_LIKE_MK_RE = /=>\s*(?:[\w$.]*\.)?mk\s*\(/;
7086
7360
  function analyzeMkInlineBuild(def) {
7087
7361
  if (!def.build) return null;
7088
7362
  if (def.type === void 0) return null;
7089
- const src = (def.buildSrc ?? def.build.toString()).trim();
7363
+ const src = buildAnalysisSrc(def).trim();
7090
7364
  const strict = src.match(MK_BUILD_RE);
7091
7365
  if (strict) {
7092
7366
  const mkType = strict[6];
7093
- return mkType === def.type ? def.type : null;
7367
+ if (mkType === def.type) return def.type;
7368
+ reportMkMiss(def.type, src, `it builds a "${mkType}" node, not "${def.type}"`);
7369
+ return null;
7094
7370
  }
7095
7371
  if (looseMkBuildRe(def.type).test(src)) return def.type;
7372
+ if (def.typeSrc !== void 0 && identMkBuildRe(def.typeSrc).test(src)) return def.type;
7373
+ if (LOOKS_LIKE_MK_RE.test(src)) {
7374
+ reportMkMiss(def.type, src, "its parameter list or argument order does not match `(children, fields, span, rawChildren, triviaLog) => mk(type, children, rawChildren, span, triviaLog)`");
7375
+ }
7096
7376
  return null;
7097
7377
  }
7378
+ function reportMkMiss(type, src, why) {
7379
+ recordDegradation({
7380
+ code: "mk-inline-missed",
7381
+ severity: "warn",
7382
+ where: `node("${type}")`,
7383
+ subject: `build reducer \`${src.split("\n")[0].slice(0, 80)}\``,
7384
+ fellBackTo: `looks like an inline-\`mk\` builder but ${why}, so every match pays a \`_build[n](...)\` call instead of an inlined object literal`,
7385
+ otherwise: "the node object would be constructed at the call site with no call frame"
7386
+ });
7387
+ }
7098
7388
  function emitInlineMkNodeExpr(type, chV, rawV, spanExpr, tlV) {
7099
7389
  return `{ _tag: 'node', type: ${JSON.stringify(type)}, span: ${spanExpr}, children: ${chV}, rawCount: ${rawV}.length, localTriviaLen: ${tlV}.length }`;
7100
7390
  }
@@ -7277,10 +7567,34 @@ function leadingFirstSetRecipe(p, seen = /* @__PURE__ */ new Set()) {
7277
7567
  seen.delete(p);
7278
7568
  }
7279
7569
  }
7570
+ function tern(cond, whenTrue, whenFalse) {
7571
+ if (cond === "false") return whenFalse;
7572
+ if (cond === "true") return whenTrue;
7573
+ return `${cond} ? ${whenTrue} : ${whenFalse}`;
7574
+ }
7575
+ function notGate(cond) {
7576
+ if (cond === "false") return "true";
7577
+ if (cond === "true") return "false";
7578
+ return `!(${cond})`;
7579
+ }
7580
+ function orGate(a, b) {
7581
+ if (a === "false") return b;
7582
+ if (b === "false") return a;
7583
+ if (a === "true" || b === "true") return "true";
7584
+ return `${a} || ${b}`;
7585
+ }
7586
+ function andGate(a, b) {
7587
+ if (a === "false" || b === "false") return "false";
7588
+ if (a === "true") return b;
7589
+ if (b === "true") return a;
7590
+ return `${a} && ${b}`;
7591
+ }
7280
7592
  var HOST_READS_DECL = "const _hostReads = (b, n) => { if (b === undefined) return false; let s; try { s = Function.prototype.toString.call(b) } catch (e) { return true } if (/\\barguments\\b/.test(s)) return true; const m = /^[^(]*\\(([\\s\\S]*?)\\)/.exec(s); if (m && /\\.\\.\\.|=/.test(m[1])) return true; return b.length > n }";
7593
+ var RAW_ENTRY_DECL = "const _rawEntry = (v, input, s, e, span) => (typeof v === 'object' && v !== null && (v._tag === 'node' || v._tag === 'leaf' || v._tag === 'parseError')) ? v : { _tag: 'leaf', value: typeof v === 'string' ? v : (typeof v === 'object' && v !== null ? input.slice(s, e) : ''), span: span ?? { start: s, end: e } }";
7281
7594
  var LINE_TRACK_DECL = "const _trackLines = (_ctx, input, start, end) => { const from = _ctx._lineScannedTo ?? 0; if (end <= from) return; for (let i = from; i < end; i++) if (input.charCodeAt(i) === 10) _ctx._lineStarts.push(i + 1); _ctx._lineScannedTo = end }";
7282
7595
  var LINE_SPAN_DECL = "const _lineCol = (_ctx, offset) => { const starts = _ctx._lineStarts; let lo = 0, hi = starts.length - 1; while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (starts[mid] <= offset) lo = mid; else hi = mid - 1 } return [lo + 1, offset - starts[lo] + 1] }\nconst _spanLines = (_ctx, start, end) => { const s = _lineCol(_ctx, start), e = _lineCol(_ctx, end); return { start, end, startLine: s[0], startColumn: s[1], endLine: e[0], endColumn: e[1] } }";
7283
7596
  var _loweringSink = null;
7597
+ var _inlineCapSink = null;
7284
7598
  function hasSelectedRootTrivia(ctx) {
7285
7599
  return ctx.rootTriviaClassified === true;
7286
7600
  }
@@ -8029,9 +8343,11 @@ function emitSeqValues(def, ctx, pos) {
8029
8343
  const markLog = v(ctx, "_mklg");
8030
8344
  const markRootLog = hasSelectedRootTrivia(ctx) ? v(ctx, "_mkrlg") : null;
8031
8345
  const scanEndV = v(ctx, "_sne");
8032
- const capArg = ctx.noHoist ? "0" : hasSelectedRootTrivia(ctx) ? "(_ctx._triviaLog !== undefined || _ctx._rootTriviaLog !== undefined) ? 2 : 0" : "_ctx._triviaLog !== undefined ? 2 : 0";
8346
+ const logV = v(ctx, "_tlg");
8347
+ const capArg = ctx.noHoist ? "0" : hasSelectedRootTrivia(ctx) ? `(${logV} !== undefined || _ctx._rootTriviaLog !== undefined) ? 2 : 0` : `${logV} !== undefined ? 2 : 0`;
8033
8348
  stmts.push(
8034
- `${ind(ctx)}const ${markLog} = _ctx._triviaLog ? _ctx._triviaLog.length : 0`,
8349
+ `${ind(ctx)}const ${logV} = _ctx._triviaLog`,
8350
+ `${ind(ctx)}const ${markLog} = ${logV} !== undefined ? ${logV}.length : 0`,
8035
8351
  ...markRootLog ? [`${ind(ctx)}const ${markRootLog} = _ctx._rootTriviaLog ? _ctx._rootTriviaLog.length : 0`] : [],
8036
8352
  `${ind(ctx)}const ${scanEndV} = ${trivFn}(input, ${curV}, _ctx, ${capArg})`,
8037
8353
  ...emitLineTrack(ctx, curV, scanEndV)
@@ -8041,7 +8357,7 @@ function emitSeqValues(def, ctx, pos) {
8041
8357
  const endAfterV = v(ctx, "_sea");
8042
8358
  stmts.push(
8043
8359
  `${ind(ctx)}const ${endAfterV} = ${r2.endVar}`,
8044
- `${ind(ctx)}if (${endAfterV} > ${scanEndV}) ${curV} = ${endAfterV}; else { if (_ctx._triviaLog && _ctx._triviaLog.length !== ${markLog}) _ctx._triviaLog.length = ${markLog};${markRootLog ? ` if (_ctx._rootTriviaLog && _ctx._rootTriviaLog.length !== ${markRootLog}) _ctx._rootTriviaLog.length = ${markRootLog};` : ""} }`
8360
+ `${ind(ctx)}if (${endAfterV} > ${scanEndV}) ${curV} = ${endAfterV}; else { if (${logV} !== undefined && ${logV}.length !== ${markLog}) ${logV}.length = ${markLog};${markRootLog ? ` if (_ctx._rootTriviaLog && _ctx._rootTriviaLog.length !== ${markRootLog}) _ctx._rootTriviaLog.length = ${markRootLog};` : ""} }`
8045
8361
  );
8046
8362
  valueVars.push(r2.valueVar);
8047
8363
  continue;
@@ -9113,9 +9429,18 @@ function emitPeek(def, ctx, pos) {
9113
9429
  endVar: pos
9114
9430
  };
9115
9431
  }
9116
- function emitRouted(ctx, pos) {
9432
+ function emitRouted(ctx, pos, def) {
9117
9433
  const local = ctx.routedLocal;
9434
+ const fallback = def.fallback;
9118
9435
  if (local !== void 0) {
9436
+ if (fallback !== void 0 && local.startVar !== pos) {
9437
+ return emitRoutedWithFallback(ctx, pos, fallback, {
9438
+ test: `${local.startVar} === ${pos}`,
9439
+ valExpr: local.valueVar,
9440
+ startExpr: local.startVar,
9441
+ endExpr: local.endVar
9442
+ });
9443
+ }
9119
9444
  return {
9120
9445
  stmts: [
9121
9446
  ...emitIfFail(ctx, `${local.startVar} !== ${pos}`, failBody(ctx, '"routed()"', pos)),
@@ -9126,9 +9451,19 @@ function emitRouted(ctx, pos) {
9126
9451
  };
9127
9452
  }
9128
9453
  const item = v(ctx, "_rt");
9454
+ const read = `${ind(ctx)}const ${item} = _ctx._routed`;
9455
+ if (fallback !== void 0) {
9456
+ const r = emitRoutedWithFallback(ctx, pos, fallback, {
9457
+ test: `${item} !== undefined && ${item}.span.start === ${pos}`,
9458
+ valExpr: `${item}.value`,
9459
+ startExpr: `${item}.span.start`,
9460
+ endExpr: `${item}.span.end`
9461
+ });
9462
+ return { ...r, stmts: [read, ...r.stmts] };
9463
+ }
9129
9464
  return {
9130
9465
  stmts: [
9131
- `${ind(ctx)}const ${item} = _ctx._routed`,
9466
+ read,
9132
9467
  ...emitIfFail(ctx, `${item} === undefined || ${item}.span.start !== ${pos}`, failBody(ctx, '"routed()"', pos)),
9133
9468
  ...emitLeafCapture(ctx, `${item}.value`, `${item}.span.start`, `${item}.span.end`)
9134
9469
  ],
@@ -9136,6 +9471,30 @@ function emitRouted(ctx, pos) {
9136
9471
  endVar: `${item}.span.end`
9137
9472
  };
9138
9473
  }
9474
+ function emitRoutedWithFallback(ctx, pos, fallback, routed2) {
9475
+ const valV = v(ctx, "_rtv");
9476
+ const endV = v(ctx, "_rte");
9477
+ const i = ind(ctx);
9478
+ ctx.indent++;
9479
+ const capture = emitLeafCapture(ctx, routed2.valExpr, routed2.startExpr, routed2.endExpr);
9480
+ const fb = emit(fallback, ctx, pos);
9481
+ const inner = ind(ctx);
9482
+ ctx.indent--;
9483
+ return {
9484
+ stmts: [
9485
+ `${i}let ${valV}, ${endV}`,
9486
+ `${i}if (${routed2.test}) {`,
9487
+ ...capture,
9488
+ `${inner}${valV} = ${routed2.valExpr}; ${endV} = ${routed2.endExpr}`,
9489
+ `${i}} else {`,
9490
+ ...fb.stmts,
9491
+ `${inner}${valV} = ${fb.valueVar}; ${endV} = ${fb.endVar}`,
9492
+ `${i}}`
9493
+ ],
9494
+ valueVar: valV,
9495
+ endVar: endV
9496
+ };
9497
+ }
9139
9498
  function escapeTokenLiteral(value) {
9140
9499
  return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
9141
9500
  }
@@ -9317,22 +9676,16 @@ function emitNode(def, ctx, pos) {
9317
9676
  const smk = structural ? v(ctx, "_smk") : null;
9318
9677
  if (structural) ctx.needsHostReads = true;
9319
9678
  const hostTriviaGate = `_ctx.build !== undefined && (_ctx.build._parsemanCaptureTrivia !== undefined ? _ctx.build._parsemanCaptureTrivia(${JSON.stringify(def.type)}) : (_ctx._pmCapTL ??= _hostReads(_ctx.build, 5)))`;
9320
- const pmV = v(ctx, "_pm"), recV = v(ctx, "_rec"), capV = v(ctx, "_cap");
9321
- const profHoist = `${i}const ${pmV} = _ctx._pmProfile, ${recV} = ${pmV}?.phase === 'recognizer', ${capV} = ${pmV}?.phase === 'capture'`;
9322
- const profileRecognizer = recV;
9323
- const profileCapture = capV;
9324
- const cstMode = ctx.hostMode === "cst";
9325
- const directCstV = !structural && !cstMode && (!capturesChildren || !capturesRaw) ? v(ctx, "_dcst") : null;
9326
- const directCstGate = directCstV === null ? "false" : profileCapture;
9679
+ const profileRecognizer = "false";
9680
+ const profileCapture = "false";
9327
9681
  const structuralCapturesTrivia = structural && (def.captureTrivia === true || def.trailingTrivia === true);
9328
9682
  const chNeededExpr = def.unwrap || def.collapse ? "true" : `(_ctx._pmReadsCh ??= (_ctx.build === undefined || _ctx.build._parsemanReadsChildren !== false || _ctx.build._parsemanCstCollapse !== undefined))`;
9329
- const chAlloc = def.unwrap || def.collapse ? `${profileRecognizer} ? undefined : []` : `${profileRecognizer} ? undefined : ((${profileCapture} || ${chNeededExpr}) ? [] : undefined)`;
9330
- const allocStmt = structural ? `${i}const ${capTLv} = !(${profileRecognizer}) && (${profileCapture} || ${structuralCapturesTrivia ? "true" : hostTriviaGate}), ${capSTv} = !(${profileRecognizer} || ${profileCapture}) && (_ctx._pmCapST ??= (_ctx.build === undefined || _hostReads(_ctx.build, 6)))${capFv ? `, ${capFv} = !(${profileRecognizer}) && (${profileCapture} || (_ctx.build !== undefined && _hostReads(_ctx.build, 2)))` : ""}
9331
- ${i}const ${chV} = ${chAlloc}, ${rawV} = ${profileRecognizer} ? undefined : [], ${tlV} = ${profileRecognizer} ? undefined : ${innerEnablesTriviaCapture ? "[]" : `${capTLv} ? [] : _EMPTY_TL`}` : capturesTrivia ? `${directCstV ? `${i}const ${directCstV} = ${directCstGate}
9332
- ` : ""}${i}const ${capTLv} = !(${profileRecognizer}), ${chV} = ${profileRecognizer} ? undefined : ${capturesChildren ? "[]" : `${directCstV} ? [] : undefined`}, ${rawV} = ${profileRecognizer} ? undefined : ${capturesRaw ? "[]" : `${directCstV} ? [] : undefined`}, ${tlV} = ${profileRecognizer} ? undefined : []` : `${directCstV ? `${i}const ${directCstV} = ${directCstGate}
9333
- ` : ""}${i}const ${chV} = ${profileRecognizer} ? undefined : ${capturesChildren ? "[]" : `${directCstV} ? [] : undefined`}, ${rawV} = ${profileRecognizer} ? undefined : ${capturesRaw ? "[]" : `${directCstV} ? [] : undefined`}`;
9683
+ const chAlloc = def.unwrap || def.collapse ? tern(profileRecognizer, "undefined", "[]") : tern(profileRecognizer, "undefined", `${tern(orGate(profileCapture, chNeededExpr), "[]", "undefined")}`);
9684
+ const dcstAlloc = "undefined";
9685
+ const allocStmt = structural ? `${i}const ${capTLv} = ${andGate(notGate(profileRecognizer), orGate(profileCapture, structuralCapturesTrivia ? "true" : hostTriviaGate))}, ${capSTv} = ${andGate(notGate(orGate(profileRecognizer, profileCapture)), "(_ctx._pmCapST ??= (_ctx.build === undefined || _hostReads(_ctx.build, 6)))")}${capFv ? `, ${capFv} = ${andGate(notGate(profileRecognizer), orGate(profileCapture, "(_ctx.build !== undefined && _hostReads(_ctx.build, 2))"))}` : ""}
9686
+ ${i}const ${chV} = ${chAlloc}, ${rawV} = ${tern(profileRecognizer, "undefined", "[]")}, ${tlV} = ${tern(profileRecognizer, "undefined", innerEnablesTriviaCapture ? "[]" : `${capTLv} ? [] : _EMPTY_TL`)}` : capturesTrivia ? `${i}const ${capTLv} = ${notGate(profileRecognizer)}, ${chV} = ${tern(profileRecognizer, "undefined", capturesChildren ? "[]" : dcstAlloc)}, ${rawV} = ${tern(profileRecognizer, "undefined", capturesRaw ? "[]" : dcstAlloc)}, ${tlV} = ${tern(profileRecognizer, "undefined", "[]")}` : `${i}const ${chV} = ${tern(profileRecognizer, "undefined", capturesChildren ? "[]" : dcstAlloc)}, ${rawV} = ${tern(profileRecognizer, "undefined", capturesRaw ? "[]" : dcstAlloc)}`;
9334
9687
  const innerTl = structural || capturesTrivia ? structural && !innerEnablesTriviaCapture ? `${capTLv} ? ${tlV} : undefined` : tlV : "undefined";
9335
- const fieldsOn = structural ? capFv ?? "false" : `${profileRecognizer} ? false : ${capturesFields ? "true" : "false"}`;
9688
+ const fieldsOn = structural ? capFv ?? "false" : tern(profileRecognizer, "false", capturesFields ? "true" : "false");
9336
9689
  const sf = hasFields ? v(ctx, "_sf") : null;
9337
9690
  const fArr = hasFields ? v(ctx, "_fa") : null;
9338
9691
  const fObj = hasFields ? v(ctx, "_fields") : "undefined";
@@ -9341,7 +9694,7 @@ ${i}const ${chV} = ${chAlloc}, ${rawV} = ${profileRecognizer} ? undefined : [],
9341
9694
  const installTrivia = needsTriviaFrame ? `; _ctx.captureTrivia = ${capTLv ?? "false"}; _ctx._cstTriviaLog = ${innerTl}${smk ? `; _ctx._triviaCaptureMask = ${capTLv} && _ctx.build !== undefined && _ctx.build._parsemanTriviaKinds !== undefined ? _ctx.build._parsemanTriviaKinds(${JSON.stringify(def.type)}) : ${smk}` : ""}` : "";
9342
9695
  const restoreTrivia = needsTriviaFrame ? `; _ctx.captureTrivia = ${st}; _ctx._cstTriviaLog = ${stl}${smk ? `; _ctx._triviaCaptureMask = ${smk}` : ""}` : "";
9343
9696
  const preGuard = [];
9344
- if (!ctx.recovery && !ctx.coverage && (capturesChildren || structural) && needsFirstSetGuard(def.parser)) {
9697
+ if (!ctx.recovery && !ctx.coverage && needsFirstSetGuard(def.parser)) {
9345
9698
  const gcV = v(ctx, "_ngc");
9346
9699
  const gExp = armStaticExpected(ctx, def.parser);
9347
9700
  const gFail = ctx.failLabel ? ctx.recordFail ? `{ _ctx._fe = ${pos}; _ctx._fx = ${gExp}; break ${ctx.failLabel} }` : `break ${ctx.failLabel}` : failReturnArr(ctx, gExp, pos);
@@ -9352,7 +9705,6 @@ ${i}const ${chV} = ${chAlloc}, ${rawV} = ${profileRecognizer} ? undefined : [],
9352
9705
  }
9353
9706
  const stmts = [
9354
9707
  ...preGuard,
9355
- profHoist,
9356
9708
  allocStmt,
9357
9709
  `${i}const ${sc} = _ctx._cstChildren, ${sl} = _ctx._cstLeaves, ${sr} = _ctx._cstRawChildren${saveTrivia}${sf ? `, ${sf} = _ctx._fields` : ""}`,
9358
9710
  `${i}_ctx._cstChildren = ${chV}; _ctx._cstLeaves = ${chV}; _ctx._cstRawChildren = ${rawV}${installTrivia}${sf ? `; _ctx._fields = ${fieldsOn} ? [] : undefined` : ""}`
@@ -9369,21 +9721,13 @@ ${i}const ${chV} = ${chAlloc}, ${rawV} = ${profileRecognizer} ? undefined : [],
9369
9721
  }
9370
9722
  stmts.push(`${i}_ctx._cstChildren = ${sc}; _ctx._cstLeaves = ${sl}; _ctx._cstRawChildren = ${sr}${restoreTrivia}${sf ? `; _ctx._fields = ${sf}` : ""}`);
9371
9723
  stmts.push(...emitIfFail(ctx, `!${okVar}`, propagateFailBody(ctx)));
9372
- stmts.push(
9373
- `${i}if (_ctx._pmProfile) {`,
9374
- `${i} _ctx._pmProfile.nodes++`,
9375
- `${i} if (${profileCapture}) {`,
9376
- `${i} _ctx._pmProfile.childSlots += ${chV}.length; _ctx._pmProfile.rawSlots += ${rawV}.length; _ctx._pmProfile.triviaSlots += ${tlV}.length${fArr ? `; _ctx._pmProfile.fieldSlots += ${fArr} ? ${fArr}.length : 0` : ""}`,
9377
- `${i} }`,
9378
- `${i}}`
9379
- );
9380
9724
  let stV = "undefined";
9381
9725
  if (structural) {
9382
9726
  stV = v(ctx, "_nst");
9383
9727
  stmts.push(`${i}const ${stV} = ${capSTv} && _ctx.state !== undefined ? Object.assign({}, _ctx.state) : undefined`);
9384
9728
  } else if (clonesState) {
9385
9729
  stV = v(ctx, "_nst");
9386
- stmts.push(`${i}const ${stV} = !(${profileRecognizer} || ${profileCapture}) && _ctx.state !== undefined ? Object.assign({}, _ctx.state) : undefined`);
9730
+ stmts.push(`${i}const ${stV} = ${andGate(notGate(orGate(profileRecognizer, profileCapture)), "_ctx.state !== undefined")} ? Object.assign({}, _ctx.state) : undefined`);
9387
9731
  }
9388
9732
  if (sf && fArr) {
9389
9733
  const fe = v(ctx, "_fe");
@@ -9407,19 +9751,22 @@ ${i}const ${chV} = ${chAlloc}, ${rawV} = ${profileRecognizer} ? undefined : [],
9407
9751
  if (ctx.lineTracking) stmts.push(`${i}const ${nodeSpanV} = ${nodeSpanExpr}`);
9408
9752
  const buildExpr = structural ? `{ _tag: 'node', type: ${JSON.stringify(def.type)}, span: ${nodeSpanV}, state: ${stV} ?? null, children: ${chV} }` : mkType ? emitInlineMkNodeExpr(mkType, chV, rawV, nodeSpanV, tlV) : def.project !== void 0 ? emitNodeProjectExpr(def.type, def.project, chV) : `${buildRef(ctx)}[${buildIdx}](${chV}, ${fObj}, ${nodeSpanV}, ${rawV}, ${tlV}, ${stV})`;
9409
9753
  const hostBuildArgs = `${JSON.stringify(def.type)}, ${chV}, ${fObj}, ${nodeSpanV}, ${rawV}, ${tlV}, ${stV}${def.tags !== void 0 && def.tags.length > 0 ? `, ${JSON.stringify(def.tags)}` : ""}`;
9410
- const hostBuildExpr = `_ctx._pmProfile?.phase === 'host' && _ctx._pmProfile.hostCalls++, _ctx.build(${hostBuildArgs})`;
9754
+ const hostBuildExpr = `_ctx.build(${hostBuildArgs})`;
9411
9755
  const ndExpr = structural ? `_ctx.build !== undefined ? (${hostBuildExpr}) : (${buildExpr})` : cstOut ? hostBuildExpr : buildExpr;
9412
- const hostCollapseExpr = structural ? `_ctx.build !== undefined && _ctx.build._parsemanCstCollapse !== undefined && ${chV}.length === 1 && ${rawV}.length === 1 && _ctx.build._parsemanCstCollapse(${JSON.stringify(def.type)}, ${chV}[0], ${chV}, ${rawV}) ? ${chV}[0] : (${ndExpr})` : ndExpr;
9756
+ const hostCollapses = structural || cstOut;
9757
+ const hostCollapseExpr = hostCollapses ? `_ctx.build !== undefined && _ctx.build._parsemanCstCollapse !== undefined && ${chV}.length === 1 && ${rawV}.length === 1 && _ctx.build._parsemanCstCollapse(${JSON.stringify(def.type)}, ${chV}[0], ${chV}, ${rawV}) ? ${chV}[0] : (${ndExpr})` : ndExpr;
9413
9758
  const unwrapExpr = `${chV}.length === 1 ? (${chV}[0] !== null && typeof ${chV}[0] === 'object' && ${chV}[0]._tag === 'leaf' ? ${chV}[0].value : ${chV}[0]) : (${ndExpr})`;
9414
9759
  const collapseExpr = `${chV}.length === 1 ? ${chV}[0] : (${ndExpr})`;
9415
9760
  const finalExpr = def.unwrap ? unwrapExpr : def.collapse ? collapseExpr : hostCollapseExpr;
9416
- stmts.push(
9417
- `${i}const ${ndV} = (${profileRecognizer} || ${profileCapture}) ? undefined : (${finalExpr})`,
9418
- `${i}if (!(${profileRecognizer})) {`,
9419
- `${i} if (${sc}) ${sc}.push(${ndV})`,
9420
- `${i} if (${sr}) ${sr}.push((typeof ${ndV} === 'object' && ${ndV} !== null && (${ndV}._tag === 'node' || ${ndV}._tag === 'leaf' || ${ndV}._tag === 'parseError')) ? ${ndV} : { _tag: 'leaf', value: typeof ${ndV} === 'string' ? ${ndV} : (typeof ${ndV} === 'object' && ${ndV} !== null ? input.slice(${pos}, ${endVar}) : ''), span: ${nodeSpanV} })`,
9421
- `${i}}`
9422
- );
9761
+ const ndGate = orGate(profileRecognizer, profileCapture);
9762
+ const recGate = notGate(profileRecognizer);
9763
+ const pushIndent = recGate === "true" ? i : `${i} `;
9764
+ const pushCh = `${pushIndent}if (${sc}) ${sc}.push(${ndV})`;
9765
+ ctx.needsRawEntry = true;
9766
+ const pushRaw = `${pushIndent}if (${sr}) ${sr}.push(_rawEntry(${ndV}, input, ${pos}, ${endVar}${ctx.lineTracking ? `, ${nodeSpanV}` : ""}))`;
9767
+ stmts.push(`${i}const ${ndV} = ${tern(ndGate, "undefined", `(${finalExpr})`)}`);
9768
+ if (recGate === "true") stmts.push(pushCh, pushRaw);
9769
+ else stmts.push(`${i}if (${recGate}) {`, pushCh, pushRaw, `${i}}`);
9423
9770
  return { stmts, valueVar: ndV, endVar };
9424
9771
  }
9425
9772
  function emitRuntimeFallback(parser2, ctx, pos) {
@@ -9448,7 +9795,12 @@ function emitLazy(p, def, ctx, pos) {
9448
9795
  } catch {
9449
9796
  return emitRuntimeFallback(p, ctx, pos);
9450
9797
  }
9451
- return emit(resolved, ctx, pos);
9798
+ const cost = usage.sizes.get(resolved) ?? 1;
9799
+ if (cost <= ctx.inlineLeft) {
9800
+ ctx.inlineLeft -= cost;
9801
+ return emit(resolved, ctx, pos);
9802
+ }
9803
+ _inlineCapSink?.push({ fn: ctx.currentFnName ?? "<root>", nodes: cost });
9452
9804
  }
9453
9805
  if (!ctx.namedParsers.has(p)) {
9454
9806
  const fnName2 = canonical2 ?? `${nsp(ctx)}_pf${ctx.namedParsers.size}`;
@@ -9463,6 +9815,9 @@ function emitLazy(p, def, ctx, pos) {
9463
9815
  const savedIndent = ctx.indent;
9464
9816
  const savedFailLabel = ctx.failLabel;
9465
9817
  const savedRecord = ctx.recordFail;
9818
+ const savedInlineLeft = ctx.inlineLeft, savedFnName = ctx.currentFnName;
9819
+ ctx.inlineLeft = ctx.inlineMax;
9820
+ ctx.currentFnName = fnName2;
9466
9821
  const failureRuleId = p === ctx.coverage?.entry ? void 0 : ctx.coverage?.plan.rules.get(p) ?? ctx.coverage?.plan.rules.get(resolved);
9467
9822
  const savedActiveRule = ctx.activeCoverageRuleId;
9468
9823
  const savedSuppressFailure = ctx.suppressCoverageFailure;
@@ -9477,6 +9832,8 @@ function emitLazy(p, def, ctx, pos) {
9477
9832
  ctx.indent = savedIndent;
9478
9833
  ctx.failLabel = savedFailLabel;
9479
9834
  ctx.recordFail = savedRecord;
9835
+ ctx.inlineLeft = savedInlineLeft;
9836
+ ctx.currentFnName = savedFnName;
9480
9837
  ctx.activeCoverageRuleId = savedActiveRule;
9481
9838
  ctx.suppressCoverageFailure = savedSuppressFailure;
9482
9839
  pushNamedFnDecl(ctx, fnName2, r.stmts, r.valueVar, r.endVar, failureRuleId);
@@ -9571,6 +9928,9 @@ function emit(p, ctx, pos) {
9571
9928
  const savedIndent = ctx.indent;
9572
9929
  const savedFailLabel = ctx.failLabel;
9573
9930
  const savedRecord = ctx.recordFail;
9931
+ const savedInlineLeft = ctx.inlineLeft, savedFnName = ctx.currentFnName;
9932
+ ctx.inlineLeft = ctx.inlineMax;
9933
+ ctx.currentFnName = fnName;
9574
9934
  ctx.indent = 1;
9575
9935
  ctx.failLabel = "_pfail";
9576
9936
  ctx.recordFail = true;
@@ -9578,6 +9938,8 @@ function emit(p, ctx, pos) {
9578
9938
  ctx.indent = savedIndent;
9579
9939
  ctx.failLabel = savedFailLabel;
9580
9940
  ctx.recordFail = savedRecord;
9941
+ ctx.inlineLeft = savedInlineLeft;
9942
+ ctx.currentFnName = savedFnName;
9581
9943
  pushNamedFnDecl(ctx, fnName, r.stmts, r.valueVar, r.endVar);
9582
9944
  return instrument(emitNamedFnCall(ctx, fnName, pos));
9583
9945
  }
@@ -9771,7 +10133,7 @@ function emitDispatch(p, ctx, pos) {
9771
10133
  case "peek":
9772
10134
  return emitPeek(def, ctx, pos);
9773
10135
  case "routed":
9774
- return emitRouted(ctx, pos);
10136
+ return emitRouted(ctx, pos, def);
9775
10137
  case "node":
9776
10138
  return emitNode(def, ctx, pos);
9777
10139
  case "scanTo":
@@ -9806,6 +10168,9 @@ function emitDispatch(p, ctx, pos) {
9806
10168
  const savedIndent = ctx.indent;
9807
10169
  const savedFailLabel = ctx.failLabel;
9808
10170
  const savedRecord = ctx.recordFail;
10171
+ const savedInlineLeft = ctx.inlineLeft, savedFnName = ctx.currentFnName;
10172
+ ctx.inlineLeft = ctx.inlineMax;
10173
+ ctx.currentFnName = fnName;
9809
10174
  ctx.indent = 1;
9810
10175
  ctx.failLabel = "_pfail";
9811
10176
  ctx.recordFail = true;
@@ -9813,6 +10178,8 @@ function emitDispatch(p, ctx, pos) {
9813
10178
  ctx.indent = savedIndent;
9814
10179
  ctx.failLabel = savedFailLabel;
9815
10180
  ctx.recordFail = savedRecord;
10181
+ ctx.inlineLeft = savedInlineLeft;
10182
+ ctx.currentFnName = savedFnName;
9816
10183
  pushNamedFnDecl(ctx, fnName, innerR.stmts, innerR.valueVar, innerR.endVar);
9817
10184
  }
9818
10185
  const fn = ctx.namedParsers.get(innerParser);
@@ -9971,12 +10338,16 @@ function childrenOf3(def) {
9971
10338
  return [def.parser, def.sentinel];
9972
10339
  case "scanTo":
9973
10340
  return [def.sentinel, ...def.skip];
10341
+ // A `routed()` fallback IS emitted at this site (emitRouted), so the usage
10342
+ // analysis must see the edge — otherwise a fallback shared by several routed()
10343
+ // sites is miscounted as single-use and inlined at each.
10344
+ case "routed":
10345
+ return def.fallback ? [def.fallback] : [];
9974
10346
  case "lazy":
9975
10347
  case "literal":
9976
10348
  case "regex":
9977
10349
  case "keywords":
9978
10350
  case "guard":
9979
- case "routed":
9980
10351
  case "unknown":
9981
10352
  return [];
9982
10353
  }
@@ -9997,6 +10368,17 @@ function isHoistableTag(tag) {
9997
10368
  return HOISTABLE_TAGS.has(tag);
9998
10369
  }
9999
10370
  var HOIST_MIN_SUBTREE = 3;
10371
+ var INLINE_MAX_NODES = 1e3;
10372
+ function resolveInlineMax(explicit) {
10373
+ if (explicit !== void 0) return explicit;
10374
+ const env = typeof process !== "undefined" ? process.env?.PARSEMAN_MAX_INLINE : void 0;
10375
+ if (env !== void 0 && env !== "") {
10376
+ if (env === "off" || env === "infinity") return Infinity;
10377
+ const n = Number(env);
10378
+ if (Number.isFinite(n) && n >= 0) return n;
10379
+ }
10380
+ return INLINE_MAX_NODES;
10381
+ }
10000
10382
  function analyzeLazyUsage(root) {
10001
10383
  return analyzeLazyUsageMulti([root]);
10002
10384
  }
@@ -10102,102 +10484,6 @@ function ruleDependencies(ruleMap) {
10102
10484
  }
10103
10485
  return deps;
10104
10486
  }
10105
- function resolveGatingLevel(opt) {
10106
- const explicit = typeof opt === "string" ? opt : opt?.level;
10107
- if (explicit !== void 0) return explicit;
10108
- const env = typeof process !== "undefined" ? process.env?.PARSEMAN_GATING : void 0;
10109
- if (env === "off" || env === "warn" || env === "error") return env;
10110
- return "warn";
10111
- }
10112
- function runGatingDiagnostic(combinator, opt) {
10113
- return reportGating(opt, (o) => analyzeGating(combinator, o));
10114
- }
10115
- function runGatingDiagnosticRules(ruleMap, opt) {
10116
- return reportGating(opt, (o) => analyzeGatingRules(ruleMap, o));
10117
- }
10118
- function runFusedGatingDiagnostic(getRuleMaps, opt, getOpaque) {
10119
- if (resolveGatingLevel(opt) === "off") return void 0;
10120
- const ruleMaps = getRuleMaps();
10121
- const opaque = getOpaque?.() ?? [];
10122
- const opaqueFindings = opaque.map((o) => ({
10123
- rule: o.ruleNames.length > 0 ? o.ruleNames.join(", ") : `<artifact ${o.ns}>`,
10124
- kind: "opaque-artifact",
10125
- reason: `precompiled artifact "${o.ns}" carries compiled rule functions, not re-lowerable IR \u2014 its choices were not examined at this fuse.`
10126
- }));
10127
- if (!ruleMaps.some((map) => hasExternalRuleRef(map))) {
10128
- if (opaqueFindings.length === 0) return void 0;
10129
- return reportGating(opt, () => ({
10130
- totalChoices: 0,
10131
- gated: 0,
10132
- recoverable: 0,
10133
- unanalysable: opaqueFindings,
10134
- ungated: [],
10135
- accepted: [],
10136
- deferred: [],
10137
- acceptedUnused: [],
10138
- choices: [],
10139
- antiPatterns: []
10140
- }));
10141
- }
10142
- const winners = /* @__PURE__ */ new Map();
10143
- for (const map of ruleMaps) for (const [name, rule] of map) {
10144
- if (rule._def.tag === "lazy") {
10145
- try {
10146
- rule._def.thunk();
10147
- } catch {
10148
- continue;
10149
- }
10150
- }
10151
- winners.set(name, rule);
10152
- }
10153
- const entries = [...winners];
10154
- return reportGating(opt, (o) => {
10155
- const deferredHere = new Set(analyzeGatingRules(entries, o).deferred.map((c) => c.id));
10156
- const fused = analyzeGatingRules(entries, { ...o, resolveRef: (name) => winners.get(name) });
10157
- return {
10158
- ...fused,
10159
- ungated: fused.ungated.filter((c) => deferredHere.has(c.id)),
10160
- antiPatterns: [],
10161
- unanalysable: [...fused.unanalysable, ...opaqueFindings]
10162
- };
10163
- });
10164
- }
10165
- function reportGating(opt, analyze) {
10166
- const level = resolveGatingLevel(opt);
10167
- if (level === "off") return void 0;
10168
- const obj = opt !== null && typeof opt === "object" ? opt : void 0;
10169
- const analyzeOpts = obj?.accept !== void 0 || obj?.entryName !== void 0 ? { ...obj.accept !== void 0 ? { accept: obj.accept } : {}, ...obj.entryName !== void 0 ? { entryName: obj.entryName } : {} } : void 0;
10170
- let report;
10171
- try {
10172
- report = analyze(analyzeOpts);
10173
- } catch (e) {
10174
- const reason = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
10175
- const failed = {
10176
- totalChoices: 0,
10177
- gated: 0,
10178
- recoverable: 0,
10179
- unanalysable: [{ rule: "<whole grammar>", kind: "not-a-combinator", reason: `gating analysis threw \u2014 ${reason}` }],
10180
- ungated: [],
10181
- accepted: [],
10182
- deferred: [],
10183
- acceptedUnused: [],
10184
- choices: [],
10185
- antiPatterns: []
10186
- };
10187
- const failLines = formatGatingWarnings(failed);
10188
- if (level === "error") throw new Error(`parseman: gating analysis FAILED (no verdict available)
10189
- ${failLines.join("\n")}`);
10190
- for (const l of failLines) console.warn(l);
10191
- return failed;
10192
- }
10193
- const lines = formatGatingWarnings(report);
10194
- if (lines.length > 0) {
10195
- if (level === "error") throw new Error(`parseman: ${report.ungated.length} ungated hot choice(s) / ${report.antiPatterns.length} anti-pattern(s) / ${report.unanalysable.length} unanalysable rule(s)
10196
- ${lines.join("\n")}`);
10197
- for (const l of lines) console.warn(l);
10198
- }
10199
- return report;
10200
- }
10201
10487
  function resolveDuplicationLevel(opt) {
10202
10488
  const explicit = typeof opt === "string" ? opt : opt?.level;
10203
10489
  if (explicit !== void 0) return explicit;
@@ -10240,7 +10526,17 @@ function runDuplicationDiagnosticRules(ruleMap, opt) {
10240
10526
  return reportDuplication(opt, (o) => analyzeDuplicationRules(ruleMap, o));
10241
10527
  }
10242
10528
  function compile(combinator, mapFnSources, opts) {
10243
- const gatingReport = runGatingDiagnostic(combinator, opts?.gating);
10529
+ const drain = beginCompileDegradationDrain();
10530
+ let completed = false;
10531
+ try {
10532
+ const compiled = compileImpl(combinator, mapFnSources, opts);
10533
+ completed = true;
10534
+ return compiled;
10535
+ } finally {
10536
+ drain(completed);
10537
+ }
10538
+ }
10539
+ function compileImpl(combinator, mapFnSources, opts) {
10244
10540
  runDuplicationDiagnostic(combinator, opts?.duplication);
10245
10541
  markUnusedValues(combinator);
10246
10542
  const grammarTrivia = combinator._meta.grammarTrivia;
@@ -10248,9 +10544,12 @@ function compile(combinator, mapFnSources, opts) {
10248
10544
  const grammarScanSkip = combinator._meta.grammarScanSkip;
10249
10545
  const grammarHostMode = combinator._meta.grammarHostMode;
10250
10546
  const grammarTrackLines = combinator._meta.grammarTrackLines;
10547
+ const _inlineMax = resolveInlineMax(opts?.maxInline);
10251
10548
  const ctx = {
10252
10549
  vars: 0,
10253
10550
  indent: 1,
10551
+ inlineMax: _inlineMax,
10552
+ inlineLeft: _inlineMax,
10254
10553
  regexDecls: [],
10255
10554
  regexMap: /* @__PURE__ */ new Map(),
10256
10555
  expectedDecls: [],
@@ -10296,11 +10595,13 @@ function compile(combinator, mapFnSources, opts) {
10296
10595
  const namedPrelude = ctx.namedFnDecls.length > 0 ? [...namedFnPrelude(), ""] : [];
10297
10596
  const emptyTlDecls = ctx.needsEmptyTl ? ["const _EMPTY_TL = Object.freeze([])"] : [];
10298
10597
  const hostReadsDecls = ctx.needsHostReads ? [HOST_READS_DECL] : [];
10598
+ const rawEntryDecls = ctx.needsRawEntry ? [RAW_ENTRY_DECL] : [];
10299
10599
  const lineTrackDecls = ctx.needsLineTrack ? [LINE_TRACK_DECL] : [];
10300
10600
  const lineSpanDecls = ctx.needsLineSpan ? [LINE_SPAN_DECL] : [];
10301
10601
  const source = [
10302
10602
  ...emptyTlDecls,
10303
10603
  ...hostReadsDecls,
10604
+ ...rawEntryDecls,
10304
10605
  ...lineTrackDecls,
10305
10606
  ...lineSpanDecls,
10306
10607
  ...ctx.regexDecls,
@@ -10315,6 +10616,7 @@ function compile(combinator, mapFnSources, opts) {
10315
10616
  const fn = new Function("input", "_pos", "_rp", "_mf", "_build", "_ctx", [
10316
10617
  ...emptyTlDecls,
10317
10618
  ...hostReadsDecls,
10619
+ ...rawEntryDecls,
10318
10620
  ...lineTrackDecls,
10319
10621
  ...lineSpanDecls,
10320
10622
  ...ctx.regexDecls,
@@ -10364,8 +10666,7 @@ function compile(combinator, mapFnSources, opts) {
10364
10666
  const compiledMeta = {
10365
10667
  source,
10366
10668
  inlineExpression,
10367
- ...ctx.coverage === void 0 ? {} : { coverageDefinitions: ctx.coverage.plan.definitions },
10368
- ...gatingReport === void 0 ? {} : { gating: gatingReport }
10669
+ ...ctx.coverage === void 0 ? {} : { coverageDefinitions: ctx.coverage.plan.definitions }
10369
10670
  };
10370
10671
  if (!ctx.lineTracking) {
10371
10672
  return {
@@ -10431,7 +10732,6 @@ function publicRuleWrapperSource(rule, fnSource, ambientTriviaKindLabels, ambien
10431
10732
  }
10432
10733
  function compileLinkable(ruleMapArg, ns, opts) {
10433
10734
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
10434
- if (opts?.gating !== void 0) runGatingDiagnosticRules(ruleMapArg, opts.gating);
10435
10735
  if (opts?.duplication !== void 0) runDuplicationDiagnosticRules(ruleMapArg, opts.duplication);
10436
10736
  for (const [, rule] of ruleMapArg) markUnusedValues(rule);
10437
10737
  const grammarTrivia = opts?.trivia ?? ruleMapArg.map(([, r]) => r._meta.grammarTrivia).find(Boolean);
@@ -10450,9 +10750,12 @@ function compileLinkable(ruleMapArg, ns, opts) {
10450
10750
  }
10451
10751
  });
10452
10752
  const nodeMeta = new Map(ruleMap.map(([name, rule]) => [name, collectGrammarReflection([[name, rule]], { followLazy: false })]));
10753
+ const _inlineMax = resolveInlineMax(opts?.maxInline);
10453
10754
  const ctx = {
10454
10755
  vars: 0,
10455
10756
  indent: 1,
10757
+ inlineMax: _inlineMax,
10758
+ inlineLeft: _inlineMax,
10456
10759
  regexDecls: [],
10457
10760
  regexMap: /* @__PURE__ */ new Map(),
10458
10761
  expectedDecls: [],
@@ -10548,6 +10851,9 @@ function compileLinkable(ruleMapArg, ns, opts) {
10548
10851
  }
10549
10852
  }
10550
10853
  const savedIndent = ctx.indent, savedFail = ctx.failLabel, savedRec = ctx.recordFail;
10854
+ const savedInlineLeft = ctx.inlineLeft, savedFnName = ctx.currentFnName;
10855
+ ctx.inlineLeft = ctx.inlineMax;
10856
+ ctx.currentFnName = fn;
10551
10857
  ctx.indent = 1;
10552
10858
  ctx.failLabel = "_pfail";
10553
10859
  ctx.recordFail = true;
@@ -10563,6 +10869,8 @@ function compileLinkable(ruleMapArg, ns, opts) {
10563
10869
  ctx.indent = savedIndent;
10564
10870
  ctx.failLabel = savedFail;
10565
10871
  ctx.recordFail = savedRec;
10872
+ ctx.inlineLeft = savedInlineLeft;
10873
+ ctx.currentFnName = savedFnName;
10566
10874
  const ruleId = ctx.coverage?.plan.rules.get(rule);
10567
10875
  const stmts = ruleId === void 0 ? body.stmts : [
10568
10876
  `${ind(ctx)}_ctx._grammarCoverage?.(${JSON.stringify(ruleId)})`,
@@ -10647,6 +10955,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
10647
10955
  deps: ruleDependencies(ruleMap),
10648
10956
  needsEmptyTl: !!ctx.needsEmptyTl,
10649
10957
  needsHostReads: !!ctx.needsHostReads,
10958
+ needsRawEntry: !!ctx.needsRawEntry,
10650
10959
  hostMode: ctx.hostMode ?? "ast",
10651
10960
  hostBranchElided: !!ctx.hostBranchElided,
10652
10961
  hasDirectBuilders: ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
@@ -10656,26 +10965,6 @@ function compileLinkable(ruleMapArg, ns, opts) {
10656
10965
  buildFns: buildSrcs ? [] : ctx.buildFns
10657
10966
  };
10658
10967
  }
10659
- function hasExternalRuleRef(ruleMap) {
10660
- const seen = /* @__PURE__ */ new Set();
10661
- const visit = (p) => {
10662
- if (seen.has(p)) return false;
10663
- seen.add(p);
10664
- const def = p._def;
10665
- if (def.tag === "lazy") {
10666
- let resolved;
10667
- try {
10668
- resolved = def.thunk();
10669
- } catch {
10670
- resolved = void 0;
10671
- }
10672
- if (resolved === void 0) return p._ruleName !== void 0;
10673
- return visit(resolved);
10674
- }
10675
- return childrenOf3(def).some(visit);
10676
- };
10677
- return ruleMap.some(([, rule]) => visit(rule));
10678
- }
10679
10968
  function hasSemanticReduction(roots, externalRefs) {
10680
10969
  const seen = /* @__PURE__ */ new Set();
10681
10970
  const visit = (parser2) => {
@@ -10721,14 +11010,16 @@ function buildInlineExpression(ctx, r, mapFnSources, buildSources, coverageRootR
10721
11010
  const buildDecl = buildSources?.length ? ` const _build = [${buildSources.join(", ")}]` : "";
10722
11011
  const emptyTlDecl = ctx.needsEmptyTl ? ` const _EMPTY_TL = Object.freeze([])` : "";
10723
11012
  const hostReadsDecl = ctx.needsHostReads ? ` ${HOST_READS_DECL}` : "";
11013
+ const rawEntryDecl = ctx.needsRawEntry ? ` ${RAW_ENTRY_DECL}` : "";
10724
11014
  const lineTrackDecl = ctx.needsLineTrack ? ` ${LINE_TRACK_DECL}` : "";
10725
11015
  const lineSpanDecl = ctx.needsLineSpan ? ` ${LINE_SPAN_DECL}` : "";
10726
- const needsWrapper = ctx.regexDecls.length > 0 || ctx.expectedDecls.length > 0 || ctx.namedFnDecls.length > 0 || !!mfDecl || !!buildDecl || !!emptyTlDecl || !!hostReadsDecl || !!lineTrackDecl || !!lineSpanDecl;
11016
+ const needsWrapper = ctx.regexDecls.length > 0 || ctx.expectedDecls.length > 0 || ctx.namedFnDecls.length > 0 || !!mfDecl || !!buildDecl || !!emptyTlDecl || !!hostReadsDecl || !!rawEntryDecl || !!lineTrackDecl || !!lineSpanDecl;
10727
11017
  if (!needsWrapper) return innerFn;
10728
11018
  const namedPrelude = ctx.namedFnDecls.length > 0 ? namedFnPrelude() : [];
10729
11019
  const hoistedDecls = [
10730
11020
  emptyTlDecl,
10731
11021
  hostReadsDecl,
11022
+ rawEntryDecl,
10732
11023
  lineTrackDecl,
10733
11024
  lineSpanDecl,
10734
11025
  ...ctx.regexDecls.map((d) => ` ${d}`),
@@ -10798,11 +11089,12 @@ function childrenOf4(def) {
10798
11089
  return [def.main, def.skipped];
10799
11090
  case "scanTo":
10800
11091
  return [def.sentinel, ...def.skip];
11092
+ case "routed":
11093
+ return def.fallback ? [def.fallback] : [];
10801
11094
  case "lazy":
10802
11095
  case "literal":
10803
11096
  case "regex":
10804
11097
  case "keywords":
10805
- case "routed":
10806
11098
  case "guard":
10807
11099
  case "recover":
10808
11100
  case "unknown":
@@ -10884,7 +11176,7 @@ function evalRuleMapIR(ir) {
10884
11176
  l._def.fnSrc = src;
10885
11177
  return l;
10886
11178
  };
10887
- const _nd = (type, child, src, opts, staticError) => {
11179
+ const _nd = (type, child, src, opts, staticError, sigSrc) => {
10888
11180
  if (staticError !== void 0 && staticError.length > 0) {
10889
11181
  throw new Error(`IR direct node builder for ${type} must be macro-static and self-contained; unsupported binding(s): ${staticError.join(", ")}`);
10890
11182
  }
@@ -10893,6 +11185,7 @@ function evalRuleMapIR(ir) {
10893
11185
  }), opts);
10894
11186
  n._def.buildSrc = src;
10895
11187
  if (staticError !== void 0) n._def.buildStaticError = staticError;
11188
+ if (sigSrc !== void 0) n._def.buildSigSrc = sigSrc;
10896
11189
  return n;
10897
11190
  };
10898
11191
  const _gch = (items) => {
@@ -11205,7 +11498,7 @@ ${outerDecls.map((d) => " " + d).join("\n")}
11205
11498
  case "peek":
11206
11499
  return `peek(${kid(def.parser)})`;
11207
11500
  case "routed":
11208
- return "routed()";
11501
+ return def.fallback === void 0 ? "routed()" : `routed(${kid(def.fallback)})`;
11209
11502
  case "trivia":
11210
11503
  return `trivia(${kid(def.parser)})`;
11211
11504
  case "token":
@@ -11238,6 +11531,9 @@ ${outerDecls.map((d) => " " + d).join("\n")}
11238
11531
  ...projectEntry === void 0 ? [] : [projectEntry],
11239
11532
  ...def.captureTrivia ? ["captureTrivia: true"] : [],
11240
11533
  ...def.trailingTrivia ? ["trailingTrivia: true"] : [],
11534
+ // Declared or resolved arity. Carried so a re-lowered composed artifact does
11535
+ // not silently re-acquire the fail-open capture cost this module resolved away.
11536
+ ...def.buildArity === void 0 ? [] : [`buildArity: ${def.buildArity}`],
11241
11537
  ...tagEntry === void 0 ? [] : [tagEntry]
11242
11538
  ];
11243
11539
  const opts = optEntries.length > 0 ? `, { ${optEntries.join(", ")} }` : "";
@@ -11246,8 +11542,14 @@ ${outerDecls.map((d) => " " + d).join("\n")}
11246
11542
  return opts ? `node(${kid(def.parser)}, undefined${opts})` : `node(${kid(def.parser)})`;
11247
11543
  }
11248
11544
  if (def.buildSrc !== void 0) {
11249
- const staticError = def.buildStaticError === void 0 ? "" : `${opts ? "" : ", undefined"}, ${JSON.stringify(def.buildStaticError)}`;
11250
- return `_nd(${JSON.stringify(def.type)}, ${kid(def.parser)}, ${JSON.stringify(def.buildSrc)}${opts}${staticError})`;
11545
+ const tail = [];
11546
+ if (def.buildStaticError !== void 0 || def.buildSigSrc !== void 0) {
11547
+ if (!opts) tail.push("undefined");
11548
+ tail.push(def.buildStaticError === void 0 ? "undefined" : JSON.stringify(def.buildStaticError));
11549
+ }
11550
+ if (def.buildSigSrc !== void 0) tail.push(JSON.stringify(def.buildSigSrc));
11551
+ const trailing = tail.length > 0 ? `, ${tail.join(", ")}` : "";
11552
+ return `_nd(${JSON.stringify(def.type)}, ${kid(def.parser)}, ${JSON.stringify(def.buildSrc)}${opts}${trailing})`;
11251
11553
  }
11252
11554
  return opts ? `node(${JSON.stringify(def.type)}, ${kid(def.parser)}, undefined${opts})` : `node(${JSON.stringify(def.type)}, ${kid(def.parser)})`;
11253
11555
  }
@@ -11304,7 +11606,7 @@ function cstBuildHost(typeOrOptions, children2, _fields, span, rawChildren, triv
11304
11606
  return host;
11305
11607
  }
11306
11608
  cstBuildHost._parsemanCstOutput = true;
11307
- function fusedBody(pieces) {
11609
+ function fusedBody(pieces, hoist) {
11308
11610
  for (const p of pieces) {
11309
11611
  if (p.v === void 0) {
11310
11612
  throw new Error(
@@ -11328,12 +11630,14 @@ function fusedBody(pieces) {
11328
11630
  const contributingPieces = [...contributing];
11329
11631
  const needsEmptyTl = contributingPieces.some((p) => p.needsEmptyTl);
11330
11632
  const needsHostReads = contributingPieces.some((p) => p.needsHostReads);
11633
+ const needsRawEntry = contributingPieces.some((p) => p.needsRawEntry);
11331
11634
  const lines = [
11332
11635
  // Shared sentinel protocol (must match NAMED_FN_FAIL / NAMED_FN_END in codegen).
11333
11636
  "const _pfFail = {}",
11334
11637
  "let _pfEnd",
11335
11638
  ...needsEmptyTl ? ["const _EMPTY_TL = Object.freeze([])"] : [],
11336
11639
  ...needsHostReads ? [HOST_READS_DECL] : [],
11640
+ ...needsRawEntry ? [RAW_ENTRY_DECL] : [],
11337
11641
  // Each contributing artifact's namespaced private prelude (regexes, _pf, …).
11338
11642
  ...new Set(contributingPieces.flatMap((p) => p.prelude)),
11339
11643
  // The winning `_r_<Name>` function for each rule (one per name → no redeclare).
@@ -11342,8 +11646,7 @@ function fusedBody(pieces) {
11342
11646
  const wrapperEntries = [...winner].map(([k, p]) => `${JSON.stringify(k)}: ${p.wrappers.get(k)}`);
11343
11647
  const reflection = mergeGrammarReflections([...winner].map(([k, p]) => p.nodeMeta.get(k) ?? { nodes: [] }));
11344
11648
  const { mode, elided } = hostModeOfPieces(pieces);
11345
- const rawBody = [
11346
- ...lines,
11649
+ const tailSrc = [
11347
11650
  "const _map = {",
11348
11651
  wrapperEntries.join(",\n"),
11349
11652
  "}",
@@ -11359,6 +11662,7 @@ function fusedBody(pieces) {
11359
11662
  "}",
11360
11663
  "return _map"
11361
11664
  ].join("\n");
11665
+ const rawBody = [...lines, tailSrc].join("\n");
11362
11666
  const ANY2 = { kind: "any" };
11363
11667
  const EMPTY3 = { kind: "empty" };
11364
11668
  const recipes = /* @__PURE__ */ new Map();
@@ -11399,7 +11703,7 @@ function fusedBody(pieces) {
11399
11703
  }
11400
11704
  if (!changed) break;
11401
11705
  }
11402
- const body = rawBody.replace(
11706
+ const resolveFS = (src) => src.replace(
11403
11707
  // Rule name + code-point var are both JS identifiers (rule names are validated
11404
11708
  // at compile time — see assertRuleName in codegen), so an identifier class
11405
11709
  // matches every well-formed placeholder.
@@ -11410,6 +11714,7 @@ function fusedBody(pieces) {
11410
11714
  return `(${firstSetCond(codevar, fs)})`;
11411
11715
  }
11412
11716
  );
11717
+ const body = hoist === void 0 ? resolveFS(rawBody) : [...hoist.claim(lines.map(resolveFS)), resolveFS(tailSrc)].join("\n");
11413
11718
  const env = {};
11414
11719
  for (const p of contributingPieces) {
11415
11720
  if (p.mfFns.length) env[`${p.ns}mf`] = p.mfFns;
@@ -11449,17 +11754,6 @@ var COMPOSED_COVERAGE_RULES = /* @__PURE__ */ Symbol.for("parseman.composedCover
11449
11754
  function isIRPiece(p) {
11450
11755
  return !!p && typeof p === "object" && typeof p.ir === "string" && typeof p.ns === "string" && !("ruleFns" in p);
11451
11756
  }
11452
- function once(fn) {
11453
- let done = false;
11454
- let value;
11455
- return () => {
11456
- if (!done) {
11457
- value = fn();
11458
- done = true;
11459
- }
11460
- return value;
11461
- };
11462
- }
11463
11757
  function carriedRuleMapsDetailed(carried) {
11464
11758
  const maps = [];
11465
11759
  const opaque = [];
@@ -11576,12 +11870,6 @@ function compose(items, opts) {
11576
11870
  const used = /* @__PURE__ */ new Set();
11577
11871
  const trivia2 = composingTriviaOf(items);
11578
11872
  const carried = items.flatMap((item) => itemCarried(item, used, trivia2, opts?.hostMode));
11579
- const detailed = once(() => carriedRuleMapsDetailed(carried));
11580
- runFusedGatingDiagnostic(
11581
- () => detailed().maps,
11582
- void 0,
11583
- () => detailed().opaque
11584
- );
11585
11873
  const pieces = carried.map((p) => materializePiece(p, trivia2, false, opts?.hostMode));
11586
11874
  const map = fuseRules(pieces);
11587
11875
  Object.defineProperty(map, COMPOSED_PIECES, { value: carried, enumerable: false });
@@ -11590,8 +11878,168 @@ function compose(items, opts) {
11590
11878
  if (coverageRules) Object.defineProperty(map, COMPOSED_COVERAGE_RULES, { value: coverageRules, enumerable: false });
11591
11879
  return map;
11592
11880
  }
11593
- function composeLeaf(_items) {
11594
- 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(", ")}`;
11595
12043
  }
11596
12044
 
11597
12045
  // src/analysis/grammar.ts
@@ -11608,14 +12056,1123 @@ function analyzeGrammarGating(grammar, opts) {
11608
12056
  kind: "opaque-artifact",
11609
12057
  reason: `precompiled artifact "${o.ns}" carries compiled rule functions, not re-lowerable IR \u2014 its choices cannot be examined. Recompile the contributing grammar so it carries IR (the macro emits IR by default) to bring it back into analysis.`
11610
12058
  }));
12059
+ for (const o of opaque) {
12060
+ recordDegradation({
12061
+ code: "opaque-artifact",
12062
+ // `info`, not `warn`: the artifact is usually someone else's package, so the author
12063
+ // of THIS grammar has nothing to act on — but the blind spot still has to be counted.
12064
+ severity: "info",
12065
+ where: o.ruleNames.length > 0 ? o.ruleNames.join(", ") : `<artifact ${o.ns}>`,
12066
+ subject: `precompiled artifact "${o.ns}"`,
12067
+ fellBackTo: `it carries compiled rule functions rather than re-lowerable IR, so its ${o.ruleNames.length > 0 ? `${o.ruleNames.length} rule(s)` : "rules"} were NOT analysed and a clean gating report must not be read as a pass`,
12068
+ otherwise: "its choices would be walked like any other rule. Recompile the contributing grammar so it carries IR (the macro emits IR by default)"
12069
+ });
12070
+ }
11611
12071
  return fromOpaque.length === 0 ? report : { ...report, unanalysable: [...report.unanalysable, ...fromOpaque] };
11612
12072
  }
11613
12073
 
11614
- // src/combinators/gate.ts
11615
- function gate(predicate) {
11616
- const meta = {
11617
- firstSet: { kind: "any" },
11618
- canMatchNewline: false,
12074
+ // src/analysis/diagnose.ts
12075
+ var isCombinator = (g) => typeof g === "object" && g !== null && "_def" in g;
12076
+ var isRuleEntries = (g) => Array.isArray(g);
12077
+ function diagnoseGrammar(grammar, opts) {
12078
+ const depth = degradationCaptureDepth();
12079
+ beginDegradationCapture();
12080
+ let report;
12081
+ let degradations;
12082
+ try {
12083
+ report = runAnalysis(grammar, opts);
12084
+ } catch (e) {
12085
+ const reason = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
12086
+ report = emptyReport(`gating analysis threw \u2014 ${reason}`);
12087
+ } finally {
12088
+ degradations = unwindDegradationCapture(depth);
12089
+ }
12090
+ return assemble(report, degradations);
12091
+ }
12092
+ function examinedNothing(d) {
12093
+ return d.summary.totalChoices === 0 && d.summary.unanalysable > 0;
12094
+ }
12095
+ function formatGrammarDiagnosis(d) {
12096
+ const s = d.summary;
12097
+ const lines = [];
12098
+ lines.push(
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).`
12100
+ );
12101
+ if (s.unanalysable > 0 && !examinedNothing(d)) {
12102
+ lines.push(
12103
+ ` ${s.unanalysable} rule(s) UNANALYSABLE \u2014 THIS REPORT IS PARTIAL. An empty finding list below does NOT mean the grammar is clean.`
12104
+ );
12105
+ }
12106
+ for (const f of d.findings) {
12107
+ lines.push(`${f.severity === "blocking" ? "\u2717" : "\xB7"} [${f.code}] ${f.id}: ${f.message}`);
12108
+ for (const detail of f.details) lines.push(` ${detail.replace(/\n/g, "\n ")}`);
12109
+ if (f.acceptKey !== void 0) {
12110
+ lines.push(` intentional? add to the gating snapshot: { accept: ['${f.acceptKey}'] }`);
12111
+ }
12112
+ }
12113
+ if (!d.ok && d.acceptSnapshot.length > 0) {
12114
+ lines.push(` accept-all snapshot: { accept: [${d.acceptSnapshot.map((i) => `'${i}'`).join(", ")}] }`);
12115
+ }
12116
+ return lines;
12117
+ }
12118
+ function runAnalysis(grammar, opts) {
12119
+ if (isCombinator(grammar)) return analyzeGating(grammar, opts);
12120
+ if (isRuleEntries(grammar)) return analyzeGatingRules(grammar, opts);
12121
+ return analyzeGrammarGating(grammar, opts);
12122
+ }
12123
+ function emptyReport(reason) {
12124
+ return {
12125
+ totalChoices: 0,
12126
+ gated: 0,
12127
+ recoverable: 0,
12128
+ unanalysable: [{ rule: "<whole grammar>", kind: "not-a-combinator", reason }],
12129
+ ungated: [],
12130
+ accepted: [],
12131
+ deferred: [],
12132
+ acceptedUnused: [],
12133
+ choices: [],
12134
+ antiPatterns: []
12135
+ };
12136
+ }
12137
+ var blockingOf = (d) => d.findings.filter((f) => f.severity === "blocking");
12138
+ var SEVERITY_RANK = { blocking: 0, advisory: 1 };
12139
+ var CODE_RANK = {
12140
+ unanalysable: 0,
12141
+ "ungated-choice": 1,
12142
+ "anti-pattern": 2,
12143
+ degraded: 3,
12144
+ "stale-accept": 4
12145
+ };
12146
+ function assemble(report, degradations) {
12147
+ const findings = [];
12148
+ for (const u of report.unanalysable) {
12149
+ findings.push({
12150
+ id: u.rule,
12151
+ code: "unanalysable",
12152
+ severity: "blocking",
12153
+ rule: u.rule,
12154
+ message: `could not be examined [${u.kind}] \u2014 no verdict about it is available`,
12155
+ details: [u.reason]
12156
+ });
12157
+ }
12158
+ for (const c of report.ungated) {
12159
+ findings.push({
12160
+ id: c.id,
12161
+ code: "ungated-choice",
12162
+ severity: "blocking",
12163
+ rule: c.rule,
12164
+ message: `choice is UNGATED [${c.strategy}] \u2014 no first-char dispatch; every position speculatively enters doomed arms`,
12165
+ details: [
12166
+ ...c.anyArms.map((a) => `arm[${a.index}] first-set ANY (${a.cause}): ${a.detail}
12167
+ fix: ${a.suggestion}`),
12168
+ ...c.overlaps.map((o) => `arm[${o.a}] \u2229 arm[${o.b}] overlap on ${firstSetToString(o.on)}
12169
+ fix: ${o.suggestion}`)
12170
+ ],
12171
+ acceptKey: c.id
12172
+ });
12173
+ }
12174
+ for (const ap of report.antiPatterns) {
12175
+ findings.push({
12176
+ id: `${ap.rule}#arm${ap.armIndex}`,
12177
+ code: "anti-pattern",
12178
+ severity: "blocking",
12179
+ rule: ap.rule,
12180
+ message: `[${ap.kind}] ${ap.message}`,
12181
+ details: []
12182
+ });
12183
+ }
12184
+ for (const d of degradations) {
12185
+ findings.push({
12186
+ // `where` + `code` is the degradation channel's own identity; reuse it so the two
12187
+ // surfaces name the same thing the same way.
12188
+ id: `${d.code}@${d.where}`,
12189
+ code: "degraded",
12190
+ // Mirrors `DegradationSeverity`: 'warn' is a real cost the author can remove,
12191
+ // 'info' is a fact they cannot act on (someone else's package).
12192
+ severity: d.severity === "warn" ? "blocking" : "advisory",
12193
+ rule: d.where,
12194
+ message: formatDegradation(d),
12195
+ details: []
12196
+ });
12197
+ }
12198
+ for (const id of report.acceptedUnused) {
12199
+ findings.push({
12200
+ id,
12201
+ code: "stale-accept",
12202
+ severity: "advisory",
12203
+ rule: id,
12204
+ message: "accept entry matched no ungated choice \u2014 the grammar was fixed; prune this line",
12205
+ details: []
12206
+ });
12207
+ }
12208
+ findings.sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] || CODE_RANK[a.code] - CODE_RANK[b.code] || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
12209
+ return {
12210
+ schema: "parseman.diagnosis/1",
12211
+ ok: !findings.some((f) => f.severity === "blocking"),
12212
+ summary: {
12213
+ totalChoices: report.totalChoices,
12214
+ gated: report.gated,
12215
+ recoverable: report.recoverable,
12216
+ ungated: report.ungated.length,
12217
+ accepted: report.accepted.length,
12218
+ deferred: report.deferred.length,
12219
+ antiPatterns: report.antiPatterns.length,
12220
+ unanalysable: report.unanalysable.length,
12221
+ degraded: degradations.length,
12222
+ staleAccepts: report.acceptedUnused.length
12223
+ },
12224
+ findings,
12225
+ acceptSnapshot: report.ungated.map((c) => c.id).sort(),
12226
+ gating: report,
12227
+ degradations
12228
+ };
12229
+ }
12230
+
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;
12260
+ }
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);
12267
+ }
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;
12273
+ }
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;
12278
+ }
12279
+ return index;
12280
+ }
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
+ );
12286
+ }
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.");
12299
+ }
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
+ }
12306
+ }
12307
+ if (captureRootTrivia && !rootTriviaClassifiedFromRunnable(entry) && !rootTriviaClassifiedFromRunnable(options.trivia)) {
12308
+ throw new TypeError(
12309
+ "run(): rootTrivia.select requires classifiedTrivia() at the root."
12310
+ );
12311
+ }
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,
11619
13176
  isTrivia: false
11620
13177
  };
11621
13178
  return {
@@ -11630,9 +13187,6 @@ function gate(predicate) {
11630
13187
  };
11631
13188
  }
11632
13189
 
11633
- // src/combinators/guard.ts
11634
- var guard = gate;
11635
-
11636
13190
  // src/combinators/completions.ts
11637
13191
  function completionsAt(target, input, offset, options = {}) {
11638
13192
  const probe = { offset, best: null };
@@ -11859,9 +13413,9 @@ function parseMiddle(input, reStart, reEnd, cStart, ruleFn, separator, build, to
11859
13413
  const ctx = mkCtx(null, build, tolerant);
11860
13414
  const out = [];
11861
13415
  let pos = reStart;
11862
- let guard2 = 0;
13416
+ let guard = 0;
11863
13417
  while (pos < reEnd) {
11864
- if (++guard2 > reEnd - reStart + 2) return null;
13418
+ if (++guard > reEnd - reStart + 2) return null;
11865
13419
  if (separator && out.length > 0 && out[out.length - 1]._tag === "node") {
11866
13420
  if (pos + separator.length > reEnd || !input.startsWith(separator, pos)) return null;
11867
13421
  out.push({ _tag: "leaf", value: separator, span: { start: pos - cStart, end: pos + separator.length - cStart } });
@@ -12153,142 +13707,6 @@ function parseDoc(registry, rootRule, input, opts = {}) {
12153
13707
  return new ParseDocImpl(registry, rootRule, opts, { abs: null }, [fail], null, input);
12154
13708
  }
12155
13709
 
12156
- // src/functional/run.ts
12157
- var invoke = (r, input, pos, ctx) => typeof r === "function" ? r(input, pos, ctx) : r.parse(input, pos, ctx);
12158
- function triviaKindLabelsFromRunnable(r) {
12159
- if (!r) return void 0;
12160
- if (typeof r === "function") return r._meta?.triviaKindLabels;
12161
- const grammarTrivia = r._meta.grammarTrivia;
12162
- return grammarTrivia?._meta.triviaKindLabels ?? r._meta.triviaKindLabels ?? (r._def.tag === "grammar" ? r._def.triviaParser?._meta.triviaKindLabels : void 0);
12163
- }
12164
- function rootTriviaClassifiedFromRunnable(r) {
12165
- if (!r) return false;
12166
- if (typeof r === "function") return r._meta?.rootTriviaClassified === true;
12167
- const grammarTrivia = r._meta.grammarTrivia;
12168
- return grammarTrivia?._meta.rootTriviaClassified === true || r._meta.rootTriviaClassified === true;
12169
- }
12170
- function makeSelectedRootLabelIndex(labels) {
12171
- const index = /* @__PURE__ */ Object.create(null);
12172
- for (let i = 0; i < labels.length; i++) {
12173
- if (index[labels[i]] === void 0) index[labels[i]] = i;
12174
- }
12175
- return index;
12176
- }
12177
- function runOnce(entry, input, options, phase, profileState) {
12178
- if (typeof entry !== "function" && typeof entry?.parse !== "function") {
12179
- throw new TypeError(
12180
- `run(): start production is ${entry === null ? "null" : typeof entry}, not a rule \u2014 the requested grammar rule does not exist (check the rule name).`
12181
- );
12182
- }
12183
- const rootTriviaSelection = options.rootTrivia?.select;
12184
- const captureRootTrivia = rootTriviaSelection !== void 0 && rootTriviaSelection.length > 0;
12185
- const selectedRootLog = captureRootTrivia ? [] : void 0;
12186
- const selectedRootLabelIndex = captureRootTrivia ? makeSelectedRootLabelIndex(rootTriviaSelection) : void 0;
12187
- const errors = [];
12188
- const profile = profileState ?? (phase === void 0 ? void 0 : { phase, nodes: 0, childSlots: 0, rawSlots: 0, triviaSlots: 0, fieldSlots: 0, hostCalls: 0 });
12189
- const skipGlobalSinks = phase === "recognizer" || phase === "capture";
12190
- const grammarTrivia = typeof entry !== "function" ? entry._meta.grammarTrivia : void 0;
12191
- const grammarScanSkip = typeof entry !== "function" ? entry._meta.grammarScanSkip : void 0;
12192
- const triviaKindLabels = triviaKindLabelsFromRunnable(entry) ?? triviaKindLabelsFromRunnable(options.trivia);
12193
- if (captureRootTrivia && triviaKindLabels === void 0) {
12194
- throw new TypeError("run(): rootTrivia.select requires labeled grammar trivia.");
12195
- }
12196
- if (captureRootTrivia) {
12197
- for (const label2 of rootTriviaSelection) {
12198
- if (!triviaKindLabels.includes(label2)) {
12199
- throw new TypeError(`run(): rootTrivia.select contains unknown trivia label ${JSON.stringify(label2)}.`);
12200
- }
12201
- }
12202
- }
12203
- if (captureRootTrivia && !rootTriviaClassifiedFromRunnable(entry) && !rootTriviaClassifiedFromRunnable(options.trivia)) {
12204
- throw new TypeError(
12205
- "run(): rootTrivia.select requires classifiedTrivia() at the root."
12206
- );
12207
- }
12208
- const stamped = entry;
12209
- assertHostModeCompatible(
12210
- typeof entry === "function" ? stamped[FUSED_HOST_MODE] ?? "ast" : entry._meta.grammarHostMode ?? "ast",
12211
- options.build,
12212
- typeof entry === "function" && stamped[FUSED_HOST_ELIDED] === true
12213
- );
12214
- const ctx = {
12215
- trackLines: false,
12216
- ...skipGlobalSinks ? { _pmProfile: profile } : {
12217
- ...captureRootTrivia ? {
12218
- _rootTriviaLog: selectedRootLog,
12219
- _rootTriviaKindIndex: selectedRootLabelIndex,
12220
- _rootTriviaStrictScopes: true
12221
- } : {},
12222
- _errors: errors,
12223
- ...profile === void 0 ? {} : { _pmProfile: profile }
12224
- },
12225
- build: options.build,
12226
- state: options.state,
12227
- ...grammarTrivia !== void 0 ? { trivia: grammarTrivia, ...triviaKindLabels ? { triviaKindLabels } : {} } : triviaKindLabels ? { triviaKindLabels } : {},
12228
- ...grammarScanSkip !== void 0 ? { scanSkip: grammarScanSkip } : {},
12229
- ...options.triviaCaptureMask !== void 0 ? { _triviaCaptureMask: options.triviaCaptureMask } : {},
12230
- ...options.tolerant ? { _tolerant: true, _rec: REC } : {},
12231
- ...options.instrumentation === void 0 ? {} : options.instrumentation
12232
- };
12233
- const r = invoke(entry, input, 0, ctx);
12234
- let unconsumedFrom = null;
12235
- if (r.ok) {
12236
- let pos = r.span?.end ?? 0;
12237
- if (options.trivia && pos < input.length) {
12238
- const t = invoke(options.trivia, input, pos, { trackLines: false });
12239
- if (t.ok && t.span.end > pos) pos = t.span.end;
12240
- }
12241
- unconsumedFrom = pos < input.length ? pos : null;
12242
- }
12243
- return {
12244
- ok: r.ok,
12245
- value: r.ok ? r.value : void 0,
12246
- span: r.span ?? { start: 0, end: 0 },
12247
- expected: r.ok ? [] : r.expected ?? [],
12248
- errors,
12249
- ...selectedRootLog !== void 0 && selectedRootLog.length > 0 ? {
12250
- rootTrivia: {
12251
- rows: selectedRootLog,
12252
- select: rootTriviaSelection,
12253
- index: buildRootTriviaIndex(selectedRootLog, rootTriviaSelection)
12254
- }
12255
- } : {},
12256
- unconsumedFrom
12257
- };
12258
- }
12259
- function clonePassState(state) {
12260
- if (state === null || typeof state !== "object") return state;
12261
- return Array.isArray(state) ? [...state] : { ...state };
12262
- }
12263
- function profilePass(entry, input, options, phase) {
12264
- const state = { phase, nodes: 0, childSlots: 0, rawSlots: 0, triviaSlots: 0, fieldSlots: 0, hostCalls: 0 };
12265
- const passOptions = options.state === void 0 ? options : { ...options, state: clonePassState(options.state) };
12266
- const start = performance.now();
12267
- const result = runOnce(entry, input, passOptions, phase, state);
12268
- const { phase: _phase, ...counts } = state;
12269
- return { result, profile: { ms: performance.now() - start, ...counts } };
12270
- }
12271
- function run(entry, input, options = {}) {
12272
- if (!options.profile) return runOnce(entry, input, options);
12273
- if (typeof entry !== "function") {
12274
- throw new TypeError("run({ profile: true }) requires a compiled parser entry");
12275
- }
12276
- const recognizer = profilePass(entry, input, options, "recognizer");
12277
- const capture = profilePass(entry, input, options, "capture");
12278
- const host = profilePass(entry, input, options, "host");
12279
- if (recognizer.result.ok !== host.result.ok || capture.result.ok !== host.result.ok || recognizer.result.unconsumedFrom !== host.result.unconsumedFrom || capture.result.unconsumedFrom !== host.result.unconsumedFrom) {
12280
- throw new Error("run({ profile: true }) changed recognition; the grammar is not profile-safe");
12281
- }
12282
- return {
12283
- ...host.result,
12284
- profile: {
12285
- recognizer: recognizer.profile,
12286
- structuralCapture: capture.profile,
12287
- hostConstruction: host.profile
12288
- }
12289
- };
12290
- }
12291
-
12292
13710
  // src/coverage.ts
12293
13711
  function dispatchMatcher(entry) {
12294
13712
  switch (entry.kind) {
@@ -12303,7 +13721,7 @@ function dispatchMatcher(entry) {
12303
13721
  var GRAMMAR_COVERAGE_DEFINITIONS = /* @__PURE__ */ Symbol.for("parseman.grammarCoverageDefinitions");
12304
13722
  function compiledGrammarCoverageDefinitions(grammar) {
12305
13723
  const definitions = grammar[GRAMMAR_COVERAGE_DEFINITIONS];
12306
- if (!Array.isArray(definitions) || !definitions.every((definition) => typeof definition === "object" && definition !== null && typeof definition.id === "string" && (definition.kind === "rule" || definition.kind === "choice-arm" || definition.kind === "dispatch-arm" || definition.kind === "label"))) {
13724
+ if (!Array.isArray(definitions) || definitions.length === 0 || !definitions.every((definition) => typeof definition === "object" && definition !== null && typeof definition.id === "string" && (definition.kind === "rule" || definition.kind === "choice-arm" || definition.kind === "dispatch-arm" || definition.kind === "label"))) {
12307
13725
  throw new TypeError("grammar has no coverage definitions; enable grammarCoverage for this macro build");
12308
13726
  }
12309
13727
  return definitions;
@@ -12357,7 +13775,13 @@ function createGrammarCoverageCollector(definitions) {
12357
13775
  snapshot() {
12358
13776
  const hitList = [...hits].sort();
12359
13777
  const unhit = ordered.map((definition) => definition.id).filter((id) => !hits.has(id));
12360
- return { definitions: ordered, hits: hitList, unhit, ratio: ordered.length === 0 ? 1 : hitList.length / ordered.length };
13778
+ return {
13779
+ definitions: ordered,
13780
+ hits: hitList,
13781
+ unhit,
13782
+ ratio: hitList.length / ordered.length,
13783
+ measurable: ordered.length > 0
13784
+ };
12361
13785
  },
12362
13786
  reset() {
12363
13787
  hits.clear();
@@ -12677,13 +14101,13 @@ function buildTriviaIndex(root, input = "", opts) {
12677
14101
  }
12678
14102
 
12679
14103
  // src/cst/walk.ts
12680
- function isCombinator(value) {
14104
+ function isCombinator2(value) {
12681
14105
  return !!value && typeof value === "object" && "_def" in value && "_meta" in value && typeof value.parse === "function";
12682
14106
  }
12683
14107
  function reflectionFor(grammar) {
12684
14108
  const stamped = grammarReflectionOf(grammar);
12685
14109
  if (stamped) return stamped;
12686
- const entries = Object.entries(grammar).filter((entry) => isCombinator(entry[1]));
14110
+ const entries = Object.entries(grammar).filter((entry) => isCombinator2(entry[1]));
12687
14111
  return collectGrammarReflection(entries);
12688
14112
  }
12689
14113
  function createVisitor(grammar, spec) {
@@ -12721,206 +14145,16 @@ function createVisitor(grammar, spec) {
12721
14145
  };
12722
14146
  }
12723
14147
 
12724
- // src/cst/offset-model.ts
12725
- var { isFinite } = Number;
12726
- function floorIndex(arr, len, x) {
12727
- let lo = 0;
12728
- let hi = len - 1;
12729
- let ans = -1;
12730
- while (lo <= hi) {
12731
- const mid = lo + hi >>> 1;
12732
- if (arr[mid] <= x) {
12733
- ans = mid;
12734
- lo = mid + 1;
12735
- } else {
12736
- hi = mid - 1;
12737
- }
12738
- }
12739
- return ans;
12740
- }
12741
- var OffsetIndex = class {
12742
- /** Slot starts, sorted ascending. Parallel to `_ends`. */
12743
- _starts;
12744
- _ends;
12745
- count;
12746
- docStart;
12747
- docEnd;
12748
- constructor(slots, docStart = 0, docEnd) {
12749
- const sorted = [...slots].sort((a, b) => a.start - b.start);
12750
- const starts = [];
12751
- const ends = [];
12752
- let prevEnd = docStart;
12753
- for (const s of sorted) {
12754
- if (!isFinite(s.start) || !isFinite(s.end) || s.end < s.start) {
12755
- throw new RangeError(`OffsetIndex: invalid slot [${s.start}, ${s.end}]`);
12756
- }
12757
- if (s.start < prevEnd) {
12758
- throw new RangeError(`OffsetIndex: overlapping/out-of-order slot [${s.start}, ${s.end}] after end ${prevEnd}`);
12759
- }
12760
- starts.push(s.start);
12761
- ends.push(s.end);
12762
- prevEnd = s.end;
12763
- }
12764
- this._starts = starts;
12765
- this._ends = ends;
12766
- this.count = starts.length;
12767
- this.docStart = docStart;
12768
- this.docEnd = docEnd ?? (this.count ? Math.max(prevEnd, ends[ends.length - 1]) : docStart);
12769
- }
12770
- /** The slot at index `i` (0-based, source order), or undefined. */
12771
- slot(i) {
12772
- if (i < 0 || i >= this.count) return void 0;
12773
- return { start: this._starts[i], end: this._ends[i] };
12774
- }
12775
- /**
12776
- * Index of the gap that CONTAINS `offset`, in `[0, count]`. Gap `i` spans
12777
- * `[endOf(i-1), startOf(i))` where `endOf(-1)=docStart` and `startOf(count)=docEnd`.
12778
- * An offset that lands inside a slot returns that slot's TRAILING gap index
12779
- * (i.e. the gap after it) — callers querying at slot boundaries get the
12780
- * intuitive answer via `gapBefore`/`gapAfter`.
12781
- */
12782
- gapIndexAt(offset) {
12783
- const i = floorIndex(this._starts, this.count, offset);
12784
- if (i < 0) return 0;
12785
- if (offset < this._ends[i]) return i + 1;
12786
- return i + 1;
12787
- }
12788
- /** The gap at index `i` in `[0, count]`. */
12789
- gap(i) {
12790
- if (i < 0 || i > this.count) return void 0;
12791
- const start = i === 0 ? this.docStart : this._ends[i - 1];
12792
- const end = i === this.count ? this.docEnd : this._starts[i];
12793
- return { start, end };
12794
- }
12795
- /**
12796
- * The trivia gap immediately BEFORE `offset` (`[prevSlotEnd, offset)`), where
12797
- * `prevSlotEnd` is the end of the last slot ending at or before `offset` (or
12798
- * `docStart`). For a real slot start this is the gap preceding that slot; for an
12799
- * adjacent slot the gap is empty and agrees with `gapAfter` on the same seam.
12800
- */
12801
- gapBefore(offset) {
12802
- const e = floorIndex(this._ends, this.count, offset);
12803
- const start = e >= 0 ? this._ends[e] : this.docStart;
12804
- return { start, end: offset };
12805
- }
12806
- /**
12807
- * The trivia gap immediately AFTER `offset` (`[offset, nextSlotStart)`), where
12808
- * `nextSlotStart` is the least slot start at or after `offset` (or `docEnd`).
12809
- */
12810
- gapAfter(offset) {
12811
- return { start: offset, end: this._ceilStart(offset) };
12812
- }
12813
- /** Least slot start `>= offset`, else `docEnd`. */
12814
- _ceilStart(offset) {
12815
- let lo = 0;
12816
- let hi = this.count - 1;
12817
- let ans = this.docEnd;
12818
- while (lo <= hi) {
12819
- const mid = lo + hi >>> 1;
12820
- if (this._starts[mid] >= offset) {
12821
- ans = this._starts[mid];
12822
- hi = mid - 1;
12823
- } else {
12824
- lo = mid + 1;
12825
- }
12826
- }
12827
- return ans;
12828
- }
12829
- /** Is this gap empty (adjacent slots, no trivia)? */
12830
- isEmpty(gap) {
12831
- return gap.end <= gap.start;
12832
- }
12833
- };
12834
- var NL = 10;
12835
- var CR = 13;
12836
- var SP = 32;
12837
- var TAB = 9;
12838
- function gapText(source, gap) {
12839
- return gap.end > gap.start ? source.slice(gap.start, gap.end) : "";
12840
- }
12841
- function lineBreaksIn(source, gap) {
12842
- let n = 0;
12843
- for (let i = gap.start; i < gap.end; i++) {
12844
- const c = source.charCodeAt(i);
12845
- if (c === NL) n++;
12846
- else if (c === CR && source.charCodeAt(i + 1) !== NL) n++;
12847
- }
12848
- return n;
12849
- }
12850
- function blankLinesIn(source, gap) {
12851
- return Math.max(0, lineBreaksIn(source, gap) - 1);
12852
- }
12853
- function lineStartWithin(source, gap) {
12854
- for (let i = gap.end - 1; i >= gap.start; i--) {
12855
- const c = source.charCodeAt(i);
12856
- if (c === NL || c === CR) return i + 1;
12857
- }
12858
- return gap.start;
12859
- }
12860
- function indentWidth(source, gap) {
12861
- const ls = lineStartWithin(source, gap);
12862
- if (ls === gap.start) return 0;
12863
- return gap.end - ls;
12864
- }
12865
- function indentMixed(source, gap) {
12866
- const ls = lineStartWithin(source, gap);
12867
- let sawSpace = false;
12868
- let sawTab = false;
12869
- for (let i = ls; i < gap.end; i++) {
12870
- const c = source.charCodeAt(i);
12871
- if (c === SP) sawSpace = true;
12872
- else if (c === TAB) sawTab = true;
12873
- }
12874
- return sawSpace && sawTab;
12875
- }
12876
- function commentsIn(source, gap, comment) {
12877
- const re = new RegExp(comment.source, comment.flags.replace(/[gy]/g, "") + "g");
12878
- re.lastIndex = gap.start;
12879
- const out = [];
12880
- let m;
12881
- while ((m = re.exec(source)) && m.index < gap.end) {
12882
- if (m[0].length === 0) {
12883
- re.lastIndex++;
12884
- continue;
12885
- }
12886
- if (m.index + m[0].length > gap.end) break;
12887
- out.push({ start: m.index, end: m.index + m[0].length });
12888
- }
12889
- return out;
12890
- }
12891
- function gapIsSignificant(source, gap, comment) {
12892
- if (gap.end <= gap.start) return false;
12893
- if (lineBreaksIn(source, gap) > 0) return true;
12894
- if (comment && commentsIn(source, gap, comment).length > 0) return true;
12895
- return false;
12896
- }
12897
- function collectLeafSlots(root) {
12898
- const slots = [];
12899
- const visit = (n) => {
12900
- const kids = n.children;
12901
- if (kids && kids.length) {
12902
- for (const c of kids) visit(c);
12903
- } else if (n.span) {
12904
- slots.push({ start: n.span.start, end: n.span.end });
12905
- }
12906
- };
12907
- if (root) visit(root);
12908
- return slots;
12909
- }
12910
- function buildOffsetIndex(root, input = "") {
12911
- return new OffsetIndex(collectLeafSlots(root), 0, input.length);
12912
- }
12913
-
12914
14148
  // src/index.ts
12915
14149
  registerRegexAnalyzer(firstSetFromRegex);
12916
14150
  export {
12917
14151
  GRAMMAR_COVERAGE_DEFINITIONS,
12918
- OffsetIndex,
12919
14152
  absoluteSpanAt,
12920
14153
  absoluteSpanCST,
12921
14154
  absolutize,
12922
14155
  absolutizeCST,
12923
14156
  alternationGroups,
14157
+ analyzeChoiceInventory,
12924
14158
  analyzeDuplication,
12925
14159
  analyzeDuplicationRules,
12926
14160
  analyzeGating,
@@ -12929,18 +14163,18 @@ export {
12929
14163
  annotateSpan,
12930
14164
  annotateTreeSpans,
12931
14165
  applyEdit,
14166
+ armLabel,
12932
14167
  attempt,
12933
14168
  balanced,
12934
- blankLinesIn,
12935
14169
  buildLineIndex,
12936
- buildOffsetIndex,
12937
14170
  buildRootTriviaIndex,
12938
14171
  buildTriviaIndex,
14172
+ buildWastedWorkBaseline,
12939
14173
  charClassMembers,
14174
+ checkWastedWork,
12940
14175
  choice,
14176
+ choiceSiteKey,
12941
14177
  classifiedTrivia,
12942
- collectLeafSlots,
12943
- commentsIn,
12944
14178
  compile,
12945
14179
  compiledGrammarCoverageDefinitions,
12946
14180
  completionsAt,
@@ -12953,30 +14187,29 @@ export {
12953
14187
  createLineIndex,
12954
14188
  createVisitor,
12955
14189
  cstBuildHost,
14190
+ diagnoseGrammar,
12956
14191
  dispatch,
12957
14192
  duplicationFindingCount,
12958
14193
  endsWith,
14194
+ examinedNothing,
12959
14195
  expect,
12960
14196
  extractCharClasses,
12961
14197
  field,
12962
14198
  firstSetToString,
12963
14199
  formatDuplicationFindings,
12964
14200
  formatGatingWarnings,
12965
- gapIsSignificant,
12966
- gapText,
14201
+ formatGrammarDiagnosis,
14202
+ fuseInterpreted,
12967
14203
  gate,
12968
14204
  grammarCoverageDefinitions,
12969
- guard,
12970
- indentMixed,
12971
- indentWidth,
14205
+ isInterpretedFuse,
12972
14206
  isParseError,
12973
14207
  keywordAlternationHazards,
12974
14208
  keywordRegexShape,
12975
14209
  keywords,
12976
14210
  label,
12977
14211
  leaf,
12978
- lineBreaksIn,
12979
- lineStartWithin,
14212
+ leftFactorPreview,
12980
14213
  literal,
12981
14214
  makeWhen,
12982
14215
  makeWord,
@@ -12995,11 +14228,14 @@ export {
12995
14228
  parseDoc,
12996
14229
  parser,
12997
14230
  peek,
14231
+ profileWastedWork,
12998
14232
  recordLineRange,
12999
14233
  ref,
13000
14234
  regex,
13001
14235
  relativize,
13002
14236
  relativizeCST,
14237
+ renderChoiceInventory,
14238
+ renderWastedWork,
13003
14239
  routed,
13004
14240
  rules,
13005
14241
  run,