parseman 0.47.1 → 0.48.1

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 (126) hide show
  1. package/README.md +27 -21
  2. package/dist/analysis/diagnostics.js +2964 -347
  3. package/dist/analysis/diagnostics.js.map +3 -3
  4. package/dist/cli/index.js +2960 -343
  5. package/dist/cli/index.js.map +3 -3
  6. package/dist/combinators/choice.d.ts.map +1 -1
  7. package/dist/combinators/expect.d.ts +1 -0
  8. package/dist/combinators/expect.d.ts.map +1 -1
  9. package/dist/combinators/expected.d.ts +8 -0
  10. package/dist/combinators/expected.d.ts.map +1 -0
  11. package/dist/combinators/first-set.d.ts +8 -0
  12. package/dist/combinators/first-set.d.ts.map +1 -1
  13. package/dist/combinators/grammar.d.ts.map +1 -1
  14. package/dist/combinators/keywords.d.ts +4 -0
  15. package/dist/combinators/keywords.d.ts.map +1 -1
  16. package/dist/combinators/literal.d.ts.map +1 -1
  17. package/dist/combinators/not.d.ts.map +1 -1
  18. package/dist/combinators/peek.d.ts.map +1 -1
  19. package/dist/combinators/regex.d.ts.map +1 -1
  20. package/dist/combinators/repeat.d.ts.map +1 -1
  21. package/dist/combinators/sequence.d.ts.map +1 -1
  22. package/dist/combinators/trivia-skip.d.ts +16 -3
  23. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  24. package/dist/compiler/direct-projection.d.ts +12 -0
  25. package/dist/compiler/direct-projection.d.ts.map +1 -0
  26. package/dist/compiler/token-alphabet.d.ts +845 -5
  27. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  28. package/dist/compiler/token-capability.d.ts +3 -0
  29. package/dist/compiler/token-capability.d.ts.map +1 -0
  30. package/dist/compiler/token-capability.js +3192 -0
  31. package/dist/compiler/token-capability.js.map +6 -0
  32. package/dist/cst/trivia-charscan.d.ts +3 -0
  33. package/dist/cst/trivia-charscan.d.ts.map +1 -1
  34. package/dist/cst/trivia-css-scanner.d.ts +5 -0
  35. package/dist/cst/trivia-css-scanner.d.ts.map +1 -0
  36. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  37. package/dist/index.js +2973 -343
  38. package/dist/index.js.map +3 -3
  39. package/dist/language-service/index.js +5 -1
  40. package/dist/language-service/index.js.map +1 -1
  41. package/dist/plugin/index.js +2982 -352
  42. package/dist/plugin/index.js.map +3 -3
  43. package/dist/spec/index.js +325 -47
  44. package/dist/spec/index.js.map +3 -3
  45. package/dist/table/assemble.d.ts.map +1 -1
  46. package/dist/table/child-slots.d.ts.map +1 -1
  47. package/dist/table/compile-rule-map.d.ts +3 -1
  48. package/dist/table/compile-rule-map.d.ts.map +1 -1
  49. package/dist/table/emit-assembly.d.ts +12 -13
  50. package/dist/table/emit-assembly.d.ts.map +1 -1
  51. package/dist/table/emit.d.ts.map +1 -1
  52. package/dist/table/encode.d.ts.map +1 -1
  53. package/dist/table/exec-baseline.d.ts.map +1 -1
  54. package/dist/table/exec.d.ts.map +1 -1
  55. package/dist/table/index.js +2937 -311
  56. package/dist/table/index.js.map +3 -3
  57. package/dist/table/lex-body.d.ts +9 -0
  58. package/dist/table/lex-body.d.ts.map +1 -0
  59. package/dist/table/lex-program.d.ts +25 -0
  60. package/dist/table/lex-program.d.ts.map +1 -0
  61. package/dist/table/ops.d.ts +47 -11
  62. package/dist/table/ops.d.ts.map +1 -1
  63. package/dist/table/program.d.ts +50 -2
  64. package/dist/table/program.d.ts.map +1 -1
  65. package/dist/table/scalar-terminal.d.ts +13 -0
  66. package/dist/table/scalar-terminal.d.ts.map +1 -0
  67. package/dist/table/site-labels.d.ts.map +1 -1
  68. package/dist/types.d.ts +2 -0
  69. package/dist/types.d.ts.map +1 -1
  70. package/dist/version.d.ts +1 -1
  71. package/package.json +11 -11
  72. package/src/combinators/choice.ts +10 -1
  73. package/src/combinators/expect.ts +1 -0
  74. package/src/combinators/expected.ts +17 -0
  75. package/src/combinators/first-set.ts +58 -5
  76. package/src/combinators/grammar.ts +8 -3
  77. package/src/combinators/keywords.ts +22 -6
  78. package/src/combinators/literal.ts +2 -1
  79. package/src/combinators/not.ts +5 -1
  80. package/src/combinators/peek.ts +5 -1
  81. package/src/combinators/regex.ts +5 -3
  82. package/src/combinators/repeat.ts +4 -10
  83. package/src/combinators/sequence.ts +17 -14
  84. package/src/combinators/trivia-skip.ts +46 -13
  85. package/src/compiler/direct-projection.ts +43 -0
  86. package/src/compiler/token-alphabet.ts +4316 -12
  87. package/src/compiler/token-capability.ts +6 -0
  88. package/src/cst/trivia-charscan.ts +34 -0
  89. package/src/cst/trivia-css-scanner.ts +142 -0
  90. package/src/cst/trivia-kinds.ts +3 -1
  91. package/src/plugin/index.ts +4 -4
  92. package/src/table/assemble.ts +1253 -240
  93. package/src/table/child-slots.ts +2 -1
  94. package/src/table/compile-rule-map.ts +21 -3
  95. package/src/table/emit-assembly.ts +335 -87
  96. package/src/table/emit.ts +16 -2
  97. package/src/table/encode.ts +248 -60
  98. package/src/table/exec-baseline.ts +3 -7
  99. package/src/table/exec.ts +134 -25
  100. package/src/table/lex-body.ts +30 -0
  101. package/src/table/lex-program.ts +431 -0
  102. package/src/table/ops.ts +49 -11
  103. package/src/table/program.ts +46 -2
  104. package/src/table/scalar-terminal.ts +51 -0
  105. package/src/table/site-labels.ts +11 -1
  106. package/src/table/stamp.ts +1 -1
  107. package/src/types.ts +1 -1
  108. package/src/version.ts +1 -1
  109. package/dist/analysis/diagnostics.cjs +0 -13824
  110. package/dist/analysis/diagnostics.cjs.map +0 -6
  111. package/dist/cli/index.cjs +0 -14925
  112. package/dist/cli/index.cjs.map +0 -6
  113. package/dist/index.cjs +0 -16391
  114. package/dist/index.cjs.map +0 -6
  115. package/dist/language-service/index.cjs +0 -1304
  116. package/dist/language-service/index.cjs.map +0 -6
  117. package/dist/oracle/index.cjs +0 -254
  118. package/dist/oracle/index.cjs.map +0 -6
  119. package/dist/plugin/index.cjs +0 -16097
  120. package/dist/plugin/index.cjs.map +0 -6
  121. package/dist/run/index.cjs +0 -577
  122. package/dist/run/index.cjs.map +0 -6
  123. package/dist/spec/index.cjs +0 -6236
  124. package/dist/spec/index.cjs.map +0 -6
  125. package/dist/table/index.cjs +0 -11463
  126. package/dist/table/index.cjs.map +0 -6
