parseman 0.36.0 → 0.38.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 (63) hide show
  1. package/README.md +34 -19
  2. package/dist/analysis/duplication.d.ts +421 -0
  3. package/dist/analysis/duplication.d.ts.map +1 -0
  4. package/dist/analysis/gating.d.ts +25 -0
  5. package/dist/analysis/gating.d.ts.map +1 -1
  6. package/dist/analysis/grammar.d.ts +12 -0
  7. package/dist/analysis/grammar.d.ts.map +1 -0
  8. package/dist/combinators/keywords.d.ts +9 -8
  9. package/dist/combinators/keywords.d.ts.map +1 -1
  10. package/dist/combinators/parser.d.ts +26 -0
  11. package/dist/combinators/parser.d.ts.map +1 -1
  12. package/dist/compiler/build-arity.d.ts +12 -0
  13. package/dist/compiler/build-arity.d.ts.map +1 -1
  14. package/dist/compiler/codegen.d.ts +43 -1
  15. package/dist/compiler/codegen.d.ts.map +1 -1
  16. package/dist/compiler/linker.d.ts +64 -7
  17. package/dist/compiler/linker.d.ts.map +1 -1
  18. package/dist/cst/host-mode.d.ts +43 -0
  19. package/dist/cst/host-mode.d.ts.map +1 -0
  20. package/dist/functional/doc.d.ts.map +1 -1
  21. package/dist/functional/run.d.ts.map +1 -1
  22. package/dist/index.cjs +1657 -64
  23. package/dist/index.cjs.map +4 -4
  24. package/dist/index.d.ts +7 -3
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1657 -64
  27. package/dist/index.js.map +4 -4
  28. package/dist/language-service/index.cjs +37 -1
  29. package/dist/language-service/index.cjs.map +3 -3
  30. package/dist/language-service/index.js +37 -1
  31. package/dist/language-service/index.js.map +3 -3
  32. package/dist/oracle/corpus.d.ts +32 -0
  33. package/dist/oracle/corpus.d.ts.map +1 -0
  34. package/dist/oracle/digest.d.ts +21 -0
  35. package/dist/oracle/digest.d.ts.map +1 -0
  36. package/dist/oracle/identity.d.ts +117 -0
  37. package/dist/oracle/identity.d.ts.map +1 -0
  38. package/dist/oracle/index.cjs +423 -0
  39. package/dist/oracle/index.cjs.map +7 -0
  40. package/dist/oracle/index.d.ts +37 -0
  41. package/dist/oracle/index.d.ts.map +1 -0
  42. package/dist/oracle/index.js +400 -0
  43. package/dist/oracle/index.js.map +7 -0
  44. package/dist/plugin/evaluator.d.ts +2 -1
  45. package/dist/plugin/evaluator.d.ts.map +1 -1
  46. package/dist/plugin/index.cjs +1696 -85
  47. package/dist/plugin/index.cjs.map +4 -4
  48. package/dist/plugin/index.d.ts.map +1 -1
  49. package/dist/plugin/index.js +1696 -85
  50. package/dist/plugin/index.js.map +4 -4
  51. package/dist/run/index.cjs +23 -0
  52. package/dist/run/index.cjs.map +3 -3
  53. package/dist/run/index.js +23 -0
  54. package/dist/run/index.js.map +3 -3
  55. package/dist/spec/index.cjs +3001 -60
  56. package/dist/spec/index.cjs.map +4 -4
  57. package/dist/spec/index.js +3001 -60
  58. package/dist/spec/index.js.map +4 -4
  59. package/dist/spec/model.d.ts.map +1 -1
  60. package/dist/types.d.ts +7 -0
  61. package/dist/types.d.ts.map +1 -1
  62. package/dist/version.d.ts +1 -1
  63. package/package.json +10 -3
@@ -2358,6 +2358,16 @@ function tagRule(r, key) {
2358
2358
  r._ruleName = key;
2359
2359
  if (r._def.tag === "node" && r._def.type === void 0) r._def.type = key;
2360
2360
  }
