parseman 0.49.0 → 0.50.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) 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 +1824 -737
  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 +1252 -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/token-alphabet.d.ts +11 -0
  16. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  17. package/dist/compiler/token-capability.js +185 -20
  18. package/dist/compiler/token-capability.js.map +1 -1
  19. package/dist/cst/capture-buffer.d.ts +5 -1
  20. package/dist/cst/capture-buffer.d.ts.map +1 -1
  21. package/dist/index.js +1365 -279
  22. package/dist/index.js.map +3 -3
  23. package/dist/language-service/index.js +2 -1
  24. package/dist/language-service/index.js.map +1 -1
  25. package/dist/plugin/entry-supercompiler.d.ts +48 -0
  26. package/dist/plugin/entry-supercompiler.d.ts.map +1 -0
  27. package/dist/plugin/evaluator.d.ts.map +1 -1
  28. package/dist/plugin/index.d.ts.map +1 -1
  29. package/dist/plugin/index.js +1957 -385
  30. package/dist/plugin/index.js.map +3 -3
  31. package/dist/run/index.js +2 -1
  32. package/dist/run/index.js.map +1 -1
  33. package/dist/spec/index.js +28 -7
  34. package/dist/spec/index.js.map +1 -1
  35. package/dist/table/assemble.d.ts.map +1 -1
  36. package/dist/table/compile-rule-map.d.ts +6 -5
  37. package/dist/table/compile-rule-map.d.ts.map +1 -1
  38. package/dist/table/emit-assembly.d.ts +6 -1
  39. package/dist/table/emit-assembly.d.ts.map +1 -1
  40. package/dist/table/encode.d.ts.map +1 -1
  41. package/dist/table/index.js +1315 -231
  42. package/dist/table/index.js.map +3 -3
  43. package/dist/table/ops.d.ts +3 -2
  44. package/dist/table/ops.d.ts.map +1 -1
  45. package/dist/table/program.d.ts +15 -1
  46. package/dist/table/program.d.ts.map +1 -1
  47. package/dist/table/scalar-terminal.d.ts +30 -0
  48. package/dist/table/scalar-terminal.d.ts.map +1 -1
  49. package/dist/table/site-labels.d.ts +8 -0
  50. package/dist/table/site-labels.d.ts.map +1 -1
  51. package/dist/types.d.ts +2 -1
  52. package/dist/types.d.ts.map +1 -1
  53. package/dist/version.d.ts +1 -1
  54. package/package.json +1 -1
  55. package/src/analysis/commitment.ts +19 -8
  56. package/src/analysis/rebuild.ts +1 -0
  57. package/src/combinators/trivia-skip.ts +2 -2
  58. package/src/compiler/build-arity.ts +50 -0
  59. package/src/compiler/degradation.ts +0 -6
  60. package/src/compiler/ir-serialize.ts +6 -4
  61. package/src/compiler/token-alphabet.ts +322 -28
  62. package/src/cst/capture-buffer.ts +34 -11
  63. package/src/plugin/entry-supercompiler.ts +567 -0
  64. package/src/plugin/evaluator.ts +10 -0
  65. package/src/plugin/index.ts +7 -3
  66. package/src/table/assemble.ts +125 -42
  67. package/src/table/compile-rule-map.ts +8 -7
  68. package/src/table/compile.ts +7 -8
  69. package/src/table/emit-assembly.ts +646 -126
  70. package/src/table/emit.ts +1 -1
  71. package/src/table/encode.ts +152 -7
  72. package/src/table/ops.ts +3 -2
  73. package/src/table/program.ts +44 -3
  74. package/src/table/scalar-terminal.ts +213 -1
  75. package/src/table/site-labels.ts +22 -9
  76. package/src/types.ts +1 -1
  77. 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,25 @@ 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>()