@@ -115,8 +115,10 @@ export function parser<T>(opts: ParserOptions, root: Combinator<T>): ParsemanPar
115
115
  const refuseUnclassified = scopeTrivia !== undefined
116
116
  && !scopeTrivia._meta?.rootTriviaClassified && !opaqueRootCapture
117
117
  const forceCaptureTrivia = opts.captureTrivia === true
118
- const trackLinesOf = opts.trackLines === true ? TRACK_LINES_ON
119
- : opts.trackLines === false ? TRACK_LINES_OFF
118
+ const trackLinesPolicy = opts.trackLines === true ? 'on'
119
+ : opts.trackLines === false ? 'off' : 'inherit'
120
+ const trackLinesOf = trackLinesPolicy === 'on' ? TRACK_LINES_ON
121
+ : trackLinesPolicy === 'off' ? TRACK_LINES_OFF
120
122
  : TRACK_LINES_INHERIT
121
123
  /**
122
124
  * The per-node capture mask, resolved as far as this scope can resolve it.
@@ -130,7 +132,7 @@ export function parser<T>(opts: ParserOptions, root: Combinator<T>): ParsemanPar
130
132
  : scopeLabels !== undefined ? triviaKindMask(scopeLabels, captureKinds)
131
133
  : undefined
132
134
  const inheritCaptureMask = captureKinds !== undefined && scopeLabels === undefined
133
- return {
135
+ const grammar: ParsemanParser<T> = {
134
136
  _tag: 'grammar',
135
137
  _meta: {
136
138
  ...root._meta,
@@ -150,6 +152,8 @@ export function parser<T>(opts: ParserOptions, root: Combinator<T>): ParsemanPar
150
152
  ...(opaqueRootCapture ? { rootCapture: 'opaque' as const } : {}),
151
153
  ...(opts.captureTrivia ? { captureTrivia: true } : {}),
152
154
  trackLines: opts.trackLines ?? false,
155
+ constructionTrackLines: trackLinesPolicy,
156
+ ...(captureKinds === undefined ? {} : { constructionCaptureTriviaKinds: captureKinds }),
153
157
  },
154
158
  parse(input: string, pos?: number, _ctx?: ParseContext): ParseResult<T> {
155
159
  if (refuseUnclassified) refuseUnclassifiedRootScope(_ctx?._rootTriviaStrictScopes)
@@ -197,6 +201,7 @@ export function parser<T>(opts: ParserOptions, root: Combinator<T>): ParsemanPar
197
201
  return lineIndex ? annotateResultLines(result, normalizeLineIndex(lineIndex)) : result
198
202
  },
199
203
  } as ParsemanParser<T>
204
+ return grammar
200
205
  }
201
206
 
202
207
  /**
@@ -3,6 +3,7 @@ import { fromChar, union, empty } from './first-set.ts'
3
3
  import { caseFoldVariants } from './case-fold.ts'
4
4
  import { failAt } from './probe.ts'
5
5
  import { pushCstLeaf, cstCaptureActive } from '../cst/capture-buffer.ts'
6
+ import { directTerminalFailureExpected } from './expected.ts'
6
7
 
7
8
  export type KeywordsOptions = {
8
9
  /** Match case-insensitively. */
