parseman 0.59.2 → 0.60.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 (118) hide show
  1. package/dist/analysis/commitment.d.ts +1 -1
  2. package/dist/analysis/diagnostics.js +877 -191
  3. package/dist/analysis/diagnostics.js.map +3 -3
  4. package/dist/cli/index.js +877 -191
  5. package/dist/cli/index.js.map +3 -3
  6. package/dist/combinators/completions.d.ts +3 -3
  7. package/dist/combinators/completions.d.ts.map +1 -1
  8. package/dist/combinators/dispatch.d.ts +13 -1
  9. package/dist/combinators/dispatch.d.ts.map +1 -1
  10. package/dist/combinators/first-set.d.ts +35 -2
  11. package/dist/combinators/first-set.d.ts.map +1 -1
  12. package/dist/combinators/gate.d.ts +1 -1
  13. package/dist/combinators/regex.d.ts.map +1 -1
  14. package/dist/combinators/trivia-boundary.d.ts +11 -0
  15. package/dist/combinators/trivia-boundary.d.ts.map +1 -1
  16. package/dist/combinators/trivia-skip.d.ts +1 -1
  17. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  18. package/dist/compiler/compile-linkable-table.d.ts +2 -3
  19. package/dist/compiler/compile-linkable-table.d.ts.map +1 -1
  20. package/dist/compiler/linker.d.ts.map +1 -1
  21. package/dist/compiler/token-alphabet.d.ts +2 -0
  22. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  23. package/dist/compiler/token-capability.js +65 -53
  24. package/dist/compiler/token-capability.js.map +2 -2
  25. package/dist/cst/host-mode.d.ts +1 -1
  26. package/dist/cst/root-trivia-scope.d.ts +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +897 -200
  30. package/dist/index.js.map +3 -3
  31. package/dist/language-service/index.d.ts +1 -1
  32. package/dist/language-service/index.js.map +1 -1
  33. package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
  34. package/dist/plugin/evaluator.d.ts.map +1 -1
  35. package/dist/plugin/index.d.ts +3 -7
  36. package/dist/plugin/index.d.ts.map +1 -1
  37. package/dist/plugin/index.js +904 -202
  38. package/dist/plugin/index.js.map +3 -3
  39. package/dist/regex/first-set.d.ts +19 -0
  40. package/dist/regex/first-set.d.ts.map +1 -1
  41. package/dist/spec/index.js.map +1 -1
  42. package/dist/table/assemble.d.ts +16 -3
  43. package/dist/table/assemble.d.ts.map +1 -1
  44. package/dist/table/child-slots.d.ts.map +1 -1
  45. package/dist/table/compile-rule-map.d.ts +3 -2
  46. package/dist/table/compile-rule-map.d.ts.map +1 -1
  47. package/dist/table/compile.d.ts +2 -2
  48. package/dist/table/emit-assembly.d.ts +8 -2
  49. package/dist/table/emit-assembly.d.ts.map +1 -1
  50. package/dist/table/emit.d.ts +8 -8
  51. package/dist/table/emit.d.ts.map +1 -1
  52. package/dist/table/encode.d.ts +6 -7
  53. package/dist/table/encode.d.ts.map +1 -1
  54. package/dist/table/exec.d.ts.map +1 -1
  55. package/dist/table/factor-choice.d.ts +27 -0
  56. package/dist/table/factor-choice.d.ts.map +1 -1
  57. package/dist/table/gap-gate.d.ts +11 -0
  58. package/dist/table/gap-gate.d.ts.map +1 -0
  59. package/dist/table/index.js +927 -202
  60. package/dist/table/index.js.map +3 -3
  61. package/dist/table/lex-program.d.ts.map +1 -1
  62. package/dist/table/ops.d.ts +48 -20
  63. package/dist/table/ops.d.ts.map +1 -1
  64. package/dist/table/program.d.ts +13 -8
  65. package/dist/table/program.d.ts.map +1 -1
  66. package/dist/types.d.ts +17 -5
  67. package/dist/types.d.ts.map +1 -1
  68. package/dist/version.d.ts +6 -6
  69. package/package.json +1 -1
  70. package/src/analysis/commitment.ts +1 -1
  71. package/src/analysis/duplication.ts +1 -1
  72. package/src/analysis/terminal.ts +1 -1
  73. package/src/combinators/attempt.ts +1 -1
  74. package/src/combinators/choice.ts +1 -1
  75. package/src/combinators/completions.ts +3 -3
  76. package/src/combinators/dispatch.ts +30 -6
  77. package/src/combinators/first-set.ts +55 -12
  78. package/src/combinators/gate.ts +1 -1
  79. package/src/combinators/node.ts +1 -1
  80. package/src/combinators/regex.ts +9 -4
  81. package/src/combinators/repeat.ts +6 -6
  82. package/src/combinators/sequence.ts +1 -1
  83. package/src/combinators/trivia-boundary.ts +18 -0
  84. package/src/combinators/trivia-skip.ts +1 -1
  85. package/src/compiler/compile-linkable-table.ts +2 -3
  86. package/src/compiler/degradation.ts +2 -2
  87. package/src/compiler/ir-serialize.ts +1 -1
  88. package/src/compiler/linker.ts +5 -3
  89. package/src/compiler/token-alphabet.ts +3 -1
  90. package/src/coverage.ts +1 -1
  91. package/src/cst/capture-buffer.ts +2 -2
  92. package/src/cst/host-mode.ts +1 -1
  93. package/src/cst/root-trivia-scope.ts +1 -1
  94. package/src/index.ts +1 -1
  95. package/src/language-service/index.ts +1 -1
  96. package/src/plugin/entry-supercompiler.ts +2 -1
  97. package/src/plugin/evaluator.ts +8 -3
  98. package/src/plugin/index.ts +11 -16
  99. package/src/regex/first-set.ts +37 -3
  100. package/src/table/assemble.ts +180 -35
  101. package/src/table/child-slots.ts +8 -3
  102. package/src/table/compile-rule-map.ts +10 -6
  103. package/src/table/compile.ts +3 -3
  104. package/src/table/emit-assembly.ts +186 -35
  105. package/src/table/emit.ts +19 -18
  106. package/src/table/encode.ts +482 -88
  107. package/src/table/exec.ts +42 -18
  108. package/src/table/factor-choice.ts +62 -0
  109. package/src/table/fail-clean.ts +2 -2
  110. package/src/table/gap-gate.ts +55 -0
  111. package/src/table/hold-gap.ts +2 -2
  112. package/src/table/lex-program.ts +15 -6
  113. package/src/table/ops.ts +51 -20
  114. package/src/table/program.ts +14 -9
  115. package/src/table/select-hot.ts +7 -2
  116. package/src/table/stamp.ts +1 -1
  117. package/src/types.ts +13 -6
  118. package/src/version.ts +6 -6