148
+ /**
149
+ * `hasSideWriter` memo for `failureNeedsRollback`, shared across every call
150
+ * for the lifetime of this `Encoder`. A cached entry is written only when
151
+ * `complete` is true, which means the answer is a path-independent fact
152
+ * about that combinator node — it does not depend on which root's traversal
153
+ * asked, only on the node's own reachable subgraph. Scoping the memo per call
154
+ * (as a local inside `failureNeedsRollback`) was correct but discarded that
155
+ * fact after every arm, so a grammar with many ordered-choice arms / optional
156
+ * / attempt sites re-walked the SAME shared rule graph from scratch for each
157
+ * site — O(sites × graph size) instead of O(graph size). Measured on the
158
+ * scss-parser grammar (composed CSS-base superset, the deepest/largest of the
159
+ * four grammars): 47-50s for one `compileRuleMap` with the per-call memo,
160
+ * versus the whole macro transform completing in under a second once this
161
+ * memo is hoisted here.
162
+ */
163
+ private sideWriterMemo = new Map<Combinator<unknown>, boolean>()
144
164
  labels: readonly string[] | undefined = undefined
145
165
  /** Trivia table in scope at the row being encoded — codegen's `ctx.activeTrivia`. */
146
166
  activeTrivia: Combinator<unknown> | undefined = undefined
@@ -611,6 +631,81 @@ class Encoder {
611
631
  return w === undefined ? undefined : (name: string) => w[name]
612
632
  }
613
633
 
634
+ /**
635
+ * Whether a failed ordered-choice arm can leave state that the enclosing
636
+ * choice must restore before trying its successor.
637
+ *
638
+ * `mayLeavePartialCapture` is the existing sound CST/trivia proof. The extra
639
+ * writer census covers the side sinks that proof deliberately does not model:
640
+ * fields, recovered errors, and interpreted scanners. Strict assemblies do
641
+ * not run repeat recovery, but these rows can still publish through a caller's
642
+ * context. Cycles are safe to stop: revisiting the same combinator cannot
643
+ * discover a writer the first traversal did not already reach.
644
+ */
645
+ private failureNeedsRollback(
646
+ p: Combinator<unknown>,
647
+ autoNot: readonly AutoNotCheck[] | null | undefined,
648
+ ): boolean {
649
+ const refName = (p as unknown as { _ruleName?: string })._ruleName
650
+ const winner = refName === undefined ? undefined : this.winners?.[refName]
651
+ const root = winner !== undefined && !winnerWrapsReference(winner, p) ? winner : p
652
+ const sideWriterMemo = this.sideWriterMemo
653
+ // A single mutable `seen` set threaded through the whole call, rather than
654
+ // copied at every recursion level: this DFS visits children strictly
655
+ // sequentially, so marking `current` in progress before recursing and
656
+ // clearing it once its subtree is fully explored reproduces the exact same
657
+ // ancestor-path membership an immutable copy-per-level would have computed,
658
+ // without the O(depth) allocation on every node.
659
+ const seen = new Set<Combinator<unknown>>()
660
+ const hasSideWriter = (current: Combinator<unknown>): { found: boolean; complete: boolean } => {
661
+ const cached = sideWriterMemo.get(current)
662
+ if (cached !== undefined) return { found: cached, complete: true }
663
+ if (seen.has(current)) return { found: false, complete: false }
664
+ seen.add(current)
665
+ const def = current._def
666
+ let result: { found: boolean; complete: boolean }
667
+ switch (def.tag) {
668
+ case 'field':
669
+ case 'expect':
670
+ case 'recover':
671
+ case 'scanTo':
672
+ case 'unknown':
673
+ result = { found: true, complete: true }
674
+ break
675
+ case 'lazy': {
676
+ const name = (current as unknown as { _ruleName?: string })._ruleName
677
+ const resolvedWinner = name === undefined ? undefined : this.winners?.[name]
678
+ if (resolvedWinner !== undefined && !winnerWrapsReference(resolvedWinner, current)) {
679
+ result = hasSideWriter(resolvedWinner)
680
+ break
681
+ }
682
+ try { result = hasSideWriter(def.thunk()) }
683
+ catch { result = { found: true, complete: true } }
684
+ break
685
+ }
686
+ default: {
687
+ let complete = true
688
+ result = { found: false, complete: true }
689
+ for (const child of childrenOf(def)) {
690
+ const childResult = hasSideWriter(child)
691
+ if (childResult.found) {
692
+ result = { found: true, complete: true }
693
+ break
694
+ }
695
+ complete &&= childResult.complete
696
+ }
697
+ if (!result.found) result = { found: false, complete }
698
+ }
699
+ }
700
+ seen.delete(current)
701
+ if (result.found || result.complete) sideWriterMemo.set(current, result.found)
702
+ return result
703
+ }
704
+ return mayLeavePartialCapture(root, new Set(), true)
705
+ || hasSideWriter(root).found
706
+ || ((autoNot?.length ?? 0) > 0 && capturesLeaf(root))
707
+ }
708
+
614
709
  /** A first set becomes a char class string, or −1 for `any`. */