@@ -70,6 +71,19 @@ function escapeRe(s: string): string {
70
71
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
71
72
  }
72
73
 
74
+ /** The terminal's one canonical recognizer spelling. Table-selected lexical
75
+ * bodies call this at construction time so the authored parser, CHARACTER row,
76
+ * and TOKEN replacement cannot drift on alternation order, boundary, or flags. */
77
+ export function keywordsRegExp(
78
+ words: readonly string[],
79
+ boundary: string | undefined,
80
+ caseInsensitive: boolean,
81
+ ): RegExp {
82
+ const alt = words.map(escapeRe).join('|')
83
+ const boundarySource = boundary ? `(?![${boundary}])` : ''
84
+ return new RegExp(`(?:${alt})${boundarySource}`, caseInsensitive ? 'iy' : 'uy')
85
+ }
86
+
73
87
  /**
74
88
  * Match any one of a set of fixed keywords, longest-first (so `border` wins over
75
89
  * `bord`), with an optional trailing word-boundary guard. Compiles to a single
@@ -84,8 +98,6 @@ export function keywords(words: readonly string[], opts: KeywordsOptions = {}):
84
98
  // Longest-first keeps the alternation greedy-correct (regex alternation is
85
99
  // first-match, not longest-match).
86
100
  const sorted = [...new Set(words)].sort((a, b) => b.length - a.length)
87
- const alt = sorted.map(escapeRe).join('|')
88
- const boundary = opts.boundary ? `(?![${opts.boundary}])` : ''
89
101
  // Case-INSENSITIVE drops `u` deliberately, so that MATCHING and the first-set
90
102
  // below fold the SAME set of characters — the invariant `regex()` established in
91
103
  // 0.32.0 (see the flag-aware widening in `src/combinators/regex.ts`):
@@ -102,8 +114,7 @@ export function keywords(words: readonly string[], opts: KeywordsOptions = {}):
102
114
  // not entering Unicode mode at all, which keeps the gate.
103
115
  // Every keyword is fully escaped by `escapeRe`, so `u` mode changes nothing about
104
116
  // what a case-SENSITIVE set matches; that path keeps `u` unchanged.
105
- const flags = opts.caseInsensitive ? 'iy' : 'uy'
106
- const re = new RegExp(`(?:${alt})${boundary}`, flags)
117
+ const re = keywordsRegExp(sorted, opts.boundary, opts.caseInsensitive ?? false)
107
118
 
108
119
  // First-set: the set of first code points across all keywords (and their
109
120
  // case-folded variants when case-insensitive), for choice() dispatch.
@@ -125,16 +136,21 @@ export function keywords(words: readonly string[], opts: KeywordsOptions = {}):
125
136
  }
126
137
 
127
138
  const meta: ParserMeta = { firstSet, canMatchNewline: false, isTrivia: false }
139
+ const def = {
140
+ tag: 'keywords', words: sorted,
141
+ caseInsensitive: opts.caseInsensitive ?? false,
142
+ boundary: opts.boundary,
143
+ } as const
128
144
 
129
145
  return {
130
146
  _tag: 'keywords',
131
147
  _meta: meta,
132
- _def: { tag: 'keywords', words: sorted, caseInsensitive: opts.caseInsensitive ?? false, boundary: opts.boundary },
148
+ _def: def,
133
149
  parse(input: string, pos: number, ctx: ParseContext): ParseResult<string> {
134
150
  re.lastIndex = pos
135
151
  const m = re.exec(input)
136
152
  if (m === null || m.index !== pos) {
137
- return failAt(ctx, ['keyword'], pos)
153
+ return failAt(ctx, directTerminalFailureExpected(def), pos)
138
154
  }
139
155
  const value = m[0]!
140
156
  const span = { start: pos, end: pos + value.length }
@@ -3,6 +3,7 @@ import { fromChar, empty } from './first-set.ts'
3
3
  import { failAt } from './probe.ts'
4
4
  import { pushCstLeaf, cstCaptureActive } from '../cst/capture-buffer.ts'
5
5
  import { recordLineRangeFromContext } from '../line-index.ts'
6
+ import { directTerminalFailureExpected } from './expected.ts'
6
7
 
7
8
  /**
8
9
  * ASCII case-fold equality — the interpreter twin of the compiler's `foldEq`
@@ -59,7 +60,7 @@ export function literal(value: string, opts: LiteralOptions = {}): Combinator<st
59
60
  : firstSet
60
61
  }
61
62
 
62
- const expected = [JSON.stringify(value)]
63
+ const expected = directTerminalFailureExpected({ tag: 'literal', value, caseInsensitive })
63
64
 
64
65
  // Single-char case-sensitive literals are the bulk of punctuation-heavy
65
66
  // grammars (GraphQL `{ } ( ) : $ @ [ ] ! =`, JSON, CSS). A bare `charCodeAt`
@@ -1,6 +1,7 @@
1
1
  import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
2
2
  import { any } from './first-set.ts'
3
3
  import { saveLookaheadMark, rollbackLookahead } from './trivia-skip.ts'
4
+ import { assertionFailureExpected } from './expected.ts'
4
5
 
5
6
  /**
6
7
  * Negative lookahead. Succeeds (consuming nothing) when `combinator` fails;
@@ -47,7 +48,10 @@ export function not(combinator: Combinator<unknown>): Combinator<null> {
47
48
  const result = combinator.parse(input, pos, ctx)
48
49
  rollbackLookahead(ctx, mark)
49
50
  if (result.ok) {
50
- return { ok: false, expected: [`not(${combinator._tag})`], span: { start: pos, end: pos } }
51
+ return {
52
+ ok: false, expected: assertionFailureExpected(false, combinator._tag),
53
+ span: { start: pos, end: pos },
54
+ }
51
55
  }
52
56
  return { ok: true, value: null, span: { start: pos, end: pos } }
53
57
  },
@@ -1,6 +1,7 @@
1
1
  import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
2
2
  import { any, matchesEmpty } from './first-set.ts'
3
3
  import { saveLookaheadMark, rollbackLookahead } from './trivia-skip.ts'
4
+ import { assertionFailureExpected } from './expected.ts'
4
5
 
5
6
  /**
6
7
  * Positive lookahead. Succeeds (consuming nothing) when `combinator` matches at
@@ -57,7 +58,10 @@ export function peek(combinator: Combinator<unknown>): Combinator<null> {
57
58
  const result = combinator.parse(input, pos, ctx)
58
59
  rollbackLookahead(ctx, mark)
59
60
  if (result.ok) return { ok: true, value: null, span: { start: pos, end: pos } }
60
- return { ok: false, expected: [`peek(${combinator._tag})`], span: { start: pos, end: pos } }
61
+ return {
62
+ ok: false, expected: assertionFailureExpected(true, combinator._tag),
63
+ span: { start: pos, end: pos },
64
+ }
61
65
  },
62
66
  }
63
67
  }
@@ -5,6 +5,7 @@ import { pushCstLeaf, cstCaptureActive } from '../cst/capture-buffer.ts'
5
5
  import { recordLineRangeFromContext } from '../line-index.ts'
6
6
  import { shorthandRanges, parseClassRanges } from '../regex/classes.ts'
7
7
  import { firstSetFromRegex } from '../regex/first-set.ts'
8
+ import { directTerminalFailureExpected } from './expected.ts'
8
9
 
9
10
  /**
10
11
  * A regex terminal's first-set (for choice-dispatch fast paths) is derived by
@@ -141,15 +142,16 @@ export function regex(pattern: string | RegExp, flags = ''): Combinator<string>
141
142
  : asciiCaseFold(raw.firstSet)
142
143
  const canMatchNewline = raw.canMatchNewline
143
144
  const meta: ParserMeta = { firstSet, canMatchNewline, isTrivia: false }
145
+ const def = { tag: 'regex', source, flags: resolvedFlags } as const
144
146
 
145
147
  return {
146
148
  _tag: 'regex',
147
149
  _meta: meta,
148
- _def: { tag: 'regex', source, flags: resolvedFlags },
150
+ _def: def,
149
151
  parse(input: string, pos: number, ctx: ParseContext): ParseResult<string> {
150
152
  const scanEnd = scan?.(input, pos)
151
153
  if (scanEnd !== undefined) {
152
- if (scanEnd === null) return failAt(ctx, [`/${source}/`], pos)
154
+ if (scanEnd === null) return failAt(ctx, directTerminalFailureExpected(def), pos)
153
155
  const value = input.slice(pos, scanEnd)
154
156
  const span = { start: pos, end: scanEnd }
155
157
  if (canMatchNewline && ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, scanEnd)
@@ -159,7 +161,7 @@ export function regex(pattern: string | RegExp, flags = ''): Combinator<string>
159
161
  anchored.lastIndex = pos
160
162
  const m = anchored.exec(input)
161
163
  if (m === null) {
162
- return failAt(ctx, [`/${source}/`], pos)
164
+ return failAt(ctx, directTerminalFailureExpected(def), pos)
163
165
  }
164
166
  const span = { start: pos, end: pos + m[0]!.length }
165
167
  if (canMatchNewline && ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, span.end)
@@ -1,5 +1,5 @@
1
1
  import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
2
- import { advanceTrivia, needsDeferredTriviaCommit, rollbackTrivia, saveTriviaMark, scanTrivia } from './trivia-skip.ts'
2
+ import { advanceTrivia, commitTriviaScan, needsDeferredTriviaCommit, 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'
@@ -36,9 +36,7 @@ function repItem<T>(
36
36
  let pos = cur
37
37
  if (ctx.trivia) {
38
38
  if (needsDeferredTriviaCommit(ctx)) {
39
- const scan = scanTrivia(input, cur, ctx)
40
- scan.commit()
41
- pos = scan.end
39
+ pos = commitTriviaScan(scanTriviaCompact(input, cur, ctx))
42
40
  } else {
43
41
  pos = advanceTrivia(input, cur, ctx)
44
42
  }
@@ -478,9 +476,7 @@ export function sepBy<T, S>(
478
476
  let sepPos = cur
479
477
  if (ctx.trivia) {
480
478
  if (needsDeferredTriviaCommit(ctx)) {
481
- const scan = scanTrivia(input, cur, ctx)
482
- scan.commit()
483
- sepPos = scan.end
479
+ sepPos = commitTriviaScan(scanTriviaCompact(input, cur, ctx))
484
480
  } else {
485
481
  sepPos = advanceTrivia(input, cur, ctx)
486
482
  }
@@ -503,9 +499,7 @@ export function sepBy<T, S>(
503
499
  let nextPos = sep.span.end
504
500
  if (ctx.trivia) {
505
501
  if (needsDeferredTriviaCommit(ctx)) {
506
- const scan = scanTrivia(input, sep.span.end, ctx)
507
- scan.commit()
508
- nextPos = scan.end
502
+ nextPos = commitTriviaScan(scanTriviaCompact(input, sep.span.end, ctx))
509
503
  } else {
510
504
  nextPos = advanceTrivia(input, sep.span.end, ctx)
511
505
  }
@@ -1,6 +1,9 @@
1
1
  import type { Combinator, ParseContext, ParseResult, ParserMeta, ParseFail } from '../types.ts'
2
2
  import { sequenceFirstSet, firstSetOf, union } from './first-set.ts'
3
- import { advanceTrivia, needsDeferredTriviaCommit, rollbackScannedTriviaAt, scanTrivia } from './trivia-skip.ts'
3
+ import {
4
+ advanceTrivia, commitTriviaScan, needsDeferredTriviaCommit, rollbackScannedTriviaAt,
5
+ scanTriviaCompact,
6
+ } from './trivia-skip.ts'
4
7
  import { cstTlLen } from '../cst/capture-buffer.ts'
5
8
  import { firstSetSentinel } from '../recovery/scan.ts'
6
9
  import { adjacencyFail, adjacencyOf, holdsAdjacency, type AdjacencyDef } from './adjacency.ts'
@@ -86,9 +89,7 @@ export function sequence<T extends [Combinator<unknown>, ...Combinator<unknown>[
86
89
  const mRootLog = ctx._rootTriviaLog?.length ?? 0
87
90
  let scanEnd: number
88
91
  if (needsDeferredTriviaCommit(ctx)) {
89
- const scan = scanTrivia(input, cur, ctx)
90
- scan.commit()
91
- scanEnd = scan.end
92
+ scanEnd = commitTriviaScan(scanTriviaCompact(input, cur, ctx))
92
93
  } else {
93
94
  scanEnd = advanceTrivia(input, cur, ctx)
94
95
  }
@@ -98,7 +99,9 @@ export function sequence<T extends [Combinator<unknown>, ...Combinator<unknown>[
98
99
  const result = parsers[i]!.parse(input, scanEnd, ctx)
99
100
  if (!result.ok) return result as ParseFail
100
101
  if (result.span.end > scanEnd) cur = result.span.end
101
- else rollbackScannedTriviaAt(ctx, mTlog, scanTlog, mLog, scanLog, mRootLog, scanRootLog)
102
+ else rollbackScannedTriviaAt(
103
+ ctx, mTlog, scanTlog, mLog, scanLog, mRootLog, scanRootLog,
104
+ )
102
105
  if (values !== undefined) values.push(result.value)
103
106
  continue
104
107
  }
@@ -141,9 +144,7 @@ export function sequence<T extends [Combinator<unknown>, ...Combinator<unknown>[
141
144
  const mLog = ctx._triviaLog?.length ?? 0
142
145
  const mRootLog = ctx._rootTriviaLog?.length ?? 0
143
146
  if (needsDeferredTriviaCommit(ctx)) {
144
- const scan = scanTrivia(input, cur, ctx)
145
- scan.commit()
146
- scanEnd = scan.end
147
+ scanEnd = commitTriviaScan(scanTriviaCompact(input, cur, ctx))
147
148
  } else {
148
149
  scanEnd = advanceTrivia(input, cur, ctx)
149
150
  }
@@ -153,7 +154,9 @@ export function sequence<T extends [Combinator<unknown>, ...Combinator<unknown>[
153
154
  const result = parsers[i]!.parse(input, scanEnd, ctx)
154
155
  if (!result.ok) return result as ParseFail
155
156
  if (result.span.end > scanEnd) cur = result.span.end
156
- else rollbackScannedTriviaAt(ctx, mTlog, scanTlog, mLog, scanLog, mRootLog, scanRootLog)
157
+ else rollbackScannedTriviaAt(
158
+ ctx, mTlog, scanTlog, mLog, scanLog, mRootLog, scanRootLog,
159
+ )
157
160
  if (values !== undefined) values.push(result.value)
158
161
  continue
159
162
  }
@@ -199,22 +202,22 @@ export function sequence<T extends [Combinator<unknown>, ...Combinator<unknown>[
199
202
  const mRootLog = ctx._rootTriviaLog?.length ?? 0
200
203
 
201
204
  if (needsDeferredTriviaCommit(ctx)) {
202
- const scan = scanTrivia(input, cur, ctx)
203
- scan.commit()
204
- scanEnd = scan.end
205
+ scanEnd = commitTriviaScan(scanTriviaCompact(input, cur, ctx))
205
206
  } else {
206
207
  scanEnd = advanceTrivia(input, cur, ctx)
207
208
  }
208
-
209
209
  const scanTlog = cstTlLen(ctx)
210
210
  const scanLog = ctx._triviaLog?.length ?? 0
211
211
  const scanRootLog = ctx._rootTriviaLog?.length ?? 0
212
+
212
213
  const result = parsers[i]!.parse(input, scanEnd, ctx)
213
214
  if (!result.ok) return result as ParseFail
214
215
  if (result.span.end > scanEnd) {
215
216
  cur = result.span.end
216
217
  } else {
217
- rollbackScannedTriviaAt(ctx, mTlog, scanTlog, mLog, scanLog, mRootLog, scanRootLog)
218
+ rollbackScannedTriviaAt(
219
+ ctx, mTlog, scanTlog, mLog, scanLog, mRootLog, scanRootLog,
220
+ )
218
221
  }
219
222
  if (values !== undefined) values.push(result.value)
220
223
  continue
@@ -16,6 +16,7 @@ import {
16
16
  } from '../cst/capture-buffer.ts'
17
17
  import { recordLineRangeFromContext } from '../line-index.ts'
18
18
  import { createDetachedParseContext } from '../parse-context.ts'
19
+ import { commonCssTriviaScanner } from '../cst/trivia-css-scanner.ts'
19
20
 
20
21
  /**
21
22
  * Result of scanning trivia: the position after it, plus a `commit()` that
@@ -25,11 +26,22 @@ import { createDetachedParseContext } from '../parse-context.ts'
25
26
  * trivia that doesn't actually sit between two accepted items.
26
27
  */
