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
@@ -48,6 +48,8 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set(), resolve) {
48
48
  return d.value.length === 0;
49
49
  case "keywords":
50
50
  return false;
51
+ case "routed":
52
+ return false;
51
53
  case "regex":
52
54
  try {
53
55
  const m = new RegExp(d.source).exec("");
@@ -74,6 +76,8 @@ function matchesEmpty(p, seen = /* @__PURE__ */ new Set(), resolve) {
74
76
  return d.parsers.every(me);
75
77
  case "choice":
76
78
  return d.parsers.some(me);
79
+ case "dispatch":
80
+ return me(d.selector);
77
81
  case "transform":
78
82
  case "label":
79
83
  case "trivia":
@@ -147,6 +151,8 @@ function firstSetOf(p, seen = /* @__PURE__ */ new Set(), resolve) {
147
151
  case "keywords":
148
152
  return p._meta.firstSet;
149
153
  // terminals: no refs, cached set is exact
154
+ case "dispatch":
155
+ return fs(d.selector);
150
156
  case "lazy":
151
157
  try {
152
158
  return fs(d.thunk());
@@ -279,6 +285,12 @@ function markUnusedValues(root) {
279
285
  case "choice":
280
286
  for (const p of def.parsers) visit(p, consumed);
281
287
  return;
288
+ case "dispatch":
289
+ visit(def.selector, consumed);
290
+ for (const entry of def.cases) visit(entry.parser, consumed);
291
+ if (def.matchers) for (const entry of def.matchers) visit(entry.parser, consumed);
292
+ if (def.otherwise) visit(def.otherwise, consumed);
293
+ return;
282
294
  case "node":
283
295
  visit(def.parser, false);
284
296
  return;
@@ -689,6 +701,8 @@ function derive(c, seen) {
689
701
  return [def.label];
690
702
  case "choice":
691
703
  return def.parsers.flatMap(deriveExpected2);
704
+ case "dispatch":
705
+ return deriveExpected2(def.selector);
692
706
  // Union through the NULLABLE prefix, mirroring `sequenceFirstSet`. A leading
693
707
  // `optional(…)`/`many(…)` can match nothing, so the term after it is equally
694
708
  // able to fail first — deriving from term 0 alone named a token the parse
@@ -728,6 +742,8 @@ function derive(c, seen) {
728
742
  case "not":
729
743
  case "peek":
730
744
  return deriveExpected2(def.parser);
745
+ case "routed":
746
+ return ["routed()"];
731
747
  case "lazy": {
732
748
  const name = c._ruleName;
733
749
  if (seen.has(c)) return name ? [name] : [];
@@ -827,6 +843,7 @@ function choice(...args) {
827
843
  const result = parsers[idx].parse(input, pos, ctx);
828
844
  if (result.ok) return result;
829
845
  expected.push(...result.expected);
846
+ if (result.committed) return { ok: false, expected, span: { start: pos, end: pos }, committed: true };
830
847
  return { ok: false, expected, span: { start: pos, end: pos } };
831
848
  }
832
849
  return {
@@ -867,6 +884,7 @@ function choice(...args) {
867
884
  rollbackCstCapture(ctx, mark);
868
885
  if (logLen !== void 0 && ctx._triviaLog && ctx._triviaLog.length !== logLen) ctx._triviaLog.length = logLen;
869
886
  expected.push(...result.expected);
887
+ if (result.committed) return { ok: false, expected, span: result.span, committed: true };
870
888
  continue;
871
889
  }
872
890
  const checks = autoNot[i];
@@ -1817,6 +1835,7 @@ function repItem(combinator, input, cur, ctx, guardable) {
1817
1835
  const result = combinator.parse(input, pos, ctx);
1818
1836
  if (!result.ok) {
1819
1837
  rollbackTrivia(ctx, mark);
1838
+ if (result.committed) return { fail: result, failPos: pos };
1820
1839
  return { fail: result, failPos: pos };
1821
1840
  }
1822
1841
  if (result.span.end === pos) {
@@ -1860,6 +1879,7 @@ function many(combinator, opts = {}) {
1860
1879
  const item = repItem(combinator, input, cur, ctx, guardable);
1861
1880
  if (item === "stop") break;
1862
1881
  if ("fail" in item) {
1882
+ if (!item.fail.ok && item.fail.committed) return item.fail;
1863
1883
  const sync = ctx._tolerant ? ctx._sync : void 0;
1864
1884
  if (sync === void 0) break;
1865
1885
  if (matchesAt(sync, input, item.failPos, ctx)) break;
@@ -1904,6 +1924,7 @@ function atLeast(combinator, min, max) {
1904
1924
  while (count < min) {
1905
1925
  const item = repItem(combinator, input, cur, ctx, guardable);
1906
1926
  if (item === "stop" || "fail" in item) {
1927
+ if (item !== "stop" && !item.fail.ok && item.fail.committed) return item.fail;
1907
1928
  expected ??= deriveExpected(combinator);
1908
1929
  return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: cur, end: cur } };
1909
1930
  }
@@ -1916,6 +1937,7 @@ function atLeast(combinator, min, max) {
1916
1937
  const item = repItem(combinator, input, cur, ctx, guardable);
1917
1938
  if (item === "stop") break;
1918
1939
  if ("fail" in item) {
1940
+ if (!item.fail.ok && item.fail.committed) return item.fail;
1919
1941
  const sync = ctx._tolerant ? ctx._sync : void 0;
1920
1942
  if (sync === void 0) break;
1921
1943
  if (matchesAt(sync, input, item.failPos, ctx)) break;
@@ -1954,6 +1976,7 @@ function optional(combinator) {
1954
1976
  const result = combinator.parse(input, pos, ctx);
1955
1977
  if (result.ok) return result;
1956
1978
  rollbackTrivia(ctx, mark);
1979
+ if (result.committed) return result;
1957
1980
  return { ok: true, value: null, span: { start: pos, end: pos } };
1958
1981
  }
1959
1982
  };
@@ -1990,6 +2013,7 @@ function sepBy(combinator, separator, opts = {}) {
1990
2013
  values.push(first.value);
1991
2014
  cur = first.span.end;
1992
2015
  } else {
2016
+ if (first.committed) return first;
1993
2017
  const term = ctx._tolerant ? ctx._sync : void 0;
1994
2018
  if (term === void 0 || matchesAt(term, input, pos, ctx)) {
1995
2019
  if (min >= 1) return failAt2(pos);
@@ -2017,6 +2041,7 @@ function sepBy(combinator, separator, opts = {}) {
2017
2041
  const sep = separator.parse(input, sepPos, ctx);
2018
2042
  if (!sep.ok) {
2019
2043
  rollbackTrivia(ctx, loopMark);
2044
+ if (sep.committed) return sep;
2020
2045
  break;
2021
2046
  }
2022
2047
  const sepMark = trailing === "forbid" ? void 0 : saveTriviaMark(ctx);
@@ -2032,6 +2057,7 @@ function sepBy(combinator, separator, opts = {}) {
2032
2057
  }
2033
2058
  const next = combinator.parse(input, nextPos, ctx);
2034
2059
  if (!next.ok) {
2060
+ if (next.committed) return next;
2035
2061
  const term = ctx._tolerant ? ctx._sync : void 0;
2036
2062
  if (term !== void 0 && !matchesAt(term, input, nextPos, ctx)) {
2037
2063
  expected ??= deriveExpected(combinator);
@@ -2265,6 +2291,8 @@ function parserHasOwnFields(p, seen = /* @__PURE__ */ new Set()) {
2265
2291
  case "sequence":
2266
2292
  case "choice":
2267
2293
  return d.parsers.some((x) => parserHasOwnFields(x, seen));
2294
+ case "dispatch":
2295
+ 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);
2268
2296
  case "sepBy":
2269
2297
  return parserHasOwnFields(d.parser, seen) || parserHasOwnFields(d.separator, seen);
2270
2298
  case "skip":
@@ -2379,6 +2407,245 @@ function keywords(words, opts = {}) {
2379
2407
  };
2380
2408
  }
2381
2409
 
2410
+ // src/combinators/dispatch.ts
2411
+ function parserUsesRouted(parser2, seen = /* @__PURE__ */ new Set()) {
2412
+ if (seen.has(parser2)) return false;
2413
+ seen.add(parser2);
2414
+ const def = parser2._def;
2415
+ switch (def.tag) {
2416
+ case "routed":
2417
+ return true;
2418
+ case "sequence":
2419
+ case "choice":
2420
+ return def.parsers.some((entry) => parserUsesRouted(entry, seen));
2421
+ case "dispatch":
2422
+ return false;
2423
+ case "sepBy":
2424
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.separator, seen);
2425
+ case "skip":
2426
+ return parserUsesRouted(def.main, seen) || parserUsesRouted(def.skipped, seen);
2427
+ case "grammar":
2428
+ return parserUsesRouted(def.parser, seen) || (def.triviaParser ? parserUsesRouted(def.triviaParser, seen) : false);
2429
+ case "scanTo":
2430
+ return parserUsesRouted(def.sentinel, seen) || def.skip.some((entry) => parserUsesRouted(entry, seen));
2431
+ case "recover":
2432
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.sentinel, seen);
2433
+ case "lazy":
2434
+ try {
2435
+ return parserUsesRouted(def.thunk(), seen);
2436
+ } catch {
2437
+ return false;
2438
+ }
2439
+ case "many":
2440
+ case "oneOrMore":
2441
+ case "optional":
2442
+ case "attempt":
2443
+ case "transform":
2444
+ case "trivia":
2445
+ case "token":
2446
+ case "leaf":
2447
+ case "label":
2448
+ case "field":
2449
+ case "not":
2450
+ case "peek":
2451
+ case "node":
2452
+ case "withCtx":
2453
+ case "expect":
2454
+ return parserUsesRouted(def.parser, seen);
2455
+ default:
2456
+ return false;
2457
+ }
2458
+ }
2459
+ function branchUsesRouted(branch) {
2460
+ return branch.usesRouted === true || parserUsesRouted(branch.parser);
2461
+ }
2462
+ function asciiFoldKey(key) {
2463
+ let out = "";
2464
+ for (let i = 0; i < key.length; i++) {
2465
+ const c = key.charCodeAt(i);
2466
+ out += String.fromCharCode(c >= 65 && c <= 90 ? c + 32 : c);
2467
+ }
2468
+ return out;
2469
+ }
2470
+ function matchesDispatchMatcher(value, matcher) {
2471
+ const candidate = matcher.caseInsensitive ? asciiFoldKey(value) : value;
2472
+ const expected = matcher.caseInsensitive ? asciiFoldKey(matcher.value) : matcher.value;
2473
+ switch (matcher.kind) {
2474
+ case "startsWith":
2475
+ return candidate.startsWith(expected);
2476
+ case "endsWith":
2477
+ return candidate.endsWith(expected);
2478
+ case "matches": {
2479
+ const flags = matcher.caseInsensitive && !matcher.flags?.includes("i") ? `${matcher.flags ?? ""}i` : matcher.flags ?? "";
2480
+ return new RegExp(matcher.value, flags).test(value);
2481
+ }
2482
+ }
2483
+ }
2484
+ function isStringMatcher(value) {
2485
+ if (typeof value !== "object" || value === null) return false;
2486
+ const kind = value.kind;
2487
+ return kind === "startsWith" || kind === "endsWith" || kind === "matches";
2488
+ }
2489
+ function fixedMatcher(kind, value) {
2490
+ if (typeof value !== "string") throw new TypeError(`parseman: ${kind}() value must be a string`);
2491
+ if (value.length === 0) throw new RangeError(`parseman: ${kind}() requires a non-empty value`);
2492
+ return { kind, value };
2493
+ }
2494
+ function startsWith(prefix) {
2495
+ return fixedMatcher("startsWith", prefix);
2496
+ }
2497
+ function endsWith(suffix) {
2498
+ return fixedMatcher("endsWith", suffix);
2499
+ }
2500
+ function matches(pattern) {
2501
+ if (!(pattern instanceof RegExp)) throw new TypeError("parseman: matches() requires a RegExp");
2502
+ if (pattern.flags.includes("g") || pattern.flags.includes("y")) {
2503
+ throw new TypeError("parseman: matches() does not accept global or sticky regex flags");
2504
+ }
2505
+ return { kind: "matches", value: pattern.source, flags: pattern.flags };
2506
+ }
2507
+ function when(key, parser2, opts = {}) {
2508
+ const usesRouted = parserUsesRouted(parser2);
2509
+ if (isStringMatcher(key)) {
2510
+ return { kind: "whenMatcher", matcher: key, parser: parser2, caseInsensitive: opts.caseInsensitive ?? false, usesRouted };
2511
+ }
2512
+ const keys = Array.isArray(key) ? [...key] : [key];
2513
+ if (keys.length === 0) throw new RangeError("parseman: when() requires at least one key");
2514
+ for (const item of keys) {
2515
+ if (typeof item !== "string") throw new TypeError("parseman: when() keys must be strings");
2516
+ }
2517
+ return { kind: "when", keys, parser: parser2, caseInsensitive: opts.caseInsensitive ?? false, usesRouted };
2518
+ }
2519
+ function otherwise(parser2) {
2520
+ return { kind: "otherwise", parser: parser2, usesRouted: parserUsesRouted(parser2) };
2521
+ }
2522
+ function routed() {
2523
+ return {
2524
+ _tag: "routed",
2525
+ _meta: { firstSet: { kind: "any" }, canMatchNewline: false, isTrivia: false },
2526
+ _def: { tag: "routed" },
2527
+ parse(_input, pos, ctx) {
2528
+ const item = ctx._routed;
2529
+ if (item === void 0) {
2530
+ return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2531
+ }
2532
+ if (pos !== item.span.start) {
2533
+ return { ok: false, expected: ["routed()"], span: { start: pos, end: pos } };
2534
+ }
2535
+ if (cstCaptureActive(ctx)) {
2536
+ pushCstLeaf(ctx, { _tag: "leaf", value: item.value, span: item.span });
2537
+ }
2538
+ return { ok: true, value: item.value, span: item.span };
2539
+ }
2540
+ };
2541
+ }
2542
+ function dispatch(selector, ...arms) {
2543
+ if (parserUsesRouted(selector)) {
2544
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
2545
+ }
2546
+ let fallback;
2547
+ let fallbackUsesRouted = false;
2548
+ const matchers = [];
2549
+ const cases = [];
2550
+ const seen = [];
2551
+ for (let i = 0; i < arms.length; i++) {
2552
+ const arm = arms[i];
2553
+ if (arm.kind === "otherwise") {
2554
+ if (fallback !== void 0) throw new Error("parseman: dispatch() accepts at most one otherwise() arm");
2555
+ if (i !== arms.length - 1) throw new Error("parseman: otherwise() must be the last dispatch() arm");
2556
+ fallback = arm.parser;
2557
+ fallbackUsesRouted = arm.usesRouted === true;
2558
+ continue;
2559
+ }
2560
+ if (arm.kind === "whenMatcher") {
2561
+ const matcher = arm.matcher;
2562
+ matchers.push({
2563
+ kind: matcher.kind,
2564
+ value: matcher.value,
2565
+ ...matcher.kind === "matches" ? { flags: matcher.flags } : {},
2566
+ parser: arm.parser,
2567
+ caseInsensitive: arm.caseInsensitive,
2568
+ ...arm.usesRouted === true ? { usesRouted: true } : {}
2569
+ });
2570
+ continue;
2571
+ }
2572
+ for (const key of arm.keys) {
2573
+ const folded = asciiFoldKey(key);
2574
+ for (const prior of seen) {
2575
+ const overlaps = arm.caseInsensitive || prior.caseInsensitive ? folded === prior.folded : key === prior.raw;
2576
+ if (overlaps) throw new Error(`parseman: duplicate dispatch key ${JSON.stringify(key)}`);
2577
+ }
2578
+ seen.push({ raw: key, folded, caseInsensitive: arm.caseInsensitive });
2579
+ }
2580
+ cases.push({
2581
+ keys: arm.keys,
2582
+ parser: arm.parser,
2583
+ caseInsensitive: arm.caseInsensitive,
2584
+ ...arm.usesRouted === true ? { usesRouted: true } : {}
2585
+ });
2586
+ }
2587
+ const meta = {
2588
+ firstSet: selector._meta.firstSet,
2589
+ canMatchNewline: selector._meta.canMatchNewline || cases.some((entry) => entry.parser._meta.canMatchNewline) || matchers.some((entry) => entry.parser._meta.canMatchNewline) || (fallback?._meta.canMatchNewline ?? false),
2590
+ isTrivia: false
2591
+ };
2592
+ const byKey = /* @__PURE__ */ new Map();
2593
+ const byFoldedKey = /* @__PURE__ */ new Map();
2594
+ for (const entry of cases) {
2595
+ for (const key of entry.keys) {
2596
+ if (entry.caseInsensitive) byFoldedKey.set(asciiFoldKey(key), entry);
2597
+ else byKey.set(key, entry);
2598
+ }
2599
+ }
2600
+ return {
2601
+ _tag: "dispatch",
2602
+ _meta: meta,
2603
+ _def: {
2604
+ tag: "dispatch",
2605
+ selector,
2606
+ cases,
2607
+ ...matchers.length === 0 ? {} : { matchers },
2608
+ ...fallback === void 0 ? {} : { otherwise: fallback, ...fallbackUsesRouted ? { otherwiseUsesRouted: true } : {} }
2609
+ },
2610
+ parse(input, pos, ctx) {
2611
+ if (parserUsesRouted(selector)) {
2612
+ throw new Error("parseman: routed() can only appear inside a dispatch() branch");
2613
+ }
2614
+ const selectorMark = saveTriviaMark(ctx);
2615
+ const selected = selector.parse(input, pos, ctx);
2616
+ if (!selected.ok) return selected;
2617
+ 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 });
2618
+ if (branch === void 0) {
2619
+ const expected = cases.flatMap((entry) => entry.keys.map((key) => JSON.stringify(key)));
2620
+ return { ok: false, expected, span: { start: selected.span.end, end: selected.span.end } };
2621
+ }
2622
+ const savedRouted = ctx._routed;
2623
+ const usesRouted = branchUsesRouted(branch);
2624
+ let mark = saveTriviaMark(ctx);
2625
+ if (usesRouted) {
2626
+ rollbackTrivia(ctx, selectorMark);
2627
+ mark = saveTriviaMark(ctx);
2628
+ ctx._routed = { value: selected.value, span: selected.span };
2629
+ }
2630
+ let result;
2631
+ try {
2632
+ result = branch.parser.parse(input, usesRouted ? pos : selected.span.end, ctx);
2633
+ } finally {
2634
+ if (usesRouted) ctx._routed = savedRouted;
2635
+ }
2636
+ if (!result.ok) {
2637
+ rollbackTrivia(ctx, mark);
2638
+ return { ...result, committed: true };
2639
+ }
2640
+ return {
2641
+ ok: true,
2642
+ value: [selected.value, result.value],
2643
+ span: { start: pos, end: result.span.end }
2644
+ };
2645
+ }
2646
+ };
2647
+ }
2648
+
2382
2649
  // src/combinators/attempt.ts
2383
2650
  function attempt(parser2) {
2384
2651
  const guardable = parser2._meta.firstSet.kind !== "any" && !matchesEmpty(parser2);
@@ -2401,6 +2668,7 @@ function attempt(parser2) {
2401
2668
  if (result.ok) return result;
2402
2669
  rollbackCstCapture(ctx, mark);
2403
2670
  if (log !== void 0 && ctx._triviaLog && ctx._triviaLog.length !== log) ctx._triviaLog.length = log;
2671
+ if (result.committed) return result;
2404
2672
  return { ok: false, expected: result.expected, span: { start: pos, end: pos } };
2405
2673
  }
2406
2674
  };
@@ -2810,6 +3078,13 @@ function evalRuleMapIR(ir) {
2810
3078
  "keywords",
2811
3079
  "sequence",
2812
3080
  "choice",
3081
+ "dispatch",
3082
+ "when",
3083
+ "startsWith",
3084
+ "endsWith",
3085
+ "matches",
3086
+ "otherwise",
3087
+ "routed",
2813
3088
  "attempt",
2814
3089
  "many",
2815
3090
  "oneOrMore",
@@ -2843,6 +3118,13 @@ function evalRuleMapIR(ir) {
2843
3118
  keywords,
2844
3119
  sequence,
2845
3120
  choice,
3121
+ dispatch,
3122
+ when,
3123
+ startsWith,
3124
+ endsWith,
3125
+ matches,
3126
+ otherwise,
3127
+ routed,
2846
3128
  attempt,
2847
3129
  many,
2848
3130
  oneOrMore,
@@ -3037,6 +3319,15 @@ var Builder = class {
3037
3319
  const items = def.parsers.map((p) => this.walk(p)).filter(nonEmpty);
3038
3320
  return flattenChoice(items);
3039
3321
  }
3322
+ case "dispatch": {
3323
+ const selector = this.walk(def.selector);
3324
+ const tails = [
3325
+ ...def.cases.map((entry) => this.walk(entry.parser)),
3326
+ ...def.matchers ? def.matchers.map((entry) => this.walk(entry.parser)) : [],
3327
+ ...def.otherwise ? [this.walk(def.otherwise)] : []
3328
+ ];
3329
+ return flattenSeq([selector, flattenChoice(tails)]);
3330
+ }
3040
3331
  // `max`/`min` are carried through so a BOUNDED repeat does not render as an
3041
3332
  // unbounded one — the spec is generated to be read as the truth about what
3042
3333
  // parses, and `many(x, { min: 3, max: 8 })` is not `x+`.
@@ -3066,6 +3357,8 @@ var Builder = class {
3066
3357
  return { kind: "not", item: this.walk(def.parser) };
3067
3358
  case "peek":
3068
3359
  return { kind: "peek", item: this.walk(def.parser) };
3360
+ case "routed":
3361
+ return { kind: "empty" };
3069
3362
  // Transparent semantic wrappers — render the inner syntax.
3070
3363
  case "transform":
3071
3364
  case "token":
@@ -3223,7 +3516,7 @@ function render(node2) {
3223
3516
  case "choice": {
3224
3517
  const text = node2.items.map((it) => {
3225
3518
  const r = render(it);
3226
- return wrap(r.text, r.level, ALT);
3519
+ return r.text.length === 0 ? "/* empty */" : wrap(r.text, r.level, ALT);
3227
3520
  }).join(" | ");
3228
3521
  return { text, level: ALT };
3229
3522
  }