615
710
  private charClass(fs: FirstSet): number {
616
711
  if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
@@ -1074,14 +1169,53 @@ class Encoder {
1074
1169
  // Arm ORDER is preserved on both, which is what makes this a PEG-safe
1075
1170
  // change rather than a reordering.
1076
1171
  const finalChoice = classifyFinalChoice(arms, rr)
1077
- const classes = arms.map((_, i) => finalChoice.nullable[i]
1078
- ? -1 : this.charClass(finalChoice.firstSets[i]!))
1172
+ // attempt() preserves its child's success language, but the broad
1173
+ // classifier deliberately treats the transaction as nullable/unknown so
1174
+ // it can never by itself authorize exclusive one-arm selection. Recover
1175
+ // only the narrower fact needed by an ORDERED choice: a non-nullable
1176
+ // child can receive an individual rejection gate. Keep that authority
1177
+ // out if it would make an otherwise open site fully disjoint, preserving
1178
+ // the transaction's standing exclusion from exclusive dispatch.
1179
+ const transactionalFirst = arms.map((arm, i): FirstSet | undefined => {
1180
+ if (!finalChoice.nullable[i]) return undefined
1181
+ const def = arm._def as ParserDef
1182
+ if (def.tag !== 'attempt' || matchesEmpty(def.parser, new Set(), rr)) return undefined
1183
+ return firstSetOf(def.parser, new Set(), rr)
1184
+ })
1185
+ const gateFirst = finalChoice.firstSets.map((first, i) => transactionalFirst[i] ?? first)
1186
+ const classes = gateFirst.map((first, i) => finalChoice.nullable[i] && transactionalFirst[i] === undefined
1187
+ ? -1 : this.charClass(first))
1188
+ if (transactionalFirst.some(first => first !== undefined) && classes.every(cls => cls >= 0)) {
1189
+ let overlap = false
1190
+ for (let i = 0; i < gateFirst.length && !overlap; i++) {
1191
+ for (let j = i + 1; j < gateFirst.length; j++) {
1192
+ if (intersects(gateFirst[i]!, gateFirst[j]!)) { overlap = true; break }
1193
+ }
1194
+ }
1195
+ if (!overlap) for (let i = 0; i < classes.length; i++) {
1196
+ if (transactionalFirst[i] !== undefined) classes[i] = -1
1197
+ }
1198
+ }
1079
1199
  const dispIdx = this.disp.length
1080
1200
  this.disp.push(classes)
1081
1201
  // PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. While the best
1082
1202
  // failure remains at the choice position, an arm declined by the driver's
1083
1203
  // char gate contributes the static opener set the interpreter would get by
1084
1204
  // entering it. A deeper dynamic arm failure discards those shallower sets.
1205
+ // One compiler-only bit mask records which failed arms can actually
1206
+ // dirty a rollback sink. It is consumed while printing emitted strict
1207
+ // assemblies and never serialized; tolerant assemblies retain the
1208
+ // universal rollback because repeat recovery can publish an error from
1209
+ // inside any arm. Arity >31 falls back to -1 rather than truncating.
1210
+ let rollbackMask = kids.length > 31 ? -1 : 0
1211
+ if (rollbackMask === 0) {
1212
+ for (let i = 0; i < arms.length; i++) {
1213
+ const src = order[i]!
1214
+ if (this.failureNeedsRollback(arms[i]!, d.autoNot[src])) {
1215
+ rollbackMask |= 1 << i
1216
+ }
1217
+ }
1218
+ }
1085
1219
  const head = this.emitHead(OP_CHOICE, 3 + 2 * kids.length)
1086
1220
  this.code[head + 1] = dispIdx
1087
1221
  this.code[head + 2] = kids.length
@@ -1093,6 +1227,10 @@ class Encoder {
1093
1227
  this.code[head + 4 + i] = kids[i]!
1094
1228
  this.code[head + 4 + kids.length + i] = this.expected(deriveExpected(arms[i]!))
1095
1229
  }
1230
+ this.choiceRollbackMasks.set(head, rollbackMask)
1231
+ if (arms.every(arm => !mayCommitFailure(arm, new Set(), rr))) {
1232
+ this.nonCommittingChoiceSites.add(head)
1233
+ }
1096
1234
  return head
1097
1235
  }
1098
1236
  case 'many':
@@ -1143,8 +1281,11 @@ class Encoder {
1143
1281
  ? this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags, this.expected(deriveExpected(d.parser)))
1144
1282
  : this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags)
