@tabnas/abnf 0.4.1 → 0.4.2

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.
package/dist/converter.js CHANGED
@@ -1,12 +1,12 @@
1
1
  "use strict";
2
- /* Copyright (c) 2025 Richard Rodger and other contributors, MIT License */
2
+ /* Copyright (c) 2025-2026 Richard Rodger and other contributors, MIT License */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.AbnfParseError = exports.abnfRules = void 0;
4
+ exports.AbnfParseError = exports.abnfRules = exports.eliminateLeftRecursion = void 0;
5
5
  exports.abnf = abnf;
6
6
  exports.parseAbnf = parseAbnf;
7
7
  exports.emitGrammarSpec = emitGrammarSpec;
8
- exports.eliminateLeftRecursion = eliminateLeftRecursion;
9
- const parser_1 = require("@tabnas/parser");
8
+ const bnf_1 = require("@tabnas/bnf");
9
+ Object.defineProperty(exports, "eliminateLeftRecursion", { enumerable: true, get: function () { return bnf_1.eliminateLeftRecursion; } });
10
10
  // Declarative definition of the ABNF grammar itself, expressed as
11
11
  // tabnas rules. Each rule names its `open`/`close` alt list and, where
12
12
  // necessary, a `bo`/`bc` state hook for AST assembly.
@@ -414,8 +414,7 @@ const abnfRules = {
414
414
  },
415
415
  };
416
416
  exports.abnfRules = abnfRules;
417
- // Lazily built tabnas instance that parses ABNF source. Deferred
418
- // construction avoids a circular-import failure at module load time.
417
+ // Cached tabnas instance for the ABNF grammar above; built on first use.
419
418
  let _abnfParser = null;
