parseman 0.38.0 → 0.39.1

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 (40) hide show
  1. package/dist/analysis/duplication.d.ts.map +1 -1
  2. package/dist/analysis/gating.d.ts.map +1 -1
  3. package/dist/combinators/attempt.d.ts.map +1 -1
  4. package/dist/combinators/choice.d.ts.map +1 -1
  5. package/dist/combinators/dispatch.d.ts +53 -0
  6. package/dist/combinators/dispatch.d.ts.map +1 -0
  7. package/dist/combinators/expect.d.ts.map +1 -1
  8. package/dist/combinators/first-set.d.ts.map +1 -1
  9. package/dist/combinators/repeat.d.ts.map +1 -1
  10. package/dist/compiler/codegen.d.ts.map +1 -1
  11. package/dist/compiler/fields.d.ts.map +1 -1
  12. package/dist/compiler/grammar-coverage-ids.d.ts +2 -1
  13. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  14. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  15. package/dist/compiler/value-usage.d.ts.map +1 -1
  16. package/dist/coverage.d.ts.map +1 -1
  17. package/dist/index.cjs +772 -33
  18. package/dist/index.cjs.map +4 -4
  19. package/dist/index.d.ts +3 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +772 -33
  22. package/dist/index.js.map +4 -4
  23. package/dist/language-service/index.cjs.map +2 -2
  24. package/dist/language-service/index.js.map +2 -2
  25. package/dist/plugin/evaluator.d.ts +6 -0
  26. package/dist/plugin/evaluator.d.ts.map +1 -1
  27. package/dist/plugin/index.cjs +840 -33
  28. package/dist/plugin/index.cjs.map +4 -4
  29. package/dist/plugin/index.d.ts.map +1 -1
  30. package/dist/plugin/index.js +840 -33
  31. package/dist/plugin/index.js.map +4 -4
  32. package/dist/spec/index.cjs +294 -1
  33. package/dist/spec/index.cjs.map +4 -4
  34. package/dist/spec/index.js +294 -1
  35. package/dist/spec/index.js.map +4 -4
  36. package/dist/spec/model.d.ts.map +1 -1
  37. package/dist/types.d.ts +37 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/version.d.ts +1 -1
  40. package/package.json +1 -1
