@tabnas/abnf 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/bin/tabnas-abnf +2 -0
- package/dist/abnf.d.ts +6 -0
- package/dist/abnf.js +36 -0
- package/dist/abnf.js.map +1 -0
- package/dist/bin/tabnas-abnf-cli.d.ts +1 -0
- package/dist/bin/tabnas-abnf-cli.js +223 -0
- package/dist/bin/tabnas-abnf-cli.js.map +1 -0
- package/dist/compile.d.ts +26 -0
- package/dist/compile.js +341 -0
- package/dist/compile.js.map +1 -0
- package/dist/converter.d.ts +84 -0
- package/dist/converter.js +2106 -0
- package/dist/converter.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,2106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Copyright (c) 2025 Richard Rodger and other contributors, MIT License */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.AbnfParseError = exports.abnfRules = void 0;
|
|
5
|
+
exports.abnf = abnf;
|
|
6
|
+
exports.parseAbnf = parseAbnf;
|
|
7
|
+
exports.emitGrammarSpec = emitGrammarSpec;
|
|
8
|
+
exports.eliminateLeftRecursion = eliminateLeftRecursion;
|
|
9
|
+
// Declarative definition of the ABNF grammar itself, expressed as
|
|
10
|
+
// tabnas rules. Each rule names its `open`/`close` alt list and, where
|
|
11
|
+
// necessary, a `bo`/`bc` state hook for AST assembly.
|
|
12
|
+
//
|
|
13
|
+
// Stage 8: incremental alternatives via `name =/ alt` now fold
|
|
14
|
+
// into the earlier production with the same name. Quoted strings
|
|
15
|
+
// default to case-insensitive (ABNF semantics), `%s` / `%i` force
|
|
16
|
+
// sensitivity explicitly, numeric values and repetition prefixes
|
|
17
|
+
// work as in previous stages.
|
|
18
|
+
//
|
|
19
|
+
// Token vocabulary:
|
|
20
|
+
// #DEF `=` (rule-definition operator)
|
|
21
|
+
// #DEFA `=/` (incremental-alternatives operator)
|
|
22
|
+
// #ALT `/` (alternation)
|
|
23
|
+
// #STAR `*` (repetition separator)
|
|
24
|
+
// #NUM decimal repetition count (matched via match.token)
|
|
25
|
+
// #NV `%[xdb]NN[(-NN|(.NN)*)]` numeric value (match.token)
|
|
26
|
+
// #SS `%s` (case-sensitive string prefix)
|
|
27
|
+
// #SI `%i` (case-insensitive string prefix — same as default)
|
|
28
|
+
// #LP `(`
|
|
29
|
+
// #RP `)`
|
|
30
|
+
// #OB `[` (optional-group open)
|
|
31
|
+
// #CB `]` (optional-group close)
|
|
32
|
+
// #TX bare identifier (tabnas default text token)
|
|
33
|
+
// #ST quoted string literal (tabnas default string token)
|
|
34
|
+
// #ZZ end-of-source
|
|
35
|
+
//
|
|
36
|
+
// Grammar:
|
|
37
|
+
// abnf = production*
|
|
38
|
+
// production = IDENT ('=' / '=/') alts
|
|
39
|
+
// alts = seq ('/' seq)*
|
|
40
|
+
// seq = element*
|
|
41
|
+
// element = repetition? atom
|
|
42
|
+
// repetition = NUM '*' NUM / NUM '*' / '*' NUM / '*' / NUM
|
|
43
|
+
// atom = IDENT | STRING | ['%s' | '%i'] STRING | NUMVAL
|
|
44
|
+
// | '(' alts ')' | '[' alts ']'
|
|
45
|
+
// numval = '%' ('x' / 'd' / 'b') DIGITS [ '-' DIGITS | ('.' DIGITS)* ]
|
|
46
|
+
const abnfRules = {
|
|
47
|
+
// Top-level: accumulates productions into r.node.
|
|
48
|
+
abnf: {
|
|
49
|
+
bo: (r) => { r.node = []; },
|
|
50
|
+
open: [
|
|
51
|
+
{ s: '#ZZ', g: 'empty' },
|
|
52
|
+
{ p: 'prod' },
|
|
53
|
+
],
|
|
54
|
+
close: [{ s: '#ZZ' }],
|
|
55
|
+
},
|
|
56
|
+
// One production per invocation; tail-recurses (r:'prod') for the
|
|
57
|
+
// next. Inherits its parent's node (the productions array) and
|
|
58
|
+
// appends to it in `bc` once its `alts` child has returned.
|
|
59
|
+
// Production header is `IDENT =` — a bareword rule name followed
|
|
60
|
+
// by the `=` definition operator.
|
|
61
|
+
prod: {
|
|
62
|
+
open: [
|
|
63
|
+
// Standalone definition: name = alts
|
|
64
|
+
{
|
|
65
|
+
s: '#TX #DEF',
|
|
66
|
+
a: (r) => {
|
|
67
|
+
r.u.name = r.o[0].val;
|
|
68
|
+
r.u.incremental = false;
|
|
69
|
+
},
|
|
70
|
+
p: 'alts',
|
|
71
|
+
},
|
|
72
|
+
// Incremental alternatives: name =/ alts
|
|
73
|
+
{
|
|
74
|
+
s: '#TX #DEFA',
|
|
75
|
+
a: (r) => {
|
|
76
|
+
r.u.name = r.o[0].val;
|
|
77
|
+
r.u.incremental = true;
|
|
78
|
+
},
|
|
79
|
+
p: 'alts',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
close: [
|
|
83
|
+
// A TX followed by `=` or `=/` means the next production has
|
|
84
|
+
// begun — back up 2 tokens so a fresh `prod` invocation sees
|
|
85
|
+
// them.
|
|
86
|
+
{ s: '#TX #DEF', b: 2, r: 'prod' },
|
|
87
|
+
{ s: '#TX #DEFA', b: 2, r: 'prod' },
|
|
88
|
+
{ b: 1 },
|
|
89
|
+
],
|
|
90
|
+
bc: (r) => {
|
|
91
|
+
if (r.child && r.child.node !== undefined) {
|
|
92
|
+
const prod = { name: r.u.name, alts: r.child.node };
|
|
93
|
+
if (r.u.incremental)
|
|
94
|
+
prod.incremental = true;
|
|
95
|
+
r.node.push(prod);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
// A list of alternative sequences separated by `/` (ABNF
|
|
100
|
+
// alternation). Owns its own array (`bo` resets it) and pushes
|
|
101
|
+
// each seq result in `bc`.
|
|
102
|
+
alts: {
|
|
103
|
+
bo: (r) => { r.node = []; },
|
|
104
|
+
open: [{ p: 'seq' }],
|
|
105
|
+
close: [
|
|
106
|
+
{ s: '#ALT', p: 'seq' },
|
|
107
|
+
{ b: 1 },
|
|
108
|
+
],
|
|
109
|
+
bc: (r) => {
|
|
110
|
+
if (r.child && r.child.node !== undefined) {
|
|
111
|
+
r.node.push(r.child.node);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
// A (possibly empty) sequence of elements. The 2-token lookahead
|
|
116
|
+
// `#TX #DEF` detects a following production boundary and bails
|
|
117
|
+
// out without consuming the tokens; a plain `#TX` at the leading
|
|
118
|
+
// position (tried later so the longer alt wins) is a rule
|
|
119
|
+
// reference inside the current sequence.
|
|
120
|
+
seq: {
|
|
121
|
+
bo: (r) => { r.node = []; },
|
|
122
|
+
open: [
|
|
123
|
+
{ s: '#TX #DEF', b: 2, g: 'end' },
|
|
124
|
+
{ s: '#TX #DEFA', b: 2, g: 'end' },
|
|
125
|
+
{ s: '#ALT', b: 1, g: 'end' },
|
|
126
|
+
{ s: '#ZZ', b: 1, g: 'end' },
|
|
127
|
+
{ s: '#RP', b: 1, g: 'end' },
|
|
128
|
+
{ s: '#CB', b: 1, g: 'end' },
|
|
129
|
+
// Listing element-starter tokens in `s:` here ensures the
|
|
130
|
+
// tcol-driven matcher considers each one when lexing.
|
|
131
|
+
{ s: '#ST', b: 1, p: 'elem' },
|
|
132
|
+
{ s: '#NV', b: 1, p: 'elem' },
|
|
133
|
+
{ s: '#SS', b: 1, p: 'elem' },
|
|
134
|
+
{ s: '#SI', b: 1, p: 'elem' },
|
|
135
|
+
{ s: '#TX', b: 1, p: 'elem' },
|
|
136
|
+
{ s: '#LP', b: 1, p: 'elem' },
|
|
137
|
+
{ s: '#OB', b: 1, p: 'elem' },
|
|
138
|
+
{ s: '#STAR', b: 1, p: 'elem' },
|
|
139
|
+
{ s: '#NUM', b: 1, p: 'elem' },
|
|
140
|
+
{ p: 'elem' },
|
|
141
|
+
],
|
|
142
|
+
close: [
|
|
143
|
+
{ s: '#TX #DEF', b: 2, g: 'end' },
|
|
144
|
+
{ s: '#TX #DEFA', b: 2, g: 'end' },
|
|
145
|
+
{ s: '#ALT', b: 1, g: 'end' },
|
|
146
|
+
{ s: '#ZZ', b: 1, g: 'end' },
|
|
147
|
+
{ s: '#RP', b: 1, g: 'end' },
|
|
148
|
+
{ s: '#CB', b: 1, g: 'end' },
|
|
149
|
+
{ s: '#ST', b: 1, p: 'elem' },
|
|
150
|
+
{ s: '#NV', b: 1, p: 'elem' },
|
|
151
|
+
{ s: '#SS', b: 1, p: 'elem' },
|
|
152
|
+
{ s: '#SI', b: 1, p: 'elem' },
|
|
153
|
+
{ s: '#TX', b: 1, p: 'elem' },
|
|
154
|
+
{ s: '#LP', b: 1, p: 'elem' },
|
|
155
|
+
{ s: '#OB', b: 1, p: 'elem' },
|
|
156
|
+
{ s: '#STAR', b: 1, p: 'elem' },
|
|
157
|
+
{ s: '#NUM', b: 1, p: 'elem' },
|
|
158
|
+
{ b: 1 },
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
// One element: an optional ABNF repetition prefix (`*A`, `1*A`,
|
|
162
|
+
// `m*nA`, `*nA`, `m*A`, `nA`) followed by an atom. The prefix is
|
|
163
|
+
// matched up front, stored on `r.u.min`/`r.u.max`; then `atom` is
|
|
164
|
+
// pushed to parse the actual element body, whose result is wrapped
|
|
165
|
+
// into an AST node and appended to the parent seq's array in close.
|
|
166
|
+
elem: {
|
|
167
|
+
bo: (r) => { r.u.min = 1; r.u.max = 1; },
|
|
168
|
+
open: [
|
|
169
|
+
// NUM '*' NUM — bounded repetition, followed by the atom
|
|
170
|
+
// itself (listed via the ATOM tokenset so every atom-starter
|
|
171
|
+
// tin — including `#NV` — is in tcol for this position).
|
|
172
|
+
{
|
|
173
|
+
s: '#NUM #STAR #NUM #ATOM',
|
|
174
|
+
b: 1,
|
|
175
|
+
a: (r) => {
|
|
176
|
+
r.u.min = parseInt(r.o[0].src, 10);
|
|
177
|
+
r.u.max = parseInt(r.o[2].src, 10);
|
|
178
|
+
},
|
|
179
|
+
p: 'atom',
|
|
180
|
+
},
|
|
181
|
+
// NUM '*' — at-least-NUM repetition followed by an atom.
|
|
182
|
+
{
|
|
183
|
+
s: '#NUM #STAR #ATOM',
|
|
184
|
+
b: 1,
|
|
185
|
+
a: (r) => {
|
|
186
|
+
r.u.min = parseInt(r.o[0].src, 10);
|
|
187
|
+
r.u.max = Infinity;
|
|
188
|
+
},
|
|
189
|
+
p: 'atom',
|
|
190
|
+
},
|
|
191
|
+
// '*' NUM — at-most-NUM repetition.
|
|
192
|
+
{
|
|
193
|
+
s: '#STAR #NUM #ATOM',
|
|
194
|
+
b: 1,
|
|
195
|
+
a: (r) => {
|
|
196
|
+
r.u.min = 0;
|
|
197
|
+
r.u.max = parseInt(r.o[1].src, 10);
|
|
198
|
+
},
|
|
199
|
+
p: 'atom',
|
|
200
|
+
},
|
|
201
|
+
// '*' — zero-or-more.
|
|
202
|
+
{
|
|
203
|
+
s: '#STAR #ATOM',
|
|
204
|
+
b: 1,
|
|
205
|
+
a: (r) => { r.u.min = 0; r.u.max = Infinity; },
|
|
206
|
+
p: 'atom',
|
|
207
|
+
},
|
|
208
|
+
// NUM — exact repetition count.
|
|
209
|
+
{
|
|
210
|
+
s: '#NUM #ATOM',
|
|
211
|
+
b: 1,
|
|
212
|
+
a: (r) => {
|
|
213
|
+
const n = parseInt(r.o[0].src, 10);
|
|
214
|
+
r.u.min = n;
|
|
215
|
+
r.u.max = n;
|
|
216
|
+
},
|
|
217
|
+
p: 'atom',
|
|
218
|
+
},
|
|
219
|
+
// No prefix — push atom directly (min = max = 1).
|
|
220
|
+
{ p: 'atom' },
|
|
221
|
+
],
|
|
222
|
+
close: [{
|
|
223
|
+
// Wrap the returned atom (r.child.node) based on r.u.min/max
|
|
224
|
+
// and append to the parent seq's array.
|
|
225
|
+
a: (r) => {
|
|
226
|
+
const item = r.child.node;
|
|
227
|
+
const { min, max } = r.u;
|
|
228
|
+
if (min === 1 && max === 1) {
|
|
229
|
+
r.node.push(item);
|
|
230
|
+
}
|
|
231
|
+
else if (min === 0 && max === Infinity) {
|
|
232
|
+
r.node.push({ kind: 'star', inner: item });
|
|
233
|
+
}
|
|
234
|
+
else if (min === 1 && max === Infinity) {
|
|
235
|
+
r.node.push({ kind: 'plus', inner: item });
|
|
236
|
+
}
|
|
237
|
+
else if (min === 0 && max === 1) {
|
|
238
|
+
r.node.push({ kind: 'opt', inner: item });
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
r.node.push({ kind: 'rep', min, max, inner: item });
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
}],
|
|
245
|
+
},
|
|
246
|
+
// The atom body — a bareword ref, quoted-string terminal,
|
|
247
|
+
// parenthesised group, or bracketed optional. Sets its OWN r.node
|
|
248
|
+
// to the AST element so the enclosing `elem` rule can read it
|
|
249
|
+
// from `r.child.node` in its close state.
|
|
250
|
+
atom: {
|
|
251
|
+
bo: (r) => { r.node = undefined; },
|
|
252
|
+
open: [
|
|
253
|
+
// Case-sensitive string: %s"foo"
|
|
254
|
+
{
|
|
255
|
+
s: '#SS #ST',
|
|
256
|
+
a: (r) => {
|
|
257
|
+
r.node = {
|
|
258
|
+
kind: 'term',
|
|
259
|
+
literal: r.o[1].val,
|
|
260
|
+
caseSensitive: true,
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
// Case-insensitive string: %i"foo" (same as bare "foo" below,
|
|
265
|
+
// but spelled explicitly).
|
|
266
|
+
{
|
|
267
|
+
s: '#SI #ST',
|
|
268
|
+
a: (r) => {
|
|
269
|
+
r.node = { kind: 'term', literal: r.o[1].val };
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
// Bare quoted string — case-insensitive per ABNF default.
|
|
273
|
+
{
|
|
274
|
+
s: '#ST',
|
|
275
|
+
a: (r) => {
|
|
276
|
+
r.node = { kind: 'term', literal: r.o[0].val };
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
s: '#NV',
|
|
281
|
+
a: (r) => {
|
|
282
|
+
r.node = parseNumericValue(r.o[0].src);
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
s: '#TX',
|
|
287
|
+
a: (r) => {
|
|
288
|
+
r.node = { kind: 'ref', name: r.o[0].val };
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
s: '#LP',
|
|
293
|
+
a: (r) => { r.u.groupKind = 'group'; },
|
|
294
|
+
p: 'alts',
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
s: '#OB',
|
|
298
|
+
a: (r) => { r.u.groupKind = 'opt'; },
|
|
299
|
+
p: 'alts',
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
close: [
|
|
303
|
+
{
|
|
304
|
+
s: '#RP',
|
|
305
|
+
c: (r) => r.u.groupKind === 'group',
|
|
306
|
+
a: (r) => {
|
|
307
|
+
r.node = { kind: 'group', alts: r.child.node };
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
s: '#CB',
|
|
312
|
+
c: (r) => r.u.groupKind === 'opt',
|
|
313
|
+
a: (r) => {
|
|
314
|
+
r.node = {
|
|
315
|
+
kind: 'opt',
|
|
316
|
+
inner: { kind: 'group', alts: r.child.node },
|
|
317
|
+
};
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
// For simple atoms (string/ref), r.node is already set by
|
|
321
|
+
// open; we want to pop without consuming the next token.
|
|
322
|
+
// List every token that can legitimately follow an atom so
|
|
323
|
+
// the lexer's tcol-driven match-matcher emits #NUM, #STAR,
|
|
324
|
+
// and friends as their proper types here — otherwise the
|
|
325
|
+
// default number-matcher would lex `1` as #NR and the
|
|
326
|
+
// enclosing seq.close wouldn't recognise the digit as the
|
|
327
|
+
// start of a repetition prefix.
|
|
328
|
+
{ s: '#TX', b: 1 },
|
|
329
|
+
{ s: '#ST', b: 1 },
|
|
330
|
+
{ s: '#NV', b: 1 },
|
|
331
|
+
{ s: '#SS', b: 1 },
|
|
332
|
+
{ s: '#SI', b: 1 },
|
|
333
|
+
{ s: '#NUM', b: 1 },
|
|
334
|
+
{ s: '#STAR', b: 1 },
|
|
335
|
+
{ s: '#LP', b: 1 },
|
|
336
|
+
{ s: '#OB', b: 1 },
|
|
337
|
+
{ s: '#RP', b: 1 },
|
|
338
|
+
{ s: '#CB', b: 1 },
|
|
339
|
+
{ s: '#ALT', b: 1 },
|
|
340
|
+
{ s: '#DEF', b: 1 },
|
|
341
|
+
{ s: '#ZZ', b: 1 },
|
|
342
|
+
{ b: 1 },
|
|
343
|
+
],
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
exports.abnfRules = abnfRules;
|
|
347
|
+
// Lazily built tabnas instance that parses ABNF source. Deferred
|
|
348
|
+
// construction avoids a circular-import failure at module load time.
|
|
349
|
+
let _abnfParser = null;
|
|
350
|
+
function getAbnfParser() {
|
|
351
|
+
if (_abnfParser)
|
|
352
|
+
return _abnfParser;
|
|
353
|
+
const { Tabnas } = require('@tabnas/parser');
|
|
354
|
+
// ABNF defines its own grammar from scratch, so we don't load any
|
|
355
|
+
// grammar plugin — just use the bare engine with default tokens.
|
|
356
|
+
const j = new Tabnas({
|
|
357
|
+
rule: { start: 'abnf' },
|
|
358
|
+
fixed: {
|
|
359
|
+
token: {
|
|
360
|
+
// Clear JSON-oriented defaults we're not using so `:`, `,`
|
|
361
|
+
// and `{` have no special meaning inside ABNF source.
|
|
362
|
+
'#OS': null,
|
|
363
|
+
'#CS': null,
|
|
364
|
+
'#CL': null,
|
|
365
|
+
'#CA': null,
|
|
366
|
+
// Re-map `#OB` / `#CB` from JSON's `{` / `}` to ABNF's
|
|
367
|
+
// `[` / `]` optional-group brackets.
|
|
368
|
+
'#OB': '[',
|
|
369
|
+
'#CB': ']',
|
|
370
|
+
'#DEF': '=',
|
|
371
|
+
// `=/` — ABNF's incremental-alternatives operator. Longer
|
|
372
|
+
// than `=`, so tabnas's longest-match-wins fixed matcher
|
|
373
|
+
// tries it first.
|
|
374
|
+
'#DEFA': '=/',
|
|
375
|
+
'#ALT': '/',
|
|
376
|
+
'#STAR': '*',
|
|
377
|
+
'#LP': '(',
|
|
378
|
+
'#RP': ')',
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
match: {
|
|
382
|
+
token: {
|
|
383
|
+
// ABNF repetition counts: decimal integers.
|
|
384
|
+
'#NUM': /^[0-9]+/,
|
|
385
|
+
// ABNF numeric value notation:
|
|
386
|
+
// %xNN single hex code point
|
|
387
|
+
// %dNN single decimal code point
|
|
388
|
+
// %bNN single binary code point
|
|
389
|
+
// %xNN-NN hex range
|
|
390
|
+
// %xNN.NN.NN concatenated hex code points (= string)
|
|
391
|
+
// Digits are permissive (hex covers the decimal / binary
|
|
392
|
+
// subsets); `parseNumericValue` re-validates against the
|
|
393
|
+
// actual base.
|
|
394
|
+
'#NV': /^%[xdbXDB][0-9a-fA-F]+(?:[-.][0-9a-fA-F]+)*/,
|
|
395
|
+
// `%s` / `%i` prefixes on a quoted string. The lookahead
|
|
396
|
+
// requires `"` so they don't steal the `%` of `%xNN`.
|
|
397
|
+
'#SS': /^%[sS](?=")/,
|
|
398
|
+
'#SI': /^%[iI](?=")/,
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
tokenSet: {
|
|
402
|
+
// Tokens that can legitimately open an atom. Declaring this
|
|
403
|
+
// as a set lets elem.open use `#ATOM` inside its `s:` patterns
|
|
404
|
+
// — that way the tcol at the atom-starter position includes
|
|
405
|
+
// every matcher tin (notably #NV), so the lexer doesn't fall
|
|
406
|
+
// through to #TX when the actual atom is `%xNN`.
|
|
407
|
+
ATOM: ['#ST', '#NV', '#TX', '#LP', '#OB', '#SS', '#SI'],
|
|
408
|
+
},
|
|
409
|
+
comment: {
|
|
410
|
+
// ABNF uses `;` to start a line comment. Override tabnas's
|
|
411
|
+
// default `hash` definition (which used `#`) and disable the
|
|
412
|
+
// other comment styles so `//` and `/* */` aren't confused
|
|
413
|
+
// with the alternation operator.
|
|
414
|
+
def: {
|
|
415
|
+
hash: { line: true, start: ';', lex: true, eatline: false },
|
|
416
|
+
slash: null,
|
|
417
|
+
multi: null,
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
// Drop the default JSON rules — they would otherwise compete with
|
|
422
|
+
// ours for the starting token set.
|
|
423
|
+
const existing = j.rule();
|
|
424
|
+
for (const name of Object.keys(existing)) {
|
|
425
|
+
j.rule(name, null);
|
|
426
|
+
}
|
|
427
|
+
for (const name of Object.keys(abnfRules)) {
|
|
428
|
+
const spec = abnfRules[name];
|
|
429
|
+
j.rule(name, (rs) => {
|
|
430
|
+
if (spec.bo)
|
|
431
|
+
rs.bo(spec.bo);
|
|
432
|
+
if (spec.bc)
|
|
433
|
+
rs.bc(spec.bc);
|
|
434
|
+
if (spec.open)
|
|
435
|
+
rs.open(spec.open);
|
|
436
|
+
if (spec.close)
|
|
437
|
+
rs.close(spec.close);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
_abnfParser = (src) => j.parse(src);
|
|
441
|
+
return _abnfParser;
|
|
442
|
+
}
|
|
443
|
+
// Rewrite a grammar so that the only element kinds remaining are
|
|
444
|
+
// `term` and `ref`. Each `X?`, `X*`, `X+` occurrence is replaced by a
|
|
445
|
+
// reference to a newly-generated helper production that expresses the
|
|
446
|
+
// same language in plain ABNF.
|
|
447
|
+
// Eliminate left recursion — both direct (P → P α) and indirect
|
|
448
|
+
// (P → Q α, Q → P β) — via Paull's algorithm.
|
|
449
|
+
//
|
|
450
|
+
// Order the productions, and for each A_i walk back over A_1..A_{i-1}
|
|
451
|
+
// inlining any leading reference into A_i's alternatives. Once the
|
|
452
|
+
// only remaining leading self-reference on A_i is direct, rewrite to
|
|
453
|
+
// the iterative form
|
|
454
|
+
// P → (β_1 | … | β_m) (α_1 | … | α_n)*
|
|
455
|
+
// which tabnas's push-down parser can execute without re-entering P
|
|
456
|
+
// at the same source position.
|
|
457
|
+
//
|
|
458
|
+
// The substitution step can duplicate alternatives, so pathological
|
|
459
|
+
// grammars will enlarge — caller is expected to keep the grammar
|
|
460
|
+
// reasonably small (this is a first-step converter, not a full
|
|
461
|
+
// toolchain).
|
|
462
|
+
function eliminateLeftRecursion(grammar) {
|
|
463
|
+
const originalOrder = grammar.productions.map((p) => p.name);
|
|
464
|
+
// Order productions so that rules referenced at a leading position
|
|
465
|
+
// are processed before the rules that reference them. Paull's
|
|
466
|
+
// substitution inlines A_j's alts into A_i for j < i, so putting
|
|
467
|
+
// dependencies first is what makes nullable-prefixed hidden left
|
|
468
|
+
// recursion reachable by the substitution step.
|
|
469
|
+
//
|
|
470
|
+
// Note: substitution here always runs, even for cycle-free
|
|
471
|
+
// grammars. The reason is pragmatic rather than theoretical —
|
|
472
|
+
// populating tcol from multi-token altPrefixes (needed so the
|
|
473
|
+
// lexer's regex matchers fire with the right tin in nested
|
|
474
|
+
// contexts) requires the full inlined shape. A future refactor
|
|
475
|
+
// could compute tcol from the un-substituted grammar and only
|
|
476
|
+
// apply Paull's to the cyclic SCCs, which would preserve more
|
|
477
|
+
// named-rule structure in the emitted AST.
|
|
478
|
+
let prods = topoOrderForPaull(grammar.productions.map((p) => ({
|
|
479
|
+
name: p.name,
|
|
480
|
+
alts: p.alts.map((a) => a.slice()),
|
|
481
|
+
nodeKind: p.nodeKind,
|
|
482
|
+
})));
|
|
483
|
+
for (let i = 0; i < prods.length; i++) {
|
|
484
|
+
// For each earlier production A_j, inline any alternative of
|
|
485
|
+
// A_i whose leading element is a reference to A_j.
|
|
486
|
+
for (let j = 0; j < i; j++) {
|
|
487
|
+
prods[i] = substituteLeadingRef(prods[i], prods[j]);
|
|
488
|
+
}
|
|
489
|
+
prods[i] = eliminateDirectLeftRec(prods[i]);
|
|
490
|
+
}
|
|
491
|
+
// Restore the caller's declared order, so the start rule still
|
|
492
|
+
// ends up first (and the user sees their rule names in a
|
|
493
|
+
// recognisable order when inspecting the spec).
|
|
494
|
+
const byName = new Map(prods.map((p) => [p.name, p]));
|
|
495
|
+
const ordered = [];
|
|
496
|
+
for (const name of originalOrder) {
|
|
497
|
+
const p = byName.get(name);
|
|
498
|
+
if (p) {
|
|
499
|
+
ordered.push(p);
|
|
500
|
+
byName.delete(name);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// Any generated productions created during substitution (none in
|
|
504
|
+
// the current implementation) would fall through here.
|
|
505
|
+
for (const p of byName.values())
|
|
506
|
+
ordered.push(p);
|
|
507
|
+
return { productions: ordered };
|
|
508
|
+
}
|
|
509
|
+
// Tarjan-flavoured SCC scan over the leading-reference graph:
|
|
510
|
+
// returns the names of productions that participate in at least one
|
|
511
|
+
// cycle (self-loop or longer). Used to scope Paull's substitution to
|
|
512
|
+
// only the rules that actually need it.
|
|
513
|
+
function findLeadingRefCycleMembers(prods) {
|
|
514
|
+
const byName = new Map(prods.map((p) => [p.name, p]));
|
|
515
|
+
const leadingRefs = (p) => {
|
|
516
|
+
const out = [];
|
|
517
|
+
for (const alt of p.alts) {
|
|
518
|
+
if (alt.length === 0)
|
|
519
|
+
continue;
|
|
520
|
+
const first = alt[0];
|
|
521
|
+
if (first.kind === 'ref' && byName.has(first.name))
|
|
522
|
+
out.push(first.name);
|
|
523
|
+
}
|
|
524
|
+
return out;
|
|
525
|
+
};
|
|
526
|
+
// Tarjan's SCC algorithm.
|
|
527
|
+
let index = 0;
|
|
528
|
+
const stack = [];
|
|
529
|
+
const onStack = new Set();
|
|
530
|
+
const indices = new Map();
|
|
531
|
+
const lowlinks = new Map();
|
|
532
|
+
const cyclic = new Set();
|
|
533
|
+
function strongConnect(name) {
|
|
534
|
+
indices.set(name, index);
|
|
535
|
+
lowlinks.set(name, index);
|
|
536
|
+
index++;
|
|
537
|
+
stack.push(name);
|
|
538
|
+
onStack.add(name);
|
|
539
|
+
const prod = byName.get(name);
|
|
540
|
+
if (prod) {
|
|
541
|
+
for (const target of leadingRefs(prod)) {
|
|
542
|
+
if (!indices.has(target)) {
|
|
543
|
+
strongConnect(target);
|
|
544
|
+
lowlinks.set(name, Math.min(lowlinks.get(name), lowlinks.get(target)));
|
|
545
|
+
}
|
|
546
|
+
else if (onStack.has(target)) {
|
|
547
|
+
lowlinks.set(name, Math.min(lowlinks.get(name), indices.get(target)));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (lowlinks.get(name) === indices.get(name)) {
|
|
552
|
+
// Pop the SCC. If it has more than one member, or it's a
|
|
553
|
+
// single member with a self-loop, mark as cyclic.
|
|
554
|
+
const scc = [];
|
|
555
|
+
let w;
|
|
556
|
+
do {
|
|
557
|
+
w = stack.pop();
|
|
558
|
+
onStack.delete(w);
|
|
559
|
+
scc.push(w);
|
|
560
|
+
} while (w !== name);
|
|
561
|
+
const isCycle = scc.length > 1 ||
|
|
562
|
+
(scc.length === 1 && leadingRefs(byName.get(scc[0])).includes(scc[0]));
|
|
563
|
+
if (isCycle)
|
|
564
|
+
for (const n of scc)
|
|
565
|
+
cyclic.add(n);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
for (const p of prods) {
|
|
569
|
+
if (!indices.has(p.name))
|
|
570
|
+
strongConnect(p.name);
|
|
571
|
+
}
|
|
572
|
+
return cyclic;
|
|
573
|
+
}
|
|
574
|
+
// Topological order over the "leading-position reference" graph:
|
|
575
|
+
// an edge A → B exists when A has at least one alternative whose
|
|
576
|
+
// first element is a reference to B. Cycles are preserved as-is
|
|
577
|
+
// (Paull's handles them via the substitution + direct-LR rewrite).
|
|
578
|
+
function topoOrderForPaull(prods) {
|
|
579
|
+
const byName = new Map(prods.map((p) => [p.name, p]));
|
|
580
|
+
const colour = new Map(); // 0 unseen, 1 in-progress, 2 done
|
|
581
|
+
const order = [];
|
|
582
|
+
function visit(name) {
|
|
583
|
+
const c = colour.get(name) ?? 0;
|
|
584
|
+
if (c !== 0)
|
|
585
|
+
return; // already seen or on the current path
|
|
586
|
+
colour.set(name, 1);
|
|
587
|
+
const p = byName.get(name);
|
|
588
|
+
if (p) {
|
|
589
|
+
for (const alt of p.alts) {
|
|
590
|
+
if (alt.length > 0 && alt[0].kind === 'ref' && byName.has(alt[0].name)) {
|
|
591
|
+
visit(alt[0].name);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
colour.set(name, 2);
|
|
595
|
+
order.push(p);
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
colour.set(name, 2);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
for (const p of prods)
|
|
602
|
+
visit(p.name);
|
|
603
|
+
return order;
|
|
604
|
+
}
|
|
605
|
+
// For every alternative of `target` that begins with a ref to
|
|
606
|
+
// `source`, replace that alt with |source.alts| copies — each one
|
|
607
|
+
// with the leading source-ref expanded to one of source's alts.
|
|
608
|
+
function substituteLeadingRef(target, source) {
|
|
609
|
+
const newAlts = [];
|
|
610
|
+
for (const alt of target.alts) {
|
|
611
|
+
if (alt.length > 0 &&
|
|
612
|
+
alt[0].kind === 'ref' &&
|
|
613
|
+
alt[0].name === source.name) {
|
|
614
|
+
const tail = alt.slice(1);
|
|
615
|
+
for (const srcAlt of source.alts) {
|
|
616
|
+
newAlts.push([...srcAlt, ...tail]);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
newAlts.push(alt);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return { name: target.name, alts: newAlts, nodeKind: target.nodeKind };
|
|
624
|
+
}
|
|
625
|
+
// Rewrite a single production's direct left recursion to its
|
|
626
|
+
// iterative equivalent. Equivalent to the previous version of
|
|
627
|
+
// `eliminateLeftRecursion` but scoped to one production.
|
|
628
|
+
function eliminateDirectLeftRec(prod) {
|
|
629
|
+
const recursive = [];
|
|
630
|
+
const seeds = [];
|
|
631
|
+
for (const alt of prod.alts) {
|
|
632
|
+
if (alt.length > 0 &&
|
|
633
|
+
alt[0].kind === 'ref' &&
|
|
634
|
+
alt[0].name === prod.name) {
|
|
635
|
+
recursive.push(alt.slice(1));
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
seeds.push(alt);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// A trivial recursive alt `[P]` (P ::= P, nothing else) would
|
|
642
|
+
// derive P from P with no progress — semantically a no-op. Drop
|
|
643
|
+
// them silently, since nullable-prefix expansion in Paull's can
|
|
644
|
+
// legitimately produce them and erroring would hide a legal
|
|
645
|
+
// grammar.
|
|
646
|
+
const nonTrivialRecursive = recursive.filter((t) => t.length > 0);
|
|
647
|
+
if (nonTrivialRecursive.length === 0) {
|
|
648
|
+
// Either no recursion at all, or only trivial self-refs — keep
|
|
649
|
+
// just the seeds.
|
|
650
|
+
return { name: prod.name, alts: seeds, nodeKind: prod.nodeKind };
|
|
651
|
+
}
|
|
652
|
+
if (seeds.length === 0) {
|
|
653
|
+
throw new Error(`abnf: rule '${prod.name}' is purely left-recursive ` +
|
|
654
|
+
`(no seed alternative); cannot eliminate`);
|
|
655
|
+
}
|
|
656
|
+
const seedElement = seeds.length === 1 && seeds[0].length === 1
|
|
657
|
+
? seeds[0][0]
|
|
658
|
+
: { kind: 'group', alts: seeds };
|
|
659
|
+
const tailInner = nonTrivialRecursive.length === 1 && nonTrivialRecursive[0].length === 1
|
|
660
|
+
? nonTrivialRecursive[0][0]
|
|
661
|
+
: { kind: 'group', alts: nonTrivialRecursive };
|
|
662
|
+
return {
|
|
663
|
+
name: prod.name,
|
|
664
|
+
alts: [[seedElement, { kind: 'star', inner: tailInner }]],
|
|
665
|
+
nodeKind: prod.nodeKind,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function desugar(grammar) {
|
|
669
|
+
const extra = [];
|
|
670
|
+
const used = new Set(grammar.productions.map((p) => p.name));
|
|
671
|
+
function freshName(hint) {
|
|
672
|
+
// Collision-avoiding name like `_gen1`, `_gen2`, …
|
|
673
|
+
let i = extra.length;
|
|
674
|
+
let name;
|
|
675
|
+
do {
|
|
676
|
+
i++;
|
|
677
|
+
name = `_gen${i}_${hint}`;
|
|
678
|
+
} while (used.has(name));
|
|
679
|
+
used.add(name);
|
|
680
|
+
return name;
|
|
681
|
+
}
|
|
682
|
+
function desugarAlt(alt) {
|
|
683
|
+
return alt.map(desugarElement);
|
|
684
|
+
}
|
|
685
|
+
function desugarElement(el) {
|
|
686
|
+
if (el.kind === 'term' || el.kind === 'ref' || el.kind === 'regex') {
|
|
687
|
+
return el;
|
|
688
|
+
}
|
|
689
|
+
if (el.kind === 'group') {
|
|
690
|
+
// Recurse into the group's alts so nested sugar is flattened,
|
|
691
|
+
// then emit a helper production whose body is those alts.
|
|
692
|
+
const innerAlts = el.alts.map((a) => desugarAlt(a));
|
|
693
|
+
const name = freshName('group');
|
|
694
|
+
extra.push({ name, alts: innerAlts, nodeKind: 'helper' });
|
|
695
|
+
return { kind: 'ref', name };
|
|
696
|
+
}
|
|
697
|
+
// `opt`, `star`, `plus` all wrap a single inner element.
|
|
698
|
+
const inner = desugarElement(el.inner);
|
|
699
|
+
const hint = inner.kind === 'ref' ? inner.name :
|
|
700
|
+
inner.kind === 'term' ? 'term' : 'x';
|
|
701
|
+
if (el.kind === 'opt') {
|
|
702
|
+
// H ::= inner | (empty)
|
|
703
|
+
const name = freshName('opt_' + hint);
|
|
704
|
+
extra.push({ name, alts: [[inner], []], nodeKind: 'helper' });
|
|
705
|
+
return { kind: 'ref', name };
|
|
706
|
+
}
|
|
707
|
+
if (el.kind === 'star') {
|
|
708
|
+
// H = inner H / (empty)
|
|
709
|
+
const name = freshName('star_' + hint);
|
|
710
|
+
const selfRef = { kind: 'ref', name };
|
|
711
|
+
extra.push({ name, alts: [[inner, selfRef], []], nodeKind: 'helper' });
|
|
712
|
+
return { kind: 'ref', name };
|
|
713
|
+
}
|
|
714
|
+
if (el.kind === 'plus') {
|
|
715
|
+
// H = inner Tail where Tail = inner Tail / (empty)
|
|
716
|
+
const tailName = freshName('star_' + hint);
|
|
717
|
+
const plusName = freshName('plus_' + hint);
|
|
718
|
+
const tailRef = { kind: 'ref', name: tailName };
|
|
719
|
+
extra.push({
|
|
720
|
+
name: tailName,
|
|
721
|
+
alts: [[inner, tailRef], []],
|
|
722
|
+
nodeKind: 'helper',
|
|
723
|
+
});
|
|
724
|
+
extra.push({
|
|
725
|
+
name: plusName,
|
|
726
|
+
alts: [[inner, tailRef]],
|
|
727
|
+
nodeKind: 'helper',
|
|
728
|
+
});
|
|
729
|
+
return { kind: 'ref', name: plusName };
|
|
730
|
+
}
|
|
731
|
+
// ABNF m*n bounded repetition. Desugars to a concatenation of
|
|
732
|
+
// `min` mandatory copies of the inner element followed by a
|
|
733
|
+
// tail that accepts up to `(max - min)` more.
|
|
734
|
+
// m*n A => A{m} [A[A[A...[A]]]] (nested optionals)
|
|
735
|
+
// m* A => A{m} *A (mandatory prefix + star)
|
|
736
|
+
// *n A => [A [A ... [A]]] (n nested optionals)
|
|
737
|
+
// The helper's single alt has `min` repetitions of inner, then
|
|
738
|
+
// either a star-helper for (min, ∞) or `max - min` nested
|
|
739
|
+
// optionals for a finite range.
|
|
740
|
+
const { min, max } = el;
|
|
741
|
+
const repName = freshName('rep_' + hint);
|
|
742
|
+
const repAlt = [];
|
|
743
|
+
for (let i = 0; i < min; i++)
|
|
744
|
+
repAlt.push(inner);
|
|
745
|
+
if (max === Infinity) {
|
|
746
|
+
// Tail: unbounded star of inner.
|
|
747
|
+
const tailStarName = freshName('star_' + hint);
|
|
748
|
+
const tailStarRef = { kind: 'ref', name: tailStarName };
|
|
749
|
+
extra.push({
|
|
750
|
+
name: tailStarName,
|
|
751
|
+
alts: [[inner, tailStarRef], []],
|
|
752
|
+
nodeKind: 'helper',
|
|
753
|
+
});
|
|
754
|
+
repAlt.push(tailStarRef);
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
// Nest (max - min) optionals: [A [A [A ...]]].
|
|
758
|
+
let nested = [];
|
|
759
|
+
for (let i = 0; i < max - min; i++) {
|
|
760
|
+
// Wrap current `nested` into an optional and prepend `inner`.
|
|
761
|
+
if (nested.length === 0) {
|
|
762
|
+
nested = [{ kind: 'opt', inner: { kind: 'group', alts: [[inner]] } }];
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
nested = [{
|
|
766
|
+
kind: 'opt',
|
|
767
|
+
inner: { kind: 'group', alts: [[inner, ...nested]] },
|
|
768
|
+
}];
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
repAlt.push(...nested);
|
|
772
|
+
}
|
|
773
|
+
extra.push({ name: repName, alts: [desugarAlt(repAlt)], nodeKind: 'helper' });
|
|
774
|
+
return { kind: 'ref', name: repName };
|
|
775
|
+
}
|
|
776
|
+
const rewritten = grammar.productions.map((p) => {
|
|
777
|
+
const out = {
|
|
778
|
+
name: p.name,
|
|
779
|
+
alts: p.alts.map(desugarAlt),
|
|
780
|
+
nodeKind: p.nodeKind,
|
|
781
|
+
};
|
|
782
|
+
// Probe-dispatch flags survive desugar unchanged — the emitter
|
|
783
|
+
// routes around the standard alt-compilation path for these.
|
|
784
|
+
if (p.probeDispatch)
|
|
785
|
+
out.probeDispatch = p.probeDispatch;
|
|
786
|
+
if (p.probeHelper)
|
|
787
|
+
out.probeHelper = p.probeHelper;
|
|
788
|
+
return out;
|
|
789
|
+
});
|
|
790
|
+
return { productions: [...rewritten, ...extra] };
|
|
791
|
+
}
|
|
792
|
+
// Error raised when the ABNF source itself can't be parsed. Surfaces
|
|
793
|
+
// line and column from the underlying tabnas error so the caller can
|
|
794
|
+
// report them directly. The original error is kept on `.cause`.
|
|
795
|
+
class AbnfParseError extends Error {
|
|
796
|
+
constructor(message, location, cause) {
|
|
797
|
+
super(message);
|
|
798
|
+
this.name = 'AbnfParseError';
|
|
799
|
+
this.line = location?.line;
|
|
800
|
+
this.column = location?.column;
|
|
801
|
+
this.cause = cause;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
exports.AbnfParseError = AbnfParseError;
|
|
805
|
+
// Parse ABNF source into a grammar AST via the tabnas-based parser.
|
|
806
|
+
function parseAbnf(src) {
|
|
807
|
+
const parser = getAbnfParser();
|
|
808
|
+
let productions;
|
|
809
|
+
try {
|
|
810
|
+
productions = parser(src) ?? [];
|
|
811
|
+
}
|
|
812
|
+
catch (e) {
|
|
813
|
+
// TabnasError carries `lineNumber` / `columnNumber`; fall back to
|
|
814
|
+
// ad-hoc extraction from the error message otherwise.
|
|
815
|
+
const line = e?.lineNumber ?? e?.row;
|
|
816
|
+
const column = e?.columnNumber ?? e?.col;
|
|
817
|
+
const loc = (line != null && column != null)
|
|
818
|
+
? ` at line ${line}, column ${column}`
|
|
819
|
+
: '';
|
|
820
|
+
const raw = e?.message ? String(e.message).split('\n')[0] : String(e);
|
|
821
|
+
throw new AbnfParseError(`abnf: parse error${loc}: ${raw}`, { line, column }, e);
|
|
822
|
+
}
|
|
823
|
+
if (!Array.isArray(productions) || productions.length === 0) {
|
|
824
|
+
throw new AbnfParseError('abnf: no productions found');
|
|
825
|
+
}
|
|
826
|
+
const merged = mergeIncrementals(productions);
|
|
827
|
+
return { productions: withCoreRules(merged) };
|
|
828
|
+
}
|
|
829
|
+
// RFC 5234 Appendix B.1 core rules. Parsed lazily on first use
|
|
830
|
+
// and spliced into any user grammar that references them but
|
|
831
|
+
// doesn't define them locally.
|
|
832
|
+
const CORE_RULES_ABNF = `
|
|
833
|
+
ALPHA = %x41-5A / %x61-7A
|
|
834
|
+
BIT = "0" / "1"
|
|
835
|
+
CHAR = %x01-7F
|
|
836
|
+
CR = %x0D
|
|
837
|
+
LF = %x0A
|
|
838
|
+
CRLF = CR LF
|
|
839
|
+
CTL = %x00-1F / %x7F
|
|
840
|
+
DIGIT = %x30-39
|
|
841
|
+
DQUOTE = %x22
|
|
842
|
+
HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
|
|
843
|
+
HTAB = %x09
|
|
844
|
+
OCTET = %x00-FF
|
|
845
|
+
SP = %x20
|
|
846
|
+
VCHAR = %x21-7E
|
|
847
|
+
WSP = SP / HTAB
|
|
848
|
+
`;
|
|
849
|
+
let _coreRules = null;
|
|
850
|
+
function getCoreRules() {
|
|
851
|
+
if (_coreRules)
|
|
852
|
+
return _coreRules;
|
|
853
|
+
const parser = getAbnfParser();
|
|
854
|
+
const raw = parser(CORE_RULES_ABNF);
|
|
855
|
+
// Core rules flatten to `src` in the output AST — they're
|
|
856
|
+
// character-class bricks, not structural nodes users want to see
|
|
857
|
+
// one-per-matched-character.
|
|
858
|
+
for (const p of raw)
|
|
859
|
+
p.nodeKind = 'core';
|
|
860
|
+
_coreRules = new Map(raw.map((p) => [p.name, p]));
|
|
861
|
+
return _coreRules;
|
|
862
|
+
}
|
|
863
|
+
function refsIn(alt, out) {
|
|
864
|
+
for (const el of alt) {
|
|
865
|
+
if (el.kind === 'ref')
|
|
866
|
+
out.add(el.name);
|
|
867
|
+
else if (el.kind === 'opt' || el.kind === 'star' ||
|
|
868
|
+
el.kind === 'plus' || el.kind === 'rep') {
|
|
869
|
+
refsIn([el.inner], out);
|
|
870
|
+
}
|
|
871
|
+
else if (el.kind === 'group') {
|
|
872
|
+
for (const a of el.alts)
|
|
873
|
+
refsIn(a, out);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
// Add each RFC 5234 core rule that the user's grammar references
|
|
878
|
+
// but doesn't define locally. Resolution is transitive: if the
|
|
879
|
+
// user mentions HEXDIG, DIGIT is pulled in too. User definitions
|
|
880
|
+
// always win — a local `DIGIT = …` is left untouched.
|
|
881
|
+
function withCoreRules(user) {
|
|
882
|
+
const core = getCoreRules();
|
|
883
|
+
const defined = new Set(user.map((p) => p.name));
|
|
884
|
+
const needed = new Set();
|
|
885
|
+
const scan = (prods) => {
|
|
886
|
+
for (const p of prods) {
|
|
887
|
+
for (const alt of p.alts)
|
|
888
|
+
refsIn(alt, needed);
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
scan(user);
|
|
892
|
+
const out = [];
|
|
893
|
+
// Transitively add core rules, in declaration order.
|
|
894
|
+
let added = true;
|
|
895
|
+
while (added) {
|
|
896
|
+
added = false;
|
|
897
|
+
for (const [name, prod] of core) {
|
|
898
|
+
if (defined.has(name))
|
|
899
|
+
continue;
|
|
900
|
+
if (!needed.has(name))
|
|
901
|
+
continue;
|
|
902
|
+
defined.add(name);
|
|
903
|
+
out.push(prod);
|
|
904
|
+
scan([prod]);
|
|
905
|
+
added = true;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return [...user, ...out];
|
|
909
|
+
}
|
|
910
|
+
// Fold every `name =/ alt` production into the earlier production
|
|
911
|
+
// with the same name by appending its alternatives. Throws if an
|
|
912
|
+
// incremental references a name that hasn't been defined yet — ABNF
|
|
913
|
+
// requires the base production to appear first.
|
|
914
|
+
function mergeIncrementals(prods) {
|
|
915
|
+
const out = [];
|
|
916
|
+
const byName = new Map();
|
|
917
|
+
for (const p of prods) {
|
|
918
|
+
if (p.incremental) {
|
|
919
|
+
const base = byName.get(p.name);
|
|
920
|
+
if (!base) {
|
|
921
|
+
throw new AbnfParseError(`abnf: '${p.name} =/ …' has no earlier '${p.name} = …' to extend`);
|
|
922
|
+
}
|
|
923
|
+
base.alts.push(...p.alts);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
// Strip the (absent) flag on a cleanly-written production so
|
|
927
|
+
// downstream code never sees it.
|
|
928
|
+
const clean = { name: p.name, alts: p.alts };
|
|
929
|
+
if (p.nodeKind)
|
|
930
|
+
clean.nodeKind = p.nodeKind;
|
|
931
|
+
out.push(clean);
|
|
932
|
+
byName.set(p.name, clean);
|
|
933
|
+
}
|
|
934
|
+
return out;
|
|
935
|
+
}
|
|
936
|
+
// -- Probe-dispatch analyser + rewriter -----------------------------
|
|
937
|
+
//
|
|
938
|
+
// ABNF has a large family of grammars that aren't LL(k) for any
|
|
939
|
+
// bounded k. The canonical example is RFC 3986's `authority`:
|
|
940
|
+
//
|
|
941
|
+
// authority = [ userinfo "@" ] host [ ":" port ]
|
|
942
|
+
// userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
|
|
943
|
+
// host = IP-literal / IPv4address / reg-name
|
|
944
|
+
// reg-name = *( unreserved / pct-encoded / sub-delims )
|
|
945
|
+
//
|
|
946
|
+
// `userinfo` and `reg-name` share a character vocabulary, so a
|
|
947
|
+
// FIRST-set dispatcher can't decide which branch the optional
|
|
948
|
+
// `[ userinfo "@" ]` belongs to — the disambiguating `@` can be
|
|
949
|
+
// arbitrarily far from the start.
|
|
950
|
+
//
|
|
951
|
+
// For the common pattern `[X D] Y` — an optional group whose body
|
|
952
|
+
// ends with a terminal D, followed by a sequence Y whose leading
|
|
953
|
+
// terminals overlap with X's — we handle the ambiguity by rewriting
|
|
954
|
+
// the rule to a probe+phase-retry dispatcher:
|
|
955
|
+
//
|
|
956
|
+
// 1. On first entry (phase 0), mark the token position and push a
|
|
957
|
+
// failure-proof probe rule that greedily consumes every token
|
|
958
|
+
// in the joint vocabulary of X and Y.
|
|
959
|
+
// 2. When the probe returns, peek ctx.t[0]:
|
|
960
|
+
// D seen → phase = 1 (take the `X D Y` branch)
|
|
961
|
+
// D absent → phase = 2 (take the `Y` branch)
|
|
962
|
+
// Rewind to the mark and `r:` back into the dispatcher.
|
|
963
|
+
// 3. The dispatcher's open has a `c:`-guarded alt for each phase
|
|
964
|
+
// that pushes the corresponding committed branch.
|
|
965
|
+
//
|
|
966
|
+
// The primitives used (`r:`, `k:`, `c:`, `ctx.mark`, `ctx.rewind`,
|
|
967
|
+
// `ctx.t`) are the same building blocks rules/parser already exposes
|
|
968
|
+
// — no new tabnas machinery is needed.
|
|
969
|
+
// Predicate: element is `[ X D ]` where X is one or more elements
|
|
970
|
+
// and D is a terminal literal or a regex terminal.
|
|
971
|
+
function isProbeableOpt(el) {
|
|
972
|
+
if (el.kind !== 'opt')
|
|
973
|
+
return null;
|
|
974
|
+
const inner = el.inner;
|
|
975
|
+
if (inner.kind !== 'group')
|
|
976
|
+
return null;
|
|
977
|
+
if (inner.alts.length !== 1)
|
|
978
|
+
return null;
|
|
979
|
+
const seq = inner.alts[0];
|
|
980
|
+
if (seq.length < 2)
|
|
981
|
+
return null;
|
|
982
|
+
const last = seq[seq.length - 1];
|
|
983
|
+
if (last.kind !== 'term' && last.kind !== 'regex')
|
|
984
|
+
return null;
|
|
985
|
+
return { xSeq: seq.slice(0, -1), disambiguator: last };
|
|
986
|
+
}
|
|
987
|
+
// Union of every terminal reachable by walking an element's subtree,
|
|
988
|
+
// following refs transitively. Cycles are broken by the visited set.
|
|
989
|
+
// Returns terminals as AbnfElements so the caller isn't tied to the
|
|
990
|
+
// emitter's token-allocation step.
|
|
991
|
+
function collectTerminalVocabElements(el, grammar, out, visited) {
|
|
992
|
+
if (el.kind === 'term') {
|
|
993
|
+
const k = termKey(el);
|
|
994
|
+
if (!out.has(k))
|
|
995
|
+
out.set(k, el);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
if (el.kind === 'regex') {
|
|
999
|
+
const k = regexKey(el);
|
|
1000
|
+
if (!out.has(k))
|
|
1001
|
+
out.set(k, el);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
if (el.kind === 'ref') {
|
|
1005
|
+
if (visited.has(el.name))
|
|
1006
|
+
return;
|
|
1007
|
+
visited.add(el.name);
|
|
1008
|
+
const prod = grammar.productions.find((p) => p.name === el.name);
|
|
1009
|
+
if (!prod)
|
|
1010
|
+
return;
|
|
1011
|
+
for (const alt of prod.alts)
|
|
1012
|
+
for (const sub of alt)
|
|
1013
|
+
collectTerminalVocabElements(sub, grammar, out, visited);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
if (el.kind === 'opt' || el.kind === 'star' || el.kind === 'plus' ||
|
|
1017
|
+
el.kind === 'rep') {
|
|
1018
|
+
collectTerminalVocabElements(el.inner, grammar, out, visited);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (el.kind === 'group') {
|
|
1022
|
+
for (const alt of el.alts)
|
|
1023
|
+
for (const sub of alt)
|
|
1024
|
+
collectTerminalVocabElements(sub, grammar, out, visited);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
function collectSeqVocabElements(seq, grammar) {
|
|
1029
|
+
const out = new Map();
|
|
1030
|
+
const visited = new Set();
|
|
1031
|
+
for (const el of seq)
|
|
1032
|
+
collectTerminalVocabElements(el, grammar, out, visited);
|
|
1033
|
+
return out;
|
|
1034
|
+
}
|
|
1035
|
+
function mapsOverlap(a, b) {
|
|
1036
|
+
for (const x of a.keys())
|
|
1037
|
+
if (b.has(x))
|
|
1038
|
+
return true;
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
// Rewrite every ambiguous `[X D] Y` subsequence in `grammar` into a
|
|
1042
|
+
// probe-dispatch pattern. The grammar at this point still has `opt`,
|
|
1043
|
+
// `group`, `star`, `plus`, `rep` sugar — intentionally, since that's
|
|
1044
|
+
// where the pattern is easy to recognise. Runs BEFORE token
|
|
1045
|
+
// allocation; probe metadata stores AbnfElements, and the emitter
|
|
1046
|
+
// resolves them to token names at emit time.
|
|
1047
|
+
function rewriteProbeDispatches(grammar) {
|
|
1048
|
+
const reports = grammar.ambiguities ?? [];
|
|
1049
|
+
const extra = [];
|
|
1050
|
+
const used = new Set(grammar.productions.map((p) => p.name));
|
|
1051
|
+
function freshName(hint) {
|
|
1052
|
+
let name = hint;
|
|
1053
|
+
let i = 1;
|
|
1054
|
+
while (used.has(name)) {
|
|
1055
|
+
name = hint + i;
|
|
1056
|
+
i++;
|
|
1057
|
+
}
|
|
1058
|
+
used.add(name);
|
|
1059
|
+
return name;
|
|
1060
|
+
}
|
|
1061
|
+
const rewritten = [];
|
|
1062
|
+
for (const prod of grammar.productions) {
|
|
1063
|
+
let newAlts = [];
|
|
1064
|
+
let touched = false;
|
|
1065
|
+
for (let altIdx = 0; altIdx < prod.alts.length; altIdx++) {
|
|
1066
|
+
const alt = prod.alts[altIdx];
|
|
1067
|
+
let resultAlt = [];
|
|
1068
|
+
for (let i = 0; i < alt.length; i++) {
|
|
1069
|
+
const el = alt[i];
|
|
1070
|
+
const info = isProbeableOpt(el);
|
|
1071
|
+
if (!info) {
|
|
1072
|
+
resultAlt.push(el);
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
const ySeq = alt.slice(i + 1);
|
|
1076
|
+
if (ySeq.length === 0) {
|
|
1077
|
+
// `[X D]` is the last thing in the alt — nothing follows, so
|
|
1078
|
+
// there's nothing to disambiguate against. Standard emit.
|
|
1079
|
+
resultAlt.push(el);
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
const xVocab = collectSeqVocabElements(info.xSeq, grammar);
|
|
1083
|
+
const yVocab = collectSeqVocabElements(ySeq, grammar);
|
|
1084
|
+
if (!mapsOverlap(xVocab, yVocab)) {
|
|
1085
|
+
// The optional's leading tokens don't overlap with the tail's
|
|
1086
|
+
// leading tokens, so the normal FIRST-based dispatcher can
|
|
1087
|
+
// decide. No rewrite needed.
|
|
1088
|
+
resultAlt.push(el);
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
// Joint vocab: union of everything the probe might need to
|
|
1092
|
+
// consume. Includes the disambiguator, which we then remove so
|
|
1093
|
+
// the probe stops on it and the peek works.
|
|
1094
|
+
const vocab = new Map([...xVocab, ...yVocab]);
|
|
1095
|
+
const d = info.disambiguator;
|
|
1096
|
+
const dKey = d.kind === 'term' ? termKey(d)
|
|
1097
|
+
: d.kind === 'regex' ? regexKey(d)
|
|
1098
|
+
: null;
|
|
1099
|
+
if (dKey)
|
|
1100
|
+
vocab.delete(dKey);
|
|
1101
|
+
const dispatchName = freshName(`${prod.name}$pd${i}`);
|
|
1102
|
+
const probeName = freshName(`${dispatchName}$probe`);
|
|
1103
|
+
const withName = freshName(`${dispatchName}$with`);
|
|
1104
|
+
const noName = freshName(`${dispatchName}$no`);
|
|
1105
|
+
// Synthesise the probe helper.
|
|
1106
|
+
extra.push({
|
|
1107
|
+
name: probeName,
|
|
1108
|
+
alts: [],
|
|
1109
|
+
probeHelper: { vocabElements: [...vocab.values()] },
|
|
1110
|
+
nodeKind: 'helper',
|
|
1111
|
+
});
|
|
1112
|
+
// Synthesise the committed branches. `with` = X D Y, `no` = Y.
|
|
1113
|
+
extra.push({
|
|
1114
|
+
name: withName,
|
|
1115
|
+
alts: [[...info.xSeq, info.disambiguator, ...ySeq]],
|
|
1116
|
+
nodeKind: 'helper',
|
|
1117
|
+
});
|
|
1118
|
+
extra.push({
|
|
1119
|
+
name: noName,
|
|
1120
|
+
alts: [ySeq],
|
|
1121
|
+
nodeKind: 'helper',
|
|
1122
|
+
});
|
|
1123
|
+
// Synthesise the dispatcher. The `alts` list is a "virtual"
|
|
1124
|
+
// spec — two ref-only alts — that exists solely to feed
|
|
1125
|
+
// computeFirstSets the right FIRST/nullable answers (FIRST
|
|
1126
|
+
// = FIRST(with) ∪ FIRST(no)). The emitter checks
|
|
1127
|
+
// `probeDispatch` first and emits the phase-retry body
|
|
1128
|
+
// instead of compiling `alts`.
|
|
1129
|
+
extra.push({
|
|
1130
|
+
name: dispatchName,
|
|
1131
|
+
alts: [
|
|
1132
|
+
[{ kind: 'ref', name: withName }],
|
|
1133
|
+
[{ kind: 'ref', name: noName }],
|
|
1134
|
+
],
|
|
1135
|
+
probeDispatch: {
|
|
1136
|
+
probeRule: probeName,
|
|
1137
|
+
disambiguator: info.disambiguator,
|
|
1138
|
+
withBranch: withName,
|
|
1139
|
+
noBranch: noName,
|
|
1140
|
+
},
|
|
1141
|
+
nodeKind: 'helper',
|
|
1142
|
+
});
|
|
1143
|
+
reports.push({
|
|
1144
|
+
rule: prod.name, altIdx, optIdx: i,
|
|
1145
|
+
reason: `optional prefix shares vocabulary with tail`,
|
|
1146
|
+
resolved: true,
|
|
1147
|
+
});
|
|
1148
|
+
resultAlt.push({ kind: 'ref', name: dispatchName });
|
|
1149
|
+
// Everything that followed the opt is now inside the dispatcher
|
|
1150
|
+
// (withBranch / noBranch), so skip the rest of the alt.
|
|
1151
|
+
i = alt.length;
|
|
1152
|
+
touched = true;
|
|
1153
|
+
}
|
|
1154
|
+
newAlts.push(resultAlt);
|
|
1155
|
+
}
|
|
1156
|
+
if (touched) {
|
|
1157
|
+
rewritten.push({
|
|
1158
|
+
name: prod.name,
|
|
1159
|
+
alts: newAlts,
|
|
1160
|
+
nodeKind: prod.nodeKind,
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
else {
|
|
1164
|
+
rewritten.push(prod);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return {
|
|
1168
|
+
productions: [...rewritten, ...extra],
|
|
1169
|
+
ambiguities: reports,
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
// Emit a probe helper production. A self-looping rule that matches any
|
|
1173
|
+
// one of the vocab tokens and restarts; a final empty-alt fallback
|
|
1174
|
+
// ensures the rule NEVER fails — if the current lookahead isn't in the
|
|
1175
|
+
// vocab (or we're at #ZZ), the rule pops cleanly. This is the
|
|
1176
|
+
// failure-proof property the probe pattern relies on.
|
|
1177
|
+
function emitProbeHelper(prod, tag, ruleSpec, literals, regexTokens) {
|
|
1178
|
+
const elems = prod.probeHelper.vocabElements;
|
|
1179
|
+
const opens = [];
|
|
1180
|
+
for (const el of elems) {
|
|
1181
|
+
const tok = el.kind === 'term'
|
|
1182
|
+
? literals.get(termKey(el))
|
|
1183
|
+
: el.kind === 'regex' ? regexTokens.get(regexKey(el))
|
|
1184
|
+
: undefined;
|
|
1185
|
+
if (tok)
|
|
1186
|
+
opens.push({ s: tok, r: prod.name, g: tag });
|
|
1187
|
+
}
|
|
1188
|
+
// Empty fallback — pops without consuming anything. Must be last.
|
|
1189
|
+
opens.push({ g: tag });
|
|
1190
|
+
ruleSpec[prod.name] = { open: opens };
|
|
1191
|
+
}
|
|
1192
|
+
// Emit a probe-dispatch production. Encodes the three-phase retry
|
|
1193
|
+
// pattern; uses only standard tabnas primitives (r:, p:, c:, k:,
|
|
1194
|
+
// ctx.mark/rewind/t).
|
|
1195
|
+
function emitProbeDispatch(prod, tag, ruleSpec, refs, literals, regexTokens, useBuiltins) {
|
|
1196
|
+
const { probeRule, disambiguator, withBranch, noBranch } = prod.probeDispatch;
|
|
1197
|
+
const disambiguatorToken = disambiguator.kind === 'term'
|
|
1198
|
+
? literals.get(termKey(disambiguator))
|
|
1199
|
+
: disambiguator.kind === 'regex'
|
|
1200
|
+
? regexTokens.get(regexKey(disambiguator))
|
|
1201
|
+
: undefined;
|
|
1202
|
+
if (!disambiguatorToken) {
|
|
1203
|
+
throw new Error(`abnf: probe-dispatch rule '${prod.name}' has unresolvable ` +
|
|
1204
|
+
`disambiguator (kind=${disambiguator.kind})`);
|
|
1205
|
+
}
|
|
1206
|
+
// `bubble` lifts the committed child's node up — pure tree-building
|
|
1207
|
+
// (a `@bubble$` builtin or a closure, per refs mode; dropped in
|
|
1208
|
+
// recognition mode either way).
|
|
1209
|
+
const bubbleFields = refs.bubble((r) => {
|
|
1210
|
+
if (r.child && r.child.node !== undefined)
|
|
1211
|
+
r.node = r.child.node;
|
|
1212
|
+
});
|
|
1213
|
+
if (useBuiltins) {
|
|
1214
|
+
// Function-free dispatcher: control logic is engine `$`-builtins,
|
|
1215
|
+
// the disambiguator token rides in `k` config. See
|
|
1216
|
+
// docs/design/alt-action-refs.md §6.3.
|
|
1217
|
+
ruleSpec[prod.name] = {
|
|
1218
|
+
open: [
|
|
1219
|
+
{ c: '@probePhase0$', a: '@probeInit$', p: probeRule,
|
|
1220
|
+
k: { pd_d: disambiguatorToken }, g: tag },
|
|
1221
|
+
{ c: '@probePhase1$', p: withBranch, g: tag },
|
|
1222
|
+
{ c: '@probePhase2$', p: noBranch, g: tag },
|
|
1223
|
+
],
|
|
1224
|
+
close: [
|
|
1225
|
+
{ c: '@probePhase0$', a: '@probeDecide$', r: prod.name, g: tag },
|
|
1226
|
+
{ ...bubbleFields, g: tag },
|
|
1227
|
+
],
|
|
1228
|
+
};
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
const initMark = refs.register((r, ctx) => {
|
|
1232
|
+
r.k.pd_phase = 0;
|
|
1233
|
+
r.k.pd_mark = ctx.mark();
|
|
1234
|
+
});
|
|
1235
|
+
const decide = refs.register((r, ctx) => {
|
|
1236
|
+
// ctx.t[0] is the first token the probe didn't consume. The probe
|
|
1237
|
+
// never fails, so this always reflects a real position.
|
|
1238
|
+
const peek = ctx.t[0];
|
|
1239
|
+
ctx.rewind(r.k.pd_mark);
|
|
1240
|
+
const matched = peek && peek.name === disambiguatorToken;
|
|
1241
|
+
r.k.pd_phase = matched ? 1 : 2;
|
|
1242
|
+
});
|
|
1243
|
+
ruleSpec[prod.name] = {
|
|
1244
|
+
open: [
|
|
1245
|
+
// Phase 0 — first pass: mark and probe.
|
|
1246
|
+
{
|
|
1247
|
+
c: refs.register((r) => !r.k.pd_phase),
|
|
1248
|
+
a: initMark,
|
|
1249
|
+
p: probeRule,
|
|
1250
|
+
g: tag,
|
|
1251
|
+
},
|
|
1252
|
+
// Phase 1 — disambiguator was seen: commit to X D Y.
|
|
1253
|
+
{
|
|
1254
|
+
c: refs.register((r) => r.k.pd_phase === 1),
|
|
1255
|
+
p: withBranch,
|
|
1256
|
+
g: tag,
|
|
1257
|
+
},
|
|
1258
|
+
// Phase 2 — disambiguator was not seen: commit to Y alone.
|
|
1259
|
+
{
|
|
1260
|
+
c: refs.register((r) => r.k.pd_phase === 2),
|
|
1261
|
+
p: noBranch,
|
|
1262
|
+
g: tag,
|
|
1263
|
+
},
|
|
1264
|
+
],
|
|
1265
|
+
close: [
|
|
1266
|
+
// Phase 0 close: decide phase based on peek, rewind, retry self.
|
|
1267
|
+
{
|
|
1268
|
+
c: refs.register((r) => r.k.pd_phase === 0),
|
|
1269
|
+
a: decide,
|
|
1270
|
+
r: prod.name,
|
|
1271
|
+
g: tag,
|
|
1272
|
+
},
|
|
1273
|
+
// Phase 1 / 2 close: lift the committed child's node up.
|
|
1274
|
+
{ ...bubbleFields, g: tag },
|
|
1275
|
+
],
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
// Convert an ABNF grammar AST into a tabnas GrammarSpec.
|
|
1279
|
+
function emitGrammarSpec(grammar, opts) {
|
|
1280
|
+
const start = opts?.start ?? grammar.productions[0].name;
|
|
1281
|
+
const tag = opts?.tag ?? 'abnf';
|
|
1282
|
+
// Eliminate direct left recursion (P → P α | β) by rewriting to
|
|
1283
|
+
// the equivalent right-recursive form P → β (α)*, then detect
|
|
1284
|
+
// ambiguous `[X D] Y` optional-prefix patterns and rewrite them
|
|
1285
|
+
// into probe-dispatch helpers; finally flatten any EBNF sugar
|
|
1286
|
+
// (`?`, `*`, `+`, grouping) into plain ABNF.
|
|
1287
|
+
grammar = eliminateLeftRecursion(grammar);
|
|
1288
|
+
grammar = rewriteProbeDispatches(grammar);
|
|
1289
|
+
grammar = desugar(grammar);
|
|
1290
|
+
// Allocate a fixed token for each unique literal, and a match
|
|
1291
|
+
// token for each unique regex terminal. Literals are keyed by
|
|
1292
|
+
// (literal, effective-case-sensitivity) so a `%s"foo"` (sensitive)
|
|
1293
|
+
// and a bare `"foo"` (insensitive) produce distinct tokens.
|
|
1294
|
+
const literals = new Map(); // literal-key -> token name
|
|
1295
|
+
const regexTokens = new Map(); // regex key -> token name
|
|
1296
|
+
const usedNames = new Set();
|
|
1297
|
+
const fixedTokens = {};
|
|
1298
|
+
const matchTokens = {};
|
|
1299
|
+
for (const prod of grammar.productions) {
|
|
1300
|
+
for (const alt of prod.alts) {
|
|
1301
|
+
for (const el of alt) {
|
|
1302
|
+
if (el.kind === 'term') {
|
|
1303
|
+
const key = termKey(el);
|
|
1304
|
+
if (!literals.has(key)) {
|
|
1305
|
+
const name = allocTokenName(el.literal, usedNames);
|
|
1306
|
+
literals.set(key, name);
|
|
1307
|
+
if (isEffectivelyCaseSensitive(el)) {
|
|
1308
|
+
fixedTokens[name] = el.literal;
|
|
1309
|
+
}
|
|
1310
|
+
else {
|
|
1311
|
+
// Insensitive literal with at least one letter — emit
|
|
1312
|
+
// as an anchored regex with the `i` flag. Mark the
|
|
1313
|
+
// matcher `eager$` so tabnas's lexer fires it even
|
|
1314
|
+
// when the current rule's tcol doesn't list its tin.
|
|
1315
|
+
const re = new RegExp('^' + escapeRegExp(el.literal), 'i');
|
|
1316
|
+
re.eager$ = true;
|
|
1317
|
+
matchTokens[name] = re;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
else if (el.kind === 'regex') {
|
|
1322
|
+
const key = regexKey(el);
|
|
1323
|
+
if (!regexTokens.has(key)) {
|
|
1324
|
+
const name = allocTokenName('rx_' + el.pattern, usedNames);
|
|
1325
|
+
regexTokens.set(key, name);
|
|
1326
|
+
matchTokens[name] = new RegExp('^' + el.pattern, el.flags);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
// Probe-helper productions store their vocab as AbnfElements —
|
|
1332
|
+
// walk those too so the required tokens get allocated.
|
|
1333
|
+
if (prod.probeHelper) {
|
|
1334
|
+
for (const el of prod.probeHelper.vocabElements) {
|
|
1335
|
+
if (el.kind === 'term') {
|
|
1336
|
+
const key = termKey(el);
|
|
1337
|
+
if (!literals.has(key)) {
|
|
1338
|
+
const name = allocTokenName(el.literal, usedNames);
|
|
1339
|
+
literals.set(key, name);
|
|
1340
|
+
if (isEffectivelyCaseSensitive(el)) {
|
|
1341
|
+
fixedTokens[name] = el.literal;
|
|
1342
|
+
}
|
|
1343
|
+
else {
|
|
1344
|
+
const re = new RegExp('^' + escapeRegExp(el.literal), 'i');
|
|
1345
|
+
re.eager$ = true;
|
|
1346
|
+
matchTokens[name] = re;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
else if (el.kind === 'regex') {
|
|
1351
|
+
const key = regexKey(el);
|
|
1352
|
+
if (!regexTokens.has(key)) {
|
|
1353
|
+
const name = allocTokenName('rx_' + el.pattern, usedNames);
|
|
1354
|
+
regexTokens.set(key, name);
|
|
1355
|
+
matchTokens[name] = new RegExp('^' + el.pattern, el.flags);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
const knownRules = new Set(grammar.productions.map((p) => p.name));
|
|
1362
|
+
const { firstSets, nullable } = computeFirstSets(grammar, literals, regexTokens);
|
|
1363
|
+
const refs = new RefRegistry();
|
|
1364
|
+
refs.useBuiltins = !!opts?.builtins;
|
|
1365
|
+
refs.emitMarks = !!opts?.marks;
|
|
1366
|
+
const ruleSpec = {};
|
|
1367
|
+
for (const prod of grammar.productions) {
|
|
1368
|
+
if (prod.probeHelper) {
|
|
1369
|
+
emitProbeHelper(prod, tag, ruleSpec, literals, regexTokens);
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
if (prod.probeDispatch) {
|
|
1373
|
+
emitProbeDispatch(prod, tag, ruleSpec, refs, literals, regexTokens, !!opts?.builtins);
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
// Standard path: a (possibly single-segment) set of alternatives
|
|
1377
|
+
// compiled to tabnas alts. Simple alts collapse into `open` alts
|
|
1378
|
+
// directly; multi-segment alts emit a chain of aux rules.
|
|
1379
|
+
emitProduction(prod, grammar, literals, regexTokens, knownRules, tag, ruleSpec, firstSets, nullable, refs);
|
|
1380
|
+
}
|
|
1381
|
+
// Wrap the user-visible start rule in a synthetic rule that
|
|
1382
|
+
// explicitly consumes #ZZ. Without this, a user rule that pops
|
|
1383
|
+
// without matching the end-of-source token lets trailing content
|
|
1384
|
+
// slip past tabnas's post-loop endtkn check (the lookahead buffer
|
|
1385
|
+
// outlives the parse loop).
|
|
1386
|
+
const startWrapper = '__start__';
|
|
1387
|
+
ruleSpec[startWrapper] = {
|
|
1388
|
+
open: [{
|
|
1389
|
+
p: start,
|
|
1390
|
+
g: tag,
|
|
1391
|
+
}],
|
|
1392
|
+
close: [{
|
|
1393
|
+
s: '#ZZ',
|
|
1394
|
+
// Return the start rule's AST node directly — the `__start__`
|
|
1395
|
+
// wrapper exists only to ensure end-of-source gets consumed.
|
|
1396
|
+
// The caller of `tabnas(src)` receives the tagged user-rule
|
|
1397
|
+
// node (e.g. `{rule: 'URI', src, kids: [...]}`) unadorned.
|
|
1398
|
+
...refs.bubble((r) => {
|
|
1399
|
+
if (r.child && r.child.node !== undefined) {
|
|
1400
|
+
r.node = r.child.node;
|
|
1401
|
+
}
|
|
1402
|
+
}),
|
|
1403
|
+
g: tag,
|
|
1404
|
+
}],
|
|
1405
|
+
};
|
|
1406
|
+
const options = {
|
|
1407
|
+
fixed: { token: fixedTokens },
|
|
1408
|
+
rule: { start: startWrapper },
|
|
1409
|
+
};
|
|
1410
|
+
if (Object.keys(matchTokens).length > 0) {
|
|
1411
|
+
options.match = { token: matchTokens };
|
|
1412
|
+
}
|
|
1413
|
+
const spec = {
|
|
1414
|
+
ref: refs.map,
|
|
1415
|
+
options,
|
|
1416
|
+
rule: ruleSpec,
|
|
1417
|
+
};
|
|
1418
|
+
return spec;
|
|
1419
|
+
}
|
|
1420
|
+
// Break an alternative into segments. Each segment is a (possibly
|
|
1421
|
+
// empty) run of terminal tokens followed by at most one rule
|
|
1422
|
+
// reference. A single-segment alt has at most one ref, located at the
|
|
1423
|
+
// very end; everything else has two or more segments.
|
|
1424
|
+
function segmentize(alt, literals, regexTokens) {
|
|
1425
|
+
const segs = [];
|
|
1426
|
+
let current = { terms: [], ref: null };
|
|
1427
|
+
for (const el of alt) {
|
|
1428
|
+
if (el.kind === 'term') {
|
|
1429
|
+
current.terms.push(literals.get(termKey(el)));
|
|
1430
|
+
}
|
|
1431
|
+
else if (el.kind === 'regex') {
|
|
1432
|
+
const key = regexKey(el);
|
|
1433
|
+
current.terms.push(regexTokens.get(key));
|
|
1434
|
+
}
|
|
1435
|
+
else if (el.kind === 'ref') {
|
|
1436
|
+
current.ref = el.name;
|
|
1437
|
+
segs.push(current);
|
|
1438
|
+
current = { terms: [], ref: null };
|
|
1439
|
+
}
|
|
1440
|
+
else {
|
|
1441
|
+
// `opt`, `star`, `plus`, `group` must have been desugared
|
|
1442
|
+
// before reaching the emitter.
|
|
1443
|
+
throw new Error(`abnf: internal — unexpected element kind '${el.kind}' in emitter`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
if (current.terms.length > 0 || segs.length === 0) {
|
|
1447
|
+
segs.push(current);
|
|
1448
|
+
}
|
|
1449
|
+
return segs;
|
|
1450
|
+
}
|
|
1451
|
+
function regexKey(el) {
|
|
1452
|
+
return `/${el.pattern}/${el.flags}`;
|
|
1453
|
+
}
|
|
1454
|
+
function isSingleSegment(alt) {
|
|
1455
|
+
let sawRef = false;
|
|
1456
|
+
for (const el of alt) {
|
|
1457
|
+
if (el.kind === 'ref') {
|
|
1458
|
+
if (sawRef)
|
|
1459
|
+
return false;
|
|
1460
|
+
sawRef = true;
|
|
1461
|
+
}
|
|
1462
|
+
else if (el.kind === 'term' || el.kind === 'regex') {
|
|
1463
|
+
if (sawRef)
|
|
1464
|
+
return false; // terminal after a ref — multi-segment
|
|
1465
|
+
}
|
|
1466
|
+
else {
|
|
1467
|
+
// Desugar should have eliminated sugar kinds.
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
return true;
|
|
1472
|
+
}
|
|
1473
|
+
function validateRefs(alt, knownRules, ruleName) {
|
|
1474
|
+
for (const el of alt) {
|
|
1475
|
+
if (el.kind === 'ref' && !knownRules.has(el.name)) {
|
|
1476
|
+
throw new Error(`abnf: rule '${ruleName}' references unknown rule '${el.name}'`);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
// Registry used by the emitter to allocate unique `@`-prefixed
|
|
1481
|
+
// FuncRef names for inline action functions. The resulting spec is
|
|
1482
|
+
// still declarative: every function appears once, keyed by name,
|
|
1483
|
+
// under the spec's `ref` map.
|
|
1484
|
+
class RefRegistry {
|
|
1485
|
+
constructor() {
|
|
1486
|
+
this.refs = {};
|
|
1487
|
+
this.counter = 0;
|
|
1488
|
+
// When set, tree-building actions are emitted as engine `$`-builtin
|
|
1489
|
+
// refs + `k` config (pure data) instead of registered closures. See
|
|
1490
|
+
// docs/design/alt-action-refs.md §6.4 and implementation-diary.md.
|
|
1491
|
+
this.useBuiltins = false;
|
|
1492
|
+
// When set, the emitter stamps user-rule alts with a `m` mark.
|
|
1493
|
+
this.emitMarks = false;
|
|
1494
|
+
}
|
|
1495
|
+
register(fn) {
|
|
1496
|
+
const name = `@abnf_a${this.counter++}`;
|
|
1497
|
+
this.refs[name] = fn;
|
|
1498
|
+
return name;
|
|
1499
|
+
}
|
|
1500
|
+
get map() {
|
|
1501
|
+
return this.refs;
|
|
1502
|
+
}
|
|
1503
|
+
// Tree-action emitters. Each returns the alt-spec fields to merge
|
|
1504
|
+
// (`{a}` in closure mode, `{a, k}` in builtins mode).
|
|
1505
|
+
node(cfg, closure) {
|
|
1506
|
+
return this.useBuiltins
|
|
1507
|
+
? { a: '@node$', k: { node$: cfg } }
|
|
1508
|
+
: { a: this.register(closure) };
|
|
1509
|
+
}
|
|
1510
|
+
capture(cfg, closure) {
|
|
1511
|
+
return this.useBuiltins
|
|
1512
|
+
? { a: '@capture$', k: { capture$: cfg } }
|
|
1513
|
+
: { a: this.register(closure) };
|
|
1514
|
+
}
|
|
1515
|
+
bubble(closure) {
|
|
1516
|
+
return this.useBuiltins ? { a: '@bubble$' } : { a: this.register(closure) };
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
function mkAstNode(ruleName, nodeKind) {
|
|
1520
|
+
return nodeKind === 'user'
|
|
1521
|
+
? { rule: ruleName, src: '', kids: [] }
|
|
1522
|
+
: { src: '', kids: [] };
|
|
1523
|
+
}
|
|
1524
|
+
// A stable, human-predictable "mark" for an alternative — its leading
|
|
1525
|
+
// discriminator: the first matched token name (sans `#`), the pushed
|
|
1526
|
+
// rule name, or `_` for the empty alt. Used for `@<rule>:o|c:<mark>`
|
|
1527
|
+
// user-action references. See docs/design/alt-action-refs.md §3.
|
|
1528
|
+
function altDiscriminator(alt, literals, regexTokens) {
|
|
1529
|
+
if (alt.length === 0)
|
|
1530
|
+
return '_';
|
|
1531
|
+
const el = alt[0];
|
|
1532
|
+
if (el.kind === 'term') {
|
|
1533
|
+
return (literals.get(termKey(el)) || '').replace(/^#/, '') || '_';
|
|
1534
|
+
}
|
|
1535
|
+
if (el.kind === 'regex') {
|
|
1536
|
+
return (regexTokens.get(regexKey(el)) || '').replace(/^#/, '') || '_';
|
|
1537
|
+
}
|
|
1538
|
+
if (el.kind === 'ref')
|
|
1539
|
+
return el.name;
|
|
1540
|
+
return '_';
|
|
1541
|
+
}
|
|
1542
|
+
// Assign a unique mark per source alternative (same alt object → same
|
|
1543
|
+
// mark, so fan-out copies share it). Collisions get a `~N` suffix.
|
|
1544
|
+
function assignMarks(alts, literals, regexTokens) {
|
|
1545
|
+
const marks = new Map();
|
|
1546
|
+
const seen = new Map();
|
|
1547
|
+
for (const alt of alts) {
|
|
1548
|
+
const base = altDiscriminator(alt, literals, regexTokens);
|
|
1549
|
+
const n = (seen.get(base) || 0) + 1;
|
|
1550
|
+
seen.set(base, n);
|
|
1551
|
+
marks.set(alt, n === 1 ? base : `${base}~${n}`);
|
|
1552
|
+
}
|
|
1553
|
+
return marks;
|
|
1554
|
+
}
|
|
1555
|
+
function segmentToAlt(seg, tag, refs, initNode, ruleName, nodeKind) {
|
|
1556
|
+
const spec = { g: tag };
|
|
1557
|
+
if (seg.terms.length > 0)
|
|
1558
|
+
spec.s = seg.terms.join(' ');
|
|
1559
|
+
if (seg.ref)
|
|
1560
|
+
spec.p = seg.ref;
|
|
1561
|
+
// Default tree-building: accumulate each matched terminal's source
|
|
1562
|
+
// text into `r.node.src`. Head alts also allocate a fresh AST node
|
|
1563
|
+
// so the child doesn't inherit (and then mutate) its parent's.
|
|
1564
|
+
const nterms = seg.terms.length;
|
|
1565
|
+
if (nterms > 0 || initNode) {
|
|
1566
|
+
Object.assign(spec, refs.node({ init: initNode, rule: ruleName, kind: nodeKind, nterms }, (r) => {
|
|
1567
|
+
if (initNode)
|
|
1568
|
+
r.node = mkAstNode(ruleName, nodeKind);
|
|
1569
|
+
const n = r.node;
|
|
1570
|
+
for (let i = 0; i < nterms; i++)
|
|
1571
|
+
n.src += r.o[i].src;
|
|
1572
|
+
}));
|
|
1573
|
+
}
|
|
1574
|
+
return spec;
|
|
1575
|
+
}
|
|
1576
|
+
// Close-state action: merge the just-returned child rule's AST node
|
|
1577
|
+
// into the current rule's. Tagged children (user rules) get pushed
|
|
1578
|
+
// verbatim into `kids`; untagged (helper / core) flatten — their
|
|
1579
|
+
// `src` appends and their `kids` extend. Either way `src`
|
|
1580
|
+
// concatenates so every ancestor's `.src` reflects everything it
|
|
1581
|
+
// matched.
|
|
1582
|
+
function captureChildFields(refs, ruleName, nodeKind) {
|
|
1583
|
+
return refs.capture({ rule: ruleName, kind: nodeKind }, (r) => {
|
|
1584
|
+
if (r.node == null)
|
|
1585
|
+
r.node = mkAstNode(ruleName, nodeKind);
|
|
1586
|
+
const n = r.node;
|
|
1587
|
+
const c = r.child && r.child.node;
|
|
1588
|
+
if (c == null)
|
|
1589
|
+
return;
|
|
1590
|
+
if (typeof c !== 'object' || !('src' in c)) {
|
|
1591
|
+
// Legacy shape — wrap as a leaf kid.
|
|
1592
|
+
n.kids.push(c);
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
// Defensive: if the child somehow shares this rule's node
|
|
1596
|
+
// object, skip the merge rather than push a self-reference. (A
|
|
1597
|
+
// properly-emitted grammar always allocates fresh child nodes.)
|
|
1598
|
+
if (c === n)
|
|
1599
|
+
return;
|
|
1600
|
+
n.src += c.src;
|
|
1601
|
+
if (c.rule)
|
|
1602
|
+
n.kids.push(c);
|
|
1603
|
+
else if (Array.isArray(c.kids))
|
|
1604
|
+
n.kids.push(...c.kids);
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
function emitProduction(prod, grammar, literals, regexTokens, knownRules, tag, ruleSpec, firstSets, nullable, refs) {
|
|
1608
|
+
for (const alt of prod.alts) {
|
|
1609
|
+
validateRefs(alt, knownRules, prod.name);
|
|
1610
|
+
}
|
|
1611
|
+
const allSimple = prod.alts.every(isSingleSegment);
|
|
1612
|
+
if (allSimple) {
|
|
1613
|
+
// Every alternative collapses to one tabnas alt — emit them
|
|
1614
|
+
// directly into the production's open state. This is a head
|
|
1615
|
+
// rule, so each alt initialises its own node array. Empty alts
|
|
1616
|
+
// are sorted to the end so tabnas's first-match-wins doesn't let
|
|
1617
|
+
// them short-circuit non-empty alternatives.
|
|
1618
|
+
const ordered = [
|
|
1619
|
+
...prod.alts.filter((alt) => alt.length > 0),
|
|
1620
|
+
...prod.alts.filter((alt) => alt.length === 0),
|
|
1621
|
+
];
|
|
1622
|
+
// Ref-only alternatives have no terminal to discriminate on, so
|
|
1623
|
+
// tabnas's first-match-wins would silently let them shadow any
|
|
1624
|
+
// later alternative. Guard them with FIRST-set peeks when the
|
|
1625
|
+
// production has more than one alt.
|
|
1626
|
+
const prodKind = prod.nodeKind ?? 'user';
|
|
1627
|
+
const marks = (prodKind === 'user' && refs.emitMarks)
|
|
1628
|
+
? assignMarks(ordered, literals, regexTokens)
|
|
1629
|
+
: null;
|
|
1630
|
+
const needsPeek = ordered.length > 1;
|
|
1631
|
+
const opens = [];
|
|
1632
|
+
for (const alt of ordered) {
|
|
1633
|
+
const segs = segmentize(alt, literals, regexTokens);
|
|
1634
|
+
const seg = segs[0];
|
|
1635
|
+
const isRefOnly = alt.length >= 1 &&
|
|
1636
|
+
alt.every((el) => el.kind === 'ref') &&
|
|
1637
|
+
seg.terms.length === 0 &&
|
|
1638
|
+
seg.ref != null;
|
|
1639
|
+
const mark = marks ? marks.get(alt) : undefined;
|
|
1640
|
+
if (needsPeek && isRefOnly) {
|
|
1641
|
+
const firstTokens = firstOfAlt(alt, literals, regexTokens, firstSets, nullable);
|
|
1642
|
+
if (firstTokens) {
|
|
1643
|
+
for (const tok of firstTokens) {
|
|
1644
|
+
const o = {
|
|
1645
|
+
s: tok,
|
|
1646
|
+
b: 1,
|
|
1647
|
+
p: seg.ref,
|
|
1648
|
+
...refs.node({ init: true, rule: prod.name, kind: prodKind, nterms: 0 }, (r) => { r.node = mkAstNode(prod.name, prodKind); }),
|
|
1649
|
+
g: tag,
|
|
1650
|
+
};
|
|
1651
|
+
if (mark)
|
|
1652
|
+
o.m = mark;
|
|
1653
|
+
opens.push(o);
|
|
1654
|
+
}
|
|
1655
|
+
continue;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
const o = segmentToAlt(seg, tag, refs, true, prod.name, prodKind);
|
|
1659
|
+
if (mark)
|
|
1660
|
+
o.m = mark;
|
|
1661
|
+
opens.push(o);
|
|
1662
|
+
}
|
|
1663
|
+
const rs = { open: opens };
|
|
1664
|
+
// If any alt has a push, the close state must capture the
|
|
1665
|
+
// returned child. Add a universal fallback close alt whose
|
|
1666
|
+
// action is a no-op when there was no push.
|
|
1667
|
+
if (prod.alts.some((alt) => alt.some((el) => el.kind === 'ref'))) {
|
|
1668
|
+
const close = {
|
|
1669
|
+
...captureChildFields(refs, prod.name, prod.nodeKind ?? 'user'),
|
|
1670
|
+
g: tag,
|
|
1671
|
+
};
|
|
1672
|
+
if (marks)
|
|
1673
|
+
close.m = '_';
|
|
1674
|
+
rs.close = [close];
|
|
1675
|
+
}
|
|
1676
|
+
ruleSpec[prod.name] = rs;
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
if (prod.alts.length === 1) {
|
|
1680
|
+
// Single-alt, multi-segment: chain rules directly on the
|
|
1681
|
+
// production.
|
|
1682
|
+
emitChain(prod.name, prod.alts[0], literals, regexTokens, tag, ruleSpec, refs, prod.nodeKind ?? 'user');
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
// Multi-alt with at least one multi-segment alternative: emit a
|
|
1686
|
+
// dispatcher. Each alt becomes its own chained impl rule
|
|
1687
|
+
// (`<prodname>$alt<i>`); the main rule's open peeks the first token
|
|
1688
|
+
// and pushes the matching impl rule. Using `p:` (not `r:`) keeps
|
|
1689
|
+
// the parent's `child` pointer valid so the parent can read the
|
|
1690
|
+
// impl's node in its close-state action.
|
|
1691
|
+
const dispatchOpen = [];
|
|
1692
|
+
let emptyAltSeen = false;
|
|
1693
|
+
const dispatchMarks = ((prod.nodeKind ?? 'user') === 'user' && refs.emitMarks)
|
|
1694
|
+
? assignMarks(prod.alts, literals, regexTokens)
|
|
1695
|
+
: null;
|
|
1696
|
+
for (let i = 0; i < prod.alts.length; i++) {
|
|
1697
|
+
const alt = prod.alts[i];
|
|
1698
|
+
const implName = `${prod.name}$alt${i}`;
|
|
1699
|
+
const mark = dispatchMarks ? dispatchMarks.get(alt) : undefined;
|
|
1700
|
+
if (alt.length === 0) {
|
|
1701
|
+
// Empty alt acts as fallback — handled after the loop.
|
|
1702
|
+
emptyAltSeen = true;
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1705
|
+
emitChain(implName, alt, literals, regexTokens, tag, ruleSpec, refs, 'helper');
|
|
1706
|
+
// Fan out this alt into one dispatch entry per concrete token
|
|
1707
|
+
// sequence it can start with. Up to LOOKAHEAD_K tokens per
|
|
1708
|
+
// prefix is enough for the grammars this converter targets; a
|
|
1709
|
+
// ref with multiple alts produces one prefix per sub-alt so
|
|
1710
|
+
// overlapping FIRST sets between competing alts can still be
|
|
1711
|
+
// separated by their second (or later) token.
|
|
1712
|
+
// The dispatcher itself is a user (or helper) rule — it must
|
|
1713
|
+
// allocate its own AST node on every dispatch alt, otherwise the
|
|
1714
|
+
// node inherited from the parent via makeRule(ctx, rule.node)
|
|
1715
|
+
// would be shared and the dispatcher's captureChildRef would
|
|
1716
|
+
// mutate the parent's tree.
|
|
1717
|
+
const dispatchKind = prod.nodeKind ?? 'user';
|
|
1718
|
+
const initDispatchFields = refs.node({ init: true, rule: prod.name, kind: dispatchKind, nterms: 0 }, (r) => { r.node = mkAstNode(prod.name, dispatchKind); });
|
|
1719
|
+
const LOOKAHEAD_K = 4;
|
|
1720
|
+
const prefixes = altPrefixes(alt, grammar, literals, regexTokens, LOOKAHEAD_K);
|
|
1721
|
+
const usable = prefixes.filter((p) => p.length > 0);
|
|
1722
|
+
if (usable.length > 0) {
|
|
1723
|
+
for (const p of usable) {
|
|
1724
|
+
const o = {
|
|
1725
|
+
s: p.join(' '),
|
|
1726
|
+
b: p.length,
|
|
1727
|
+
p: implName,
|
|
1728
|
+
...initDispatchFields,
|
|
1729
|
+
g: tag,
|
|
1730
|
+
};
|
|
1731
|
+
if (mark)
|
|
1732
|
+
o.m = mark;
|
|
1733
|
+
dispatchOpen.push(o);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
else {
|
|
1737
|
+
const firstTokens = firstOfAlt(alt, literals, regexTokens, firstSets, nullable);
|
|
1738
|
+
if (firstTokens === null) {
|
|
1739
|
+
throw new Error(`abnf: rule '${prod.name}' alternative ${i} is nullable ` +
|
|
1740
|
+
`but is not the only empty alt; FIRST set is ambiguous`);
|
|
1741
|
+
}
|
|
1742
|
+
for (const tok of firstTokens) {
|
|
1743
|
+
const o = {
|
|
1744
|
+
s: tok, b: 1, p: implName, ...initDispatchFields, g: tag,
|
|
1745
|
+
};
|
|
1746
|
+
if (mark)
|
|
1747
|
+
o.m = mark;
|
|
1748
|
+
dispatchOpen.push(o);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (emptyAltSeen) {
|
|
1753
|
+
// Fallback: matches any token (or none), pops immediately with
|
|
1754
|
+
// an empty tree. Tagged with the user rule name so a consumer
|
|
1755
|
+
// walking the tree still gets a placeholder node for the empty
|
|
1756
|
+
// alternative.
|
|
1757
|
+
const fallbackKind = prod.nodeKind ?? 'user';
|
|
1758
|
+
const o = {
|
|
1759
|
+
...refs.node({ init: true, rule: prod.name, kind: fallbackKind, nterms: 0 }, (r) => { r.node = mkAstNode(prod.name, fallbackKind); }),
|
|
1760
|
+
g: tag,
|
|
1761
|
+
};
|
|
1762
|
+
if (dispatchMarks)
|
|
1763
|
+
o.m = '_';
|
|
1764
|
+
dispatchOpen.push(o);
|
|
1765
|
+
}
|
|
1766
|
+
const dispClose = {
|
|
1767
|
+
// Merge the chosen impl's result up into the dispatcher's node,
|
|
1768
|
+
// tagged with the user rule name (so the enclosing rule sees a
|
|
1769
|
+
// `{rule, src, kids}` child, not the impl chain's transparent
|
|
1770
|
+
// `{src, kids}`).
|
|
1771
|
+
...captureChildFields(refs, prod.name, prod.nodeKind ?? 'user'),
|
|
1772
|
+
g: tag,
|
|
1773
|
+
};
|
|
1774
|
+
if (dispatchMarks)
|
|
1775
|
+
dispClose.m = '_';
|
|
1776
|
+
ruleSpec[prod.name] = { open: dispatchOpen, close: [dispClose] };
|
|
1777
|
+
}
|
|
1778
|
+
// Emit a (possibly single-step) chain of rules for one alt under the
|
|
1779
|
+
// given head rule name. Segment 0 goes into `headName`; later
|
|
1780
|
+
// segments get synthetic `<headName>$stepN` continuations.
|
|
1781
|
+
//
|
|
1782
|
+
// `headKind` controls the head rule's AST node shape: 'user' tags
|
|
1783
|
+
// the head's node with the rule name; 'helper' leaves it untagged
|
|
1784
|
+
// (transparent to the enclosing user rule). Step rules are always
|
|
1785
|
+
// helpers — they inherit and accumulate into the head's node via
|
|
1786
|
+
// `r:` replacement.
|
|
1787
|
+
function emitChain(headName, alt, literals, regexTokens, tag, ruleSpec, refs, headKind = 'helper') {
|
|
1788
|
+
const segs = segmentize(alt, literals, regexTokens);
|
|
1789
|
+
const chainName = (i) => i === 0 ? headName : `${headName}$step${i}`;
|
|
1790
|
+
for (let i = 0; i < segs.length; i++) {
|
|
1791
|
+
const name = chainName(i);
|
|
1792
|
+
const seg = segs[i];
|
|
1793
|
+
const kind = i === 0 ? headKind : 'helper';
|
|
1794
|
+
// Only the head of the chain initialises the node object; later
|
|
1795
|
+
// steps inherit and continue to accumulate into it via `r:`.
|
|
1796
|
+
const headAlt = segmentToAlt(seg, tag, refs, i === 0, name, kind);
|
|
1797
|
+
// Single-alt user rule: the head alt is user-addressable.
|
|
1798
|
+
if (i === 0 && headKind === 'user' && refs.emitMarks) {
|
|
1799
|
+
headAlt.m = altDiscriminator(alt, literals, regexTokens);
|
|
1800
|
+
}
|
|
1801
|
+
const open = [headAlt];
|
|
1802
|
+
const rs = { open };
|
|
1803
|
+
const isLast = i === segs.length - 1;
|
|
1804
|
+
if (!isLast) {
|
|
1805
|
+
// Non-last step: after the push returns, capture the child's
|
|
1806
|
+
// node and replace with the next step rule.
|
|
1807
|
+
rs.close = [{
|
|
1808
|
+
r: chainName(i + 1),
|
|
1809
|
+
...captureChildFields(refs, name, kind),
|
|
1810
|
+
g: tag,
|
|
1811
|
+
}];
|
|
1812
|
+
}
|
|
1813
|
+
else if (seg.ref) {
|
|
1814
|
+
// Last step, but it had a push — we still need to capture the
|
|
1815
|
+
// final child before popping.
|
|
1816
|
+
rs.close = [{ ...captureChildFields(refs, name, kind), g: tag }];
|
|
1817
|
+
}
|
|
1818
|
+
ruleSpec[name] = rs;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
// Compute FIRST(ref) for every production, plus which productions
|
|
1822
|
+
// are nullable (can derive the empty string). Iterates to a fixed
|
|
1823
|
+
// point. Terminals in FIRST sets are represented by their allocated
|
|
1824
|
+
// token names (e.g. `#X`).
|
|
1825
|
+
function computeFirstSets(grammar, literals, regexTokens) {
|
|
1826
|
+
const firstSets = new Map();
|
|
1827
|
+
const nullable = new Set();
|
|
1828
|
+
for (const p of grammar.productions)
|
|
1829
|
+
firstSets.set(p.name, new Set());
|
|
1830
|
+
let changed = true;
|
|
1831
|
+
while (changed) {
|
|
1832
|
+
changed = false;
|
|
1833
|
+
for (const prod of grammar.productions) {
|
|
1834
|
+
const first = firstSets.get(prod.name);
|
|
1835
|
+
for (const alt of prod.alts) {
|
|
1836
|
+
// Walk the alt, accumulating FIRST until a non-nullable
|
|
1837
|
+
// position is hit.
|
|
1838
|
+
let altNullable = true;
|
|
1839
|
+
for (const el of alt) {
|
|
1840
|
+
if (el.kind === 'term' || el.kind === 'regex') {
|
|
1841
|
+
const tok = el.kind === 'term'
|
|
1842
|
+
? literals.get(termKey(el))
|
|
1843
|
+
: regexTokens.get(regexKey(el));
|
|
1844
|
+
if (!first.has(tok)) {
|
|
1845
|
+
first.add(tok);
|
|
1846
|
+
changed = true;
|
|
1847
|
+
}
|
|
1848
|
+
altNullable = false;
|
|
1849
|
+
break;
|
|
1850
|
+
}
|
|
1851
|
+
if (el.kind === 'ref') {
|
|
1852
|
+
const refFirst = firstSets.get(el.name) ?? new Set();
|
|
1853
|
+
for (const tok of refFirst) {
|
|
1854
|
+
if (!first.has(tok)) {
|
|
1855
|
+
first.add(tok);
|
|
1856
|
+
changed = true;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
if (!nullable.has(el.name)) {
|
|
1860
|
+
altNullable = false;
|
|
1861
|
+
break;
|
|
1862
|
+
}
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
// Desugar should have eliminated other kinds.
|
|
1866
|
+
throw new Error(`abnf: internal — unexpected kind in FIRST: ${el.kind}`);
|
|
1867
|
+
}
|
|
1868
|
+
if (altNullable && !nullable.has(prod.name)) {
|
|
1869
|
+
nullable.add(prod.name);
|
|
1870
|
+
changed = true;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
return { firstSets, nullable };
|
|
1876
|
+
}
|
|
1877
|
+
// FIRST set for a specific alternative (not the whole production).
|
|
1878
|
+
// Returns null if the alt is nullable — the caller must treat that
|
|
1879
|
+
// case separately (typically as a fallback empty alt).
|
|
1880
|
+
function firstOfAlt(alt, literals, regexTokens, firstSets, nullable) {
|
|
1881
|
+
const out = new Set();
|
|
1882
|
+
for (const el of alt) {
|
|
1883
|
+
if (el.kind === 'term' || el.kind === 'regex') {
|
|
1884
|
+
const tok = el.kind === 'term'
|
|
1885
|
+
? literals.get(termKey(el))
|
|
1886
|
+
: regexTokens.get(regexKey(el));
|
|
1887
|
+
out.add(tok);
|
|
1888
|
+
return out;
|
|
1889
|
+
}
|
|
1890
|
+
if (el.kind === 'ref') {
|
|
1891
|
+
const rf = firstSets.get(el.name) ?? new Set();
|
|
1892
|
+
for (const tok of rf)
|
|
1893
|
+
out.add(tok);
|
|
1894
|
+
if (!nullable.has(el.name))
|
|
1895
|
+
return out;
|
|
1896
|
+
// else keep walking into the next element
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1899
|
+
throw new Error(`abnf: internal — unexpected kind in firstOfAlt: ${el.kind}`);
|
|
1900
|
+
}
|
|
1901
|
+
// Alt is nullable — no non-empty prefix.
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
// Longest deterministic terminal prefix of a rule — the longest
|
|
1905
|
+
// sequence of tokens that every alternative of the rule starts
|
|
1906
|
+
// with. Refs are followed into their target rule, with a `visited`
|
|
1907
|
+
// set guarding cycles. An empty array means there's no confident
|
|
1908
|
+
// prefix (the rule either has divergent alts, starts with a multi-
|
|
1909
|
+
// alt ref, or hits a cycle), so the caller should fall back to a
|
|
1910
|
+
// single-token FIRST-set lookahead instead.
|
|
1911
|
+
function ruleLiteralPrefix(name, grammar, literals, regexTokens, visited) {
|
|
1912
|
+
if (visited.has(name))
|
|
1913
|
+
return [];
|
|
1914
|
+
const next = new Set(visited);
|
|
1915
|
+
next.add(name);
|
|
1916
|
+
const prod = grammar.productions.find((p) => p.name === name);
|
|
1917
|
+
if (!prod || prod.alts.length === 0)
|
|
1918
|
+
return [];
|
|
1919
|
+
const prefixes = prod.alts.map((alt) => altLiteralPrefix(alt, grammar, literals, regexTokens, next));
|
|
1920
|
+
if (prefixes.some((p) => p.length === 0))
|
|
1921
|
+
return [];
|
|
1922
|
+
const minLen = Math.min(...prefixes.map((p) => p.length));
|
|
1923
|
+
const common = [];
|
|
1924
|
+
for (let i = 0; i < minLen; i++) {
|
|
1925
|
+
const tok = prefixes[0][i];
|
|
1926
|
+
if (prefixes.every((p) => p[i] === tok))
|
|
1927
|
+
common.push(tok);
|
|
1928
|
+
else
|
|
1929
|
+
break;
|
|
1930
|
+
}
|
|
1931
|
+
return common;
|
|
1932
|
+
}
|
|
1933
|
+
function altLiteralPrefix(alt, grammar, literals, regexTokens, visited) {
|
|
1934
|
+
const out = [];
|
|
1935
|
+
for (const el of alt) {
|
|
1936
|
+
if (el.kind === 'term') {
|
|
1937
|
+
out.push(literals.get(termKey(el)));
|
|
1938
|
+
}
|
|
1939
|
+
else if (el.kind === 'regex') {
|
|
1940
|
+
out.push(regexTokens.get(regexKey(el)));
|
|
1941
|
+
}
|
|
1942
|
+
else if (el.kind === 'ref') {
|
|
1943
|
+
const sub = ruleLiteralPrefix(el.name, grammar, literals, regexTokens, visited);
|
|
1944
|
+
// Take the ref's literal prefix and stop — we can't see past
|
|
1945
|
+
// the ref without more expensive analysis.
|
|
1946
|
+
out.push(...sub);
|
|
1947
|
+
return out;
|
|
1948
|
+
}
|
|
1949
|
+
else {
|
|
1950
|
+
return out;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
return out;
|
|
1954
|
+
}
|
|
1955
|
+
// Enumerate concrete token-sequence prefixes an alternative can
|
|
1956
|
+
// start with, each at most `maxK` tokens long. Refs with multiple
|
|
1957
|
+
// alternatives fan out into one prefix per sub-alternative so the
|
|
1958
|
+
// caller can emit a dedicated dispatch alt for each path. When a
|
|
1959
|
+
// ref cycles back or exhausts depth, the path is *terminated* at
|
|
1960
|
+
// the tokens accumulated so far — the `done` flag is propagated
|
|
1961
|
+
// out of nested calls so a truncated sub-prefix is never extended
|
|
1962
|
+
// with tokens from elements the outer alt happens to list after the
|
|
1963
|
+
// cycled ref.
|
|
1964
|
+
function altPrefixesRaw(alt, grammar, literals, regexTokens, maxK, visited = new Set()) {
|
|
1965
|
+
let paths = [{ tokens: [], done: false }];
|
|
1966
|
+
for (const el of alt) {
|
|
1967
|
+
const next = [];
|
|
1968
|
+
for (const p of paths) {
|
|
1969
|
+
if (p.done || p.tokens.length >= maxK) {
|
|
1970
|
+
next.push(p);
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
if (el.kind === 'term') {
|
|
1974
|
+
next.push({
|
|
1975
|
+
tokens: [...p.tokens, literals.get(termKey(el))],
|
|
1976
|
+
done: false,
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
else if (el.kind === 'regex') {
|
|
1980
|
+
next.push({
|
|
1981
|
+
tokens: [...p.tokens, regexTokens.get(regexKey(el))],
|
|
1982
|
+
done: false,
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
else if (el.kind === 'ref') {
|
|
1986
|
+
if (visited.has(el.name)) {
|
|
1987
|
+
next.push({ tokens: p.tokens, done: true });
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
const childVisited = new Set(visited);
|
|
1991
|
+
childVisited.add(el.name);
|
|
1992
|
+
const target = grammar.productions.find((pr) => pr.name === el.name);
|
|
1993
|
+
if (!target || target.alts.length === 0) {
|
|
1994
|
+
next.push({ tokens: p.tokens, done: true });
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1997
|
+
for (const sub of target.alts) {
|
|
1998
|
+
const subPaths = altPrefixesRaw(sub, grammar, literals, regexTokens, maxK - p.tokens.length, childVisited);
|
|
1999
|
+
for (const sp of subPaths) {
|
|
2000
|
+
next.push({
|
|
2001
|
+
tokens: [...p.tokens, ...sp.tokens],
|
|
2002
|
+
// Propagate `done` so the outer loop won't extend a
|
|
2003
|
+
// cycle-truncated sub-prefix.
|
|
2004
|
+
done: sp.done,
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
else {
|
|
2010
|
+
// Desugar should have eliminated group/star/etc. at this point.
|
|
2011
|
+
next.push({ tokens: p.tokens, done: true });
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
paths = next;
|
|
2015
|
+
if (paths.every((p) => p.done || p.tokens.length >= maxK))
|
|
2016
|
+
break;
|
|
2017
|
+
}
|
|
2018
|
+
return paths;
|
|
2019
|
+
}
|
|
2020
|
+
function altPrefixes(alt, grammar, literals, regexTokens, maxK) {
|
|
2021
|
+
const raw = altPrefixesRaw(alt, grammar, literals, regexTokens, maxK);
|
|
2022
|
+
const seen = new Set();
|
|
2023
|
+
const out = [];
|
|
2024
|
+
for (const p of raw) {
|
|
2025
|
+
const key = p.tokens.join(' ');
|
|
2026
|
+
if (!seen.has(key)) {
|
|
2027
|
+
seen.add(key);
|
|
2028
|
+
out.push(p.tokens);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
return out;
|
|
2032
|
+
}
|
|
2033
|
+
// A quoted-string literal is effectively case-sensitive either
|
|
2034
|
+
// when the user explicitly wrote `%s"…"` or when it contains no
|
|
2035
|
+
// ASCII letters (there's nothing to fold — `"+"` matches `+` in
|
|
2036
|
+
// any "case").
|
|
2037
|
+
function isEffectivelyCaseSensitive(el) {
|
|
2038
|
+
if (el.caseSensitive === true)
|
|
2039
|
+
return true;
|
|
2040
|
+
return !/[A-Za-z]/.test(el.literal);
|
|
2041
|
+
}
|
|
2042
|
+
// Map a term element to the key used to look up (or allocate) its
|
|
2043
|
+
// emitted token. The key folds together the literal and its
|
|
2044
|
+
// effective case-sensitivity so a sensitive and an insensitive
|
|
2045
|
+
// occurrence of the same string are distinct tokens.
|
|
2046
|
+
function termKey(el) {
|
|
2047
|
+
return (isEffectivelyCaseSensitive(el) ? 'cs:' : 'ci:') + el.literal;
|
|
2048
|
+
}
|
|
2049
|
+
function escapeRegExp(s) {
|
|
2050
|
+
return s.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
|
|
2051
|
+
}
|
|
2052
|
+
// Decode an ABNF numeric value (`%xNN`, `%dNN`, `%bNN`, or one of
|
|
2053
|
+
// the range/concatenation forms) into a `AbnfElement`.
|
|
2054
|
+
//
|
|
2055
|
+
// %x61 => single-char term "a"
|
|
2056
|
+
// %x66.6f.6f => concatenated term "foo"
|
|
2057
|
+
// %x30-39 => regex character class [\u0030-\u0039]
|
|
2058
|
+
//
|
|
2059
|
+
// Hex is case-insensitive; decimal and binary accept only digits
|
|
2060
|
+
// in their respective ranges. Range endpoints must be the same
|
|
2061
|
+
// base as the prefix (RFC 5234 doesn't allow mixing).
|
|
2062
|
+
function parseNumericValue(src) {
|
|
2063
|
+
const base = src[1].toLowerCase();
|
|
2064
|
+
const radix = base === 'x' ? 16 : base === 'd' ? 10 : 2;
|
|
2065
|
+
const body = src.slice(2);
|
|
2066
|
+
if (body.includes('-')) {
|
|
2067
|
+
const [loStr, hiStr] = body.split('-');
|
|
2068
|
+
const lo = parseInt(loStr, radix);
|
|
2069
|
+
const hi = parseInt(hiStr, radix);
|
|
2070
|
+
if (lo === hi) {
|
|
2071
|
+
return { kind: 'term', literal: String.fromCharCode(lo) };
|
|
2072
|
+
}
|
|
2073
|
+
const toEsc = (n) => '\\u' + n.toString(16).padStart(4, '0');
|
|
2074
|
+
return {
|
|
2075
|
+
kind: 'regex',
|
|
2076
|
+
pattern: '[' + toEsc(lo) + '-' + toEsc(hi) + ']',
|
|
2077
|
+
flags: '',
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
const parts = body.split('.');
|
|
2081
|
+
const chars = parts.map((n) => String.fromCharCode(parseInt(n, radix)));
|
|
2082
|
+
return { kind: 'term', literal: chars.join('') };
|
|
2083
|
+
}
|
|
2084
|
+
function allocTokenName(literal, used) {
|
|
2085
|
+
const base = literal
|
|
2086
|
+
.replace(/[^A-Za-z0-9]/g, '_')
|
|
2087
|
+
.toUpperCase()
|
|
2088
|
+
.replace(/^_+|_+$/g, '');
|
|
2089
|
+
const candidate = base.length > 0 ? '#' + base : '#T';
|
|
2090
|
+
if (!used.has(candidate)) {
|
|
2091
|
+
used.add(candidate);
|
|
2092
|
+
return candidate;
|
|
2093
|
+
}
|
|
2094
|
+
let i = 1;
|
|
2095
|
+
while (used.has(candidate + i))
|
|
2096
|
+
i++;
|
|
2097
|
+
const chosen = candidate + i;
|
|
2098
|
+
used.add(chosen);
|
|
2099
|
+
return chosen;
|
|
2100
|
+
}
|
|
2101
|
+
// Public entry point: take ABNF source and return a tabnas GrammarSpec.
|
|
2102
|
+
function abnf(src, opts) {
|
|
2103
|
+
const grammar = parseAbnf(src);
|
|
2104
|
+
return emitGrammarSpec(grammar, opts);
|
|
2105
|
+
}
|
|
2106
|
+
//# sourceMappingURL=converter.js.map
|