@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/abnf.d.ts +1 -1
- package/dist/abnf.js +1 -1
- package/dist/compile.d.ts +6 -25
- package/dist/compile.js +11 -329
- package/dist/compile.js.map +1 -1
- package/dist/converter.d.ts +8 -77
- package/dist/converter.js +17 -2086
- package/dist/converter.js.map +1 -1
- package/package.json +5 -3
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
|
-
|
|
9
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
2814
|
-
return emitGrammarSpec(grammar, opts);
|
|
745
|
+
return emitGrammarSpec(parseAbnf(src), opts);
|
|
2815
746
|
}
|
|
2816
747
|
//# sourceMappingURL=converter.js.map
|