parseman 0.56.0 → 0.58.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 (106) hide show
  1. package/dist/analysis/diagnostics.js +2232 -1088
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +2247 -1103
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/dispatch.d.ts.map +1 -1
  6. package/dist/combinators/node.d.ts +1 -1
  7. package/dist/compiler/build-arity.d.ts +1 -1
  8. package/dist/compiler/build-arity.d.ts.map +1 -1
  9. package/dist/compiler/token-alphabet.d.ts +11 -1
  10. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  11. package/dist/compiler/token-capability.js +69 -16
  12. package/dist/compiler/token-capability.js.map +2 -2
  13. package/dist/cst/capture-buffer.d.ts +6 -0
  14. package/dist/cst/capture-buffer.d.ts.map +1 -1
  15. package/dist/cst/trivia-kinds.d.ts +9 -1
  16. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  17. package/dist/index.js +1446 -236
  18. package/dist/index.js.map +3 -3
  19. package/dist/language-service/index.js +1 -0
  20. package/dist/language-service/index.js.map +1 -1
  21. package/dist/parse-context.d.ts.map +1 -1
  22. package/dist/plugin/entry-supercompiler.d.ts +1 -1
  23. package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
  24. package/dist/plugin/index.d.ts +51 -1
  25. package/dist/plugin/index.d.ts.map +1 -1
  26. package/dist/plugin/index.js +2126 -380
  27. package/dist/plugin/index.js.map +3 -3
  28. package/dist/plugin/ir-eval.d.ts +2 -0
  29. package/dist/plugin/ir-eval.d.ts.map +1 -1
  30. package/dist/regex/classes.d.ts.map +1 -1
  31. package/dist/run/index.js +1 -0
  32. package/dist/run/index.js.map +1 -1
  33. package/dist/table/assemble.d.ts +9 -0
  34. package/dist/table/assemble.d.ts.map +1 -1
  35. package/dist/table/child-slots.d.ts +4 -6
  36. package/dist/table/child-slots.d.ts.map +1 -1
  37. package/dist/table/compile-rule-map.d.ts +14 -2
  38. package/dist/table/compile-rule-map.d.ts.map +1 -1
  39. package/dist/table/emit-assembly.d.ts +17 -2
  40. package/dist/table/emit-assembly.d.ts.map +1 -1
  41. package/dist/table/emit.d.ts +5 -1
  42. package/dist/table/emit.d.ts.map +1 -1
  43. package/dist/table/encode.d.ts.map +1 -1
  44. package/dist/table/exec-baseline.d.ts.map +1 -1
  45. package/dist/table/exec.d.ts.map +1 -1
  46. package/dist/table/factor-choice.d.ts +42 -0
  47. package/dist/table/factor-choice.d.ts.map +1 -0
  48. package/dist/table/fail-clean.d.ts +3 -0
  49. package/dist/table/fail-clean.d.ts.map +1 -0
  50. package/dist/table/hold-gap.d.ts +8 -0
  51. package/dist/table/hold-gap.d.ts.map +1 -0
  52. package/dist/table/index.js +1798 -316
  53. package/dist/table/index.js.map +3 -3
  54. package/dist/table/lex-program.d.ts +7 -3
  55. package/dist/table/lex-program.d.ts.map +1 -1
  56. package/dist/table/ops.d.ts +17 -6
  57. package/dist/table/ops.d.ts.map +1 -1
  58. package/dist/table/program.d.ts +13 -0
  59. package/dist/table/program.d.ts.map +1 -1
  60. package/dist/table/run-support.d.ts +19 -0
  61. package/dist/table/run-support.d.ts.map +1 -1
  62. package/dist/table/scalar-terminal.d.ts +2 -0
  63. package/dist/table/scalar-terminal.d.ts.map +1 -1
  64. package/dist/table/select-hot.d.ts +6 -0
  65. package/dist/table/select-hot.d.ts.map +1 -0
  66. package/dist/table/site-labels.d.ts +66 -0
  67. package/dist/table/site-labels.d.ts.map +1 -1
  68. package/dist/table/trivia-gap.d.ts +31 -0
  69. package/dist/table/trivia-gap.d.ts.map +1 -0
  70. package/dist/types.d.ts +17 -0
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/version.d.ts +1 -1
  73. package/package.json +4 -1
  74. package/src/combinators/dispatch.ts +12 -3
  75. package/src/combinators/node.ts +6 -13
  76. package/src/combinators/trivia-skip.ts +44 -1
  77. package/src/compiler/build-arity.ts +3 -3
  78. package/src/compiler/token-alphabet.ts +57 -17
  79. package/src/cst/capture-buffer.ts +25 -0
  80. package/src/cst/trivia-kinds.ts +12 -1
  81. package/src/parse-context.ts +1 -0
  82. package/src/plugin/entry-supercompiler.ts +8 -9
  83. package/src/plugin/index.ts +362 -42
  84. package/src/plugin/ir-eval.ts +7 -1
  85. package/src/regex/classes.ts +3 -0
  86. package/src/table/assemble.ts +371 -83
  87. package/src/table/child-slots.ts +6 -8
  88. package/src/table/compile-rule-map.ts +64 -15
  89. package/src/table/emit-assembly.ts +244 -42
  90. package/src/table/emit.ts +11 -8
  91. package/src/table/encode.ts +149 -35
  92. package/src/table/exec-baseline.ts +1 -3
  93. package/src/table/exec.ts +34 -14
  94. package/src/table/factor-choice.ts +173 -0
  95. package/src/table/fail-clean.ts +130 -0
  96. package/src/table/hold-gap.ts +68 -0
  97. package/src/table/lex-program.ts +197 -43
  98. package/src/table/ops.ts +18 -7
  99. package/src/table/program.ts +24 -0
  100. package/src/table/run-support.ts +35 -0
  101. package/src/table/scalar-terminal.ts +8 -0
  102. package/src/table/select-hot.ts +70 -0
  103. package/src/table/site-labels.ts +135 -4
  104. package/src/table/trivia-gap.ts +109 -0
  105. package/src/types.ts +18 -0
  106. package/src/version.ts +1 -1
