parseman 0.48.1 → 0.50.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 (83) hide show
  1. package/dist/analysis/commitment.d.ts +2 -1
  2. package/dist/analysis/commitment.d.ts.map +1 -1
  3. package/dist/analysis/diagnostics.js +1931 -861
  4. package/dist/analysis/diagnostics.js.map +3 -3
  5. package/dist/analysis/rebuild.d.ts.map +1 -1
  6. package/dist/cli/index.js +1236 -164
  7. package/dist/cli/index.js.map +3 -3
  8. package/dist/combinators/trivia-skip.d.ts +1 -1
  9. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  10. package/dist/compiler/build-arity.d.ts +16 -0
  11. package/dist/compiler/build-arity.d.ts.map +1 -1
  12. package/dist/compiler/degradation.d.ts +0 -2
  13. package/dist/compiler/degradation.d.ts.map +1 -1
  14. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  15. package/dist/compiler/linker.d.ts.map +1 -1
  16. package/dist/compiler/token-alphabet.d.ts +11 -0
  17. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  18. package/dist/compiler/token-capability.js +185 -20
  19. package/dist/compiler/token-capability.js.map +1 -1
  20. package/dist/cst/capture-buffer.d.ts +5 -1
  21. package/dist/cst/capture-buffer.d.ts.map +1 -1
  22. package/dist/index.js +1354 -278
  23. package/dist/index.js.map +3 -3
  24. package/dist/language-service/index.js +2 -1
  25. package/dist/language-service/index.js.map +1 -1
  26. package/dist/plugin/direct-builder-static.d.ts +20 -0
  27. package/dist/plugin/direct-builder-static.d.ts.map +1 -1
  28. package/dist/plugin/entry-supercompiler.d.ts +48 -0
  29. package/dist/plugin/entry-supercompiler.d.ts.map +1 -0
  30. package/dist/plugin/evaluator.d.ts +13 -0
  31. package/dist/plugin/evaluator.d.ts.map +1 -1
  32. package/dist/plugin/index.d.ts.map +1 -1
  33. package/dist/plugin/index.js +2118 -376
  34. package/dist/plugin/index.js.map +3 -3
  35. package/dist/run/index.js +2 -1
  36. package/dist/run/index.js.map +1 -1
  37. package/dist/spec/index.js +29 -7
  38. package/dist/spec/index.js.map +1 -1
  39. package/dist/table/assemble.d.ts.map +1 -1
  40. package/dist/table/compile-rule-map.d.ts +6 -5
  41. package/dist/table/compile-rule-map.d.ts.map +1 -1
  42. package/dist/table/emit-assembly.d.ts +6 -1
  43. package/dist/table/emit-assembly.d.ts.map +1 -1
  44. package/dist/table/encode.d.ts.map +1 -1
  45. package/dist/table/index.js +1298 -231
  46. package/dist/table/index.js.map +3 -3
  47. package/dist/table/ops.d.ts +3 -2
  48. package/dist/table/ops.d.ts.map +1 -1
  49. package/dist/table/program.d.ts +15 -1
  50. package/dist/table/program.d.ts.map +1 -1
  51. package/dist/table/scalar-terminal.d.ts +30 -0
  52. package/dist/table/scalar-terminal.d.ts.map +1 -1
  53. package/dist/table/site-labels.d.ts +8 -0
  54. package/dist/table/site-labels.d.ts.map +1 -1
  55. package/dist/types.d.ts +8 -2
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/version.d.ts +1 -1
  58. package/package.json +1 -1
  59. package/src/analysis/commitment.ts +19 -8
  60. package/src/analysis/rebuild.ts +1 -0
  61. package/src/combinators/trivia-skip.ts +2 -2
  62. package/src/compiler/build-arity.ts +50 -0
  63. package/src/compiler/degradation.ts +0 -6
  64. package/src/compiler/ir-serialize.ts +17 -3
  65. package/src/compiler/linker.ts +14 -1
  66. package/src/compiler/token-alphabet.ts +322 -28
  67. package/src/cst/capture-buffer.ts +34 -11
  68. package/src/plugin/direct-builder-static.ts +116 -12
  69. package/src/plugin/entry-supercompiler.ts +567 -0
  70. package/src/plugin/evaluator.ts +60 -2
  71. package/src/plugin/index.ts +147 -4
  72. package/src/table/assemble.ts +125 -42
  73. package/src/table/compile-rule-map.ts +8 -7
  74. package/src/table/compile.ts +7 -8
  75. package/src/table/emit-assembly.ts +646 -126
  76. package/src/table/emit.ts +1 -1
  77. package/src/table/encode.ts +132 -7
  78. package/src/table/ops.ts +3 -2
  79. package/src/table/program.ts +44 -3
  80. package/src/table/scalar-terminal.ts +213 -1
  81. package/src/table/site-labels.ts +22 -9
  82. package/src/types.ts +1 -1
  83. package/src/version.ts +1 -1