2361
+ function isDefinableRef(v2) {
2362
+ return !!v2 && typeof v2 === "object" && "_def" in v2 && v2._def.tag === "lazy" && typeof v2.define === "function";
2363
+ }
2364
+ function ruleNameOf(r) {
2365
+ return r._ruleName;
2366
+ }
2367
+ function isNamedRuleRefForAnotherRule(r, key) {
2368
+ const name = r._def.tag === "lazy" ? ruleNameOf(r) : void 0;
2369
+ return name !== void 0 && name !== key;
2370
+ }
2361
2371
  function rules(a, b) {
2362
2372
  const factory = typeof a === "function" ? a : b;
2363
2373
  const options = typeof a === "function" ? b : a;
@@ -2378,11 +2388,21 @@ function rules(a, b) {
2378
2388
  for (const key of Object.keys(definitions)) {
2379
2389
  const placeholder = cache[key];
2380
2390
  const parser2 = definitions[key];
2381
- if (placeholder !== void 0 && typeof placeholder.define === "function") {
2382
- tagRule(parser2, key);
2391
+ if (placeholder === parser2) {
2392
+ throw new Error(`rules(): rule "${key}" cannot be a direct alias to itself`);
2393
+ }
2394
+ if (isDefinableRef(placeholder)) {
2395
+ if (!isNamedRuleRefForAnotherRule(parser2, key)) tagRule(parser2, key);
2383
2396
  placeholder.define(parser2);
2384
2397
  placeholder._meta.firstSet = parser2._meta.firstSet;
2385
2398
  placeholder._meta.canMatchNewline = parser2._meta.canMatchNewline;
2399
+ } else if (isNamedRuleRefForAnotherRule(parser2, key)) {
2400
+ const alias = ref();
2401
+ tagRule(alias, key);
2402
+ alias.define(parser2);
2403
+ alias._meta.firstSet = parser2._meta.firstSet;
2404
+ alias._meta.canMatchNewline = parser2._meta.canMatchNewline;
2405
+ cache[key] = alias;
2386
2406
  } else {
2387
2407
  tagRule(parser2, key);
2388
2408
  cache[key] = parser2;
@@ -2400,6 +2420,12 @@ function rules(a, b) {
2400
2420
  if (rule && !rule._meta.isTrivia) rule._meta.grammarScanSkip = options.scanSkip;
2401
2421
  }
2402
2422
  }
2423
+ if (options?.hostMode === "cst") {
2424
+ for (const key of Object.keys(definitions)) {
2425
+ const rule = cache[key];
2426
+ if (rule && !rule._meta.isTrivia) rule._meta.grammarHostMode = "cst";
2427
+ }
2428
+ }
2403
2429
  for (const key of Object.keys(definitions)) {
2404
2430
  markUnusedValues(cache[key]);
2405
2431
  }
@@ -2458,6 +2484,9 @@ function peek(combinator) {
2458
2484
  }
2459
2485
 
2460
2486
  // src/compiler/build-arity.ts
2487
+ function cstOutputHost(build) {
2488
+ return build?._parsemanCstOutput === true;
2489
+ }
2461
2490
  var PARAM_LIST_RE = (
2462
2491
  // (params) => ... | function name?(params) ... | single-ident arrow `x => ...`
2463
2492
  /^(?:function\b[^(]*\(([^)]*)\)|\(([^)]*)\)\s*=>|([A-Za-z_$][\w$]*)\s*=>)/
@@ -2707,7 +2736,8 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2707
2736
  const def = unwrap || collapse || captureTrivia || trailingTrivia ? { ...baseDef, ...unwrap ? { unwrap: true } : {}, ...collapse ? { collapse: true } : {}, ...captureTrivia ? { captureTrivia: true } : {}, ...trailingTrivia ? { trailingTrivia: true } : {} } : baseDef;
2708
2737
  const capturesTrivia = captureTrivia || trailingTrivia || (build ? buildReadsTrivia(def) : true);
2709
2738
  const clonesState = build ? buildReadsState(def) : true;
2710
- const capturesFields = parserHasOwnFields(combinator) && (build ? buildReadsFields(def) : true);
2739
+ const hasOwnFields = parserHasOwnFields(combinator);
2740
+ const capturesFields = hasOwnFields && (build ? buildReadsFields(def) : true);
2711
2741
  const guardable = combinator._meta.firstSet.kind !== "any" && !matchesEmpty(combinator);
2712
2742
  let failExpected;
2713
2743
  return {
@@ -2722,28 +2752,32 @@ function node(typeOrCombinator, combinatorOrBuild, buildOrOpts, maybeOpts) {
2722
2752
  }
2723
2753
  return { ok: false, expected: failExpected, span: { start: pos, end: pos } };
2724
2754
  }
2755
+ const hostCst = build !== void 0 && cstOutputHost(ctx.build);
2756
+ const effTrivia = capturesTrivia || hostCst;
2757
+ const effFields = capturesFields || hostCst && hasOwnFields;
2725
2758
  const saved = beginCstNodeCapture(ctx);
2726
2759
  const savedFields = ctx._fields;
2727
- ctx._fields = capturesFields ? [] : void 0;
2760
+ ctx._fields = effFields ? [] : void 0;
2728
2761
  const savedMask = ctx._triviaCaptureMask;
2729
2762
  if (build === void 0 && ctx.build?._parsemanTriviaKinds !== void 0 && def.type !== void 0) {
2730
2763
  ctx._triviaCaptureMask = ctx.build._parsemanTriviaKinds(def.type);
2731
2764
  }
2732
- if (!capturesTrivia) ctx.captureTrivia = false;
2765
+ if (!effTrivia) ctx.captureTrivia = false;
2733
2766
  let r = combinator.parse(input, pos, ctx);
2734
2767
  if (r.ok && trailingTrivia && ctx.trivia) {
2735
2768
  const end = consumeTrivia(input, r.span.end, ctx);
2736
2769
  r = { ...r, span: { start: r.span.start, end } };
2737
2770
  }
2738
- const fields = capturesFields ? buildFieldMap(ctx._fields) : void 0;
2771
+ const fields = effFields ? buildFieldMap(ctx._fields) : void 0;
2739
2772
  ctx._fields = savedFields;
2740
2773
  ctx._triviaCaptureMask = savedMask;
2741
2774
  const { children: children2, rawChildren, triviaLog } = endCstNodeCapture(ctx, saved);
2742
2775
  if (!r.ok) return r;
2743
2776
  const st = clonesState && ctx.state !== void 0 ? Object.assign({}, ctx.state) : void 0;
2744
2777
  const nodeType = def.type ?? missingInferredType();
2745
- const cstOutput = ctx.build?._parsemanCstOutput === true;
2746
- const built = unwrap && children2.length === 1 ? unwrapChild(children2[0]) : collapse && children2.length === 1 ? children2[0] : !build && ctx.build?._parsemanCstCollapse && children2.length === 1 && rawChildren.length === 1 && ctx.build._parsemanCstCollapse(nodeType, children2[0], children2, rawChildren) ? children2[0] : build ? cstOutput && ctx.build ? ctx.build(nodeType, children2, fields, r.span, rawChildren, triviaLog, st) : build(children2, fields, r.span, rawChildren, triviaLog, st) : ctx.build ? ctx.build(nodeType, children2, fields, r.span, rawChildren, triviaLog, st) : { _tag: "node", type: nodeType, span: { start: r.span.start, end: r.span.end }, state: st ?? null, children: children2 };
2778
+ const cstOutput = cstOutputHost(ctx.build);
2779
+ const hostState = clonesState ? st : ctx.state !== void 0 ? Object.assign({}, ctx.state) : void 0;
2780
+ const built = unwrap && children2.length === 1 ? unwrapChild(children2[0]) : collapse && children2.length === 1 ? children2[0] : !build && ctx.build?._parsemanCstCollapse && children2.length === 1 && rawChildren.length === 1 && ctx.build._parsemanCstCollapse(nodeType, children2[0], children2, rawChildren) ? children2[0] : build ? cstOutput && ctx.build ? ctx.build(nodeType, children2, fields, r.span, rawChildren, triviaLog, hostState) : build(children2, fields, r.span, rawChildren, triviaLog, st) : ctx.build ? ctx.build(nodeType, children2, fields, r.span, rawChildren, triviaLog, st) : { _tag: "node", type: nodeType, span: { start: r.span.start, end: r.span.end }, state: st ?? null, children: children2 };
2747
2781
  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: r.span };
2748
2782
  if (saved.buf !== void 0 || saved.ch !== void 0) {
2749
2783
  pushCstChild(ctx, built, rawEntry);
@@ -2994,7 +3028,24 @@ function leaf(root, fn) {
2994
3028
  }
2995
3029
 
2996
3030
  // src/version.ts
2997
- var PARSEMAN_VERSION = "0.36.0";
3031
+ var PARSEMAN_VERSION = "0.38.0";
3032
+
3033
+ // src/cst/host-mode.ts
3034
+ var FUSED_HOST_MODE = /* @__PURE__ */ Symbol.for("parseman.fusedHostMode");
3035
+ var FUSED_HOST_ELIDED = /* @__PURE__ */ Symbol.for("parseman.fusedHostElided");
3036
+ function assertHostModeCompatible(mode, build, hostBranchElided = true) {
3037
+ const isCstHost = build?._parsemanCstOutput === true;
3038
+ if (mode === "ast" && isCstHost && hostBranchElided) {
3039
+ throw new Error(
3040
+ "parseman: this parser was compiled for host mode \"ast\" (the default), so its nodes build through their own `build` callbacks and no positioned-CST branch was emitted. It cannot be driven with a positioned-CST host (cstBuildHost / a language-service host). Compile a second artifact from the SAME grammar with `compile(grammar, { hostMode: 'cst' })` \u2014 or, under the macro, a second `rules({ hostMode: 'cst' }, factory)` call site \u2014 and drive that one instead."
3041
+ );
3042
+ }
3043
+ if (mode === "cst" && !isCstHost) {
3044
+ throw new Error(
3045
+ 'parseman: this parser was compiled for host mode "cst", so every node builds through a positioned-CST `ctx.build` host. Pass one (e.g. `{ build: cstBuildHost }`), or use an artifact compiled with the default `hostMode: "ast"` to get the grammar\'s own AST.'
3046
+ );
3047
+ }
3048
+ }
2998
3049
 
2999
3050
  // src/analysis/gating.ts
3000
3051
  var isAny = (fs2) => fs2.kind === "any";
@@ -3031,7 +3082,7 @@ function firstSetToString(fs2) {
3031
3082
  if (fs2.kind === "empty") return "(empty)";
3032
3083
  return fs2.ranges.map((r) => r.lo === r.hi ? ch(r.lo) : `${ch(r.lo)}-${ch(r.hi)}`).join(",");
3033
3084
  }
3034
- var ruleNameOf = (p) => p._ruleName;
3085
+ var ruleNameOf2 = (p) => p._ruleName;
3035
3086
  var SUGGESTIONS = {
3036
3087
  "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.",
3037
3088
  "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.",
@@ -3189,17 +3240,40 @@ function detectAntiPatterns(rule, arms) {
3189
3240
  return out;
3190
3241
  }
3191
3242
  function analyzeGating(entry, opts) {
3192
- return analyzeGatingRules([[ruleNameOf(entry) ?? opts?.entryName ?? "<entry>", entry]], opts);
3243
+ return analyzeGatingRules([[ruleNameOf2(entry) ?? opts?.entryName ?? "<entry>", entry]], opts);
3193
3244
  }
3194
3245
  function analyzeGatingRules(ruleMap, opts) {
3195
3246
  const raw = [];
3196
3247
  const antiPatterns = [];
3248
+ const unanalysable = [];
3249
+ const seenUnanalysable = /* @__PURE__ */ new Set();
3197
3250
  const visited = /* @__PURE__ */ new Set();
3251
+ const noteUnanalysable = (u) => {
3252
+ const key = `${u.rule}\0${u.kind}`;
3253
+ if (seenUnanalysable.has(key)) return;
3254
+ seenUnanalysable.add(key);
3255
+ unanalysable.push(u);
3256
+ };
3198
3257
  const visit = (p, enclosingRule) => {
3199
3258
  if (visited.has(p)) return;
3200
3259
  visited.add(p);
3201
- const d = p._def;
3202
- const rule = ruleNameOf(p) ?? enclosingRule;
3260
+ const def = p?._def;
3261
+ if (def === null || typeof def !== "object") {
3262
+ noteUnanalysable(
3263
+ typeof p === "function" ? {
3264
+ rule: enclosingRule,
3265
+ kind: "fused-rule",
3266
+ reason: "fused rule function \u2014 a compose()/fuse result has no combinator graph. Analyze the composed grammar itself (analyzeGrammarGating) so the carried IR is re-lowered first."
3267
+ } : {
3268
+ rule: enclosingRule,
3269
+ kind: "not-a-combinator",
3270
+ reason: `value of type ${p === null ? "null" : typeof p} is not a combinator and carries no _def descriptor.`
3271
+ }
3272
+ );
3273
+ return;
3274
+ }
3275
+ const d = def;
3276
+ const rule = ruleNameOf2(p) ?? enclosingRule;
3203
3277
  if (d.tag === "choice") {
3204
3278
  raw.push({ g: analyzeChoice(p, d, rule, opts?.resolveRef), rule });
3205
3279
  antiPatterns.push(...detectAntiPatterns(rule, d.parsers));
@@ -3218,7 +3292,7 @@ function analyzeGatingRules(ruleMap, opts) {
3218
3292
  }
3219
3293
  for (const k of kids) visit(k, rule);
3220
3294
  };
3221
- for (const [name, root] of ruleMap) visit(root, ruleNameOf(root) ?? name);
3295
+ for (const [name, root] of ruleMap) visit(root, ruleNameOf2(root) ?? name);
3222
3296
  const perRule = /* @__PURE__ */ new Map();
3223
3297
  for (const r of raw) perRule.set(r.rule, (perRule.get(r.rule) ?? 0) + 1);
3224
3298
  const seenInRule = /* @__PURE__ */ new Map();
@@ -3238,7 +3312,18 @@ function analyzeGatingRules(ruleMap, opts) {
3238
3312
  const accepted = choices.filter((c) => c.gates === "no" && c.accepted);
3239
3313
  const deferred = choices.filter((c) => c.gates === "no" && !c.accepted && c.deferred);
3240
3314
  const acceptedUnused = [...accept].filter((id) => !usedAccept.has(id));
3241
- return { totalChoices: choices.length, gated, recoverable, ungated, accepted, deferred, acceptedUnused, choices, antiPatterns };
3315
+ return {
3316
+ totalChoices: choices.length,
3317
+ gated,
3318
+ recoverable,
3319
+ unanalysable,
3320
+ ungated,
3321
+ accepted,
3322
+ deferred,
3323
+ acceptedUnused,
3324
+ choices,
3325
+ antiPatterns
3326
+ };
3242
3327
  }
3243
3328
  function analyzeChoice(p, d, rule, resolve2) {
3244
3329
  const arms = d.parsers;
@@ -3275,6 +3360,13 @@ function analyzeChoice(p, d, rule, resolve2) {
3275
3360
  }
3276
3361
  function formatGatingWarnings(report) {
3277
3362
  const lines = [];
3363
+ if (report.unanalysable.length > 0) {
3364
+ lines.push(
3365
+ `parseman gating: ${report.unanalysable.length} rule(s) UNANALYSABLE \u2014 this report is PARTIAL (${report.totalChoices} choice(s) examined). An empty finding list below does NOT mean the grammar is clean.`
3366
+ );
3367
+ for (const u of report.unanalysable)
3368
+ lines.push(` \xB7 ${u.rule} [${u.kind}]: ${u.reason}`);
3369
+ }
3278
3370
  for (const c of report.ungated) {
3279
3371
  lines.push(`parseman gating: choice @ ${c.id} is UNGATED [${c.strategy}] \u2014 no first-char dispatch; every position speculatively enters doomed arms.`);
3280
3372
  for (const a of c.anyArms)
@@ -3290,6 +3382,1284 @@ function formatGatingWarnings(report) {
3290
3382
  return lines;
3291
3383
  }
3292
3384
 
3385
+ // src/analysis/duplication.ts
3386
+ var siteToString = (s) => s.path === "" ? s.rule : `${s.rule} \u203A ${s.path}`;
3387
+ function childrenOf(d) {
3388
+ switch (d.tag) {
3389
+ case "sequence":
3390
+ case "choice":
3391
+ return d.parsers;
3392
+ case "skip":
3393
+ return [d.main, d.skipped];
3394
+ case "sepBy":
3395
+ return [d.parser, d.separator];
3396
+ case "recover":
3397
+ return [d.parser, d.sentinel];
3398
+ case "scanTo":
3399
+ return [d.sentinel, ...d.skip];
3400
+ case "grammar":
3401
+ return d.triviaParser ? [d.parser, d.triviaParser] : [d.parser];
3402
+ // A `lazy` is a REFERENCE, not a subtree: hashing through it would make every
3403
+ // rule's shape include the whole reachable grammar. Treated as a leaf, keyed
3404
+ // by the rule name it refers to — which is also the right semantics, since two
3405
+ // productions referencing `g.Ident` really do fill that slot the same way.
3406
+ case "lazy":
3407
+ case "literal":
3408
+ case "regex":
3409
+ case "keywords":
3410
+ case "guard":
3411
+ case "unknown":
3412
+ return [];
3413
+ default: {
3414
+ const rec = d;
3415
+ return rec.parser ? [rec.parser] : [];
3416
+ }
3417
+ }
3418
+ }
3419
+ var ruleNameOf3 = (p) => p._ruleName;
3420
+ var fnKey = (src, fn) => src ?? (typeof fn === "function" ? String(fn) : "");
3421
+ function payloadKey(p, d) {
3422
+ switch (d.tag) {
3423
+ case "literal":
3424
+ return `literal\0${d.value}\0${d.caseInsensitive}`;
3425
+ case "regex":
3426
+ return `regex\0${d.source}\0${d.flags}`;
3427
+ case "keywords":
3428
+ return `keywords\0${[...d.words].join("")}\0${d.caseInsensitive}\0${d.boundary ?? ""}`;
3429
+ case "lazy": {
3430
+ const n = ruleNameOf3(p);
3431
+ return n === void 0 ? `ref\0#${anonId(p)}` : `ref\0${n}`;
3432
+ }
3433
+ case "label":
3434
+ return `label\0${d.label}`;
3435
+ case "field":
3436
+ return `field\0${d.name}`;
3437
+ case "node":
3438
+ return `node\0${d.type ?? ""}\0${d.unwrap === true}\0${d.collapse === true}\0${fnKey(d.buildSrc, d.build)}`;
3439
+ case "transform":
3440
+ return `transform\0${fnKey(d.fnSrc, d.fn)}\0${d.recognitionOnly === true}`;
3441
+ case "leaf":
3442
+ return `leaf\0${fnKey(d.fnSrc, d.fn)}`;
3443
+ case "many":
3444
+ case "oneOrMore":
3445
+ return `${d.tag}\0${d.min}\0${d.max ?? ""}`;
3446
+ case "sepBy":
3447
+ return `sepBy\0${d.min}\0${d.max ?? ""}\0${d.trailing ?? ""}`;
3448
+ case "expect":
3449
+ return `expect\0${d.label ?? ""}\0${d.expected.join("")}`;
3450
+ case "scanTo":
3451
+ return `scanTo\0${d.raw}\0${d.orEOF}`;
3452
+ case "guard":
3453
+ return `guard\0${fnKey(d.predSrc, d.predicate)}`;
3454
+ case "withCtx":
3455
+ return `withCtx\0${d.extraSrc ?? safeJson(d.extra)}`;
3456
+ case "grammar":
3457
+ return `grammar\0${d.clearTrivia === true}\0${d.captureTrivia === true}\0${d.trackLines}`;
3458
+ default:
3459
+ return d.tag;
3460
+ }
3461
+ }
3462
+ function safeJson(v2) {
3463
+ try {
3464
+ return JSON.stringify(v2) ?? String(v2);
3465
+ } catch {
3466
+ return "?";
3467
+ }
3468
+ }
3469
+ var anonCounter = 0;
3470
+ var anonIds = /* @__PURE__ */ new WeakMap();
3471
+ function anonId(p) {
3472
+ let id = anonIds.get(p);
3473
+ if (id === void 0) {
3474
+ id = ++anonCounter;
3475
+ anonIds.set(p, id);
3476
+ }
3477
+ return id;
3478
+ }
3479
+ var Interner = class {
3480
+ ids = /* @__PURE__ */ new Map();
3481
+ hole;
3482
+ constructor() {
3483
+ this.hole = this.of("\0HOLE\0");
3484
+ }
3485
+ of(key) {
3486
+ let id = this.ids.get(key);
3487
+ if (id === void 0) {
3488
+ id = this.ids.size;
3489
+ this.ids.set(key, id);
3490
+ }
3491
+ return id;
3492
+ }
3493
+ };
3494
+ var MAX_RENDER = 160;
3495
+ function render(p, depth = 3) {
3496
+ const d = p._def;
3497
+ const name = ruleNameOf3(p);
3498
+ switch (d.tag) {
3499
+ case "literal":
3500
+ return `literal('${d.value}')`;
3501
+ case "regex":
3502
+ return `regex(/${d.source}/${d.flags})`;
3503
+ case "keywords":
3504
+ return `keywords([${[...d.words].slice(0, 4).map((w) => `'${w}'`).join(", ")}${d.words.length > 4 ? ", \u2026" : ""}])`;
3505
+ case "lazy":
3506
+ return `g.${name ?? "?"}`;
3507
+ case "guard":
3508
+ return "gate(\u2026)";
3509
+ default:
3510
+ break;
3511
+ }
3512
+ if (depth <= 0) return `${d.tag}(\u2026)`;
3513
+ const kids = childrenOf(d).map((k) => render(k, depth - 1));
3514
+ switch (d.tag) {
3515
+ case "sequence":
3516
+ return `sequence(${kids.join(", ")})`;
3517
+ case "choice":
3518
+ return `choice(${kids.join(", ")})`;
3519
+ case "node":
3520
+ return `node(${d.type ? `'${d.type}', ` : ""}${kids[0] ?? ""})`;
3521
+ case "label":
3522
+ return `label('${d.label}', ${kids[0] ?? ""})`;
3523
+ case "field":
3524
+ return `field('${d.name}', ${kids[0] ?? ""})`;
3525
+ case "many":
3526
+ return `many(${kids[0] ?? ""}${d.min === 0 && d.max === void 0 ? "" : `, { min: ${d.min}${d.max === void 0 ? "" : `, max: ${d.max}`} }`})`;
3527
+ case "oneOrMore":
3528
+ return `oneOrMore(${kids[0] ?? ""}${d.min === 1 && d.max === void 0 ? "" : `, { min: ${d.min}${d.max === void 0 ? "" : `, max: ${d.max}`} }`})`;
3529
+ case "sepBy":
3530
+ return `sepBy(${kids[0] ?? ""}, ${kids[1] ?? ""})`;
3531
+ default:
3532
+ return `${d.tag}(${kids.join(", ")})`;
3533
+ }
3534
+ }
3535
+ var clamp = (s) => s.length <= MAX_RENDER ? s : `${s.slice(0, MAX_RENDER - 1)}\u2026`;
3536
+ function segment(d, index) {
3537
+ switch (d.tag) {
3538
+ case "sequence":
3539
+ return `seq[${index}]`;
3540
+ case "choice":
3541
+ return `choice[${index}]`;
3542
+ case "node":
3543
+ return `node(${d.type ?? ""})`;
3544
+ case "label":
3545
+ return `label(${d.label})`;
3546
+ case "field":
3547
+ return `field(${d.name})`;
3548
+ case "sepBy":
3549
+ return index === 0 ? "sepBy.item" : "sepBy.sep";
3550
+ case "skip":
3551
+ return index === 0 ? "skip.main" : "skip.skipped";
3552
+ case "recover":
3553
+ return index === 0 ? "recover.body" : "recover.sentinel";
3554
+ case "scanTo":
3555
+ return index === 0 ? "scanTo.sentinel" : `scanTo.skip[${index - 1}]`;
3556
+ default:
3557
+ return d.tag;
3558
+ }
3559
+ }
3560
+ function alternationGroups(src) {
3561
+ const out = [];
3562
+ const stack = [{ branches: [], last: 0 }];
3563
+ let i = 0;
3564
+ while (i < src.length) {
3565
+ const c = src[i];
3566
+ if (c === "\\") {
3567
+ i += 2;
3568
+ continue;
3569
+ }
3570
+ if (c === "[") {
3571
+ let j = i + 1;
3572
+ if (src[j] === "^") j++;
3573
+ if (src[j] === "]") j++;
3574
+ while (j < src.length && src[j] !== "]") {
3575
+ if (src[j] === "\\") j++;
3576
+ j++;
3577
+ }
3578
+ i = j + 1;
3579
+ continue;
3580
+ }
3581
+ if (c === "(") {
3582
+ let open = i + 1;
3583
+ if (src[open] === "?") {
3584
+ const rest = src.slice(open);
3585
+ const m = /^\?(?::|=|!|<=|<!|<[^>]*>)/.exec(rest);
3586
+ open += m ? m[0].length : 1;
3587
+ }
3588
+ stack.push({ branches: [], last: open });
3589
+ i = open;
3590
+ continue;
3591
+ }
3592
+ if (c === ")") {
3593
+ const top = stack.pop();
3594
+ if (top !== void 0) {
3595
+ top.branches.push(src.slice(top.last, i));
3596
+ if (top.branches.length >= 2) out.push(top.branches);
3597
+ }
3598
+ i++;
3599
+ continue;
3600
+ }
3601
+ if (c === "|") {
3602
+ const top = stack[stack.length - 1];
3603
+ top.branches.push(src.slice(top.last, i));
3604
+ top.last = i + 1;
3605
+ i++;
3606
+ continue;
3607
+ }
3608
+ i++;
3609
+ }
3610
+ const root = stack[0];
3611
+ root.branches.push(src.slice(root.last));
3612
+ if (root.branches.length >= 2) out.push(root.branches);
3613
+ return out;
3614
+ }
3615
+ var WORD_RE_SRC = "-?[A-Za-z][A-Za-z0-9_-]*";
3616
+ var WORDS_RE = new RegExp(`^${WORD_RE_SRC}(?:\\|${WORD_RE_SRC})*$`);
3617
+ var VOCABULARY_MIN_WORDS = 3;
3618
+ function keywordRegexShape(source) {
3619
+ let s = source;
3620
+ if (s.startsWith("^")) s = s.slice(1);
3621
+ s = s.replace(/^\(\?<!\[[^\]]+\]\)/, "");
3622
+ let boundary = null;
3623
+ const tail = /(?:\(\?!\[([^\]]+)\]\)|\\b)$/.exec(s);
3624
+ if (tail !== null) {
3625
+ const cls = tail[1];
3626
+ if (cls !== void 0 && cls.startsWith("^")) return null;
3627
+ boundary = cls ?? "_0-9A-Za-z";
3628
+ s = s.slice(0, tail.index);
3629
+ }
3630
+ const grouped = /^\(\?:([^()]*)\)$/.exec(s);
3631
+ if (grouped !== null) s = grouped[1];
3632
+ if (s.endsWith("$")) s = s.slice(0, -1);
3633
+ if (!WORDS_RE.test(s)) return null;
3634
+ const words = s.split("|");
3635
+ if (boundary === null && words.length < VOCABULARY_MIN_WORDS) return null;
3636
+ return { words, boundary };
3637
+ }
3638
+ function boundaryMatches(boundary, ch2) {
3639
+ const cp = ch2.codePointAt(0);
3640
+ for (const m of charClassMembers(boundary)) {
3641
+ if (m === "\\w") {
3642
+ if (/[A-Za-z0-9_]/.test(ch2)) return true;
3643
+ continue;
3644
+ }
3645
+ if (m === "\\d") {
3646
+ if (/[0-9]/.test(ch2)) return true;
3647
+ continue;
3648
+ }
3649
+ if (m === "\\s") {
3650
+ if (/\s/.test(ch2)) return true;
3651
+ continue;
3652
+ }
3653
+ const range = /^(.|\\u[0-9a-f]{4})-(.|\\u[0-9a-f]{4})$/.exec(m);
3654
+ if (range !== null) {
3655
+ const dec = (t) => t.startsWith("\\u") ? parseInt(t.slice(2), 16) : t.codePointAt(0);
3656
+ if (cp >= dec(range[1]) && cp <= dec(range[2])) return true;
3657
+ continue;
3658
+ }
3659
+ if (m === ch2) return true;
3660
+ }
3661
+ return false;
3662
+ }
3663
+ function keywordAlternationHazards(words, boundary) {
3664
+ const out = [];
3665
+ for (let i = 0; i < words.length; i++) {
3666
+ for (let j = i + 1; j < words.length; j++) {
3667
+ const a = words[i], b = words[j];
3668
+ if (a.length >= b.length || !b.startsWith(a)) continue;
3669
+ const at = b[a.length];
3670
+ out.push({ shorter: a, longer: b, at, rescuedByBoundary: boundary !== null && boundaryMatches(boundary, at) });
3671
+ }
3672
+ }
3673
+ return out;
3674
+ }
3675
+ function analyzeDuplication(entry, opts) {
3676
+ return analyzeDuplicationRules([[ruleNameOf3(entry) ?? opts?.entryName ?? "<entry>", entry]], opts);
3677
+ }
3678
+ function assertAnalyzable(ruleMap) {
3679
+ for (const [name, rule] of ruleMap) {
3680
+ const def = rule?._def;
3681
+ if (def !== null && typeof def === "object" && typeof def.tag === "string") continue;
3682
+ throw new TypeError(
3683
+ `analyzeDuplicationRules: rule '${name}' is not a combinator (no _def). This analysis walks the COMBINATOR TREE; the value returned by compose()/composeLeaf() is a fused, already-compiled artifact whose entries are parse functions. Pass the rules() map itself (or the map given to compileRuleMap/compileLinkable) instead \u2014 for a composed grammar, analyze each piece's own rules() map.`
3684
+ );
3685
+ }
3686
+ }
3687
+ function analyzeDuplicationRules(ruleMap, opts) {
3688
+ assertAnalyzable(ruleMap);
3689
+ const minSize = opts?.minSize ?? 3;
3690
+ const maxFindings = opts?.maxFindings ?? 25;
3691
+ const interner = new Interner();
3692
+ const shapeCache = /* @__PURE__ */ new Map();
3693
+ const sizeCache = /* @__PURE__ */ new Map();
3694
+ const shapeOf = (p) => {
3695
+ const hit = shapeCache.get(p);
3696
+ if (hit !== void 0) return hit;
3697
+ const d = p._def;
3698
+ shapeCache.set(p, interner.hole);
3699
+ const kids = childrenOf(d);
3700
+ const id = interner.of(`${payloadKey(p, d)}|${kids.map(shapeOf).join(",")}`);
3701
+ shapeCache.set(p, id);
3702
+ return id;
3703
+ };
3704
+ const sizeOf = (p) => {
3705
+ const hit = sizeCache.get(p);
3706
+ if (hit !== void 0) return hit;
3707
+ sizeCache.set(p, 1);
3708
+ let n = 1;
3709
+ for (const k of childrenOf(p._def)) n += sizeOf(k);
3710
+ sizeCache.set(p, n);
3711
+ return n;
3712
+ };
3713
+ const visited = /* @__PURE__ */ new Map();
3714
+ const visit = (p, rule, path2, enclosingNode, enclosingReducer) => {
3715
+ if (visited.has(p)) return;
3716
+ const d = p._def;
3717
+ const own = ruleNameOf3(p);
3718
+ const here = own !== void 0 && own !== rule ? { rule: own, path: "" } : { rule, path: path2 };
3719
+ visited.set(p, { p, d, site: here, shape: shapeOf(p), size: sizeOf(p), enclosingNode, enclosingReducer });
3720
+ const inner = d.tag === "node" ? d.type ?? "(anonymous)" : enclosingNode;
3721
+ const innerReducer = enclosingReducer || d.tag === "node" && (d.build !== void 0 || d.buildSrc !== void 0) || d.tag === "transform" || d.tag === "leaf";
3722
+ const kids = childrenOf(d);
3723
+ kids.forEach((k, i) => {
3724
+ const seg = segment(d, i);
3725
+ visit(k, here.rule, here.path === "" ? seg : `${here.path} \u203A ${seg}`, inner, innerReducer);
3726
+ });
3727
+ if (d.tag === "lazy") {
3728
+ try {
3729
+ const target = d.thunk();
3730
+ visit(target, ruleNameOf3(target) ?? ruleNameOf3(p) ?? here.rule, "", null, false);
3731
+ } catch {
3732
+ }
3733
+ }
3734
+ };
3735
+ for (const [name, root] of ruleMap) visit(root, ruleNameOf3(root) ?? name, "", null, false);
3736
+ const accept = new Set(opts?.accept ?? []);
3737
+ const used = /* @__PURE__ */ new Set();
3738
+ const keep = (f) => {
3739
+ if (!accept.has(f.id)) return true;
3740
+ used.add(f.id);
3741
+ return false;
3742
+ };
3743
+ const duplicates = findExactDuplicates(visited, minSize);
3744
+ const nearDuplicates = findNearDuplicates(visited, interner, shapeOf, minSize);
3745
+ const regexFragments = findRegexFragments(visited);
3746
+ const regexClasses = findRegexClasses(visited);
3747
+ const overlaps = findOverlaps(visited, shapeOf, opts?.resolveRef);
3748
+ const rewrites = findRewrites(visited, shapeOf);
3749
+ const divergentNodes = findDivergentNodes(visited, shapeOf);
3750
+ const structureLoss = findStructureLoss(visited, opts?.resolveRef);
3751
+ const keywordRegexes = findKeywordRegexes(visited);
3752
+ const subsume = (fs2) => {
3753
+ const covered = /* @__PURE__ */ new Set();
3754
+ const surviving = /* @__PURE__ */ new Set();
3755
+ for (const f of [...fs2].sort((a, b) => b.savings - a.savings)) {
3756
+ if (f._members.every((m) => covered.has(m))) continue;
3757
+ surviving.add(f.id);
3758
+ for (const m of f._members) markSubtree(m, covered);
3759
+ }
3760
+ return surviving;
3761
+ };
3762
+ const survivingExact = subsume(duplicates);
3763
+ const survivingNear = subsume(nearDuplicates);
3764
+ const report = {
3765
+ duplicates: duplicates.filter((f) => survivingExact.has(f.id)).filter(keep).slice(0, maxFindings).map(stripMembers),
3766
+ nearDuplicates: nearDuplicates.filter((f) => survivingNear.has(f.id)).filter(keep).slice(0, maxFindings).map(stripMembers),
3767
+ regexFragments: regexFragments.filter(keep).slice(0, maxFindings),
3768
+ regexClasses: regexClasses.filter(keep).slice(0, maxFindings),
3769
+ overlaps: overlaps.filter(keep).slice(0, maxFindings),
3770
+ rewrites: rewrites.filter(keep).slice(0, maxFindings),
3771
+ divergentNodes: divergentNodes.filter(keep).slice(0, maxFindings),
3772
+ structureLoss: structureLoss.filter(keep).slice(0, maxFindings),
3773
+ keywordRegexes: keywordRegexes.filter(keep).slice(0, maxFindings),
3774
+ acceptedUnused: [...accept].filter((id) => !used.has(id)),
3775
+ stats: { rules: ruleMap.length, nodes: visited.size, shapes: new Set([...visited.values()].map((v2) => v2.shape)).size }
3776
+ };
3777
+ return report;
3778
+ }
3779
+ function markSubtree(p, into) {
3780
+ if (into.has(p)) return;
3781
+ into.add(p);
3782
+ for (const k of childrenOf(p._def)) markSubtree(k, into);
3783
+ }
3784
+ var stripMembers = (f) => {
3785
+ const { _members, ...rest } = f;
3786
+ void _members;
3787
+ return rest;
3788
+ };
3789
+ function findExactDuplicates(visited, minSize) {
3790
+ const byShape = /* @__PURE__ */ new Map();
3791
+ for (const v2 of visited.values()) {
3792
+ if (v2.size < minSize) continue;
3793
+ const list = byShape.get(v2.shape);
3794
+ if (list === void 0) byShape.set(v2.shape, [v2]);
3795
+ else list.push(v2);
3796
+ }
3797
+ const out = [];
3798
+ for (const group of byShape.values()) {
3799
+ if (group.length < 2) continue;
3800
+ const size = group[0].size;
3801
+ out.push({
3802
+ kind: "exact-duplicate",
3803
+ id: `exact-duplicate:${siteToString(group[0].site)}`,
3804
+ size,
3805
+ count: group.length,
3806
+ savings: (group.length - 1) * size,
3807
+ shape: clamp(render(group[0].p)),
3808
+ sites: group.map((g) => g.site),
3809
+ suggestion: `${group.length} DISTINCT instances build this identical ${size}-node shape. Bind it to one const and reference that \u2014 hoisting removes ${(group.length - 1) * size} nodes and makes a future edit land in one place instead of ${group.length}.`,
3810
+ _members: group.map((g) => g.p)
3811
+ });
3812
+ }
3813
+ return out.sort((a, b) => b.savings - a.savings);
3814
+ }
3815
+ function findNearDuplicates(visited, interner, shapeOf, minSize) {
3816
+ const holeCache = /* @__PURE__ */ new Map();
3817
+ const holesOf = (p) => {
3818
+ const hit = holeCache.get(p);
3819
+ if (hit !== void 0) return hit;
3820
+ holeCache.set(p, []);
3821
+ const d = p._def;
3822
+ const kids = childrenOf(d);
3823
+ const kidShapes = kids.map(shapeOf);
3824
+ const payload = payloadKey(p, d);
3825
+ const out2 = [];
3826
+ kids.forEach((k, i) => {
3827
+ for (const inner of [interner.hole, ...holesOf(k)]) {
3828
+ const arr = kidShapes.slice();
3829
+ arr[i] = inner;
3830
+ out2.push(interner.of(`${payload}|${arr.join(",")}`));
3831
+ }
3832
+ });
3833
+ holeCache.set(p, out2);
3834
+ return out2;
3835
+ };
3836
+ const byHole = /* @__PURE__ */ new Map();
3837
+ for (const v2 of visited.values()) {
3838
+ if (v2.size < minSize) continue;
3839
+ for (const h of holesOf(v2.p)) {
3840
+ const list = byHole.get(h);
3841
+ if (list === void 0) byHole.set(h, [v2]);
3842
+ else list.push(v2);
3843
+ }
3844
+ }
3845
+ const out = [];
3846
+ const seenGroups = /* @__PURE__ */ new Set();
3847
+ for (const group of byHole.values()) {
3848
+ if (group.length < 2) continue;
3849
+ const shapes = new Set(group.map((g) => g.shape));
3850
+ if (shapes.size < 2) continue;
3851
+ const perShape = /* @__PURE__ */ new Map();
3852
+ for (const g of group) if (!perShape.has(g.shape)) perShape.set(g.shape, g);
3853
+ const members = [...perShape.values()];
3854
+ const key = members.map((m) => m.shape).sort((a, b) => a - b).join(",");
3855
+ if (seenGroups.has(key)) continue;
3856
+ seenGroups.add(key);
3857
+ const slot = findDivergentSlot(members.map((m) => m.p), shapeOf);
3858
+ if (slot === null) continue;
3859
+ const sharedSize = Math.min(...members.map((m) => m.size)) - slot.slotSize;
3860
+ if (sharedSize < minSize) continue;
3861
+ out.push({
3862
+ kind: "near-duplicate",
3863
+ id: `near-duplicate:${siteToString(members[0].site)}`,
3864
+ sharedSize,
3865
+ count: members.length,
3866
+ savings: (members.length - 1) * sharedSize,
3867
+ shape: clamp(renderWithHole(members[0].p, slot.path)),
3868
+ slotPath: slot.path.map((i, k) => segment(slot.defs[k], i)).join(" \u203A "),
3869
+ variants: slot.variants.map((v2) => clamp(render(v2))),
3870
+ sites: members.map((m) => m.site),
3871
+ suggestion: `${members.length} productions share this ${sharedSize}-node scaffolding and differ ONLY at \`${slot.path.map((i, k) => segment(slot.defs[k], i)).join(" \u203A ")}\`. That is one production whose varying slot is a \`choice(${slot.variants.map((v2) => clamp(render(v2, 1))).join(", ")})\` \u2014 not ${members.length} copies of the scaffolding. Re-check the AST: a single production emits one node type where the clones may emit several.`,
3872
+ _members: members.map((m) => m.p)
3873
+ });
3874
+ }
3875
+ return out.sort((a, b) => b.savings - a.savings);
3876
+ }
3877
+ function findDivergentSlot(members, shapeOf) {
3878
+ const path2 = [];
3879
+ const defs = [];
3880
+ let cur = [...members];
3881
+ for (let guard2 = 0; guard2 < 64; guard2++) {
3882
+ const d0 = cur[0]._def;
3883
+ const kidLists = cur.map((c) => childrenOf(c._def));
3884
+ const arity = kidLists[0].length;
3885
+ if (kidLists.some((k) => k.length !== arity) || arity === 0) break;
3886
+ const diff = [];
3887
+ for (let i2 = 0; i2 < arity; i2++) {
3888
+ const s = shapeOf(kidLists[0][i2]);
3889
+ if (kidLists.some((k) => shapeOf(k[i2]) !== s)) diff.push(i2);
3890
+ }
3891
+ if (diff.length !== 1) break;
3892
+ const i = diff[0];
3893
+ path2.push(i);
3894
+ defs.push(d0);
3895
+ cur = kidLists.map((k) => k[i]);
3896
+ const nextArity = childrenOf(cur[0]._def).length;
3897
+ const samePayload = cur.every((c) => payloadKey(c, c._def) === payloadKey(cur[0], cur[0]._def));
3898
+ if (!samePayload || nextArity === 0) break;
3899
+ }
3900
+ if (path2.length === 0) return null;
3901
+ let slotSize = 0;
3902
+ const sizeOf = (p) => {
3903
+ let n = 1;
3904
+ for (const k of childrenOf(p._def)) n += sizeOf(k);
3905
+ return n;
3906
+ };
3907
+ for (const c of cur) slotSize = Math.max(slotSize, sizeOf(c));
3908
+ return { path: path2, defs, variants: cur, slotSize };
3909
+ }
3910
+ function renderWithHole(p, path2, depth = 0) {
3911
+ if (depth === path2.length) return "\u2039slot\u203A";
3912
+ const d = p._def;
3913
+ const kids = childrenOf(d);
3914
+ const idx = path2[depth];
3915
+ const parts = kids.map((k, i) => i === idx ? renderWithHole(k, path2, depth + 1) : render(k, 2));
3916
+ switch (d.tag) {
3917
+ case "sequence":
3918
+ return `sequence(${parts.join(", ")})`;
3919
+ case "choice":
3920
+ return `choice(${parts.join(", ")})`;
3921
+ case "node":
3922
+ return `node(${d.type ? `'${d.type}', ` : ""}${parts[0] ?? ""})`;
3923
+ default:
3924
+ return `${d.tag}(${parts.join(", ")})`;
3925
+ }
3926
+ }
3927
+ var MAX_RUN = 32;
3928
+ function findRegexFragments(visited) {
3929
+ const byRun = /* @__PURE__ */ new Map();
3930
+ for (const v2 of visited.values()) {
3931
+ const d = v2.d;
3932
+ if (d.tag !== "regex") continue;
3933
+ const runs = /* @__PURE__ */ new Set();
3934
+ for (const branches of alternationGroups(d.source)) {
3935
+ if (branches.length >= 2) runs.add(branches.join("|"));
3936
+ for (let i = 0; i < branches.length; i++) {
3937
+ for (let n = 2; n <= Math.min(MAX_RUN, branches.length - i); n++) {
3938
+ const run2 = branches.slice(i, i + n).join("|");
3939
+ if (run2.length < 4) continue;
3940
+ runs.add(run2);
3941
+ }
3942
+ }
3943
+ }
3944
+ for (const run2 of runs) {
3945
+ let e = byRun.get(run2);
3946
+ if (e === void 0) {
3947
+ e = { occ: [], nodes: /* @__PURE__ */ new Set(), branches: run2.split("|").length };
3948
+ byRun.set(run2, e);
3949
+ }
3950
+ if (e.nodes.has(v2.p)) continue;
3951
+ e.nodes.add(v2.p);
3952
+ e.occ.push({ site: v2.site, source: d.source });
3953
+ }
3954
+ }
3955
+ const candidates = [...byRun.entries()].filter(([, e]) => e.occ.length >= 2).map(([run2, e]) => ({ run: run2, e, savings: (e.occ.length - 1) * run2.length })).sort((a, b) => b.e.occ.length - a.e.occ.length || b.savings - a.savings || b.run.length - a.run.length);
3956
+ const kept = [];
3957
+ for (const c of candidates) {
3958
+ if (kept.some((k) => k.e.occ.length >= c.e.occ.length && `|${k.run}|`.includes(`|${c.run}|`))) continue;
3959
+ kept.push(c);
3960
+ }
3961
+ return kept.map(({ run: run2, e }) => ({
3962
+ kind: "regex-fragment",
3963
+ id: `regex-fragment:${run2}`,
3964
+ fragment: run2,
3965
+ branches: e.branches,
3966
+ count: e.occ.length,
3967
+ savings: (e.occ.length - 1) * run2.length,
3968
+ sources: e.occ.map((o) => `/${o.source}/`),
3969
+ sites: e.occ.map((o) => o.site),
3970
+ suggestion: `this ${e.branches}-branch alternation is re-spelled in ${e.occ.length} DISTINCT regex() terminals. Hoist it to one shared source string and interpolate it (\`new RegExp(\\\`\u2026\\\${OP}\u2026\\\`)\`) or lift the shared part into its own terminal \u2014 an edit to the operator set currently has to land in ${e.occ.length} places, and the analysis cannot tell you when one of them drifts.`
3971
+ }));
3972
+ }
3973
+ function extractCharClasses(src) {
3974
+ const out = [];
3975
+ let i = 0;
3976
+ while (i < src.length) {
3977
+ const c = src[i];
3978
+ if (c === "\\") {
3979
+ i += 2;
3980
+ continue;
3981
+ }
3982
+ if (c !== "[") {
3983
+ i++;
3984
+ continue;
3985
+ }
3986
+ let j = i + 1;
3987
+ if (src[j] === "^") j++;
3988
+ if (src[j] === "]") j++;
3989
+ while (j < src.length && src[j] !== "]") {
3990
+ if (src[j] === "\\") j++;
3991
+ j++;
3992
+ }
3993
+ out.push(src.slice(i + 1, j));
3994
+ i = j + 1;
3995
+ }
3996
+ return out;
3997
+ }
3998
+ function charClassMembers(body) {
3999
+ const esc = (ch2) => {
4000
+ const cp = ch2.codePointAt(0);
4001
+ return cp >= 128 ? `\\u${cp.toString(16).padStart(4, "0")}` : ch2;
4002
+ };
4003
+ const atoms = [];
4004
+ let i = 0;
4005
+ while (i < body.length) {
4006
+ if (body[i] === "\\") {
4007
+ const m = /^\\(?:u\{([0-9a-fA-F]+)\}|u([0-9a-fA-F]{4})|x([0-9a-fA-F]{2}))/.exec(body.slice(i));
4008
+ if (m !== null) {
4009
+ const cp = parseInt(m[1] ?? m[2] ?? m[3], 16);
4010
+ atoms.push(cp >= 128 ? `\\u${cp.toString(16).padStart(4, "0")}` : String.fromCodePoint(cp));
4011
+ i += m[0].length;
4012
+ continue;
4013
+ }
4014
+ atoms.push(body.slice(i, i + 2));
4015
+ i += 2;
4016
+ continue;
4017
+ }
4018
+ atoms.push(esc(body[i]));
4019
+ i++;
4020
+ }
4021
+ const members = [];
4022
+ for (let k = 0; k < atoms.length; k++) {
4023
+ if (k + 2 < atoms.length && atoms[k + 1] === "-" && atoms[k + 2] !== void 0) {
4024
+ members.push(`${atoms[k]}-${atoms[k + 2]}`);
4025
+ k += 2;
4026
+ continue;
4027
+ }
4028
+ members.push(atoms[k]);
4029
+ }
4030
+ return members;
4031
+ }
4032
+ var CLASS_MIN_MEMBERS = 2;
4033
+ function findRegexClasses(visited) {
4034
+ const spellings = /* @__PURE__ */ new Map();
4035
+ for (const v2 of visited.values()) {
4036
+ if (v2.d.tag !== "regex") continue;
4037
+ for (const body of new Set(extractCharClasses(v2.d.source))) {
4038
+ const members = charClassMembers(body);
4039
+ if (members.length < CLASS_MIN_MEMBERS) continue;
4040
+ const key = [...new Set(members)].sort().join("");
4041
+ let s = spellings.get(key);
4042
+ if (s === void 0) {
4043
+ s = { source: body, members, key, nodes: /* @__PURE__ */ new Set(), sites: [] };
4044
+ spellings.set(key, s);
4045
+ }
4046
+ if (s.nodes.has(v2.p)) continue;
4047
+ s.nodes.add(v2.p);
4048
+ s.sites.push(v2.site);
4049
+ }
4050
+ }
4051
+ const MAX_DELTA = 3;
4052
+ const MIN_JACCARD = 0.6;
4053
+ const all = [...spellings.values()];
4054
+ const parent = all.map((_, i) => i);
4055
+ const find = (i) => {
4056
+ while (parent[i] !== i) {
4057
+ parent[i] = parent[parent[i]];
4058
+ i = parent[i];
4059
+ }
4060
+ return i;
4061
+ };
4062
+ const union2 = (a, b) => {
4063
+ const ra = find(a), rb = find(b);
4064
+ if (ra !== rb) parent[ra] = rb;
4065
+ };
4066
+ for (let i = 0; i < all.length; i++) for (let j = i + 1; j < all.length; j++) {
4067
+ if (all[i].members.includes("^") !== all[j].members.includes("^")) continue;
4068
+ const A = new Set(all[i].members), B = new Set(all[j].members);
4069
+ let shared = 0;
4070
+ for (const m of A) if (B.has(m)) shared++;
4071
+ const delta = A.size - shared + (B.size - shared);
4072
+ const jaccard = shared / (shared + delta);
4073
+ if (shared >= 2 && delta > 0 && delta <= MAX_DELTA && jaccard >= MIN_JACCARD) union2(i, j);
4074
+ }
4075
+ const clusters = /* @__PURE__ */ new Map();
4076
+ all.forEach((_, i) => {
4077
+ const r = find(i);
4078
+ const list = clusters.get(r);
4079
+ if (list === void 0) clusters.set(r, [i]);
4080
+ else list.push(i);
4081
+ });
4082
+ const out = [];
4083
+ for (const idxs of clusters.values()) {
4084
+ const members = idxs.map((i) => all[i]).sort((a, b) => b.nodes.size - a.nodes.size);
4085
+ const total = members.reduce((n, m) => n + m.nodes.size, 0);
4086
+ const drifted = members.length > 1;
4087
+ if (!drifted && total < 2) continue;
4088
+ const base = new Set(members[0].members);
4089
+ const variants = members.map((m) => {
4090
+ const mine = new Set(m.members);
4091
+ const plus = [...mine].filter((x) => !base.has(x));
4092
+ const minus = [...base].filter((x) => !mine.has(x));
4093
+ return {
4094
+ source: m.source,
4095
+ delta: m === members[0] ? "(reference)" : `${plus.map((p) => `+\`${p}\``).join(" ")}${plus.length > 0 && minus.length > 0 ? " " : ""}${minus.map((p) => `-\`${p}\``).join(" ")}` || "(same members, different spelling)",
4096
+ count: m.nodes.size,
4097
+ sites: m.sites
4098
+ };
4099
+ });
4100
+ const bmpCeiling = members.some((m) => m.members.some((x) => x.endsWith("-\\uffff")));
4101
+ out.push({
4102
+ kind: "regex-class",
4103
+ id: `regex-class:${members[0].key}`,
4104
+ canonical: members[0].source,
4105
+ variants,
4106
+ count: total,
4107
+ drifted,
4108
+ bmpCeiling,
4109
+ suggestion: [
4110
+ drifted ? `${members.length} NEARLY-identical spellings of one class, across ${total} regex() terminals. They differ by ${variants.slice(1).map((v2) => v2.delta).join("; ")} \u2014 which is drift, not intent, unless every difference is deliberate. Two classes this close cannot be told apart by reading, so one of them is probably wrong and the tests cannot say which.` : `this class is re-spelled in ${total} DISTINCT regex() terminals. Extract it once \u2014 a named sub-leaf, or an interpolated source constant \u2014 so a range fix lands in one place. Re-spelling is how the near-identical variants elsewhere in this report came to exist.`,
4111
+ `Prefer the shared recognition surface where one already covers this class rather than a local extraction.`,
4112
+ bmpCeiling ? `The class tops out at U+FFFF, so astral-plane characters (emoji, many CJK extensions) fall OUTSIDE it. Widening the ceiling is exactly the kind of edit that has to reach every spelling.` : "",
4113
+ `Single-regex hygiene (duplicate class members, obscure ranges, a missing \`u\` flag) is \`eslint-plugin-regexp\`'s job and is not duplicated here \u2014 it sees one regex at a time, which is why it cannot see this.`
4114
+ ].filter((s) => s !== "").join(" ")
4115
+ });
4116
+ }
4117
+ return out.sort((a, b) => Number(b.drifted) - Number(a.drifted) || b.count - a.count);
4118
+ }
4119
+ function intersects3(a, b) {
4120
+ if (a.kind === "any" || b.kind === "any") return true;
4121
+ if (a.kind === "empty" || b.kind === "empty") return false;
4122
+ for (const ra of a.ranges) for (const rb of b.ranges) if (ra.lo <= rb.hi && rb.lo <= ra.hi) return true;
4123
+ return false;
4124
+ }
4125
+ function intersection2(a, b) {
4126
+ if (a.kind === "any") return b;
4127
+ if (b.kind === "any") return a;
4128
+ if (a.kind === "empty" || b.kind === "empty") return { kind: "empty" };
4129
+ const ranges = [];
4130
+ for (const ra of a.ranges) for (const rb of b.ranges) {
4131
+ const lo = Math.max(ra.lo, rb.lo);
4132
+ const hi = Math.min(ra.hi, rb.hi);
4133
+ if (lo <= hi) ranges.push({ lo, hi });
4134
+ }
4135
+ return ranges.length === 0 ? { kind: "empty" } : { kind: "ranges", ranges };
4136
+ }
4137
+ function termsOf(arm) {
4138
+ let cur = arm;
4139
+ for (; ; ) {
4140
+ const d2 = cur._def;
4141
+ if (d2.tag === "node" || d2.tag === "transform" || d2.tag === "label" || d2.tag === "attempt" || d2.tag === "leaf") {
4142
+ cur = d2.parser;
4143
+ continue;
4144
+ }
4145
+ break;
4146
+ }
4147
+ const d = cur._def;
4148
+ return d.tag === "sequence" ? d.parsers : [cur];
4149
+ }
4150
+ function leadingRegex(arm) {
4151
+ const t = termsOf(arm)[0];
4152
+ return t !== void 0 && t._def.tag === "regex";
4153
+ }
4154
+ function findOverlaps(visited, shapeOf, resolve2) {
4155
+ const out = [];
4156
+ for (const v2 of visited.values()) {
4157
+ const d = v2.d;
4158
+ if (d.tag !== "choice") continue;
4159
+ const arms = d.parsers;
4160
+ const fs2 = arms.map((a) => firstSetOf(a, /* @__PURE__ */ new Set(), resolve2));
4161
+ const strategy = d.strategy;
4162
+ for (let i = 0; i < arms.length; i++) {
4163
+ for (let j = i + 1; j < arms.length; j++) {
4164
+ const A = fs2[i], B = fs2[j];
4165
+ if (A.kind === "any" || B.kind === "any") continue;
4166
+ if (!intersects3(A, B)) continue;
4167
+ const ta = termsOf(arms[i]), tb = termsOf(arms[j]);
4168
+ let shared = 0;
4169
+ while (shared < ta.length && shared < tb.length && shapeOf(ta[shared]) === shapeOf(tb[shared])) shared++;
4170
+ const handled = strategy?.tag === "sharedPrefix" && Array.isArray(strategy.members) && strategy.members.includes(i) && strategy.members.includes(j);
4171
+ const regexPair = leadingRegex(arms[i]) && leadingRegex(arms[j]);
4172
+ out.push({
4173
+ kind: "arm-overlap",
4174
+ id: `arm-overlap:${siteToString(v2.site)}:${i}-${j}`,
4175
+ site: v2.site,
4176
+ a: i,
4177
+ b: j,
4178
+ on: intersection2(A, B),
4179
+ sharedLeadingTerms: shared,
4180
+ sharedPrefix: shared > 0 ? clamp(ta.slice(0, shared).map((t) => render(t, 2)).join(", ")) : null,
4181
+ handledByStrategy: handled,
4182
+ regexPair,
4183
+ suggestion: overlapSuggestion(shared, handled, regexPair)
4184
+ });
4185
+ }
4186
+ }
4187
+ }
4188
+ return out.sort((a, b) => b.sharedLeadingTerms - a.sharedLeadingTerms);
4189
+ }
4190
+ function overlapSuggestion(shared, handled, regexPair) {
4191
+ if (shared > 0 && handled)
4192
+ return `the arms spell ${shared} identical leading term(s). parseman's \`sharedPrefix\` strategy already recognizes that prefix ONCE at runtime, so this costs nothing at parse time \u2014 it is a READABILITY finding: left-factoring into \`sequence(prefix, choice(\u2026))\` says in the grammar what the compiler had to infer.`;
4193
+ if (shared > 0)
4194
+ return `the arms spell ${shared} identical leading term(s) but do NOT qualify for the \`sharedPrefix\` strategy (it needs every arm to be a bare \`sequence\` starting with the same concrete terminal). Left-factor into \`sequence(prefix, choice(\u2026))\` \u2014 that both removes the re-spelling and lets the inner choice dispatch on its own first char.`;
4195
+ if (regexPair)
4196
+ return `two regex() arms whose character classes intersect. Nothing dispatches between them: the first is tried, and on failure the second re-scans the same position. Two regexes that can start on the same char are usually one terminal \u2014 merge them, or make the distinction explicit with a leading literal.`;
4197
+ return `the arms can start on the same character, so the choice cannot dispatch between them: on those chars the first arm is entered speculatively and rolled back before the second is tried. Give the arms disjoint leading terminals, or left-factor the common part out.`;
4198
+ }
4199
+ var PERF_ARM = "removes a speculative arm: on every input position reaching this choice, the deleted arm no longer costs a ctx save, a child-array push and a rollback before the surviving arm is tried. The saving scales with how often the choice is reached and how much the doomed arm consumes before failing.";
4200
+ var PERF_NONE = "";
4201
+ function findRewrites(visited, shapeOf) {
4202
+ const out = [];
4203
+ const push = (f, at) => {
4204
+ const site = siteToString(f.site);
4205
+ out.push({ kind: "rewrite", id: `rewrite:${f.rewrite}:${site}${at === void 0 ? "" : `:${at}`}`, ...f });
4206
+ };
4207
+ for (const v2 of visited.values()) {
4208
+ const d = v2.d;
4209
+ const site = v2.site;
4210
+ if (d.tag === "optional" || d.tag === "many" || d.tag === "oneOrMore") {
4211
+ const inner = d.parser;
4212
+ const id = inner._def;
4213
+ const collapsed = collapseRepeat(d, id);
4214
+ if (collapsed !== null) {
4215
+ push({
4216
+ rewrite: "idempotent-nesting",
4217
+ site,
4218
+ from: clamp(render(v2.p, 2)),
4219
+ to: collapsed,
4220
+ astNeutral: false,
4221
+ perf: "the outer wrapper still runs its own bookkeeping (and, for `many(optional(X))`, guards a zero-width iteration on every pass) for a nullability the inner combinator already has.",
4222
+ bug: id.tag === "optional" && d.tag === "many",
4223
+ suggestion: `\`${clamp(render(v2.p, 2))}\` is exactly \`${collapsed}\`. ${id.tag === "optional" && d.tag === "many" ? "The nested form is also a zero-width-iteration hazard: the inner `optional` succeeds consuming nothing, which the repeat has to detect and break on every pass." : ""} Candidate \u2014 verify AST identity: the collapsed form produces ONE level of array/undefined where the nested form produces two.`
4224
+ });
4225
+ }
4226
+ }
4227
+ if ((d.tag === "choice" || d.tag === "sequence") && d.parsers.length === 1) {
4228
+ push({
4229
+ rewrite: "single-element",
4230
+ site,
4231
+ from: clamp(render(v2.p, 2)),
4232
+ to: d.tag === "choice" ? clamp(render(d.parsers[0], 2)) : `${clamp(render(d.parsers[0], 2))} (note: sequence() of one still wraps its value in a 1-tuple)`,
4233
+ astNeutral: d.tag === "choice",
4234
+ perf: d.tag === "choice" ? "a one-arm choice still emits its dispatch scaffolding." : PERF_NONE,
4235
+ bug: false,
4236
+ suggestion: d.tag === "choice" ? "a `choice` with one arm IS that arm \u2014 the wrapper only adds dispatch scaffolding. Safe to unwrap (choice forwards its arm value unchanged)." : "a `sequence` with one element only exists to build a 1-tuple. If nothing reads that tuple, drop the wrapper \u2014 CANDIDATE, verify AST identity first."
4237
+ });
4238
+ }
4239
+ if (d.tag !== "choice") continue;
4240
+ const arms = d.parsers;
4241
+ const armShapes = arms.map(shapeOf);
4242
+ const strategy = d.strategy;
4243
+ for (let i = 0; i < arms.length; i++) for (let j = i + 1; j < arms.length; j++) {
4244
+ if (armShapes[i] !== armShapes[j]) continue;
4245
+ push({
4246
+ rewrite: "duplicate-arm",
4247
+ site,
4248
+ from: `choice(\u2026 arm[${i}] \u2026, arm[${j}] \u2026)`,
4249
+ to: `delete arm[${j}]`,
4250
+ astNeutral: true,
4251
+ perf: PERF_ARM,
4252
+ bug: true,
4253
+ suggestion: `arm[${i}] and arm[${j}] are the SAME shape (\`${clamp(render(arms[i], 2))}\`). Under ordered choice arm[${i}] always wins, so arm[${j}] is unreachable \u2014 delete it. This is AST-neutral by construction: an arm that can never be selected contributes nothing to any parse.`
4254
+ }, `${i}-${j}`);
4255
+ }
4256
+ for (let i = 0; i < arms.length; i++) for (let j = 0; j < arms.length; j++) {
4257
+ if (i >= j) continue;
4258
+ if (armShapes[i] === armShapes[j]) continue;
4259
+ const ta = termsOf(arms[i]), tb = termsOf(arms[j]);
4260
+ if (ta.length === 0 || ta.length >= tb.length) continue;
4261
+ let k = 0;
4262
+ while (k < ta.length && shapeOf(ta[k]) === shapeOf(tb[k])) k++;
4263
+ if (k !== ta.length) continue;
4264
+ push({
4265
+ rewrite: "shadowed-arm",
4266
+ site,
4267
+ from: `choice(\u2026 arm[${i}]=${clamp(render(arms[i], 2))}, \u2026 arm[${j}]=${clamp(render(arms[j], 2))} \u2026)`,
4268
+ to: `arm[${j}] is unreachable`,
4269
+ astNeutral: true,
4270
+ perf: PERF_ARM,
4271
+ bug: true,
4272
+ suggestion: `arm[${i}] is a strict prefix of arm[${j}] and comes FIRST. Whenever arm[${j}] would match, arm[${i}] also matches, and ordered choice commits to it \u2014 so arm[${j}] can never be selected. Either swap them (longest first) or fold arm[${j}]'s extra terms into arm[${i}] as \`optional(\u2026)\`. Verify by hand where a \`not()\`/\`gate()\` in arm[${i}] makes its success input-dependent in a way this static check cannot see.`
4273
+ }, `${i}-${j}`);
4274
+ }
4275
+ for (let i = 0; i + 1 < arms.length; i++) {
4276
+ const j = i + 1;
4277
+ const longer = termsOf(arms[i]), shorter = termsOf(arms[j]);
4278
+ const ordered = detectOptionalTerm(longer, shorter, shapeOf);
4279
+ const reversed = ordered === null ? detectOptionalTerm(shorter, longer, shapeOf) : null;
4280
+ const hit = ordered ?? reversed;
4281
+ if (hit === null) continue;
4282
+ const nodeWrapped = v2.enclosingNode ?? wrappingNodeType(arms[i]) ?? wrappingNodeType(arms[j]);
4283
+ const rest = hit.rest.map((t) => render(t, 2)).join(", ");
4284
+ const to = hit.where === "prefix" ? `sequence(optional(${render(hit.term, 2)}), ${rest})` : `sequence(${rest}, optional(${render(hit.term, 2)}))`;
4285
+ push({
4286
+ rewrite: hit.where === "prefix" ? "optional-prefix" : "optional-suffix",
4287
+ site,
4288
+ from: clamp(render(v2.p, 2)),
4289
+ to: clamp(to),
4290
+ astNeutral: false,
4291
+ perf: PERF_ARM,
4292
+ bug: false,
4293
+ suggestion: `these two arms differ only by a ${hit.where === "prefix" ? "leading" : "trailing"} \`${clamp(render(hit.term, 2))}\`, so the choice is exactly \`${clamp(to)}\` \u2014 same language, one arm instead of two.${reversed !== null ? ` NOTE THE ORDER: the shorter arm is FIRST, so under ordered choice the longer arm is only reachable when the shorter one fails \u2014 check that is what you meant.` : ""} CANDIDATE \u2014 verify AST identity before applying:${nodeWrapped === null ? "" : ` this sits under \`node('${nodeWrapped}')\`, and`} the two forms produce different child arrays (two children vs. one child plus an absent optional). If a build fn or a downstream consumer reads children positionally, the rewrite MOVES the tree.`
4294
+ }, `${i}-${j}`);
4295
+ }
4296
+ for (let i = 0; i + 1 < arms.length; i++) {
4297
+ const j = i + 1;
4298
+ const ta = termsOf(arms[i]), tb = termsOf(arms[j]);
4299
+ if (ta.length < 2 || tb.length < 2) continue;
4300
+ let k = 0;
4301
+ while (k < ta.length - 1 && k < tb.length - 1 && shapeOf(ta[k]) === shapeOf(tb[k])) k++;
4302
+ if (k === 0) continue;
4303
+ const handled = strategy?.tag === "sharedPrefix" && Array.isArray(strategy.members) && strategy.members.includes(i) && strategy.members.includes(j);
4304
+ push({
4305
+ rewrite: "left-factor",
4306
+ site,
4307
+ from: `choice(sequence(${ta.map((t) => render(t, 1)).join(", ")}), sequence(${tb.map((t) => render(t, 1)).join(", ")}))`,
4308
+ to: clamp(`sequence(${ta.slice(0, k).map((t) => render(t, 2)).join(", ")}, choice(sequence(${ta.slice(k).map((t) => render(t, 1)).join(", ")}), sequence(${tb.slice(k).map((t) => render(t, 1)).join(", ")})))`),
4309
+ astNeutral: false,
4310
+ perf: handled ? "NONE \u2014 parseman's `sharedPrefix` choice strategy already recognizes this prefix once and replays it, so the runtime cost is identical. This is a readability finding." : PERF_ARM,
4311
+ bug: false,
4312
+ suggestion: `arm[${i}] and arm[${j}] spell the same ${k} leading term(s). ${handled ? "The `sharedPrefix` strategy already collapses this AT RUNTIME, so left-factoring buys readability, not speed \u2014 but writing it out means the next reader does not have to trust the optimizer." : "Left-factor to `sequence(prefix, choice(tailA, tailB))`: the prefix is then recognized once and the inner choice dispatches on its own first char."} CANDIDATE \u2014 verify AST identity: factoring nests the tails one level deeper in the value.`
4313
+ }, `${i}-${j}`);
4314
+ }
4315
+ }
4316
+ for (const v2 of visited.values()) {
4317
+ if (v2.d.tag !== "sequence") continue;
4318
+ const hit = detectHandRolledSepBy(v2.d.parsers, shapeOf);
4319
+ if (hit === null) continue;
4320
+ const captured = v2.d.parsers.some((t) => capturesPositionally(t));
4321
+ const verdict = captured ? "blocked-by-capture" : v2.enclosingReducer ? "reducer-stride-review" : "convertible";
4322
+ const to = `sepBy(${clamp(render(hit.item, 2))}, ${clamp(render(hit.sep, 2))}${hit.trailing ? `, { trailing: 'allow' }` : ""}, { min: ${hit.min} })`;
4323
+ out.push({
4324
+ kind: "rewrite",
4325
+ id: `rewrite:hand-rolled-sepby:${siteToString(v2.site)}`,
4326
+ rewrite: "hand-rolled-sepby",
4327
+ site: v2.site,
4328
+ sepByVerdict: verdict,
4329
+ from: clamp(render(v2.p, 3)),
4330
+ to: verdict === "convertible" ? to : `${to} \u2014 NOT APPLICABLE HERE (${verdict})`,
4331
+ astNeutral: false,
4332
+ perf: verdict === "convertible" ? "the hand-rolled form allocates a tuple per repetition (`sequence(sep, item)`) and a wrapper array around them; `sepBy` builds the item list directly." : PERF_NONE,
4333
+ bug: false,
4334
+ suggestion: `${SEPBY_SPELLED_OUT(hit.trailing)} ${SEPBY_VERDICT_TEXT[verdict](hit.trailing)}`
4335
+ });
4336
+ }
4337
+ return out;
4338
+ }
4339
+ function collapseRepeat(outer, inner) {
4340
+ const o = outer.tag, i = inner.tag;
4341
+ if (o === "optional" && i === "optional") return "optional(X)";
4342
+ if (o === "optional" && i === "many") return "many(X)";
4343
+ if (o === "many" && i === "many") return "many(X)";
4344
+ if (o === "many" && i === "optional") return "many(X)";
4345
+ if (o === "optional" && i === "oneOrMore" && inner.min === 1 && inner.max === void 0) return "many(X)";
4346
+ if (o === "many" && i === "oneOrMore" && inner.min === 1 && inner.max === void 0) return "many(X)";
4347
+ return null;
4348
+ }
4349
+ function wrappingNodeType(arm) {
4350
+ const d = arm._def;
4351
+ return d.tag === "node" ? d.type ?? "(anonymous)" : null;
4352
+ }
4353
+ function detectOptionalTerm(longer, shorter, shapeOf) {
4354
+ if (longer.length !== shorter.length + 1 || shorter.length === 0) return null;
4355
+ const same = (a, b) => a.length === b.length && a.every((x, i) => shapeOf(x) === shapeOf(b[i]));
4356
+ if (same(longer.slice(1), shorter)) return { where: "prefix", term: longer[0], rest: shorter };
4357
+ if (same(longer.slice(0, -1), shorter)) return { where: "suffix", term: longer[longer.length - 1], rest: shorter };
4358
+ return null;
4359
+ }
4360
+ var SEPBY_SPELLED_OUT = (trailing) => `this is \`sepBy(item, sep${trailing ? ", { trailing: 'allow' }" : ""})\` spelled out.`;
4361
+ var SEPBY_VERDICT_TEXT = {
4362
+ "blocked-by-capture": () => "BLOCKED: the repetition CAPTURES its separator (`field`/`label`), which `sepBy` cannot express \u2014 it yields items and discards separators, so byte-faithful layout replay is lost. Leave this site alone; read it as a parseman gap (a separator-capturing repeat), not as work.",
4363
+ "reducer-stride-review": () => "NEEDS REVIEW, not conversion: an enclosing `node()`/`transform()` reducer reads this repetition's children, and a left-associating reducer typically STRIDES BY TWO over `[item, sep, item, sep, \u2026]` and re-emits the operator text. `sepBy` yields a FLAT ITEM list with the separators gone, so the reducer has to be rewritten in the same change or the tree moves. Convert only together with the reducer.",
4364
+ convertible: (trailing) => `No separator capture, and no enclosing reducer reads these children \u2014 this site is convertible. Beyond the ${trailing ? "four" : "three"} combinators it replaces, \`sepBy\` carries the right NULLABILITY: \`min: 0\` (the default) matches empty and does NOT gate as a choice arm, \`min: 1\` does \u2014 a distinction the hand-rolled shape leaves implicit and gets wrong silently. Still check the consumer: \`sepBy\` yields a flat item list where the hand-rolled form yields \`[first, [[sep, item], \u2026]]\`.`
4365
+ };
4366
+ function capturesPositionally(p, seen = /* @__PURE__ */ new Set()) {
4367
+ if (seen.has(p)) return false;
4368
+ seen.add(p);
4369
+ const d = p._def;
4370
+ if (d.tag === "field" || d.tag === "label") return true;
4371
+ for (const k of childrenOf(d)) if (capturesPositionally(k, seen)) return true;
4372
+ return false;
4373
+ }
4374
+ function detectHandRolledSepBy(terms, shapeOf) {
4375
+ if (terms.length < 2 || terms.length > 3) return null;
4376
+ const item = terms[0];
4377
+ const rep = terms[1]._def;
4378
+ if (rep.tag !== "many" && rep.tag !== "oneOrMore") return null;
4379
+ if (rep.max !== void 0) return null;
4380
+ const innerDef = rep.parser._def;
4381
+ if (innerDef.tag !== "sequence" || innerDef.parsers.length !== 2) return null;
4382
+ const [sep2, repeated] = innerDef.parsers;
4383
+ if (shapeOf(repeated) !== shapeOf(item)) return null;
4384
+ let trailing = false;
4385
+ if (terms.length === 3) {
4386
+ const t = terms[2]._def;
4387
+ if (t.tag !== "optional" || shapeOf(t.parser) !== shapeOf(sep2)) return null;
4388
+ trailing = true;
4389
+ }
4390
+ return { item, sep: sep2, trailing, min: 1 + rep.min };
4391
+ }
4392
+ function bodyTermsOf(p) {
4393
+ let cur = p;
4394
+ for (; ; ) {
4395
+ const d2 = cur._def;
4396
+ if (d2.tag === "node" || d2.tag === "grammar" || d2.tag === "transform" || d2.tag === "label" || d2.tag === "attempt" || d2.tag === "leaf" || d2.tag === "token" || d2.tag === "expect" || d2.tag === "field") {
4397
+ cur = d2.parser;
4398
+ continue;
4399
+ }
4400
+ break;
4401
+ }
4402
+ const d = cur._def;
4403
+ return d.tag === "sequence" ? d.parsers : [cur];
4404
+ }
4405
+ var MIN_SHARED_TERMS = 2;
4406
+ function findDivergentNodes(visited, shapeOf) {
4407
+ const byType = /* @__PURE__ */ new Map();
4408
+ for (const v2 of visited.values()) {
4409
+ if (v2.d.tag !== "node" || v2.d.type === void 0) continue;
4410
+ let m = byType.get(v2.d.type);
4411
+ if (m === void 0) {
4412
+ m = /* @__PURE__ */ new Map();
4413
+ byType.set(v2.d.type, m);
4414
+ }
4415
+ if (!m.has(v2.shape)) m.set(v2.shape, v2);
4416
+ }
4417
+ const out = [];
4418
+ for (const [type, m] of byType) {
4419
+ if (m.size < 2) continue;
4420
+ const members = [...m.values()];
4421
+ const termSets = members.map((v2) => bodyTermsOf(v2.p).map((t) => ({ id: shapeOf(t), text: clamp(render(t, 2)) })));
4422
+ const shared = termSets[0].filter((t) => termSets.every((set) => set.some((x) => x.id === t.id)));
4423
+ const uniqueShared = [...new Map(shared.map((t) => [t.id, t])).values()];
4424
+ if (uniqueShared.length < MIN_SHARED_TERMS) continue;
4425
+ const sharedIds = new Set(uniqueShared.map((t) => t.id));
4426
+ out.push({
4427
+ kind: "divergent-node",
4428
+ id: `divergent-node:${type}`,
4429
+ nodeType: type,
4430
+ count: members.length,
4431
+ sharedTerms: uniqueShared.map((t) => t.text),
4432
+ productions: members.map((v2, i) => ({
4433
+ shape: clamp(render(v2.p, 3)),
4434
+ site: v2.site,
4435
+ distinctTerms: termSets[i].filter((t) => !sharedIds.has(t.id)).map((t) => t.text)
4436
+ })),
4437
+ suggestion: `\`${type}\` is built by ${members.length} structurally different productions that nevertheless spell ${uniqueShared.length} identical term(s) (${uniqueShared.map((t) => `\`${t.text}\``).join(", ")}) \u2014 they are variants of ONE shape, not ${members.length} constructs. Nothing keeps them in sync: an edit to the ${type} shape has to land in all ${members.length}, and no test fails when it lands in one. Fold them into a single production whose varying part is a \`choice\`, or, if the variants exist for a parse-order reason (a fast path tried first), say so at the definition \u2014 the cost of the split is that a reader cannot tell which it is.`
4438
+ });
4439
+ }
4440
+ return out.sort((a, b) => b.sharedTerms.length * b.count - a.sharedTerms.length * a.count);
4441
+ }
4442
+ function resolveArm(p) {
4443
+ let cur = p;
4444
+ const seen = /* @__PURE__ */ new Set();
4445
+ for (; ; ) {
4446
+ if (seen.has(cur)) return cur;
4447
+ seen.add(cur);
4448
+ const d = cur._def;
4449
+ if (d.tag === "label" || d.tag === "attempt") {
4450
+ cur = d.parser;
4451
+ continue;
4452
+ }
4453
+ if (d.tag === "lazy") {
4454
+ try {
4455
+ cur = d.thunk();
4456
+ continue;
4457
+ } catch {
4458
+ return cur;
4459
+ }
4460
+ }
4461
+ return cur;
4462
+ }
4463
+ }
4464
+ function nestedNodeTypes(root, out, seen, isRoot) {
4465
+ if (seen.has(root)) return;
4466
+ seen.add(root);
4467
+ const d = root._def;
4468
+ if (d.tag === "leaf" || d.tag === "token") return;
4469
+ if (d.tag === "node" && !isRoot) out.add(d.type ?? "(anonymous)");
4470
+ if (d.tag === "lazy") {
4471
+ try {
4472
+ nestedNodeTypes(d.thunk(), out, seen, false);
4473
+ } catch {
4474
+ }
4475
+ return;
4476
+ }
4477
+ for (const k of childrenOf(d)) nestedNodeTypes(k, out, seen, false);
4478
+ }
4479
+ var MAX_LOST_TYPES = 8;
4480
+ function findStructureLoss(visited, resolve2) {
4481
+ const nestedCache = /* @__PURE__ */ new Map();
4482
+ const nested = (p) => {
4483
+ const hit = nestedCache.get(p);
4484
+ if (hit !== void 0) return hit;
4485
+ const out = /* @__PURE__ */ new Set();
4486
+ nestedNodeTypes(p, out, /* @__PURE__ */ new Set(), true);
4487
+ nestedCache.set(p, out);
4488
+ return out;
4489
+ };
4490
+ const findings = [];
4491
+ for (const v2 of visited.values()) {
4492
+ const d = v2.d;
4493
+ if (d.tag !== "choice") continue;
4494
+ const arms = d.parsers;
4495
+ const resolved = arms.map(resolveArm);
4496
+ const nodeTypes = resolved.map(wrappingNodeType);
4497
+ const fs2 = arms.map((a) => firstSetOf(a, /* @__PURE__ */ new Set(), resolve2));
4498
+ for (let i = 0; i < arms.length; i++) {
4499
+ if (d.gates[i] != null) continue;
4500
+ const type = nodeTypes[i] ?? null;
4501
+ if (type === null) continue;
4502
+ if (nested(resolved[i]).size !== 0) continue;
4503
+ const A = fs2[i];
4504
+ if (A.kind === "any") continue;
4505
+ for (let j = i + 1; j < arms.length; j++) {
4506
+ if (nodeTypes[j] !== type) continue;
4507
+ const B = fs2[j];
4508
+ if (B.kind === "any" || !intersects3(A, B)) continue;
4509
+ const lost = nested(resolved[j]);
4510
+ if (lost.size === 0) continue;
4511
+ const lostNodeTypes = [...lost].sort();
4512
+ findings.push({
4513
+ kind: "structure-loss",
4514
+ id: `structure-loss:${siteToString(v2.site)}:${i}-${j}`,
4515
+ site: v2.site,
4516
+ nodeType: type,
4517
+ earlier: i,
4518
+ later: j,
4519
+ on: intersection2(A, B),
4520
+ lostNodeTypes: lostNodeTypes.slice(0, MAX_LOST_TYPES),
4521
+ earlierShape: clamp(render(resolved[i], 3)),
4522
+ laterShape: clamp(render(resolved[j], 3)),
4523
+ suggestion: structureLossSuggestion(type, i, j, lostNodeTypes)
4524
+ });
4525
+ }
4526
+ }
4527
+ }
4528
+ return findings.sort((a, b) => b.lostNodeTypes.length - a.lostNodeTypes.length);
4529
+ }
4530
+ function structureLossSuggestion(type, i, j, lost) {
4531
+ const names = lost.slice(0, MAX_LOST_TYPES).map((t) => `\`${t}\``).join(", ");
4532
+ const more = lost.length > MAX_LOST_TYPES ? ` (and ${lost.length - MAX_LOST_TYPES} more)` : "";
4533
+ return `arm[${i}] and arm[${j}] both build \`${type}\` and can start on the same character, so on every input BOTH accept, arm[${i}] wins \u2014 and arm[${i}] contains no \`node()\`, so it yields a \`${type}\` over bare leaves. arm[${j}] would have built ${names}${more} there. The parse still succeeds and the span is still right; only the tree shape changes, and only on the inputs arm[${i}] happens to accept \u2014 so a suite that asserts "it parses" and an output diff both stay green while every consumer of the tree (language service, formatter, refactor) silently loses those nodes on the commonest inputs. Decide which tree is the contract: if the structured one is, DELETE arm[${i}] or move it after arm[${j}]; if the flat one is, it must be flat for ALL inputs of this shape, not just the ones arm[${i}] matches \u2014 a fast path that is not tree-neutral is not a fast path.`;
4534
+ }
4535
+ function findKeywordRegexes(visited) {
4536
+ const armIndexOf = /* @__PURE__ */ new Map();
4537
+ for (const v2 of visited.values()) {
4538
+ if (v2.d.tag !== "choice") continue;
4539
+ v2.d.parsers.forEach((a, i) => {
4540
+ let cur = a;
4541
+ for (; ; ) {
4542
+ const cd = cur._def;
4543
+ if (cd.tag === "node" || cd.tag === "label" || cd.tag === "transform" || cd.tag === "leaf" || cd.tag === "attempt") {
4544
+ cur = cd.parser;
4545
+ continue;
4546
+ }
4547
+ break;
4548
+ }
4549
+ armIndexOf.set(cur, { owner: v2.p, index: i });
4550
+ });
4551
+ }
4552
+ const keywordArms = /* @__PURE__ */ new Map();
4553
+ for (const [arm, { owner }] of armIndexOf) {
4554
+ const d = arm._def;
4555
+ if (d.tag !== "regex" || keywordRegexShape(d.source) === null) continue;
4556
+ const list = keywordArms.get(owner) ?? [];
4557
+ list.push(armIndexOf.get(arm).index);
4558
+ keywordArms.set(owner, list);
4559
+ }
4560
+ const out = [];
4561
+ for (const v2 of visited.values()) {
4562
+ const d = v2.d;
4563
+ if (d.tag !== "regex") continue;
4564
+ const shape = keywordRegexShape(d.source);
4565
+ if (shape === null) continue;
4566
+ const ci = d.flags.includes("i");
4567
+ const unicode = d.flags.includes("u") || d.flags.includes("v");
4568
+ const own = armIndexOf.get(v2.p);
4569
+ const siblings = own === void 0 ? [] : (keywordArms.get(own.owner) ?? []).filter((i) => i !== own.index);
4570
+ const bq = (s) => `'${s.replace(/\\/g, "\\\\")}'`;
4571
+ const vocabulary = shape.words.length >= VOCABULARY_MIN_WORDS;
4572
+ const hazards = keywordAlternationHazards(shape.words, shape.boundary);
4573
+ const unrescued = hazards.filter((h) => !h.rescuedByBoundary);
4574
+ const longestFirst = shape.words.every((w, i) => i === 0 || shape.words[i - 1].length >= w.length);
4575
+ const shown = shape.words.length > 8 ? [...shape.words.slice(0, 6), "/* \u2026 */", shape.words[shape.words.length - 1]] : shape.words;
4576
+ const call = shape.words.length === 1 ? `word(${bq(shape.words[0])}, ${bq(shape.boundary ?? "_0-9A-Za-z")}${ci ? ", { caseInsensitive: true }" : ""})` : `keywords([${shown.map((w) => w === "/* \u2026 */" ? w : bq(w)).join(", ")}], { boundary: ${bq(shape.boundary ?? "_0-9A-Za-z")}${ci ? ", caseInsensitive: true" : ""} })`;
4577
+ out.push({
4578
+ kind: "keyword-regex",
4579
+ id: `keyword-regex:${siteToString(v2.site)}`,
4580
+ site: v2.site,
4581
+ source: d.source,
4582
+ flags: d.flags,
4583
+ words: shape.words,
4584
+ boundary: shape.boundary,
4585
+ caseFoldRisk: ci && !unicode,
4586
+ siblingArms: siblings,
4587
+ vocabulary,
4588
+ longestFirst,
4589
+ hazards,
4590
+ bug: unrescued.length > 0,
4591
+ suggestion: [
4592
+ vocabulary ? `this regex enumerates a FIXED VOCABULARY of ${shape.words.length} literal words${shape.words.length > 20 ? " \u2014 the alternation is long enough that nobody re-reads it" : ""}. A regex enumerating a fixed vocabulary is a keyword set written the hard way. Use \`${call}\` \u2014 it exposes an EXACT first-set that \`choice\` can dispatch on (the alternation exposes none), it owns the boundary so it is not re-spelled per word, it sorts LONGEST-FIRST by construction, and it lowers to a single charCodeAt scan.` : `\`regex(/${d.source}/${d.flags})\` hand-rolls a keyword and its word boundary. Use \`${call}\` \u2014 it owns the boundary (so \`${shape.boundary ?? ""}\` is not re-spelled per keyword), exposes an EXACT first-set that \`choice\` can dispatch on, and lowers to the same charCodeAt scan.`,
4593
+ unrescued.length > 0 ? `LIVE BUG \u2014 the alternation is ordered so ${unrescued.length} later alternative(s) can NEVER match: ${unrescued.slice(0, 3).map((h) => `\`${h.shorter}\` precedes \`${h.longer}\` and nothing rejects the following \`${h.at}\``).join("; ")}${unrescued.length > 3 ? `, and ${unrescued.length - 3} more` : ""}. Regex alternation is FIRST-match, not longest-match, and with no boundary guard to force a backtrack the shorter branch wins outright. \`keywords()\` cannot express this bug: it sorts longest-first.` : hazards.length > 0 ? `ORDERING HAZARD (currently harmless): ${hazards.length} earlier alternative(s) are strict prefixes of later ones \u2014 e.g. \`${hazards[0].shorter}\` before \`${hazards[0].longer}\`. Regex alternation is first-match, so these work ONLY because the trailing boundary guard rejects the next character and the engine backtracks into the longer branch. That correctness rests on a hand-maintained order plus a guard that happens to cover the right characters; \`keywords()\` sorts longest-first by construction and does not depend on either.` : !longestFirst && vocabulary ? `The alternation is not in longest-first order. It is correct today${shape.boundary === null ? "" : " (the boundary guard forces a backtrack where it matters)"}, but the ordering is hand-maintained and nothing checks it \u2014 adding a word that extends an existing one is a silent bug. \`keywords()\` sorts longest-first by construction.` : "",
4594
+ ci && !unicode ? `CORRECTNESS, not style: \`/i\` without \`/u\` does NOT fold case the way \`{c, toUpperCase(c), toLowerCase(c)}\` suggests \u2014 67 BMP code points fold in ways those three miss (\u03C2/\u03C3, \xB5/\u03BC, the \u01C4\u01C5\u01C6 digraphs, combining iota subscript). parseman fixed exactly this INSIDE \`keywords()\` (see combinators/case-fold.ts); a hand-rolled copy never received the fix, so its first-set is unsound for those inputs.` : "",
4595
+ siblings.length > 0 ? `arm[${own?.index}] here sits alongside arm(s) [${siblings.join(", ")}] in the same \`choice\`, which are also hand-rolled keywords. A single \`keywords([\u2026])\` replaces the whole group and gives longest-match dispatch across it \u2014 which is what that combinator exists for.` : ""
4596
+ ].filter((s) => s !== "").join(" ")
4597
+ });
4598
+ }
4599
+ return out.sort((a, b) => Number(b.bug) - Number(a.bug) || b.hazards.length - a.hazards.length || b.words.length - a.words.length || b.siblingArms.length - a.siblingArms.length);
4600
+ }
4601
+ var sitesLine = (sites) => sites.slice(0, 6).map(siteToString).join("\n ") + (sites.length > 6 ? `
4602
+ \u2026 and ${sites.length - 6} more` : "");
4603
+ function formatDuplicationFindings(report) {
4604
+ const lines = [];
4605
+ for (const f of report.structureLoss) {
4606
+ lines.push(`parseman BUG [structure-loss] @ ${siteToString(f.site)}: arm[${f.earlier}] flattens \`${f.nodeType}\` where arm[${f.later}] structures it, on ${firstSetToString(f.on)}`);
4607
+ lines.push(` flat: ${f.earlierShape}`);
4608
+ lines.push(` rich: ${f.laterShape}`);
4609
+ lines.push(` lost: ${f.lostNodeTypes.map((t) => `\`${t}\``).join(", ")}`);
4610
+ lines.push(` fix: ${f.suggestion}`);
4611
+ }
4612
+ for (const f of report.rewrites) {
4613
+ lines.push(`parseman ${f.bug ? "BUG" : "rewrite"} [${f.rewrite}${f.sepByVerdict === void 0 ? "" : `/${f.sepByVerdict}`}] @ ${siteToString(f.site)}${f.astNeutral ? "" : " (candidate \u2014 verify AST identity)"}`);
4614
+ lines.push(` from: ${f.from}`);
4615
+ lines.push(` to: ${f.to}`);
4616
+ lines.push(` fix: ${f.suggestion}`);
4617
+ if (f.perf !== "") lines.push(` perf: ${f.perf}`);
4618
+ }
4619
+ for (const f of report.divergentNodes) {
4620
+ lines.push(`parseman divergent-node: \`${f.nodeType}\` is built by ${f.count} different productions sharing ${f.sharedTerms.length} term(s): ${f.sharedTerms.map((t) => `\`${t}\``).join(", ")}`);
4621
+ for (const p of f.productions) lines.push(` ${siteToString(p.site)}
4622
+ ${p.shape}
4623
+ unique to it: ${p.distinctTerms.length === 0 ? "(none)" : p.distinctTerms.map((t) => `\`${t}\``).join(", ")}`);
4624
+ lines.push(` fix: ${f.suggestion}`);
4625
+ }
4626
+ for (const f of report.keywordRegexes) {
4627
+ const src = f.source.length > 90 ? `${f.source.slice(0, 88)}\u2026` : f.source;
4628
+ lines.push(`parseman ${f.bug ? "BUG " : ""}keyword-regex @ ${siteToString(f.site)}: /${src}/${f.flags}${f.vocabulary ? ` [${f.words.length} literal words${f.longestFirst ? "" : ", NOT longest-first"}]` : ""}${f.hazards.length > 0 ? ` [${f.hazards.length} prefix hazard(s)${f.bug ? ", UNRESCUED" : ""}]` : ""}${f.caseFoldRisk ? " [CASE-FOLD RISK]" : ""}`);
4629
+ lines.push(` fix: ${f.suggestion}`);
4630
+ }
4631
+ for (const f of report.duplicates) {
4632
+ lines.push(`parseman duplication: ${f.count}\xD7 identical ${f.size}-node shape (saves ${f.savings} nodes) \u2014 ${f.shape}`);
4633
+ lines.push(` at: ${sitesLine(f.sites)}`);
4634
+ lines.push(` fix: ${f.suggestion}`);
4635
+ }
4636
+ for (const f of report.nearDuplicates) {
4637
+ lines.push(`parseman near-duplication: ${f.count} clones of a ${f.sharedSize}-node shape differing at ONE slot (saves ${f.savings} nodes) \u2014 ${f.shape}`);
4638
+ lines.push(` slot: ${f.slotPath} \u2208 { ${f.variants.join(" | ")} }`);
4639
+ lines.push(` at: ${sitesLine(f.sites)}`);
4640
+ lines.push(` fix: ${f.suggestion}`);
4641
+ }
4642
+ for (const f of report.regexFragments) {
4643
+ lines.push(`parseman regex-fragment: \`${f.fragment}\` (${f.branches} branches) re-spelled in ${f.count} regex() terminals`);
4644
+ lines.push(` at: ${sitesLine(f.sites)}`);
4645
+ lines.push(` fix: ${f.suggestion}`);
4646
+ }
4647
+ for (const f of report.regexClasses) {
4648
+ lines.push(`parseman regex-class${f.drifted ? " DRIFT" : ""}: [${f.canonical}] across ${f.count} regex() terminals${f.bmpCeiling ? " [BMP ceiling]" : ""}`);
4649
+ for (const v2 of f.variants) lines.push(` [${v2.source}] \xD7${v2.count} ${v2.delta}
4650
+ ${sitesLine(v2.sites)}`);
4651
+ lines.push(` fix: ${f.suggestion}`);
4652
+ }
4653
+ for (const f of report.overlaps) {
4654
+ lines.push(`parseman overlap @ ${siteToString(f.site)}: arm[${f.a}] \u2229 arm[${f.b}] on ${firstSetToString(f.on)}${f.sharedPrefix === null ? "" : ` \u2014 shared prefix: ${f.sharedPrefix}`}${f.handledByStrategy ? " [sharedPrefix strategy already handles this]" : ""}`);
4655
+ lines.push(` fix: ${f.suggestion}`);
4656
+ }
4657
+ return lines;
4658
+ }
4659
+ function duplicationFindingCount(report) {
4660
+ return report.duplicates.length + report.nearDuplicates.length + report.regexFragments.length + report.regexClasses.length + report.overlaps.length + report.rewrites.length + report.divergentNodes.length + report.structureLoss.length + report.keywordRegexes.length;
4661
+ }
4662
+
3293
4663
  // src/combinators/scanTo.ts
3294
4664
  function resolveScanSkip(explicitSkip, raw, ctx) {
3295
4665
  if (raw) return [];
@@ -6518,12 +7888,14 @@ function emitNode(def, ctx, pos) {
6518
7888
  ctx.buildSrcs.push(def.buildSrc ?? null);
6519
7889
  }
6520
7890
  const i = ind(ctx);
6521
- const capturesTrivia = mkType !== null || def.captureTrivia === true || def.trailingTrivia === true || !structural && buildReadsTrivia(def);
6522
- const clonesState = !structural && buildReadsState(def);
6523
- const capturesChildren = !structural && (mkType !== null || def.unwrap || def.collapse || buildReadsChildren(def));
6524
- const capturesRaw = !structural && (mkType !== null || buildReadsRaw(def));
7891
+ const cstOut = ctx.hostMode === "cst" && !structural;
7892
+ if (!structural && !cstOut) ctx.hostBranchElided = true;
7893
+ const capturesTrivia = cstOut || mkType !== null || def.captureTrivia === true || def.trailingTrivia === true || !structural && buildReadsTrivia(def);
7894
+ const clonesState = !structural && (cstOut || buildReadsState(def));
7895
+ const capturesChildren = !structural && (cstOut || mkType !== null || def.unwrap || def.collapse || buildReadsChildren(def));
7896
+ const capturesRaw = !structural && (cstOut || mkType !== null || buildReadsRaw(def));
6525
7897
  const hasFields = parserHasOwnFields(def.parser);
6526
- const capturesFields = hasFields && !structural && buildReadsFields(def);
7898
+ const capturesFields = hasFields && !structural && (cstOut || buildReadsFields(def));
6527
7899
  const innerEnablesTriviaCapture = parserEnablesTriviaCapture(def.parser);
6528
7900
  const chV = v(ctx, "_ch");
6529
7901
  const rawV = v(ctx, "_raw");
@@ -6540,8 +7912,9 @@ function emitNode(def, ctx, pos) {
6540
7912
  const profHoist = `${i}const ${pmV} = _ctx._pmProfile, ${recV} = ${pmV}?.phase === 'recognizer', ${capV} = ${pmV}?.phase === 'capture'`;
6541
7913
  const profileRecognizer = recV;
6542
7914
  const profileCapture = capV;
6543
- const directCstV = !structural && (!capturesChildren || !capturesRaw) ? v(ctx, "_dcst") : null;
6544
- const directCstGate = directCstV === null ? "false" : `${profileCapture} || _ctx.build?._parsemanCstOutput === true`;
7915
+ const cstMode = ctx.hostMode === "cst";
7916
+ const directCstV = !structural && !cstMode && (!capturesChildren || !capturesRaw) ? v(ctx, "_dcst") : null;
7917
+ const directCstGate = directCstV === null ? "false" : profileCapture;
6545
7918
  const structuralCapturesTrivia = structural && (def.captureTrivia === true || def.trailingTrivia === true);
6546
7919
  const chNeededExpr = def.unwrap || def.collapse ? "true" : `(_ctx._pmReadsCh ??= (_ctx.build === undefined || _ctx.build._parsemanReadsChildren !== false || _ctx.build._parsemanCstCollapse !== undefined))`;
6547
7920
  const chAlloc = def.unwrap || def.collapse ? `${profileRecognizer} ? undefined : []` : `${profileRecognizer} ? undefined : ((${profileCapture} || ${chNeededExpr}) ? [] : undefined)`;
@@ -6622,7 +7995,7 @@ ${i}const ${chV} = ${chAlloc}, ${rawV} = ${profileRecognizer} ? undefined : [],
6622
7995
  const ndV = v(ctx, "_nd");
6623
7996
  const buildExpr = structural ? `{ _tag: 'node', type: ${JSON.stringify(def.type)}, span: { start: ${pos}, end: ${endVar} }, state: ${stV} ?? null, children: ${chV} }` : mkType ? emitInlineMkNodeExpr(mkType, chV, rawV, pos, endVar, tlV) : `${buildRef(ctx)}[${buildIdx}](${chV}, ${fObj}, { start: ${pos}, end: ${endVar} }, ${rawV}, ${tlV}, ${stV})`;
6624
7997
  const hostBuildExpr = `_ctx._pmProfile?.phase === 'host' && _ctx._pmProfile.hostCalls++, _ctx.build(${JSON.stringify(def.type)}, ${chV}, ${fObj}, { start: ${pos}, end: ${endVar} }, ${rawV}, ${tlV}, ${stV})`;
6625
- const ndExpr = structural ? `_ctx.build !== undefined ? (${hostBuildExpr}) : (${buildExpr})` : `_ctx.build?._parsemanCstOutput === true ? (${hostBuildExpr}) : (${buildExpr})`;
7998
+ const ndExpr = structural ? `_ctx.build !== undefined ? (${hostBuildExpr}) : (${buildExpr})` : cstOut ? hostBuildExpr : buildExpr;
6626
7999
  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;
6627
8000
  const unwrapExpr = `${chV}.length === 1 ? (${chV}[0] !== null && typeof ${chV}[0] === 'object' && ${chV}[0]._tag === 'leaf' ? ${chV}[0].value : ${chV}[0]) : (${ndExpr})`;
6628
8001
  const collapseExpr = `${chV}.length === 1 ? ${chV}[0] : (${ndExpr})`;
@@ -7071,9 +8444,9 @@ function hasDirectBuildDef(p, seen = /* @__PURE__ */ new Set()) {
7071
8444
  return false;
7072
8445
  }
7073
8446
  }
7074
- return childrenOf(d).some((child) => hasDirectBuildDef(child, seen));
8447
+ return childrenOf2(d).some((child) => hasDirectBuildDef(child, seen));
7075
8448
  }
7076
- function childrenOf(def) {
8449
+ function childrenOf2(def) {
7077
8450
  switch (def.tag) {
7078
8451
  case "sequence":
7079
8452
  case "choice":
@@ -7144,7 +8517,7 @@ function subtreeSizes(roots) {
7144
8517
  const def = p._def;
7145
8518
  let s = 1;
7146
8519
  if (def.tag !== "lazy") {
7147
- for (const child of childrenOf(def)) s += child._def.tag === "lazy" ? 1 : sz(child);
8520
+ for (const child of childrenOf2(def)) s += child._def.tag === "lazy" ? 1 : sz(child);
7148
8521
  }
7149
8522
  sizes.set(p, s);
7150
8523
  return s;
@@ -7165,7 +8538,7 @@ function subtreeSizes(roots) {
7165
8538
  return;
7166
8539
  }
7167
8540
  sz(p);
7168
- for (const child of childrenOf(def)) visit(child);
8541
+ for (const child of childrenOf2(def)) visit(child);
7169
8542
  };
7170
8543
  for (const root of roots) visit(root);
7171
8544
  return sizes;
@@ -7199,7 +8572,7 @@ function analyzeLazyUsageMulti(roots) {
7199
8572
  if (isHoistableTag(def.tag)) {
7200
8573
  counts.set(p, (counts.get(p) ?? 0) + 1);
7201
8574
  }
7202
- for (const child of childrenOf(def)) walk2(child);
8575
+ for (const child of childrenOf2(def)) walk2(child);
7203
8576
  }
7204
8577
  const rootsArr = [...roots];
7205
8578
  for (const root of rootsArr) walk2(root);
@@ -7214,7 +8587,7 @@ function ruleDependencies(ruleMap) {
7214
8587
  const seen = /* @__PURE__ */ new Set();
7215
8588
  const walk2 = (p, isRoot) => {
7216
8589
  const def = p._def;
7217
- const boundary = def.tag === "lazy" ? p._ruleName ?? nameOf.get(p) : nameOf.get(p);
8590
+ const boundary = def.tag === "lazy" ? nameOf.get(p) ?? p._ruleName : nameOf.get(p);
7218
8591
  if (!isRoot && boundary !== void 0) {
7219
8592
  found.add(boundary);
7220
8593
  return;
@@ -7231,7 +8604,7 @@ function ruleDependencies(ruleMap) {
7231
8604
  walk2(resolved, false);
7232
8605
  return;
7233
8606
  }
7234
- for (const child of childrenOf(def)) walk2(child, false);
8607
+ for (const child of childrenOf2(def)) walk2(child, false);
7235
8608
  };
7236
8609
  walk2(comb, true);
7237
8610
  deps.set(name, [...found]);
@@ -7251,10 +8624,30 @@ function runGatingDiagnostic(combinator, opt) {
7251
8624
  function runGatingDiagnosticRules(ruleMap, opt) {
7252
8625
  return reportGating(opt, (o) => analyzeGatingRules(ruleMap, o));
7253
8626
  }
7254
- function runFusedGatingDiagnostic(getRuleMaps, opt) {
8627
+ function runFusedGatingDiagnostic(getRuleMaps, opt, getOpaque) {
7255
8628
  if (resolveGatingLevel(opt) === "off") return void 0;
7256
8629
  const ruleMaps = getRuleMaps();
7257
- if (!ruleMaps.some((map) => hasExternalRuleRef(map))) return void 0;
8630
+ const opaque = getOpaque?.() ?? [];
8631
+ const opaqueFindings = opaque.map((o) => ({
8632
+ rule: o.ruleNames.length > 0 ? o.ruleNames.join(", ") : `<artifact ${o.ns}>`,
8633
+ kind: "opaque-artifact",
8634
+ reason: `precompiled artifact "${o.ns}" carries compiled rule functions, not re-lowerable IR \u2014 its choices were not examined at this fuse.`
8635
+ }));
8636
+ if (!ruleMaps.some((map) => hasExternalRuleRef(map))) {
8637
+ if (opaqueFindings.length === 0) return void 0;
8638
+ return reportGating(opt, () => ({
8639
+ totalChoices: 0,
8640
+ gated: 0,
8641
+ recoverable: 0,
8642
+ unanalysable: opaqueFindings,
8643
+ ungated: [],
8644
+ accepted: [],
8645
+ deferred: [],
8646
+ acceptedUnused: [],
8647
+ choices: [],
8648
+ antiPatterns: []
8649
+ }));
8650
+ }
7258
8651
  const winners = /* @__PURE__ */ new Map();
7259
8652
  for (const map of ruleMaps) for (const [name, rule] of map) {
7260
8653
  if (rule._def.tag === "lazy") {
@@ -7270,7 +8663,12 @@ function runFusedGatingDiagnostic(getRuleMaps, opt) {
7270
8663
  return reportGating(opt, (o) => {
7271
8664
  const deferredHere = new Set(analyzeGatingRules(entries, o).deferred.map((c) => c.id));
7272
8665
  const fused = analyzeGatingRules(entries, { ...o, resolveRef: (name) => winners.get(name) });
7273
- return { ...fused, ungated: fused.ungated.filter((c) => deferredHere.has(c.id)), antiPatterns: [] };
8666
+ return {
8667
+ ...fused,
8668
+ ungated: fused.ungated.filter((c) => deferredHere.has(c.id)),
8669
+ antiPatterns: [],
8670
+ unanalysable: [...fused.unanalysable, ...opaqueFindings]
8671
+ };
7274
8672
  });
7275
8673
  }
7276
8674
  function reportGating(opt, analyze) {
@@ -7281,22 +8679,82 @@ function reportGating(opt, analyze) {
7281
8679
  let report;
7282
8680
  try {
7283
8681
  report = analyze(analyzeOpts);
7284
- } catch {
7285
- return void 0;
8682
+ } catch (e) {
8683
+ const reason = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
8684
+ const failed = {
8685
+ totalChoices: 0,
8686
+ gated: 0,
8687
+ recoverable: 0,
8688
+ unanalysable: [{ rule: "<whole grammar>", kind: "not-a-combinator", reason: `gating analysis threw \u2014 ${reason}` }],
8689
+ ungated: [],
8690
+ accepted: [],
8691
+ deferred: [],
8692
+ acceptedUnused: [],
8693
+ choices: [],
8694
+ antiPatterns: []
8695
+ };
8696
+ const failLines = formatGatingWarnings(failed);
8697
+ if (level === "error") throw new Error(`parseman: gating analysis FAILED (no verdict available)
8698
+ ${failLines.join("\n")}`);
8699
+ for (const l of failLines) console.warn(l);
8700
+ return failed;
7286
8701
  }
7287
8702
  const lines = formatGatingWarnings(report);
7288
8703
  if (lines.length > 0) {
7289
- if (level === "error") throw new Error(`parseman: ${report.ungated.length} ungated hot choice(s) / ${report.antiPatterns.length} anti-pattern(s)
8704
+ 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)
8705
+ ${lines.join("\n")}`);
8706
+ for (const l of lines) console.warn(l);
8707
+ }
8708
+ return report;
8709
+ }
8710
+ function resolveDuplicationLevel(opt) {
8711
+ const explicit = typeof opt === "string" ? opt : opt?.level;
8712
+ if (explicit !== void 0) return explicit;
8713
+ const env = typeof process !== "undefined" ? process.env?.PARSEMAN_DUPLICATION : void 0;
8714
+ if (env === "off" || env === "warn" || env === "error") return env;
8715
+ return "off";
8716
+ }
8717
+ function reportDuplication(opt, analyze) {
8718
+ const level = resolveDuplicationLevel(opt);
8719
+ if (level === "off") return void 0;
8720
+ const obj = opt !== null && typeof opt === "object" ? opt : void 0;
8721
+ const analyzeOpts = obj === void 0 ? void 0 : {
8722
+ ...obj.accept !== void 0 ? { accept: obj.accept } : {},
8723
+ ...obj.minSize !== void 0 ? { minSize: obj.minSize } : {},
8724
+ ...obj.maxFindings !== void 0 ? { maxFindings: obj.maxFindings } : {},
8725
+ ...obj.entryName !== void 0 ? { entryName: obj.entryName } : {}
8726
+ };
8727
+ let report;
8728
+ try {
8729
+ report = analyze(analyzeOpts);
8730
+ } catch (err) {
8731
+ console.warn(
8732
+ `parseman: the duplication diagnostic could not run, so NOTHING was checked \u2014 this is not a clean result.
8733
+ ${err instanceof Error ? err.message : String(err)}`
8734
+ );
8735
+ return void 0;
8736
+ }
8737
+ const lines = formatDuplicationFindings(report);
8738
+ if (lines.length > 0) {
8739
+ if (level === "error") throw new Error(`parseman: ${duplicationFindingCount(report)} duplication/overlap finding(s)
7290
8740
  ${lines.join("\n")}`);
7291
8741
  for (const l of lines) console.warn(l);
7292
8742
  }
7293
8743
  return report;
7294
8744
  }
8745
+ function runDuplicationDiagnostic(combinator, opt) {
8746
+ return reportDuplication(opt, (o) => analyzeDuplication(combinator, o));
8747
+ }
8748
+ function runDuplicationDiagnosticRules(ruleMap, opt) {
8749
+ return reportDuplication(opt, (o) => analyzeDuplicationRules(ruleMap, o));
8750
+ }
7295
8751
  function compile(combinator, mapFnSources, opts) {
7296
8752
  const gatingReport = runGatingDiagnostic(combinator, opts?.gating);
8753
+ runDuplicationDiagnostic(combinator, opts?.duplication);
7297
8754
  markUnusedValues(combinator);
7298
8755
  const grammarTrivia = combinator._meta.grammarTrivia;
7299
8756
  const grammarScanSkip = combinator._meta.grammarScanSkip;
8757
+ const grammarHostMode = combinator._meta.grammarHostMode;
7300
8758
  const ctx = {
7301
8759
  vars: 0,
7302
8760
  indent: 1,
@@ -7316,6 +8774,7 @@ function compile(combinator, mapFnSources, opts) {
7316
8774
  namedFnDecls: [],
7317
8775
  capturing: hasNodeDef(combinator),
7318
8776
  recovery: opts?.recovery ?? false,
8777
+ hostMode: opts?.hostMode ?? grammarHostMode ?? "ast",
7319
8778
  ...opts?.coverage ? { coverage: { plan: buildGrammarPlan(combinator), entry: combinator } } : {},
7320
8779
  lazyUsage: analyzeLazyUsage(combinator),
7321
8780
  ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
@@ -7388,6 +8847,7 @@ function compile(combinator, mapFnSources, opts) {
7388
8847
  return fn(input, pos, ctx.runtimeParsers, ctx.mapFns, ctx.buildFns, defaultCtx);
7389
8848
  },
7390
8849
  parseWithContext(input, parseCtx, pos = 0) {
8850
+ assertHostModeCompatible(ctx.hostMode ?? "ast", parseCtx.build, !!ctx.hostBranchElided);
7391
8851
  return fn(input, pos, ctx.runtimeParsers, ctx.mapFns, ctx.buildFns, parseCtx);
7392
8852
  },
7393
8853
  // Note: collects expect() errors via _errors. Unlike interpreter
@@ -7417,10 +8877,12 @@ function publicRuleWrapperSource(rule, fnSource) {
7417
8877
  }
7418
8878
  function compileRuleMap(ruleMap, opts) {
7419
8879
  runGatingDiagnosticRules(ruleMap, opts?.gating);
8880
+ runDuplicationDiagnosticRules(ruleMap, opts?.duplication);
7420
8881
  for (const [, rule] of ruleMap) markUnusedValues(rule);
7421
8882
  const coverageWinners = opts?.coverage ? Object.fromEntries(ruleMap.filter(([, rule]) => rule._def.tag !== "lazy")) : void 0;
7422
8883
  const grammarTrivia = opts?.trivia;
7423
8884
  const grammarScanSkip = opts?.scanSkip ?? ruleMap.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
8885
+ const grammarHostMode = opts?.hostMode ?? ruleMap.map(([, r]) => r._meta.grammarHostMode).find(Boolean);
7424
8886
  const ctx = {
7425
8887
  vars: 0,
7426
8888
  indent: 1,
@@ -7440,6 +8902,7 @@ function compileRuleMap(ruleMap, opts) {
7440
8902
  namedFnDecls: [],
7441
8903
  capturing: ruleMap.some(([, rule]) => hasNodeDef(rule)),
7442
8904
  recovery: opts?.recovery ?? false,
8905
+ hostMode: grammarHostMode ?? "ast",
7443
8906
  ...opts?.coverage ? { coverage: { plan: buildGrammarPlan(ruleMap.map(([, rule]) => rule), coverageWinners) } } : {},
7444
8907
  lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
7445
8908
  ...grammarTrivia ? { activeTrivia: grammarTrivia, triviaKindLabels: grammarTrivia._meta.triviaKindLabels } : {},
@@ -7530,15 +8993,22 @@ function compileRuleMap(ruleMap, opts) {
7530
8993
  return {
7531
8994
  keys: perEntry.map((e) => e.key),
7532
8995
  replacement,
8996
+ // Reported so the MACRO can stamp the emitted map, exactly as `fusedBody` stamps a
8997
+ // fused one. Without it a `rules()` artifact carries no mode and every driver-side
8998
+ // host check passes vacuously — see `withHostMode` in the plugin.
8999
+ hostMode: ctx.hostMode ?? "ast",
9000
+ hostBranchElided: !!ctx.hostBranchElided,
7533
9001
  ...ctx.coverage === void 0 ? {} : { coverageDefinitions: ctx.coverage.plan.definitions }
7534
9002
  };
7535
9003
  }
7536
9004
  function compileLinkable(ruleMapArg, ns, opts) {
7537
9005
  if (!ns) throw new Error("compileLinkable: ns must be a non-empty namespace");
7538
9006
  if (opts?.gating !== void 0) runGatingDiagnosticRules(ruleMapArg, opts.gating);
9007
+ if (opts?.duplication !== void 0) runDuplicationDiagnosticRules(ruleMapArg, opts.duplication);
7539
9008
  for (const [, rule] of ruleMapArg) markUnusedValues(rule);
7540
9009
  const grammarTrivia = opts?.trivia ?? ruleMapArg.map(([, r]) => r._meta.grammarTrivia).find(Boolean);
7541
9010
  const grammarScanSkip = opts?.scanSkip ?? ruleMapArg.map(([, r]) => r._meta.grammarScanSkip).find(Boolean);
9011
+ const grammarHostMode = opts?.hostMode ?? ruleMapArg.map(([, r]) => r._meta.grammarHostMode).find(Boolean);
7542
9012
  const ruleMap = ruleMapArg.filter(([, val]) => {
7543
9013
  const d = val._def;
7544
9014
  if (d.tag !== "lazy") return true;
@@ -7572,6 +9042,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
7572
9042
  // merely returning their scalar parse values.
7573
9043
  capturing: opts?.captureTerminals === true || ruleMap.some(([, rule]) => hasNodeDef(rule)),
7574
9044
  recovery: opts?.recovery ?? false,
9045
+ hostMode: grammarHostMode ?? "ast",
7575
9046
  ...opts?.coverage ? { coverage: { plan: opts.coverage } } : {},
7576
9047
  lazyUsage: analyzeLazyUsageMulti(ruleMap.map(([, rule]) => rule)),
7577
9048
  ns,
@@ -7622,7 +9093,7 @@ function compileLinkable(ruleMapArg, ns, opts) {
7622
9093
  scanExternal(resolved);
7623
9094
  return;
7624
9095
  }
7625
- for (const child of childrenOf(def)) scanExternal(child);
9096
+ for (const child of childrenOf2(def)) scanExternal(child);
7626
9097
  };
7627
9098
  for (const [, rule] of ruleMap) scanExternal(rule);
7628
9099
  }
@@ -7727,6 +9198,8 @@ function compileLinkable(ruleMapArg, ns, opts) {
7727
9198
  deps: ruleDependencies(ruleMap),
7728
9199
  needsEmptyTl: !!ctx.needsEmptyTl,
7729
9200
  needsHostReads: !!ctx.needsHostReads,
9201
+ hostMode: ctx.hostMode ?? "ast",
9202
+ hostBranchElided: !!ctx.hostBranchElided,
7730
9203
  hasDirectBuilders: ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
7731
9204
  isRecognitionOnly: !hasSemanticReduction(ruleMap.map(([, rule]) => rule), externalRefs),
7732
9205
  mfFns: mfSrcs ? [] : ctx.mapFns,
@@ -7749,7 +9222,7 @@ function hasExternalRuleRef(ruleMap) {
7749
9222
  if (resolved === void 0) return p._ruleName !== void 0;
7750
9223
  return visit(resolved);
7751
9224
  }
7752
- return childrenOf(def).some(visit);
9225
+ return childrenOf2(def).some(visit);
7753
9226
  };
7754
9227
  return ruleMap.some(([, rule]) => visit(rule));
7755
9228
  }
@@ -7771,7 +9244,7 @@ function hasSemanticReduction(roots, externalRefs) {
7771
9244
  return true;
7772
9245
  }
7773
9246
  }
7774
- return childrenOf(def).some(visit);
9247
+ return childrenOf2(def).some(visit);
7775
9248
  };
7776
9249
  return roots.some(visit);
7777
9250
  }
@@ -7840,7 +9313,7 @@ function withCtx(extra, combinator) {
7840
9313
  }
7841
9314
 
7842
9315
  // src/compiler/ir-serialize.ts
7843
- function childrenOf2(def) {
9316
+ function childrenOf3(def) {
7844
9317
  switch (def.tag) {
7845
9318
  case "sequence":
7846
9319
  case "choice":
@@ -7879,7 +9352,7 @@ function childrenOf2(def) {
7879
9352
  return [];
7880
9353
  }
7881
9354
  }
7882
- var ruleNameOf2 = (c) => c._ruleName;
9355
+ var ruleNameOf4 = (c) => c._ruleName;
7883
9356
  function lazyTarget(c) {
7884
9357
  if (c._def.tag !== "lazy") return null;
7885
9358
  try {
@@ -8061,12 +9534,16 @@ var Serializer = class {
8061
9534
  emitted = /* @__PURE__ */ new Set();
8062
9535
  decls = [];
8063
9536
  ruleValues = /* @__PURE__ */ new Set();
9537
+ localRuleNames = /* @__PURE__ */ new Map();
8064
9538
  ruleMap;
8065
9539
  scanSkip;
8066
9540
  constructor(ruleMap, scanSkip = []) {
8067
9541
  this.ruleMap = ruleMap;
8068
9542
  this.scanSkip = scanSkip;
8069
- for (const [, c] of ruleMap) this.ruleValues.add(this.body(c));
9543
+ for (const [name, c] of ruleMap) {
9544
+ this.localRuleNames.set(c, name);
9545
+ this.ruleValues.add(this.body(c));
9546
+ }
8070
9547
  }
8071
9548
  /** A grammar-level `scanSkip` unit is an opaque TERMINAL (a string, a balanced
8072
9549
  * bracket) — it must not reach a grammar-rule ref (`g[name]`), because the
@@ -8077,18 +9554,18 @@ var Serializer = class {
8077
9554
  if (seen.has(c)) return;
8078
9555
  seen.add(c);
8079
9556
  if (c._def.tag === "lazy") {
8080
- if (ruleNameOf2(c) !== void 0) throw new Unserializable("scanSkip unit references a grammar rule");
9557
+ if (ruleNameOf4(c) !== void 0) throw new Unserializable("scanSkip unit references a grammar rule");
8081
9558
  const target = lazyTarget(c);
8082
9559
  if (target) this.assertNoRuleRef(target, seen);
8083
9560
  return;
8084
9561
  }
8085
- for (const child of childrenOf2(c._def)) this.assertNoRuleRef(child, seen);
9562
+ for (const child of childrenOf3(c._def)) this.assertNoRuleRef(child, seen);
8086
9563
  }
8087
9564
  /** A `rules()` map wraps each rule value in a named `lazy` proxy; the real body is
8088
9565
  * its target (compileLinkable unwraps the same way). A ref INSIDE a body stays a
8089
9566
  * `g[name]` ref — only the top-level entry is unwrapped. */
8090
9567
  body(c) {
8091
- return c._def.tag === "lazy" && ruleNameOf2(c) !== void 0 ? lazyTarget(c) ?? c : c;
9568
+ return c._def.tag === "lazy" && ruleNameOf4(c) !== void 0 ? lazyTarget(c) ?? c : c;
8092
9569
  }
8093
9570
  run() {
8094
9571
  for (const [, c] of this.ruleMap) this.analyze(this.body(c), /* @__PURE__ */ new Set());
@@ -8130,7 +9607,7 @@ ${outerDecls.map((d) => " " + d).join("\n")}
8130
9607
  analyze(c, active) {
8131
9608
  const def = c._def;
8132
9609
  if (def.tag === "lazy") {
8133
- if (ruleNameOf2(c) !== void 0) return;
9610
+ if (ruleNameOf4(c) !== void 0) return;
8134
9611
  const target = lazyTarget(c);
8135
9612
  if (!target) throw new Unserializable("unresolved unnamed ref");
8136
9613
  if (active.has(target)) {
@@ -8146,7 +9623,7 @@ ${outerDecls.map((d) => " " + d).join("\n")}
8146
9623
  this.counts.set(c, (this.counts.get(c) ?? 0) + 1);
8147
9624
  if ((this.counts.get(c) ?? 0) > 1) return;
8148
9625
  active.add(c);
8149
- for (const child of childrenOf2(def)) this.analyze(child, active);
9626
+ for (const child of childrenOf3(def)) this.analyze(child, active);
8150
9627
  active.delete(c);
8151
9628
  }
8152
9629
  /** Reference `c`: a const var if it has one (emitting its decl first), else inline.
@@ -8182,7 +9659,8 @@ ${outerDecls.map((d) => " " + d).join("\n")}
8182
9659
  const kid = (x) => this.ref(x, selfOf);
8183
9660
  switch (def.tag) {
8184
9661
  case "lazy": {
8185
- if (ruleNameOf2(c) !== void 0) return `g[${JSON.stringify(ruleNameOf2(c))}]`;
9662
+ const name = this.localRuleNames.get(c) ?? ruleNameOf4(c);
9663
+ if (name !== void 0) return `g[${JSON.stringify(name)}]`;
8186
9664
  const target = lazyTarget(c);
8187
9665
  if (selfOf && target === selfOf.comb) return selfOf.var;
8188
9666
  return this.ref(target, selfOf);
@@ -8356,7 +9834,23 @@ function fusedBody(pieces) {
8356
9834
  ...[...winner].map(([k, p]) => p.ruleFns.get(k))
8357
9835
  ];
8358
9836
  const wrapperEntries = [...winner].map(([k, p]) => `${JSON.stringify(k)}: ${p.wrappers.get(k)}`);
8359
- const rawBody = [...lines, "return {", wrapperEntries.join(",\n"), "}"].join("\n");
9837
+ const { mode, elided } = hostModeOfPieces(pieces);
9838
+ const rawBody = [
9839
+ ...lines,
9840
+ "const _map = {",
9841
+ wrapperEntries.join(",\n"),
9842
+ "}",
9843
+ `Object.defineProperty(_map, Symbol.for(${JSON.stringify(FUSED_HOST_MODE.description)}), { value: ${JSON.stringify(mode)}, enumerable: false })`,
9844
+ `Object.defineProperty(_map, Symbol.for(${JSON.stringify(FUSED_HOST_ELIDED.description)}), { value: ${JSON.stringify(elided)}, enumerable: false })`,
9845
+ // …and on each rule FUNCTION, because `run(map.Rule, …)` is handed the rule, not the
9846
+ // map, and would otherwise have nothing to check against. Done once at fuse time,
9847
+ // before any rule has been called, so it costs nothing per parse.
9848
+ "for (const _k of Object.keys(_map)) {",
9849
+ ` Object.defineProperty(_map[_k], Symbol.for(${JSON.stringify(FUSED_HOST_MODE.description)}), { value: ${JSON.stringify(mode)}, enumerable: false })`,
9850
+ ` Object.defineProperty(_map[_k], Symbol.for(${JSON.stringify(FUSED_HOST_ELIDED.description)}), { value: ${JSON.stringify(elided)}, enumerable: false })`,
9851
+ "}",
9852
+ "return _map"
9853
+ ].join("\n");
8360
9854
  const ANY2 = { kind: "any" };
8361
9855
  const EMPTY3 = { kind: "empty" };
8362
9856
  const recipes = /* @__PURE__ */ new Map();
@@ -8415,6 +9909,16 @@ function fusedBody(pieces) {
8415
9909
  }
8416
9910
  return { body, env };
8417
9911
  }
9912
+ function hostModeOfPieces(pieces) {
9913
+ const mode = pieces.find((p) => p.hostMode !== void 0 && p.hostMode !== "ast")?.hostMode ?? "ast";
9914
+ const elided = pieces.filter((p) => p.hostBranchElided === true);
9915
+ if (mode === "cst" && elided.length > 0) {
9916
+ throw new Error(
9917
+ `compose: cannot build a positioned-CST artifact from pieces that were compiled for host mode "ast" \u2014 ${elided.map((p) => `"${p.ns}"`).join(", ")} ${elided.length === 1 ? "dropped its" : "dropped their"} direct builders' CST branch, so ${elided.length === 1 ? "it" : "they"} would return AST objects inside the CST. An already-compiled piece keeps the mode it was built with; only pieces carried as IR are re-lowered by this compose. Re-compile ${elided.length === 1 ? "it" : "them"} with hostMode: 'cst', or pass the source grammar so it can be re-lowered.`
9918
+ );
9919
+ }
9920
+ return { mode, elided: elided.length > 0 };
9921
+ }
8418
9922
  function emitFusedSource(pieces) {
8419
9923
  const { body, env } = fusedBody(pieces);
8420
9924
  if (Object.keys(env).length > 0) {
@@ -8427,11 +9931,23 @@ ${body}
8427
9931
  function isIRPiece(p) {
8428
9932
  return !!p && typeof p === "object" && typeof p.ir === "string" && typeof p.ns === "string" && !("ruleFns" in p);
8429
9933
  }
8430
- function materializePiece(p, trivia2, captureTerminals = false) {
9934
+ function once(fn) {
9935
+ let done = false;
9936
+ let value;
9937
+ return () => {
9938
+ if (!done) {
9939
+ value = fn();
9940
+ done = true;
9941
+ }
9942
+ return value;
9943
+ };
9944
+ }
9945
+ function materializePiece(p, trivia2, captureTerminals = false, hostMode) {
8431
9946
  if (!isIRPiece(p)) return p;
8432
9947
  const pieces = compileLinkable(evalRuleMapIR(p.ir), p.ns, {
8433
9948
  ...trivia2 ? { trivia: trivia2 } : {},
8434
- ...captureTerminals ? { captureTerminals: true } : {}
9949
+ ...captureTerminals ? { captureTerminals: true } : {},
9950
+ ...hostMode ? { hostMode } : {}
8435
9951
  });
8436
9952
  if (!pieces) throw new Error(`compose: carried IR for ns "${p.ns}" could not be re-lowered`);
8437
9953
  return pieces;
@@ -8654,6 +10170,25 @@ function stripTsFromSource(node2, code) {
8654
10170
  function isWordFactory(v2) {
8655
10171
  return !!v2 && typeof v2 === "object" && v2.tag === "wordFactory";
8656
10172
  }
10173
+ function wordFactoryFromArgs(args, scope, code, mfs) {
10174
+ const [boundaryOrOptsArg, optsArg] = args;
10175
+ if (boundaryOrOptsArg?.type === "SpreadElement" || optsArg?.type === "SpreadElement") return null;
10176
+ const rawBoundaryOrOpts = boundaryOrOptsArg === void 0 ? void 0 : anyValue(boundaryOrOptsArg, scope, code, mfs);
10177
+ const boundaryOrOpts = rawBoundaryOrOpts === void 0 ? "_0-9A-Za-z" : rawBoundaryOrOpts;
10178
+ const opts = optsArg === void 0 ? void 0 : anyValue(optsArg, scope, code, mfs);
10179
+ if (typeof boundaryOrOpts === "string") {
10180
+ if (opts !== void 0 && (typeof opts !== "object" || opts === null || Array.isArray(opts))) return null;
10181
+ const caseInsensitive2 = typeof opts === "object" && opts !== null && "caseInsensitive" in opts ? opts.caseInsensitive : false;
10182
+ if (typeof caseInsensitive2 !== "boolean") return null;
10183
+ return { tag: "wordFactory", boundary: boundaryOrOpts, caseInsensitive: caseInsensitive2 };
10184
+ }
10185
+ if (typeof boundaryOrOpts !== "object" || boundaryOrOpts === null || Array.isArray(boundaryOrOpts) || opts !== void 0) {
10186
+ return null;
10187
+ }
10188
+ const caseInsensitive = "caseInsensitive" in boundaryOrOpts ? boundaryOrOpts.caseInsensitive : false;
10189
+ if (typeof caseInsensitive !== "boolean") return null;
10190
+ return { tag: "wordFactory", boundary: "_0-9A-Za-z", caseInsensitive };
10191
+ }
8657
10192
  var SUPPORTED = {
8658
10193
  literal: (...a) => literal(a[0], a[1]),
8659
10194
  regex: (...a) => regex(a[0], a[1]),
@@ -8737,14 +10272,13 @@ function exprToCombi(node2, scope, code, mfs) {
8737
10272
  if (node2.type !== "CallExpression") return null;
8738
10273
  const callee = node2.callee;
8739
10274
  if (callee.type === "CallExpression" && callee.callee.type === "Identifier" && callee.callee.name === "makeWord") {
8740
- const boundaryArg = callee.arguments[0];
8741
10275
  const strArg = node2.arguments[0];
8742
10276
  if (!strArg || strArg.type === "SpreadElement") return null;
8743
- const boundary = boundaryArg && boundaryArg.type !== "SpreadElement" ? anyValue(boundaryArg, scope, code, mfs) : "_0-9A-Za-z";
10277
+ const factory2 = wordFactoryFromArgs(callee.arguments, scope, code, mfs);
8744
10278
  const str = anyValue(strArg, scope, code, mfs);
8745
- if (typeof boundary !== "string" || typeof str !== "string") return null;
10279
+ if (factory2 === null || typeof str !== "string") return null;
8746
10280
  try {
8747
- return word(str, boundary);
10281
+ return word(str, factory2.boundary, { caseInsensitive: factory2.caseInsensitive });
8748
10282
  } catch {
8749
10283
  return null;
8750
10284
  }
@@ -8757,7 +10291,7 @@ function exprToCombi(node2, scope, code, mfs) {
8757
10291
  const str = anyValue(strArg, scope, code, mfs);
8758
10292
  if (typeof str !== "string") return null;
8759
10293
  try {
8760
- return word(str, factory2.boundary);
10294
+ return word(str, factory2.boundary, { caseInsensitive: factory2.caseInsensitive });
8761
10295
  } catch {
8762
10296
  return null;
8763
10297
  }
@@ -9067,10 +10601,7 @@ function anyValue(node2, scope, code, mfs) {
9067
10601
  if (node2.type === "CallExpression") {
9068
10602
  const callee = node2.callee;
9069
10603
  if (callee.type === "Identifier" && callee.name === "makeWord") {
9070
- const boundaryArg = node2.arguments[0];
9071
- const boundary = boundaryArg && boundaryArg.type !== "SpreadElement" ? anyValue(boundaryArg, scope, code, mfs) : "_0-9A-Za-z";
9072
- if (typeof boundary !== "string") return null;
9073
- return { tag: "wordFactory", boundary };
10604
+ return wordFactoryFromArgs(node2.arguments, scope, code, mfs);
9074
10605
  }
9075
10606
  return exprToCombi(node2, scope, code, mfs);
9076
10607
  }
@@ -9080,10 +10611,7 @@ function evaluateWordFactory(node2, scope, code) {
9080
10611
  if (node2.type !== "CallExpression") return null;
9081
10612
  const callee = node2.callee;
9082
10613
  if (callee.type !== "Identifier" || callee.name !== "makeWord") return null;
9083
- const boundaryArg = node2.arguments[0];
9084
- const boundary = boundaryArg && boundaryArg.type !== "SpreadElement" ? anyValue(boundaryArg, scope, code) : "_0-9A-Za-z";
9085
- if (typeof boundary !== "string") return null;
9086
- return { tag: "wordFactory", boundary };
10614
+ return wordFactoryFromArgs(node2.arguments, scope, code);
9087
10615
  }
9088
10616
  function evaluateExpr(node2, scope, code, mapFnSources) {
9089
10617
  return exprToCombi(node2, scope, code, mapFnSources);
@@ -9178,6 +10706,11 @@ function evaluateParserFactory(factoryNode, scope, code, mapFnSources) {
9178
10706
  r._ruleName = name;
9179
10707
  if (r._def.tag === "node" && r._def.type === void 0) r._def.type = name;
9180
10708
  };
10709
+ const ruleNameOf5 = (r) => r._ruleName;
10710
+ const isNamedRuleRefForAnotherRule2 = (r, key) => {
10711
+ const name = r._def.tag === "lazy" ? ruleNameOf5(r) : void 0;
10712
+ return name !== void 0 && name !== key;
10713
+ };
9181
10714
  for (const key of keys) {
9182
10715
  if (!ruleRefs.has(key)) {
9183
10716
  const r = ref();
@@ -9208,12 +10741,15 @@ function evaluateParserFactory(factoryNode, scope, code, mapFnSources) {
9208
10741
  for (const [key, e] of finalByKey) {
9209
10742
  const val = anyValue(e.value, e.scope, e.code, mapFnSources);
9210
10743
  if (!isCombinator(val)) return null;
9211
- tagRef(val, key);
10744
+ if (val === ruleRefs.get(key)) return null;
10745
+ if (!isNamedRuleRefForAnotherRule2(val, key)) {
10746
+ tagRef(val, key);
10747
+ }
9212
10748
  ruleRefs.get(key).define(val);
9213
10749
  }
9214
10750
  return ruleRefs;
9215
10751
  }
9216
- function isDefinableRef(v2) {
10752
+ function isDefinableRef2(v2) {
9217
10753
  return isCombinator(v2) && typeof v2.define === "function" && v2._def.tag === "lazy";
9218
10754
  }
9219
10755
  function evaluateRefDeclaration(init, name, scope) {
@@ -9235,7 +10771,7 @@ function applyDefineStatement(callExpr, scope, code) {
9235
10771
  if (mem.object.type !== "Identifier" || !mem.object.name) return false;
9236
10772
  const target = scope.get(mem.object.name);
9237
10773
  const refCombi = isScopeEntry(target) ? target.combi : isCombinator(target) ? target : null;
9238
- if (!refCombi || !isDefinableRef(refCombi)) return false;
10774
+ if (!refCombi || !isDefinableRef2(refCombi)) return false;
9239
10775
  const args = callExpr.arguments;
9240
10776
  if (args.length !== 1 || args[0].type === "SpreadElement") return false;
9241
10777
  const inner = anyValue(args[0], scope, code, []);
@@ -9531,6 +11067,23 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9531
11067
  for (const n of names) allNames.add(n);
9532
11068
  }
9533
11069
  if (macroImports.length === 0) return null;
11070
+ const factoryDecls = /* @__PURE__ */ new Map();
11071
+ for (const stmt of body) {
11072
+ const decl = stmt.type === "ExportNamedDeclaration" ? stmt.declaration : stmt;
11073
+ if (decl?.type !== "VariableDeclaration") continue;
11074
+ if (decl.kind !== "const") continue;
11075
+ for (const d of decl.declarations) {
11076
+ const id2 = d.id;
11077
+ const init = d.init;
11078
+ if (id2.type !== "Identifier" || !id2.name || !init) continue;
11079
+ if (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression") {
11080
+ factoryDecls.set(id2.name, {
11081
+ fn: init,
11082
+ declaredAt: d.end
11083
+ });
11084
+ }
11085
+ }
11086
+ }
9534
11087
  const scope = /* @__PURE__ */ new Map();
9535
11088
  const replacements = [];
9536
11089
  const warnings = [];
@@ -9553,20 +11106,28 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9553
11106
  const arg0 = args[0];
9554
11107
  const arg1 = args[1];
9555
11108
  const optionsFirst = arg0?.type === "ObjectExpression";
9556
- const factoryArg = optionsFirst ? arg1 : arg0;
9557
- if (!factoryArg) {
11109
+ const factoryArgRaw = optionsFirst ? arg1 : arg0;
11110
+ if (!factoryArgRaw) {
9558
11111
  warn(init.start, `${label2}: rules() needs a factory argument`);
9559
11112
  return null;
9560
11113
  }
11114
+ const namedFactory = factoryArgRaw.type === "Identifier" ? factoryDecls.get(factoryArgRaw.name) : void 0;
11115
+ const factoryArg = namedFactory !== void 0 && namedFactory.declaredAt <= init.start ? namedFactory.fn : factoryArgRaw;
11116
+ const optionsArg = optionsFirst ? arg0 : arg1;
11117
+ const optionValue = (name) => optionsArg?.type === "ObjectExpression" ? (optionsArg.properties ?? []).find(
11118
+ (p) => p.key?.name === name
11119
+ )?.value : void 0;
11120
+ const hostModeValue = optionValue("hostMode");
11121
+ const gHostMode = hostModeValue?.type === "Literal" ? hostModeValue.value : void 0;
11122
+ if (hostModeValue !== void 0 && gHostMode !== "ast" && gHostMode !== "cst") {
11123
+ warn(init.start, `${label2}: rules({ hostMode }) must be the literal 'ast' or 'cst'`);
11124
+ return null;
11125
+ }
9561
11126
  const ruleMap = evaluateParserFactory(factoryArg, scope, code, []);
9562
11127
  if (!ruleMap) {
9563
11128
  warn(init.start, `${label2}: rules(...) factory isn't statically evaluable`);
9564
11129
  return null;
9565
11130
  }
9566
- const optionsArg = optionsFirst ? arg0 : arg1;
9567
- const optionValue = (name) => optionsArg?.type === "ObjectExpression" ? (optionsArg.properties ?? []).find(
9568
- (p) => p.key?.name === name
9569
- )?.value : void 0;
9570
11131
  const triviaValue = optionValue("trivia");
9571
11132
  const gTrivia = triviaValue ? evaluateExpr(triviaValue, scope, code, []) : void 0;
9572
11133
  const scanSkipValue = optionValue("scanSkip");
@@ -9579,13 +11140,29 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9579
11140
  }
9580
11141
  }
9581
11142
  }
11143
+ if (gHostMode === "cst") {
11144
+ for (const rule of ruleMap.values()) {
11145
+ if (rule && !rule._meta.isTrivia) {
11146
+ ;
11147
+ rule._meta.grammarHostMode = "cst";
11148
+ }
11149
+ }
11150
+ }
9582
11151
  return { ruleMap, ...gTrivia ? { trivia: gTrivia } : {}, ...gScanSkip ? { scanSkip: gScanSkip } : {} };
9583
11152
  };
9584
11153
  const compileRulesFactory = (init, label2) => {
9585
11154
  const evaluated = evaluateRulesFactory(init, label2);
9586
11155
  if (!evaluated) return null;
9587
11156
  const compiled = compileRuleMap([...evaluated.ruleMap], { ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, ...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {}, recovery, coverage: grammarCoverage });
9588
- return { replacement: compiled?.replacement ?? null, ruleMap: evaluated.ruleMap, ...compiled?.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {}, ...evaluated.trivia ? { trivia: evaluated.trivia } : {}, ...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {} };
11157
+ return {
11158
+ replacement: compiled?.replacement ?? null,
11159
+ ruleMap: evaluated.ruleMap,
11160
+ hostMode: compiled?.hostMode ?? "ast",
11161
+ hostBranchElided: compiled?.hostBranchElided ?? false,
11162
+ ...compiled?.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {},
11163
+ ...evaluated.trivia ? { trivia: evaluated.trivia } : {},
11164
+ ...evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {}
11165
+ };
9589
11166
  };
9590
11167
  const isRulesCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "rules";
9591
11168
  const isComposeCall = (init) => init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "compose";
@@ -9600,7 +11177,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9600
11177
  const mapLit = (m, stripVals = false) => `new Map([${[...m].map(
9601
11178
  ([k, v2]) => `[${JSON.stringify(k)}, ${JSON.stringify(stripVals && typeof v2 === "string" ? stripIndent(v2) : v2)}]`
9602
11179
  ).join(", ")}])`;
9603
- return `{ v: ${JSON.stringify(p.v)}, ns: ${JSON.stringify(p.ns)}, keys: ${JSON.stringify(p.keys)}, prelude: ${JSON.stringify(p.prelude.map(stripIndent))}, ruleFns: ${mapLit(p.ruleFns, true)}, wrappers: ${mapLit(p.wrappers, true)}, firstSets: ${mapLit(p.firstSets)}, nullable: ${p.nullable ? mapLit(p.nullable) : "new Map()"}, firstSetRecipes: ${p.firstSetRecipes ? mapLit(p.firstSetRecipes) : "new Map()"}, deps: ${mapLit(p.deps)}, needsEmptyTl: ${p.needsEmptyTl}, needsHostReads: ${p.needsHostReads}, hasDirectBuilders: ${p.hasDirectBuilders === true}, isRecognitionOnly: ${p.isRecognitionOnly === true}, mfFns: [], buildFns: [] }`;
11180
+ return `{ v: ${JSON.stringify(p.v)}, ns: ${JSON.stringify(p.ns)}, keys: ${JSON.stringify(p.keys)}, prelude: ${JSON.stringify(p.prelude.map(stripIndent))}, ruleFns: ${mapLit(p.ruleFns, true)}, wrappers: ${mapLit(p.wrappers, true)}, firstSets: ${mapLit(p.firstSets)}, nullable: ${p.nullable ? mapLit(p.nullable) : "new Map()"}, firstSetRecipes: ${p.firstSetRecipes ? mapLit(p.firstSetRecipes) : "new Map()"}, deps: ${mapLit(p.deps)}, hostMode: ${JSON.stringify(p.hostMode ?? "ast")}, hostBranchElided: ${p.hostBranchElided === true}, needsEmptyTl: ${p.needsEmptyTl}, needsHostReads: ${p.needsHostReads}, hasDirectBuilders: ${p.hasDirectBuilders === true}, isRecognitionOnly: ${p.isRecognitionOnly === true}, mfFns: [], buildFns: [] }`;
9604
11181
  };
9605
11182
  const isSpread = (it) => "__spreadLocal" in it;
9606
11183
  const isIR = (it) => "ir" in it && !("ruleFns" in it);
@@ -9608,6 +11185,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9608
11185
  const serializeList = (list) => `[${list.map(serializeItem).join(", ")}]`;
9609
11186
  const withCarriedPieces = (grammarExpr, list) => `/* @__PURE__ */ Object.defineProperty(${grammarExpr}, Symbol.for('parseman.composedPieces'), { value: ${serializeList(list)}, enumerable: false })`;
9610
11187
  const withLeafMarker = (grammarExpr) => `/* @__PURE__ */ Object.defineProperty(${grammarExpr}, Symbol.for('parseman.leafComposed'), { value: true, enumerable: false })`;
11188
+ const withHostMode = (grammarExpr, mode, elided) => `/* @__PURE__ */ (m => { for (const k of Object.keys(m)) { Object.defineProperty(m[k], Symbol.for('parseman.fusedHostMode'), { value: ${JSON.stringify(mode)}, enumerable: false }); Object.defineProperty(m[k], Symbol.for('parseman.fusedHostElided'), { value: ${JSON.stringify(elided)}, enumerable: false }) } Object.defineProperty(m, Symbol.for('parseman.fusedHostMode'), { value: ${JSON.stringify(mode)}, enumerable: false }); Object.defineProperty(m, Symbol.for('parseman.fusedHostElided'), { value: ${JSON.stringify(elided)}, enumerable: false }); return m })(${grammarExpr})`;
9611
11189
  const withCoverageDefinitions = (grammarExpr, definitions) => !grammarCoverage ? grammarExpr : `/* @__PURE__ */ Object.defineProperty(${grammarExpr}, Symbol.for('parseman.grammarCoverageDefinitions'), { value: Object.freeze(${JSON.stringify(definitions)}.map(Object.freeze)), enumerable: false })`;
9612
11190
  const emittedCoverageDefinitions = (source) => {
9613
11191
  const ids = /* @__PURE__ */ new Set();
@@ -9667,29 +11245,38 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9667
11245
  importedPiecesCache.set(localName, result2);
9668
11246
  return result2;
9669
11247
  };
9670
- const materializeCarried = (items, composing, captureTerminals = false) => {
11248
+ const materializeCarried = (items, composing, captureTerminals = false, hostMode) => {
9671
11249
  const out = [];
9672
11250
  for (const it of items) {
9673
11251
  if (isSpread(it)) {
9674
- for (const p of importedPieces(it.__spreadLocal) ?? []) out.push(materializePiece(p, composing, captureTerminals));
11252
+ for (const p of importedPieces(it.__spreadLocal) ?? []) out.push(materializePiece(p, composing, captureTerminals, hostMode));
9675
11253
  } else if (isIR(it)) {
9676
- out.push(materializePiece(it, composing, captureTerminals));
11254
+ out.push(materializePiece(it, composing, captureTerminals, hostMode));
9677
11255
  } else {
9678
11256
  out.push(it);
9679
11257
  }
9680
11258
  }
9681
11259
  return out;
9682
11260
  };
9683
- const carriedRuleMaps = (items) => {
9684
- const out = [];
11261
+ const carriedRuleMapsDetailed = (items) => {
11262
+ const maps = [];
11263
+ const opaque = [];
9685
11264
  const add = (it) => {
9686
- if (isIR(it)) out.push(evalRuleMapIR(it.ir));
11265
+ if (isIR(it)) {
11266
+ maps.push(evalRuleMapIR(it.ir));
11267
+ return;
11268
+ }
11269
+ const o = it;
11270
+ opaque.push({
11271
+ ns: o.ns ?? "<unknown>",
11272
+ ruleNames: o.ruleFns instanceof Map ? [...o.ruleFns.keys()] : []
11273
+ });
9687
11274
  };
9688
11275
  for (const it of items) {
9689
11276
  if (isSpread(it)) for (const p of importedPieces(it.__spreadLocal) ?? []) add(p);
9690
11277
  else add(it);
9691
11278
  }
9692
- return out;
11279
+ return { maps, opaque };
9693
11280
  };
9694
11281
  const materializeLeafCoverage = (items, localRules, localNs, composing, captureTerminals = false) => {
9695
11282
  const sources = [];
@@ -9727,6 +11314,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9727
11314
  const ns = nsFor(label2);
9728
11315
  const ir = serializeRuleMap(entries, scanSkip);
9729
11316
  if (ir) return { carried: [{ ns, ir }] };
11317
+ warn(0, `${label2}: rule map could not be serialized to IR; carrying FULL pieces instead. The artifact is correct but larger, and a downstream compose cannot re-lower it. Re-run with PARSEMAN_IR_DEBUG=1 to print the exact combinator that blocked serialization.`);
9730
11318
  const p = compileLinkable(entries, ns, { ...composing ? { trivia: composing } : {}, ...scanSkip ? { scanSkip } : {}, recovery });
9731
11319
  return p ? { carried: [p] } : null;
9732
11320
  };
@@ -9814,6 +11402,15 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9814
11402
  return null;
9815
11403
  }
9816
11404
  const elements = arr.elements;
11405
+ const cOptions = init.arguments[1];
11406
+ const cHostModeValue = cOptions?.type === "ObjectExpression" ? (cOptions.properties ?? []).find(
11407
+ (p) => p.key?.name === "hostMode"
11408
+ )?.value : void 0;
11409
+ const cHostMode = cHostModeValue?.type === "Literal" ? cHostModeValue.value : void 0;
11410
+ if (cHostModeValue !== void 0 && cHostMode !== "ast" && cHostMode !== "cst") {
11411
+ warn(init.start, "compose({ hostMode }) must be the literal 'ast' or 'cst'");
11412
+ return null;
11413
+ }
9817
11414
  const composing = composingTrivia(elements);
9818
11415
  const carried = [];
9819
11416
  for (let i = 0; i < elements.length; i++) {
@@ -9824,8 +11421,13 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9824
11421
  }
9825
11422
  carried.push(...r.carried);
9826
11423
  }
9827
- runFusedGatingDiagnostic(() => carriedRuleMaps(carried));
9828
- const pieces = materializeCarried(carried, composing);
11424
+ const detailed = once(() => carriedRuleMapsDetailed(carried));
11425
+ runFusedGatingDiagnostic(
11426
+ () => detailed().maps,
11427
+ void 0,
11428
+ () => detailed().opaque
11429
+ );
11430
+ const pieces = materializeCarried(carried, composing, false, cHostMode);
9829
11431
  try {
9830
11432
  return { replacement: emitFusedSource(pieces), carried, ...composing ? { trivia: composing } : {} };
9831
11433
  } catch (e) {
@@ -9873,7 +11475,12 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9873
11475
  }
9874
11476
  try {
9875
11477
  const localNs = nsFor(`composeLeaf${init.start}`);
9876
- runFusedGatingDiagnostic(() => [...carriedRuleMaps(carried), [...localRules]]);
11478
+ const detailedLeaf = once(() => carriedRuleMapsDetailed(carried));
11479
+ runFusedGatingDiagnostic(
11480
+ () => [...detailedLeaf().maps, [...localRules]],
11481
+ void 0,
11482
+ () => detailedLeaf().opaque
11483
+ );
9877
11484
  const plainLocalPiece = compileLinkable([...localRules], localNs, { ...composing ? { trivia: composing } : {}, ...localScanSkip ? { scanSkip: localScanSkip } : {}, recovery });
9878
11485
  if (!plainLocalPiece) {
9879
11486
  warn(init.start, "composeLeaf(): local rules could not be statically compiled");
@@ -9994,6 +11601,9 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
9994
11601
  source,
9995
11602
  compiledRules.coverageDefinitions?.length ? compiledRules.coverageDefinitions : emittedCoverageDefinitions(source)
9996
11603
  );
11604
+ if (compiledRules.replacement !== null) {
11605
+ replacement = withHostMode(replacement, compiledRules.hostMode, compiledRules.hostBranchElided);
11606
+ }
9997
11607
  if (exportPrefix && pieces && !pieces.mfFns.length && !pieces.buildFns.length) {
9998
11608
  const ir = serializeRuleMap([...compiledRules.ruleMap], compiledRules.scanSkip);
9999
11609
  replacement = withCarriedPieces(replacement, [ir ? { ns, ir } : pieces]);
@@ -10036,6 +11646,7 @@ function transformMacro(code, id, moduleAliases = /* @__PURE__ */ new Set([PARSE
10036
11646
  scope.set(varName, combiArray);
10037
11647
  continue;
10038
11648
  }
11649
+ if (factoryDecls.has(varName)) continue;
10039
11650
  warn(init.start, `"${varName}" references a parseman macro import but isn't a statically-evaluable combinator`);
10040
11651
  continue;
10041
11652
  }