parseman 0.50.0 → 0.50.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +14 -10
  2. package/dist/analysis/diagnostics.js +455 -226
  3. package/dist/analysis/diagnostics.js.map +3 -3
  4. package/dist/cli/index.js +445 -191
  5. package/dist/cli/index.js.map +3 -3
  6. package/dist/combinators/choice.d.ts.map +1 -1
  7. package/dist/combinators/grammar.d.ts.map +1 -1
  8. package/dist/combinators/keywords.d.ts.map +1 -1
  9. package/dist/combinators/literal.d.ts.map +1 -1
  10. package/dist/combinators/map.d.ts.map +1 -1
  11. package/dist/combinators/ref.d.ts.map +1 -1
  12. package/dist/combinators/regex.d.ts.map +1 -1
  13. package/dist/combinators/repeat.d.ts.map +1 -1
  14. package/dist/combinators/scalar.d.ts +6 -0
  15. package/dist/combinators/scalar.d.ts.map +1 -0
  16. package/dist/combinators/sequence.d.ts.map +1 -1
  17. package/dist/combinators/trivia-skip.d.ts +2 -0
  18. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  19. package/dist/compiler/token-capability.js +0 -3
  20. package/dist/compiler/token-capability.js.map +2 -2
  21. package/dist/index.js +446 -192
  22. package/dist/index.js.map +3 -3
  23. package/dist/language-service/index.js +68 -172
  24. package/dist/language-service/index.js.map +3 -3
  25. package/dist/parse-context.d.ts.map +1 -1
  26. package/dist/plugin/index.js +491 -306
  27. package/dist/plugin/index.js.map +3 -3
  28. package/dist/run/index.js +1 -0
  29. package/dist/run/index.js.map +1 -1
  30. package/dist/spec/index.js +399 -277
  31. package/dist/spec/index.js.map +3 -3
  32. package/dist/table/emit-assembly.d.ts.map +1 -1
  33. package/dist/table/encode.d.ts.map +1 -1
  34. package/dist/table/index.js +331 -188
  35. package/dist/table/index.js.map +3 -3
  36. package/dist/types.d.ts +8 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/version.d.ts +1 -1
  39. package/package.json +4 -2
  40. package/src/combinators/choice.ts +34 -1
  41. package/src/combinators/grammar.ts +39 -0
  42. package/src/combinators/keywords.ts +14 -3
  43. package/src/combinators/literal.ts +26 -0
  44. package/src/combinators/map.ts +60 -0
  45. package/src/combinators/ref.ts +26 -7
  46. package/src/combinators/regex.ts +23 -0
  47. package/src/combinators/repeat.ts +109 -16
  48. package/src/combinators/scalar.ts +30 -0
  49. package/src/combinators/sequence.ts +76 -161
  50. package/src/combinators/trivia-skip.ts +14 -1
  51. package/src/parse-context.ts +1 -0
  52. package/src/table/emit-assembly.ts +39 -16
  53. package/src/table/encode.ts +30 -10
  54. package/src/types.ts +8 -0
  55. 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 item value + end position, the underlying failure (so oneOrMore
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
- ): { value: T; end: number } | { fail: ParseResult<T>; failPos: number } | 'stop' {
35
- const mark = saveTriviaMark(ctx)
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 { value: result.value, end: result.span.end }
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
- // Automatic recovery-sync inference, built lazily on the first tolerant parse and
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
- const inheritedSync = ctx._sync
75
- try {
76
- for (let i = 0; i < parsers.length; i++) {
77
- // Publish this term's follow set (or keep the inherited sync when the local
78
- // follow isn't usable, e.g. the last term or an `any` first set).
79
- ctx._sync = followSentinels[i] ?? inheritedSync
80
- const adj = adjacency === null ? null : adjacency[i]
81
- if (adj) {
82
- if (!holdsAdjacency(input, cur, ctx, adj)) return adjacencyFail(cur, adj)
83
- if (values !== undefined) values.push(null)
84
- continue
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
- } finally {
114
- ctx._sync = inheritedSync
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 cur = pos
130
-
131
- for (let i = 0; i < parsers.length; i++) {
132
- const adj = adjacency![i]
133
- if (adj) {
134
- // Assert over the gap at `cur` and move nothing. The NEXT term re-scans the
135
- // same gap and owns the commit/rewind decision, so the tree, the spans and
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
- // One cold branch: in tolerant mode publish each term's follow-set as ctx._sync
183
- // so a nested list resyncs to the enclosing delimiter (dynamic scoping through
184
- // refs carries this across rule boundaries automatically). The strict loop
185
- // below is byte-identical to a parser with no recovery.
186
- if (ctx._tolerant) return parseTolerant(input, pos, ctx)
187
- if (hasAdjacency) return parseAdjacent(input, pos, ctx)
188
-
189
- // Skip the tuple when it's never observed (markUnusedValues): terms still
190
- // parse (and self-capture) — only the array of their values is elided.
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
- for (let i = 0; i < parsers.length; i++) {
195
- if (ctx.trivia && i > 0) {
196
- // Skip trivia between terms, but only *consume* it for span purposes if
197
- // this term actually matches content past the trivia. A term that matches
198
- // empty (optional/many/lookahead) leaves the trivia for the enclosing rule.
199
- let scanEnd: number
200
- const mTlog = cstTlLen(ctx)
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
- const scanTlog = cstTlLen(ctx)
210
- const scanLog = ctx._triviaLog?.length ?? 0
211
- const scanRootLog = ctx._rootTriviaLog?.length ?? 0
212
-
213
- const result = parsers[i]!.parse(input, scanEnd, ctx)
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 (result.span.end > scanEnd) {
216
- cur = result.span.end
217
- } else {
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, mTlog, scanTlog, mLog, scanLog, mRootLog, scanRootLog,
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
- const result = parsers[i]!.parse(input, cur, ctx)
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 fast = fastTriviaScanner(triviaP)
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))
@@ -56,6 +56,7 @@ export function createParseContext(): ParseContext {
56
56
  _fe: undefined,
57
57
  _fx: undefined,
58
58
  _fc: undefined,
59
+ _sv: undefined,
59
60
  _probe: undefined,
60
61
  _cstChildren: undefined,
61
62
  _cstLeaves: undefined,