package/src/table/emit.ts CHANGED
@@ -208,7 +208,7 @@ function emitAssemblies(prog: TableProgram, cfgs: readonly RunCfg[]): string[] {
208
208
  seen.add(key)
209
209
  let em
210
210
  try {
211
- em = emitAssemblySource(t, prog, cfg, extraIps)
211
+ em = emitAssemblySource(t, prog, cfg, extraIps, true)
212
212
  } catch (e) {
213
213
  if (e instanceof Unemittable) continue
214
214
  throw e
@@ -1,6 +1,7 @@
1
1
  import type { AutoNotCheck, Combinator, FirstSet, ParserDef } from '../types.ts'
2
- import { classifyFinalChoice, firstSetOf, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
2
+ import { classifyFinalChoice, firstSetOf, intersects, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
3
3
  import { childrenOf } from '../analysis/gating.ts'
4
+ import { capturesLeaf, mayCommitFailure, mayLeavePartialCapture } from '../analysis/commitment.ts'
4
5
  import { getCoreLiteralValue } from '../combinators/choice.ts'
5
6
  import { deriveExpected } from '../combinators/expect.ts'
6
7
  import { assertionFailureExpected, directTerminalFailureExpected } from '../combinators/expected.ts'
@@ -33,7 +34,7 @@ import { covKindCode, encodeClassSpec, ownTableProgram } from './program.ts'
33
34
  import type { GrammarCoveragePlan } from '../compiler/grammar-coverage-ids.ts'
34
35
  import { directArrayProjection } from '../compiler/direct-projection.ts'
35
36
  import {
36
- assertLexicalCapabilityClosure, collectLexicalCapabilities, winnerWrapsReference,
37
+ collectLexicalCapabilities, winnerWrapsReference,
37
38
  } from '../compiler/token-capability.ts'
38
39
  import { directExecutableTokenBody, type ExecutableLexNode } from '../compiler/token-alphabet.ts'
39
40
 
@@ -141,6 +142,9 @@ class Encoder {
141
142
  private lexIndex = new Map<string, number>()
142
143
  lexPrograms: LexProgramSpec[] = []
143
144
  private lexProgramIndex = new Map<string, number>()
145
+ private choiceRollbackMasks = new Map<number, number>()
146
+ private nonCommittingChoiceSites = new Set<number>()
147
+ private failureRollbackCleanSites = new Set<number>()
144
148
  labels: readonly string[] | undefined = undefined
145
149
  /** Trivia table in scope at the row being encoded — codegen's `ctx.activeTrivia`. */
146
150
  activeTrivia: Combinator<unknown> | undefined = undefined
@@ -611,6 +615,77 @@ class Encoder {
611
615
  return w === undefined ? undefined : (name: string) => w[name]
612
616
  }
613
617
 
618
+ /**
619
+ * Whether a failed ordered-choice arm can leave state that the enclosing
620
+ * choice must restore before trying its successor.
621
+ *
622
+ * `mayLeavePartialCapture` is the existing sound CST/trivia proof. The extra
623
+ * writer census covers the side sinks that proof deliberately does not model:
624
+ * fields, recovered errors, and interpreted scanners. Strict assemblies do
625
+ * not run repeat recovery, but these rows can still publish through a caller's
626
+ * context. Cycles are safe to stop: revisiting the same combinator cannot
627
+ * discover a writer the first traversal did not already reach.
628
+ */
629
+ private failureNeedsRollback(
630
+ p: Combinator<unknown>,
631
+ autoNot: readonly AutoNotCheck[] | null | undefined,
632
+ ): boolean {
633
+ const refName = (p as unknown as { _ruleName?: string })._ruleName
634
+ const winner = refName === undefined ? undefined : this.winners?.[refName]
635
+ const root = winner !== undefined && !winnerWrapsReference(winner, p) ? winner : p
636
+ const sideWriterMemo = new Map<Combinator<unknown>, boolean>()
637
+ const hasSideWriter = (
638
+ current: Combinator<unknown>,
639
+ seen = new Set<Combinator<unknown>>(),
640
+ ): { found: boolean; complete: boolean } => {
641
+ const cached = sideWriterMemo.get(current)
642
+ if (cached !== undefined) return { found: cached, complete: true }
643
+ if (seen.has(current)) return { found: false, complete: false }
644
+ const next = new Set(seen)
645
+ next.add(current)
646
+ const def = current._def
647
+ let result: { found: boolean; complete: boolean }
648
+ switch (def.tag) {
649
+ case 'field':
650
+ case 'expect':
651
+ case 'recover':
652
+ case 'scanTo':
653
+ case 'unknown':
654
+ result = { found: true, complete: true }
655
+ break
656
+ case 'lazy': {
657
+ const name = (current as unknown as { _ruleName?: string })._ruleName
658
+ const resolvedWinner = name === undefined ? undefined : this.winners?.[name]
659
+ if (resolvedWinner !== undefined && !winnerWrapsReference(resolvedWinner, current)) {
660
+ result = hasSideWriter(resolvedWinner, next)
661
+ break
662
+ }
663
+ try { result = hasSideWriter(def.thunk(), next) }
664
+ catch { result = { found: true, complete: true } }
665
+ break
666
+ }
667
+ default: {
668
+ let complete = true
669
+ result = { found: false, complete: true }
670
+ for (const child of childrenOf(def)) {
671
+ const childResult = hasSideWriter(child, next)
672
+ if (childResult.found) {
673
+ result = { found: true, complete: true }
674
+ break
675
+ }
676
+ complete &&= childResult.complete
677
+ }
678
+ if (!result.found) result = { found: false, complete }
679
+ }
680
+ }
681
+ if (result.found || result.complete) sideWriterMemo.set(current, result.found)
682
+ return result
683
+ }
684
+ return mayLeavePartialCapture(root, new Set(), true)
685
+ || hasSideWriter(root).found
686
+ || ((autoNot?.length ?? 0) > 0 && capturesLeaf(root))
687
+ }
688
+
614
689
  /** A first set becomes a char class string, or −1 for `any`. */
615
690
  private charClass(fs: FirstSet): number {
616
691
  if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
@@ -1074,14 +1149,53 @@ class Encoder {
1074
1149
  // Arm ORDER is preserved on both, which is what makes this a PEG-safe
1075
1150
  // change rather than a reordering.
1076
1151
  const finalChoice = classifyFinalChoice(arms, rr)
1077
- const classes = arms.map((_, i) => finalChoice.nullable[i]
1078
- ? -1 : this.charClass(finalChoice.firstSets[i]!))
1152
+ // attempt() preserves its child's success language, but the broad
1153
+ // classifier deliberately treats the transaction as nullable/unknown so
1154
+ // it can never by itself authorize exclusive one-arm selection. Recover
1155
+ // only the narrower fact needed by an ORDERED choice: a non-nullable
1156
+ // child can receive an individual rejection gate. Keep that authority
1157
+ // out if it would make an otherwise open site fully disjoint, preserving
1158
+ // the transaction's standing exclusion from exclusive dispatch.
1159
+ const transactionalFirst = arms.map((arm, i): FirstSet | undefined => {
1160
+ if (!finalChoice.nullable[i]) return undefined
1161
+ const def = arm._def as ParserDef
1162
+ if (def.tag !== 'attempt' || matchesEmpty(def.parser, new Set(), rr)) return undefined
1163
+ return firstSetOf(def.parser, new Set(), rr)
1164
+ })
1165
+ const gateFirst = finalChoice.firstSets.map((first, i) => transactionalFirst[i] ?? first)
1166
+ const classes = gateFirst.map((first, i) => finalChoice.nullable[i] && transactionalFirst[i] === undefined
1167
+ ? -1 : this.charClass(first))
1168
+ if (transactionalFirst.some(first => first !== undefined) && classes.every(cls => cls >= 0)) {
1169
+ let overlap = false
1170
+ for (let i = 0; i < gateFirst.length && !overlap; i++) {
1171
+ for (let j = i + 1; j < gateFirst.length; j++) {
1172
+ if (intersects(gateFirst[i]!, gateFirst[j]!)) { overlap = true; break }
1173
+ }
1174
+ }
1175
+ if (!overlap) for (let i = 0; i < classes.length; i++) {
1176
+ if (transactionalFirst[i] !== undefined) classes[i] = -1
1177
+ }
1178
+ }
1079
1179
  const dispIdx = this.disp.length
1080
1180
  this.disp.push(classes)
1081
1181
  // PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. While the best
1082
1182
  // failure remains at the choice position, an arm declined by the driver's
1083
1183
  // char gate contributes the static opener set the interpreter would get by
1084
1184
  // entering it. A deeper dynamic arm failure discards those shallower sets.
1185
+ // One compiler-only bit mask records which failed arms can actually
1186
+ // dirty a rollback sink. It is consumed while printing emitted strict
1187
+ // assemblies and never serialized; tolerant assemblies retain the
1188
+ // universal rollback because repeat recovery can publish an error from
1189
+ // inside any arm. Arity >31 falls back to -1 rather than truncating.
1190
+ let rollbackMask = kids.length > 31 ? -1 : 0
1191
+ if (rollbackMask === 0) {
1192
+ for (let i = 0; i < arms.length; i++) {
1193
+ const src = order[i]!
1194
+ if (this.failureNeedsRollback(arms[i]!, d.autoNot[src])) {
1195
+ rollbackMask |= 1 << i
1196
+ }
1197
+ }
1198
+ }
1085
1199
  const head = this.emitHead(OP_CHOICE, 3 + 2 * kids.length)
1086
1200
  this.code[head + 1] = dispIdx
1087
1201
  this.code[head + 2] = kids.length
@@ -1093,6 +1207,10 @@ class Encoder {
1093
1207
  this.code[head + 4 + i] = kids[i]!
1094
1208
  this.code[head + 4 + kids.length + i] = this.expected(deriveExpected(arms[i]!))
1095
1209
  }
1210
+ this.choiceRollbackMasks.set(head, rollbackMask)
1211
+ if (arms.every(arm => !mayCommitFailure(arm, new Set(), rr))) {
1212
+ this.nonCommittingChoiceSites.add(head)
1213
+ }
1096
1214
  return head
1097
1215
  }
1098
1216
  case 'many':
@@ -1143,8 +1261,11 @@ class Encoder {
1143
1261
  ? this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags, this.expected(deriveExpected(d.parser)))
1144
1262
  : this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags)
1145
1263
  }
1146
- case 'optional':
1147
- return this.emit(OP_OPT, this.node(d.parser).ip)
1264
+ case 'optional': {
1265
+ const head = this.emit(OP_OPT, this.node(d.parser).ip)
1266
+ if (!this.failureNeedsRollback(d.parser, undefined)) this.failureRollbackCleanSites.add(head)
1267
+ return head
1268
+ }
1148
1269
  case 'transform': {
1149
1270
  // Declared on the def and not lowered here. Refuse rather than assume it is
1150
1271
  // inert: a recognition-only transform suppresses its value, and a table that
@@ -1444,6 +1565,7 @@ class Encoder {
1444
1565
  // lowering was correct only for a choice arm.
1445
1566
  case 'attempt': {
1446
1567
  const inner = this.emit(OP_ATTEMPT, this.node(d.parser).ip)
1568
+ if (!this.failureNeedsRollback(d.parser, undefined)) this.failureRollbackCleanSites.add(inner)
1447
1569
  // THE FIRST-SET FAIL-FAST GUARD, lowered as the `OP_GATE` row the `node()`
1448
1570
  // case already uses — it is the same guard, written twice in the
1449
1571
  // interpreter (`attempt.ts:22`, `node.ts:239`). It is NOT merely an
@@ -1652,6 +1774,10 @@ class Encoder {
1652
1774
  ...(this.rec ? { rec: 1 as const } : {}),
1653
1775
  ...(this.cov === undefined ? {} : { cov: this.cov }),
1654
1776
  lines: this.track ? 1 : 0,
1777
+ }, undefined, {
1778
+ choiceRollbackMasks: this.choiceRollbackMasks,
1779
+ failureRollbackCleanSites: this.failureRollbackCleanSites,
1780
+ nonCommittingChoiceSites: this.nonCommittingChoiceSites,
1655
1781
  })
1656
1782
  }
1657
1783
  }
@@ -1748,7 +1874,6 @@ export function encodeTableProgram(
1748
1874
  const lexicalRoots = [...names].sort().map(name => ruleMap[name]!)
1749
1875
  const resolveLexical = (name: string): Combinator<unknown> | undefined => ruleMap[name]
1750
1876
  const capabilities = collectLexicalCapabilities(lexicalRoots, resolveLexical)
1751
- assertLexicalCapabilityClosure(lexicalRoots, capabilities, resolveLexical)
1752
1877
  const enc = new Encoder(resolvedSettings, capabilities.capabilityComplete)
1753
1878
  enc.winners = ruleMap
1754
1879
  for (const name of names) enc.encodeRule(name, ruleMap[name]!)
package/src/table/ops.ts CHANGED
@@ -27,8 +27,9 @@ export const OP_SEQ = 3
27
27
  /** `SEQV n c1 … cn` — `SEQ` with `valueUnused`: terms run, no tuple is built. */
28
28
  export const OP_SEQV = 4
29
29
  /**
30
- * `CHOICE d n fx c1 … cn` — `d` indexes a dispatch table (or −1 for ordered
31
- * try); `fx` indexes the choice's OWN expected set.
30
+ * `CHOICE d n fx c1 … cn e1 … en` — `d` indexes a dispatch table (or −1 for
31
+ * ordered try); `fx` indexes the choice's OWN expected set and each `e` is one
32
+ * arm's set. Compiler-only rollback authority is deliberately not on the wire.
32
33
  *
33
34
  * Ordered failure keeps the expected sets at the deepest arm offset, merging
34
35
  * exact ties in source order. Exclusive dispatch propagates its sole selected
@@ -490,12 +490,53 @@ export type ResolvedTable = {
490
490
  */
491
491
  const TABLE_RUNTIME = Symbol('parseman.tableRuntime')
492
492
  type RuntimeTableProgram = TableProgram & {
493
- readonly [TABLE_RUNTIME]?: { resolved: ResolvedTable | undefined }
493
+ readonly [TABLE_RUNTIME]?: {
494
+ resolved: ResolvedTable | undefined
495
+ readonly choiceRollbackMasks?: ReadonlyMap<number, number>
496
+ readonly nonCommittingChoiceSites?: ReadonlySet<number>
497
+ readonly failureRollbackCleanSites?: ReadonlySet<number>
498
+ }
494
499
  }
495
500
 
496
501
  /** Give an internally-created program its fixed-shape runtime owner. */
497
- export function ownTableProgram(prog: TableProgram, resolved?: ResolvedTable): TableProgram {
498
- return { ...prog, [TABLE_RUNTIME]: { resolved } } as RuntimeTableProgram
502
+ export function ownTableProgram(
503
+ prog: TableProgram,
504
+ resolved?: ResolvedTable,
505
+ caches: {
506
+ readonly choiceRollbackMasks?: ReadonlyMap<number, number>
507
+ readonly failureRollbackCleanSites?: ReadonlySet<number>
508
+ readonly nonCommittingChoiceSites?: ReadonlySet<number>
509
+ } = {},
510
+ ): TableProgram {
511
+ const runtime = (prog as RuntimeTableProgram)[TABLE_RUNTIME]
512
+ return {
513
+ ...prog,
514
+ [TABLE_RUNTIME]: {
515
+ resolved,
516
+ choiceRollbackMasks: caches.choiceRollbackMasks ?? runtime?.choiceRollbackMasks,
517
+ nonCommittingChoiceSites: caches.nonCommittingChoiceSites ?? runtime?.nonCommittingChoiceSites,
518
+ failureRollbackCleanSites: caches.failureRollbackCleanSites ?? runtime?.failureRollbackCleanSites,
519
+ },
520
+ } as RuntimeTableProgram
521
+ }
522
+
523
+ /** Compiler-only choice rollback authority, consumed while printing a static
524
+ * assembly and never serialized into the shipped table. */
525
+ export function choiceRollbackMask(prog: TableProgram, ip: number): number | undefined {
526
+ return (prog as RuntimeTableProgram)[TABLE_RUNTIME]?.choiceRollbackMasks?.get(ip)
527
+ }
528
+
529
+ /** Compiler-only proof that no arm at this choice can raise a committed
530
+ * failure. Static assembly may defer its diagnostic merge until total failure. */
531
+ export function choiceCannotCommit(prog: TableProgram, ip: number): boolean {
532
+ return (prog as RuntimeTableProgram)[TABLE_RUNTIME]?.nonCommittingChoiceSites?.has(ip) === true
533
+ }
534
+
535
+ /** Compiler-only authority that a failed transactional child cannot leak any
536
+ * capture, trivia, field, or error sink. Static assembly consumes it to omit the
537
+ * enclosing mark and rollback; it is never serialized into the shipped table. */
538
+ export function failureRollbackClean(prog: TableProgram, ip: number): boolean {
539
+ return (prog as RuntimeTableProgram)[TABLE_RUNTIME]?.failureRollbackCleanSites?.has(ip) === true
499
540
  }
500
541
 
501
542
  /** A compiled entry, shaped exactly like a codegen rule function. */
@@ -1,4 +1,8 @@
1
- import { OP_LIT, OP_NOT, OP_RX } from './ops.ts'
1
+ import {
2
+ OP_ATTEMPT, OP_CHOICE, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF, OP_LIT, OP_NODE, OP_NOT,
3
+ OP_PEEK, OP_RULE, OP_RX, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ, OP_SEQV,
4
+ OP_SEQX, OP_TOKEN, OP_XFORM,
5
+ } from './ops.ts'
2
6
 
3
7
  /** Pure recognition only. Consumption owns value, CST, line, failure and cursor effects. */
4
8
  export type ScalarRecognizer = (input: string, pos: number, seed?: number) => number
@@ -49,3 +53,211 @@ export function scalarTerminalNotChild(code: ArrayLike<number>, ip: number): num
49
53
  const op = code[child]
50
54
  return op === OP_RX || op === OP_LIT ? child : -1
51
55
  }
56
+
57
+ /**
58
+ * A direct scalar terminal that every successful execution of `ip` must match
59
+ * first, through wrappers that cannot consume or select before their child.
60
+ * Returns only after `minDepth` edges so a caller never duplicates recognition
61
+ * merely to avoid one already-cheap terminal call.
62
+ */
63
+ export function leadingScalarTerminal(
64
+ code: ArrayLike<number>, ip: number, minDepth = 2, throughAttempt = true, throughPeek = false,
65
+ ): number {
66
+ const seen = new Set<number>()
67
+ let at = ip
68
+ let depth = 0
69
+ while (!seen.has(at)) {
70
+ seen.add(at)
71
+ const op = code[at]
72
+ if (op === OP_LIT || op === OP_RX) return depth >= minDepth ? at : -1
73
+ if (op === OP_RULE || op === OP_LABEL || op === OP_TOKEN || (throughAttempt && op === OP_ATTEMPT)) {
74
+ at = code[at + 1]!
75
+ depth++
76
+ continue
77
+ }
78
+ // A positive lookahead has exactly its child's success language and no
79
+ // effects on success. Choice predecisions may therefore execute a scalar
80
+ // child to reject the enclosing arm early. This is opt-in because callers
81
+ // that require a CONSUMING opener (notably exact start-failure diagnostics)
82
+ // must continue to treat peek() as zero-width.
83
+ if (throughPeek && op === OP_PEEK) {
84
+ at = code[at + 1]!
85
+ depth++
86
+ continue
87
+ }
88
+ if (op === OP_GATE || op === OP_SCOPE || op === OP_SCOPE_CAP || op === OP_SCOPE_PLAIN
89
+ || op === OP_XFORM || op === OP_NODE || op === OP_FIELD || op === OP_LEAF) {
90
+ at = code[at + 2]!
91
+ depth++
92
+ continue
93
+ }
94
+ if (op === OP_SEQ || op === OP_SEQV) {
95
+ if (code[at + 1]! < 1) return -1
96
+ at = code[at + 2]!
97
+ depth++
98
+ continue
99
+ }
100
+ if (op === OP_SEQX) {
101
+ if (code[at + 2]! < 1) return -1
102
+ at = code[at + 3]!
103
+ depth++
104
+ continue
105
+ }
106
+ return -1
107
+ }
108
+ return -1
109
+ }
110
+
111
+ export type LeadingScalarSequence = {
112
+ readonly terminals: readonly number[]
113
+ /** The explicit ambient trivia slot between terms; -1 means no trivia. */
114
+ readonly trivia: number
115
+ }
116
+
117
+ /**
118
+ * The first three complete scalar terms of a sequence under an explicit trivia
119
+ * scope. Unlike `leadingScalarTerminal`, each returned terminal is the WHOLE
120
+ * corresponding sequence term: recognizing it therefore identifies the exact
121
+ * position at which the enclosing sequence would scan trivia for the next one.
122
+ *
123
+ * This is rejection authority only. Gates, attempts, transforms and builders
124
+ * may still make the arm fail after the prefix matched, so callers must enter
125
+ * the ordinary arm on a positive result and may only skip it on a miss.
126
+ */
127
+ export function leadingScalarSequence(
128
+ code: ArrayLike<number>, ip: number,
129
+ ): LeadingScalarSequence | undefined {
130
+ const wholeScalar = (start: number): number => {
131
+ const seen = new Set<number>()
132
+ let at = start
133
+ while (!seen.has(at)) {
134
+ seen.add(at)
135
+ const op = code[at]
136
+ if (op === OP_LIT || op === OP_RX) return at
137
+ if (op === OP_RULE || op === OP_TOKEN || op === OP_ATTEMPT) {
138
+ at = code[at + 1]!
139
+ continue
140
+ }
141
+ if (op === OP_GATE || op === OP_SCOPE_PLAIN
142
+ || op === OP_XFORM || op === OP_NODE || op === OP_FIELD || op === OP_LEAF) {
143
+ at = code[at + 2]!
144
+ continue
145
+ }
146
+ if (op === OP_SEQ || op === OP_SEQV) {
147
+ if (code[at + 1] !== 1) return -1
148
+ at = code[at + 2]!
149
+ continue
150
+ }
151
+ if (op === OP_SEQX) {
152
+ if (code[at + 2] !== 1) return -1
153
+ at = code[at + 3]!
154
+ continue
155
+ }
156
+ return -1
157
+ }
158
+ return -1
159
+ }
160
+
161
+ const seen = new Set<number>()
162
+ let at = ip
163
+ let ambientTrivia: number | undefined
164
+ while (!seen.has(at)) {
165
+ seen.add(at)
166
+ const op = code[at]
167
+ if (op === OP_RULE) {
168
+ at = code[at + 1]!
169
+ continue
170
+ }
171
+ if (op === OP_SCOPE_PLAIN) {
172
+ ambientTrivia = code[at + 1]!
173
+ at = code[at + 2]!
174
+ continue
175
+ }
176
+ if (op === OP_GATE || op === OP_XFORM || op === OP_NODE || op === OP_FIELD || op === OP_LEAF) {
177
+ at = code[at + 2]!
178
+ continue
179
+ }
180
+
181
+ const n = op === OP_SEQ || op === OP_SEQV
182
+ ? code[at + 1]!
183
+ : op === OP_SEQX ? code[at + 2]! : 0
184
+ if (n < 3 || ambientTrivia === undefined) return undefined
185
+ const base = op === OP_SEQX ? at + 3 : at + 2
186
+ const terminals = [
187
+ wholeScalar(code[base]!),
188
+ wholeScalar(code[base + 1]!),
189
+ wholeScalar(code[base + 2]!),
190
+ ]
191
+ return terminals.every(terminal => terminal >= 0)
192
+ ? { terminals, trivia: ambientTrivia }
193
+ : undefined
194
+ }
195
+ return undefined
196
+ }
197
+
198
+ /**
199
+ * A bounded union of literal prefixes that every successful execution of `ip`
200
+ * must enter through. Unlike `leadingScalarTerminal`, this may cross nested
201
+ * choices, but only when EVERY arm yields literal authority. Longer literals
202
+ * covered by a shorter prefix are removed (`@{-}` is already covered by `@{`).
203
+ */
204
+ export function leadingLiteralFamily(
205
+ code: ArrayLike<number>, constants: readonly unknown[], ip: number,
206
+ minDepth = 2, limit = 4,
207
+ ): readonly number[] | undefined {
208
+ const active = new Set<number>()
209
+
210
+ const walk = (at: number, depth: number): number[] | undefined => {
211
+ if (active.has(at)) return undefined
212
+ active.add(at)
213
+ try {
214
+ const op = code[at]
215
+ if (op === OP_LIT) {
216
+ const value = constants[code[at + 1]!]
217
+ return depth >= minDepth && typeof value === 'string' && value.length > 0 ? [at] : undefined
218
+ }
219
+ if (op === OP_RULE || op === OP_LABEL || op === OP_TOKEN || op === OP_ATTEMPT || op === OP_PEEK) {
220
+ return walk(code[at + 1]!, depth + 1)
221
+ }
222
+ if (op === OP_GATE || op === OP_SCOPE || op === OP_SCOPE_CAP || op === OP_SCOPE_PLAIN
223
+ || op === OP_XFORM || op === OP_NODE || op === OP_FIELD || op === OP_LEAF) {
224
+ return walk(code[at + 2]!, depth + 1)
225
+ }
226
+ if (op === OP_SEQ || op === OP_SEQV) {
227
+ return code[at + 1]! < 1 ? undefined : walk(code[at + 2]!, depth + 1)
228
+ }
229
+ if (op === OP_SEQX) {
230
+ return code[at + 2]! < 1 ? undefined : walk(code[at + 3]!, depth + 1)
231
+ }
232
+ if (op !== OP_CHOICE) return undefined
233
+
234
+ const n = code[at + 2]!
235
+ if (n < 1) return undefined
236
+ const out: number[] = []
237
+ for (let i = 0; i < n; i++) {
238
+ const branch = walk(code[at + 4 + i]!, depth + 1)
239
+ if (branch === undefined) return undefined
240
+ for (const terminal of branch) {
241
+ const value = constants[code[terminal + 1]!] as string
242
+ let covered = false
243
+ for (let j = 0; j < out.length; j++) {
244
+ const prior = constants[code[out[j]! + 1]!] as string
245
+ if (value.startsWith(prior)) { covered = true; break }
246
+ }
247
+ if (covered) continue
248
+ for (let j = out.length - 1; j >= 0; j--) {
249
+ const prior = constants[code[out[j]! + 1]!] as string
250
+ if (prior.startsWith(value)) out.splice(j, 1)
251
+ }
252
+ out.push(terminal)
253
+ if (out.length > limit) return undefined
254
+ }
255
+ }
256
+ return out
257
+ } finally {
258
+ active.delete(at)
259
+ }
260
+ }
261
+
262
+ return walk(ip, 0)
263
+ }
@@ -79,6 +79,13 @@ export const CAP_OFF = 1
79
79
  /** `ctx.captureTrivia === true` is guaranteed here. */
80
80
  export const CAP_ON = 2
81
81
 
82
+ /** Buffer raw-entry mode is not provable at this site. */
83
+ export const RAW_UNKNOWN = 0
84
+ /** Buffer retains raw entry values. */
85
+ export const RAW_CAPTURE = 1
86
+ /** Buffer retains only a raw source-order count. */
87
+ export const RAW_OMIT = 2
88
+
82
89
  export type SiteLabel = {
83
90
  /** `TRI_UNKNOWN`, `TRI_NONE`, or the trivia slot the enclosing scope installed. */
84
91
  readonly tri: number
@@ -87,19 +94,22 @@ export type SiteLabel = {
87
94
  * asymmetry note in this file's header. Never read as "guaranteed absent".
88
95
  */
89
96
  readonly buf: boolean
97
+ /** `RAW_UNKNOWN` / `RAW_CAPTURE` / `RAW_OMIT`. */
98
+ readonly raw: number
90
99
  /** `CAP_UNKNOWN` / `CAP_OFF` / `CAP_ON`. */
91
100
  readonly cap: number
92
101
  }
93
102
 
94
103
  /** The lattice's top: every entry point starts here. */
95
- export const TOP: SiteLabel = { tri: TRI_UNKNOWN, buf: false, cap: CAP_UNKNOWN }
104
+ export const TOP: SiteLabel = { tri: TRI_UNKNOWN, buf: false, raw: RAW_UNKNOWN, cap: CAP_UNKNOWN }
96
105
 
97
106
  function meet(a: SiteLabel, b: SiteLabel): SiteLabel {
98
107
  const tri = a.tri === b.tri ? a.tri : TRI_UNKNOWN
99
108
  const buf = a.buf && b.buf
109
+ const raw = a.raw === b.raw ? a.raw : RAW_UNKNOWN
100
110
  const cap = a.cap === b.cap ? a.cap : CAP_UNKNOWN
101
- if (tri === a.tri && buf === a.buf && cap === a.cap) return a
102
- return { tri, buf, cap }
111
+ if (tri === a.tri && buf === a.buf && raw === a.raw && cap === a.cap) return a
112
+ return { tri, buf, raw, cap }
103
113
  }
104
114
 
105
115
  /**
@@ -145,7 +155,7 @@ function transfer(code: Int32Array, ip: number, at: SiteLabel, hostCst: boolean)
145
155
  // (`encode.ts:1153-1158`), so it can only ever raise `cap` to `CAP_ON`.
146
156
  const cap = op === OP_SCOPE_CAP ? CAP_ON : at.cap
147
157
  if (tri === at.tri && cap === at.cap) return at
148
- return { tri, buf: at.buf, cap }
158
+ return { tri, buf: at.buf, raw: at.raw, cap }
149
159
  }
150
160
  if (op === OP_NODE || op === OP_NODE_TRACK) {
151
161
  const flags = code[ip + 3]!
@@ -156,14 +166,17 @@ function transfer(code: Int32Array, ip: number, at: SiteLabel, hostCst: boolean)
156
166
  // arrays instead of a CstCaptureBuf. Descendants still capture, but they
157
167
  // must take the generic collector branch rather than dereference `_cstBuf`.
158
168
  // False means UNKNOWN in this lattice, which is exactly the safe answer.
159
- return { tri: at.tri, buf: false, cap: CAP_OFF }
169
+ return { tri: at.tri, buf: false, raw: RAW_UNKNOWN, cap: CAP_OFF }
160
170
  }
161
171
  // `ctx._cstBuf = buf` and `ctx.captureTrivia = <literal>`, both unconditional
162
172
  // — `emit-assembly.ts`'s `OP_NODE` body opens the buffer before it descends
163
173
  // and closes it after, whatever the host mode is.
164
174
  const cap = ((flags & 4) !== 0 || hostCst) ? CAP_ON : CAP_OFF
165
- if (at.buf && cap === at.cap) return at
166
- return { tri: at.tri, buf: true, cap }
175
+ const raw = !hostCst && code[ip + 1]! >= 0 && code[ip + 4]! < 0 && (flags & 2) !== 0
176
+ ? RAW_OMIT
177
+ : RAW_CAPTURE
178
+ if (at.buf && raw === at.raw && cap === at.cap) return at
179
+ return { tri: at.tri, buf: true, raw, cap }
167
180
  }
168
181
  // THE TWO BOUNDARIES clear every capture sink for their child and contribute
169
182
  // ONE leaf for the whole match; `token()` clears `ctx.trivia` as well. So a
@@ -178,11 +191,11 @@ function transfer(code: Int32Array, ip: number, at: SiteLabel, hostCst: boolean)
178
191
  // an option.
179
192
  if (op === OP_TOKEN) {
180
193
  if (at.tri === TRI_NONE && !at.buf) return at
181
- return { tri: TRI_NONE, buf: false, cap: at.cap }
194
+ return { tri: TRI_NONE, buf: false, raw: RAW_UNKNOWN, cap: at.cap }
182
195
  }
183
196
  if (op === OP_LEAF) {
184
197
  if (!at.buf) return at
185
- return { tri: at.tri, buf: false, cap: at.cap }
198
+ return { tri: at.tri, buf: false, raw: RAW_UNKNOWN, cap: at.cap }
186
199
  }
187
200
  return at
188
201
  }
package/src/types.ts CHANGED
@@ -89,7 +89,7 @@ export type ParserDef =
89
89
  // it requires, so it carries `parser`'s first-set and a leading `peek()` gates
90
90
  // its choice arm (see `isPositiveLookahead` / `sequenceFirstSet`).
91
91
  | { tag: 'peek'; parser: Combinator<unknown> }
92
- | { tag: 'node'; type?: string; parser: Combinator<unknown>; build?: ((children: ReadonlyArray<unknown>, fields: FieldMap | undefined, span: { start: number; end: number }, rawChildren: ReadonlyArray<unknown>, triviaLog: readonly number[], state: unknown) => unknown) | undefined; buildSrc?: string; /** The IDENTIFIER the type argument was written as at the `node()` call site, when it was a plain identifier rather than a literal — ANALYSIS ONLY, never emitted. Lets the inline-`mk` matcher recognise `node(t, …, (…) => mk(t, …))` from a factory, where the two `t`s are provably one binding. */ typeSrc?: string; /** Resolved source of a NAMED `buildSrc` — ANALYSIS ONLY, never emitted. See `buildAnalysisSrc`. */ buildSigSrc?: string; /** Declared positional arity of `build`: author-declared via `node(..., { buildArity })`, or resolved from the reducer's declaration. Wins over source inspection. */ buildArity?: number; /** Why arity could not be resolved, for the diagnostic. */ buildArityUnresolved?: string; buildStaticError?: readonly string[]; unwrap?: boolean; collapse?: boolean; project?: number; captureTrivia?: boolean; trailingTrivia?: boolean; tags?: readonly string[] }
92
+ | { tag: 'node'; type?: string; parser: Combinator<unknown>; build?: ((children: ReadonlyArray<unknown>, fields: FieldMap | undefined, span: { start: number; end: number }, rawChildren: ReadonlyArray<unknown>, triviaLog: readonly number[], state: unknown) => unknown) | undefined; buildSrc?: string; /** The IDENTIFIER the type argument was written as at the `node()` call site, when it was a plain identifier rather than a literal — ANALYSIS ONLY, never emitted. Lets the inline-`mk` matcher recognise `node(t, …, (…) => mk(t, …))` from a factory, where the two `t`s are provably one binding. */ typeSrc?: string; /** Resolved source of a NAMED `buildSrc` — ANALYSIS ONLY, never emitted. See `buildAnalysisSrc`. */ buildSigSrc?: string; /** Macro analysis proved the fourth formal (`rawChildren`) unreferenced. Never inferred from an author-declared `buildArity`; carried through composed IR. */ buildRawUnused?: true; /** Declared positional arity of `build`: author-declared via `node(..., { buildArity })`, or resolved from the reducer's declaration. Wins over source inspection. */ buildArity?: number; /** Why arity could not be resolved, for the diagnostic. */ buildArityUnresolved?: string; buildStaticError?: readonly string[]; /** Import provenance for a direct builder's free names — each `{ local, source, imported }` lets a downstream compose() re-emit `import { imported as local } from source` so the inlined builder source can bind. Carried through the IR round-trip. */ buildImports?: ReadonlyArray<{ local: string; source: string; imported: string }>; unwrap?: boolean; collapse?: boolean; project?: number; captureTrivia?: boolean; trailingTrivia?: boolean; tags?: readonly string[] }
93
93
  // `predSrc`/`extraSrc` (set by the macro evaluator): SOURCE TEXT of the guard
94
94
  // predicate / the withCtx `extra` value, so codegen inlines them into `_mf`
95
95
  // rather than pushing a `null` source. Absent under runtime compile() (the real
package/src/version.ts CHANGED
@@ -13,4 +13,4 @@
13
13
  * Kept in sync with `package.json` by `test/unit/version-sync.test.ts`, which fails
14
14
  * the build if this constant drifts from the published version.
15
15
  */
16
- export const PARSEMAN_VERSION = '0.48.1'
16
+ export const PARSEMAN_VERSION = '0.50.0'