parseman 0.47.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +27 -21
  2. package/dist/analysis/diagnostics.js +2955 -357
  3. package/dist/analysis/diagnostics.js.map +3 -3
  4. package/dist/cli/index.js +2946 -348
  5. package/dist/cli/index.js.map +3 -3
  6. package/dist/combinators/expect.d.ts +1 -0
  7. package/dist/combinators/expect.d.ts.map +1 -1
  8. package/dist/combinators/expected.d.ts +8 -0
  9. package/dist/combinators/expected.d.ts.map +1 -0
  10. package/dist/combinators/first-set.d.ts +8 -0
  11. package/dist/combinators/first-set.d.ts.map +1 -1
  12. package/dist/combinators/grammar.d.ts.map +1 -1
  13. package/dist/combinators/keywords.d.ts +4 -0
  14. package/dist/combinators/keywords.d.ts.map +1 -1
  15. package/dist/combinators/literal.d.ts.map +1 -1
  16. package/dist/combinators/not.d.ts.map +1 -1
  17. package/dist/combinators/peek.d.ts.map +1 -1
  18. package/dist/combinators/regex.d.ts.map +1 -1
  19. package/dist/combinators/repeat.d.ts.map +1 -1
  20. package/dist/combinators/sequence.d.ts.map +1 -1
  21. package/dist/combinators/trivia-skip.d.ts +19 -1
  22. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  23. package/dist/compiler/direct-projection.d.ts +12 -0
  24. package/dist/compiler/direct-projection.d.ts.map +1 -0
  25. package/dist/compiler/token-alphabet.d.ts +845 -5
  26. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  27. package/dist/compiler/token-capability.d.ts +3 -0
  28. package/dist/compiler/token-capability.d.ts.map +1 -0
  29. package/dist/compiler/token-capability.js +3192 -0
  30. package/dist/compiler/token-capability.js.map +6 -0
  31. package/dist/cst/trivia-charscan.d.ts +3 -0
  32. package/dist/cst/trivia-charscan.d.ts.map +1 -1
  33. package/dist/cst/trivia-css-scanner.d.ts +5 -0
  34. package/dist/cst/trivia-css-scanner.d.ts.map +1 -0
  35. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  36. package/dist/index.js +2959 -348
  37. package/dist/index.js.map +3 -3
  38. package/dist/language-service/index.js +7 -1
  39. package/dist/language-service/index.js.map +1 -1
  40. package/dist/plugin/index.js +2969 -358
  41. package/dist/plugin/index.js.map +3 -3
  42. package/dist/spec/index.js +366 -49
  43. package/dist/spec/index.js.map +2 -2
  44. package/dist/table/assemble.d.ts.map +1 -1
  45. package/dist/table/child-slots.d.ts.map +1 -1
  46. package/dist/table/compile-rule-map.d.ts +3 -1
  47. package/dist/table/compile-rule-map.d.ts.map +1 -1
  48. package/dist/table/emit-assembly.d.ts +13 -14
  49. package/dist/table/emit-assembly.d.ts.map +1 -1
  50. package/dist/table/emit.d.ts.map +1 -1
  51. package/dist/table/encode.d.ts.map +1 -1
  52. package/dist/table/exec-baseline.d.ts.map +1 -1
  53. package/dist/table/exec.d.ts.map +1 -1
  54. package/dist/table/index.js +4839 -2232
  55. package/dist/table/index.js.map +3 -3
  56. package/dist/table/lex-body.d.ts +9 -0
  57. package/dist/table/lex-body.d.ts.map +1 -0
  58. package/dist/table/lex-program.d.ts +25 -0
  59. package/dist/table/lex-program.d.ts.map +1 -0
  60. package/dist/table/ops.d.ts +47 -4
  61. package/dist/table/ops.d.ts.map +1 -1
  62. package/dist/table/program.d.ts +53 -2
  63. package/dist/table/program.d.ts.map +1 -1
  64. package/dist/table/scalar-terminal.d.ts +13 -0
  65. package/dist/table/scalar-terminal.d.ts.map +1 -0
  66. package/dist/table/site-labels.d.ts.map +1 -1
  67. package/dist/types.d.ts +2 -0
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/version.d.ts +1 -1
  70. package/package.json +12 -11
  71. package/src/combinators/expect.ts +1 -0
  72. package/src/combinators/expected.ts +17 -0
  73. package/src/combinators/first-set.ts +82 -3
  74. package/src/combinators/grammar.ts +8 -3
  75. package/src/combinators/keywords.ts +22 -6
  76. package/src/combinators/literal.ts +2 -1
  77. package/src/combinators/not.ts +5 -1
  78. package/src/combinators/peek.ts +5 -1
  79. package/src/combinators/regex.ts +5 -3
  80. package/src/combinators/repeat.ts +4 -10
  81. package/src/combinators/sequence.ts +35 -16
  82. package/src/combinators/trivia-skip.ts +75 -11
  83. package/src/compiler/direct-projection.ts +43 -0
  84. package/src/compiler/token-alphabet.ts +4316 -12
  85. package/src/compiler/token-capability.ts +6 -0
  86. package/src/cst/trivia-charscan.ts +34 -0
  87. package/src/cst/trivia-css-scanner.ts +142 -0
  88. package/src/cst/trivia-kinds.ts +3 -1
  89. package/src/plugin/index.ts +4 -4
  90. package/src/table/assemble.ts +1224 -222
  91. package/src/table/child-slots.ts +4 -3
  92. package/src/table/compile-rule-map.ts +21 -3
  93. package/src/table/emit-assembly.ts +363 -109
  94. package/src/table/emit.ts +26 -1
  95. package/src/table/encode.ts +246 -54
  96. package/src/table/exec-baseline.ts +3 -7
  97. package/src/table/exec.ts +140 -18
  98. package/src/table/lex-body.ts +30 -0
  99. package/src/table/lex-program.ts +422 -0
  100. package/src/table/ops.ts +50 -4
  101. package/src/table/program.ts +67 -2
  102. package/src/table/scalar-terminal.ts +51 -0
  103. package/src/table/site-labels.ts +13 -3
  104. package/src/table/stamp.ts +3 -3
  105. package/src/types.ts +1 -1
  106. package/src/version.ts +1 -1
  107. package/dist/analysis/diagnostics.cjs +0 -13736
  108. package/dist/analysis/diagnostics.cjs.map +0 -6
  109. package/dist/cli/index.cjs +0 -14837
  110. package/dist/cli/index.cjs.map +0 -6
  111. package/dist/index.cjs +0 -16303
  112. package/dist/index.cjs.map +0 -6
  113. package/dist/language-service/index.cjs +0 -1302
  114. package/dist/language-service/index.cjs.map +0 -6
  115. package/dist/oracle/index.cjs +0 -254
  116. package/dist/oracle/index.cjs.map +0 -6
  117. package/dist/plugin/index.cjs +0 -16009
  118. package/dist/plugin/index.cjs.map +0 -6
  119. package/dist/run/index.cjs +0 -577
  120. package/dist/run/index.cjs.map +0 -6
  121. package/dist/spec/index.cjs +0 -6188
  122. package/dist/spec/index.cjs.map +0 -6
  123. package/dist/table/index.cjs +0 -11375
  124. package/dist/table/index.cjs.map +0 -6