27
28
  export type TriviaScan = { end: number; commit: () => void }
29
+ export type CompactTriviaScan = number | TriviaScan
28
30
 
29
31
  /** Saved lengths for rolling back speculative trivia commits. */
30
32
  export type TriviaRollbackMark = { raw: number; tlog: number; leaves: number; fields: number; errors: number; log: number; rootLog: number }
31
33
 
32
34
  const NOOP_COMMIT = () => {}
35
+
36
+ export function triviaScanEnd(scan: CompactTriviaScan): number {
37
+ return typeof scan === 'number' ? scan : scan.end
38
+ }
39
+
40
+ export function commitTriviaScan(scan: CompactTriviaScan): number {
41
+ if (typeof scan === 'number') return scan
42
+ scan.commit()
43
+ return scan.end
44
+ }
33
45
  export type FastTriviaScanner = (input: string, cur: number) => number
34
46
  /**
35
47
  * Bounded structural cache for the handful of trivia grammars a process actively
@@ -102,8 +114,14 @@ function removeNumberRange(values: number[] | undefined, start: number, end: num
102
114
  }
103
115
 
104
116
  /**
105
- * Remove only the trivia rows appended by an ambient scan, preserving effects
106
- * appended later by a successful zero-width child.
117
+ * Remove only the trivia rows appended by an ambient scan, preserving rows a
118
+ * successful zero-width child appended after that scan.
119
+ *
120
+ * A sequence cannot use `rollbackTriviaAt` for this case: its pre-scan mark also
121
+ * precedes the child, so truncating to it erases the child's nodes, fields,
122
+ * recovery errors, and trivia. The scan's contribution is instead the half-open
123
+ * range between the pre-scan and post-scan lengths. Compact that range in place;
124
+ * this is the cold zero-width-success path and allocates no replacement arrays.
107
125
  */