@@ -1,4 +1,5 @@
1
1
  import { scanRefs } from './program.ts'
2
+ import { factorSharedEnds } from './factor-choice.ts'
2
3
  import type { AutoNotCheck, Combinator, FirstSet, ParserDef } from '../types.ts'
3
4
  import { classifyFinalChoice, firstSetOf, intersects, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
4
5
  import { childrenOf } from '../analysis/gating.ts'
@@ -16,7 +17,7 @@ import {
16
17
  OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_EXPECT, OP_SEQX, OP_SCAN,
17
18
  OP_LIVE, OP_ATTEMPT, OP_LABEL,
18
19
  OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD,
19
- OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_COV,
20
+ OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE, OP_COV,
20
21
  OP_LEX_BODY, OP_LEX_PROGRAM,
21
22
  } from './ops.ts'
22
23
  import { adjacencyExpected } from '../combinators/adjacency.ts'
@@ -27,12 +28,12 @@ import type { BalancedSpec } from '../combinators/scanTo.ts'
27
28
  import type { DispatchSpec, LexBodySpec, LexProgramSpec, ScanSpec, SubtreeRef, TableProgram, TriviaSpec } from './program.ts'
28
29
  import {
29
30
  lexProgramDigest,
30
- LEX_NODE_CHOICE2, LEX_NODE_CHOICE4, LEX_NODE_CHOICE8,
31
- LEX_NODE_NOT, LEX_NODE_OPTIONAL,
31
+ LEX_NODE_CHOICE,
32
+ LEX_NODE_NOT, LEX_NODE_OPTIONAL, LEX_NODE_REPEAT,
32
33
  LEX_NODE_SEQUENCE2, LEX_NODE_SEQUENCE3, LEX_NODE_SEQUENCE4, LEX_NODE_SEQUENCE5,
33
34
  LEX_NODE_TERMINAL,
34
35
  } from './lex-program.ts'
35
- import { covKindCode, encodeClassSpec, ownTableProgram } from './program.ts'
36
+ import { covKindCode, dispatchIsExclusive, encodeClassSpec, ownTableProgram } from './program.ts'
36
37
  import type { GrammarCoveragePlan } from '../compiler/grammar-coverage-ids.ts'
37
38
  import { directArrayProjection } from '../compiler/direct-projection.ts'
38
39
  import {
@@ -298,15 +299,18 @@ class Encoder {
298
299
  }
299
300
  case 'choice': {
300
301
  for (const arm of node.arms) if (!emitNode(arm)) return false
301
- const op = node.arms.length === 2 ? LEX_NODE_CHOICE2
302
- : node.arms.length === 4 ? LEX_NODE_CHOICE4
303
- : node.arms.length === 8 ? LEX_NODE_CHOICE8 : -1
304
- if (op < 0) return false
305
- words.push(op, this.expected(node.expected))
302
+ // The arm classes and the exclusive (one-arm) decision are the
303
+ // character choice's own (`case 'choice'`), under the trivia the
304
+ // encoder would hold at that row, so both lowerings rank the same
305
+ // failures.
306
+ const final = classifyFinalChoice(
307
+ node.parsers, this.refResolver(), node.cleared ? undefined : this.activeTrivia,
308
+ )
309
+ const classes = final.firstSets.map((first, i) => final.nullable[i] ? -1 : this.charClass(first))
310
+ const exclusive = final.exclusive && classes.every(cls => cls >= 0)
311
+ words.push(LEX_NODE_CHOICE, node.arms.length, exclusive ? 1 : 0, this.expected(node.expected))
306
312
  for (let i = 0; i < node.arms.length; i++) {
307
- const first = node.firstSets[i]!
308
- words.push(first.kind === 'ranges' ? this.charClass(first) : -1)
309
- words.push(this.expected(node.armExpected[i]!))
313
+ words.push(classes[i]!, this.expected(node.armExpected[i]!))
310
314
  }
311
315
  return true
312
316
  }
@@ -318,6 +322,10 @@ class Encoder {
318
322
  if (!emitNode(node.body)) return false
319
323
  words.push(LEX_NODE_OPTIONAL)
320
324
  return true
325
+ case 'repeat':
326
+ if (!emitNode(node.body)) return false
327
+ words.push(LEX_NODE_REPEAT, node.min, node.max)
328
+ return true
321
329
  }
322
330
  }
323
331
  if (!emitNode(body.root)) return undefined
@@ -699,6 +707,38 @@ class Encoder {
699
707
  }
700
708
  }
701
709
 
710
+ /**
711
+ * THE FIRST-CHARACTER GATE OF AN `optional`, `not` OR `peek` CHILD.
712
+ *
713
+ * A non-nullable child cannot match at a character outside its first set, so
714
+ * the wrapper's outcome there is already decided: `optional` yields `null`,
715
+ * `not` succeeds, `peek` fails with its own set. The interpreter's `optional`
716
+ * makes that test itself (`repeat.ts` `startsFirstSet`); the table entered the
717
+ * child — a rollback mark, the call, the child's own failure — to learn it.
718
+ *
719
+ * The gate is an `OP_GATE` row around the child, so every engine runs it
720
+ * unchanged, a probe or tolerant parse forwards through it as for a node's
721
+ * gate, and the wrapper's row keeps its layout. The engines test the class
722
+ * before taking the wrapper's mark. The first set is resolved through
723
+ * `winners`, the analysis a choice arm's gate uses, so a composed rule's set
724
+ * is the merged one. A scalar `not` child stays bare: `scalarTerminalNotChild`
725
+ * already reduces it to one recognizer call.
726
+ */
727
+ private lookaheadGate(p: Combinator<unknown>, child: number): number {
728
+ // By opcode FAMILY: a line-tracked variant spells the same terminal
729
+ // `OP_LIT_TRACK`/`OP_RX_TRACK`, and every variant of one grammar must lay
730
+ // down the same words (`foldPrograms`).
731
+ const op = this.code[child]
732
+ if (op === OP_GATE || op === OP_LIT || op === OP_LIT_CI || op === OP_LIT_TRACK || op === OP_LIT_CI_TRACK
733
+ || op === OP_RX || op === OP_RX_TRACK) return child
734
+ const rr = this.refResolver()
735
+ if (matchesEmpty(p, new Set(), rr)) return child
736
+ const cls = this.charClass(firstSetOf(p, new Set(), rr, this.activeTrivia))
737
+ if (cls < 0) return child
738
+ const e = deriveExpected(p)
739
+ return this.emit(OP_GATE, cls, child, this.expected(e.length > 0 ? e : [p._tag]))
740
+ }
741
+
702
742
  /** A first set becomes a char class string, or −1 for `any`. */