420
419
  function getAbnfParser() {
421
420
  if (_abnfParser)
@@ -544,504 +543,6 @@ function getAbnfParser() {
544
543
  _abnfParser = (src) => j.parse(src);
545
544
  return _abnfParser;
546
545
  }
547
- // Rewrite a grammar so that the only element kinds remaining are
548
- // `term` and `ref`. Each `X?`, `X*`, `X+` occurrence is replaced by a
549
- // reference to a newly-generated helper production that expresses the
550
- // same language in plain ABNF.
551
- // Eliminate left recursion — both direct (P → P α) and indirect
552
- // (P → Q α, Q → P β) — via Paull's algorithm.
553
- //
554
- // Order the productions, and for each A_i walk back over A_1..A_{i-1}
555
- // inlining any leading reference into A_i's alternatives. Once the
556
- // only remaining leading self-reference on A_i is direct, rewrite to
557
- // the iterative form
558
- // P → (β_1 | … | β_m) (α_1 | … | α_n)*
559
- // which tabnas's push-down parser can execute without re-entering P
560
- // at the same source position.
561
- //
562
- // The substitution step can duplicate alternatives, so pathological
563
- // grammars will enlarge — caller is expected to keep the grammar
564
- // reasonably small (this is a first-step converter, not a full
565
- // toolchain).
566
- function eliminateLeftRecursion(grammar) {
567
- const originalOrder = grammar.productions.map((p) => p.name);
568
- // Order productions so that rules referenced at a leading position
569
- // are processed before the rules that reference them. Paull's
570
- // substitution inlines A_j's alts into A_i for j < i, so putting
571
- // dependencies first is what makes nullable-prefixed hidden left
572
- // recursion reachable by the substitution step.
573
- let prods = topoOrderForPaull(grammar.productions.map((p) => ({
574
- name: p.name,
575
- alts: p.alts.map((a) => a.slice()),
576
- nodeKind: p.nodeKind,
577
- })));
578
- // Substitution normally runs for every production, even a cycle-free
579
- // one. That is pragmatic rather than theoretical: the multi-token
580
- // `altPrefixes` used to populate tcol (so the lexer's regex matchers
581
- // fire with the right tin in nested contexts) are read off the fully
582
- // inlined shape, and a rule choosing between several alternatives
583
- // needs that lookahead to dispatch. Scoping substitution to the cyclic
584
- // SCCs in general therefore has to wait for tcol to be computed from
585
- // the un-substituted grammar.
586
- //
587
- // One case is safe to exempt today, and it is the one that visibly
588
- // mangles a grammar: a *pure alias*, a production whose single
589
- // alternative is a single rule reference (`val = add`). Inlining it
590
- // rewrites `val = add` into `val = NR [ PL add ]` — the alias name is
591
- // dissolved, so the rule vanishes from the emitted AST and the grammar
592
- // no longer renders back to the ABNF it was written in. Because such a
593
- // production has exactly one alternative, it has nothing to dispatch
594
- // between: it unconditionally pushes its target, needs no lookahead,
595
- // and so cannot depend on the inlined prefixes. Aliases caught up in a
596
- // leading-reference cycle are still inlined — that is where Paull's
597
- // substitution is doing real work (`P = Q`, `Q = P a / b`).
598
- const cyclic = findLeadingRefCycleMembers(prods);
599
- const isExemptAlias = (p) => p.alts.length === 1 &&
600
- p.alts[0].length === 1 &&
601
- p.alts[0][0].kind === 'ref' &&
602
- !cyclic.has(p.name) &&
603
- !cyclic.has(p.alts[0][0].name);
604
- for (let i = 0; i < prods.length; i++) {
605
- // For each earlier production A_j, inline any alternative of
606
- // A_i whose leading element is a reference to A_j.
607
- //
608
- // Paull's invariant is that after this inner loop no alternative
609
- // of A_i begins with a ref to any A_j, j < i. A single increasing
610
- // pass gives that only when every A_j has itself been fully
611
- // substituted — but the pure-alias exemption above deliberately
612
- // leaves some A_j un-substituted, so inlining such an alias can
613
- // (re)introduce a leading ref to an A_k with k < j, which the pass
614
- // has already walked past. Left in place, a nullable A_k hides the
615
- // left recursion from `eliminateDirectLeftRec` and the emitted
616
- // grammar re-enters A_i at the same source position
617
- // (`a = b a / "x"`, `b = c`, `c = "y" /`). So re-run the pass
618
- // until it reaches a fixed point.
619
- //
620
- // Termination: each round only fires where a leading ref to an
621
- // earlier production remains, and earlier productions have already
622
- // had their own direct left recursion eliminated, so no
623
- // substitution can reproduce the ref it just consumed. The guard
624
- // is belt-and-braces against a pathological grammar.
625
- if (!isExemptAlias(prods[i])) {
626
- const guard = prods.length + 1;
627
- for (let round = 0; round < guard; round++) {
628
- let changed = false;
629
- for (let j = 0; j < i; j++) {
630
- if (!hasLeadingRefTo(prods[i], prods[j].name))
631
- continue;
632
- prods[i] = substituteLeadingRef(prods[i], prods[j]);
633
- changed = true;
634
- }
635
- if (!changed)
636
- break;
637
- }
638
- }
639
- prods[i] = eliminateDirectLeftRec(prods[i]);
640
- }
641
- // Restore the caller's declared order, so the start rule still
642
- // ends up first (and the user sees their rule names in a
643
- // recognisable order when inspecting the spec).
644
- const byName = new Map(prods.map((p) => [p.name, p]));
645
- const ordered = [];
646
- for (const name of originalOrder) {
647
- const p = byName.get(name);
648
- if (p) {
649
- ordered.push(p);
650
- byName.delete(name);
651
- }
652
- }
653
- // Any generated productions created during substitution (none in
654
- // the current implementation) would fall through here.
655
- for (const p of byName.values())
656
- ordered.push(p);
657
- return { productions: ordered };
658
- }
659
- // Tarjan-flavoured SCC scan over the leading-reference graph:
660
- // returns the names of productions that participate in at least one
661
- // cycle (self-loop or longer). Used to scope Paull's substitution to
662
- // only the rules that actually need it.
663
- function findLeadingRefCycleMembers(prods) {
664
- const byName = new Map(prods.map((p) => [p.name, p]));
665
- const leadingRefs = (p) => {
666
- const out = [];
667
- for (const alt of p.alts) {
668
- if (alt.length === 0)
669
- continue;
670
- const first = alt[0];
671
- if (first.kind === 'ref' && byName.has(first.name))
672
- out.push(first.name);
673
- }
674
- return out;
675
- };
676
- // Tarjan's SCC algorithm.
677
- let index = 0;
678
- const stack = [];
679
- const onStack = new Set();
680
- const indices = new Map();
681
- const lowlinks = new Map();
682
- const cyclic = new Set();
683
- function strongConnect(name) {
684
- indices.set(name, index);
685
- lowlinks.set(name, index);
686
- index++;
687
- stack.push(name);
688
- onStack.add(name);
689
- const prod = byName.get(name);
690
- if (prod) {
691
- for (const target of leadingRefs(prod)) {
692
- if (!indices.has(target)) {
693
- strongConnect(target);
694
- lowlinks.set(name, Math.min(lowlinks.get(name), lowlinks.get(target)));
695
- }
696
- else if (onStack.has(target)) {
697
- lowlinks.set(name, Math.min(lowlinks.get(name), indices.get(target)));
698
- }
699
- }
700
- }
701
- if (lowlinks.get(name) === indices.get(name)) {
702
- // Pop the SCC. If it has more than one member, or it's a
703
- // single member with a self-loop, mark as cyclic.
704
- const scc = [];
705
- let w;
706
- do {
707
- w = stack.pop();
708
- onStack.delete(w);
709
- scc.push(w);
710
- } while (w !== name);
711
- const isCycle = scc.length > 1 ||
712
- (scc.length === 1 && leadingRefs(byName.get(scc[0])).includes(scc[0]));
713
- if (isCycle)
714
- for (const n of scc)
715
- cyclic.add(n);
716
- }
717
- }
718
- for (const p of prods) {
719
- if (!indices.has(p.name))
720
- strongConnect(p.name);
721
- }
722
- return cyclic;
723
- }
724
- // Topological order over the "leading-position reference" graph:
725
- // an edge A → B exists when A has at least one alternative whose
726
- // first element is a reference to B. Cycles are preserved as-is
727
- // (Paull's handles them via the substitution + direct-LR rewrite).
728
- function topoOrderForPaull(prods) {
729
- const byName = new Map(prods.map((p) => [p.name, p]));
730
- const colour = new Map(); // 0 unseen, 1 in-progress, 2 done
731
- const order = [];
732
- function visit(name) {
733
- const c = colour.get(name) ?? 0;
734
- if (c !== 0)
735
- return; // already seen or on the current path
736
- colour.set(name, 1);
737
- const p = byName.get(name);
738
- if (p) {
739
- for (const alt of p.alts) {
740
- if (alt.length > 0 && alt[0].kind === 'ref' && byName.has(alt[0].name)) {
741
- visit(alt[0].name);
742
- }
743
- }
744
- colour.set(name, 2);
745
- order.push(p);
746
- }
747
- else {
748
- colour.set(name, 2);
749
- }
750
- }
751
- for (const p of prods)
752
- visit(p.name);
753
- return order;
754
- }
755
- // True when at least one alternative of `prod` begins with a
756
- // reference to `name` — i.e. `substituteLeadingRef` would change it.
757
- function hasLeadingRefTo(prod, name) {
758
- for (const alt of prod.alts) {
759
- if (alt.length > 0 && alt[0].kind === 'ref' && alt[0].name === name) {
760
- return true;
761
- }
762
- }
763
- return false;
764
- }
765
- // For every alternative of `target` that begins with a ref to
766
- // `source`, replace that alt with |source.alts| copies — each one
767
- // with the leading source-ref expanded to one of source's alts.
768
- function substituteLeadingRef(target, source) {
769
- const newAlts = [];
770
- for (const alt of target.alts) {
771
- if (alt.length > 0 &&
772
- alt[0].kind === 'ref' &&
773
- alt[0].name === source.name) {
774
- const tail = alt.slice(1);
775
- for (const srcAlt of source.alts) {
776
- newAlts.push([...srcAlt, ...tail]);
777
- }
778
- }
779
- else {
780
- newAlts.push(alt);
781
- }
782
- }
783
- return { name: target.name, alts: newAlts, nodeKind: target.nodeKind };
784
- }
785
- // Rewrite a single production's direct left recursion to its
786
- // iterative equivalent. Equivalent to the previous version of
787
- // `eliminateLeftRecursion` but scoped to one production.
788
- function eliminateDirectLeftRec(prod) {
789
- const recursive = [];
790
- const seeds = [];
791
- for (const alt of prod.alts) {
792
- if (alt.length > 0 &&
793
- alt[0].kind === 'ref' &&
794
- alt[0].name === prod.name) {
795
- recursive.push(alt.slice(1));
796
- }
797
- else {
798
- seeds.push(alt);
799
- }
800
- }
801
- // A trivial recursive alt `[P]` (P ::= P, nothing else) would
802
- // derive P from P with no progress — semantically a no-op. Drop
803
- // them silently, since nullable-prefix expansion in Paull's can
804
- // legitimately produce them and erroring would hide a legal
805
- // grammar.
806
- const nonTrivialRecursive = recursive.filter((t) => t.length > 0);
807
- if (nonTrivialRecursive.length === 0) {
808
- // Either no recursion at all, or only trivial self-refs — keep
809
- // just the seeds.
810
- return { name: prod.name, alts: seeds, nodeKind: prod.nodeKind };
811
- }
812
- if (seeds.length === 0) {
813
- throw new Error(`abnf: rule '${prod.name}' is purely left-recursive ` +
814
- `(no seed alternative); cannot eliminate`);
815
- }
816
- const seedElement = seeds.length === 1 && seeds[0].length === 1
817
- ? seeds[0][0]
818
- : { kind: 'group', alts: seeds };
819
- const tailInner = nonTrivialRecursive.length === 1 && nonTrivialRecursive[0].length === 1
820
- ? nonTrivialRecursive[0][0]
821
- : { kind: 'group', alts: nonTrivialRecursive };
822
- return {
823
- name: prod.name,
824
- alts: [[seedElement, { kind: 'star', inner: tailInner }]],
825
- nodeKind: prod.nodeKind,
826
- };
827
- }
828
- // Rewrite tail self-references into same-depth repeats.
829
- //
830
- // X = prefix [ sep X ]
831
- //
832
- // compiles naturally to a rule that repeats itself (`r: X`) from its
833
- // close phase — the form a hand-written tabnas grammar uses — rather
834
- // than to an optional-group helper chain that re-pushes X. The repeat
835
- // keeps every iteration at one stack depth with the SAME parent, which
836
- // is what makes `r.parent.node` usable from user actions, and flattens
837
- // the tree: `1+2+3` yields sibling X kids instead of a right-nested
838
- // chain.
839
- //
840
- // The rewrite is deliberately narrow. It applies only when:
841
- // - the production has exactly one alternative;
842
- // - its last element is an option wrapping `sep… X` with the
843
- // self-reference LAST and at least one separator element;
844
- // - every prefix and separator element is a terminal (literal,
845
- // token, or regex — all resolved before this pass runs);
846
- // - the production is not the start production (the `__start__`
847
- // wrapper allocates no node for a fold to land in).
848
- // Anything else keeps the existing compilation.
849
- function rewriteTailRepeats(grammar, start) {
850
- const isTerminal = (el) => el.kind === 'term' || el.kind === 'token' || el.kind === 'regex';
851
- for (const prod of grammar.productions) {
852
- if (prod.probeDispatch || prod.probeHelper)
853
- continue;
854
- if (prod.name === start)
855
- continue;
856
- if (prod.alts.length !== 1)
857
- continue;
858
- const alt = prod.alts[0];
859
- if (alt.length < 2)
860
- continue;
861
- const last = alt[alt.length - 1];
862
- if (last.kind !== 'opt')
863
- continue;
864
- // Normalize the option body to a sequence: `[ a b ]` parses as
865
- // opt(group([[a, b]])); `[ a ]` as opt(a).
866
- let seq;
867
- if (last.inner.kind === 'group') {
868
- if (last.inner.alts.length !== 1)
869
- continue;
870
- seq = last.inner.alts[0];
871
- }
872
- else {
873
- seq = [last.inner];
874
- }
875
- if (seq.length < 2)
876
- continue; // need at least one separator + the self-ref
877
- const tail = seq[seq.length - 1];
878
- if (tail.kind !== 'ref' || tail.name !== prod.name)
879
- continue;
880
- const sep = seq.slice(0, -1);
881
- if (!sep.every(isTerminal))
882
- continue;
883
- const prefix = alt.slice(0, -1);
884
- if (prefix.length === 0 || !prefix.every(isTerminal))
885
- continue;
886
- prod.alts = [prefix];
887
- prod.tailRepeat = { sep };
888
- }
889
- return grammar;
890
- }
891
- function desugar(grammar) {
892
- const extra = [];
893
- const used = new Set(grammar.productions.map((p) => p.name));
894
- function freshName(hint) {
895
- // Collision-avoiding name like `_gen1`, `_gen2`, …
896
- let i = extra.length;
897
- let name;
898
- do {
899
- i++;
900
- name = `_gen${i}_${hint}`;
901
- } while (used.has(name));
902
- used.add(name);
903
- return name;
904
- }
905
- function desugarAlt(alt) {
906
- return alt.map(desugarElement);
907
- }
908
- function desugarElement(el) {
909
- if (el.kind === 'term' || el.kind === 'ref' || el.kind === 'regex' ||
910
- el.kind === 'token') {
911
- return el;
912
- }
913
- if (el.kind === 'prose') {
914
- // Unreachable: `resolveProseTerminals` drops every prose element
915
- // (or throws) before desugaring runs.
916
- throw new Error(`abnf: internal: unresolved prose terminal '<${el.text}>'`);
917
- }
918
- if (el.kind === 'group') {
919
- // Recurse into the group's alts so nested sugar is flattened,
920
- // then emit a helper production whose body is those alts.
921
- const innerAlts = el.alts.map((a) => desugarAlt(a));
922
- const name = freshName('group');
923
- extra.push({ name, alts: innerAlts, nodeKind: 'helper' });
924
- return { kind: 'ref', name };
925
- }
926
- // `opt`, `star`, `plus` all wrap a single inner element.
927
- const inner = desugarElement(el.inner);
928
- // Name the generated helper after what it repeats. A literal lifted
929
- // from a named production (`PL = "+"`) carries that name, and a
930
- // built-in token carries its own, so `*PL` still yields
931
- // `_genN_star_PL` rather than an anonymous `_genN_star_term`.
932
- const hint = inner.kind === 'ref' ? inner.name :
933
- inner.kind === 'term' ? (inner.tokenName ?? 'term') :
934
- inner.kind === 'token' ? inner.name.replace(/^#/, '') : 'x';
935
- if (el.kind === 'opt') {
936
- // H ::= inner | (empty)
937
- const name = freshName('opt_' + hint);
938
- extra.push({ name, alts: [[inner], []], nodeKind: 'helper' });
939
- return { kind: 'ref', name };
940
- }
941
- if (el.kind === 'star') {
942
- // H = inner H / (empty)
943
- const name = freshName('star_' + hint);
944
- const selfRef = { kind: 'ref', name };
945
- extra.push({ name, alts: [[inner, selfRef], []], nodeKind: 'helper' });
946
- return { kind: 'ref', name };
947
- }
948
- if (el.kind === 'plus') {
949
- // H = inner Tail where Tail = inner Tail / (empty)
950
- const tailName = freshName('star_' + hint);
951
- const plusName = freshName('plus_' + hint);
952
- const tailRef = { kind: 'ref', name: tailName };
953
- extra.push({
954
- name: tailName,
955
- alts: [[inner, tailRef], []],
956
- nodeKind: 'helper',
957
- });
958
- extra.push({
959
- name: plusName,
960
- alts: [[inner, tailRef]],
961
- nodeKind: 'helper',
962
- });
963
- return { kind: 'ref', name: plusName };
964
- }
965
- // ABNF m*n bounded repetition. Desugars to a concatenation of
966
- // `min` mandatory copies of the inner element followed by a
967
- // tail that accepts up to `(max - min)` more.
968
- // m*n A => A{m} [A[A[A...[A]]]] (nested optionals)
969
- // m* A => A{m} *A (mandatory prefix + star)
970
- // *n A => [A [A ... [A]]] (n nested optionals)
971
- // The helper's single alt has `min` repetitions of inner, then
972
- // either a star-helper for (min, ∞) or `max - min` nested
973
- // optionals for a finite range.
974
- const { min, max } = el;
975
- const repName = freshName('rep_' + hint);
976
- const repAlt = [];
977
- for (let i = 0; i < min; i++)
978
- repAlt.push(inner);
979
- if (max === Infinity) {
980
- // Tail: unbounded star of inner.
981
- const tailStarName = freshName('star_' + hint);
982
- const tailStarRef = { kind: 'ref', name: tailStarName };
983
- extra.push({
984
- name: tailStarName,
985
- alts: [[inner, tailStarRef], []],
986
- nodeKind: 'helper',
987
- });
988
- repAlt.push(tailStarRef);
989
- }
990
- else {
991
- // Nest (max - min) optionals: [A [A [A ...]]].
992
- //
993
- // Built bottom-up as an explicit chain of helper productions
994
- // rather than as one deeply-nested inline element tree. The
995
- // shape (and every generated name) is identical either way, but
996
- // handing a nested tree to `desugarAlt` costs a stack frame per
997
- // repetition, and real ABNF carries big bounds — RFC 5322's
998
- // `body = (*(*998text CRLF) *998text)` blew the call stack with
999
- // `Maximum call stack size exceeded` before reaching the emitter.
1000
- //
1001
- // Each level is exactly what `desugarElement` would have emitted
1002
- // for `opt(group([[inner, <previous level>]]))`: the group helper
1003
- // first, then the optional wrapping a reference to it. Pushing in
1004
- // that order keeps `freshName`'s numbering unchanged.
1005
- let nestedRef = null;
1006
- for (let i = 0; i < max - min; i++) {
1007
- const seq = nestedRef ? [inner, nestedRef] : [inner];
1008
- const groupName = freshName('group');
1009
- extra.push({ name: groupName, alts: [seq], nodeKind: 'helper' });
1010
- const groupRef = { kind: 'ref', name: groupName };
1011
- const optName = freshName('opt_' + groupName);
1012
- extra.push({
1013
- name: optName,
1014
- alts: [[groupRef], []],
1015
- nodeKind: 'helper',
1016
- });
1017
- nestedRef = { kind: 'ref', name: optName };
1018
- }
1019
- if (nestedRef)
1020
- repAlt.push(nestedRef);
1021
- }
1022
- extra.push({ name: repName, alts: [desugarAlt(repAlt)], nodeKind: 'helper' });
1023
- return { kind: 'ref', name: repName };
1024
- }
1025
- const rewritten = grammar.productions.map((p) => {
1026
- const out = {
1027
- name: p.name,
1028
- alts: p.alts.map(desugarAlt),
1029
- nodeKind: p.nodeKind,
1030
- };
1031
- // Probe-dispatch and tail-repeat flags survive desugar unchanged —
1032
- // the emitter routes around the standard alt-compilation path for
1033
- // these. (A tail-repeat separator is all-terminal by construction,
1034
- // so it needs no desugaring of its own.)
1035
- if (p.probeDispatch)
1036
- out.probeDispatch = p.probeDispatch;
1037
- if (p.probeHelper)
1038
- out.probeHelper = p.probeHelper;
1039
- if (p.tailRepeat)
1040
- out.tailRepeat = p.tailRepeat;
1041
- return out;
1042
- });
1043
- return { productions: [...rewritten, ...extra] };
1044
- }
1045
546
  // Error raised when the ABNF source itself can't be parsed. Surfaces
1046
547
  // line and column from the underlying tabnas error so the caller can
1047
548
  // report them directly. The original error is kept on `.cause`.
@@ -1114,20 +615,6 @@ function getCoreRules() {
1114
615
  _coreRules = new Map(raw.map((p) => [p.name, p]));
1115
616
  return _coreRules;
1116
617
  }
1117
- function refsIn(alt, out) {
1118
- for (const el of alt) {
1119
- if (el.kind === 'ref')
1120
- out.add(el.name);
1121
- else if (el.kind === 'opt' || el.kind === 'star' ||
1122
- el.kind === 'plus' || el.kind === 'rep') {
1123
- refsIn([el.inner], out);
1124
- }
1125
- else if (el.kind === 'group') {
1126
- for (const a of el.alts)
1127
- refsIn(a, out);
1128
- }
1129
- }
1130
- }
1131
618
  // Add each RFC 5234 core rule that the user's grammar references
1132
619
  // but doesn't define locally. Resolution is transitive: if the
1133
620
  // user mentions HEXDIG, DIGIT is pulled in too. User definitions
@@ -1139,7 +626,7 @@ function withCoreRules(user) {
1139
626
  const scan = (prods) => {
1140
627
  for (const p of prods) {
1141
628
  for (const alt of p.alts)
1142
- refsIn(alt, needed);
629
+ (0, bnf_1.refsIn)(alt, needed);
1143
630
  }
1144
631
  };
1145
632
  scan(user);
@@ -1187,1545 +674,6 @@ function mergeIncrementals(prods) {
1187
674
  }
1188
675
  return out;
1189
676
  }
1190
- // -- Probe-dispatch analyser + rewriter -----------------------------
1191
- //
1192
- // ABNF has a large family of grammars that aren't LL(k) for any
1193
- // bounded k. The canonical example is RFC 3986's `authority`:
1194
- //
1195
- // authority = [ userinfo "@" ] host [ ":" port ]
1196
- // userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
1197
- // host = IP-literal / IPv4address / reg-name
1198
- // reg-name = *( unreserved / pct-encoded / sub-delims )
1199
- //
1200
- // `userinfo` and `reg-name` share a character vocabulary, so a
1201
- // FIRST-set dispatcher can't decide which branch the optional
1202
- // `[ userinfo "@" ]` belongs to — the disambiguating `@` can be
1203
- // arbitrarily far from the start.
1204
- //
1205
- // For the common pattern `[X D] Y` — an optional group whose body
1206
- // ends with a terminal D, followed by a sequence Y whose leading
1207
- // terminals overlap with X's — we handle the ambiguity by rewriting
1208
- // the rule to a probe+phase-retry dispatcher:
1209
- //
1210
- // 1. On first entry (phase 0), mark the token position and push a
1211
- // failure-proof probe rule that greedily consumes every token
1212
- // in the joint vocabulary of X and Y.
1213
- // 2. When the probe returns, peek ctx.t[0]:
1214
- // D seen → phase = 1 (take the `X D Y` branch)
1215
- // D absent → phase = 2 (take the `Y` branch)
1216
- // Rewind to the mark and `r:` back into the dispatcher.
1217
- // 3. The dispatcher's open has a `c:`-guarded alt for each phase
1218
- // that pushes the corresponding committed branch.
1219
- //
1220
- // The primitives used (`r:`, `k:`, `c:`, `ctx.mark`, `ctx.rewind`,
1221
- // `ctx.t`) are the same building blocks rules/parser already exposes
1222
- // — no new tabnas machinery is needed.
1223
- // Predicate: element is `[ X D ]` where X is one or more elements
1224
- // and D is a terminal literal or a regex terminal.
1225
- function isProbeableOpt(el) {
1226
- if (el.kind !== 'opt')
1227
- return null;
1228
- const inner = el.inner;
1229
- if (inner.kind !== 'group')
1230
- return null;
1231
- if (inner.alts.length !== 1)
1232
- return null;
1233
- const seq = inner.alts[0];
1234
- if (seq.length < 2)
1235
- return null;
1236
- const last = seq[seq.length - 1];
1237
- if (last.kind !== 'term' && last.kind !== 'regex' && last.kind !== 'token')
1238
- return null;
1239
- return { xSeq: seq.slice(0, -1), disambiguator: last };
1240
- }
1241
- // Union of every terminal reachable by walking an element's subtree,
1242
- // following refs transitively. Cycles are broken by the visited set.
1243
- // Returns terminals as AbnfElements so the caller isn't tied to the
1244
- // emitter's token-allocation step.
1245
- function collectTerminalVocabElements(el, grammar, out, visited) {
1246
- if (el.kind === 'term') {
1247
- const k = termKey(el);
1248
- if (!out.has(k))
1249
- out.set(k, el);
1250
- return;
1251
- }
1252
- if (el.kind === 'regex') {
1253
- const k = regexKey(el);
1254
- if (!out.has(k))
1255
- out.set(k, el);
1256
- return;
1257
- }
1258
- if (el.kind === 'token') {
1259
- if (!out.has(el.name))
1260
- out.set(el.name, el);
1261
- return;
1262
- }
1263
- if (el.kind === 'ref') {
1264
- if (visited.has(el.name))
1265
- return;
1266
- visited.add(el.name);
1267
- const prod = grammar.productions.find((p) => p.name === el.name);
1268
- if (!prod)
1269
- return;
1270
- for (const alt of prod.alts)
1271
- for (const sub of alt)
1272
- collectTerminalVocabElements(sub, grammar, out, visited);
1273
- return;
1274
- }
1275
- if (el.kind === 'opt' || el.kind === 'star' || el.kind === 'plus' ||
1276
- el.kind === 'rep') {
1277
- collectTerminalVocabElements(el.inner, grammar, out, visited);
1278
- return;
1279
- }
1280
- if (el.kind === 'group') {
1281
- for (const alt of el.alts)
1282
- for (const sub of alt)
1283
- collectTerminalVocabElements(sub, grammar, out, visited);
1284
- return;
1285
- }
1286
- }
1287
- function collectSeqVocabElements(seq, grammar) {
1288
- const out = new Map();
1289
- const visited = new Set();
1290
- for (const el of seq)
1291
- collectTerminalVocabElements(el, grammar, out, visited);
1292
- return out;
1293
- }
1294
- function mapsOverlap(a, b) {
1295
- for (const x of a.keys())
1296
- if (b.has(x))
1297
- return true;
1298
- return false;
1299
- }
1300
- // Rewrite every ambiguous `[X D] Y` subsequence in `grammar` into a
1301
- // probe-dispatch pattern. The grammar at this point still has `opt`,
1302
- // `group`, `star`, `plus`, `rep` sugar — intentionally, since that's
1303
- // where the pattern is easy to recognise. Runs BEFORE token
1304
- // allocation; probe metadata stores AbnfElements, and the emitter
1305
- // resolves them to token names at emit time.
1306
- function rewriteProbeDispatches(grammar) {
1307
- const reports = grammar.ambiguities ?? [];
1308
- const extra = [];
1309
- const used = new Set(grammar.productions.map((p) => p.name));
1310
- function freshName(hint) {
1311
- let name = hint;
1312
- let i = 1;
1313
- while (used.has(name)) {
1314
- name = hint + i;
1315
- i++;
1316
- }
1317
- used.add(name);
1318
- return name;
1319
- }
1320
- const rewritten = [];
1321
- for (const prod of grammar.productions) {
1322
- let newAlts = [];
1323
- let touched = false;
1324
- for (let altIdx = 0; altIdx < prod.alts.length; altIdx++) {
1325
- const alt = prod.alts[altIdx];
1326
- let resultAlt = [];
1327
- for (let i = 0; i < alt.length; i++) {
1328
- const el = alt[i];
1329
- const info = isProbeableOpt(el);
1330
- if (!info) {
1331
- resultAlt.push(el);
1332
- continue;
1333
- }
1334
- const ySeq = alt.slice(i + 1);
1335
- if (ySeq.length === 0) {
1336
- // `[X D]` is the last thing in the alt — nothing follows, so
1337
- // there's nothing to disambiguate against. Standard emit.
1338
- resultAlt.push(el);
1339
- continue;
1340
- }
1341
- const xVocab = collectSeqVocabElements(info.xSeq, grammar);
1342
- const yVocab = collectSeqVocabElements(ySeq, grammar);
1343
- if (!mapsOverlap(xVocab, yVocab)) {
1344
- // The optional's leading tokens don't overlap with the tail's
1345
- // leading tokens, so the normal FIRST-based dispatcher can
1346
- // decide. No rewrite needed.
1347
- resultAlt.push(el);
1348
- continue;
1349
- }
1350
- // Joint vocab: union of everything the probe might need to
1351
- // consume. Includes the disambiguator, which we then remove so
1352
- // the probe stops on it and the peek works.
1353
- const vocab = new Map([...xVocab, ...yVocab]);
1354
- const d = info.disambiguator;
1355
- const dKey = d.kind === 'term' ? termKey(d)
1356
- : d.kind === 'regex' ? regexKey(d)
1357
- : d.kind === 'token' ? d.name
1358
- : null;
1359
- if (dKey)
1360
- vocab.delete(dKey);
1361
- const dispatchName = freshName(`${prod.name}$pd${i}`);
1362
- const probeName = freshName(`${dispatchName}$probe`);
1363
- const withName = freshName(`${dispatchName}$with`);
1364
- const noName = freshName(`${dispatchName}$no`);
1365
- // Synthesise the probe helper.
1366
- extra.push({
1367
- name: probeName,
1368
- alts: [],
1369
- probeHelper: { vocabElements: [...vocab.values()] },
1370
- nodeKind: 'helper',
1371
- });
1372
- // Synthesise the committed branches. `with` = X D Y, `no` = Y.
1373
- extra.push({
1374
- name: withName,
1375
- alts: [[...info.xSeq, info.disambiguator, ...ySeq]],
1376
- nodeKind: 'helper',
1377
- });
1378
- extra.push({
1379
- name: noName,
1380
- alts: [ySeq],
1381
- nodeKind: 'helper',
1382
- });
1383
- // Synthesise the dispatcher. The `alts` list is a "virtual"
1384
- // spec — two ref-only alts — that exists solely to feed
1385
- // computeFirstSets the right FIRST/nullable answers (FIRST
1386
- // = FIRST(with) ∪ FIRST(no)). The emitter checks
1387
- // `probeDispatch` first and emits the phase-retry body
1388
- // instead of compiling `alts`.
1389
- extra.push({
1390
- name: dispatchName,
1391
- alts: [
1392
- [{ kind: 'ref', name: withName }],
1393
- [{ kind: 'ref', name: noName }],
1394
- ],
1395
- probeDispatch: {
1396
- probeRule: probeName,
1397
- disambiguator: info.disambiguator,
1398
- withBranch: withName,
1399
- noBranch: noName,
1400
- },
1401
- nodeKind: 'helper',
1402
- });
1403
- reports.push({
1404
- rule: prod.name, altIdx, optIdx: i,
1405
- reason: `optional prefix shares vocabulary with tail`,
1406
- resolved: true,
1407
- });
1408
- resultAlt.push({ kind: 'ref', name: dispatchName });
1409
- // Everything that followed the opt is now inside the dispatcher
1410
- // (withBranch / noBranch), so skip the rest of the alt.
1411
- i = alt.length;
1412
- touched = true;
1413
- }
1414
- newAlts.push(resultAlt);
1415
- }
1416
- if (touched) {
1417
- rewritten.push({
1418
- name: prod.name,
1419
- alts: newAlts,
1420
- nodeKind: prod.nodeKind,
1421
- });
1422
- }
1423
- else {
1424
- rewritten.push(prod);
1425
- }
1426
- }
1427
- return {
1428
- productions: [...rewritten, ...extra],
1429
- ambiguities: reports,
1430
- };
1431
- }
1432
- // Emit a probe helper production. A self-looping rule that matches any
1433
- // one of the vocab tokens and restarts; a final empty-alt fallback
1434
- // ensures the rule NEVER fails — if the current lookahead isn't in the
1435
- // vocab (or we're at #ZZ), the rule pops cleanly. This is the
1436
- // failure-proof property the probe pattern relies on.
1437
- function emitProbeHelper(prod, tag, ruleSpec, literals, regexTokens) {
1438
- const elems = prod.probeHelper.vocabElements;
1439
- const opens = [];
1440
- for (const el of elems) {
1441
- const tok = el.kind === 'term'
1442
- ? literals.get(termKey(el))
1443
- : el.kind === 'regex' ? regexTokens.get(regexKey(el))
1444
- : el.kind === 'token' ? el.name
1445
- : undefined;
1446
- if (tok)
1447
- opens.push({ s: tok, r: prod.name, g: tag });
1448
- }
1449
- // Empty fallback — pops without consuming anything. Must be last.
1450
- opens.push({ g: tag });
1451
- ruleSpec[prod.name] = { open: opens };
1452
- }
1453
- // Emit a probe-dispatch production. Encodes the three-phase retry
1454
- // pattern; uses only standard tabnas primitives (r:, p:, c:, k:,
1455
- // ctx.mark/rewind/t).
1456
- function emitProbeDispatch(prod, tag, ruleSpec, refs, literals, regexTokens, useBuiltins) {
1457
- const { probeRule, disambiguator, withBranch, noBranch } = prod.probeDispatch;
1458
- const disambiguatorToken = disambiguator.kind === 'term'
1459
- ? literals.get(termKey(disambiguator))
1460
- : disambiguator.kind === 'regex'
1461
- ? regexTokens.get(regexKey(disambiguator))
1462
- : disambiguator.kind === 'token'
1463
- ? disambiguator.name
1464
- : undefined;
1465
- if (!disambiguatorToken) {
1466
- throw new Error(`abnf: probe-dispatch rule '${prod.name}' has unresolvable ` +
1467
- `disambiguator (kind=${disambiguator.kind})`);
1468
- }
1469
- // `bubble` lifts the committed child's node up — pure tree-building
1470
- // (a `@bubble$` builtin or a closure, per refs mode; dropped in
1471
- // recognition mode either way).
1472
- const bubbleFields = refs.bubble((r) => {
1473
- if (r.child && r.child.node !== undefined)
1474
- r.node = r.child.node;
1475
- });
1476
- if (useBuiltins) {
1477
- // Function-free dispatcher: control logic is engine `$`-builtins,
1478
- // the disambiguator token rides in `k` config. See
1479
- // docs/design/alt-action-refs.md §6.3.
1480
- ruleSpec[prod.name] = {
1481
- open: [
1482
- { c: '@probePhase0$', a: '@probeInit$', p: probeRule,
1483
- k: { pd_d: disambiguatorToken }, g: tag },
1484
- { c: '@probePhase1$', p: withBranch, g: tag },
1485
- { c: '@probePhase2$', p: noBranch, g: tag },
1486
- ],
1487
- close: [
1488
- { c: '@probePhase0$', a: '@probeDecide$', r: prod.name, g: tag },
1489
- { ...bubbleFields, g: tag },
1490
- ],
1491
- };
1492
- return;
1493
- }
1494
- const initMark = refs.register((r, ctx) => {
1495
- r.k.pd_phase = 0;
1496
- r.k.pd_mark = ctx.mark();
1497
- });
1498
- const decide = refs.register((r, ctx) => {
1499
- // ctx.t[0] is the first token the probe didn't consume. The probe
1500
- // never fails, so this always reflects a real position.
1501
- const peek = ctx.t[0];
1502
- ctx.rewind(r.k.pd_mark);
1503
- const matched = peek && peek.name === disambiguatorToken;
1504
- r.k.pd_phase = matched ? 1 : 2;
1505
- });
1506
- ruleSpec[prod.name] = {
1507
- open: [
1508
- // Phase 0 — first pass: mark and probe.
1509
- {
1510
- c: refs.register((r) => !r.k.pd_phase),
1511
- a: initMark,
1512
- p: probeRule,
1513
- g: tag,
1514
- },
1515
- // Phase 1 — disambiguator was seen: commit to X D Y.
1516
- {
1517
- c: refs.register((r) => r.k.pd_phase === 1),
1518
- p: withBranch,
1519
- g: tag,
1520
- },
1521
- // Phase 2 — disambiguator was not seen: commit to Y alone.
1522
- {
1523
- c: refs.register((r) => r.k.pd_phase === 2),
1524
- p: noBranch,
1525
- g: tag,
1526
- },
1527
- ],
1528
- close: [
1529
- // Phase 0 close: decide phase based on peek, rewind, retry self.
1530
- {
1531
- c: refs.register((r) => r.k.pd_phase === 0),
1532
- a: decide,
1533
- r: prod.name,
1534
- g: tag,
1535
- },
1536
- // Phase 1 / 2 close: lift the committed child's node up.
1537
- { ...bubbleFields, g: tag },
1538
- ],
1539
- };
1540
- }
1541
- // Built-in engine lexer tokens that an ABNF rule may reference by a bare
1542
- // uppercase name, mapping the name to the token the lexer emits. This lets a
1543
- // grammar say `ident = TX` to match the lexer's whole-word text token rather
1544
- // than re-deriving identifiers char-by-char (which, since whitespace is
1545
- // ignored between tokens, would greedily merge across spaces). A user (or
1546
- // core) rule of the same name always wins.
1547
- const BUILTIN_TOKENS = {
1548
- TX: '#TX', // bareword / identifier (text matcher)
1549
- NR: '#NR', // number (number matcher)
1550
- ST: '#ST', // quoted string (string matcher)
1551
- VL: '#VL', // keyword value: true / false / null (value matcher)
1552
- };
1553
- // The one prose directive the compiler acts on. Matched case-insensitively
1554
- // after trimming, so `<remove>`, `<Remove>` and `< remove >` all work.
1555
- const REMOVE_PROSE = 'remove';
1556
- // The one prose *name*: `<all> = <remove>` clears the whole grammar.
1557
- const REMOVE_ALL = 'all';
1558
- // A production whose name came from a prose token keeps its angle
1559
- // brackets, which no ordinary rulename can contain — so `<all>` and a
1560
- // production actually called `all` stay distinct.
1561
- const isProseName = (name) => name.startsWith('<') && name.endsWith('>');
1562
- // Resolve RFC 5234 `prose-val` terminals (`<free text>`).
1563
- //
1564
- // Prose is informational by definition — RFC 5234 §4 calls it a "last
1565
- // resort" escape hatch for describing a terminal in English when no
1566
- // formal notation will do. There is nothing to compile, so the converter
1567
- // accepts it in exactly one position: as the *entire* body of a
1568
- // production naming a built-in lexer token.
1569
- //
1570
- // NR = <number>
1571
- //
1572
- // That line documents, in the grammar text, that `NR` is the engine's
1573
- // number token. The lexer already supplies it, so the production is
1574
- // dropped here and every `NR` reference falls through to
1575
- // `normalizeBuiltinTokens`, which binds it to `#NR`. This is what lets a
1576
- // grammar state its terminals explicitly and still round-trip: the same
1577
- // line is what @tabnas/debug emits when it renders a live grammar back
1578
- // to ABNF.
1579
- //
1580
- // Prose anywhere else has no definition behind it, so it is an error
1581
- // rather than a silently-ignored rule.
1582
- function resolveProseTerminals(grammar) {
1583
- const isProse = (el) => el.kind === 'prose';
1584
- const kept = [];
1585
- for (const prod of grammar.productions) {
1586
- const onlyProse = prod.alts.length === 1 &&
1587
- prod.alts[0].length === 1 &&
1588
- isProse(prod.alts[0][0]);
1589
- // A prose name is only ever the removal directive. Checked here as
1590
- // well as in the prose-body branch below, because `<all> = "x"` has
1591
- // a *literal* body and would otherwise fall through and be lifted
1592
- // into a token literally named `#<all>`.
1593
- if (isProseName(prod.name) && !onlyProse) {
1594
- throw new Error(`abnf: '${prod.name}' is prose, and prose is only valid as a ` +
1595
- `production name for the removal directive '<all> = <remove>'.`);
1596
- }
1597
- if (onlyProse) {
1598
- const text = prod.alts[0][0].text;
1599
- // `<remove>` — the one prose form that *does* compile to something.
1600
- // Prose is otherwise informational, which is exactly why it is the
1601
- // right place for a directive: it cannot collide with a real
1602
- // terminal, and RFC 5234 already says a tool may interpret it.
1603
- //
1604
- // name = <remove> drop that rule and that fixed token
1605
- // * = <remove> drop everything — a fresh empty grammar
1606
- if (REMOVE_PROSE === text.trim().toLowerCase()) {
1607
- if (isProseName(prod.name)) {
1608
- const target = prod.name.slice(1, -1).trim().toLowerCase();
1609
- if (REMOVE_ALL !== target) {
1610
- throw new Error(`abnf: '<${target}>' is not a removal target. The only prose ` +
1611
- `name is '<all>', as in '<all> = <remove>', which clears the ` +
1612
- `whole grammar. To remove one rule or token, name it directly: ` +
1613
- `'${target} = <remove>'.`);
1614
- }
1615
- grammar.clearAll = true;
1616
- }
1617
- else {
1618
- (grammar.remove ??= []).push(prod.name);
1619
- }
1620
- continue;
1621
- }
1622
- if (isProseName(prod.name)) {
1623
- throw new Error(`abnf: '${prod.name}' is prose, and prose is only valid as a ` +
1624
- `production name for the removal directive '<all> = <remove>'.`);
1625
- }
1626
- if (BUILTIN_TOKENS[prod.name])
1627
- continue; // informational — the lexer defines it
1628
- throw new Error(`abnf: rule '${prod.name}' is defined only by prose ('<${text}>'), ` +
1629
- `which describes a terminal but does not define one. Prose is ` +
1630
- `allowed only for built-in lexer tokens (${Object.keys(BUILTIN_TOKENS).join(', ')}).`);
1631
- }
1632
- // Any surviving prose is embedded in a larger expression, where it
1633
- // cannot be given a meaning. Search nested groups and repetitions
1634
- // too, so `x = ( <foo> / "a" )` reports the same clear error as a
1635
- // top-level `x = "a" <foo>`.
1636
- const findStray = (el) => {
1637
- if (isProse(el))
1638
- return el;
1639
- if (el.kind === 'opt' || el.kind === 'star' || el.kind === 'plus' ||
1640
- el.kind === 'rep') {
1641
- return findStray(el.inner);
1642
- }
1643
- if (el.kind === 'group') {
1644
- for (const alt of el.alts) {
1645
- for (const inner of alt) {
1646
- const hit = findStray(inner);
1647
- if (hit)
1648
- return hit;
1649
- }
1650
- }
1651
- }
1652
- return undefined;
1653
- };
1654
- for (const alt of prod.alts) {
1655
- for (const el of alt) {
1656
- const stray = findStray(el);
1657
- if (stray) {
1658
- throw new Error(`abnf: rule '${prod.name}' uses prose ('<${stray.text}>') inside an ` +
1659
- `expression; prose may only stand alone as the whole definition ` +
1660
- `of a built-in lexer token.`);
1661
- }
1662
- }
1663
- }
1664
- kept.push(prod);
1665
- }
1666
- if (kept.length === 0 && !grammar.clearAll &&
1667
- (undefined === grammar.remove || 0 === grammar.remove.length)) {
1668
- throw new Error('abnf: grammar defines no rules — only informational prose terminals.');
1669
- }
1670
- grammar.productions = kept;
1671
- }
1672
- // Which token names belong to a lexer matcher is the engine's rule, and
1673
- // the engine is where it is enforced (a matcher binding throws from
1674
- // `configure()`). This compiler asks rather than keeping its own copy,
1675
- // so the two cannot drift: an engine that grows a matcher token gets the
1676
- // right compilation here without a matching edit.
1677
- //
1678
- // The distinction still matters at compile time, because it selects how
1679
- // a single-literal production is compiled, not whether it is allowed:
1680
- //
1681
- // CA = ";" fixed token — a literal by definition, so this
1682
- // rebinds #CA, exactly as `fixed: { token: … }` would
1683
- // TX = "literal" matcher token — cannot be rebound, so the production
1684
- // stays an ordinary rule shadowing the bareword
1685
- //
1686
- // ABNF production names are bare (`TX`), engine token names are prefixed
1687
- // (`#TX`).
1688
- const isMatcherTokenName = (name) => {
1689
- const fn = parser_1.util.isMatcherToken;
1690
- if ('function' !== typeof fn) {
1691
- throw new Error('abnf: this @tabnas/parser is too old — it does not export ' +
1692
- 'util.isMatcherToken, which the compiler needs to tell a fixed ' +
1693
- 'token from a matcher-owned one. Upgrade @tabnas/parser.');
1694
- }
1695
- return fn('#' + name);
1696
- };
1697
- // Lift single-literal productions into *named lexer tokens*.
1698
- //
1699
- // A production whose whole body is one string literal is a lexical
1700
- // definition, not a syntactic rule:
1701
- //
1702
- // PL = "+"
1703
- //
1704
- // Compiled as a rule it would produce a token named after the literal
1705
- // text — and since `+` has no word characters to name it after, that
1706
- // degrades to `#T` / `#T1` / … — plus a one-token `PL` rule wrapping it,
1707
- // so a grammar rendered back to ABNF reads `PL = T` with a separate
1708
- // `T = "+"`. Lifting instead binds the literal to `#PL` directly and
1709
- // drops the rule, which is exactly how the same grammar is written by
1710
- // hand against the engine (`fixed: { token: { '#PL': '+' } }`), and what
1711
- // lets `PL = "+"` survive the round-trip through @tabnas/debug unchanged.
1712
- //
1713
- // The start rule is never lifted: it has to stay a rule for the grammar
1714
- // to have an entry point, so `greet = "hi"` still compiles to a rule.
1715
- // Multi-alternative productions (`sign = "+" / "-"`) are real choices and
1716
- // are left alone, as are the RFC 5234 core rules, which callers expect to
1717
- // behave as rules wherever they are referenced.
1718
- //
1719
- // Naming an existing fixed token *redefines* it: `CA = ";"` binds the
1720
- // comma token to a semicolon, the same as `fixed: { token: { '#CA': ';' } }`
1721
- // by hand. Matcher-owned names are never lifted — see
1722
- // isMatcherTokenName.
1723
- function liftLiteralTokens(grammar, start) {
1724
- const lifted = new Map();
1725
- for (const prod of grammar.productions) {
1726
- if (prod.name === start)
1727
- continue;
1728
- // A matcher-owned name is never lifted. `TX = "literal"` stays an
1729
- // ordinary rule that shadows the bareword for references inside this
1730
- // grammar (see token.test.js, 'a user rule of the same name wins over
1731
- // the built-in') — which leaves #TX itself untouched. Lifting would
1732
- // instead try to bind #TX to a literal, which the engine refuses.
1733
- if (isMatcherTokenName(prod.name))
1734
- continue;
1735
- if (prod.nodeKind === 'core')
1736
- continue;
1737
- if (prod.alts.length !== 1 || prod.alts[0].length !== 1)
1738
- continue;
1739
- const el = prod.alts[0][0];
1740
- if (el.kind !== 'term')
1741
- continue;
1742
- // `path-empty = ""` (RFC 3986) matches the empty string — a rule that
1743
- // derives epsilon, not a token the lexer could ever emit.
1744
- if (el.literal === '')
1745
- continue;
1746
- lifted.set(prod.name, { literal: el.literal, caseSensitive: el.caseSensitive });
1747
- }
1748
- // The engine keys its fixed tokens by literal (`cfg.fixed.token` is
1749
- // inverted to src -> tin), so one literal is one token — two names for
1750
- // the same literal cannot both become tokens. When `A = "+"` and
1751
- // `B = "+"` both claim `+`, lifting either would silently drop the
1752
- // other, so neither is lifted and both stay ordinary rules.
1753
- const byLiteral = new Map();
1754
- for (const [name, lit] of lifted) {
1755
- const key = termKey(lit);
1756
- const names = byLiteral.get(key);
1757
- if (names)
1758
- names.push(name);
1759
- else
1760
- byLiteral.set(key, [name]);
1761
- }
1762
- for (const names of byLiteral.values()) {
1763
- if (1 < names.length)
1764
- for (const n of names)
1765
- lifted.delete(n);
1766
- }
1767
- if (lifted.size === 0)
1768
- return [];
1769
- const walk = (el) => {
1770
- if (el.kind === 'ref') {
1771
- const lit = lifted.get(el.name);
1772
- return lit
1773
- ? { kind: 'term', ...lit, tokenName: el.name }
1774
- : el;
1775
- }
1776
- if (el.kind === 'opt' || el.kind === 'star' || el.kind === 'plus' ||
1777
- el.kind === 'rep') {
1778
- return { ...el, inner: walk(el.inner) };
1779
- }
1780
- if (el.kind === 'group') {
1781
- return { kind: 'group', alts: el.alts.map((a) => a.map(walk)) };
1782
- }
1783
- return el;
1784
- };
1785
- grammar.productions = grammar.productions
1786
- .filter((p) => !lifted.has(p.name))
1787
- .map((p) => ({ ...p, alts: p.alts.map((alt) => alt.map(walk)) }));
1788
- // Return every lifted definition, referenced or not. The production is
1789
- // gone from the grammar, so an unreferenced one (`top = "x"` with a
1790
- // spare `PL = "+"`) would otherwise leave no element behind for the
1791
- // emitter to allocate a token from, and the user's declaration would
1792
- // vanish silently instead of compiling to the promised named token.
1793
- return [...lifted].map(([name, lit]) => ({ kind: 'term', ...lit, tokenName: name }));
1794
- }
1795
- // Rewrite every bareword reference whose name is a built-in token AND is not a
1796
- // defined production into a `token` terminal element. Run before any other
1797
- // pass so the rest of the pipeline treats these as ordinary terminals.
1798
- function normalizeBuiltinTokens(grammar) {
1799
- const defined = new Set(grammar.productions.map((p) => p.name));
1800
- const walk = (el) => {
1801
- if (el.kind === 'ref') {
1802
- const tok = BUILTIN_TOKENS[el.name];
1803
- if (tok && !defined.has(el.name))
1804
- return { kind: 'token', name: tok };
1805
- return el;
1806
- }
1807
- if (el.kind === 'opt' || el.kind === 'star' || el.kind === 'plus' ||
1808
- el.kind === 'rep') {
1809
- return { ...el, inner: walk(el.inner) };
1810
- }
1811
- if (el.kind === 'group') {
1812
- return { kind: 'group', alts: el.alts.map((a) => a.map(walk)) };
1813
- }
1814
- return el;
1815
- };
1816
- for (const prod of grammar.productions) {
1817
- prod.alts = prod.alts.map((alt) => alt.map(walk));
1818
- }
1819
- }
1820
- // Allocate the lexer token for a string-literal terminal. A
1821
- // case-sensitive literal is normally a fixed token and a case-insensitive
1822
- // one an anchored `i`-flagged regex. When `wordKeywords` is set and the
1823
- // literal ends in a word character, it is emitted as a regex with a
1824
- // trailing `(?![A-Za-z0-9_])` guard so the keyword matches only as a whole
1825
- // word (e.g. `option` won't match inside `optional`).
1826
- function emitLiteralToken(el, name, fixedTokens, matchTokens, wordKeywords) {
1827
- const boundary = wordKeywords && /[A-Za-z0-9_]$/.test(el.literal)
1828
- ? '(?![A-Za-z0-9_])'
1829
- : '';
1830
- if (isEffectivelyCaseSensitive(el) && boundary === '') {
1831
- fixedTokens[name] = el.literal;
1832
- return;
1833
- }
1834
- // Insensitive literal, or a word-keyword needing a boundary guard:
1835
- // emit as an anchored regex. Mark it `eager$` so the lexer fires it
1836
- // even when the current rule's tcol doesn't list its tin.
1837
- const flags = isEffectivelyCaseSensitive(el) ? '' : 'i';
1838
- const re = new RegExp('^' + escapeRegExp(el.literal) + boundary, flags);
1839
- re.eager$ = true;
1840
- matchTokens[name] = re;
1841
- }
1842
- // Copy a grammar deeply enough that the emit pipeline cannot disturb the
1843
- // caller's AST. The passes below replace `productions`, `alts` and the
1844
- // individual sequences, but treat elements as immutable (each rewriting
1845
- // walk returns fresh element objects), so sharing elements is safe.
1846
- function cloneGrammar(grammar) {
1847
- return {
1848
- productions: grammar.productions.map((p) => ({
1849
- ...p,
1850
- alts: p.alts.map((alt) => alt.slice()),
1851
- })),
1852
- };
1853
- }
1854
- // Convert an ABNF grammar AST into a tabnas GrammarSpec.
1855
- function emitGrammarSpec(grammar, opts) {
1856
- // Work on a copy: `resolveProseTerminals`, `liftLiteralTokens` and
1857
- // `normalizeBuiltinTokens` all rewrite the grammar in place, so emitting
1858
- // twice from one `parseAbnf` result would otherwise give two different
1859
- // specs — the second missing every lifted production, since the first
1860
- // pass had already removed them.
1861
- grammar = cloneGrammar(grammar);
1862
- // Drop informational prose definitions (`NR = <number>`) first, so the
1863
- // names they document fall through to the built-in tokens — and so a
1864
- // leading prose line is never mistaken for the start rule.
1865
- resolveProseTerminals(grammar);
1866
- // Capture the <remove> directives before the rewrite passes below:
1867
- // each returns a fresh grammar object carrying only `productions`, so
1868
- // anything else on the grammar is dropped at the first reassignment.
1869
- const removeNames = grammar.remove ? [...grammar.remove] : [];
1870
- const clearAll = !!grammar.clearAll;
1871
- const start = opts?.start ?? grammar.productions[0].name;
1872
- const tag = opts?.tag ?? 'abnf';
1873
- const wordKeywords = !!opts?.wordKeywords;
1874
- // Turn single-literal productions (`PL = "+"`) into named lexer
1875
- // tokens, then resolve bare built-in token names (`TX`/`NR`/`ST`/`VL`)
1876
- // to token terminals — both before any structural pass sees them as
1877
- // rule references.
1878
- const liftedLiterals = liftLiteralTokens(grammar, start);
1879
- normalizeBuiltinTokens(grammar);
1880
- // Eliminate direct left recursion (P → P α | β) by rewriting to
1881
- // the equivalent right-recursive form P → β (α)*, then detect
1882
- // ambiguous `[X D] Y` optional-prefix patterns and rewrite them
1883
- // into probe-dispatch helpers; finally flatten any EBNF sugar
1884
- // (`?`, `*`, `+`, grouping) into plain ABNF.
1885
- grammar = eliminateLeftRecursion(grammar);
1886
- grammar = rewriteProbeDispatches(grammar);
1887
- grammar = rewriteTailRepeats(grammar, start);
1888
- grammar = desugar(grammar);
1889
- // Allocate a fixed token for each unique literal, and a match
1890
- // token for each unique regex terminal. Literals are keyed by
1891
- // (literal, effective-case-sensitivity) so a `%s"foo"` (sensitive)
1892
- // and a bare `"foo"` (insensitive) produce distinct tokens.
1893
- const literals = new Map(); // literal-key -> token name
1894
- const regexTokens = new Map(); // regex key -> token name
1895
- const usedNames = new Set();
1896
- const fixedTokens = {};
1897
- const matchTokens = {};
1898
- // Gather every terminal first. Probe-helper productions store their
1899
- // vocab as AbnfElements rather than in `alts`, so walk those too. The
1900
- // lifted literals are seeded up front: their productions no longer
1901
- // exist, so an unreferenced one has no element anywhere in `alts`.
1902
- const terminals = [...liftedLiterals];
1903
- for (const prod of grammar.productions) {
1904
- for (const alt of prod.alts)
1905
- terminals.push(...alt);
1906
- if (prod.probeHelper)
1907
- terminals.push(...prod.probeHelper.vocabElements);
1908
- }
1909
- // Terminals carrying a lifted production name are allocated first, so
1910
- // the name wins even when the same literal also appears inline in an
1911
- // earlier rule (`PL = "+"` must yield `#PL`, not `#T`, regardless of
1912
- // where the bare `"+"` shows up).
1913
- const named = terminals.filter((el) => el.kind === 'term' && el.tokenName);
1914
- for (const el of [...named, ...terminals]) {
1915
- if (el.kind === 'term') {
1916
- const key = termKey(el);
1917
- if (!literals.has(key)) {
1918
- const name = allocTokenName(el.literal, usedNames, el.tokenName);
1919
- literals.set(key, name);
1920
- emitLiteralToken(el, name, fixedTokens, matchTokens, wordKeywords);
1921
- }
1922
- }
1923
- else if (el.kind === 'regex') {
1924
- const key = regexKey(el);
1925
- if (!regexTokens.has(key)) {
1926
- const name = allocTokenName('rx_' + el.pattern, usedNames);
1927
- regexTokens.set(key, name);
1928
- matchTokens[name] = new RegExp('^' + el.pattern, el.flags);
1929
- }
1930
- }
1931
- }
1932
- const knownRules = new Set(grammar.productions.map((p) => p.name));
1933
- const { firstSets, nullable } = computeFirstSets(grammar, literals, regexTokens);
1934
- const refs = new RefRegistry();
1935
- refs.useBuiltins = !!opts?.builtins;
1936
- refs.emitMarks = !!opts?.marks;
1937
- const ruleSpec = {};
1938
- for (const prod of grammar.productions) {
1939
- if (prod.probeHelper) {
1940
- emitProbeHelper(prod, tag, ruleSpec, literals, regexTokens);
1941
- continue;
1942
- }
1943
- if (prod.probeDispatch) {
1944
- emitProbeDispatch(prod, tag, ruleSpec, refs, literals, regexTokens, !!opts?.builtins);
1945
- continue;
1946
- }
1947
- // Standard path: a (possibly single-segment) set of alternatives
1948
- // compiled to tabnas alts. Simple alts collapse into `open` alts
1949
- // directly; multi-segment alts emit a chain of aux rules.
1950
- emitProduction(prod, grammar, literals, regexTokens, knownRules, tag, ruleSpec, firstSets, nullable, refs);
1951
- }
1952
- // Wrap the user-visible start rule in a synthetic rule that
1953
- // explicitly consumes #ZZ. Without this, a user rule that pops
1954
- // without matching the end-of-source token lets trailing content
1955
- // slip past tabnas's post-loop endtkn check (the lookahead buffer
1956
- // outlives the parse loop).
1957
- const startWrapper = '__start__';
1958
- ruleSpec[startWrapper] = {
1959
- open: [{
1960
- p: start,
1961
- g: tag,
1962
- }],
1963
- close: [{
1964
- s: '#ZZ',
1965
- // Return the start rule's AST node directly — the `__start__`
1966
- // wrapper exists only to ensure end-of-source gets consumed.
1967
- // The caller of `tabnas(src)` receives the tagged user-rule
1968
- // node (e.g. `{rule: 'URI', src, kids: [...]}`) unadorned.
1969
- ...refs.bubble((r) => {
1970
- if (r.child && r.child.node !== undefined) {
1971
- r.node = r.child.node;
1972
- }
1973
- }),
1974
- g: tag,
1975
- }],
1976
- };
1977
- const options = {
1978
- fixed: { token: fixedTokens },
1979
- rule: { start: startWrapper },
1980
- };
1981
- if (Object.keys(matchTokens).length > 0) {
1982
- options.match = { token: matchTokens };
1983
- }
1984
- const spec = {
1985
- ref: refs.map,
1986
- options,
1987
- rule: ruleSpec,
1988
- };
1989
- // `<remove>` directives. `* = <remove>` maps to the engine's `clear`,
1990
- // which wipes rules and fixed tokens before the rest of the spec is
1991
- // applied — so a grammar can reset an instance and rebuild it in one
1992
- // pass. A named removal drops both the rule and the fixed token of
1993
- // that name, because ABNF does not distinguish them at the point of
1994
- // use and a removal that matches nothing is a no-op either way.
1995
- if (clearAll) {
1996
- spec.clear = true;
1997
- }
1998
- if (0 < removeNames.length) {
1999
- for (const name of removeNames) {
2000
- ;
2001
- spec.rule[name] = null;
2002
- options.fixed.token['#' + name] = null;
2003
- }
2004
- }
2005
- return spec;
2006
- }
2007
- // Break an alternative into segments. Each segment is a (possibly
2008
- // empty) run of terminal tokens followed by at most one rule
2009
- // reference. A single-segment alt has at most one ref, located at the
2010
- // very end; everything else has two or more segments.
2011
- function segmentize(alt, literals, regexTokens) {
2012
- const segs = [];
2013
- let current = { terms: [], ref: null };
2014
- for (const el of alt) {
2015
- if (el.kind === 'term') {
2016
- current.terms.push(literals.get(termKey(el)));
2017
- }
2018
- else if (el.kind === 'regex') {
2019
- const key = regexKey(el);
2020
- current.terms.push(regexTokens.get(key));
2021
- }
2022
- else if (el.kind === 'token') {
2023
- current.terms.push(el.name);
2024
- }
2025
- else if (el.kind === 'ref') {
2026
- current.ref = el.name;
2027
- segs.push(current);
2028
- current = { terms: [], ref: null };
2029
- }
2030
- else {
2031
- // `opt`, `star`, `plus`, `group` must have been desugared
2032
- // before reaching the emitter.
2033
- throw new Error(`abnf: internal — unexpected element kind '${el.kind}' in emitter`);
2034
- }
2035
- }
2036
- if (current.terms.length > 0 || segs.length === 0) {
2037
- segs.push(current);
2038
- }
2039
- return segs;
2040
- }
2041
- function regexKey(el) {
2042
- return `/${el.pattern}/${el.flags}`;
2043
- }
2044
- function isSingleSegment(alt) {
2045
- let sawRef = false;
2046
- for (const el of alt) {
2047
- if (el.kind === 'ref') {
2048
- if (sawRef)
2049
- return false;
2050
- sawRef = true;
2051
- }
2052
- else if (el.kind === 'term' || el.kind === 'regex' ||
2053
- el.kind === 'token') {
2054
- if (sawRef)
2055
- return false; // terminal after a ref — multi-segment
2056
- }
2057
- else {
2058
- // Desugar should have eliminated sugar kinds.
2059
- return false;
2060
- }
2061
- }
2062
- return true;
2063
- }
2064
- function validateRefs(alt, knownRules, ruleName) {
2065
- for (const el of alt) {
2066
- if (el.kind === 'ref' && !knownRules.has(el.name)) {
2067
- throw new Error(`abnf: rule '${ruleName}' references unknown rule '${el.name}'`);
2068
- }
2069
- }
2070
- }
2071
- // Registry used by the emitter to allocate unique `@`-prefixed
2072
- // FuncRef names for inline action functions. The resulting spec is
2073
- // still declarative: every function appears once, keyed by name,
2074
- // under the spec's `ref` map.
2075
- class RefRegistry {
2076
- constructor() {
2077
- this.refs = {};
2078
- this.counter = 0;
2079
- // When set, tree-building actions are emitted as engine `$`-builtin
2080
- // refs + `k` config (pure data) instead of registered closures. See
2081
- // docs/design/alt-action-refs.md §6.4 and implementation-diary.md.
2082
- this.useBuiltins = false;
2083
- // When set, the emitter stamps user-rule alts with a `m` mark.
2084
- this.emitMarks = false;
2085
- }
2086
- register(fn) {
2087
- const name = `@abnf_a${this.counter++}`;
2088
- this.refs[name] = fn;
2089
- return name;
2090
- }
2091
- get map() {
2092
- return this.refs;
2093
- }
2094
- // Tree-action emitters. Each returns the alt-spec fields to merge
2095
- // (`{a}` in closure mode, `{a, k}` in builtins mode).
2096
- node(cfg, closure) {
2097
- return this.useBuiltins
2098
- ? { a: '@node$', k: { node$: cfg } }
2099
- : { a: this.register(closure) };
2100
- }
2101
- capture(cfg, closure) {
2102
- return this.useBuiltins
2103
- ? { a: '@capture$', k: { capture$: cfg } }
2104
- : { a: this.register(closure) };
2105
- }
2106
- bubble(closure) {
2107
- return this.useBuiltins ? { a: '@bubble$' } : { a: this.register(closure) };
2108
- }
2109
- fold(cfg, closure) {
2110
- return this.useBuiltins
2111
- ? { a: '@fold$', k: { fold$: cfg } }
2112
- : { a: this.register(closure) };
2113
- }
2114
- }
2115
- // Closure-mode twin of the engine's `@fold$` builtin (see
2116
- // `@tabnas/parser` builtins.ts — the two MUST stay behaviourally
2117
- // identical; the fixture suite pins this). Folds a tail-repeat
2118
- // iteration's node into its parent as a sibling kid, appends `cN`
2119
- // close-phase (separator) tokens' src to the parent, and clears the
2120
- // own node so the parent's capture no-ops on its stale first-iteration
2121
- // child pointer.
2122
- function mkFoldClosure(cN) {
2123
- return (r) => {
2124
- const p = r.parent && r.parent.node;
2125
- if (null == p || 'object' !== typeof p || !('src' in p))
2126
- return;
2127
- const own = r.node;
2128
- if (null != own && 'object' === typeof own && 'src' in own && own !== p) {
2129
- p.src += own.src;
2130
- if (own.rule)
2131
- p.kids.push(own);
2132
- else if (Array.isArray(own.kids))
2133
- p.kids.push(...own.kids);
2134
- }
2135
- for (let i = 0; i < cN; i++)
2136
- p.src += r.c[i].src;
2137
- r.node = undefined;
2138
- };
2139
- }
2140
- function mkAstNode(ruleName, nodeKind) {
2141
- return nodeKind === 'user'
2142
- ? { rule: ruleName, src: '', kids: [] }
2143
- : { src: '', kids: [] };
2144
- }
2145
- // A stable, human-predictable "mark" for an alternative — its leading
2146
- // discriminator: the first matched token name (sans `#`), the pushed
2147
- // rule name, or `_` for the empty alt. Used for `@<rule>:o|c:<mark>`
2148
- // user-action references. See docs/design/alt-action-refs.md §3.
2149
- function altDiscriminator(alt, literals, regexTokens) {
2150
- if (alt.length === 0)
2151
- return '_';
2152
- const el = alt[0];
2153
- if (el.kind === 'term') {
2154
- return (literals.get(termKey(el)) || '').replace(/^#/, '') || '_';
2155
- }
2156
- if (el.kind === 'regex') {
2157
- return (regexTokens.get(regexKey(el)) || '').replace(/^#/, '') || '_';
2158
- }
2159
- if (el.kind === 'token')
2160
- return el.name.replace(/^#/, '') || '_';
2161
- if (el.kind === 'ref')
2162
- return el.name;
2163
- return '_';
2164
- }
2165
- // Assign a unique mark per source alternative (same alt object → same
2166
- // mark, so fan-out copies share it). Collisions get a `~N` suffix.
2167
- function assignMarks(alts, literals, regexTokens) {
2168
- const marks = new Map();
2169
- const seen = new Map();
2170
- for (const alt of alts) {
2171
- const base = altDiscriminator(alt, literals, regexTokens);
2172
- const n = (seen.get(base) || 0) + 1;
2173
- seen.set(base, n);
2174
- marks.set(alt, n === 1 ? base : `${base}~${n}`);
2175
- }
2176
- return marks;
2177
- }
2178
- function segmentToAlt(seg, tag, refs, initNode, ruleName, nodeKind) {
2179
- const spec = { g: tag };
2180
- if (seg.terms.length > 0)
2181
- spec.s = seg.terms.join(' ');
2182
- if (seg.ref)
2183
- spec.p = seg.ref;
2184
- // Default tree-building: accumulate each matched terminal's source
2185
- // text into `r.node.src`. Head alts also allocate a fresh AST node
2186
- // so the child doesn't inherit (and then mutate) its parent's.
2187
- const nterms = seg.terms.length;
2188
- if (nterms > 0 || initNode) {
2189
- Object.assign(spec, refs.node({ init: initNode, rule: ruleName, kind: nodeKind, nterms }, (r) => {
2190
- if (initNode)
2191
- r.node = mkAstNode(ruleName, nodeKind);
2192
- const n = r.node;
2193
- for (let i = 0; i < nterms; i++)
2194
- n.src += r.o[i].src;
2195
- }));
2196
- }
2197
- return spec;
2198
- }
2199
- // Close-state action: merge the just-returned child rule's AST node
2200
- // into the current rule's. Tagged children (user rules) get pushed
2201
- // verbatim into `kids`; untagged (helper / core) flatten — their
2202
- // `src` appends and their `kids` extend. Either way `src`
2203
- // concatenates so every ancestor's `.src` reflects everything it
2204
- // matched.
2205
- function captureChildFields(refs, ruleName, nodeKind) {
2206
- return refs.capture({ rule: ruleName, kind: nodeKind }, (r) => {
2207
- if (r.node == null)
2208
- r.node = mkAstNode(ruleName, nodeKind);
2209
- const n = r.node;
2210
- const c = r.child && r.child.node;
2211
- if (c == null)
2212
- return;
2213
- if (typeof c !== 'object' || !('src' in c)) {
2214
- // Legacy shape — wrap as a leaf kid.
2215
- n.kids.push(c);
2216
- return;
2217
- }
2218
- // Defensive: if the child somehow shares this rule's node
2219
- // object, skip the merge rather than push a self-reference. (A
2220
- // properly-emitted grammar always allocates fresh child nodes.)
2221
- if (c === n)
2222
- return;
2223
- n.src += c.src;
2224
- if (c.rule)
2225
- n.kids.push(c);
2226
- else if (Array.isArray(c.kids))
2227
- n.kids.push(...c.kids);
2228
- });
2229
- }
2230
- // Emit a production marked by `rewriteTailRepeats`:
2231
- //
2232
- // open: [ { s: prefix, node$ init } ]
2233
- // close: [ { s: sep, r: SELF, fold$ cN } , { fold$ } ]
2234
- //
2235
- // The same shape a hand-written tabnas grammar uses for `X = a [ b X ]`.
2236
- // Every iteration folds itself into the parent (see mkFoldClosure /
2237
- // `@fold$`); marks land on the close alts too, so `@X:c:<sep>` and
2238
- // `@X:c:_` user actions can attach.
2239
- function emitTailRepeat(prod, literals, regexTokens, tag, ruleSpec, refs) {
2240
- const prodKind = prod.nodeKind ?? 'user';
2241
- const prefixAlt = prod.alts[0];
2242
- const sep = prod.tailRepeat.sep;
2243
- // All-terminal sequences (guaranteed by the rewrite's guards), so
2244
- // each segmentizes to exactly one ref-free segment.
2245
- const prefixSeg = segmentize(prefixAlt, literals, regexTokens)[0];
2246
- const sepSeg = segmentize(sep, literals, regexTokens)[0];
2247
- const marks = (prodKind === 'user' && refs.emitMarks)
2248
- ? assignMarks([prefixAlt, sep], literals, regexTokens)
2249
- : null;
2250
- const open = segmentToAlt(prefixSeg, tag, refs, true, prod.name, prodKind);
2251
- if (marks)
2252
- open.m = marks.get(prefixAlt);
2253
- const repeat = {
2254
- s: sepSeg.terms.join(' '),
2255
- r: prod.name,
2256
- ...refs.fold({ cN: sepSeg.terms.length }, mkFoldClosure(sepSeg.terms.length)),
2257
- g: tag,
2258
- };
2259
- if (marks)
2260
- repeat.m = marks.get(sep);
2261
- const end = {
2262
- ...refs.fold({}, mkFoldClosure(0)),
2263
- g: tag,
2264
- };
2265
- if (marks)
2266
- end.m = '_';
2267
- ruleSpec[prod.name] = { open: [open], close: [repeat, end] };
2268
- }
2269
- function emitProduction(prod, grammar, literals, regexTokens, knownRules, tag, ruleSpec, firstSets, nullable, refs) {
2270
- for (const alt of prod.alts) {
2271
- validateRefs(alt, knownRules, prod.name);
2272
- }
2273
- if (prod.tailRepeat) {
2274
- emitTailRepeat(prod, literals, regexTokens, tag, ruleSpec, refs);
2275
- return;
2276
- }
2277
- const allSimple = prod.alts.every(isSingleSegment);
2278
- if (allSimple) {
2279
- // Every alternative collapses to one tabnas alt — emit them
2280
- // directly into the production's open state. This is a head
2281
- // rule, so each alt initialises its own node array. Empty alts
2282
- // are sorted to the end so tabnas's first-match-wins doesn't let
2283
- // them short-circuit non-empty alternatives.
2284
- const ordered = [
2285
- ...prod.alts.filter((alt) => alt.length > 0),
2286
- ...prod.alts.filter((alt) => alt.length === 0),
2287
- ];
2288
- // Ref-only alternatives have no terminal to discriminate on, so
2289
- // tabnas's first-match-wins would silently let them shadow any
2290
- // later alternative. Guard them with FIRST-set peeks when the
2291
- // production has more than one alt.
2292
- const prodKind = prod.nodeKind ?? 'user';
2293
- const marks = (prodKind === 'user' && refs.emitMarks)
2294
- ? assignMarks(ordered, literals, regexTokens)
2295
- : null;
2296
- const needsPeek = ordered.length > 1;
2297
- const opens = [];
2298
- for (const alt of ordered) {
2299
- const segs = segmentize(alt, literals, regexTokens);
2300
- const seg = segs[0];
2301
- const isRefOnly = alt.length >= 1 &&
2302
- alt.every((el) => el.kind === 'ref') &&
2303
- seg.terms.length === 0 &&
2304
- seg.ref != null;
2305
- const mark = marks ? marks.get(alt) : undefined;
2306
- if (needsPeek && isRefOnly) {
2307
- const firstTokens = firstOfAlt(alt, literals, regexTokens, firstSets, nullable);
2308
- if (firstTokens) {
2309
- for (const tok of firstTokens) {
2310
- const o = {
2311
- s: tok,
2312
- b: 1,
2313
- p: seg.ref,
2314
- ...refs.node({ init: true, rule: prod.name, kind: prodKind, nterms: 0 }, (r) => { r.node = mkAstNode(prod.name, prodKind); }),
2315
- g: tag,
2316
- };
2317
- if (mark)
2318
- o.m = mark;
2319
- opens.push(o);
2320
- }
2321
- continue;
2322
- }
2323
- }
2324
- const o = segmentToAlt(seg, tag, refs, true, prod.name, prodKind);
2325
- if (mark)
2326
- o.m = mark;
2327
- opens.push(o);
2328
- }
2329
- const rs = { open: opens };
2330
- // If any alt has a push, the close state must capture the
2331
- // returned child. Add a universal fallback close alt whose
2332
- // action is a no-op when there was no push.
2333
- if (prod.alts.some((alt) => alt.some((el) => el.kind === 'ref'))) {
2334
- const close = {
2335
- ...captureChildFields(refs, prod.name, prod.nodeKind ?? 'user'),
2336
- g: tag,
2337
- };
2338
- if (marks)
2339
- close.m = '_';
2340
- rs.close = [close];
2341
- }
2342
- ruleSpec[prod.name] = rs;
2343
- return;
2344
- }
2345
- if (prod.alts.length === 1) {
2346
- // Single-alt, multi-segment: chain rules directly on the
2347
- // production.
2348
- emitChain(prod.name, prod.alts[0], literals, regexTokens, tag, ruleSpec, refs, prod.nodeKind ?? 'user');
2349
- return;
2350
- }
2351
- // Multi-alt with at least one multi-segment alternative: emit a
2352
- // dispatcher. Each alt becomes its own chained impl rule
2353
- // (`<prodname>$alt<i>`); the main rule's open peeks the first token
2354
- // and pushes the matching impl rule. Using `p:` (not `r:`) keeps
2355
- // the parent's `child` pointer valid so the parent can read the
2356
- // impl's node in its close-state action.
2357
- const dispatchOpen = [];
2358
- let emptyAltSeen = false;
2359
- const dispatchMarks = ((prod.nodeKind ?? 'user') === 'user' && refs.emitMarks)
2360
- ? assignMarks(prod.alts, literals, regexTokens)
2361
- : null;
2362
- for (let i = 0; i < prod.alts.length; i++) {
2363
- const alt = prod.alts[i];
2364
- const implName = `${prod.name}$alt${i}`;
2365
- const mark = dispatchMarks ? dispatchMarks.get(alt) : undefined;
2366
- if (alt.length === 0) {
2367
- // Empty alt acts as fallback — handled after the loop.
2368
- emptyAltSeen = true;
2369
- continue;
2370
- }
2371
- emitChain(implName, alt, literals, regexTokens, tag, ruleSpec, refs, 'helper');
2372
- // Fan out this alt into one dispatch entry per concrete token
2373
- // sequence it can start with. Up to LOOKAHEAD_K tokens per
2374
- // prefix is enough for the grammars this converter targets; a
2375
- // ref with multiple alts produces one prefix per sub-alt so
2376
- // overlapping FIRST sets between competing alts can still be
2377
- // separated by their second (or later) token.
2378
- // The dispatcher itself is a user (or helper) rule — it must
2379
- // allocate its own AST node on every dispatch alt, otherwise the
2380
- // node inherited from the parent via makeRule(ctx, rule.node)
2381
- // would be shared and the dispatcher's captureChildRef would
2382
- // mutate the parent's tree.
2383
- const dispatchKind = prod.nodeKind ?? 'user';
2384
- const initDispatchFields = refs.node({ init: true, rule: prod.name, kind: dispatchKind, nterms: 0 }, (r) => { r.node = mkAstNode(prod.name, dispatchKind); });
2385
- const LOOKAHEAD_K = 4;
2386
- const prefixes = altPrefixes(alt, grammar, literals, regexTokens, LOOKAHEAD_K);
2387
- const usable = prefixes.filter((p) => p.length > 0);
2388
- if (usable.length > 0) {
2389
- for (const p of usable) {
2390
- const o = {
2391
- s: p.join(' '),
2392
- b: p.length,
2393
- p: implName,
2394
- ...initDispatchFields,
2395
- g: tag,
2396
- };
2397
- if (mark)
2398
- o.m = mark;
2399
- dispatchOpen.push(o);
2400
- }
2401
- }
2402
- else {
2403
- const firstTokens = firstOfAlt(alt, literals, regexTokens, firstSets, nullable);
2404
- if (firstTokens === null) {
2405
- throw new Error(`abnf: rule '${prod.name}' alternative ${i} is nullable ` +
2406
- `but is not the only empty alt; FIRST set is ambiguous`);
2407
- }
2408
- for (const tok of firstTokens) {
2409
- const o = {
2410
- s: tok, b: 1, p: implName, ...initDispatchFields, g: tag,
2411
- };
2412
- if (mark)
2413
- o.m = mark;
2414
- dispatchOpen.push(o);
2415
- }
2416
- }
2417
- }
2418
- if (emptyAltSeen) {
2419
- // Fallback: matches any token (or none), pops immediately with
2420
- // an empty tree. Tagged with the user rule name so a consumer
2421
- // walking the tree still gets a placeholder node for the empty
2422
- // alternative.
2423
- const fallbackKind = prod.nodeKind ?? 'user';
2424
- const o = {
2425
- ...refs.node({ init: true, rule: prod.name, kind: fallbackKind, nterms: 0 }, (r) => { r.node = mkAstNode(prod.name, fallbackKind); }),
2426
- g: tag,
2427
- };
2428
- if (dispatchMarks)
2429
- o.m = '_';
2430
- dispatchOpen.push(o);
2431
- }
2432
- const dispClose = {
2433
- // Merge the chosen impl's result up into the dispatcher's node,
2434
- // tagged with the user rule name (so the enclosing rule sees a
2435
- // `{rule, src, kids}` child, not the impl chain's transparent
2436
- // `{src, kids}`).
2437
- ...captureChildFields(refs, prod.name, prod.nodeKind ?? 'user'),
2438
- g: tag,
2439
- };
2440
- if (dispatchMarks)
2441
- dispClose.m = '_';
2442
- ruleSpec[prod.name] = { open: dispatchOpen, close: [dispClose] };
2443
- }
2444
- // Emit a (possibly single-step) chain of rules for one alt under the
2445
- // given head rule name. Segment 0 goes into `headName`; later
2446
- // segments get synthetic `<headName>$stepN` continuations.
2447
- //
2448
- // `headKind` controls the head rule's AST node shape: 'user' tags
2449
- // the head's node with the rule name; 'helper' leaves it untagged
2450
- // (transparent to the enclosing user rule). Step rules are always
2451
- // helpers — they inherit and accumulate into the head's node via
2452
- // `r:` replacement.
2453
- function emitChain(headName, alt, literals, regexTokens, tag, ruleSpec, refs, headKind = 'helper') {
2454
- const segs = segmentize(alt, literals, regexTokens);
2455
- const chainName = (i) => i === 0 ? headName : `${headName}$step${i}`;
2456
- for (let i = 0; i < segs.length; i++) {
2457
- const name = chainName(i);
2458
- const seg = segs[i];
2459
- const kind = i === 0 ? headKind : 'helper';
2460
- // Only the head of the chain initialises the node object; later
2461
- // steps inherit and continue to accumulate into it via `r:`.
2462
- const headAlt = segmentToAlt(seg, tag, refs, i === 0, name, kind);
2463
- // Single-alt user rule: the head alt is user-addressable.
2464
- if (i === 0 && headKind === 'user' && refs.emitMarks) {
2465
- headAlt.m = altDiscriminator(alt, literals, regexTokens);
2466
- }
2467
- const open = [headAlt];
2468
- const rs = { open };
2469
- const isLast = i === segs.length - 1;
2470
- if (!isLast) {
2471
- // Non-last step: after the push returns, capture the child's
2472
- // node and replace with the next step rule.
2473
- rs.close = [{
2474
- r: chainName(i + 1),
2475
- ...captureChildFields(refs, name, kind),
2476
- g: tag,
2477
- }];
2478
- }
2479
- else if (seg.ref) {
2480
- // Last step, but it had a push — we still need to capture the
2481
- // final child before popping.
2482
- rs.close = [{ ...captureChildFields(refs, name, kind), g: tag }];
2483
- }
2484
- ruleSpec[name] = rs;
2485
- }
2486
- }
2487
- // Compute FIRST(ref) for every production, plus which productions
2488
- // are nullable (can derive the empty string). Iterates to a fixed
2489
- // point. Terminals in FIRST sets are represented by their allocated
2490
- // token names (e.g. `#X`).
2491
- function computeFirstSets(grammar, literals, regexTokens) {
2492
- const firstSets = new Map();
2493
- const nullable = new Set();
2494
- for (const p of grammar.productions)
2495
- firstSets.set(p.name, new Set());
2496
- let changed = true;
2497
- while (changed) {
2498
- changed = false;
2499
- for (const prod of grammar.productions) {
2500
- const first = firstSets.get(prod.name);
2501
- for (const alt of prod.alts) {
2502
- // Walk the alt, accumulating FIRST until a non-nullable
2503
- // position is hit.
2504
- let altNullable = true;
2505
- for (const el of alt) {
2506
- if (el.kind === 'term' || el.kind === 'regex' ||
2507
- el.kind === 'token') {
2508
- const tok = el.kind === 'term'
2509
- ? literals.get(termKey(el))
2510
- : el.kind === 'token'
2511
- ? el.name
2512
- : regexTokens.get(regexKey(el));
2513
- if (!first.has(tok)) {
2514
- first.add(tok);
2515
- changed = true;
2516
- }
2517
- altNullable = false;
2518
- break;
2519
- }
2520
- if (el.kind === 'ref') {
2521
- const refFirst = firstSets.get(el.name) ?? new Set();
2522
- for (const tok of refFirst) {
2523
- if (!first.has(tok)) {
2524
- first.add(tok);
2525
- changed = true;
2526
- }
2527
- }
2528
- if (!nullable.has(el.name)) {
2529
- altNullable = false;
2530
- break;
2531
- }
2532
- continue;
2533
- }
2534
- // Desugar should have eliminated other kinds.
2535
- throw new Error(`abnf: internal — unexpected kind in FIRST: ${el.kind}`);
2536
- }
2537
- if (altNullable && !nullable.has(prod.name)) {
2538
- nullable.add(prod.name);
2539
- changed = true;
2540
- }
2541
- }
2542
- }
2543
- }
2544
- return { firstSets, nullable };
2545
- }
2546
- // FIRST set for a specific alternative (not the whole production).
2547
- // Returns null if the alt is nullable — the caller must treat that
2548
- // case separately (typically as a fallback empty alt).
2549
- function firstOfAlt(alt, literals, regexTokens, firstSets, nullable) {
2550
- const out = new Set();
2551
- for (const el of alt) {
2552
- if (el.kind === 'term' || el.kind === 'regex' || el.kind === 'token') {
2553
- const tok = el.kind === 'term'
2554
- ? literals.get(termKey(el))
2555
- : el.kind === 'token'
2556
- ? el.name
2557
- : regexTokens.get(regexKey(el));
2558
- out.add(tok);
2559
- return out;
2560
- }
2561
- if (el.kind === 'ref') {
2562
- const rf = firstSets.get(el.name) ?? new Set();
2563
- for (const tok of rf)
2564
- out.add(tok);
2565
- if (!nullable.has(el.name))
2566
- return out;
2567
- // else keep walking into the next element
2568
- continue;
2569
- }
2570
- throw new Error(`abnf: internal — unexpected kind in firstOfAlt: ${el.kind}`);
2571
- }
2572
- // Alt is nullable — no non-empty prefix.
2573
- return null;
2574
- }
2575
- // Longest deterministic terminal prefix of a rule — the longest
2576
- // sequence of tokens that every alternative of the rule starts
2577
- // with. Refs are followed into their target rule, with a `visited`
2578
- // set guarding cycles. An empty array means there's no confident
2579
- // prefix (the rule either has divergent alts, starts with a multi-
2580
- // alt ref, or hits a cycle), so the caller should fall back to a
2581
- // single-token FIRST-set lookahead instead.
2582
- function ruleLiteralPrefix(name, grammar, literals, regexTokens, visited) {
2583
- if (visited.has(name))
2584
- return [];
2585
- const next = new Set(visited);
2586
- next.add(name);
2587
- const prod = grammar.productions.find((p) => p.name === name);
2588
- if (!prod || prod.alts.length === 0)
2589
- return [];
2590
- const prefixes = prod.alts.map((alt) => altLiteralPrefix(alt, grammar, literals, regexTokens, next));
2591
- if (prefixes.some((p) => p.length === 0))
2592
- return [];
2593
- const minLen = Math.min(...prefixes.map((p) => p.length));
2594
- const common = [];
2595
- for (let i = 0; i < minLen; i++) {
2596
- const tok = prefixes[0][i];
2597
- if (prefixes.every((p) => p[i] === tok))
2598
- common.push(tok);
2599
- else
2600
- break;
2601
- }
2602
- return common;
2603
- }
2604
- function altLiteralPrefix(alt, grammar, literals, regexTokens, visited) {
2605
- const out = [];
2606
- for (const el of alt) {
2607
- if (el.kind === 'term') {
2608
- out.push(literals.get(termKey(el)));
2609
- }
2610
- else if (el.kind === 'regex') {
2611
- out.push(regexTokens.get(regexKey(el)));
2612
- }
2613
- else if (el.kind === 'token') {
2614
- out.push(el.name);
2615
- }
2616
- else if (el.kind === 'ref') {
2617
- const sub = ruleLiteralPrefix(el.name, grammar, literals, regexTokens, visited);
2618
- // Take the ref's literal prefix and stop — we can't see past
2619
- // the ref without more expensive analysis.
2620
- out.push(...sub);
2621
- return out;
2622
- }
2623
- else {
2624
- return out;
2625
- }
2626
- }
2627
- return out;
2628
- }
2629
- // Enumerate concrete token-sequence prefixes an alternative can
2630
- // start with, each at most `maxK` tokens long. Refs with multiple
2631
- // alternatives fan out into one prefix per sub-alternative so the
2632
- // caller can emit a dedicated dispatch alt for each path. When a
2633
- // ref cycles back or exhausts depth, the path is *terminated* at
2634
- // the tokens accumulated so far — the `done` flag is propagated
2635
- // out of nested calls so a truncated sub-prefix is never extended
2636
- // with tokens from elements the outer alt happens to list after the
2637
- // cycled ref.
2638
- function altPrefixesRaw(alt, grammar, literals, regexTokens, maxK, visited = new Set()) {
2639
- let paths = [{ tokens: [], done: false }];
2640
- for (const el of alt) {
2641
- const next = [];
2642
- for (const p of paths) {
2643
- if (p.done || p.tokens.length >= maxK) {
2644
- next.push(p);
2645
- continue;
2646
- }
2647
- if (el.kind === 'term') {
2648
- next.push({
2649
- tokens: [...p.tokens, literals.get(termKey(el))],
2650
- done: false,
2651
- });
2652
- }
2653
- else if (el.kind === 'regex') {
2654
- next.push({
2655
- tokens: [...p.tokens, regexTokens.get(regexKey(el))],
2656
- done: false,
2657
- });
2658
- }
2659
- else if (el.kind === 'token') {
2660
- next.push({ tokens: [...p.tokens, el.name], done: false });
2661
- }
2662
- else if (el.kind === 'ref') {
2663
- if (visited.has(el.name)) {
2664
- next.push({ tokens: p.tokens, done: true });
2665
- continue;
2666
- }
2667
- const childVisited = new Set(visited);
2668
- childVisited.add(el.name);
2669
- const target = grammar.productions.find((pr) => pr.name === el.name);
2670
- if (!target || target.alts.length === 0) {
2671
- next.push({ tokens: p.tokens, done: true });
2672
- continue;
2673
- }
2674
- for (const sub of target.alts) {
2675
- const subPaths = altPrefixesRaw(sub, grammar, literals, regexTokens, maxK - p.tokens.length, childVisited);
2676
- for (const sp of subPaths) {
2677
- next.push({
2678
- tokens: [...p.tokens, ...sp.tokens],
2679
- // Propagate `done` so the outer loop won't extend a
2680
- // cycle-truncated sub-prefix.
2681
- done: sp.done,
2682
- });
2683
- }
2684
- }
2685
- }
2686
- else {
2687
- // Desugar should have eliminated group/star/etc. at this point.
2688
- next.push({ tokens: p.tokens, done: true });
2689
- }
2690
- }
2691
- paths = next;
2692
- if (paths.every((p) => p.done || p.tokens.length >= maxK))
2693
- break;
2694
- }
2695
- return paths;
2696
- }
2697
- function altPrefixes(alt, grammar, literals, regexTokens, maxK) {
2698
- const raw = altPrefixesRaw(alt, grammar, literals, regexTokens, maxK);
2699
- const seen = new Set();
2700
- const out = [];
2701
- for (const p of raw) {
2702
- const key = p.tokens.join(' ');
2703
- if (!seen.has(key)) {
2704
- seen.add(key);
2705
- out.push(p.tokens);
2706
- }
2707
- }
2708
- return out;
2709
- }
2710
- // A quoted-string literal is effectively case-sensitive either
2711
- // when the user explicitly wrote `%s"…"` or when it contains no
2712
- // ASCII letters (there's nothing to fold — `"+"` matches `+` in
2713
- // any "case").
2714
- function isEffectivelyCaseSensitive(el) {
2715
- if (el.caseSensitive === true)
2716
- return true;
2717
- return !/[A-Za-z]/.test(el.literal);
2718
- }
2719
- // Map a term element to the key used to look up (or allocate) its
2720
- // emitted token. The key folds together the literal and its
2721
- // effective case-sensitivity so a sensitive and an insensitive
2722
- // occurrence of the same string are distinct tokens.
2723
- function termKey(el) {
2724
- return (isEffectivelyCaseSensitive(el) ? 'cs:' : 'ci:') + el.literal;
2725
- }
2726
- function escapeRegExp(s) {
2727
- return s.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
2728
- }
2729
677
  // Decode an ABNF numeric value (`%xNN`, `%dNN`, `%bNN`, or one of
2730
678
  // the range/concatenation forms) into a `AbnfElement`.
2731
679
  //
@@ -2782,35 +730,18 @@ function parseNumericValue(src) {
2782
730
  const chars = parts.map((n) => String.fromCodePoint(codePoint(n)));
2783
731
  return { kind: 'term', literal: chars.join('') };
2784
732
  }
2785
- function allocTokenName(literal, used, preferred) {
2786
- // A literal lifted from a named production (`PL = "+"`) keeps that
2787
- // name, so the emitted grammar reads `PL` rather than `T`.
2788
- if (preferred) {
2789
- const want = '#' + preferred;
2790
- if (!used.has(want)) {
2791
- used.add(want);
2792
- return want;
2793
- }
2794
- }
2795
- const base = literal
2796
- .replace(/[^A-Za-z0-9]/g, '_')
2797
- .toUpperCase()
2798
- .replace(/^_+|_+$/g, '');
2799
- const candidate = base.length > 0 ? '#' + base : '#T';
2800
- if (!used.has(candidate)) {
2801
- used.add(candidate);
2802
- return candidate;
2803
- }
2804
- let i = 1;
2805
- while (used.has(candidate + i))
2806
- i++;
2807
- const chosen = candidate + i;
2808
- used.add(chosen);
2809
- return chosen;
2810
- }
2811
733
  // Public entry point: take ABNF source and return a tabnas GrammarSpec.
734
+ // Convert ABNF source into a tabnas grammar spec: parse this notation,
735
+ // then hand the IR to the shared compiler. `tag` defaults to 'abnf' so
736
+ // the emitted alts keep their historical group tag.
737
+ // Emit a spec from an already-parsed ABNF grammar. Wraps the shared
738
+ // emitter to keep this package's historical `tag: 'abnf'` default, which
739
+ // consumers use to group and inspect the emitted alts. An explicit tag
740
+ // still wins.
741
+ function emitGrammarSpec(grammar, opts) {
742
+ return (0, bnf_1.emitGrammarSpec)(grammar, { ...opts, tag: opts?.tag ?? 'abnf' });
743
+ }
2812
744
  function abnf(src, opts) {
2813
- const grammar = parseAbnf(src);
2814
- return emitGrammarSpec(grammar, opts);
745
+ return emitGrammarSpec(parseAbnf(src), opts);
2815
746
  }
2816
747
  //# sourceMappingURL=converter.js.map