108
126
  export function rollbackScannedTriviaAt(
109
127
  ctx: ParseContext,
@@ -169,10 +187,10 @@ export function rollbackLookahead(ctx: ParseContext, mark: LookaheadMark): void
169
187
  if (ctx._probe) ctx._probe.best = mark.probeBest
170
188
  }
171
189
 
172
- function scanWithLabels(input: string, cur: number, ctx: ParseContext): TriviaScan {
190
+ function scanWithLabels(input: string, cur: number, ctx: ParseContext): CompactTriviaScan {
173
191
  const triviaP = ctx.trivia!
174
192
  const spec = analyzeLabeledTrivia(triviaP)
175
- if (!spec) return { end: cur, commit: NOOP_COMMIT }
193
+ if (!spec) return cur
176
194
 
177
195
  const log = ctx._triviaLog
178
196
  const rootLog = ctx._rootTriviaLog
@@ -193,7 +211,8 @@ function scanWithLabels(input: string, cur: number, ctx: ParseContext): TriviaSc
193
211
  }
194
212
  })
195
213
  if (visitedEnd !== undefined) {
196
- if (visitedEnd === cur) return { end: cur, commit: NOOP_COMMIT }
214
+ if (visitedEnd === cur) return cur
215
+ if (fullRows === undefined && rootRows === undefined && cstRows === undefined) return visitedEnd
197
216
  return {
198
217
  end: visitedEnd,
199
218
  commit: () => {
@@ -213,7 +232,7 @@ function scanWithLabels(input: string, cur: number, ctx: ParseContext): TriviaSc
213
232
  }
214
233
 
215
234
  const { end, chunks } = scanLabeledTriviaChunks(input, cur, spec, ctx.state)
216
- if (end === cur) return { end: cur, commit: NOOP_COMMIT }
235
+ if (end === cur) return cur
217
236
 
218
237
  return {
219
238
  end,
@@ -273,9 +292,11 @@ export function advanceTrivia(input: string, cur: number, ctx: ParseContext): nu
273
292
 
274
293
  /**
275
294
  * Scan trivia at `cur` using `ctx.trivia`, WITHOUT recording it. Returns the
276
- * position after the trivia (or `cur` if none) and a `commit()` to record it.
295
+ * position directly when a classified scan retained no rows; otherwise returns
296
+ * that position with a deferred `commit()` that records the retained rows.
297
+ * Use `triviaScanEnd()` / `commitTriviaScan()` rather than inspecting the shape.
277
298
  */
278
- export function scanTrivia(input: string, cur: number, ctx: ParseContext): TriviaScan {
299
+ export function scanTriviaCompact(input: string, cur: number, ctx: ParseContext): CompactTriviaScan {
279
300
  const triviaP = ctx.trivia
280
301
  if (!triviaP) return { end: cur, commit: NOOP_COMMIT }
281
302
 
@@ -337,7 +358,7 @@ export function scanTrivia(input: string, cur: number, ctx: ParseContext): Trivi
337
358
 
338
359
  if (ctx.triviaKindLabels && (log !== undefined || rootLog !== undefined || captureTl)) {
339
360
  const scan = scanWithLabels(input, cur, ctx)
340
- if (trackTriviaLines) recordLineRangeFromContext(ctx, input, cur, scan.end)
361
+ if (trackTriviaLines) recordLineRangeFromContext(ctx, input, cur, triviaScanEnd(scan))
341
362
  return scan
342
363
  }
343
364
 
@@ -377,6 +398,12 @@ export function scanTrivia(input: string, cur: number, ctx: ParseContext): Trivi
377
398
  return { end: tr.ok ? tr.span.end : cur, commit: NOOP_COMMIT }
378
399
  }
379
400
 
401
+ /** Legacy object-shaped contract used by previously emitted assembly factories. */
402
+ export function scanTrivia(input: string, cur: number, ctx: ParseContext): TriviaScan {
403
+ const scan = scanTriviaCompact(input, cur, ctx)
404
+ return typeof scan === 'number' ? { end: scan, commit: NOOP_COMMIT } : scan
405
+ }
406
+
380
407
  /**
381
408
  * Skip trivia at `cur` through an INSTALLED scanner, recording it immediately.
382
409
  *
@@ -410,9 +437,7 @@ export function skipTriviaScanned(
410
437
  */
411
438
  export function consumeTrivia(input: string, cur: number, ctx: ParseContext): number {
412
439
  if (!needsDeferredTriviaCommit(ctx)) return advanceTrivia(input, cur, ctx)
413
- const scan = scanTrivia(input, cur, ctx)
414
- scan.commit()
415
- return scan.end
440
+ return commitTriviaScan(scanTriviaCompact(input, cur, ctx))
416
441
  }
417
442
 
418
443
  /**
@@ -436,7 +461,7 @@ export function probeTriviaEnd(input: string, cur: number, ctx: ParseContext): n
436
461
  // actually need to find the end (`trivia`, `state`, the deferred-commit sinks)
437
462
  // is carried through unchanged.
438
463
  const probeCtx: ParseContext = { ...ctx, trackLines: false }
439
- if (needsDeferredTriviaCommit(probeCtx)) return scanTrivia(input, cur, probeCtx).end
464
+ if (needsDeferredTriviaCommit(probeCtx)) return triviaScanEnd(scanTriviaCompact(input, cur, probeCtx))
440
465
  return advanceTrivia(input, cur, probeCtx)
441
466
  }
442
467
 
@@ -480,11 +505,19 @@ function buildFastTriviaScanner(trivia: Combinator<unknown>): FastTriviaScanner
480
505
  if (one) return loopScanner([one])
481
506
 
482
507
  if (repeat._def.tag !== 'choice') return null
508
+ const fused = commonCssTriviaScanner(repeat._def.parsers.map(triviaRegexSource))
509
+ if (fused !== null) return fused
483
510
  const arms = repeat._def.parsers.map(regexTriviaScanner)
484
511
  if (arms.some(s => s === null)) return null
485
512
  return loopScanner(arms as FastTriviaScanner[])
486
513
  }
487
514
 
515
+ /** The regex source beneath a classified-trivia `label()`, when it is plain. */
516
+ function triviaRegexSource(parser: Combinator<unknown>): string | null {
517
+ const d = parser._def.tag === 'label' ? parser._def.parser._def : parser._def
518
+ return d.tag === 'regex' && d.flags === '' ? d.source : null
519
+ }
520
+
488
521
  function loopScanner(arms: FastTriviaScanner[]): FastTriviaScanner {
489
522
  return (input, cur) => {
490
523
  let pos = cur
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Recognise the deliberately tiny transform shape whose result is already one
3
+ * of a sequence's values: `([, value]) => value`.
4
+ *
5
+ * This is a descriptor extraction, not callback source execution. Anything
6
+ * with defaults, rest, nesting, a block body, or an expression is left as an
7
+ * ordinary callback. `fnSrc` is preferred because serialized grammars carry
8
+ * the author's stable source; live combinators fall back to the function's own
9
+ * source only for the same exact syntax.
10
+ */
11
+ export function directArrayProjection(
12
+ fn: (...args: never[]) => unknown,
13
+ fnSrc: string | undefined,
14
+ arity: number,
15
+ ): number | null {
16
+ let source = fnSrc
17
+ if (source === undefined) {
18
+ source = Function.prototype.toString.call(fn)
19
+ if (source.includes('[native code]')) return null
20
+ }
21
+
22
+ const match = /^\s*\(\s*\[([^\]]*)\]\s*\)\s*=>\s*([A-Za-z_$][\w$]*)\s*$/.exec(source)
23
+ if (match === null) return null
24
+
25
+ const slots = match[1]!.split(',').map(slot => slot.trim())
26
+ // Array destructuring may omit trailing elements (`([key]) => key`) while
27
+ // the sequence still has more children. Only an explicit slot can be the
28
+ // result, and an over-long pattern cannot describe this sequence.
29
+ if (slots.length > arity) return null
30
+
31
+ const result = match[2]!
32
+ let selected = -1
33
+ for (let i = 0; i < slots.length; i++) {
34
+ const slot = slots[i]!
35
+ if (slot === '') continue
36
+ if (!/^[A-Za-z_$][\w$]*$/.test(slot)) return null
37
+ if (slot === result) {
38
+ if (selected >= 0) return null
39
+ selected = i
40
+ }
41
+ }
42
+ return selected >= 0 ? selected : null
43
+ }