@@ -82,6 +82,8 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set(), resolve) {
82
82
  return d.value.length === 0;
83
83
  case "keywords":
84
84
  return false;
85
+ case "routed":
86
+ return false;
85
87
  case "regex":
86
88
  try {
87
89
  const m = new RegExp(d.source).exec("");
@@ -108,6 +110,8 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set(), resolve) {
108
110
  return d.parsers.every(me);
109
111
  case "choice":
110
112
  return d.parsers.some(me);
113
+ case "dispatch":
114
+ return me(d.selector);
111
115
  case "transform":
112
116
  case "label":
113
117
  case "trivia":
@@ -181,6 +185,8 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set(), resolve) {
181
185
  case "keywords":
182
186
  return p._meta.firstSet;
183
187
  // terminals: no refs, cached set is exact
188
+ case "dispatch":
189
+ return fs(d.selector);
184
190
  case "lazy":
185
191
  try {
186
192
  return fs(d.thunk());
@@ -313,6 +319,12 @@ function markUnusedValues(root) {
313
319
  case "choice":
314
320
  for (const p of def.parsers) visit(p, consumed);
315
321
  return;
322
+ case "dispatch":
323
+ visit(def.selector, consumed);
324
+ for (const entry of def.cases) visit(entry.parser, consumed);
325
+ if (def.matchers) for (const entry of def.matchers) visit(entry.parser, consumed);
326
+ if (def.otherwise) visit(def.otherwise, consumed);
327
+ return;
316
328
  case "node":
317
329
  visit(def.parser, false);
318
330
  return;
@@ -723,6 +735,8 @@ function derive(c, seen) {
723
735
  return [def.label];
724
736
  case "choice":
725
737
  return def.parsers.flatMap(deriveExpected2);
738
+ case "dispatch":
739
+ return deriveExpected2(def.selector);
726
740
  // Union through the NULLABLE prefix, mirroring `sequenceFirstSet`. A leading
727
741
  // `optional(…)`/`many(…)` can match nothing, so the term after it is equally
728
742
  // able to fail first — deriving from term 0 alone named a token the parse
@@ -762,6 +776,8 @@ function derive(c, seen) {
762
776
  case "not":
763
777
  case "peek":
764
778
  return deriveExpected2(def.parser);
779
+ case "routed":
780
+ return ["routed()"];
765
781
  case "lazy": {
766
782
  const name = c._ruleName;
767
783
  if (seen.has(c)) return name ? [name] : [];
@@ -861,6 +877,7 @@ function choice(...args) {
861
877
  const result = parsers[idx].parse(input, pos, ctx);
862
878
  if (result.ok) return result;
863
879
  expected.push(...result.expected);
880
+ if (result.committed) return { ok: false, expected, span: { start: pos, end: pos }, committed: true };
864
881
  return { ok: false, expected, span: { start: pos, end: pos } };
865
882
  }
866
883
  return {
@@ -901,6 +918,7 @@ function choice(...args) {
901
918
  rollbackCstCapture(ctx, mark);
902
919
  if (logLen !== void 0 && ctx._triviaLog && ctx._triviaLog.length !== logLen) ctx._triviaLog.length = logLen;
903
920
  expected.push(...result.expected);
921
+ if (result.committed) return { ok: false, expected, span: result.span, committed: true };
904
922
  continue;
905
923
  }
906
924
  const checks = autoNot[i];
@@ -1851,6 +1869,7 @@ function repItem(combinator, input, cur, ctx, guardable) {
1851
1869
  const result = combinator.parse(input, pos, ctx);
1852
1870
  if (!result.ok) {
1853
1871
  rollbackTrivia(ctx, mark);
1872
+ if (result.committed) return { fail: result, failPos: pos };
1854
1873
  return { fail: result, failPos: pos };
1855
1874
  }
1856
1875
  if (result.span.end === pos) {
@@ -1894,6 +1913,7 @@ function many(combinator, opts = {}) {
1894
1913
  const item = repItem(combinator, input, cur, ctx, guardable);
1895
1914
  if (item === "stop") break;
1896
1915
  if ("fail" in item) {
1916
+ if (!item.fail.ok && item.fail.committed) return item.fail;
1897
1917
  const sync = ctx._tolerant ? ctx._sync : void 0;
1898
1918
  if (sync === void 0) break;
1899
1919
  if (matchesAt(sync, input, item.failPos, ctx)) break;
@@ -1938,6 +1958,7 @@ function atLeast(combinator, min, max) {
1938
1958
  while (count < min) {
1939
1959
  const item = repItem(combinator, input, cur, ctx, guardable);
1940
1960
  if (item === "stop" || "fail" in item) {
1961
+ if (item !== "stop" && !item.fail.ok && item.fail.committed) return item.fail;
1941
1962
  expected ??= deriveExpected(combinator);
1942
1963
  return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: cur, end: cur } };
1943
1964
  }
@@ -1950,6 +1971,7 @@ function atLeast(combinator, min, max) {
1950
1971
  const item = repItem(combinator, input, cur, ctx, guardable);
1951
1972
  if (item === "stop") break;
1952
1973
  if ("fail" in item) {
1974
+ if (!item.fail.ok && item.fail.committed) return item.fail;
1953
1975
  const sync = ctx._tolerant ? ctx._sync : void 0;
1954
1976
  if (sync === void 0) break;
1955
1977
  if (matchesAt(sync, input, item.failPos, ctx)) break;
@@ -1988,6 +2010,7 @@ function optional(combinator) {
1988
2010
  const result = combinator.parse(input, pos, ctx);
1989
2011
  if (result.ok) return result;
1990
2012
  rollbackTrivia(ctx, mark);
2013
+ if (result.committed) return result;
1991
2014
  return { ok: true, value: null, span: { start: pos, end: pos } };
1992
2015
  }
1993
2016
  };
@@ -2024,6 +2047,7 @@ function sepBy(combinator, separator, opts = {}) {
2024
2047
  values.push(first.value);
2025
2048
  cur = first.span.end;
2026
2049
  } else {
2050
+ if (first.committed) return first;
2027
2051
  const term = ctx._tolerant ? ctx._sync : void 0;
2028
2052
  if (term === void 0 || matchesAt(term, input, pos, ctx)) {
2029
2053
  if (min >= 1) return failAt2(pos);
@@ -2051,6 +2075,7 @@ function sepBy(combinator, separator, opts = {}) {
2051
2075
  const sep = separator.parse(input, sepPos, ctx);
2052
2076
  if (!sep.ok) {
2053
2077
  rollbackTrivia(ctx, loopMark);
2078
+ if (sep.committed) return sep;
2054
2079
  break;
2055
2080
  }
2056
2081
  const sepMark = trailing === "forbid" ? void 0 : saveTriviaMark(ctx);
@@ -2066,6 +2091,7 @@ function sepBy(combinator, separator, opts = {}) {
2066
2091
  }
2067
2092
  const next = combinator.parse(input, nextPos, ctx);
2068
2093
  if (!next.ok) {
2094
+ if (next.committed) return next;
2069
2095
  const term = ctx._tolerant ? ctx._sync : void 0;
2070
2096
  if (term !== void 0 && !matchesAt(term, input, nextPos, ctx)) {
2071
2097
  expected ??= deriveExpected(combinator);
@@ -2299,6 +2325,8 @@ function parserHasOwnFields(p, seen = /* @__PURE__ */ new Set()) {
2299
2325
  case "sequence":
2300
2326
  case "choice":
2301
2327
  return d.parsers.some((x) => parserHasOwnFields(x, seen));
2328
+ case "dispatch":
2329
+ return parserHasOwnFields(d.selector, seen) || d.cases.some((x) => parserHasOwnFields(x.parser, seen)) || (d.matchers ? d.matchers.some((entry) => parserHasOwnFields(entry.parser, seen)) : false) || (d.otherwise ? parserHasOwnFields(d.otherwise, seen) : false);
2302
2330
  case "sepBy":
2303
2331
  return parserHasOwnFields(d.parser, seen) || parserHasOwnFields(d.separator, seen);
2304
2332
  case "skip":
@@ -2413,6 +2441,245 @@ function keywords(words, opts = {}) {
2413
2441
  };
2414
2442
  }
2415
2443
 
2444
+ // src/combinators/dispatch.ts
2445
+ function parserUsesRouted(parser2, seen = /* @__PURE__ */ new Set()) {
2446
+ if (seen.has(parser2)) return false;
2447
+ seen.add(parser2);
2448
+ const def = parser2._def;
2449
+ switch (def.tag) {
2450
+ case "routed":
2451
+ return true;
2452
+ case "sequence":
2453
+ case "choice":
2454
+ return def.parsers.some((entry) => parserUsesRouted(entry, seen));
2455
+ case "dispatch":
2456
+ return false;
2457
+ case "sepBy":
2458
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.separator, seen);
2459
+ case "skip":
2460
+ return parserUsesRouted(def.main, seen) || parserUsesRouted(def.skipped, seen);
2461
+ case "grammar":
2462
+ return parserUsesRouted(def.parser, seen) || (def.triviaParser ? parserUsesRouted(def.triviaParser, seen) : false);
2463
+ case "scanTo":
2464
+ return parserUsesRouted(def.sentinel, seen) || def.skip.some((entry) => parserUsesRouted(entry, seen));
2465
+ case "recover":
2466
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.sentinel, seen);
2467
+ case "lazy":
2468
+ try {
2469
+ return parserUsesRouted(def.thunk(), seen);
2470
+ } catch {
2471
+ return false;
2472
+ }
2473
+ case "many":
2474
+ case "oneOrMore":
2475
+ case "optional":
2476
+ case "attempt":
2477
+ case "transform":
2478
+ case "trivia":
2479
+ case "token":
2480
+ case "leaf":
2481
+ case "label":
2482
+ case "field":
2483
+ case "not":
2484
+ case "peek":
2485
+ case "node":
2486
+ case "withCtx":
2487
+ case "expect":
2488
+ return parserUsesRouted(def.parser, seen);
2489
+ default:
2490
+ return false;
2491
+ }
2492
+ }
2493
+ function branchUsesRouted(branch) {
2494
+ return branch.usesRouted === true || parserUsesRouted(branch.parser);
2495
+ }
2496
+ function asciiFoldKey(key) {
2497
+ let out = "";
2498
+ for (let i = 0; i < key.length; i++) {
2499
+ const c = key.charCodeAt(i);
2500
+ out += String.fromCharCode(c >= 65 && c <= 90 ? c + 32 : c);
2501
+ }
2502
+ return out;
2503
+ }
2504
+ function matchesDispatchMatcher(value, matcher) {
2505
+ const candidate = matcher.caseInsensitive ? asciiFoldKey(value) : value;
2506
+ const expected = matcher.caseInsensitive ? asciiFoldKey(matcher.value) : matcher.value;
2507
+ switch (matcher.kind) {
2508
+ case "startsWith":
2509
+ return candidate.startsWith(expected);
2510
+ case "endsWith":
2511
+ return candidate.endsWith(expected);
2512
+ case "matches": {
2513
+ const flags = matcher.caseInsensitive && !matcher.flags?.includes("i") ? `${matcher.flags ?? ""}i` : matcher.flags ?? "";
2514
+ return new RegExp(matcher.value, flags).test(value);
2515
+ }
2516
+ }
2517
+ }
2518
+ function isStringMatcher(value) {
2519
+ if (typeof value !== "object" || value === null) return false;
2520
+ const kind = value.kind;
2521
+ return kind === "startsWith" || kind === "endsWith" || kind === "matches";
2522
+ }
2523
+ function fixedMatcher(kind, value) {
2524
+ if (typeof value !== "string") throw new TypeError(`parseman: ${kind}() value must be a string`);
2525
+ if (value.length === 0) throw new RangeError(`parseman: ${kind}() requires a non-empty value`);
2526
+ return { kind, value };
2527
+ }
2528
+ function startsWith(prefix) {
2529
+ return fixedMatcher("startsWith", prefix);
2530
+ }
2531
+ function endsWith(suffix) {
2532
+ return fixedMatcher("endsWith", suffix);
2533
+ }
2534
+ function matches(pattern) {
2535
+ if (!(pattern instanceof RegExp)) throw new TypeError("parseman: matches() requires a RegExp");
2536
+ if (pattern.flags.includes("g") || pattern.flags.includes("y")) {
2537
+ throw new TypeError("parseman: matches() does not accept global or sticky regex flags");
2538
+ }
2539
+ return { kind: "matches", value: pattern.source, flags: pattern.flags };
2540
+ }
2541
+ function when(key, parser2, opts = {}) {
2542
+ const usesRouted = parserUsesRouted(parser2);
2543
+ if (isStringMatcher(key)) {
2544
+ return { kind: "whenMatcher", matcher: key, parser: parser2, caseInsensitive: opts.caseInsensitive ?? false, usesRouted };
2545
+ }
2546
+ const keys = Array.isArray(key) ? [...key] : [key];
2547
+ if (keys.length === 0) throw new RangeError("parseman: when() requires at least one key");
2548
+ for (const item of keys) {
2549
+ if (typeof item !== "string") throw new TypeError("parseman: when() keys must be strings");
2550
+ }
2551
+ return { kind: "when", keys, parser: parser2, caseInsensitive: opts.caseInsensitive ?? false, usesRouted };
2552
+ }
2553
+ function otherwise(parser2) {
2554
+ return { kind: "otherwise", parser: parser2, usesRouted: parserUsesRouted(parser2) };
2555
+ }
2556
+ function routed() {
2557
+ return {
2558
+ _tag: "routed",
2559
+ _meta: { firstSet: { kind: "any" }, canMatchNewline: false, isTrivia: false },
2560
+ _def: { tag: "routed" },
2561
+ parse(_input, pos, ctx) {
2562
+ const item = ctx._routed;
2563
+ if (item === void 0) {
2564
+ return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2565
+ }
2566
+ if (pos !== item.span.start) {
2567
+ return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2568
+ }
2569
+ if (cstCaptureActive(ctx)) {
2570
+ pushCstLeaf(ctx, { _tag: "leaf", value: item.value, span: item.span });
2571
+ }
2572
+ return { ok: true, value: item.value, span: item.span };
2573
+ }
2574
+ };
2575
+ }
2576
+ function dispatch(selector, ...arms) {
2577
+ if (parserUsesRouted(selector)) {
2578
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
2579
+ }
2580
+ let fallback;
2581
+ let fallbackUsesRouted = false;
2582
+ const matchers = [];
2583
+ const cases = [];
2584
+ const seen = [];
2585
+ for (let i = 0; i < arms.length; i++) {
2586
+ const arm = arms[i];
2587
+ if (arm.kind === "otherwise") {
2588
+ if (fallback !== void 0) throw new Error("parseman: dispatch() accepts at most one otherwise() arm");
2589
+ if (i !== arms.length - 1) throw new Error("parseman: otherwise() must be the last dispatch() arm");
2590
+ fallback = arm.parser;
2591
+ fallbackUsesRouted = arm.usesRouted === true;
2592
+ continue;
2593
+ }
2594
+ if (arm.kind === "whenMatcher") {
2595
+ const matcher = arm.matcher;
2596
+ matchers.push({
2597
+ kind: matcher.kind,
2598
+ value: matcher.value,
2599
+ ...matcher.kind === "matches" ? { flags: matcher.flags } : {},
2600
+ parser: arm.parser,
2601
+ caseInsensitive: arm.caseInsensitive,
2602
+ ...arm.usesRouted === true ? { usesRouted: true } : {}
2603
+ });
2604
+ continue;
2605
+ }
2606
+ for (const key of arm.keys) {
2607
+ const folded = asciiFoldKey(key);
2608
+ for (const prior of seen) {
2609
+ const overlaps = arm.caseInsensitive || prior.caseInsensitive ? folded === prior.folded : key === prior.raw;
2610
+ if (overlaps) throw new Error(`parseman: duplicate dispatch key ${JSON.stringify(key)}`);
2611
+ }
2612
+ seen.push({ raw: key, folded, caseInsensitive: arm.caseInsensitive });
2613
+ }
2614
+ cases.push({
2615
+ keys: arm.keys,
2616
+ parser: arm.parser,
2617
+ caseInsensitive: arm.caseInsensitive,
2618
+ ...arm.usesRouted === true ? { usesRouted: true } : {}
2619
+ });
2620
+ }
2621
+ const meta = {
2622
+ firstSet: selector._meta.firstSet,
2623
+ canMatchNewline: selector._meta.canMatchNewline || cases.some((entry) => entry.parser._meta.canMatchNewline) || matchers.some((entry) => entry.parser._meta.canMatchNewline) || (fallback?._meta.canMatchNewline ?? false),
2624
+ isTrivia: false
2625
+ };
2626
+ const byKey = /* @__PURE__ */ new Map();
2627
+ const byFoldedKey = /* @__PURE__ */ new Map();
2628
+ for (const entry of cases) {
2629
+ for (const key of entry.keys) {
2630
+ if (entry.caseInsensitive) byFoldedKey.set(asciiFoldKey(key), entry);
2631
+ else byKey.set(key, entry);
2632
+ }
2633
+ }
2634
+ return {
2635
+ _tag: "dispatch",
2636
+ _meta: meta,
2637
+ _def: {
2638
+ tag: "dispatch",
2639
+ selector,
2640
+ cases,
2641
+ ...matchers.length === 0 ? {} : { matchers },
2642
+ ...fallback === void 0 ? {} : { otherwise: fallback, ...fallbackUsesRouted ? { otherwiseUsesRouted: true } : {} }
2643
+ },
2644
+ parse(input, pos, ctx) {
2645
+ if (parserUsesRouted(selector)) {
2646
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
2647
+ }
2648
+ const selectorMark = saveTriviaMark(ctx);
2649
+ const selected = selector.parse(input, pos, ctx);
2650
+ if (!selected.ok) return selected;
2651
+ const branch = byKey.get(selected.value) ?? (byFoldedKey.size === 0 ? void 0 : byFoldedKey.get(asciiFoldKey(selected.value))) ?? matchers.find((matcher) => matchesDispatchMatcher(selected.value, matcher)) ?? (fallback === void 0 ? void 0 : { keys: [], parser: fallback, caseInsensitive: false, usesRouted: fallbackUsesRouted });
2652
+ if (branch === void 0) {
2653
+ const expected = cases.flatMap((entry) => entry.keys.map((key) => JSON.stringify(key)));
2654
+ return { ok: false, expected, span: { start: selected.span.end, end: selected.span.end } };
2655
+ }
2656
+ const savedRouted = ctx._routed;
2657
+ const usesRouted = branchUsesRouted(branch);
2658
+ let mark = saveTriviaMark(ctx);
2659
+ if (usesRouted) {
2660
+ rollbackTrivia(ctx, selectorMark);
2661
+ mark = saveTriviaMark(ctx);
2662
+ ctx._routed = { value: selected.value, span: selected.span };
2663
+ }
2664
+ let result;
2665
+ try {
2666
+ result = branch.parser.parse(input, usesRouted ? pos : selected.span.end, ctx);
2667
+ } finally {
2668
+ if (usesRouted) ctx._routed = savedRouted;
2669
+ }
2670
+ if (!result.ok) {
2671
+ rollbackTrivia(ctx, mark);
2672
+ return { ...result, committed: true };
2673
+ }
2674
+ return {
2675
+ ok: true,
2676
+ value: [selected.value, result.value],
2677
+ span: { start: pos, end: result.span.end }
2678
+ };
2679
+ }
2680
+ };
2681
+ }
2682
+
2416
2683
  // src/combinators/attempt.ts
2417
2684
  function attempt(parser2) {
2418
2685
  const guardable = parser2._meta.firstSet.kind !== "any" && !matchesEmpty(parser2);
@@ -2435,6 +2702,7 @@ function attempt(parser2) {
2435
2702
  if (result.ok) return result;
2436
2703
  rollbackCstCapture(ctx, mark);
2437
2704
  if (log !== void 0 && ctx._triviaLog && ctx._triviaLog.length !== log) ctx._triviaLog.length = log;
2705
+ if (result.committed) return result;
2438
2706
  return { ok: false, expected: result.expected, span: { start: pos, end: pos } };
2439
2707
  }
2440
2708
  };
@@ -2844,6 +3112,13 @@ function evalRuleMapIR(ir) {
2844
3112
  "keywords",
2845
3113
  "sequence",
2846
3114
  "choice",
3115
+ "dispatch",
3116
+ "when",
3117
+ "startsWith",
3118
+ "endsWith",
3119
+ "matches",
3120
+ "otherwise",
3121
+ "routed",
2847
3122
  "attempt",
2848
3123
  "many",
2849
3124
  "oneOrMore",
@@ -2877,6 +3152,13 @@ function evalRuleMapIR(ir) {
2877
3152
  keywords,
2878
3153
  sequence,
2879
3154
  choice,
3155
+ dispatch,
3156
+ when,
3157
+ startsWith,
3158
+ endsWith,
3159
+ matches,
3160
+ otherwise,
3161
+ routed,
2880
3162
  attempt,
2881
3163
  many,
2882
3164
  oneOrMore,
@@ -3071,6 +3353,15 @@ var Builder = class {
3071
3353
  const items = def.parsers.map((p) => this.walk(p)).filter(nonEmpty);
3072
3354
  return flattenChoice(items);
3073
3355
  }
3356
+ case "dispatch": {
3357
+ const selector = this.walk(def.selector);
3358
+ const tails = [
3359
+ ...def.cases.map((entry) => this.walk(entry.parser)),
3360
+ ...def.matchers ? def.matchers.map((entry) => this.walk(entry.parser)) : [],
3361
+ ...def.otherwise ? [this.walk(def.otherwise)] : []
3362
+ ];
3363
+ return flattenSeq([selector, flattenChoice(tails)]);
3364
+ }
3074
3365
  // `max`/`min` are carried through so a BOUNDED repeat does not render as an
3075
3366
  // unbounded one — the spec is generated to be read as the truth about what
3076
3367
  // parses, and `many(x, { min: 3, max: 8 })` is not `x+`.
@@ -3100,6 +3391,8 @@ var Builder = class {
3100
3391
  return { kind: "not", item: this.walk(def.parser) };
3101
3392
  case "peek":
3102
3393
  return { kind: "peek", item: this.walk(def.parser) };
3394
+ case "routed":
3395
+ return { kind: "empty" };
3103
3396
  // Transparent semantic wrappers — render the inner syntax.
3104
3397
  case "transform":
3105
3398
  case "token":
@@ -3257,7 +3550,7 @@ function render(node2) {
3257
3550
  case "choice": {
3258
3551
  const text = node2.items.map((it) => {
3259
3552
  const r = render(it);
3260
- return wrap(r.text, r.level, ALT);
3553
+ return r.text.length === 0 ? "/* empty */" : wrap(r.text, r.level, ALT);
3261
3554
  }).join(" | ");
3262
3555
  return { text, level: ALT };
3263
3556
  }