parseman 0.50.1 → 0.50.4
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/README.md +14 -10
- package/dist/analysis/diagnostics.js +455 -229
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +445 -194
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/choice.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/grammar.d.ts.map +1 -1
- package/dist/combinators/keywords.d.ts.map +1 -1
- package/dist/combinators/literal.d.ts.map +1 -1
- package/dist/combinators/map.d.ts.map +1 -1
- package/dist/combinators/ref.d.ts.map +1 -1
- package/dist/combinators/regex.d.ts.map +1 -1
- package/dist/combinators/repeat.d.ts.map +1 -1
- package/dist/combinators/scalar.d.ts +6 -0
- package/dist/combinators/scalar.d.ts.map +1 -0
- package/dist/combinators/sequence.d.ts.map +1 -1
- package/dist/combinators/trivia-skip.d.ts +2 -0
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +22 -11
- package/dist/compiler/token-capability.js.map +2 -2
- package/dist/index.js +446 -195
- package/dist/index.js.map +3 -3
- package/dist/language-service/index.js +68 -172
- package/dist/language-service/index.js.map +3 -3
- package/dist/parse-context.d.ts.map +1 -1
- package/dist/plugin/index.js +491 -309
- package/dist/plugin/index.js.map +3 -3
- package/dist/run/index.js +1 -0
- package/dist/run/index.js.map +1 -1
- package/dist/spec/index.js +421 -285
- package/dist/spec/index.js.map +3 -3
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/index.js +331 -191
- package/dist/table/index.js.map +3 -3
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +5 -3
- package/src/combinators/choice.ts +34 -1
- package/src/combinators/first-set.ts +33 -7
- package/src/combinators/grammar.ts +39 -0
- package/src/combinators/keywords.ts +14 -3
- package/src/combinators/literal.ts +26 -0
- package/src/combinators/map.ts +60 -0
- package/src/combinators/ref.ts +26 -7
- package/src/combinators/regex.ts +23 -0
- package/src/combinators/repeat.ts +109 -16
- package/src/combinators/scalar.ts +30 -0
- package/src/combinators/sequence.ts +76 -161
- package/src/combinators/trivia-skip.ts +14 -1
- package/src/parse-context.ts +1 -0
- package/src/table/emit-assembly.ts +39 -16
- package/src/table/encode.ts +14 -2
- package/src/types.ts +8 -0
- package/src/version.ts +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
|
|
2
|
-
import { advanceTrivia, commitTriviaScan, needsDeferredTriviaCommit, rollbackTrivia, saveTriviaMark, scanTriviaCompact } from './trivia-skip.ts'
|
|
1
|
+
import type { Combinator, ParseContext, ParseOk, ParseResult, ParserMeta } from '../types.ts'
|
|
2
|
+
import { advanceTrivia, commitTriviaScan, needsDeferredTriviaCommit, needsTriviaRollback, rollbackTrivia, saveTriviaMark, scanTriviaCompact } from './trivia-skip.ts'
|
|
3
3
|
import { matchesEmpty, startsFirstSet } from './first-set.ts'
|
|
4
4
|
import { deriveExpected } from './expect.ts'
|
|
5
5
|
import { matchesAt, orSentinel, recoverScan, captureError } from '../recovery/scan.ts'
|
|
6
6
|
import { demoteCapturedToRaw } from '../cst/capture-buffer.ts'
|
|
7
|
+
import { scalarOf, type ScalarParser } from './scalar.ts'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Parse one repetition item at `cur`, first skipping (and, in capture mode,
|
|
@@ -13,7 +14,7 @@ import { demoteCapturedToRaw } from '../cst/capture-buffer.ts'
|
|
|
13
14
|
* if the item then fails or makes no progress the trivia is rolled back and the
|
|
14
15
|
* loop stops (the trivia is trailing and belongs to the enclosing context).
|
|
15
16
|
*
|
|
16
|
-
* Returns the
|
|
17
|
+
* Returns the successful child result, the underlying failure (so oneOrMore
|
|
17
18
|
* can propagate a first-item failure), or 'stop'.
|
|
18
19
|
*
|
|
19
20
|
* `mandatory` marks an item `min` REQUIRES rather than one the greedy loop is
|
|
@@ -31,8 +32,9 @@ function repItem<T>(
|
|
|
31
32
|
ctx: ParseContext,
|
|
32
33
|
guardable: boolean,
|
|
33
34
|
mandatory: boolean,
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
rollbackNeeded: boolean,
|
|
36
|
+
): ParseOk<T> | { fail: ParseResult<T>; failPos: number } | 'stop' {
|
|
37
|
+
const mark = rollbackNeeded ? saveTriviaMark(ctx) : undefined
|
|
36
38
|
let pos = cur
|
|
37
39
|
if (ctx.trivia) {
|
|
38
40
|
if (needsDeferredTriviaCommit(ctx)) {
|
|
@@ -47,7 +49,7 @@ function repItem<T>(
|
|
|
47
49
|
// is the operative word: a `mandatory` item is not speculative, so it is
|
|
48
50
|
// attempted at EOF like the first item is, and a NULLABLE one matches there.
|
|
49
51
|
if (pos >= input.length && !mandatory) {
|
|
50
|
-
rollbackTrivia(ctx, mark)
|
|
52
|
+
if (mark !== undefined) rollbackTrivia(ctx, mark)
|
|
51
53
|
return 'stop'
|
|
52
54
|
}
|
|
53
55
|
// First-set fast-path (mirrors emitMany's codegen guard): a body that can't match
|
|
@@ -58,12 +60,12 @@ function repItem<T>(
|
|
|
58
60
|
// probe or tolerant recovery, where a swallowed failure still feeds the probe /
|
|
59
61
|
// triggers resync (matching the codegen guard's `!ctx.recovery` gate).
|
|
60
62
|
if (guardable && ctx._probe === undefined && !ctx._tolerant && !startsFirstSet(combinator, input, pos)) {
|
|
61
|
-
rollbackTrivia(ctx, mark)
|
|
63
|
+
if (mark !== undefined) rollbackTrivia(ctx, mark)
|
|
62
64
|
return 'stop'
|
|
63
65
|
}
|
|
64
66
|
const result = combinator.parse(input, pos, ctx)
|
|
65
67
|
if (!result.ok) {
|
|
66
|
-
rollbackTrivia(ctx, mark)
|
|
68
|
+
if (mark !== undefined) rollbackTrivia(ctx, mark)
|
|
67
69
|
if (result.committed) return { fail: result, failPos: pos }
|
|
68
70
|
// Surface the POST-trivia position where the element actually failed. The
|
|
69
71
|
// tolerant recovery guard must check the sync token there — not at `cur`,
|
|
@@ -78,10 +80,10 @@ function repItem<T>(
|
|
|
78
80
|
// string with any number of them, so a `{ min: n }` prefix takes the n-item
|
|
79
81
|
// derivation. It cannot spin — the prefix is exactly `n` long.
|
|
80
82
|
if (result.span.end === pos && !mandatory) {
|
|
81
|
-
rollbackTrivia(ctx, mark)
|
|
83
|
+
if (mark !== undefined) rollbackTrivia(ctx, mark)
|
|
82
84
|
return 'stop'
|
|
83
85
|
}
|
|
84
|
-
return
|
|
86
|
+
return result
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
export type RepeatOptions = {
|
|
@@ -110,6 +112,25 @@ function resolveBounds(what: string, opts: { min?: number; max?: number }): { mi
|
|
|
110
112
|
return { min, max }
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
function repeatScalarTail<T>(
|
|
116
|
+
combinator: Combinator<T>, child: ScalarParser, input: string, cur: number,
|
|
117
|
+
ctx: ParseContext, guardable: boolean, values: T[] | undefined, remaining: number,
|
|
118
|
+
): number {
|
|
119
|
+
while (cur < input.length && remaining > 0) {
|
|
120
|
+
let itemPos = cur
|
|
121
|
+
if (ctx.trivia) itemPos = advanceTrivia(input, cur, ctx)
|
|
122
|
+
if (itemPos >= input.length) break
|
|
123
|
+
if (guardable && !startsFirstSet(combinator, input, itemPos)) break
|
|
124
|
+
const end = child(input, itemPos, ctx)
|
|
125
|
+
if (end <= itemPos) break
|
|
126
|
+
if (values !== undefined) values.push(ctx._sv as T)
|
|
127
|
+
cur = end
|
|
128
|
+
remaining--
|
|
129
|
+
}
|
|
130
|
+
ctx._sv = values
|
|
131
|
+
return cur
|
|
132
|
+
}
|
|
133
|
+
|
|
113
134
|
/**
|
|
114
135
|
* Repetition: `item*` by default, bounded by `{ min, max }` (both count ITEMS).
|
|
115
136
|
*
|
|
@@ -137,20 +158,25 @@ export function many<T>(combinator: Combinator<T>, opts: RepeatOptions = {}): Co
|
|
|
137
158
|
let expected: string[] | undefined
|
|
138
159
|
// A non-nullable body can be first-set-gated per loop iteration (see repItem).
|
|
139
160
|
const guardable = combinator._meta.firstSet.kind !== 'any' && !matchesEmpty(combinator)
|
|
161
|
+
const scalarChild = scalarOf(combinator)
|
|
162
|
+
const parseScalar = (input: string, pos: number, ctx: ParseContext): number =>
|
|
163
|
+
repeatScalarTail(combinator, scalarChild, input, pos, ctx, guardable, def.valueUnused ? undefined : [], max)
|
|
140
164
|
|
|
141
165
|
return {
|
|
142
166
|
_tag: 'many',
|
|
143
167
|
_meta: meta,
|
|
144
168
|
_def: def,
|
|
169
|
+
_parseScalar: parseScalar,
|
|
145
170
|
parse(input: string, pos: number, ctx: ParseContext): ParseResult<T[]> {
|
|
146
171
|
// When the aggregate is never observed (markUnusedValues), skip the array:
|
|
147
172
|
// items still parse and self-capture into the enclosing node's children.
|
|
148
173
|
const values: T[] | undefined = def.valueUnused ? undefined : []
|
|
174
|
+
const rollbackNeeded = needsTriviaRollback(ctx)
|
|
149
175
|
let cur = pos
|
|
150
176
|
let count = 0
|
|
151
177
|
while (cur < input.length) {
|
|
152
178
|
if (count >= max) break
|
|
153
|
-
const item = repItem(combinator, input, cur, ctx, guardable, false)
|
|
179
|
+
const item = repItem(combinator, input, cur, ctx, guardable, false, rollbackNeeded)
|
|
154
180
|
if (item === 'stop') break
|
|
155
181
|
if ('fail' in item) {
|
|
156
182
|
if (!item.fail.ok && item.fail.committed) return item.fail
|
|
@@ -175,7 +201,7 @@ export function many<T>(combinator: Combinator<T>, opts: RepeatOptions = {}): Co
|
|
|
175
201
|
}
|
|
176
202
|
if (values !== undefined) values.push(item.value)
|
|
177
203
|
count++
|
|
178
|
-
cur = item.end
|
|
204
|
+
cur = item.span.end
|
|
179
205
|
}
|
|
180
206
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
181
207
|
return { ok: true, value: (values ?? undefined) as T[], span: { start: pos, end: cur } }
|
|
@@ -207,11 +233,27 @@ function atLeast<T>(combinator: Combinator<T>, min: number, max: number): Combin
|
|
|
207
233
|
let expected: string[] | undefined
|
|
208
234
|
// A non-nullable body can be first-set-gated per loop iteration (see repItem).
|
|
209
235
|
const guardable = combinator._meta.firstSet.kind !== 'any' && !matchesEmpty(combinator)
|
|
236
|
+
const scalarChild = scalarOf(combinator)
|
|
237
|
+
const parseScalar = (input: string, pos: number, ctx: ParseContext): number => {
|
|
238
|
+
let cur = scalarChild(input, pos, ctx)
|
|
239
|
+
if (cur < 0) return cur
|
|
240
|
+
const values: T[] | undefined = def.valueUnused ? undefined : [ctx._sv as T]
|
|
241
|
+
for (let count = 1; count < min; count++) {
|
|
242
|
+
let itemPos = cur
|
|
243
|
+
if (ctx.trivia) itemPos = advanceTrivia(input, cur, ctx)
|
|
244
|
+
const end = scalarChild(input, itemPos, ctx)
|
|
245
|
+
if (end < 0) return ~cur
|
|
246
|
+
cur = end
|
|
247
|
+
if (values !== undefined) values.push(ctx._sv as T)
|
|
248
|
+
}
|
|
249
|
+
return repeatScalarTail(combinator, scalarChild, input, cur, ctx, guardable, values, max - min)
|
|
250
|
+
}
|
|
210
251
|
|
|
211
252
|
return {
|
|
212
253
|
_tag: 'oneOrMore',
|
|
213
254
|
_meta: meta,
|
|
214
255
|
_def: def,
|
|
256
|
+
_parseScalar: parseScalar,
|
|
215
257
|
parse(input: string, pos: number, ctx: ParseContext): ParseResult<T[]> {
|
|
216
258
|
// First item is mandatory (parsed at pos directly — leading trivia is the
|
|
217
259
|
// enclosing context's responsibility); subsequent items skip leading trivia.
|
|
@@ -219,6 +261,7 @@ function atLeast<T>(combinator: Combinator<T>, min: number, max: number): Combin
|
|
|
219
261
|
if (!first.ok) return first
|
|
220
262
|
// Aggregate skipped when never observed (see `many`).
|
|
221
263
|
const values: T[] | undefined = def.valueUnused ? undefined : [first.value]
|
|
264
|
+
const rollbackNeeded = needsTriviaRollback(ctx)
|
|
222
265
|
let cur = first.span.end
|
|
223
266
|
let count = 1
|
|
224
267
|
// Mandatory items 2..min (only entered for min > 1) — each failure propagates,
|
|
@@ -227,7 +270,7 @@ function atLeast<T>(combinator: Combinator<T>, min: number, max: number): Combin
|
|
|
227
270
|
// two loop-termination stops: a required item is attempted at its position
|
|
228
271
|
// whatever is there, and counts whatever its width.
|
|
229
272
|
while (count < min) {
|
|
230
|
-
const item = repItem(combinator, input, cur, ctx, guardable, true)
|
|
273
|
+
const item = repItem(combinator, input, cur, ctx, guardable, true, rollbackNeeded)
|
|
231
274
|
if (item === 'stop' || 'fail' in item) {
|
|
232
275
|
if (item !== 'stop' && !item.fail.ok && item.fail.committed) return item.fail
|
|
233
276
|
// Anchored at `cur` — the furthest position the repeat reached — not at
|
|
@@ -239,12 +282,12 @@ function atLeast<T>(combinator: Combinator<T>, min: number, max: number): Combin
|
|
|
239
282
|
return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: cur, end: cur } }
|
|
240
283
|
}
|
|
241
284
|
if (values !== undefined) values.push(item.value)
|
|
242
|
-
cur = item.end
|
|
285
|
+
cur = item.span.end
|
|
243
286
|
count++
|
|
244
287
|
}
|
|
245
288
|
while (cur < input.length) {
|
|
246
289
|
if (count >= max) break
|
|
247
|
-
const item = repItem(combinator, input, cur, ctx, guardable, false)
|
|
290
|
+
const item = repItem(combinator, input, cur, ctx, guardable, false, rollbackNeeded)
|
|
248
291
|
if (item === 'stop') break
|
|
249
292
|
if ('fail' in item) {
|
|
250
293
|
if (!item.fail.ok && item.fail.committed) return item.fail
|
|
@@ -262,7 +305,7 @@ function atLeast<T>(combinator: Combinator<T>, min: number, max: number): Combin
|
|
|
262
305
|
}
|
|
263
306
|
if (values !== undefined) values.push(item.value)
|
|
264
307
|
count++
|
|
265
|
-
cur = item.end
|
|
308
|
+
cur = item.span.end
|
|
266
309
|
}
|
|
267
310
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
268
311
|
return { ok: true, value: (values ?? undefined) as T[], span: { start: pos, end: cur } }
|
|
@@ -277,11 +320,23 @@ export function optional<T>(combinator: Combinator<T>): Combinator<T | null> {
|
|
|
277
320
|
isTrivia: false,
|
|
278
321
|
}
|
|
279
322
|
const firstSetSkippable = !matchesEmpty(combinator)
|
|
323
|
+
const scalarChild = scalarOf(combinator)
|
|
324
|
+
const parseScalar = (input: string, pos: number, ctx: ParseContext): number => {
|
|
325
|
+
if (firstSetSkippable && !startsFirstSet(combinator, input, pos)) {
|
|
326
|
+
ctx._sv = null
|
|
327
|
+
return pos
|
|
328
|
+
}
|
|
329
|
+
const end = scalarChild(input, pos, ctx)
|
|
330
|
+
if (end >= 0) return end
|
|
331
|
+
ctx._sv = null
|
|
332
|
+
return pos
|
|
333
|
+
}
|
|
280
334
|
|
|
281
335
|
return {
|
|
282
336
|
_tag: 'optional',
|
|
283
337
|
_meta: meta,
|
|
284
338
|
_def: { tag: 'optional', parser: combinator as Combinator<unknown> },
|
|
339
|
+
_parseScalar: parseScalar,
|
|
285
340
|
parse(input: string, pos: number, ctx: ParseContext): ParseResult<T | null> {
|
|
286
341
|
if (firstSetSkippable && ctx._probe === undefined && !startsFirstSet(combinator, input, pos)) {
|
|
287
342
|
return { ok: true, value: null, span: { start: pos, end: pos } }
|
|
@@ -412,6 +467,43 @@ export function sepBy<T, S>(
|
|
|
412
467
|
expected ??= deriveExpected(combinator)
|
|
413
468
|
return { ok: false, expected: expected.length > 0 ? expected : [combinator._tag], span: { start: at, end: at } }
|
|
414
469
|
}
|
|
470
|
+
const scalarExpected = deriveExpected(combinator)
|
|
471
|
+
const scalarChild = scalarOf(combinator)
|
|
472
|
+
const scalarSeparator = scalarOf(separator)
|
|
473
|
+
const parseScalar = (input: string, pos: number, ctx: ParseContext): number => {
|
|
474
|
+
let cur = scalarChild(input, pos, ctx)
|
|
475
|
+
if (cur < 0) {
|
|
476
|
+
if (min >= 1) {
|
|
477
|
+
ctx._fx = scalarExpected.length > 0 ? scalarExpected : [combinator._tag]
|
|
478
|
+
return ~pos
|
|
479
|
+
}
|
|
480
|
+
ctx._sv = []
|
|
481
|
+
return pos
|
|
482
|
+
}
|
|
483
|
+
const values = [ctx._sv as T]
|
|
484
|
+
while (values.length < max && (cur < input.length || values.length < min)) {
|
|
485
|
+
const beforeSeparator = cur
|
|
486
|
+
let separatorPos = cur
|
|
487
|
+
if (ctx.trivia) separatorPos = advanceTrivia(input, cur, ctx)
|
|
488
|
+
const separatorEnd = scalarSeparator(input, separatorPos, ctx)
|
|
489
|
+
if (separatorEnd < 0) break
|
|
490
|
+
let itemPos = separatorEnd
|
|
491
|
+
if (ctx.trivia) itemPos = advanceTrivia(input, itemPos, ctx)
|
|
492
|
+
const itemEnd = scalarChild(input, itemPos, ctx)
|
|
493
|
+
if (itemEnd < 0) {
|
|
494
|
+
cur = trailing === 'allow' ? separatorEnd : beforeSeparator
|
|
495
|
+
break
|
|
496
|
+
}
|
|
497
|
+
values.push(ctx._sv as T)
|
|
498
|
+
cur = itemEnd
|
|
499
|
+
}
|
|
500
|
+
if (values.length < min) {
|
|
501
|
+
ctx._fx = scalarExpected.length > 0 ? scalarExpected : [combinator._tag]
|
|
502
|
+
return ~cur
|
|
503
|
+
}
|
|
504
|
+
ctx._sv = values
|
|
505
|
+
return cur
|
|
506
|
+
}
|
|
415
507
|
|
|
416
508
|
return {
|
|
417
509
|
_tag: 'sepBy',
|
|
@@ -425,6 +517,7 @@ export function sepBy<T, S>(
|
|
|
425
517
|
// this option surface already; parity is held by test/parity, not by hope.
|
|
426
518
|
...(keepSeparators ? { keepSeparators: true } : {}),
|
|
427
519
|
},
|
|
520
|
+
_parseScalar: keepSeparators ? undefined : parseScalar,
|
|
428
521
|
parse(input: string, pos: number, ctx: ParseContext): ParseResult<T[]> {
|
|
429
522
|
const first = combinator.parse(input, pos, ctx)
|
|
430
523
|
const values: T[] = []
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Combinator, ParseContext } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
export type ScalarParser = (input: string, pos: number, ctx: ParseContext) => number
|
|
4
|
+
|
|
5
|
+
const SCALAR_TAG = /^(?:literal|regex|keywords|sequence|choice|many|oneOrMore|optional|transform|trivia|grammar|sepBy|lazy)$/
|
|
6
|
+
|
|
7
|
+
export function scalarOf<T>(combinator: Combinator<T>): ScalarParser {
|
|
8
|
+
return combinator._parseScalar!
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function scalarTree(parser: Combinator<unknown>, seen: Set<Combinator<unknown>>): boolean {
|
|
12
|
+
if (seen.has(parser)) return true
|
|
13
|
+
if (parser._parseScalar === undefined) return false
|
|
14
|
+
seen.add(parser)
|
|
15
|
+
const def = parser._def
|
|
16
|
+
if (!SCALAR_TAG.test(def.tag)) return false
|
|
17
|
+
if ('parsers' in def && !def.parsers.every(child => scalarTree(child, seen))) return false
|
|
18
|
+
if ('parser' in def && !scalarTree(def.parser, seen)) return false
|
|
19
|
+
if ('separator' in def && !scalarTree(def.separator, seen)) return false
|
|
20
|
+
return def.tag !== 'lazy' || scalarTree(def.thunk(), seen)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Select the strict value-only ABI only when the complete root is in its family. */
|
|
24
|
+
export function scalarRootOf<T>(combinator: Combinator<T>): ScalarParser | undefined {
|
|
25
|
+
const scalar = combinator._parseScalar
|
|
26
|
+
const def = combinator._def
|
|
27
|
+
return scalar !== undefined && def.tag === 'grammar' && scalarTree(def.parser, new Set())
|
|
28
|
+
? scalar
|
|
29
|
+
: undefined
|
|
30
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Combinator, ParseContext, ParseResult, ParserMeta, ParseFail } from '../types.ts'
|
|
2
|
-
import { sequenceFirstSet, firstSetOf, union } from './first-set.ts'
|
|
2
|
+
import { sequenceFirstSet, firstSetOf, matchesEmpty, union } from './first-set.ts'
|
|
3
3
|
import {
|
|
4
4
|
advanceTrivia, commitTriviaScan, needsDeferredTriviaCommit, rollbackScannedTriviaAt,
|
|
5
5
|
scanTriviaCompact,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { cstTlLen } from '../cst/capture-buffer.ts'
|
|
8
8
|
import { firstSetSentinel } from '../recovery/scan.ts'
|
|
9
9
|
import { adjacencyFail, adjacencyOf, holdsAdjacency, type AdjacencyDef } from './adjacency.ts'
|
|
10
|
+
import { scalarOf } from './scalar.ts'
|
|
10
11
|
|
|
11
12
|
type UnwrapParsers<T extends Combinator<unknown>[]> = {
|
|
12
13
|
[K in keyof T]: T[K] extends Combinator<infer U> ? U : never
|
|
@@ -36,6 +37,11 @@ export function sequence<T extends [Combinator<unknown>, ...Combinator<unknown>[
|
|
|
36
37
|
;(adjacency ??= Array.from({ length: parsers.length }, () => null))[i] = a
|
|
37
38
|
}
|
|
38
39
|
const hasAdjacency = adjacency !== null
|
|
40
|
+
const strictArity = !hasAdjacency
|
|
41
|
+
&& ((parsers.length === 2 && !matchesEmpty(parsers[1]!))
|
|
42
|
+
|| (parsers.length === 3 && parsers.every(p => !matchesEmpty(p))))
|
|
43
|
+
? parsers.length
|
|
44
|
+
: 0
|
|
39
45
|
|
|
40
46
|
const meta: ParserMeta = {
|
|
41
47
|
// Union through the nullable prefix — a leading optional()/many() lets a later
|
|
@@ -47,194 +53,103 @@ export function sequence<T extends [Combinator<unknown>, ...Combinator<unknown>[
|
|
|
47
53
|
|
|
48
54
|
const def: { tag: 'sequence'; parsers: Combinator<unknown>[]; valueUnused?: boolean } =
|
|
49
55
|
{ tag: 'sequence', parsers: parsers as Combinator<unknown>[] }
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// never touched on the strict path: followSentinels[i] matches (zero-width) when
|
|
53
|
-
// the input could start any term AFTER i, so a list nested in term i can resync
|
|
54
|
-
// to this sequence's enclosing delimiter with NO grammar annotation. This is the
|
|
55
|
-
// whole of "recovery config" — derived from structure, not authored.
|
|
56
|
-
let followSentinels: (Combinator<null> | null)[] | undefined
|
|
57
|
-
|
|
58
|
-
// Tolerant twin of the strict loop: identical term-parsing, but around each term
|
|
59
|
-
// it publishes the inferred follow sentinel into ctx._sync so a nested list can
|
|
60
|
-
// resync to this sequence's enclosing delimiter. Cold path (tolerant only).
|
|
61
|
-
function parseTolerant(input: string, pos: number, ctx: ParseContext): ParseResult<UnwrapParsers<T>> {
|
|
62
|
-
followSentinels ??= parsers.map((_, i) => {
|
|
63
|
-
// A nested list can resync to the start of ANY term that follows it here, so
|
|
64
|
-
// union every following term's first set (not just up to the first
|
|
65
|
-
// non-nullable one — a mandatory middle term must not hide a later close).
|
|
66
|
-
const fs = parsers.slice(i + 1).reduce<ReturnType<typeof firstSetOf>>(
|
|
67
|
-
(acc, p) => union(acc, firstSetOf(p)),
|
|
68
|
-
{ kind: 'empty' },
|
|
69
|
-
)
|
|
70
|
-
return firstSetSentinel(fs)
|
|
71
|
-
})
|
|
72
|
-
const values: unknown[] | undefined = def.valueUnused ? undefined : []
|
|
56
|
+
const scalarParsers = parsers.map(scalarOf)
|
|
57
|
+
const parseScalar = (input: string, pos: number, ctx: ParseContext): number => {
|
|
73
58
|
let cur = pos
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
if (ctx.trivia && i > 0) {
|
|
87
|
-
const mTlog = cstTlLen(ctx)
|
|
88
|
-
const mLog = ctx._triviaLog?.length ?? 0
|
|
89
|
-
const mRootLog = ctx._rootTriviaLog?.length ?? 0
|
|
90
|
-
let scanEnd: number
|
|
91
|
-
if (needsDeferredTriviaCommit(ctx)) {
|
|
92
|
-
scanEnd = commitTriviaScan(scanTriviaCompact(input, cur, ctx))
|
|
93
|
-
} else {
|
|
94
|
-
scanEnd = advanceTrivia(input, cur, ctx)
|
|
95
|
-
}
|
|
96
|
-
const scanTlog = cstTlLen(ctx)
|
|
97
|
-
const scanLog = ctx._triviaLog?.length ?? 0
|
|
98
|
-
const scanRootLog = ctx._rootTriviaLog?.length ?? 0
|
|
99
|
-
const result = parsers[i]!.parse(input, scanEnd, ctx)
|
|
100
|
-
if (!result.ok) return result as ParseFail
|
|
101
|
-
if (result.span.end > scanEnd) cur = result.span.end
|
|
102
|
-
else rollbackScannedTriviaAt(
|
|
103
|
-
ctx, mTlog, scanTlog, mLog, scanLog, mRootLog, scanRootLog,
|
|
104
|
-
)
|
|
105
|
-
if (values !== undefined) values.push(result.value)
|
|
106
|
-
continue
|
|
107
|
-
}
|
|
108
|
-
const result = parsers[i]!.parse(input, cur, ctx)
|
|
109
|
-
if (!result.ok) return result as ParseFail
|
|
110
|
-
if (values !== undefined) values.push(result.value)
|
|
111
|
-
cur = result.span.end
|
|
59
|
+
if (strictArity !== 0) {
|
|
60
|
+
cur = scalarParsers[0]!(input, cur, ctx)
|
|
61
|
+
if (cur < 0) return cur
|
|
62
|
+
const first = ctx._sv
|
|
63
|
+
if (ctx.trivia) cur = advanceTrivia(input, cur, ctx)
|
|
64
|
+
cur = scalarParsers[1]!(input, cur, ctx)
|
|
65
|
+
if (cur < 0) return cur
|
|
66
|
+
const second = ctx._sv
|
|
67
|
+
if (strictArity === 2) {
|
|
68
|
+
ctx._sv = def.valueUnused ? undefined : [first, second]
|
|
69
|
+
return cur
|
|
112
70
|
}
|
|
113
|
-
|
|
114
|
-
|
|
71
|
+
if (ctx.trivia) cur = advanceTrivia(input, cur, ctx)
|
|
72
|
+
cur = scalarParsers[2]!(input, cur, ctx)
|
|
73
|
+
if (cur < 0) return cur
|
|
74
|
+
ctx._sv = def.valueUnused ? undefined : [first, second, ctx._sv]
|
|
75
|
+
return cur
|
|
115
76
|
}
|
|
116
|
-
return {
|
|
117
|
-
ok: true,
|
|
118
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
119
|
-
value: (values ?? undefined) as UnwrapParsers<T>,
|
|
120
|
-
span: { start: pos, end: cur },
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
77
|
|
|
124
|
-
// Strict twin of the loop below, for the (rare) sequence that carries an adjacency
|
|
125
|
-
// assertion. FORKED rather than branched inline so the ordinary strict loop — the
|
|
126
|
-
// hot path of every grammar — keeps exactly the instructions it had before.
|
|
127
|
-
function parseAdjacent(input: string, pos: number, ctx: ParseContext): ParseResult<UnwrapParsers<T>> {
|
|
128
78
|
const values: unknown[] | undefined = def.valueUnused ? undefined : []
|
|
129
|
-
let
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// the trivia log are byte-identical to the same sequence without the marker.
|
|
137
|
-
if (!holdsAdjacency(input, cur, ctx, adj)) return adjacencyFail(cur, adj)
|
|
138
|
-
if (values !== undefined) values.push(null)
|
|
139
|
-
continue
|
|
140
|
-
}
|
|
141
|
-
if (ctx.trivia && i > 0) {
|
|
142
|
-
let scanEnd: number
|
|
143
|
-
const mTlog = cstTlLen(ctx)
|
|
144
|
-
const mLog = ctx._triviaLog?.length ?? 0
|
|
145
|
-
const mRootLog = ctx._rootTriviaLog?.length ?? 0
|
|
146
|
-
if (needsDeferredTriviaCommit(ctx)) {
|
|
147
|
-
scanEnd = commitTriviaScan(scanTriviaCompact(input, cur, ctx))
|
|
148
|
-
} else {
|
|
149
|
-
scanEnd = advanceTrivia(input, cur, ctx)
|
|
150
|
-
}
|
|
151
|
-
const scanTlog = cstTlLen(ctx)
|
|
152
|
-
const scanLog = ctx._triviaLog?.length ?? 0
|
|
153
|
-
const scanRootLog = ctx._rootTriviaLog?.length ?? 0
|
|
154
|
-
const result = parsers[i]!.parse(input, scanEnd, ctx)
|
|
155
|
-
if (!result.ok) return result as ParseFail
|
|
156
|
-
if (result.span.end > scanEnd) cur = result.span.end
|
|
157
|
-
else rollbackScannedTriviaAt(
|
|
158
|
-
ctx, mTlog, scanTlog, mLog, scanLog, mRootLog, scanRootLog,
|
|
159
|
-
)
|
|
160
|
-
if (values !== undefined) values.push(result.value)
|
|
161
|
-
continue
|
|
162
|
-
}
|
|
163
|
-
const result = parsers[i]!.parse(input, cur, ctx)
|
|
164
|
-
if (!result.ok) return result as ParseFail
|
|
165
|
-
if (values !== undefined) values.push(result.value)
|
|
166
|
-
cur = result.span.end
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
ok: true,
|
|
171
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
172
|
-
value: (values ?? undefined) as UnwrapParsers<T>,
|
|
173
|
-
span: { start: pos, end: cur },
|
|
79
|
+
for (let i = 0; i < scalarParsers.length; i++) {
|
|
80
|
+
let termPos = cur
|
|
81
|
+
if (i > 0 && ctx.trivia) termPos = advanceTrivia(input, cur, ctx)
|
|
82
|
+
const end = scalarParsers[i]!(input, termPos, ctx)
|
|
83
|
+
if (end < 0) return end
|
|
84
|
+
if (values !== undefined) values.push(ctx._sv)
|
|
85
|
+
if (end > termPos || termPos === cur) cur = end
|
|
174
86
|
}
|
|
87
|
+
ctx._sv = values
|
|
88
|
+
return cur
|
|
175
89
|
}
|
|
176
90
|
|
|
91
|
+
// Recovery follow sets are derived only when the general family runs tolerantly.
|
|
92
|
+
let followSentinels: (Combinator<null> | null)[] | undefined
|
|
93
|
+
|
|
177
94
|
return {
|
|
178
95
|
_tag: 'sequence',
|
|
179
96
|
_meta: meta,
|
|
180
97
|
_def: def,
|
|
98
|
+
_parseScalar: hasAdjacency ? undefined : parseScalar,
|
|
181
99
|
parse(input: string, pos: number, ctx: ParseContext): ParseResult<UnwrapParsers<T>> {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
100
|
+
const tolerant = ctx._tolerant === true
|
|
101
|
+
if (tolerant) {
|
|
102
|
+
followSentinels ??= parsers.map((_, i) => firstSetSentinel(
|
|
103
|
+
parsers.slice(i + 1).reduce<ReturnType<typeof firstSetOf>>(
|
|
104
|
+
(set, parser) => union(set, firstSetOf(parser)), { kind: 'empty' },
|
|
105
|
+
),
|
|
106
|
+
))
|
|
107
|
+
}
|
|
108
|
+
const inheritedSync = ctx._sync
|
|
109
|
+
const deferredTrivia = needsDeferredTriviaCommit(ctx)
|
|
191
110
|
const values: unknown[] | undefined = def.valueUnused ? undefined : []
|
|
192
111
|
let cur = pos
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const mLog = ctx._triviaLog?.length ?? 0
|
|
202
|
-
const mRootLog = ctx._rootTriviaLog?.length ?? 0
|
|
203
|
-
|
|
204
|
-
if (needsDeferredTriviaCommit(ctx)) {
|
|
205
|
-
scanEnd = commitTriviaScan(scanTriviaCompact(input, cur, ctx))
|
|
206
|
-
} else {
|
|
207
|
-
scanEnd = advanceTrivia(input, cur, ctx)
|
|
112
|
+
try {
|
|
113
|
+
for (let i = 0; i < parsers.length; i++) {
|
|
114
|
+
if (tolerant) ctx._sync = followSentinels![i] ?? inheritedSync
|
|
115
|
+
const assertion = adjacency?.[i]
|
|
116
|
+
if (assertion) {
|
|
117
|
+
if (!holdsAdjacency(input, cur, ctx, assertion)) return adjacencyFail(cur, assertion)
|
|
118
|
+
if (values !== undefined) values.push(null)
|
|
119
|
+
continue
|
|
208
120
|
}
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
121
|
+
let termPos = cur
|
|
122
|
+
const markTlog = deferredTrivia ? cstTlLen(ctx) : 0
|
|
123
|
+
const markLog = deferredTrivia ? (ctx._triviaLog?.length ?? 0) : 0
|
|
124
|
+
const markRootLog = deferredTrivia ? (ctx._rootTriviaLog?.length ?? 0) : 0
|
|
125
|
+
if (ctx.trivia && i > 0) {
|
|
126
|
+
termPos = deferredTrivia
|
|
127
|
+
? commitTriviaScan(scanTriviaCompact(input, cur, ctx))
|
|
128
|
+
: advanceTrivia(input, cur, ctx)
|
|
129
|
+
}
|
|
130
|
+
const scanTlog = deferredTrivia ? cstTlLen(ctx) : 0
|
|
131
|
+
const scanLog = deferredTrivia ? (ctx._triviaLog?.length ?? 0) : 0
|
|
132
|
+
const scanRootLog = deferredTrivia ? (ctx._rootTriviaLog?.length ?? 0) : 0
|
|
133
|
+
const result = parsers[i]!.parse(input, termPos, ctx)
|
|
214
134
|
if (!result.ok) return result as ParseFail
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
135
|
+
if (values !== undefined) values.push(result.value)
|
|
136
|
+
if (result.span.end > termPos || termPos === cur) cur = result.span.end
|
|
137
|
+
else if (deferredTrivia) {
|
|
218
138
|
rollbackScannedTriviaAt(
|
|
219
|
-
ctx,
|
|
139
|
+
ctx, markTlog, scanTlog, markLog, scanLog, markRootLog, scanRootLog,
|
|
220
140
|
)
|
|
221
141
|
}
|
|
222
|
-
if (values !== undefined) values.push(result.value)
|
|
223
|
-
continue
|
|
224
142
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (!result.ok) return result as ParseFail
|
|
228
|
-
if (values !== undefined) values.push(result.value)
|
|
229
|
-
cur = result.span.end
|
|
143
|
+
} finally {
|
|
144
|
+
if (tolerant) ctx._sync = inheritedSync
|
|
230
145
|
}
|
|
231
|
-
|
|
232
146
|
return {
|
|
233
147
|
ok: true,
|
|
234
148
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
235
149
|
value: (values ?? undefined) as UnwrapParsers<T>,
|
|
236
150
|
span: { start: pos, end: cur },
|
|
237
151
|
}
|
|
152
|
+
|
|
238
153
|
},
|
|
239
154
|
}
|
|
240
155
|
}
|
|
@@ -66,6 +66,18 @@ export function needsDeferredTriviaCommit(ctx: ParseContext): boolean {
|
|
|
66
66
|
return ctx._triviaLog !== undefined || ctx._rootTriviaLog !== undefined || ctx._cstBuf !== undefined || ctx._cstTriviaLog !== undefined
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/** True when speculative parsing can write anything `rollbackTrivia` restores. */
|
|
70
|
+
export function needsTriviaRollback(ctx: ParseContext): boolean {
|
|
71
|
+
return ctx._cstBuf !== undefined
|
|
72
|
+
|| ctx._cstLeaves !== undefined
|
|
73
|
+
|| ctx._cstRawChildren !== undefined
|
|
74
|
+
|| ctx._cstTriviaLog !== undefined
|
|
75
|
+
|| ctx._fields !== undefined
|
|
76
|
+
|| ctx._errors !== undefined
|
|
77
|
+
|| ctx._triviaLog !== undefined
|
|
78
|
+
|| ctx._rootTriviaLog !== undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
69
81
|
export function saveTriviaMark(ctx: ParseContext): TriviaRollbackMark {
|
|
70
82
|
const m = saveCstMark(ctx)
|
|
71
83
|
return {
|
|
@@ -268,7 +280,8 @@ export function advanceTrivia(input: string, cur: number, ctx: ParseContext): nu
|
|
|
268
280
|
const triviaP = ctx.trivia
|
|
269
281
|
if (!triviaP) return cur
|
|
270
282
|
if (!ctx.trackLines) {
|
|
271
|
-
const
|
|
283
|
+
const classified = triviaP._meta.triviaScanner
|
|
284
|
+
const fast = classified === undefined ? fastTriviaScanner(triviaP) : classified
|
|
272
285
|
if (fast) return fast(input, cur)
|
|
273
286
|
if (ctx.triviaKindLabels) return skipWithLabels(input, cur, ctx)
|
|
274
287
|
const tr = triviaP.parse(input, cur, createDetachedParseContext(ctx.trackLines, ctx.state))
|