package/src/table/emit.ts CHANGED
@@ -15,6 +15,29 @@ function jsString(s: string): string {
15
15
  return JSON.stringify(s)
16
16
  }
17
17
 
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
+ */
32
+ function emitClassPool(classes: readonly string[]): string {
33
+ const array = `[${classes.map(jsString).join(',')}]`
34
+ if (classes.length < 2) return array
35
+ const delimiter = '|/~?^`'.split('').find(ch => classes.every(spec => !spec.includes(ch)))
36
+ if (delimiter === undefined) return array
37
+ const split = `${jsString(classes.join(delimiter))}.split(${jsString(delimiter)})`
38
+ return split.length < array.length ? split : array
39
+ }
40
+
18
41
  /**
19
42
  * Serialise one const-pool entry, or REFUSE it.
20
43
  *
@@ -264,7 +287,7 @@ function programFields(prog: TableProgram, fns: readonly string[], opts: EmitOpt
264
287
  ...emitAssemblies(prog, opts.assemblies ?? []),
265
288
  `c:[${prog.code.join(',')}],`,
266
289
  `k:[${prog.k.map(emitConst).join(',')}],`,
267
- `x:[${prog.cc.map(jsString).join(',')}],`,
290
+ `x:${emitClassPool(prog.cc)},`,
268
291
  `e:[${prog.fx.map(f => `[${f.map(jsString).join(',')}]`).join(',')}],`,
269
292
  `d:[${prog.disp.map(a => `[${a.join(',')}]`).join(',')}],`,
270
293
  `r:${JSON.stringify(prog.rules)},`,
@@ -276,6 +299,8 @@ function programFields(prog: TableProgram, fns: readonly string[], opts: EmitOpt
276
299
  // `run({ rootTrivia })` rejected a grammar that plainly has labels — the
277
300
  // exact failure the root-trivia work exists to prevent, one hop downstream.
278
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(',')}],`]),
279
304
  ...(prog.labels === undefined ? [] : [`lb:[${prog.labels.map(jsString).join(',')}],`]),
280
305
  ...(prog.classified === 1 ? ['rc:1,'] : []),
281
306
  // Without this a recovery table's MODULE loads as a strict one: the extra
@@ -1,27 +1,41 @@
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 {
10
11
  OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
11
12
  OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SEQ, OP_SEQV, OP_XFORM,
12
- OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_EXPECT, OP_SEQX, OP_SCAN,
13
+ OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_EXPECT, OP_SEQX, OP_SCAN,
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
  *
@@ -610,7 +764,7 @@ class Encoder {
610
764
  // straight to the body's offset (`case 'lazy'`), so every recursive call and
611
765
  // every cross-rule reference would jump past the counter, and a grammar with
612
766
  // one entry would report one hit rule however much of it ran.
613
- this.rules[name] = amb === undefined ? body : this.emit(OP_SCOPE, this.triviaSlot(amb), body)
767
+ this.rules[name] = amb === undefined ? body : this.emit(OP_SCOPE_PLAIN, this.triviaSlot(amb), body)
614
768
  }
615
769
 
616
770
  /**
@@ -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
@@ -656,7 +810,7 @@ class Encoder {
656
810
  const amb = p._meta.grammarTrivia ?? target._meta.grammarTrivia
657
811
  if (amb === undefined || this.activeTrivia !== undefined) return ip
658
812
  if (!hasOwnTriviaBoundary(target)) return ip
659
- return this.emit(OP_SCOPE, this.triviaSlot(amb), ip)
813
+ return this.emit(OP_SCOPE_PLAIN, this.triviaSlot(amb), ip)
660
814
  }
661
815
 
662
816
  node(p: Combinator<unknown>): Emitted {
@@ -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,8 +1073,9 @@ 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
1081
  // PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. The ordered
@@ -932,6 +1102,10 @@ class Encoder {
932
1102
  case 'many':
933
1103
  case 'oneOrMore': {
934
1104
  const child = this.node(d.parser).ip
1105
+ const rr = this.refResolver()
1106
+ const itemClass = matchesEmpty(d.parser, new Set(), rr)
1107
+ ? -1
1108
+ : this.existingCharClass(firstSetOf(d.parser, new Set(), rr))
935
1109
  const op = d.valueUnused ? OP_REPV : OP_REP
936
1110
  const min = d.tag === 'many' ? 0 : d.min
937
1111
  // ip + 6 is the ITEM's expected set and ip + 7 the separator's sentinel
@@ -940,7 +1114,7 @@ class Encoder {
940
1114
  // set — `deriveExpected(combinator)` in repeat.ts:170, and
941
1115
  // `deriveExpectedArr([def.parser])` in codegen's emitMany.
942
1116
  return this.rec
943
- ? this.emit(op, child, min, d.max ?? -1, -1, 0, this.expected(deriveExpected(d.parser)), -1)
1117
+ ? this.emit(op, child, min, d.max ?? -1, -1, 0, this.expected(deriveExpected(d.parser)), itemClass)
944
1118
  : this.emit(op, child, min, d.max ?? -1, -1, 0)
945
1119
  }
946
1120
  case 'sepBy': {
@@ -987,7 +1161,14 @@ class Encoder {
987
1161
  if (inner.tag === 'sequence' && !this.memo.has(d.parser) && !this.pending.has(d.parser)) {
988
1162
  const kids = inner.parsers.map(c => this.node(c).ip)
989
1163
  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)
1164
+ const projection = directArrayProjection(d.fn, d.fnSrc, kids.length)
1165
+ // A negative operand describes an already-parsed child (`~index`),
1166
+ // while every function-pool index remains non-negative. The row is
1167
+ // otherwise unchanged, so projection fusion costs no table words and
1168
+ // does not create a second closure/factory shape.
1169
+ this.code[head + 1] = projection === null
1170
+ ? this.fn(d.fn, d.fnSrc ?? null)
1171
+ : ~projection
991
1172
  this.code[head + 2] = kids.length
992
1173
  for (let i = 0; i < kids.length; i++) this.code[head + 3 + i] = kids[i]!
993
1174
  if (this.rec) {
@@ -1046,12 +1227,14 @@ class Encoder {
1046
1227
  // a `project` replaces the value outright.
1047
1228
  const noBuildCaptures = d.project === undefined
1048
1229
  const derivedTrivia = d.build !== undefined ? buildReadsTrivia(d) : noBuildCaptures
1230
+ const omitsRaw = d.build !== undefined && !buildReadsRaw(d)
1049
1231
  const derivedState = d.build !== undefined ? buildReadsState(d) : noBuildCaptures
1050
1232
  const derivedFields = d.build !== undefined ? buildReadsFields(d) : noBuildCaptures
1051
1233
  // Field capture additionally requires the body to CONTAIN `field()`
1052
1234
  // captures: a node that reads fields but has none allocates nothing.
1053
1235
  const wantsFields = parserHasOwnFields(d.parser) && (cstOut || derivedFields)
1054
1236
  const flags = (cstOut || d.captureTrivia === true || d.trailingTrivia === true || derivedTrivia ? 4 : 0)
1237
+ | (!cstOut && omitsRaw ? 2 : 0)
1055
1238
  | (cstOut || derivedState ? 8 : 0)
1056
1239
  | (wantsFields ? 16 : 0)
1057
1240
  | (d.collapse === true ? 32 : 0)
@@ -1091,8 +1274,9 @@ class Encoder {
1091
1274
  const e = deriveExpected(d.parser)
1092
1275
  return this.emit(OP_GATE, cls, body, this.expected(e.length > 0 ? e : [d.parser._tag]))
1093
1276
  }
1094
- case 'token':
1277
+ case 'token': {
1095
1278
  return this.emit(OP_TOKEN, this.node(d.parser).ip)
1279
+ }
1096
1280
  case 'routed':
1097
1281
  return this.emit(OP_ROUTED, d.fallback === undefined ? -1 : this.node(d.fallback).ip)
1098
1282
  case 'dispatch': {
@@ -1204,7 +1388,7 @@ class Encoder {
1204
1388
  // merged (composed) map, and why the identity guard is required.
1205
1389
  const refName = (p as unknown as { _ruleName?: string })._ruleName
1206
1390
  const winner = refName === undefined ? undefined : this.winners?.[refName]
1207
- if (winner !== undefined && !wrapsRef(winner, p)) return this.scopedRef(p, winner)
1391
+ if (winner !== undefined && !winnerWrapsReference(winner, p)) return this.scopedRef(p, winner)
1208
1392
  let resolved: Combinator<unknown>
1209
1393
  try { resolved = d.thunk() }
1210
1394
  catch {
@@ -1219,7 +1403,7 @@ class Encoder {
1219
1403
  // other engine reported `['not(literal)']`. It hid because `expected` is a
1220
1404
  // TOP-LEVEL field on `RunResult` and is NOT part of the identity digest.
1221
1405
  case 'not':
1222
- return this.emit(OP_NOT, this.node(d.parser).ip, this.expected([`not(${d.parser._tag})`]))
1406
+ return this.emit(OP_NOT, this.node(d.parser).ip, this.expected(assertionFailureExpected(false, d.parser._tag)))
1223
1407
  case 'guard':
1224
1408
  // `'gate'`, the public name — see gate.ts. The def tag stays `'guard'`.
1225
1409
  return this.emit(OP_GUARD, this.fn(d.predicate, d.predSrc ?? null), this.expected(['gate']))
@@ -1259,7 +1443,7 @@ class Encoder {
1259
1443
  // lookahead's failure is "the guard did not hold", and naming the body's
1260
1444
  // internals offers a token the parse never asked the author for. The
1261
1445
  // table propagated the body's set instead.
1262
- return this.emit(OP_PEEK, this.node(d.parser).ip, this.expected([`peek(${d.parser._tag})`]))
1446
+ return this.emit(OP_PEEK, this.node(d.parser).ip, this.expected(assertionFailureExpected(true, d.parser._tag)))
1263
1447
  // A TRANSACTION IS A ROW. See `OP_ATTEMPT` for why the transparent
1264
1448
  // lowering was correct only for a choice arm.
1265
1449
  case 'attempt': {
@@ -1399,7 +1583,7 @@ class Encoder {
1399
1583
  switch (this.code[ip]) {
1400
1584
  case OP_GATE: return [ip + 2]
1401
1585
  case OP_RULE: case OP_OPT: case OP_NOT: case OP_PEEK: case OP_EXPECT: case OP_ATTEMPT: case OP_LABEL: case OP_COV: return [ip + 1]
1402
- case OP_SCOPE: case OP_SCOPE_CAP: case OP_WITHCTX: case OP_XFORM: case OP_LEAF: case OP_NODE: case OP_NODE_TRACK: return [ip + 2]
1586
+ case OP_SCOPE: case OP_SCOPE_CAP: case OP_SCOPE_PLAIN: case OP_WITHCTX: case OP_XFORM: case OP_LEAF: case OP_NODE: case OP_NODE_TRACK: return [ip + 2]
1403
1587
  case OP_SEQ: case OP_SEQV: return Array.from({ length: this.code[ip + 1]! }, (_, i) => ip + 2 + i)
1404
1588
  case OP_SEQX: return Array.from({ length: this.code[ip + 2]! }, (_, i) => ip + 3 + i)
1405
1589
  // ARMS START AT ip+4. `ip+3` is the choice's own EXPECTED-SET index, and
@@ -1458,6 +1642,8 @@ class Encoder {
1458
1642
  return ownTableProgram({
1459
1643
  code: this.code, k: this.k, fns: this.fns, cc: this.cc,
1460
1644
  fx: this.fx, disp: this.disp, dsp: this.dsp, rules: this.rules,
1645
+ ...(this.lex.length === 0 ? {} : { lex: this.lex }),
1646
+ ...(this.lexPrograms.length === 0 ? {} : { lexPrograms: this.lexPrograms }),
1461
1647
  ...(this.labels === undefined ? {} : { labels: this.labels }),
1462
1648
  ...(this.classified ? { classified: 1 as const } : {}),
1463
1649
  ...(this.scanSkipSets.length === 0
@@ -1561,7 +1747,13 @@ export function encodeTableProgram(
1561
1747
  ...(hostMode === undefined ? {} : { hostMode }),
1562
1748
  ...(track ? { trackLines: true } : {}),
1563
1749
  }
1564
- const enc = new Encoder(resolvedSettings)
1750
+ // Collect once from the FINAL winner-resolved graph. Sorting affects only
1751
+ // content-id discovery; ordinary rule/code order remains the caller's order.
1752
+ const lexicalRoots = [...names].sort().map(name => ruleMap[name]!)
1753
+ const resolveLexical = (name: string): Combinator<unknown> | undefined => ruleMap[name]
1754
+ const capabilities = collectLexicalCapabilities(lexicalRoots, resolveLexical)
1755
+ assertLexicalCapabilityClosure(lexicalRoots, capabilities, resolveLexical)
1756
+ const enc = new Encoder(resolvedSettings, capabilities.capabilityComplete)
1565
1757
  enc.winners = ruleMap
1566
1758
  for (const name of names) enc.encodeRule(name, ruleMap[name]!)
1567
1759
  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
  }