@@ -1,5 +1,6 @@
1
1
  import type { CharRange, FirstSet, Combinator, ParserDef } from '../types.ts'
2
2
  import { implicitLeadsDropped, leadRanges } from '../lead-ranges.ts'
3
+ import { triviaThroughRef } from './trivia-boundary.ts'
3
4
 
4
5
  /**
5
6
  * Resolve a NAMED cross-artifact rule reference (`g.Foo`) whose own thunk does not
@@ -27,6 +28,13 @@ export function classifyFinalChoice(
27
28
  arms: readonly Combinator<unknown>[],
28
29
  resolve?: RefResolver,
29
30
  activeTrivia?: Combinator<unknown> | null,
31
+ consuming = true,
32
+ /**
33
+ * Read each arm's construction-time `_meta.firstSet`, the set a disjoint
34
+ * interpreter `choice()` dispatches on (`choice.ts`), instead of deriving it:
35
+ * the table then lets in exactly the arms the interpreter lets in.
36
+ */
37
+ constructionSets = false,
30
38
  ): FinalChoiceClassification {
31
39
  const firstSets: FirstSet[] = []
32
40
  const nullable: boolean[] = []
@@ -38,7 +46,7 @@ export function classifyFinalChoice(
38
46
  for (const arm of arms) {
39
47
  const empty = matchesEmpty(arm, new Set(), resolve)
40
48
  nullable.push(empty)
41
- firstSets.push(empty ? { kind: 'any' } : firstSetOf(arm, new Set(), resolve, activeTrivia))
49
+ firstSets.push(empty ? { kind: 'any' } : constructionSets ? arm._meta.firstSet : firstSetOf(arm, new Set(), resolve, activeTrivia, consuming))
42
50
  if (empty) exclusive = false
43
51
  }
44
52
  if (exclusive) {
@@ -73,7 +81,7 @@ export function classifyFinalChoice(
73
81
  * does not. `undefined` → no winner; fall back to the thunk. Monolithic: the winner IS
74
82
  * the thunk's target, a no-op. A winner that recurses back to `p` (recursive or
75
83
  * self-alias rule) is caught by the shared `seen` guard → `any`, a sound over-approx. */
76
- function winnerForRef(p: Combinator<unknown>, resolve: RefResolver | undefined): Combinator<unknown> | undefined {
84
+ export function winnerForRef(p: Combinator<unknown>, resolve: RefResolver | undefined): Combinator<unknown> | undefined {
77
85
  if (resolve === undefined) return undefined
78
86
  const name = (p as unknown as { _ruleName?: string })._ruleName
79
87
  const w = name === undefined ? undefined : resolve(name)
@@ -316,6 +324,7 @@ function sequenceSet(
316
324
  fs: (p: Combinator<unknown>) => FirstSet,
317
325
  empties: (p: Combinator<unknown>) => boolean,
318
326
  trivia?: Combinator<unknown> | null,
327
+ consuming = true,
319
328
  ): FirstSet {
320
329
  const afterBoundary = (i: number): FirstSet => {
321
330
  const rest = at(i)
@@ -334,13 +343,23 @@ function sequenceSet(
334
343
  }
335
344
  const first = fs(p)
336
345
  if (!empties(p) || i + 1 === parsers.length) return first
337
- return union(first, afterBoundary(i + 1))
346
+ // A nullable regex reports `any` as its own set (it may match empty
347
+ // anywhere); here the empty match is the union with the rest, so only its
348
+ // non-empty matches' first chars are its own contribution.
349
+ return union((consuming ? p._meta.consumingFirstSet : undefined) ?? first, afterBoundary(i + 1))
338
350
  }
339
351
  return at(0)
340
352
  }
341
353
 
354
+ /**
355
+ * A sequence's construction-time first set, which the interpreter's `choice()`
356
+ * dispatches on. Trivia is unknown here, and a choice may run where leading
357
+ * trivia has not been skipped, so a nullable regex stands as `any` (`consuming`
358
+ * false): read through its non-empty matches, `choice(sequence(pad, '*'), 'x')`
359
+ * would not enter the pad arm at a comment before `*`.
360
+ */
342
361
  export function sequenceFirstSet(parsers: readonly Combinator<unknown>[]): FirstSet {
343
- return sequenceSet(parsers, p => p._meta.firstSet, matchesEmpty)
362
+ return sequenceSet(parsers, p => p._meta.firstSet, matchesEmpty, undefined, false)
344
363
  }
345
364
 
346
365
  /**
@@ -356,6 +375,17 @@ export function sequenceFirstSet(parsers: readonly Combinator<unknown>[]): First
356
375
  * referenced rule can be replaced with a WIDER first-set, so a baked deep set
357
376
  * would wrongly skip valid input — the compose path defers dispatch to fuse time.
358
377
  *
378
+ * `consuming: false` reads a nullable regex as `any`, as first sets read before
379
+ * 0.60.0. Recovery derives its resync sentinels so (`sequence.ts`
380
+ * `followSentinels`, the table's follow and separator classes, with no trivia),
381
+ * as do a construction-time sequence set and a table choice's classes. Only a
382
+ * repeat item's, a gap's and a node's gate read a pad through its non-empty
383
+ * matches.
384
+ *
385
+ * `activeTrivia` is the live trivia, `null` for a scope known to have none (where
386
+ * a reference installs its rule's trivia, `triviaThroughRef`), and `undefined`
387
+ * where it is not known.
388
+ *
359
389
  * `resolve` binds NAMED cross-artifact holes (`g.Foo`) against a fused winner map —
360
390
  * see `RefResolver`. Diagnostic-only today: it is what lets the gating analysis ask
361
391
  * the question at the site where the hole actually HAS an answer.
@@ -365,6 +395,7 @@ export function firstSetOf(
365
395
  seen: Set<Combinator<unknown>> = new Set(),
366
396
  resolve?: RefResolver,
367
397
  activeTrivia?: Combinator<unknown> | null,
398
+ consuming = true,
368
399
  ): FirstSet {
369
400
  // `seen` is the current recursion path, not a global visited set. A completed
370
401
  // shared child must be analysed again for a sibling; only a back-edge on this
@@ -372,7 +403,7 @@ export function firstSetOf(
372
403
  if (seen.has(p)) return any() // cycle → any (safe over-approximation)
373
404
  seen.add(p)
374
405
  try {
375
- return firstSetBody(p, seen, resolve, activeTrivia)
406
+ return firstSetBody(p, seen, resolve, activeTrivia, consuming)
376
407
  } finally {
377
408
  seen.delete(p)
378
409
  }
@@ -383,8 +414,9 @@ function firstSetBody(
383
414
  seen: Set<Combinator<unknown>>,
384
415
  resolve: RefResolver | undefined,
385
416
  activeTrivia: Combinator<unknown> | null | undefined,
417
+ consuming: boolean,
386
418
  ): FirstSet {
387
- const fs = (c: Combinator<unknown>): FirstSet => firstSetOf(c, seen, resolve, activeTrivia)
419
+ const fs = (c: Combinator<unknown>): FirstSet => firstSetOf(c, seen, resolve, activeTrivia, consuming)
388
420
  const empties = (c: Combinator<unknown>): boolean => matchesEmpty(c, new Set(), resolve)
389
421
  const d = p._def as ParserDef
390
422
  switch (d.tag) {
@@ -393,10 +425,19 @@ function firstSetBody(
393
425
  case 'keywords': return p._meta.firstSet // terminals: no refs, cached set is exact
394
426
  case 'dispatch': return fs(d.selector)
395
427
  case 'lazy': {
396
- const w = winnerForRef(p, resolve)
397
- if (w !== undefined) return fs(w)
398
- try { return fs(d.thunk()) }
399
- catch { return any() }
428
+ let target = winnerForRef(p, resolve)
429
+ if (target === undefined) {
430
+ try { target = d.thunk() }
431
+ catch { return any() }
432
+ }
433
+ // Under a scope KNOWN to have no trivia (`null`), the target runs under the
434
+ // trivia the reference installs for it (`triviaThroughRef`). `undefined`
435
+ // says nothing about the scope, and the target is read as it stands. Only
436
+ // the table encoder (which always passes `resolve`) reads through the
437
+ // install; the interpreter's own callers (`sequence.ts` follow sentinels)
438
+ // read a reference as 0.58.0 did (#223).
439
+ const through = activeTrivia === null && resolve !== undefined
440
+ return firstSetOf(target, seen, resolve, through ? triviaThroughRef(p, target, null) : activeTrivia, consuming)
400
441
  }
401
442
  case 'choice': {
402
443
  let out: FirstSet = empty()
@@ -414,7 +455,7 @@ function firstSetBody(
414
455
  // Recurse at every term boundary because a lookahead after another
415
456
  // zero-width term runs after trivia has advanced, not at the sequence's
416
457
  // original cursor.
417
- return sequenceSet(d.parsers, fs, empties, activeTrivia)
458
+ return sequenceSet(d.parsers, fs, empties, activeTrivia, consuming)
418
459
  }
419
460
  case 'peek': {
420
461
  // Deep-resolve the body: a `ref()` reads `any()` at CONSTRUCTION, so the
@@ -428,7 +469,6 @@ function firstSetBody(
428
469
  case 'transform':
429
470
  case 'label':
430
471
  case 'trivia':
431
- case 'token':
432
472
  case 'leaf':
433
473
  case 'node':
434
474
  case 'sepBy':
@@ -437,11 +477,14 @@ function firstSetBody(
437
477
  // the inner's construction-time set, which reads `any` when the inner opens
438
478
  // with a rule reference — resolve through it like any other wrapper.
439
479
  case 'attempt': return fs(d.parser)
480
+ // A token runs its body with trivia cleared (`token.ts`).
481
+ case 'token': return firstSetOf(d.parser, seen, resolve, activeTrivia === undefined ? undefined : null, consuming)
440
482
  case 'grammar': return firstSetOf(
441
483
  d.parser,
442
484
  seen,
443
485
  resolve,
444
486
  d.clearTrivia ? null : (d.triviaParser ?? activeTrivia),
487
+ consuming,
445
488
  )
446
489
  // A run starts with its first line's prefix; a nullable prefix starts with anything.
447
490
  case 'lines': {
@@ -15,7 +15,7 @@ import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types
15
15
  * Naming: this matches the `gate:` field on a gated CHOICE arm
16
16
  * (`choice({ gate, combinator }, other)`). Use the arm FIELD to SELECT a branch by
17
17
  * a cheap state predicate; use this COMBINATOR to ASSERT a state predicate
18
- * mid-sequence. Formerly named `guard()` — see the deprecated alias in `guard.ts`.
18
+ * mid-sequence. Formerly named `guard()`; that alias was removed in 0.45.0.
19
19
  *
20
20
  * Note: like all state predicates its first-set is `any`, so a `gate(...)` as the
21
21
  * LEADING term of a choice arm poisons that choice's first-char dispatch. Keep it
@@ -187,7 +187,7 @@ export function node<N>(
187
187
  if (project !== undefined && build !== undefined) {
188
188
  throw new Error('node() options cannot combine project with a build callback')
189
189
  }
190
- const baseDef = { tag: 'node' as const, parser: combinator, ...(type === undefined ? {} : { type }), ...(build === undefined ? {} : { build }) }
190
+ const baseDef = { tag: 'node' as const, parser: combinator, ...(type === undefined ? {} : { type }), ...(build === undefined ? {} : { build }), ...(combinator._meta.firstSet.kind !== 'any' && !matchesEmpty(combinator) ? { guarded: true as const } : {}) }
191
191
  const meta: ParserMeta = {
192
192
  firstSet: combinator._meta.firstSet,
193
193
  canMatchNewline: combinator._meta.canMatchNewline,
@@ -4,7 +4,7 @@ import { failAt } from './probe.ts'
4
4
  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
- import { firstSetFromRegex } from '../regex/first-set.ts'
7
+ import { consumingFirstSetFromRegex, firstSetFromRegex, regexCanMatchEmpty } from '../regex/first-set.ts'
8
8
  import { directTerminalFailureExpected } from './expected.ts'
9
9
  import type { ScalarParser } from './scalar.ts'
10
10
  import { PlainLeaf, PlainSpan } from '../cst/plain.js'
@@ -156,13 +156,18 @@ export function regex(pattern: string | RegExp, flags = ''): Combinator<string>
156
156
  // pairs, so gating on the ASCII set would false-exclude them. Fall back to `any()`
157
157
  // (always-try) — sound, and only forfeits gating for the rare Unicode-mode `/i`
158
158
  // recognizer, never the ASCII-only `/i` at-keywords the fix targets.
159
- const firstSet = !resolvedFlags.includes('i')
160
- ? raw.firstSet
159
+ const forFlags = (fs: FirstSet): FirstSet => !resolvedFlags.includes('i')
160
+ ? fs
161
161
  : (resolvedFlags.includes('u') || resolvedFlags.includes('v'))
162
162
  ? any()
163
- : asciiCaseFold(raw.firstSet)
163
+ : asciiCaseFold(fs)
164
+ const firstSet = forFlags(raw.firstSet)
164
165
  const canMatchNewline = raw.canMatchNewline
165
166
  const meta: ParserMeta = { firstSet, canMatchNewline, isTrivia: false }
167
+ if (firstSet.kind === 'any' && regexCanMatchEmpty(source)) {
168
+ const consuming = forFlags(consumingFirstSetFromRegex(source, resolvedFlags))
169
+ if (consuming.kind !== 'any') meta.consumingFirstSet = consuming
170
+ }
166
171
  const def = { tag: 'regex', source, flags: resolvedFlags } as const
167
172
  const expected = directTerminalFailureExpected(def)
168
173
  const parseScalar: ScalarParser = (input, pos, ctx) => {
@@ -154,11 +154,11 @@ export function many<T>(combinator: Combinator<T>, opts: RepeatOptions = {}): Co
154
154
  canMatchNewline: combinator._meta.canMatchNewline,
155
155
  isTrivia: false,
156
156
  }
157
- const def: { tag: 'many'; parser: Combinator<unknown>; min: 0; max?: number; valueUnused?: boolean } =
158
- { tag: 'many', parser: combinator as Combinator<unknown>, min: 0, ...(max === Infinity ? {} : { max }) }
159
- let expected: string[] | undefined
160
157
  // A non-nullable body can be first-set-gated per loop iteration (see repItem).
161
158
  const guardable = combinator._meta.firstSet.kind !== 'any' && !matchesEmpty(combinator)
159
+ const def: { tag: 'many'; parser: Combinator<unknown>; min: 0; max?: number; valueUnused?: boolean; guarded?: true } =
160
+ { tag: 'many', parser: combinator as Combinator<unknown>, min: 0, ...(max === Infinity ? {} : { max }), ...(guardable ? { guarded: true } : {}) }
161
+ let expected: string[] | undefined
162
162
  const scalarChild = scalarOf(combinator)
163
163
  const parseScalar = (input: string, pos: number, ctx: ParseContext): number =>
164
164
  repeatScalarTail(combinator, scalarChild, input, pos, ctx, guardable, def.valueUnused ? undefined : [], max)
@@ -229,11 +229,11 @@ function atLeast<T>(combinator: Combinator<T>, min: number, max: number): Combin
229
229
  canMatchNewline: combinator._meta.canMatchNewline,
230
230
  isTrivia: false,
231
231
  }
232
- const def: { tag: 'oneOrMore'; parser: Combinator<unknown>; min: number; max?: number; valueUnused?: boolean } =
233
- { tag: 'oneOrMore', parser: combinator as Combinator<unknown>, min, ...(max === Infinity ? {} : { max }) }
234
- let expected: string[] | undefined
235
232
  // A non-nullable body can be first-set-gated per loop iteration (see repItem).
236
233
  const guardable = combinator._meta.firstSet.kind !== 'any' && !matchesEmpty(combinator)
234
+ const def: { tag: 'oneOrMore'; parser: Combinator<unknown>; min: number; max?: number; valueUnused?: boolean; guarded?: true } =
235
+ { tag: 'oneOrMore', parser: combinator as Combinator<unknown>, min, ...(max === Infinity ? {} : { max }), ...(guardable ? { guarded: true } : {}) }
236
+ let expected: string[] | undefined
237
237
  const scalarChild = scalarOf(combinator)
238
238
  const parseScalar = (input: string, pos: number, ctx: ParseContext): number => {
239
239
  let cur = scalarChild(input, pos, ctx)
@@ -108,7 +108,7 @@ export function sequence<T extends [Combinator<unknown>, ...Combinator<unknown>[
108
108
  if (tolerant) {
109
109
  followSentinels ??= parsers.map((_, i) => firstSetSentinel(
110
110
  parsers.slice(i + 1).reduce<ReturnType<typeof firstSetOf>>(
111
- (set, parser) => union(set, firstSetOf(parser)), { kind: 'empty' },
111
+ (set, parser) => union(set, firstSetOf(parser, new Set(), undefined, undefined, false)), { kind: 'empty' },
112
112
  ),
113
113
  ))
114
114
  }
@@ -46,3 +46,21 @@ export function hasOwnTriviaBoundary(p: Combinator<unknown>, depth: number = MAX
46
46
  default: return false
47
47
  }
48
48
  }
49
+
50
+ /**
51
+ * The trivia `target` runs under when it is reached through the reference `ref`
52
+ * from a scope whose live trivia is `trivia` (`null` or `undefined`: none).
53
+ *
54
+ * A reference made from a region that cleared trivia re-installs the rule's own
55
+ * ambient trivia, where the rule has a boundary for it to repair (`ref.ts`,
56
+ * `Encoder.scopedRef`). Anything derived statically through a reference — a
57
+ * first set, a gate, a gap — must take the same scope, or it describes the
58
+ * caller's scope where the body runs under its own.
59
+ */
60
+ export function triviaThroughRef(
61
+ ref: Combinator<unknown>, target: Combinator<unknown>, trivia: Combinator<unknown> | null | undefined,
62
+ ): Combinator<unknown> | null | undefined {
63
+ if (trivia !== undefined && trivia !== null) return trivia
64
+ const amb = ref._meta.grammarTrivia ?? target._meta.grammarTrivia
65
+ return amb !== undefined && hasOwnTriviaBoundary(target) ? amb : trivia
66
+ }
@@ -413,7 +413,7 @@ export function scanTriviaCompact(input: string, cur: number, ctx: ParseContext)
413
413
  return { end: tr.ok ? tr.span.end : cur, commit: NOOP_COMMIT }
414
414
  }
415
415
 
416
- /** Legacy object-shaped contract used by previously emitted assembly factories. */
416
+ /** The object-shaped form of `scanTriviaCompact`. No engine calls it; the trivia-skip tests compare the two. */
417
417
  export function scanTrivia(input: string, cur: number, ctx: ParseContext): TriviaScan {
418
418
  const scan = scanTriviaCompact(input, cur, ctx)
419
419
  return typeof scan === 'number' ? { end: scan, commit: NOOP_COMMIT } : scan
@@ -29,9 +29,8 @@ import { PARSEMAN_VERSION } from '../version.ts'
29
29
  * artifact is a table, not a bag of compiled functions.
30
30
  * `ir` — the piece's combinator graph, serialized. This is the
31
31
  * composable half, and it is NOT new machinery: the plugin
32
- * already carries pieces as IR (`plugin/index.ts:1772`) and
33
- * the linker already re-lowers them (`linker.ts:640`,
34
- * `compileLinkable(evalRuleMapIR(p.ir), …)` at build time).
32
+ * already carries pieces as IR (`plugin/index.ts`) and the
33
+ * macro re-lowers them from it at build time.
35
34
  *
36
35
  * ── WHY THAT MAKES COMPOSITION THE EASY KIND ─────────────────────────────────
37
36
  * The open question was whether table-to-table composition has to merge two
@@ -93,8 +93,8 @@ export function formatDegradations(list: readonly Degradation[]): string[] {
93
93
  //
94
94
  // Compilation is single-module and synchronous, so a module-level sink (opened per
95
95
  // transform, drained after) records findings without threading a field through every
96
- // Ctx and every compile*() return — the same shape as the regex-lowering sink in
97
- // codegen.ts, for the same reason.
96
+ // Ctx and every compile*() return — the same shape as the source lowering's regex-lowering
97
+ // sink, for the same reason.
98
98
  // ---------------------------------------------------------------------------
99
99
  //
100
100
  // A STACK, not a single slot. `transformMacro` re-enters itself when it lowers a private
@@ -461,7 +461,7 @@ class Serializer {
461
461
  arms.push(`when(${matcherExpr(entry)}, ${kid(entry.parser)}${entry.caseInsensitive ? ', { caseInsensitive: true }' : ''})`)
462
462
  }
463
463
  }
464
- if (def.otherwise) arms.push(`otherwise(${kid(def.otherwise)})`)
464
+ if (def.otherwise) arms.push(`otherwise(${kid(def.otherwise)}${def.otherwiseCommit === false ? ', { commit: false }' : ''})`)
465
465
  return `dispatch(${kid(def.selector)}${arms.length === 0 ? '' : `, ${arms.join(', ')}`})`
466
466
  }
467
467
  case 'many': return `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`
@@ -25,9 +25,11 @@ import { copySpan } from '../line-index.ts'
25
25
  /**
26
26
  * Compile a `rules()` map to a **linkable artifact** — the composable, shippable
27
27
  * form (RULE_ABI_PLAN §4). A package exports `linkable(rules(g => …))`; consumers
28
- * import that artifact and `fuse([...])` it — **no source of the base grammar is
29
- * ever read**. Under the macro this is precompiled to static pieces; in the
30
- * interpreter it compiles here at load (like `compile()`).
28
+ * import that artifact and `compose([...])` it — **no source of the base grammar is
29
+ * ever read**. Under the macro this is precompiled to static pieces, and only a
30
+ * build-time `compose()` can link it. In the interpreter it compiles here at load
31
+ * (like `compile()`) and keeps the map's `RULES_RECIPE`, which is what a runtime
32
+ * `compose()` links; an artifact without one is refused there (`recipesOf`).
31
33
  *
32
34
  * `ns` is a per-artifact namespace; omit it to auto-assign a process-unique one
33
35
  * (fine at runtime — the macro supplies a stable module-derived ns instead).
@@ -140,6 +140,8 @@ export type ExecutableLexNode =
140
140
  readonly parsers: readonly Combinator<unknown>[]
141
141
  /** Inside a no-trivia scope of the token, where the encoder's trivia is none. */
142
142
  readonly cleared: boolean
143
+ /** The authored choice's own `disjoint`, which decides how the encoder classifies its arms. */
144
+ readonly disjoint: boolean
143
145
  readonly armExpected: readonly (readonly string[])[]
144
146
  readonly expected: readonly string[]
145
147
  }
@@ -509,7 +511,7 @@ function directFixedLexNode(
509
511
  if (arms.some(arm => arm === undefined)) return undefined
510
512
  return {
511
513
  kind: 'choice', arms: arms as ExecutableLexNode[],
512
- parsers: def.parsers, cleared,
514
+ parsers: def.parsers, cleared, disjoint: def.disjoint,
513
515
  armExpected: def.parsers.map(deriveExpected),
514
516
  expected: def.parsers.flatMap(deriveExpected),
515
517
  }
package/src/coverage.ts CHANGED
@@ -309,7 +309,7 @@ function coverageEntry(entry: Combinator<unknown>, collector: GrammarCoverageCol
309
309
  arms.push(when(dispatchMatcher(entry), wrapArm(entry.parser, ids[armIndex++]), entry.caseInsensitive ? { caseInsensitive: true } : undefined))
310
310
  }
311
311
  }
312
- if (def.otherwise) arms.push(otherwise(wrapArm(def.otherwise, ids[armIndex++])))
312
+ if (def.otherwise) arms.push(otherwise(wrapArm(def.otherwise, ids[armIndex++]), def.otherwiseCommit === false ? { commit: false } : undefined))
313
313
  const unsafeDispatch = dispatch as (selector: Combinator<string>, ...items: DispatchArm<unknown>[]) => Combinator<unknown>
314
314
  const base = unsafeDispatch(build(def.selector) as Combinator<string>, ...arms)
315
315
  return {
@@ -270,8 +270,8 @@ export function rollbackCstCaptureAt(
270
270
  // speculative branch leaves no ghost error (see saveCstMark). Guarded on a
271
271
  // present sink + a defined mark (rollbackTrivia forwards the same field).
272
272
  //
273
- // Each truncation is guarded on `length !== mark`, mirroring the compiled
274
- // engine (codegen.ts `captureRestoreBody`). Assigning `array.length` runs V8's
273
+ // Each truncation is guarded on `length !== mark`, as the source lowering's
274
+ // `captureRestoreBody` did. Assigning `array.length` runs V8's
275
275
  // length setter — a backing-store trim decision — and costs the same when the
276
276
  // value is unchanged, which is the overwhelmingly common case here: a
277
277
  // speculative branch that captured nothing still pays for every store. The
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The compiled-artifact ↔ host contract, in ONE place.
3
3
  *
4
- * This lives on its own — rather than in `compiler/codegen.ts` where it started —
4
+ * This lives on its own — rather than in the source lowering where it started —
5
5
  * because the DRIVER has to enforce it too. `functional/run.ts` is the `parseman/run`
6
6
  * entry, whose whole purpose is to stay small (see `test/unit/run-entry-closure.test.ts`),
7
7
  * so it cannot import the compiler to reach an assertion. Neither can it carry its own
@@ -2,7 +2,7 @@
2
2
  * The ROOT-CAPTURE POLICY of one local trivia scope, in one place.
3
3
  *
4
4
  * Three engines install a `parser({ trivia })` scope — the interpreter
5
- * (`combinators/grammar.ts`), source lowering (`compiler/codegen.ts`) and the
5
+ * (`combinators/grammar.ts`), the source lowering (removed in 0.47.0) and the
6
6
  * table (`table/assemble.ts`, `table/exec.ts`) — and each of them has to answer
7
7
  * the same two questions about selected root trivia. The refusal below was a
8
8
  * string literal in two of them and ABSENT from the third, which is how a table
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ export type { KeywordsOptions, WordOptions } from './combinators/keywords.ts'
17
17
  export { sequence } from './combinators/sequence.ts'
18
18
  export { choice } from './combinators/choice.ts'
19
19
  export { dispatch, endsWith, makeWhen, matches, otherwise, routed, startsWith, when } from './combinators/dispatch.ts'
20
- export type { DispatchArm, DispatchOtherwise, DispatchStringMatcher, DispatchWhen, DispatchWhenFactory, DispatchWhenOptions, DispatchWhenMatcher } from './combinators/dispatch.ts'
20
+ export type { DispatchArm, DispatchOtherwise, DispatchOtherwiseOptions, DispatchStringMatcher, DispatchWhen, DispatchWhenFactory, DispatchWhenOptions, DispatchWhenMatcher } from './combinators/dispatch.ts'
21
21
  export { attempt } from './combinators/attempt.ts'
22
22
  export type { AttemptOptions } from './combinators/attempt.ts'
23
23
  export { many, oneOrMore, optional, sepBy, oneOrMoreSep, keepSeparator } from './combinators/repeat.ts'
@@ -6,7 +6,7 @@
6
6
  * name = node type, without ever modifying the grammar:
7
7
  *
8
8
  * - `parse(src, { tolerant })` → CST + `ParseError[]` (recovers on the compiled
9
- * fast path when `grammar` is a `compile(g, { recovery: true })` grammar).
9
+ * fast path when `grammar` is a `compile()`d grammar).
10
10
  * - `diagnostics(src)` → structural parse errors + your per-node-type lint rules.
11
11
  * - `completionsAt(src, offset)` → the grammar's expected-token set at the cursor,
12
12
  * mapped through your per-rule semantic completion handlers.
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Experimental build-time Entry/SCC supercompiler for static table assemblies.
2
+ * Build-time Entry/SCC supercompiler for the selective assemblies a macro build
3
+ * emits (it runs on every macro build that selects sites).
3
4
  *
4
5
  * The table emitter remains the sole semantic backend. This pass parses that
5
6
  * backend's ordinary JavaScript and joins uniquely-owned piece functions with
@@ -431,11 +431,16 @@ function dispatchArmValue(node: Expression, scope: XScope, code?: string, mfs?:
431
431
  }
432
432
 
433
433
  if (callee.name === 'otherwise') {
434
- const [parserArg] = node.arguments
435
- if (!parserArg || parserArg.type === 'SpreadElement') return null
434
+ const [parserArg, optsArg] = node.arguments
435
+ if (!parserArg || parserArg.type === 'SpreadElement' || optsArg?.type === 'SpreadElement') return null
436
436
  const parserValue = anyValue(parserArg as Expression, scope, code, mfs)
437
437
  if (!isCombinator(parserValue)) return null
438
- return parseman.otherwise(parserValue)
438
+ if (optsArg === undefined) return parseman.otherwise(parserValue)
439
+ const opts = anyValue(optsArg as Expression, scope, code, mfs)
440
+ if (typeof opts !== 'object' || opts === null) return null
441
+ const commit = (opts as { commit?: unknown }).commit
442
+ if (commit !== undefined && typeof commit !== 'boolean') return null
443
+ return parseman.otherwise(parserValue, commit === undefined ? {} : { commit })
439
444
  }
440
445
 
441
446
  if (callee.name === 'when') {
@@ -5,8 +5,9 @@
5
5
  *
6
6
  * For each such import, walks the file's AST, finds variable declarations
7
7
  * whose RHS is a pure parseman combinator call, evaluates them at build time,
8
- * compiles the result to an optimized inline function, and replaces the
9
- * declaration — removing the import entirely.
8
+ * compiles the result to a table program (plus generated code for its hottest
9
+ * sites, within `assemblyBudget`), and replaces the declaration. The macro import
10
+ * is removed; the module imports the shared driver from `parseman/table` instead.
10
11
  *
11
12
  * Usage:
12
13
  * // vite.config.ts
@@ -61,7 +62,7 @@ import type {
61
62
  * The PUBLIC subpath, not a deep path into `src/`: this string is written into a
62
63
  * consumer's build output, so it has to be a specifier their resolver can see.
63
64
  * `parseman/table` is declared in package.json `exports` and re-exports
64
- * `tableRules` as `tableRules` (src/table/index.ts:28).
65
+ * `tableRules` unrenamed (`src/table/index.ts`).
65
66
  */
66
67
  const TABLE_RUNTIME_SPECIFIER = 'parseman/table'
67
68
 
@@ -119,13 +120,9 @@ export type ParsecraftPluginOptions = {
119
120
  */
120
121
  warnUnloweredRegex?: boolean
121
122
  /**
122
- * Bake automatic list recovery into the macro-compiled output. **Default `false`.**
123
- * When on, `many`/`oneOrMore`/`sepBy` in every compiled grammar gain a
124
- * `_ctx._tolerant`-gated recovery branch (dormant on strict parses — a normal
125
- * parse is unaffected and allocates nothing). The output stays macro-inlinable
126
- * (sentinels build via `_ctx._rec`, not `_rp`). Turn it on for grammars you drive
127
- * from an editor/language service so `run(g, src, { tolerant: true })` recovers on
128
- * the fast compiled path; leave it off for build-only grammars.
123
+ * @deprecated Has no effect: every compiled table carries recovery, and
124
+ * `run(g, src, { tolerant: true })` recovers on the compiled path whatever this is
125
+ * set to. Removed in the next minor release.
129
126
  */
130
127
  recovery?: boolean
131
128
  /** Emit grammar-coverage hooks in macro output. Off by default, so ordinary
@@ -1456,9 +1453,8 @@ function transformMacroImpl(
1456
1453
 
1457
1454
  /**
1458
1455
  * Compile a `rules(factory)` call into ONE shared replacement expression
1459
- * for the whole call — see compileRuleMap() in codegen.ts for why this is
1460
- * one shared codegen pass instead of one `compile()` per entry (a `rules()`
1461
- * factory's entries commonly share large reachable sub-rule graphs; a
1456
+ * for the whole call: one shared `compileRuleMap()` pass instead of one
1457
+ * `compile()` per entry (a `rules()` factory's entries commonly share large reachable sub-rule graphs; a
1462
1458
  * shared pass compiles each shared sub-rule once instead of once per entry
1463
1459
  * that reaches it).
1464
1460
  */
@@ -1977,9 +1973,8 @@ function transformMacroImpl(
1977
1973
  * shadows the base piece which actually defines `Pair` — the encoder then finds a
1978
1974
  * hole where the winner should be and refuses the whole grammar with "ref() used
1979
1975
  * before .define()". The same filter guards `compileLinkableTable`'s `isLocal`
1980
- * (`compile-linkable-table.ts:96`), `itemCarried` (`linker.ts:696`) and
1981
- * `recoverComposedRules`; this is the fourth site that needs it, for the same
1982
- * reason, and skipping it is why cross-piece composition looked blocked.
1976
+ * (`compile-linkable-table.ts`) and `recoverComposedRules`; this site needs it
1977
+ * for the same reason, and skipping it is why cross-piece composition looked blocked.
1983
1978
  *
1984
1979
  * The reference itself is not lost: it stays inside the delta's own rule bodies and
1985
1980
  * `enc.winners` binds it BY NAME to whichever piece supplies the definition.
@@ -32,7 +32,7 @@ import { shorthandRanges, parseClassRanges, readUnicodeEscape } from './classes.
32
32
  type Node =
33
33
  | { t: 'char'; set: FirstSet; nl: boolean } // matches exactly one char from `set`
34
34
  | { t: 'any' } // unknown 1-char-ish construct: could start with anything
35
- | { t: 'empty' } // zero-width: anchors, lookaround, word boundary
35
+ | { t: 'empty'; look?: Node } // zero-width: anchors, lookaround, word boundary (`look`: a positive lookahead's body)
36
36
  | { t: 'seq'; parts: Node[] }
37
37
  | { t: 'alt'; arms: Node[] }
38
38
  | { t: 'rep'; node: Node; min: number } // quantified; min 0 ⇒ nullable
@@ -142,10 +142,11 @@ function parseRegex(src: string, unicode = false, sets = false): Node {
142
142
  i++ // consume '('
143
143
  // Group prefixes: (?:…) (?=…) (?!…) (?<=…) (?<!…) (?<name>…)
144
144
  let lookaround = false
145
+ let lookahead = false
145
146
  if (src[i] === '?') {
146
147
  const c1 = src[i + 1]
147
148
  if (c1 === ':') i += 2
148
- else if (c1 === '=' || c1 === '!') { i += 2; lookaround = true }
149
+ else if (c1 === '=' || c1 === '!') { lookahead = c1 === '='; i += 2; lookaround = true }
149
150
  else if (c1 === '<') {
150
151
  const c2 = src[i + 2]
151
152
  if (c2 === '=' || c2 === '!') { i += 3; lookaround = true } // lookbehind
@@ -159,7 +160,10 @@ function parseRegex(src: string, unicode = false, sets = false): Node {
159
160
  const inner = parseAlt()
160
161
  if (src[i] !== ')') throw BAIL
161
162
  i++ // consume ')'
162
- return lookaround ? EMPTY : inner // lookaround is zero-width
163
+ // Lookaround is zero-width. A positive LOOKAHEAD keeps its body for
164
+ // `regexFirstTwo`; every other walker reads it as `EMPTY`.
165
+ if (lookaround) return lookahead ? { t: 'empty', look: inner } : EMPTY
166
+ return inner
163
167
  }
164
168
 
165
169
  const parseClass = (): Node => {
@@ -348,6 +352,17 @@ export function regexCanMatchEmpty(source: string): boolean {
348
352
  try { return nullable(parseRegex(source)) } catch { return true }
349
353
  }
350
354
 
355
+ /**
356
+ * The first chars of a NON-EMPTY match of `source`, nullable or not: the set
357
+ * `firstSetFromRegex` reports for a pattern that must consume, without its
358
+ * widening of a nullable one to `any()`. Only a caller that accounts for the
359
+ * empty match itself may use it (`sequenceSet`). `any()` when unparseable.
360
+ * `flags` reads atoms as `firstSetFromRegex` does.
361
+ */
362
+ export function consumingFirstSetFromRegex(source: string, flags = ''): FirstSet {
363
+ try { return firstSet(parseRegex(source, flags.includes('u') || flags.includes('v'), flags.includes('v'))) } catch { return any() }
364
+ }
365
+
351
366
  /**
352
367
  * `flags` decides how atoms read: under `u`/`v` each is one code point, else one
353
368
  * code unit, so a non-`u` set holds a high surrogate where an astral character
@@ -369,3 +384,22 @@ export function firstSetFromRegex(source: string, flags = ''): { firstSet: First
369
384
  return { firstSet: firstSet(ast), canMatchNewline: canConsumeNewline(ast) }
370
385
  }
371
386
 
387
+ /**
388
+ * The first TWO characters every match of `source` begins with, where the
389
+ * pattern says them outright: a one-character class and then another, or a
390
+ * positive lookahead over one (`:(?=[ \t])`). `undefined` otherwise. Every
391
+ * match that starts with a character of `first` has a second character, and it
392
+ * is in `second`. `flags` reads atoms as `firstSetFromRegex` does.
393
+ */
394
+ export function regexFirstTwo(source: string, flags = ''): { first: FirstSet; second: FirstSet } | undefined {
395
+ let ast: Node
396
+ try { ast = parseRegex(source, flags.includes('u') || flags.includes('v'), flags.includes('v')) } catch { return undefined }
397
+ const parts = ast.t === 'seq' ? ast.parts : [ast]
398
+ const a = parts[0]
399
+ const b = parts[1]
400
+ if (a === undefined || a.t !== 'char' || a.set.kind !== 'ranges' || b === undefined) return undefined
401
+ const second = b.t === 'char'
402
+ ? b.set
403
+ : b.t === 'empty' && b.look !== undefined && !nullable(b.look) ? firstSet(b.look) : undefined
404
+ return second !== undefined && second.kind === 'ranges' ? { first: a.set, second } : undefined
405
+ }