1145
1283
  }
1146
- case 'optional':
1147
- return this.emit(OP_OPT, this.node(d.parser).ip)
1284
+ case 'optional': {
1285
+ const head = this.emit(OP_OPT, this.node(d.parser).ip)
1286
+ if (!this.failureNeedsRollback(d.parser, undefined)) this.failureRollbackCleanSites.add(head)
1287
+ return head
1288
+ }
1148
1289
  case 'transform': {
1149
1290
  // Declared on the def and not lowered here. Refuse rather than assume it is
1150
1291
  // inert: a recognition-only transform suppresses its value, and a table that
@@ -1444,6 +1585,7 @@ class Encoder {
1444
1585
  // lowering was correct only for a choice arm.
1445
1586
  case 'attempt': {
1446
1587
  const inner = this.emit(OP_ATTEMPT, this.node(d.parser).ip)
1588
+ if (!this.failureNeedsRollback(d.parser, undefined)) this.failureRollbackCleanSites.add(inner)
1447
1589
  // THE FIRST-SET FAIL-FAST GUARD, lowered as the `OP_GATE` row the `node()`
1448
1590
  // case already uses — it is the same guard, written twice in the
1449
1591
  // interpreter (`attempt.ts:22`, `node.ts:239`). It is NOT merely an
@@ -1652,6 +1794,10 @@ class Encoder {
1652
1794
  ...(this.rec ? { rec: 1 as const } : {}),
1653
1795
  ...(this.cov === undefined ? {} : { cov: this.cov }),
1654
1796
  lines: this.track ? 1 : 0,
1797
+ }, undefined, {
1798
+ choiceRollbackMasks: this.choiceRollbackMasks,
1799
+ failureRollbackCleanSites: this.failureRollbackCleanSites,
1800
+ nonCommittingChoiceSites: this.nonCommittingChoiceSites,
1655
1801
  })
1656
1802
  }
1657
1803
  }
@@ -1748,7 +1894,6 @@ export function encodeTableProgram(
1748
1894
  const lexicalRoots = [...names].sort().map(name => ruleMap[name]!)
1749
1895
  const resolveLexical = (name: string): Combinator<unknown> | undefined => ruleMap[name]
1750
1896
  const capabilities = collectLexicalCapabilities(lexicalRoots, resolveLexical)
1751
- assertLexicalCapabilityClosure(lexicalRoots, capabilities, resolveLexical)
1752
1897
  const enc = new Encoder(resolvedSettings, capabilities.capabilityComplete)
1753
1898
  enc.winners = ruleMap
1754
1899
  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[]; /** 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[] }
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.49.0'
16
+ export const PARSEMAN_VERSION = '0.50.1'