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
package/src/table/emit.ts CHANGED
@@ -15,8 +15,20 @@ function jsString(s: string): string {
15
15
  return JSON.stringify(s)
16
16
  }
17
17
 
18
- /** Emit a char-class pool in the shorter of the ordinary array and an
19
- * equivalent delimiter-split string. Fall back when no delimiter is absent. */
18
+ /**
19
+ * Emit the char-class pool in the shorter of two equivalent forms.
20
+ *
21
+ * A normal array repeats two quote bytes around every class. Real grammars have
22
+ * dozens of classes, and the recursion-stack first-set correction legitimately
23
+ * discovers more of them. One delimiter-joined string pays for `.split()` once
24
+ * and removes those repeated quotes; the resulting value handed to
25
+ * `expandCompact` is still the exact `string[]` its wire contract declares.
26
+ *
27
+ * Pick a delimiter absent from every encoded class. These strings contain RANGE
28
+ * ENDPOINTS, not regex source, so no character is universally reserved. If this
29
+ * small printable set is exhausted, keep the ordinary array — never escape or
30
+ * reinterpret class data to force the compact form.
31
+ */
20
32
  function emitClassPool(classes: readonly string[]): string {
21
33
  const array = `[${classes.map(jsString).join(',')}]`
22
34
  if (classes.length < 2) return array
@@ -287,6 +299,8 @@ function programFields(prog: TableProgram, fns: readonly string[], opts: EmitOpt
287
299
  // `run({ rootTrivia })` rejected a grammar that plainly has labels — the
288
300
  // exact failure the root-trivia work exists to prevent, one hop downstream.
289
301
  ...(prog.dsp.length === 0 ? [] : [`p:[${prog.dsp.map(emitDispatchSpec).join(',')}],`]),
302
+ ...(prog.lex === undefined ? [] : [`lx:[${prog.lex.map(row => `[${row.join(',')}]`).join(',')}],`]),
303
+ ...(prog.lexPrograms === undefined ? [] : [`lp:[${prog.lexPrograms.map(row => `[${row.join(',')}]`).join(',')}],`]),
290
304
  ...(prog.labels === undefined ? [] : [`lb:[${prog.labels.map(jsString).join(',')}],`]),
291
305
  ...(prog.classified === 1 ? ['rc:1,'] : []),
292
306
  // Without this a recovery table's MODULE loads as a strict one: the extra
@@ -1,9 +1,10 @@
1
1
  import type { AutoNotCheck, Combinator, FirstSet, ParserDef } from '../types.ts'
2
- import { firstSetOf, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
2
+ import { classifyFinalChoice, firstSetOf, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
3
3
  import { childrenOf } from '../analysis/gating.ts'
4
4
  import { getCoreLiteralValue } from '../combinators/choice.ts'
5
5
  import { deriveExpected } from '../combinators/expect.ts'
6
- import { buildReadsState, buildReadsTrivia } from '../compiler/build-arity.ts'
6
+ import { assertionFailureExpected, directTerminalFailureExpected } from '../combinators/expected.ts'
7
+ import { buildReadsRaw, buildReadsState, buildReadsTrivia } from '../compiler/build-arity.ts'
7
8
  import { buildReadsFields, parserHasOwnFields } from '../compiler/fields.ts'
8
9
  import { asciiFoldKey, branchUsesRouted, parserUsesRouted } from '../combinators/dispatch.ts'
9
10
  import {
@@ -13,15 +14,28 @@ import {
13
14
  OP_LIVE, OP_ATTEMPT, OP_LABEL,
14
15
  OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD,
15
16
  OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_COV,
17
+ OP_LEX_BODY, OP_LEX_PROGRAM,
16
18
  } from './ops.ts'
17
19
  import { adjacencyExpected } from '../combinators/adjacency.ts'
18
20
  import { resolveAdjacencyKindMask } from '../cst/trivia-kinds.ts'
19
21
  import { missingInferredType } from '../combinators/node.ts'
20
22
  import { hasOwnTriviaBoundary } from '../combinators/trivia-boundary.ts'
21
23
  import type { BalancedSpec } from '../combinators/scanTo.ts'
22
- import type { DispatchSpec, ScanSpec, SubtreeRef, TableProgram, TriviaSpec } from './program.ts'
24
+ import type { DispatchSpec, LexBodySpec, LexProgramSpec, ScanSpec, SubtreeRef, TableProgram, TriviaSpec } from './program.ts'
25
+ import {
26
+ lexProgramDigest,
27
+ LEX_NODE_CHOICE2, LEX_NODE_CHOICE4, LEX_NODE_CHOICE8,
28
+ LEX_NODE_NOT, LEX_NODE_OPTIONAL,
29
+ LEX_NODE_SEQUENCE2, LEX_NODE_SEQUENCE3, LEX_NODE_SEQUENCE4, LEX_NODE_SEQUENCE5,
30
+ LEX_NODE_TERMINAL,
31
+ } from './lex-program.ts'
23
32
  import { covKindCode, encodeClassSpec, ownTableProgram } from './program.ts'
24
33
  import type { GrammarCoveragePlan } from '../compiler/grammar-coverage-ids.ts'
34
+ import { directArrayProjection } from '../compiler/direct-projection.ts'
35
+ import {
36
+ assertLexicalCapabilityClosure, collectLexicalCapabilities, winnerWrapsReference,
37
+ } from '../compiler/token-capability.ts'
38
+ import { directExecutableTokenBody, type ExecutableLexNode } from '../compiler/token-alphabet.ts'
25
39
 
26
40
  /**
27
41
  * Can `emitConst` print this? Mirrors the guard in `emit.ts` — scalars, arrays
@@ -42,37 +56,6 @@ function emittableConst(v: unknown): boolean {
42
56
  return false
43
57
  }
44
58
 
45
- /**
46
- * Does the rule-map entry `winner` bottom out AT the reference `p`?
47
- *
48
- * The question `Encoder.winners` has to ask before resolving a `g.X` by name. A
49
- * map entry that IS the reference — directly, or under the scope wrappers that
50
- * `rules()` puts there — is not an override to resolve to; it is the reference's
51
- * own binding, and the only thing that reaches the rule's BODY from there is the
52
- * ref's thunk. Resolving by name instead hands back the row already in flight for
53
- * that entry, which is an alias cycle rather than a parser (see `winners`).
54
- *
55
- * ONLY WRAPPERS `rules()` ITSELF INTRODUCES are unwrapped — `parser()`/`noTrivia`
56
- * scopes (`grammar`) and `trivia()`. Both are single-child and both can encode to
57
- * NO ROW, which is what makes the cycle degenerate. Walking further, into a
58
- * `seq()` or a `choice()` arm, would answer a different question: a rule whose
59
- * body legitimately contains a reference to itself is ordinary recursion and must
60
- * keep resolving by name.
61
- */
62
- function wrapsRef(winner: Combinator<unknown>, p: Combinator<unknown>): boolean {
63
- let cur = winner
64
- // Bounded rather than `while (true)`: `_def.parser` is author-supplied and a
65
- // hand-built cycle of scopes would otherwise hang the encoder. Nothing legal
66
- // nests these more than a couple deep.
67
- for (let n = 0; n < 16; n++) {
68
- if (cur === p) return true
69
- const d = cur._def as ParserDef
70
- if (d.tag !== 'grammar' && d.tag !== 'trivia') return false
71
- cur = d.parser
72
- }
73
- return false
74
- }
75
-
76
59
  /** Raised when a construct has no opcode yet. Prototype scope is explicit. */
77
60
  export class UnsupportedConstruct extends Error {
78
61
  readonly tag: string
@@ -154,6 +137,10 @@ class Encoder {
154
137
  fx: (readonly string[])[] = []
155
138
  disp: (readonly number[])[] = []
156
139
  dsp: DispatchSpec[] = []
140
+ lex: LexBodySpec[] = []
141
+ private lexIndex = new Map<string, number>()
142
+ lexPrograms: LexProgramSpec[] = []
143
+ private lexProgramIndex = new Map<string, number>()
157
144
  labels: readonly string[] | undefined = undefined
158
145
  /** Trivia table in scope at the row being encoded — codegen's `ctx.activeTrivia`. */
159
146
  activeTrivia: Combinator<unknown> | undefined = undefined
@@ -207,7 +194,6 @@ class Encoder {
207
194
  winners: Readonly<Record<string, Combinator<unknown>>> | undefined = undefined
208
195
  triviaSpecs: TriviaSpec[] = []
209
196
  private triviaIndex = new Map<Combinator<unknown>, number>()
210
-
211
197
  /**
212
198
  * A trivia combinator as DATA where its shape allows.
213
199
  *
@@ -226,6 +212,160 @@ class Encoder {
226
212
  return idx
227
213
  }
228
214
 
215
+ private lexicalBodySlot(p: Combinator<unknown>): {
216
+ kind: 'body'
217
+ body: number
218
+ expectedFx: number
219
+ suffixFx: number
220
+ lineFlags: number
221
+ } | { kind: 'program'; program: number } | undefined {
222
+ if (!this.selectLexicalBodies) return undefined
223
+ const body = directExecutableTokenBody(p, name => this.winners?.[name])
224
+ if (body === undefined) return undefined
225
+ const tokenDef = p._def
226
+ if (tokenDef.tag !== 'token') throw new Error('parseman: selected lexical body lost token boundary')
227
+ const matcher = (arm: Extract<typeof body, { kind: 'regex-terminal' }>): number => {
228
+ const flags = arm.flags.includes('y') ? arm.flags : `${arm.flags.replace(/g/g, '')}y`
229
+ const regex = this.constant(new RegExp(arm.source, flags))
230
+ const key = `${regex}:-1`
231
+ const prior = this.lexIndex.get(key)
232
+ if (prior !== undefined) return prior
233
+ const id = this.lex.length
234
+ this.lexIndex.set(key, id)
235
+ this.lex.push([regex, -1])
236
+ return id
237
+ }
238
+ if (body.kind === 'balanced-scan') {
239
+ const scan = this.scanSlot({
240
+ kind: 1,
241
+ flags: (body.raw ? 1 : 0) | (body.strict ? 4 : 0),
242
+ skip: body.ownSkip.map(combinator => this.subtree(combinator)),
243
+ open: body.open,
244
+ close: body.close,
245
+ })
246
+ const words = [scan]
247
+ const spec = [3, lexProgramDigest(words), scan] as LexProgramSpec
248
+ const key = spec.join(',')
249
+ const prior = this.lexProgramIndex.get(key)
250
+ if (prior !== undefined) return { kind: 'program', program: prior }
251
+ const id = this.lexPrograms.length
252
+ this.lexProgramIndex.set(key, id)
253
+ this.lexPrograms.push(spec)
254
+ return { kind: 'program', program: id }
255
+ }
256
+ if (body.kind === 'fixed-tree') {
257
+ const words: number[] = []
258
+ const emitNode = (node: ExecutableLexNode): boolean => {
259
+ switch (node.kind) {
260
+ case 'terminal':
261
+ words.push(
262
+ LEX_NODE_TERMINAL,
263
+ matcher(node.terminal),
264
+ this.expected(node.terminal.expected),
265
+ this.track ? 1 : 0,
266
+ )
267
+ return true
268
+ case 'sequence': {
269
+ for (const part of node.parts) if (!emitNode(part)) return false
270
+ const op = node.parts.length === 2 ? LEX_NODE_SEQUENCE2
271
+ : node.parts.length === 3 ? LEX_NODE_SEQUENCE3
272
+ : node.parts.length === 4 ? LEX_NODE_SEQUENCE4
273
+ : node.parts.length === 5 ? LEX_NODE_SEQUENCE5 : -1
274
+ if (op < 0) return false
275
+ words.push(op)
276
+ return true
277
+ }
278
+ case 'choice': {
279
+ for (const arm of node.arms) if (!emitNode(arm)) return false
280
+ const op = node.arms.length === 2 ? LEX_NODE_CHOICE2
281
+ : node.arms.length === 4 ? LEX_NODE_CHOICE4
282
+ : node.arms.length === 8 ? LEX_NODE_CHOICE8 : -1
283
+ if (op < 0) return false
284
+ words.push(op, this.expected(node.expected))
285
+ for (let i = 0; i < node.arms.length; i++) {
286
+ const first = node.firstSets[i]!
287
+ words.push(first.kind === 'ranges' ? this.charClass(first) : -1)
288
+ words.push(this.expected(node.armExpected[i]!))
289
+ }
290
+ return true
291
+ }
292
+ case 'not':
293
+ if (!emitNode(node.body)) return false
294
+ words.push(LEX_NODE_NOT, this.expected(node.expected))
295
+ return true
296
+ case 'optional':
297
+ if (!emitNode(node.body)) return false
298
+ words.push(LEX_NODE_OPTIONAL)
299
+ return true
300
+ }
301
+ }
302
+ if (!emitNode(body.root)) return undefined
303
+ const spec = [2, lexProgramDigest(words), ...words] as LexProgramSpec
304
+ const key = spec.join(',')
305
+ const prior = this.lexProgramIndex.get(key)
306
+ if (prior !== undefined) return { kind: 'program', program: prior }
307
+ const id = this.lexPrograms.length
308
+ this.lexProgramIndex.set(key, id)
309
+ this.lexPrograms.push(spec)
310
+ return { kind: 'program', program: id }
311
+ }
312
+ if (body.kind === 'not2-ordered4-terminal') {
313
+ const words = [
314
+ ...body.guard0.map(matcher), this.expected(body.guard0Expected),
315
+ matcher(body.guard1), this.expected(body.guard1Expected),
316
+ matcher(body.terminal), this.expected(body.terminalExpected),
317
+ this.track ? 1 : 0,
318
+ ]
319
+ const spec = [1, lexProgramDigest(words), ...words] as unknown as LexProgramSpec
320
+ const key = spec.join(',')
321
+ const prior = this.lexProgramIndex.get(key)
322
+ if (prior !== undefined) return { kind: 'program', program: prior }
323
+ const id = this.lexPrograms.length
324
+ this.lexProgramIndex.set(key, id)
325
+ this.lexPrograms.push(spec)
326
+ return { kind: 'program', program: id }
327
+ }
328
+ if (body.kind === 'ordered4-terminals') {
329
+ const commonClass = this.charClass(body.commonFirstSet)
330
+ if (commonClass < 0) return undefined
331
+ const words: number[] = [commonClass, this.expected(body.commonMissExpected)]
332
+ for (const arm of body.arms) {
333
+ words.push(matcher(arm), this.expected(arm.expected))
334
+ }
335
+ words.push(this.track ? 1 : 0)
336
+ const spec = [0, lexProgramDigest(words), ...words] as unknown as LexProgramSpec
337
+ const key = spec.join(',')
338
+ const prior = this.lexProgramIndex.get(key)
339
+ if (prior !== undefined) return { kind: 'program', program: prior }
340
+ const id = this.lexPrograms.length
341
+ this.lexProgramIndex.set(key, id)
342
+ this.lexPrograms.push(spec)
343
+ return { kind: 'program', program: id }
344
+ }
345
+ const expectedFx = this.expected(body.expected)
346
+ const flags = body.flags.includes('y') ? body.flags : `${body.flags.replace(/g/g, '')}y`
347
+ const regex = this.constant(new RegExp(body.source, flags))
348
+ const suffix = body.kind === 'regex-terminal' ? -1 : body.suffix.charCodeAt(0)
349
+ const key = `${regex}:${suffix}`
350
+ const prior = this.lexIndex.get(key)
351
+ const suffixFx = this.expected(body.kind === 'regex-terminal' ? [] : directTerminalFailureExpected({
352
+ tag: 'literal', value: body.suffix, caseInsensitive: false,
353
+ }))
354
+ // Tracked terminal rows advance the high-water mark even when their static
355
+ // language cannot contain a newline. Otherwise a preceding newline stays
356
+ // behind an incorrectly advanced boundary and the next CST node receives a
357
+ // stale line. The suffix advances only when it actually matched (reader bit
358
+ // 2), exactly like the authored optional literal.
359
+ const lineFlags = this.track
360
+ ? (body.kind === 'regex-terminal' ? 1 : 1 | 2 | 4)
361
+ : body.kind === 'regex-terminal' ? 0 : 4
362
+ if (prior !== undefined) return { kind: 'body', body: prior, expectedFx, suffixFx, lineFlags }
363
+ const id = this.lex.length
364
+ this.lexIndex.set(key, id)
365
+ this.lex.push([regex, suffix])
366
+ return { kind: 'body', body: id, expectedFx, suffixFx, lineFlags }
367
+ }
368
+
229
369
  private triviaSpecOf(t: Combinator<unknown>): TriviaSpec {
230
370
  const unlowered = (why: string): TriviaSpec => {
231
371
  this.runtimeOnly.add(`rules({ trivia }) — ${why}`)
@@ -345,6 +485,9 @@ class Encoder {
345
485
  readonly settings: TableSettings
346
486
  /** Resolved once, HERE, at table-build time — never consulted at run time. */
347
487
  readonly track: boolean
488
+ /** TOKEN bodies are an all-program decision. A complete occurrence inside an
489
+ * otherwise incomplete final graph stays on CHARACTER with every sibling. */
490
+ readonly selectLexicalBodies: boolean
348
491
  /**
349
492
  * Decides the extra operands laid down below. ALWAYS TRUE — see
350
493
  * `TableSettings.recovery`. Kept as a field rather than folded away because it
@@ -352,9 +495,10 @@ class Encoder {
352
495
  * row is stated.
353
496
  */
354
497
  readonly rec: boolean
355
- constructor(settings: TableSettings) {
498
+ constructor(settings: TableSettings, selectLexicalBodies: boolean) {
356
499
  this.settings = settings
357
500
  this.track = settings.trackLines === true
501
+ this.selectLexicalBodies = selectLexicalBodies
358
502
  this.rec = true
359
503
  this.plan = settings.coverage
360
504
  this.cov = settings.coverage === undefined
@@ -483,6 +627,16 @@ class Encoder {
483
627
  return i
484
628
  }
485
629
 
630
+ /** Bind a guard only when its class is already part of the program. Repeat
631
+ * gating must not grow the compact artifact merely to avoid a failed call. */
632
+ private existingCharClass(fs: FirstSet): number {
633
+ if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
634
+ const hit = this.ccIndex.get(encodeClassSpec(fs.ranges))
635
+ // This reuses the recovery row's existing `-1` slot. Keep its decimal width
636
+ // at two bytes or less so a speed hint cannot grow a compact artifact.
637
+ return hit !== undefined && hit < 100 ? hit : -1
638
+ }
639
+
486
640
  /**
487
641
  * Wrap one choice arm in its `autoNot` checks (`OP_REJECT`).
488
642
  *
@@ -618,7 +772,7 @@ class Encoder {
618
772
  * in the target rule's own ambient trivia when that differs from the trivia
619
773
  * lexically active at the reference.
620
774
  *
621
- * `encodeRule` wraps a rule ENTRY in `OP_SCOPE`, but only the entry: a `g.X`
775
+ * `encodeRule` wraps a rule ENTRY in `OP_SCOPE_PLAIN`, but only the entry: a `g.X`
622
776
  * reference resolves straight to the body's offset and jumps past it (the note
623
777
  * at `encodeRule`). So a rule referenced from inside a `noTrivia(...)` region ran
624
778
  * with NO ambient trivia of its own — the table scoped trivia DYNAMICALLY, by
@@ -713,6 +867,20 @@ class Encoder {
713
867
 
714
868
  private encodeDef(p: Combinator<unknown>): number {
715
869
  const d = p._def as ParserDef
870
+ // `balanced()` returns a token carrying `_balancedSpec` on that same
871
+ // object. Give a complete selected token shell first refusal; otherwise the
872
+ // generic scan row below would permanently shadow it before the `token`
873
+ // switch is reached.
874
+ if (d.tag === 'token') {
875
+ const selected = this.lexicalBodySlot(p)
876
+ if (selected !== undefined) {
877
+ if (selected.kind === 'program') return this.emit(OP_LEX_PROGRAM, selected.program)
878
+ return this.emit(
879
+ OP_LEX_BODY, selected.body, selected.expectedFx,
880
+ selected.suffixFx, selected.lineFlags,
881
+ )
882
+ }
883
+ }
716
884
  // THE SCANNING CONSTRUCTS, as their constructor arguments.
717
885
  //
718
886
  // Neither is recoverable from `_def` alone — `balanced()` overrides `.parse`
@@ -746,13 +914,13 @@ class Encoder {
746
914
  switch (d.tag) {
747
915
  case 'literal': {
748
916
  if (d.caseInsensitive) {
749
- return this.emit(this.track ? OP_LIT_CI_TRACK : OP_LIT_CI, this.constant(d.value), this.expected(deriveExpected(p)))
917
+ return this.emit(this.track ? OP_LIT_CI_TRACK : OP_LIT_CI, this.constant(d.value), this.expected(directTerminalFailureExpected(d)))
750
918
  }
751
- return this.emit(this.track ? OP_LIT_TRACK : OP_LIT, this.constant(d.value), this.expected(deriveExpected(p)))
919
+ return this.emit(this.track ? OP_LIT_TRACK : OP_LIT, this.constant(d.value), this.expected(directTerminalFailureExpected(d)))
752
920
  }
753
921
  case 'regex': {
754
922
  const flags = d.flags.includes('y') ? d.flags : `${d.flags}y`
755
- return this.emit(this.track ? OP_RX_TRACK : OP_RX, this.constant(new RegExp(d.source, flags)), this.expected(deriveExpected(p)))
923
+ return this.emit(this.track ? OP_RX_TRACK : OP_RX, this.constant(new RegExp(d.source, flags)), this.expected(directTerminalFailureExpected(d)))
756
924
  }
757
925
  case 'keywords': {
758
926
  // `keywords()` compiles to ONE sticky regex and pushes a leaf — which is
@@ -770,7 +938,7 @@ class Encoder {
770
938
  // failing keyword arm names the CATEGORY rather than reciting every
771
939
  // literal in the family. `deriveExpected` reports the literals, so the
772
940
  // table said `'"media"'` where both other engines say `'keyword'`.
773
- this.expected(['keyword']),
941
+ this.expected(directTerminalFailureExpected(d)),
774
942
  )
775
943
  }
776
944
  case 'expect': {
@@ -875,6 +1043,7 @@ class Encoder {
875
1043
  // covered on a parse where the choice picked somebody else.
876
1044
  return this.covWrap(gated, armCovIds?.[src], 1)
877
1045
  })
1046
+ const rr = this.refResolver()
878
1047
  // O(1) first-char dispatch is only SOUND when the arms are disjoint.
879
1048
  // With overlapping first sets, "the first arm whose class contains this
880
1049
  // char" is not "the first arm that matches": a `Keyword` arm whose first
@@ -904,24 +1073,21 @@ class Encoder {
904
1073
  // the O(1) table (`exclusive`) or falls to the ordered per-arm path.
905
1074
  // Arm ORDER is preserved on both, which is what makes this a PEG-safe
906
1075
  // change rather than a reordering.
907
- const rr = this.refResolver()
908
- const classes = arms.map(a => matchesEmpty(a, new Set(), rr) ? -1 : this.charClass(firstSetOf(a, new Set(), rr)))
1076
+ const finalChoice = classifyFinalChoice(arms, rr)
1077
+ const classes = arms.map((_, i) => finalChoice.nullable[i]
1078
+ ? -1 : this.charClass(finalChoice.firstSets[i]!))
909
1079
  const dispIdx = this.disp.length
910
1080
  this.disp.push(classes)
911
- // PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. The ordered
912
- // path reports the CONCATENATION of the arms' sets (`choice.ts:167`), and
913
- // it must include the arms the driver's char gate declined to enter — the
914
- // interpreter has no such gate, runs them, and gets exactly their static
915
- // opener set back. Without these words a partially-gated site reported
916
- // only the arms that happened to run.
1081
+ // PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. While the best
1082
+ // failure remains at the choice position, an arm declined by the driver's
1083
+ // char gate contributes the static opener set the interpreter would get by
1084
+ // entering it. A deeper dynamic arm failure discards those shallower sets.
917
1085
  const head = this.emitHead(OP_CHOICE, 3 + 2 * kids.length)
918
1086
  this.code[head + 1] = dispIdx
919
1087
  this.code[head + 2] = kids.length
920
1088
  // THE CHOICE'S OWN SET IS THE ARMS' FLATMAP, NOT `deriveExpected(p)`.
921
- // `deriveExpected` DEDUPES, and neither engine does: `choice.ts:110-118`
922
- // is `parsers.flatMap(p => p.parse(...).expected)`, so a shared-prefix
923
- // choice whose two arms both open with `/::?/` reports it TWICE. The
924
- // deduped union silently collapsed that to one.
1089
+ // It is the pure-dispatch-miss and empty-dynamic fallback, where source
1090
+ // order and duplicates remain observable. `deriveExpected` would dedupe.
925
1091
  this.code[head + 3] = this.expected(arms.flatMap(a => deriveExpected(a)))
926
1092
  for (let i = 0; i < kids.length; i++) {
927
1093
  this.code[head + 4 + i] = kids[i]!
@@ -932,6 +1098,10 @@ class Encoder {
932
1098
  case 'many':
933
1099
  case 'oneOrMore': {
934
1100
  const child = this.node(d.parser).ip
1101
+ const rr = this.refResolver()
1102
+ const itemClass = matchesEmpty(d.parser, new Set(), rr)
1103
+ ? -1
1104
+ : this.existingCharClass(firstSetOf(d.parser, new Set(), rr))
935
1105
  const op = d.valueUnused ? OP_REPV : OP_REP
936
1106
  const min = d.tag === 'many' ? 0 : d.min
937
1107
  // ip + 6 is the ITEM's expected set and ip + 7 the separator's sentinel
@@ -940,7 +1110,7 @@ class Encoder {
940
1110
  // set — `deriveExpected(combinator)` in repeat.ts:170, and
941
1111
  // `deriveExpectedArr([def.parser])` in codegen's emitMany.
942
1112
  return this.rec
943
- ? this.emit(op, child, min, d.max ?? -1, -1, 0, this.expected(deriveExpected(d.parser)), -1)
1113
+ ? this.emit(op, child, min, d.max ?? -1, -1, 0, this.expected(deriveExpected(d.parser)), itemClass)
944
1114
  : this.emit(op, child, min, d.max ?? -1, -1, 0)
945
1115
  }
946
1116
  case 'sepBy': {
@@ -987,7 +1157,14 @@ class Encoder {
987
1157
  if (inner.tag === 'sequence' && !this.memo.has(d.parser) && !this.pending.has(d.parser)) {
988
1158
  const kids = inner.parsers.map(c => this.node(c).ip)
989
1159
  const head = this.emitHead(OP_SEQX, 2 + kids.length + (this.rec ? kids.length : 0))
990
- this.code[head + 1] = this.fn(d.fn, d.fnSrc ?? null)
1160
+ const projection = directArrayProjection(d.fn, d.fnSrc, kids.length)
1161
+ // A negative operand describes an already-parsed child (`~index`),
1162
+ // while every function-pool index remains non-negative. The row is
1163
+ // otherwise unchanged, so projection fusion costs no table words and
1164
+ // does not create a second closure/factory shape.
1165
+ this.code[head + 1] = projection === null
1166
+ ? this.fn(d.fn, d.fnSrc ?? null)
1167
+ : ~projection
991
1168
  this.code[head + 2] = kids.length
992
1169
  for (let i = 0; i < kids.length; i++) this.code[head + 3 + i] = kids[i]!
993
1170
  if (this.rec) {
@@ -1046,12 +1223,14 @@ class Encoder {
1046
1223
  // a `project` replaces the value outright.
1047
1224
  const noBuildCaptures = d.project === undefined
1048
1225
  const derivedTrivia = d.build !== undefined ? buildReadsTrivia(d) : noBuildCaptures
1226
+ const omitsRaw = d.build !== undefined && !buildReadsRaw(d)
1049
1227
  const derivedState = d.build !== undefined ? buildReadsState(d) : noBuildCaptures
1050
1228
  const derivedFields = d.build !== undefined ? buildReadsFields(d) : noBuildCaptures
1051
1229
  // Field capture additionally requires the body to CONTAIN `field()`
1052
1230
  // captures: a node that reads fields but has none allocates nothing.
1053
1231
  const wantsFields = parserHasOwnFields(d.parser) && (cstOut || derivedFields)
1054
1232
  const flags = (cstOut || d.captureTrivia === true || d.trailingTrivia === true || derivedTrivia ? 4 : 0)
1233
+ | (!cstOut && omitsRaw ? 2 : 0)
1055
1234
  | (cstOut || derivedState ? 8 : 0)
1056
1235
  | (wantsFields ? 16 : 0)
1057
1236
  | (d.collapse === true ? 32 : 0)
@@ -1091,8 +1270,9 @@ class Encoder {
1091
1270
  const e = deriveExpected(d.parser)
1092
1271
  return this.emit(OP_GATE, cls, body, this.expected(e.length > 0 ? e : [d.parser._tag]))
1093
1272
  }
1094
- case 'token':
1273
+ case 'token': {
1095
1274
  return this.emit(OP_TOKEN, this.node(d.parser).ip)
1275
+ }
1096
1276
  case 'routed':
1097
1277
  return this.emit(OP_ROUTED, d.fallback === undefined ? -1 : this.node(d.fallback).ip)
1098
1278
  case 'dispatch': {
@@ -1204,7 +1384,7 @@ class Encoder {
1204
1384
  // merged (composed) map, and why the identity guard is required.
1205
1385
  const refName = (p as unknown as { _ruleName?: string })._ruleName
1206
1386
  const winner = refName === undefined ? undefined : this.winners?.[refName]
1207
- if (winner !== undefined && !wrapsRef(winner, p)) return this.scopedRef(p, winner)
1387
+ if (winner !== undefined && !winnerWrapsReference(winner, p)) return this.scopedRef(p, winner)
1208
1388
  let resolved: Combinator<unknown>
1209
1389
  try { resolved = d.thunk() }
1210
1390
  catch {
@@ -1219,7 +1399,7 @@ class Encoder {
1219
1399
  // other engine reported `['not(literal)']`. It hid because `expected` is a
1220
1400
  // TOP-LEVEL field on `RunResult` and is NOT part of the identity digest.
1221
1401
  case 'not':
1222
- return this.emit(OP_NOT, this.node(d.parser).ip, this.expected([`not(${d.parser._tag})`]))
1402
+ return this.emit(OP_NOT, this.node(d.parser).ip, this.expected(assertionFailureExpected(false, d.parser._tag)))
1223
1403
  case 'guard':
1224
1404
  // `'gate'`, the public name — see gate.ts. The def tag stays `'guard'`.
1225
1405
  return this.emit(OP_GUARD, this.fn(d.predicate, d.predSrc ?? null), this.expected(['gate']))
@@ -1259,7 +1439,7 @@ class Encoder {
1259
1439
  // lookahead's failure is "the guard did not hold", and naming the body's
1260
1440
  // internals offers a token the parse never asked the author for. The
1261
1441
  // table propagated the body's set instead.
1262
- return this.emit(OP_PEEK, this.node(d.parser).ip, this.expected([`peek(${d.parser._tag})`]))
1442
+ return this.emit(OP_PEEK, this.node(d.parser).ip, this.expected(assertionFailureExpected(true, d.parser._tag)))
1263
1443
  // A TRANSACTION IS A ROW. See `OP_ATTEMPT` for why the transparent
1264
1444
  // lowering was correct only for a choice arm.
1265
1445
  case 'attempt': {
@@ -1458,6 +1638,8 @@ class Encoder {
1458
1638
  return ownTableProgram({
1459
1639
  code: this.code, k: this.k, fns: this.fns, cc: this.cc,
1460
1640
  fx: this.fx, disp: this.disp, dsp: this.dsp, rules: this.rules,
1641
+ ...(this.lex.length === 0 ? {} : { lex: this.lex }),
1642
+ ...(this.lexPrograms.length === 0 ? {} : { lexPrograms: this.lexPrograms }),
1461
1643
  ...(this.labels === undefined ? {} : { labels: this.labels }),
1462
1644
  ...(this.classified ? { classified: 1 as const } : {}),
1463
1645
  ...(this.scanSkipSets.length === 0
@@ -1561,7 +1743,13 @@ export function encodeTableProgram(
1561
1743
  ...(hostMode === undefined ? {} : { hostMode }),
1562
1744
  ...(track ? { trackLines: true } : {}),
1563
1745
  }
1564
- const enc = new Encoder(resolvedSettings)
1746
+ // Collect once from the FINAL winner-resolved graph. Sorting affects only
1747
+ // content-id discovery; ordinary rule/code order remains the caller's order.
1748
+ const lexicalRoots = [...names].sort().map(name => ruleMap[name]!)
1749
+ const resolveLexical = (name: string): Combinator<unknown> | undefined => ruleMap[name]
1750
+ const capabilities = collectLexicalCapabilities(lexicalRoots, resolveLexical)
1751
+ assertLexicalCapabilityClosure(lexicalRoots, capabilities, resolveLexical)
1752
+ const enc = new Encoder(resolvedSettings, capabilities.capabilityComplete)
1565
1753
  enc.winners = ruleMap
1566
1754
  for (const name of names) enc.encodeRule(name, ruleMap[name]!)
1567
1755
  const prog = enc.finish()
@@ -1,5 +1,5 @@
1
1
  import type { ParseContext, ParseResult } from '../types.ts'
2
- import { advanceTrivia, needsDeferredTriviaCommit, rollbackTrivia, saveTriviaMark, scanTrivia } from '../combinators/trivia-skip.ts'
2
+ import { advanceTrivia, commitTriviaScan, needsDeferredTriviaCommit, rollbackTrivia, saveTriviaMark, scanTriviaCompact } from '../combinators/trivia-skip.ts'
3
3
  import {
4
4
  beginCstNodeCapture, cstCaptureActive, endCstNodeCapture, pushCstChild,
5
5
  pushCstLeaf, rollbackCstCapture, saveCstMark, type CstRollbackMark,
@@ -142,9 +142,7 @@ function makeDriver(
142
142
  */
143
143
  function skipTrivia(input: string, cur: number, ctx: ParseContext): number {
144
144
  if (needsDeferredTriviaCommit(ctx)) {
145
- const scan = scanTrivia(input, cur, ctx)
146
- scan.commit()
147
- return scan.end
145
+ return commitTriviaScan(scanTriviaCompact(input, cur, ctx))
148
146
  }
149
147
  return advanceTrivia(input, cur, ctx)
150
148
  }
@@ -256,9 +254,7 @@ function makeDriver(
256
254
  const mark = rollbackNeeded(ctx) ? saveTriviaMark(ctx) : null
257
255
  let scanEnd: number
258
256
  if (needsDeferredTriviaCommit(ctx)) {
259
- const scan = scanTrivia(input, cur, ctx)
260
- scan.commit()
261
- scanEnd = scan.end
257
+ scanEnd = commitTriviaScan(scanTriviaCompact(input, cur, ctx))
262
258
  } else {
263
259
  scanEnd = advanceTrivia(input, cur, ctx)
264
260
  }