703
743
  private charClass(fs: FirstSet): number {
704
744
  if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
@@ -715,14 +755,21 @@ class Encoder {
715
755
  return i
716
756
  }
717
757
 
718
- /** Bind a guard only when its class is already part of the program. Repeat
719
- * gating must not grow the compact artifact merely to avoid a failed call. */
720
- private existingCharClass(fs: FirstSet): number {
721
- if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
722
- const hit = this.ccIndex.get(encodeClassSpec(fs.ranges))
723
- // This reuses the recovery row's existing `-1` slot. Keep its decimal width
724
- // at two bytes or less so a speed hint cannot grow a compact artifact.
725
- return hit !== undefined && hit < 100 ? hit : -1
758
+ /**
759
+ * The first set a node's or an attempt's fail-fast gate tests. A bare
760
+ * cross-rule ref resolves through `winners` (a compose OVERRIDE may widen
761
+ * it). Any other body keeps its construction-time `_meta.firstSet`, except
762
+ * where that is `any`: under `compose()` a body that reaches a rule of
763
+ * another piece is built before that rule exists, so its cached set is
764
+ * `any` and the node got NO gate (jess's `CompoundSelector` was entered at
765
+ * `{`, `Call` at `}`). The resolved set is the one a choice arm's gate uses,
766
+ * over the final merged map.
767
+ */
768
+ private gateFirstSet(p: Combinator<unknown>): FirstSet {
769
+ const cached = p._meta.firstSet
770
+ return p._def.tag === 'lazy' || cached.kind === 'any'
771
+ ? firstSetOf(p, new Set(), this.refResolver(), this.activeTrivia)
772
+ : cached
726
773
  }
727
774
 
728
775
  /**
@@ -901,6 +948,54 @@ class Encoder {
901
948
  return this.emit(OP_SCOPE_PLAIN, this.triviaSlot(amb), ip)
902
949
  }
903
950
 
951
+ /**
952
+ * The `OP_RGATE` operands of a choice arm that opens with `routed()`, or
953
+ * `undefined`. Through `node()`/scope wrappers and first sequence terms, the
954
+ * arm's next term after the routed token is found; the gate is that term's
955
+ * first characters, with the trivia in force at the boundary before it, so
956
+ * the arm is entered only where it can get past the token. The term must be
957
+ * unable to match empty or to commit, so a miss is exactly that term failing
958
+ * at the token's end with its own set.
959
+ */
960
+ private routedArmGate(arm: Combinator<unknown>): { cls: number; fx: number } | undefined {
961
+ type Follow = 'end' | { next: Combinator<unknown>; boundary: Combinator<unknown> | undefined } | undefined
962
+ const walk = (p: Combinator<unknown>, trivia: Combinator<unknown> | undefined, depth: number): Follow => {
963
+ if (depth > 16) return undefined
964
+ const d = p._def as ParserDef
965
+ switch (d.tag) {
966
+ case 'routed': return 'end'
967
+ case 'node': return walk(d.parser, trivia, depth + 1)
968
+ case 'grammar':
969
+ return walk(d.parser, d.clearTrivia === true ? undefined : d.triviaParser ?? trivia, depth + 1)
970
+ case 'sequence': {
971
+ const head = d.parsers[0]
972
+ if (head === undefined) return undefined
973
+ const r = walk(head, trivia, depth + 1)
974
+ if (r !== 'end') return r
975
+ const next = d.parsers[1]
976
+ return next === undefined ? 'end' : { next, boundary: trivia }
977
+ }
978
+ default: return undefined
979
+ }
980
+ }
981
+ const follow = walk(arm, this.activeTrivia, 0)
982
+ if (follow === undefined || follow === 'end') return undefined
983
+ const rr = this.refResolver()
984
+ const { next, boundary } = follow
985
+ if (matchesEmpty(next, new Set(), rr) || mayCommitFailure(next, rr, this.commitFailureMemo)) return undefined
986
+ // A miss reports `deriveExpected(next)`, a static set. Only a terminal or a
987
+ // label fails with exactly that set: a sequence over an `optional` prefix
988
+ // reports the prefix's labels statically but not when it runs, and a choice
989
+ // whose arms share an opener reports that opener once statically, once per
990
+ // arm when it runs.
991
+ const nextTag = (next._def as ParserDef).tag
992
+ if (nextTag !== 'literal' && nextTag !== 'regex' && nextTag !== 'keywords' && nextTag !== 'label') return undefined
993
+ const first = firstSetOf(next, new Set(), rr, boundary)
994
+ const cls = this.charClass(boundary === undefined ? first : union(first, firstSetOf(boundary, new Set(), rr)))
995
+ if (cls < 0) return undefined
996
+ return { cls, fx: this.expected(deriveExpected(next)) }
997
+ }
998
+
904
999
  node(p: Combinator<unknown>): Emitted {
905
1000
  // See `refMemo`: a reference is memoised per REFERENCING trivia scope, every
906
1001
  // other construct once for the program.
@@ -1062,6 +1157,12 @@ class Encoder {
1062
1157
  return head
1063
1158
  }
1064
1159
  case 'choice': {
1160
+ // Arms that differ in one term are that term's choice (`factor-choice.ts`).
1161
+ // Not under a coverage plan, which credits the arms as written.
1162
+ if (this.plan === undefined) {
1163
+ const factored = factorSharedEnds(d)
1164
+ if (factored !== undefined) return this.node(factored).ip
1165
+ }
1065
1166
  // `greedyClassify` is NOT a choice at all — one arm runs and another arm
1066
1167
  // is credited — so it gets its own row rather than an arm ordering.
1067
1168
  if (d.strategy.tag === 'greedyClassify') {
@@ -1130,11 +1231,15 @@ class Encoder {
1130
1231
  const kids = arms.map((c, k) => {
1131
1232
  const src = order[k]!
1132
1233
  const checks = d.autoNot[src]
1133
- const inner = this.node(c).ip
1234
+ const gate = d.gates[src]
1235
+ const plain = (checks === null || checks === undefined || checks.length === 0)
1236
+ && (gate === null || gate === undefined)
1237
+ const routedGate = plain ? this.routedArmGate(c) : undefined
1238
+ const body = this.node(c).ip
1239
+ const inner = routedGate === undefined ? body : this.emit(OP_RGATE, routedGate.cls, body, routedGate.fx)
1134
1240
  const ip = checks === null || checks === undefined || checks.length === 0
1135
1241
  ? inner
1136
1242
  : this.reject(inner, checks)
1137
- const gate = d.gates[src]
1138
1243
  const gated = gate === null || gate === undefined
1139
1244
  ? ip
1140
1245
  : this.emit(OP_ARMGATE, this.fn(gate, d.gateSrcs?.[src] ?? null), ip, this.expected(deriveExpected(c)))
@@ -1202,6 +1307,19 @@ class Encoder {
1202
1307
  if (transactionalFirst[i] !== undefined) classes[i] = -1
1203
1308
  }
1204
1309
  }
1310
+ // A STATE-GATED ARM ON THE ORDERED PATH IS ALWAYS ENTERED. The interpreter
1311
+ // skips a gated-off arm and contributes nothing for it (choice.ts
1312
+ // firstMatch), and gates it on only by running it. A char-gated skip would
1313
+ // instead add the arm's static opener set whatever the gate says, so an
1314
+ // ordered site leaves the decision to `OP_ARMGATE`. An exclusive site keeps
1315
+ // its classes: there the dispatched arm's gate failing IS the choice
1316
+ // failing, with that arm's set, as in the interpreter's disjoint path.
1317
+ if (d.gates.some(g => g !== null && g !== undefined) && !dispatchIsExclusive(classes, this.cc)) {
1318
+ for (let k = 0; k < classes.length; k++) {
1319
+ const gate = d.gates[order[k]!]
1320
+ if (gate !== null && gate !== undefined) classes[k] = -1
1321
+ }
1322
+ }
1205
1323
  const dispIdx = this.disp.length
1206
1324
  this.disp.push(classes)
1207
1325
  // PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. While the best
@@ -1245,7 +1363,7 @@ class Encoder {
1245
1363
  const rr = this.refResolver()
1246
1364
  const itemClass = matchesEmpty(d.parser, new Set(), rr)
1247
1365
  ? -1
1248
- : this.existingCharClass(firstSetOf(d.parser, new Set(), rr, this.activeTrivia))
1366
+ : this.charClass(firstSetOf(d.parser, new Set(), rr, this.activeTrivia))
1249
1367
  const op = d.valueUnused ? OP_REPV : OP_REP
1250
1368
  const min = d.tag === 'many' ? 0 : d.min
1251
1369
  // ip + 6 is the ITEM's expected set and ip + 7 the separator's sentinel
@@ -1288,7 +1406,7 @@ class Encoder {
1288
1406
  : this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags)
1289
1407
  }
1290
1408
  case 'optional': {
1291
- const head = this.emit(OP_OPT, this.node(d.parser).ip)
1409
+ const head = this.emit(OP_OPT, this.lookaheadGate(d.parser, this.node(d.parser).ip))
1292
1410
  if (!this.failureNeedsRollback(d.parser, undefined)) this.failureRollbackCleanSites.add(head)
1293
1411
  return head
1294
1412
  }
@@ -1409,16 +1527,14 @@ class Encoder {
1409
1527
  // `deriveExpected(body)`, which for a body with nullable leading terms
1410
1528
  // names tokens the run would have skipped — `['"@"', '"(", '"x"']` where
1411
1529
  // running the body reports `['"x"']` alone.
1412
- if (matchesEmpty(d.parser)) return body
1530
+ if (matchesEmpty(d.parser, new Set(), this.refResolver())) return body
1413
1531
  // A node whose BODY is a bare cross-rule ref (`node('X', g.Y, …)`) caches its
1414
1532
  // first set from the ref's base binding. Under a compose OVERRIDE the winner
1415
1533
  // for `Y` may be WIDER, so the cached set gates out input the merged rule
1416
1534
  // accepts (the reducer reroutes via `winners`, but this recognizer gate did
1417
1535
  // not). Resolve the ref's gate through `winners`, exactly as the `lazy` case
1418
1536
  // resolves the call. Non-ref bodies keep the cached set (byte-identical).
1419
- const fs = d.parser._def.tag === 'lazy'
1420
- ? firstSetOf(d.parser, new Set(), this.refResolver(), this.activeTrivia)
1421
- : d.parser._meta.firstSet
1537
+ const fs = this.gateFirstSet(d.parser)
1422
1538
  if (fs.kind === 'any') return body
1423
1539
  const cls = this.charClass(fs)
1424
1540
  if (cls < 0) return body
@@ -1558,7 +1674,7 @@ class Encoder {
1558
1674
  // other engine reported `['not(literal)']`. It hid because `expected` is a
1559
1675
  // TOP-LEVEL field on `RunResult` and is NOT part of the identity digest.
1560
1676
  case 'not':
1561
- return this.emit(OP_NOT, this.node(d.parser).ip, this.expected(assertionFailureExpected(false, d.parser._tag)))
1677
+ return this.emit(OP_NOT, this.lookaheadGate(d.parser, this.node(d.parser).ip), this.expected(assertionFailureExpected(false, d.parser._tag)))
1562
1678
  case 'guard':
1563
1679
  // `'gate'`, the public name — see gate.ts. The def tag stays `'guard'`.
1564
1680
  return this.emit(OP_GUARD, this.fn(d.predicate, d.predSrc ?? null), this.expected(['gate']))
@@ -1603,7 +1719,7 @@ class Encoder {
1603
1719
  // lookahead's failure is "the guard did not hold", and naming the body's
1604
1720
  // internals offers a token the parse never asked the author for. The
1605
1721
  // table propagated the body's set instead.
1606
- return this.emit(OP_PEEK, this.node(d.parser).ip, this.expected(assertionFailureExpected(true, d.parser._tag)))
1722
+ return this.emit(OP_PEEK, this.lookaheadGate(d.parser, this.node(d.parser).ip), this.expected(assertionFailureExpected(true, d.parser._tag)))
1607
1723
  // A TRANSACTION IS A ROW. See `OP_ATTEMPT` for why the transparent
1608
1724
  // lowering was correct only for a choice arm.
1609
1725
  case 'attempt': {
@@ -1619,10 +1735,8 @@ class Encoder {
1619
1735
  // not a slower equivalent.
1620
1736
  // Same override-aware resolution as the node gate: a bare-ref inner resolves
1621
1737
  // its gate through `winners` so a widened winner is not gated out.
1622
- const fs = d.parser._def.tag === 'lazy'
1623
- ? firstSetOf(d.parser, new Set(), this.refResolver(), this.activeTrivia)
1624
- : d.parser._meta.firstSet
1625
- if (fs.kind === 'any' || matchesEmpty(d.parser)) return inner
1738
+ const fs = this.gateFirstSet(d.parser)
1739
+ if (fs.kind === 'any' || matchesEmpty(d.parser, new Set(), this.refResolver())) return inner
1626
1740
  const cls = this.charClass(fs)
1627
1741
  if (cls < 0) return inner
1628
1742
  const e = deriveExpected(d.parser)
@@ -1746,7 +1860,7 @@ class Encoder {
1746
1860
  }
1747
1861
  const slots = (ip: number): number[] => {
1748
1862
  switch (this.code[ip]) {
1749
- case OP_GATE: return [ip + 2]
1863
+ case OP_GATE: case OP_RGATE: return [ip + 2]
1750
1864
  case OP_RULE: case OP_OPT: case OP_NOT: case OP_PEEK: case OP_EXPECT: case OP_ATTEMPT: case OP_LABEL: case OP_COV: return [ip + 1]
1751
1865
  case OP_SCOPE: case OP_SCOPE_CAP: case OP_SCOPE_PLAIN: case OP_WITHCTX: case OP_XFORM: case OP_LEAF: case OP_SOURCE_LEAF: case OP_NODE: case OP_NODE_TRACK: return [ip + 2]
1752
1866
  case OP_SEQ: case OP_SEQV: return Array.from({ length: this.code[ip + 1]! }, (_, i) => ip + 2 + i)
@@ -487,9 +487,7 @@ function makeDriver(
487
487
  children: readonly unknown[], fields: undefined, span: { start: number; end: number },
488
488
  rawChildren: readonly unknown[], triviaLog: readonly number[], state: unknown,
489
489
  ) => unknown
490
- const st = (flags & 8) !== 0 && ctx.state !== undefined
491
- ? Object.assign({}, ctx.state as Record<string, unknown>)
492
- : undefined
490
+ const st = (flags & 8) !== 0 ? ctx.state : undefined
493
491
  const nd = build(
494
492
  cap.children, undefined,
495
493
  code[ip] === OP_NODE_TRACK ? spanLines(ctx, pos, end) : { start: pos, end },
package/src/table/exec.ts CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  OP_LIVE,
23
23
  OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD, OP_ATTEMPT, OP_LABEL,
24
24
  OP_COV,
25
- OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE,
25
+ OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE,
26
26
  OP_LEX_BODY, OP_LEX_PROGRAM,
27
27
  } from './ops.ts'
28
28
  import { adjacencyHolds, adjacencyMisuse } from '../combinators/adjacency.ts'
@@ -49,7 +49,7 @@ import {
49
49
  type CompactProgram, type ResolvedClass, type ResolvedDispatch, type ResolvedDispatchSpec, type ResolvedTable,
50
50
  type SubtreeRef, type TableProgram, type TableRule,
51
51
  } from './program.ts'
52
- import { scalarTerminalNotChild } from './scalar-terminal.ts'
52
+ import { scalarTerminalNotChild, scalarTerminalPeekChild } from './scalar-terminal.ts'
53
53
 
54
54
  /**
55
55
  * THE SHARED DRIVER.
@@ -923,6 +923,7 @@ function makeDriver(
923
923
  const lit = k[code[child + 1]!] as string
924
924
  if (!input.startsWith(lit, cur)) {
925
925
  ctx._fe = cur; ctx._fx = fx[code[child + 2]!] as string[]
926
+ if (ctx._probe !== undefined) failAt(ctx, ctx._fx, cur)
926
927
  { if (REC) ctx._sync = inheritedSync; return FAIL }
927
928
  }
928
929
  const e = cur + lit.length
@@ -937,6 +938,7 @@ function makeDriver(
937
938
  const m = re.exec(input)
938
939
  if (m === null) {
939
940
  ctx._fe = cur; ctx._fx = fx[code[child + 2]!] as string[]
941
+ if (ctx._probe !== undefined) failAt(ctx, ctx._fx, cur)
940
942
  { if (REC) ctx._sync = inheritedSync; return FAIL }
941
943
  }
942
944
  const mv = m[0]
@@ -1118,12 +1120,25 @@ function makeDriver(
1118
1120
  return sup
1119
1121
  }
1120
1122
 
1123
+ case OP_RGATE: {
1124
+ const item = ctx._routed
1125
+ if (item !== undefined && item.span.start === pos && ctx._probe === undefined && !ctx._tolerant
1126
+ && !classHas(cc[code[ip + 1]!]!, lead(input, item.span.end))) {
1127
+ ctx._fe = item.span.end; ctx._fx = fx[code[ip + 3]!] as string[]
1128
+ return FAIL
1129
+ }
1130
+ return exec(code[ip + 2]!, input, pos, ctx)
1131
+ }
1132
+
1121
1133
  case OP_ARMGATE: {
1122
1134
  if (!(fns[code[ip + 1]!] as (s: unknown) => boolean)(ctx.state)) {
1123
1135
  // choice.ts:150 is a `continue`, not a failure — the arm is treated as
1124
1136
  // never entered, so a cut it might have raised must not cut the choice.
1125
1137
  ctx._fc = false
1126
- ctx._fe = pos
1138
+ // No failure POSITION: an ordered choice ranks arms by `_fe`, and a skipped
1139
+ // arm must not rank. A dispatched choice reads `_fx` alone, so this arm's
1140
+ // set is still the answer when it is the one selected.
1141
+ ctx._fe = -1
1127
1142
  ctx._fx = fx[code[ip + 3]!] as string[]
1128
1143
  return FAIL
1129
1144
  }
@@ -1479,9 +1494,7 @@ function makeDriver(
1479
1494
  if (v === FAIL) return FAIL
1480
1495
  const end = EC.e
1481
1496
  const span = code[ip] === OP_NODE_TRACK ? spanLines(ctx, pos, end) : { start: pos, end }
1482
- const st = (flags & 8) !== 0 && ctx.state !== undefined
1483
- ? Object.assign({}, ctx.state as Record<string, unknown>)
1484
- : undefined
1497
+ const st = (flags & 8) !== 0 ? ctx.state : undefined
1485
1498
  const tagIdx = code[ip + 6]!
1486
1499
  const tags = tagIdx < 0 ? undefined : k[tagIdx] as readonly string[]
1487
1500
 
@@ -1518,13 +1531,13 @@ function makeDriver(
1518
1531
  nd = kids[0]
1519
1532
  } else if (proj >= 0) {
1520
1533
  nd = hostCst && host !== undefined
1521
- ? host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, (flags & 8) !== 0 ? st : ctx.state !== undefined ? Object.assign({}, ctx.state as Record<string, unknown>) : undefined, tags)
1534
+ ? host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, ctx.state, tags)
1522
1535
  : projectChild(kids, proj, type)
1523
1536
  } else if (buildIdx >= 0) {
1524
1537
  if (hostCst && host !== undefined) {
1525
1538
  // A direct builder is bypassed under a CST host: the host must never
1526
1539
  // receive an arbitrary AST object as a child of a CST node.
1527
- nd = host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, (flags & 8) !== 0 ? st : ctx.state !== undefined ? Object.assign({}, ctx.state as Record<string, unknown>) : undefined, tags)
1540
+ nd = host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, ctx.state, tags)
1528
1541
  } else {
1529
1542
  const build = fns[buildIdx] as (
1530
1543
  children: readonly unknown[], fields: FieldMap | undefined, span: { start: number; end: number },
@@ -1573,15 +1586,14 @@ function makeDriver(
1573
1586
  }
1574
1587
 
1575
1588
  case OP_NOT: {
1576
- // The shipping generic closure/emitted NOT bodies predate pure-global
1577
- // lookahead rollback. Keep the reference driver aligned with them for
1578
- // every generic child; only the direct-terminal shape this lane replaces
1579
- // needs the reference oracle to hide the terminal's diagnostic writes.
1589
+ // The generic closure/emitted NOT bodies leave the child's failure
1590
+ // registers; only the direct-terminal shape hides them. Every NOT hides
1591
+ // the child's completions from the probe, as `not.ts` does.
1580
1592
  const scalarChild = scalarTerminalNotChild(code, ip) >= 0
1581
1593
  const savedFc = scalarChild ? ctx._fc : false
1582
1594
  const savedFe = scalarChild ? ctx._fe : -1
1583
1595
  const savedFx = scalarChild ? ctx._fx : undefined
1584
- const savedProbe = scalarChild ? ctx._probe?.best : undefined
1596
+ const savedProbe = ctx._probe?.best
1585
1597
  const need = rollbackNeeded(ctx)
1586
1598
  const mRaw = need ? cstRawLen(ctx) : 0
1587
1599
  const mTl = need ? cstTlLen(ctx) : 0
@@ -1596,8 +1608,8 @@ function makeDriver(
1596
1608
  ctx._fc = savedFc
1597
1609
  ctx._fe = savedFe
1598
1610
  ctx._fx = savedFx
1599
- if (ctx._probe !== undefined) ctx._probe.best = savedProbe ?? null
1600
1611
  }
1612
+ if (ctx._probe !== undefined) ctx._probe.best = savedProbe ?? null
1601
1613
  if (v === FAIL) { EC.e = pos; return null }
1602
1614
  // `not.ts:50` — the ASSERTION's own set, at the assertion's position.
1603
1615
  ctx._fe = pos
@@ -1606,6 +1618,12 @@ function makeDriver(
1606
1618
  }
1607
1619
 
1608
1620
  case OP_PEEK: {
1621
+ // A direct terminal is reduced to its recognizer in both assemblies
1622
+ // (`scalarTerminalPeekChild`), as under `not()`: hide its own diagnostic
1623
+ // writes here too.
1624
+ const scalarChild = scalarTerminalPeekChild(code, ip) >= 0
1625
+ const savedFc = scalarChild ? ctx._fc : false
1626
+ const savedProbe = ctx._probe?.best
1609
1627
  const need = rollbackNeeded(ctx)
1610
1628
  const mRaw = need ? cstRawLen(ctx) : 0
1611
1629
  const mTl = need ? cstTlLen(ctx) : 0
@@ -1616,6 +1634,8 @@ function makeDriver(
1616
1634
  const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1617
1635
  const v = exec(code[ip + 1]!, input, pos, ctx)
1618
1636
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1637
+ if (scalarChild) ctx._fc = savedFc
1638
+ if (ctx._probe !== undefined) ctx._probe.best = savedProbe ?? null
1619
1639
  // `peek.ts:60` — the ASSERTION's own set, at the assertion's position.
1620
1640
  if (v === FAIL) { ctx._fe = pos; ctx._fx = fx[code[ip + 2]!] as string[]; return FAIL }
1621
1641
  EC.e = pos
@@ -0,0 +1,173 @@
1
+ /**
2
+ * SHARED ENDS, FACTORED — a choice whose arms differ in one term.
3
+ *
4
+ * `choice(leaf(noTrivia(sequence(gap, '*', gap))), leaf(noTrivia(sequence(gap, '+', gap))))`
5
+ * scans `gap` once per arm, and on a value with no operator (`red red`) both
6
+ * arms scan it and then fail. Every arm is the same wrappers over a sequence of
7
+ * the same terms except one, so the choice is that one term:
8
+ *
9
+ * leaf(noTrivia(sequence(gap, choice('*', '+'), gap)))
10
+ *
11
+ * The gap is scanned once, and the inner choice dispatches on the character
12
+ * after it.
13
+ *
14
+ * WHY IT IS THE SAME PARSE. Each arm is tried at the same position with the same
15
+ * context, so its terms before the differing one match the same text with the
16
+ * same value in every arm: running them once is what each arm's retry did. The
17
+ * inner choice tries the differing terms in arm order, as the outer one tried
18
+ * the arms. The terms after it may differ only as `optional`/`many`, which
19
+ * cannot fail, so the arm that matched its differing term is the arm the outer
20
+ * choice took. The sequence's values, the wrappers' spans and the leaf or node a
21
+ * wrapper publishes are what that arm produced.
22
+ *
23
+ * A choice that fails rolls its last arm back and reports the position it started
24
+ * at, so the factored form is an `attempt`, which does both. An arm that may
25
+ * commit is left as written: a committed choice reports without rolling back.
26
+ *
27
+ * WHAT "THE SAME" MEANS. Two terms are the same when they are one combinator, or
28
+ * their definitions are, field for field, down to shared combinators. A reducer
29
+ * is the same function, or the same printed source: a table artifact carries a
30
+ * reducer AS its source, so two reducers printed alike are one function there.
31
+ */
32
+ import type { Combinator, ParserDef } from '../types.ts'
33
+ import { choice } from '../combinators/choice.ts'
34
+ import { sequence } from '../combinators/sequence.ts'
35
+ import { leaf, sourceLeaf } from '../combinators/token.ts'
36
+ import { noTrivia, parser } from '../combinators/grammar.ts'
37
+ import { attempt } from '../combinators/attempt.ts'
38
+ import { mayCommitFailure } from '../analysis/commitment.ts'
39
+
40
+ type ChoiceDef = Extract<ParserDef, { tag: 'choice' }>
41
+
42
+ /** Own keys every combinator may carry beside its definition; all derived from it. */
43
+ const DERIVED = new Set(['_tag', '_meta', '_def', '_parseScalar', '_stickyRegex', 'parse'])
44
+
45
+ function isCombinator(v: unknown): v is Combinator<unknown> {
46
+ return typeof v === 'object' && v !== null && '_def' in v && '_meta' in v
47
+ }
48
+
49
+ function sameComb(a: Combinator<unknown>, b: Combinator<unknown>, depth: number): boolean {
50
+ if (a === b) return true
51
+ if (depth > 32 || a._tag !== b._tag) return false
52
+ for (const k of Object.keys(a)) if (!DERIVED.has(k)) return false
53
+ for (const k of Object.keys(b)) if (!DERIVED.has(k)) return false
54
+ return sameFields(a._def as Record<string, unknown>, b._def as Record<string, unknown>, depth, undefined)
55
+ }
56
+
57
+ function sameValue(u: unknown, v: unknown, depth: number): boolean {
58
+ if (u === v) return true
59
+ if (isCombinator(u) && isCombinator(v)) return sameComb(u, v, depth + 1)
60
+ if (Array.isArray(u) && Array.isArray(v)) return u.length === v.length && u.every((x, i) => sameValue(x, v[i], depth))
61
+ if (typeof u === 'object' && u !== null && typeof v === 'object' && v !== null
62
+ && Object.getPrototypeOf(u) === Object.prototype && Object.getPrototypeOf(v) === Object.prototype) {
63
+ return sameFields(u as Record<string, unknown>, v as Record<string, unknown>, depth, undefined)
64
+ }
65
+ return false
66
+ }
67
+
68
+ /** Every field of `x` and `y` the same, `skip` aside. A reducer matches its twin by printed source. */
69
+ function sameFields(x: Record<string, unknown>, y: Record<string, unknown>, depth: number, skip: string | undefined): boolean {
70
+ const kx = Object.keys(x).filter(k => k !== skip && x[k] !== undefined)
71
+ const ky = Object.keys(y).filter(k => k !== skip && y[k] !== undefined)
72
+ if (kx.length !== ky.length) return false
73
+ for (const k of kx) {
74
+ if (!(k in y)) return false
75
+ const u = x[k]
76
+ const v = y[k]
77
+ if (k === 'fn' && typeof u === 'function' && typeof v === 'function' && u !== v) {
78
+ if (typeof x.fnSrc !== 'string' || x.fnSrc === '' || x.fnSrc !== y.fnSrc) return false
79
+ continue
80
+ }
81
+ if (!sameValue(u, v, depth)) return false
82
+ }
83
+ return true
84
+ }
85
+
86
+ /** A wrapper the factoring can rebuild around a new body: a leaf, or a trivia scope. */
87
+ function rewrap(w: Combinator<unknown>, body: Combinator<unknown>): Combinator<unknown> | undefined {
88
+ const d = w._def
89
+ let out: Combinator<unknown>
90
+ if (d.tag === 'leaf') {
91
+ out = d.cstValue === 'source' ? sourceLeaf(body, d.fn) : leaf(body, d.fn)
92
+ if (d.fnSrc !== undefined) (out._def as { fnSrc?: string }).fnSrc = d.fnSrc
93
+ } else if (d.tag === 'grammar') {
94
+ out = d.clearTrivia === true ? noTrivia(body) : parser({ trivia: d.triviaParser ?? null }, body)
95
+ } else return undefined
96
+ // The rebuilt wrapper must be the original in everything but its body.
97
+ return sameFields(d as Record<string, unknown>, out._def as Record<string, unknown>, 0, 'parser') ? out : undefined
98
+ }
99
+
100
+ function unusedOf(seq: Combinator<unknown>): boolean {
101
+ return seq._def.tag === 'sequence' && seq._def.valueUnused === true
102
+ }
103
+
104
+ function never(c: Combinator<unknown>): boolean {
105
+ const t = c._def.tag
106
+ return t === 'optional' || t === 'many'
107
+ }
108
+
109
+ /**
110
+ * The factored form of `d`'s choice, or `undefined` when its arms are not one
111
+ * shape that differs in one term.
112
+ */
113
+ export function factorSharedEnds(d: ChoiceDef): Combinator<unknown> | undefined {
114
+ const arms = d.parsers
115
+ if (arms.length < 2) return undefined
116
+ if (d.strategy.tag !== 'firstMatch' && d.strategy.tag !== 'sharedPrefix') return undefined
117
+ if (d.gates.some(g => g !== null) || d.autoNot.some(a => a !== null)) return undefined
118
+ // Each arm: its wrappers, outermost first, and the sequence they end in.
119
+ const shapes: { wrappers: Combinator<unknown>[]; terms: Combinator<unknown>[]; seq: Combinator<unknown> }[] = []
120
+ for (const arm of arms) {
121
+ const wrappers: Combinator<unknown>[] = []
122
+ let c = arm
123
+ while ((c._def.tag === 'leaf' || c._def.tag === 'grammar') && wrappers.length < 4) {
124
+ wrappers.push(c)
125
+ c = c._def.parser
126
+ }
127
+ if (c._def.tag !== 'sequence' || c._def.parsers.length < 2) return undefined
128
+ shapes.push({ wrappers, terms: c._def.parsers, seq: c })
129
+ }
130
+ const first = shapes[0]!
131
+ const n = first.terms.length
132
+ for (const s of shapes) {
133
+ if (s.wrappers.length !== first.wrappers.length || s.terms.length !== n) return undefined
134
+ if (unusedOf(s.seq) !== unusedOf(first.seq)) return undefined
135
+ for (let j = 0; j < s.wrappers.length; j++) {
136
+ const a = first.wrappers[j]!._def as Record<string, unknown>
137
+ const b = s.wrappers[j]!._def as Record<string, unknown>
138
+ if (a.tag !== b.tag || !sameFields(a, b, 0, 'parser')) return undefined
139
+ }
140
+ }
141
+ // Exactly one term position differs, and nothing after it can fail.
142
+ let at = -1
143
+ for (let i = 0; i < n; i++) {
144
+ if (shapes.every(s => sameComb(s.terms[i]!, first.terms[i]!, 0))) continue
145
+ if (at >= 0) return undefined
146
+ at = i
147
+ }
148
+ if (at < 0) return undefined
149
+ for (let i = at + 1; i < n; i++) if (!never(first.terms[i]!)) return undefined
150
+ // A committed failure leaves a choice without rolling its arm back. Asked last:
151
+ // it walks everything the arms reach.
152
+ if (arms.some(a => mayCommitFailure(a))) return undefined
153
+ const middle = choice(...(shapes.map(s => s.terms[at]!) as [Combinator<unknown>, ...Combinator<unknown>[]]))
154
+ // The inner choice must keep arm order: a strategy that reorders (longest
155
+ // literal first) or classifies would pick a different arm. Nor may it add an
156
+ // automatic lookahead the arms as written do not have: `'start'` before
157
+ // `'started'` gets a `not('ed')` as one choice, which rejects the `!start` the
158
+ // written arms take on `!started`.
159
+ if (middle._def.tag === 'choice' && middle._def.autoNot.some(a => a !== null)) return undefined
160
+ const st = middle._def.tag === 'choice' ? middle._def.strategy : undefined
161
+ if (st === undefined || (st.tag !== 'firstMatch' && st.tag !== 'sharedPrefix'
162
+ && !(st.tag === 'literalsLongestFirst' && st.sortedIndices.every((x, i) => x === i)))) return undefined
163
+ let out: Combinator<unknown> | undefined = sequence(
164
+ ...([...first.terms.slice(0, at), middle, ...first.terms.slice(at + 1)] as [Combinator<unknown>, ...Combinator<unknown>[]]),
165
+ )
166
+ if (unusedOf(first.seq)) {
167
+ (out._def as { valueUnused?: boolean }).valueUnused = true
168
+ }
169
+ for (let j = first.wrappers.length - 1; j >= 0 && out !== undefined; j--) out = rewrap(first.wrappers[j]!, out)
170
+ // A choice that fails is rolled back and reports where it started; so does an
171
+ // `attempt`. The sequence alone would report where its inner choice failed.
172
+ return out === undefined ? undefined : attempt(out)
173
+ }