parseman 0.55.4 → 0.57.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 (125) hide show
  1. package/dist/analysis/choice-cost.d.ts.map +1 -1
  2. package/dist/analysis/commitment.d.ts.map +1 -1
  3. package/dist/analysis/diagnostics.js +3745 -3180
  4. package/dist/analysis/diagnostics.js.map +3 -3
  5. package/dist/analysis/duplication.d.ts.map +1 -1
  6. package/dist/analysis/gating.d.ts.map +1 -1
  7. package/dist/cli/index.js +2182 -1617
  8. package/dist/cli/index.js.map +3 -3
  9. package/dist/combinators/attempt.d.ts.map +1 -1
  10. package/dist/combinators/choice.d.ts.map +1 -1
  11. package/dist/combinators/dispatch.d.ts.map +1 -1
  12. package/dist/combinators/expect.d.ts.map +1 -1
  13. package/dist/combinators/first-set.d.ts.map +1 -1
  14. package/dist/combinators/lines.d.ts +79 -0
  15. package/dist/combinators/lines.d.ts.map +1 -0
  16. package/dist/combinators/node.d.ts +1 -1
  17. package/dist/combinators/trivia-skip.d.ts +1 -0
  18. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  19. package/dist/compiler/build-arity.d.ts +1 -1
  20. package/dist/compiler/build-arity.d.ts.map +1 -1
  21. package/dist/compiler/fields.d.ts.map +1 -1
  22. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  23. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  24. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  25. package/dist/compiler/token-capability.js +25 -0
  26. package/dist/compiler/token-capability.js.map +1 -1
  27. package/dist/coverage.d.ts.map +1 -1
  28. package/dist/cst/reflection.d.ts.map +1 -1
  29. package/dist/cst/trivia-kinds.d.ts +9 -1
  30. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +2348 -1697
  34. package/dist/index.js.map +3 -3
  35. package/dist/language-service/index.js +3 -0
  36. package/dist/language-service/index.js.map +1 -1
  37. package/dist/parse-context.d.ts.map +1 -1
  38. package/dist/plugin/entry-supercompiler.d.ts +1 -1
  39. package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
  40. package/dist/plugin/evaluator.d.ts.map +1 -1
  41. package/dist/plugin/index.d.ts +27 -1
  42. package/dist/plugin/index.d.ts.map +1 -1
  43. package/dist/plugin/index.js +2360 -1314
  44. package/dist/plugin/index.js.map +3 -3
  45. package/dist/plugin/ir-eval.d.ts +2 -4
  46. package/dist/plugin/ir-eval.d.ts.map +1 -1
  47. package/dist/regex/classes.d.ts.map +1 -1
  48. package/dist/run/index.js +3 -0
  49. package/dist/run/index.js.map +1 -1
  50. package/dist/spec/index.js +15 -0
  51. package/dist/spec/index.js.map +1 -1
  52. package/dist/spec/model.d.ts.map +1 -1
  53. package/dist/table/assemble.d.ts +9 -65
  54. package/dist/table/assemble.d.ts.map +1 -1
  55. package/dist/table/child-slots.d.ts +4 -6
  56. package/dist/table/child-slots.d.ts.map +1 -1
  57. package/dist/table/compile-rule-map.d.ts +14 -2
  58. package/dist/table/compile-rule-map.d.ts.map +1 -1
  59. package/dist/table/emit-assembly.d.ts +17 -2
  60. package/dist/table/emit-assembly.d.ts.map +1 -1
  61. package/dist/table/emit.d.ts +5 -1
  62. package/dist/table/emit.d.ts.map +1 -1
  63. package/dist/table/encode.d.ts.map +1 -1
  64. package/dist/table/exec-baseline.d.ts.map +1 -1
  65. package/dist/table/exec.d.ts.map +1 -1
  66. package/dist/table/index.js +3884 -3262
  67. package/dist/table/index.js.map +3 -3
  68. package/dist/table/line-scan.d.ts +9 -0
  69. package/dist/table/line-scan.d.ts.map +1 -0
  70. package/dist/table/ops.d.ts +9 -6
  71. package/dist/table/ops.d.ts.map +1 -1
  72. package/dist/table/program.d.ts +24 -2
  73. package/dist/table/program.d.ts.map +1 -1
  74. package/dist/table/select-hot.d.ts +6 -0
  75. package/dist/table/select-hot.d.ts.map +1 -0
  76. package/dist/table/trivia-gap.d.ts +31 -0
  77. package/dist/table/trivia-gap.d.ts.map +1 -0
  78. package/dist/types.d.ts +38 -0
  79. package/dist/types.d.ts.map +1 -1
  80. package/dist/version.d.ts +1 -1
  81. package/package.json +4 -1
  82. package/src/analysis/choice-cost.ts +1 -0
  83. package/src/analysis/commitment.ts +13 -0
  84. package/src/analysis/duplication.ts +3 -0
  85. package/src/analysis/gating.ts +5 -0
  86. package/src/analysis/rebuild.ts +2 -2
  87. package/src/combinators/attempt.ts +2 -0
  88. package/src/combinators/choice.ts +3 -0
  89. package/src/combinators/dispatch.ts +3 -0
  90. package/src/combinators/expect.ts +5 -0
  91. package/src/combinators/first-set.ts +9 -1
  92. package/src/combinators/lines.ts +244 -0
  93. package/src/combinators/node.ts +6 -13
  94. package/src/combinators/trivia-skip.ts +48 -3
  95. package/src/compiler/build-arity.ts +3 -3
  96. package/src/compiler/fields.ts +3 -0
  97. package/src/compiler/grammar-coverage-ids.ts +1 -0
  98. package/src/compiler/ir-serialize.ts +10 -0
  99. package/src/compiler/token-alphabet.ts +3 -1
  100. package/src/coverage.ts +6 -0
  101. package/src/cst/reflection.ts +3 -0
  102. package/src/cst/trivia-kinds.ts +12 -1
  103. package/src/index.ts +2 -0
  104. package/src/parse-context.ts +3 -0
  105. package/src/plugin/entry-supercompiler.ts +8 -9
  106. package/src/plugin/evaluator.ts +4 -0
  107. package/src/plugin/index.ts +138 -13
  108. package/src/plugin/ir-eval.ts +10 -1
  109. package/src/regex/classes.ts +3 -0
  110. package/src/spec/model.ts +15 -0
  111. package/src/table/assemble.ts +125 -33
  112. package/src/table/child-slots.ts +4 -6
  113. package/src/table/compile-rule-map.ts +55 -15
  114. package/src/table/emit-assembly.ts +139 -26
  115. package/src/table/emit.ts +13 -11
  116. package/src/table/encode.ts +87 -27
  117. package/src/table/exec-baseline.ts +1 -3
  118. package/src/table/exec.ts +10 -6
  119. package/src/table/line-scan.ts +19 -0
  120. package/src/table/ops.ts +9 -6
  121. package/src/table/program.ts +41 -2
  122. package/src/table/select-hot.ts +70 -0
  123. package/src/table/trivia-gap.ts +109 -0
  124. package/src/types.ts +32 -0
  125. package/src/version.ts +1 -1
@@ -1,3 +1,4 @@
1
+ import { scanRefs } from './program.ts'
1
2
  import type { AutoNotCheck, Combinator, FirstSet, ParserDef } from '../types.ts'
2
3
  import { classifyFinalChoice, firstSetOf, intersects, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
3
4
  import { childrenOf } from '../analysis/gating.ts'
@@ -31,7 +32,7 @@ import {
31
32
  LEX_NODE_SEQUENCE2, LEX_NODE_SEQUENCE3, LEX_NODE_SEQUENCE4, LEX_NODE_SEQUENCE5,
32
33
  LEX_NODE_TERMINAL,
33
34
  } from './lex-program.ts'
34
- import { covKindCode, encodeClassSpec, ownTableProgram } from './program.ts'
35
+ import { covKindCode, dispatchIsExclusive, encodeClassSpec, ownTableProgram } from './program.ts'
35
36
  import type { GrammarCoveragePlan } from '../compiler/grammar-coverage-ids.ts'
36
37
  import { directArrayProjection } from '../compiler/direct-projection.ts'
37
38
  import {
@@ -683,6 +684,7 @@ class Encoder {
683
684
  case 'expect':
684
685
  case 'recover':
685
686
  case 'scanTo':
687
+ case 'lines':
686
688
  case 'unknown':
687
689
  return true
688
690
  case 'lazy': {
@@ -697,6 +699,38 @@ class Encoder {
697
699
  }
698
700
  }
699
701
 
702
+ /**
703
+ * THE FIRST-CHARACTER GATE OF AN `optional`, `not` OR `peek` CHILD.
704
+ *
705
+ * A non-nullable child cannot match at a character outside its first set, so
706
+ * the wrapper's outcome there is already decided: `optional` yields `null`,
707
+ * `not` succeeds, `peek` fails with its own set. The interpreter's `optional`
708
+ * makes that test itself (`repeat.ts` `startsFirstSet`); the table entered the
709
+ * child — a rollback mark, the call, the child's own failure — to learn it.
710
+ *
711
+ * The gate is an `OP_GATE` row around the child, so every engine runs it
712
+ * unchanged, a probe or tolerant parse forwards through it as for a node's
713
+ * gate, and the wrapper's row keeps its layout. The engines test the class
714
+ * before taking the wrapper's mark. The first set is resolved through
715
+ * `winners`, the analysis a choice arm's gate uses, so a composed rule's set
716
+ * is the merged one. A scalar `not` child stays bare: `scalarTerminalNotChild`
717
+ * already reduces it to one recognizer call.
718
+ */
719
+ private lookaheadGate(p: Combinator<unknown>, child: number): number {
720
+ // By opcode FAMILY: a line-tracked variant spells the same terminal
721
+ // `OP_LIT_TRACK`/`OP_RX_TRACK`, and every variant of one grammar must lay
722
+ // down the same words (`foldPrograms`).
723
+ const op = this.code[child]
724
+ if (op === OP_GATE || op === OP_LIT || op === OP_LIT_CI || op === OP_LIT_TRACK || op === OP_LIT_CI_TRACK
725
+ || op === OP_RX || op === OP_RX_TRACK) return child
726
+ const rr = this.refResolver()
727
+ if (matchesEmpty(p, new Set(), rr)) return child
728
+ const cls = this.charClass(firstSetOf(p, new Set(), rr, this.activeTrivia))
729
+ if (cls < 0) return child
730
+ const e = deriveExpected(p)
731
+ return this.emit(OP_GATE, cls, child, this.expected(e.length > 0 ? e : [p._tag]))
732
+ }
733
+
700
734
  /** A first set becomes a char class string, or −1 for `any`. */
701
735
  private charClass(fs: FirstSet): number {
702
736
  if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
@@ -713,14 +747,21 @@ class Encoder {
713
747
  return i
714
748
  }
715
749
 
716
- /** Bind a guard only when its class is already part of the program. Repeat
717
- * gating must not grow the compact artifact merely to avoid a failed call. */
718
- private existingCharClass(fs: FirstSet): number {
719
- if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
720
- const hit = this.ccIndex.get(encodeClassSpec(fs.ranges))
721
- // This reuses the recovery row's existing `-1` slot. Keep its decimal width
722
- // at two bytes or less so a speed hint cannot grow a compact artifact.
723
- return hit !== undefined && hit < 100 ? hit : -1
750
+ /**
751
+ * The first set a node's or an attempt's fail-fast gate tests. A bare
752
+ * cross-rule ref resolves through `winners` (a compose OVERRIDE may widen
753
+ * it). Any other body keeps its construction-time `_meta.firstSet`, except
754
+ * where that is `any`: under `compose()` a body that reaches a rule of
755
+ * another piece is built before that rule exists, so its cached set is
756
+ * `any` and the node got NO gate (jess's `CompoundSelector` was entered at
757
+ * `{`, `Call` at `}`). The resolved set is the one a choice arm's gate uses,
758
+ * over the final merged map.
759
+ */
760
+ private gateFirstSet(p: Combinator<unknown>): FirstSet {
761
+ const cached = p._meta.firstSet
762
+ return p._def.tag === 'lazy' || cached.kind === 'any'
763
+ ? firstSetOf(p, new Set(), this.refResolver(), this.activeTrivia)
764
+ : cached
724
765
  }
725
766
 
726
767
  /**
@@ -987,6 +1028,17 @@ class Encoder {
987
1028
  open: bal.open,
988
1029
  close: bal.close,
989
1030
  }))
1031
+ // `lines()` and `columns()` lower as their constructor arguments too: the
1032
+ // table rebuilds each and runs it, the body and prefixes being table subtrees.
1033
+ if (d.tag === 'lines') {
1034
+ return this.emit(OP_SCAN, this.scanSlot({
1035
+ kind: 2, flags: 0, skip: [],
1036
+ prefix: this.subtree(d.prefix), body: this.subtree(d.body),
1037
+ ...(d.first === undefined ? {} : { first: this.subtree(d.first) }),
1038
+ ...(d.lazy === undefined ? {} : { lazy: this.subtree(d.lazy) }),
1039
+ }))
1040
+ }
1041
+ if (d.tag === 'columns') return this.emit(OP_SCAN, this.scanSlot({ kind: 3, flags: 0, skip: [], n: d.n }))
990
1042
  if (d.tag === 'scanTo') {
991
1043
  const sentDef = d.sentinel._def
992
1044
  return this.emit(OP_SCAN, this.scanSlot({
@@ -1189,6 +1241,19 @@ class Encoder {
1189
1241
  if (transactionalFirst[i] !== undefined) classes[i] = -1
1190
1242
  }
1191
1243
  }
1244
+ // A STATE-GATED ARM ON THE ORDERED PATH IS ALWAYS ENTERED. The interpreter
1245
+ // skips a gated-off arm and contributes nothing for it (choice.ts
1246
+ // firstMatch), and gates it on only by running it. A char-gated skip would
1247
+ // instead add the arm's static opener set whatever the gate says, so an
1248
+ // ordered site leaves the decision to `OP_ARMGATE`. An exclusive site keeps
1249
+ // its classes: there the dispatched arm's gate failing IS the choice
1250
+ // failing, with that arm's set, as in the interpreter's disjoint path.
1251
+ if (d.gates.some(g => g !== null && g !== undefined) && !dispatchIsExclusive(classes, this.cc)) {
1252
+ for (let k = 0; k < classes.length; k++) {
1253
+ const gate = d.gates[order[k]!]
1254
+ if (gate !== null && gate !== undefined) classes[k] = -1
1255
+ }
1256
+ }
1192
1257
  const dispIdx = this.disp.length
1193
1258
  this.disp.push(classes)
1194
1259
  // PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. While the best
@@ -1232,7 +1297,7 @@ class Encoder {
1232
1297
  const rr = this.refResolver()
1233
1298
  const itemClass = matchesEmpty(d.parser, new Set(), rr)
1234
1299
  ? -1
1235
- : this.existingCharClass(firstSetOf(d.parser, new Set(), rr, this.activeTrivia))
1300
+ : this.charClass(firstSetOf(d.parser, new Set(), rr, this.activeTrivia))
1236
1301
  const op = d.valueUnused ? OP_REPV : OP_REP
1237
1302
  const min = d.tag === 'many' ? 0 : d.min
1238
1303
  // ip + 6 is the ITEM's expected set and ip + 7 the separator's sentinel
@@ -1275,7 +1340,7 @@ class Encoder {
1275
1340
  : this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags)
1276
1341
  }
1277
1342
  case 'optional': {
1278
- const head = this.emit(OP_OPT, this.node(d.parser).ip)
1343
+ const head = this.emit(OP_OPT, this.lookaheadGate(d.parser, this.node(d.parser).ip))
1279
1344
  if (!this.failureNeedsRollback(d.parser, undefined)) this.failureRollbackCleanSites.add(head)
1280
1345
  return head
1281
1346
  }
@@ -1396,16 +1461,14 @@ class Encoder {
1396
1461
  // `deriveExpected(body)`, which for a body with nullable leading terms
1397
1462
  // names tokens the run would have skipped — `['"@"', '"(", '"x"']` where
1398
1463
  // running the body reports `['"x"']` alone.
1399
- if (matchesEmpty(d.parser)) return body
1464
+ if (matchesEmpty(d.parser, new Set(), this.refResolver())) return body
1400
1465
  // A node whose BODY is a bare cross-rule ref (`node('X', g.Y, …)`) caches its
1401
1466
  // first set from the ref's base binding. Under a compose OVERRIDE the winner
1402
1467
  // for `Y` may be WIDER, so the cached set gates out input the merged rule
1403
1468
  // accepts (the reducer reroutes via `winners`, but this recognizer gate did
1404
1469
  // not). Resolve the ref's gate through `winners`, exactly as the `lazy` case
1405
1470
  // resolves the call. Non-ref bodies keep the cached set (byte-identical).
1406
- const fs = d.parser._def.tag === 'lazy'
1407
- ? firstSetOf(d.parser, new Set(), this.refResolver(), this.activeTrivia)
1408
- : d.parser._meta.firstSet
1471
+ const fs = this.gateFirstSet(d.parser)
1409
1472
  if (fs.kind === 'any') return body
1410
1473
  const cls = this.charClass(fs)
1411
1474
  if (cls < 0) return body
@@ -1545,7 +1608,7 @@ class Encoder {
1545
1608
  // other engine reported `['not(literal)']`. It hid because `expected` is a
1546
1609
  // TOP-LEVEL field on `RunResult` and is NOT part of the identity digest.
1547
1610
  case 'not':
1548
- return this.emit(OP_NOT, this.node(d.parser).ip, this.expected(assertionFailureExpected(false, d.parser._tag)))
1611
+ return this.emit(OP_NOT, this.lookaheadGate(d.parser, this.node(d.parser).ip), this.expected(assertionFailureExpected(false, d.parser._tag)))
1549
1612
  case 'guard':
1550
1613
  // `'gate'`, the public name — see gate.ts. The def tag stays `'guard'`.
1551
1614
  return this.emit(OP_GUARD, this.fn(d.predicate, d.predSrc ?? null), this.expected(['gate']))
@@ -1590,7 +1653,7 @@ class Encoder {
1590
1653
  // lookahead's failure is "the guard did not hold", and naming the body's
1591
1654
  // internals offers a token the parse never asked the author for. The
1592
1655
  // table propagated the body's set instead.
1593
- return this.emit(OP_PEEK, this.node(d.parser).ip, this.expected(assertionFailureExpected(true, d.parser._tag)))
1656
+ return this.emit(OP_PEEK, this.lookaheadGate(d.parser, this.node(d.parser).ip), this.expected(assertionFailureExpected(true, d.parser._tag)))
1594
1657
  // A TRANSACTION IS A ROW. See `OP_ATTEMPT` for why the transparent
1595
1658
  // lowering was correct only for a choice arm.
1596
1659
  case 'attempt': {
@@ -1606,10 +1669,8 @@ class Encoder {
1606
1669
  // not a slower equivalent.
1607
1670
  // Same override-aware resolution as the node gate: a bare-ref inner resolves
1608
1671
  // its gate through `winners` so a widened winner is not gated out.
1609
- const fs = d.parser._def.tag === 'lazy'
1610
- ? firstSetOf(d.parser, new Set(), this.refResolver(), this.activeTrivia)
1611
- : d.parser._meta.firstSet
1612
- if (fs.kind === 'any' || matchesEmpty(d.parser)) return inner
1672
+ const fs = this.gateFirstSet(d.parser)
1673
+ if (fs.kind === 'any' || matchesEmpty(d.parser, new Set(), this.refResolver())) return inner
1613
1674
  const cls = this.charClass(fs)
1614
1675
  if (cls < 0) return inner
1615
1676
  const e = deriveExpected(d.parser)
@@ -1762,12 +1823,7 @@ class Encoder {
1762
1823
  // interiors uncollapsed and — worse — left the spec's own offset pointing at
1763
1824
  // a trampoline whose target had been rewritten around it.
1764
1825
  const refs: SubtreeRef[] = []
1765
- for (const s of this.scans) {
1766
- if (s.sentinel !== undefined) refs.push(s.sentinel)
1767
- if (s.recoverAt !== undefined) refs.push(s.recoverAt)
1768
- if (s.stopAt !== undefined) refs.push(s.stopAt)
1769
- refs.push(...s.skip)
1770
- }
1826
+ for (const s of this.scans) refs.push(...scanRefs(s))
1771
1827
  for (const set of this.scanSkipSets) refs.push(...set)
1772
1828
  const stack = [...Object.values(this.rules), ...refs.map(r => r[0])]
1773
1829
  while (stack.length > 0) {
@@ -1788,6 +1844,10 @@ class Encoder {
1788
1844
  ...(s.sentinel === undefined ? {} : { sentinel: res(s.sentinel) }),
1789
1845
  ...(s.recoverAt === undefined ? {} : { recoverAt: res(s.recoverAt) }),
1790
1846
  ...(s.stopAt === undefined ? {} : { stopAt: res(s.stopAt) }),
1847
+ ...(s.prefix === undefined ? {} : { prefix: res(s.prefix) }),
1848
+ ...(s.body === undefined ? {} : { body: res(s.body) }),
1849
+ ...(s.first === undefined ? {} : { first: res(s.first) }),
1850
+ ...(s.lazy === undefined ? {} : { lazy: res(s.lazy) }),
1791
1851
  }))
1792
1852
  this.scanSkipSets = this.scanSkipSets.map(set => set.map(res))
1793
1853
  }
@@ -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
@@ -1,3 +1,4 @@
1
+ import { rebuildLineScan } from './line-scan.ts'
1
2
  import type { Combinator, FieldMap, FirstSet, ParseContext, ParseResult, ParserDef } from '../types.ts'
2
3
  import { balanced, scanTo } from '../combinators/scanTo.ts'
3
4
  import { buildFieldMap } from '../compiler/fields.ts'
@@ -1122,7 +1123,10 @@ function makeDriver(
1122
1123
  // choice.ts:150 is a `continue`, not a failure — the arm is treated as
1123
1124
  // never entered, so a cut it might have raised must not cut the choice.
1124
1125
  ctx._fc = false
1125
- ctx._fe = pos
1126
+ // No failure POSITION: an ordered choice ranks arms by `_fe`, and a skipped
1127
+ // arm must not rank. A dispatched choice reads `_fx` alone, so this arm's
1128
+ // set is still the answer when it is the one selected.
1129
+ ctx._fe = -1
1126
1130
  ctx._fx = fx[code[ip + 3]!] as string[]
1127
1131
  return FAIL
1128
1132
  }
@@ -1478,9 +1482,7 @@ function makeDriver(
1478
1482
  if (v === FAIL) return FAIL
1479
1483
  const end = EC.e
1480
1484
  const span = code[ip] === OP_NODE_TRACK ? spanLines(ctx, pos, end) : { start: pos, end }
1481
- const st = (flags & 8) !== 0 && ctx.state !== undefined
1482
- ? Object.assign({}, ctx.state as Record<string, unknown>)
1483
- : undefined
1485
+ const st = (flags & 8) !== 0 ? ctx.state : undefined
1484
1486
  const tagIdx = code[ip + 6]!
1485
1487
  const tags = tagIdx < 0 ? undefined : k[tagIdx] as readonly string[]
1486
1488
 
@@ -1517,13 +1519,13 @@ function makeDriver(
1517
1519
  nd = kids[0]
1518
1520
  } else if (proj >= 0) {
1519
1521
  nd = hostCst && host !== undefined
1520
- ? 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)
1522
+ ? host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, ctx.state, tags)
1521
1523
  : projectChild(kids, proj, type)
1522
1524
  } else if (buildIdx >= 0) {
1523
1525
  if (hostCst && host !== undefined) {
1524
1526
  // A direct builder is bypassed under a CST host: the host must never
1525
1527
  // receive an arbitrary AST object as a child of a CST node.
1526
- 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)
1528
+ nd = host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, ctx.state, tags)
1527
1529
  } else {
1528
1530
  const build = fns[buildIdx] as (
1529
1531
  children: readonly unknown[], fields: FieldMap | undefined, span: { start: number; end: number },
@@ -1682,6 +1684,8 @@ function makeDriver(
1682
1684
  * drift.
1683
1685
  */
1684
1686
  const scans: readonly Combinator<unknown>[] = (prog.scans ?? []).map(s => {
1687
+ const rebuilt = rebuildLineScan(s, subtreeComb)
1688
+ if (rebuilt !== undefined) return rebuilt
1685
1689
  const skip = s.skip.map(r => subtreeComb(r))
1686
1690
  const raw = (s.flags & 1) !== 0
1687
1691
  if (s.kind === 1) {
@@ -0,0 +1,19 @@
1
+ import type { Combinator } from '../types.ts'
2
+ import type { ScanSpec, SubtreeRef } from './program.ts'
3
+ import { columns, lines } from '../combinators/lines.ts'
4
+
5
+ /**
6
+ * Rebuild a `lines()` (kind 2) or `columns()` (kind 3) scan spec as the
7
+ * combinator it was constructed as, its subtrees bound to the table's pieces.
8
+ * Both drivers run the result through its own `parse`, as they do `scanTo`.
9
+ */
10
+ export function rebuildLineScan(s: ScanSpec, subtree: (r: SubtreeRef) => Combinator<unknown>): Combinator<unknown> | undefined {
11
+ if (s.kind === 2) {
12
+ return lines(subtree(s.prefix!), subtree(s.body!), {
13
+ ...(s.first === undefined ? {} : { first: subtree(s.first) }),
14
+ ...(s.lazy === undefined ? {} : { lazy: subtree(s.lazy) }),
15
+ }) as Combinator<unknown>
16
+ }
17
+ if (s.kind === 3) return columns(s.n!) as Combinator<unknown>
18
+ return undefined
19
+ }
package/src/table/ops.ts CHANGED
@@ -339,13 +339,16 @@ export const OP_REJECT = 35
339
339
  * arm INSTEAD of sitting inside it, so the encoder still reads the arm's own
340
340
  * first set for `disp` and the site keeps its dispatch slot.
341
341
  *
342
- * A blocked arm is SKIPPED, not failed — `choice.ts:150` is `continue`. The row
343
- * therefore clears `_fc` on the gate-false path exactly as `OP_REJECT` does:
342
+ * A blocked arm is SKIPPED, not failed — `choice.ts` firstMatch `continue`s. The
343
+ * row therefore clears `_fc` on the gate-false path exactly as `OP_REJECT` does:
344
344
  * "pretend this arm was never entered", so no cut it might have raised survives
345
- * to cut the choice. It still reports `e`, because the OTHER path a blocked arm
346
- * can be reached by is first-char dispatch, where skip-and-retry and
347
- * fail-the-choice are the same thing (choice.ts:23-34, :100-105) and the
348
- * interpreter answers with `deriveExpected(arm)` — this set.
345
+ * to cut the choice. It sets `_fe` to −1, so an ORDERED choice, which ranks arms
346
+ * by failure position, takes nothing from it, as the interpreter does. It still
347
+ * leaves `e` in `_fx`, because the other path a blocked arm can be reached by is
348
+ * exclusive first-char dispatch, where skip-and-retry and fail-the-choice are the
349
+ * same thing and the interpreter answers with `deriveExpected(arm)` — this set.
350
+ * On an ordered site the encoder gives a gated arm no char class, so it is always
351
+ * entered and this row, not a char gate, decides what it contributes.
349
352
  *
350
353
  * The predicate is a live function, like `OP_GUARD`'s, so a grammar using one is
351
354
  * runtime-only for `emitTableModule` unless `fnSources` are supplied.
@@ -241,13 +241,13 @@ export type SubtreeRef = readonly [ip: number, cls: number]
241
241
  /**
242
242
  * A scanning construct as its CONSTRUCTOR ARGUMENTS.
243
243
  *
244
- * `kind` 0 is `scanTo`, 1 is `balanced`. `flags` bit 0 = `raw`, bit 1 = `orEOF`
244
+ * `kind` 0 is `scanTo`, 1 is `balanced`, 2 is `lines`, 3 is `columns`. `flags` bit 0 = `raw`, bit 1 = `orEOF`
245
245
  * (scanTo), bit 2 = `strict` (balanced). `sent` is the sentinel's literal text
246
246
  * when it is a `literal()`, so the rebuilt scan reports the same expected set as
247
247
  * the interpreter, and `null` otherwise (which reports `"sentinel"`).
248
248
  */
249
249
  export type ScanSpec = {
250
- readonly kind: 0 | 1
250
+ readonly kind: 0 | 1 | 2 | 3
251
251
  readonly flags: number
252
252
  readonly skip: readonly SubtreeRef[]
253
253
  readonly sentinel?: SubtreeRef
@@ -256,6 +256,34 @@ export type ScanSpec = {
256
256
  readonly sent?: string | null
257
257
  readonly open?: string
258
258
  readonly close?: string
259
+ /** `lines()` (kind 2): the continuation prefix, the body, and the optional first-line prefix and lazy check. */
260
+ readonly prefix?: SubtreeRef
261
+ readonly body?: SubtreeRef
262
+ readonly first?: SubtreeRef
263
+ readonly lazy?: SubtreeRef
264
+ /** `columns()` (kind 3): the column count. */
265
+ readonly n?: number
266
+ }
267
+
268
+ /** Every subtree a scan spec references, which the table links as roots. */
269
+ export function scanRefs(s: ScanSpec): SubtreeRef[] {
270
+ const refs: SubtreeRef[] = []
271
+ for (const r of [s.sentinel, s.recoverAt, s.stopAt, s.prefix, s.body, s.first, s.lazy]) if (r !== undefined) refs.push(r)
272
+ refs.push(...s.skip)
273
+ return refs
274
+ }
275
+
276
+ /**
277
+ * The sites an assembly links from OUTSIDE its rule entries — the scan pool's
278
+ * subtrees, then the scan-skip sets — in the one order every assembler names
279
+ * them. The emitted source is keyed by this order, so a second copy of the list
280
+ * that disagrees prints different source for the same program.
281
+ */
282
+ export function assemblyExtraIps(prog: TableProgram): number[] {
283
+ const ips: number[] = []
284
+ for (const s of prog.scans ?? []) for (const r of scanRefs(s)) ips.push(r[0])
285
+ for (const set of prog.scanSkip ?? []) for (const r of set) ips.push(r[0])
286
+ return ips
259
287
  }
260
288
 
261
289
  /** A trivia combinator as data. `arms` empty means a plain `trivia(regex)`. */
@@ -629,6 +657,17 @@ export function classHas(cls: ResolvedClass, code: number): boolean {
629
657
  return false
630
658
  }
631
659
 
660
+ /**
661
+ * Whether a choice with these arm classes (indices into the class-spec pool `cc`)
662
+ * takes the O(1) exclusive dispatch — the same decision `resolveDispatch` makes,
663
+ * for an encoder that has only the specs.
664
+ */
665
+ export function dispatchIsExclusive(arms: readonly number[], cc: readonly string[]): boolean {
666
+ const resolved: ResolvedClass[] = []
667
+ for (const ci of arms) if (ci >= 0) resolved[ci] = resolveClass(cc[ci]!)
668
+ return resolveDispatch(arms, resolved).exclusive
669
+ }
670
+
632
671
  function resolveDispatch(arms: readonly number[], cc: readonly ResolvedClass[]): ResolvedDispatch {
633
672
  const ascii = new Uint8Array(128)
634
673
  const hi: number[] = []
@@ -0,0 +1,70 @@
1
+ /**
2
+ * WHICH SITES A SELECTIVE BUILD-TIME ASSEMBLY EMITS.
3
+ *
4
+ * The emitted engine beats the closure engine per site, but its bytes are paid by
5
+ * every process that loads the module. Emitting every site made jess's Less AST
6
+ * artifact 20.8x its grammar source. So a build emits only the sites with the
7
+ * highest ESTIMATED hotness per byte, up to a byte budget, and the closure engine
8
+ * runs the rest (`assemble.ts`, the `HYBRID` path).
9
+ *
10
+ * Hotness is estimated statically, since a macro build has no workload to
11
+ * profile: every rule entry starts at 1, a repetition multiplies its body by
12
+ * `LOOP`, and a choice or dispatch splits its weight evenly across its arms. The
13
+ * weights are propagated `ROUNDS` times, so a recursive cycle saturates at `CAP`
14
+ * instead of diverging. Sites are then taken greedily by weight per emitted byte.
15
+ * A build given a corpus (`assemblyProfile`) ranks by measured calls instead.
16
+ */
17
+ import { childSlots } from './child-slots.ts'
18
+ import { OP_CHOICE, OP_DISPATCH, OP_REP, OP_REPV } from './ops.ts'
19
+
20
+ const LOOP = 8
21
+ const ROUNDS = 60
22
+ const CAP = 1e12
23
+
24
+ /** The sites to emit: highest weight per byte first, while they fit in `budget` bytes. */
25
+ export function selectHotSites(
26
+ code: readonly number[],
27
+ entries: readonly number[],
28
+ siteBytes: ReadonlyMap<number, number>,
29
+ budget: number,
30
+ /** Measured calls per site (`countSiteVisits` over a corpus). Replaces the
31
+ * static estimate when given. */
32
+ profile?: ArrayLike<number>,
33
+ ): Set<number> {
34
+ const selected = new Set<number>()
35
+ if (budget <= 0) return selected
36
+ const weight = profile === undefined ? staticWeights(code, entries) : undefined
37
+ const w = (ip: number): number => (weight !== undefined ? weight.get(ip) : profile![ip]) ?? 0
38
+ // Ties (saturated cycles) break by site offset, so a build is deterministic.
39
+ const ranked = [...siteBytes.keys()].sort((a, b) =>
40
+ w(b) / siteBytes.get(b)! - w(a) / siteBytes.get(a)! || a - b)
41
+ let spent = 0
42
+ for (const ip of ranked) {
43
+ if (w(ip) === 0) break
44
+ const bytes = siteBytes.get(ip)!
45
+ if (spent + bytes > budget) continue
46
+ spent += bytes
47
+ selected.add(ip)
48
+ }
49
+ return selected
50
+ }
51
+
52
+ function staticWeights(code: readonly number[], entries: readonly number[]): Map<number, number> {
53
+ let weight = new Map<number, number>()
54
+ const kids: number[] = []
55
+ for (let round = 0; round < ROUNDS; round++) {
56
+ const next = new Map<number, number>()
57
+ for (const ip of entries) next.set(ip, 1)
58
+ for (const [ip, w] of weight) {
59
+ kids.length = 0
60
+ childSlots(code, ip, kids)
61
+ const op = code[ip]
62
+ const scale = op === OP_REP || op === OP_REPV ? LOOP
63
+ : op === OP_CHOICE || op === OP_DISPATCH ? 1 / Math.max(1, kids.length)
64
+ : 1
65
+ for (const kid of kids) next.set(kid, Math.min(CAP, (next.get(kid) ?? 0) + w * scale))
66
+ }
67
+ weight = next
68
+ }
69
+ return weight
70
+ }
@@ -0,0 +1,109 @@
1
+ import type { CharRange, Combinator, ParseContext } from '../types.ts'
2
+ import type { LabeledTriviaSpec } from '../cst/trivia-kinds.ts'
3
+ import { commitTriviaScan, fastTriviaScanner, scanTriviaCompact } from '../combinators/trivia-skip.ts'
4
+
5
+ /**
6
+ * THE TABLE ENGINES' DEFERRED-COMMIT TRIVIA SKIP: `commitTriviaScan(scanTriviaCompact(…))`,
7
+ * the path every labelled trivia scan takes once a log is live, answered without
8
+ * the scan where the answer is already known. Both engines call it: the closure
9
+ * engine's `skipTrivia` and the emitted assembly's skips.
10
+ *
11
+ * ONLY THE ROOT LOG IS LIVE, so only a root-selected chunk can be recorded,
12
+ * and a run in which no character can start a selected arm holds none: its end
13
+ * is the whole answer, and the unlabelled scanner gives it without the
14
+ * chunk-by-chunk visit. Every chunk starts inside the run, so a run free of
15
+ * selected leaders is free of selected chunks. jess parses with
16
+ * `rootTrivia: { select: [comment labels] }`, which is this case for every run
17
+ * of plain whitespace.
18
+ *
19
+ * A SEQUENCE TERM THAT MATCHES NOTHING hands back the cursor from BEFORE its
20
+ * trivia, so the next term asks for the same gap again: on `benchmark.less`,
21
+ * 21,741 of the closure engine's 53,607 trivia scans per parse started where
22
+ * the previous one did. A scan that recorded nothing records nothing again from
23
+ * the same position, in the same parse, under the same trivia, root selection,
24
+ * root-capture policy, trivia log and state, and with no CST capture, and it
25
+ * ends at the same place, so the last such scan answers the repeat (`ctx._gap`).
26
+ * The state is part of the key because a trivia arm can read it (`gate`), and
27
+ * the log because a `leaf` sets it aside while its child runs. The memo lives
28
+ * on the context so it is released with the parse.
29
+ *
30
+ * Neither applies with `trackLines` (a scan records line ranges) or under CST
31
+ * capture (a scan records into the open buffer).
32
+ */
33
+ export function skipDeferredTrivia(input: string, cur: number, ctx: ParseContext): number {
34
+ const triviaP = ctx.trivia
35
+ const plain = ctx.trackLines !== true && ctx.captureTrivia !== true
36
+ const gap = ctx._gap
37
+ if (plain && gap !== undefined && cur === gap.at && input === gap.input && triviaP === gap.of
38
+ && ctx._rootTriviaCapture === gap.root && ctx.state === gap.state
39
+ && ctx._rootTriviaKindIndex === gap.rootKinds && ctx._triviaLog === gap.log) return gap.end
40
+ const end = clearRunEnd(input, cur, ctx, triviaP, plain)
41
+ if (end >= 0) {
42
+ remember(cur, end, ctx, input, triviaP)
43
+ return end
44
+ }
45
+ const scan = scanTriviaCompact(input, cur, ctx)
46
+ if (typeof scan !== 'number') return commitTriviaScan(scan)
47
+ if (plain) remember(cur, scan, ctx, input, triviaP)
48
+ return scan
49
+ }
50
+
51
+ /** The end of the run at `cur` when nothing root-selected lies in it, else −1. */
52
+ function clearRunEnd(input: string, cur: number, ctx: ParseContext, triviaP: Combinator<unknown> | undefined, plain: boolean): number {
53
+ const rootKinds = ctx._rootTriviaKindIndex
54
+ const spec = triviaP?._meta.labeledTriviaSpec
55
+ if (!plain || triviaP === undefined || spec == null || rootKinds === undefined || ctx._triviaLog !== undefined) return -1
56
+ if (spec.rootKinds !== rootKinds) prime(spec, triviaP, rootKinds)
57
+ const fast = spec.rootScan
58
+ if (fast === null) return -1
59
+ const end = fast(input, cur)
60
+ return ctx._rootTriviaCapture === false || clearOf(spec.rootLeaders, input, cur, end) ? end : -1
61
+ }
62
+
63
+ function remember(cur: number, end: number, ctx: ParseContext, input: string, triviaP: Combinator<unknown> | undefined): void {
64
+ const gap = ctx._gap
65
+ if (gap === undefined) {
66
+ ctx._gap = { at: cur, end, input, of: triviaP, root: ctx._rootTriviaCapture, rootKinds: ctx._rootTriviaKindIndex, log: ctx._triviaLog, state: ctx.state }
67
+ return
68
+ }
69
+ gap.at = cur
70
+ gap.end = end
71
+ gap.input = input
72
+ gap.of = triviaP
73
+ gap.root = ctx._rootTriviaCapture
74
+ gap.rootKinds = ctx._rootTriviaKindIndex
75
+ gap.log = ctx._triviaLog
76
+ gap.state = ctx.state
77
+ }
78
+
79
+ /**
80
+ * Prime `spec` for the selection `rootKinds`: the first-character ranges of the
81
+ * arms it selects, and the scanner, which is `null` where one of those arms is
82
+ * unbounded or the trivia has no unlabelled scanner. `run()` builds one
83
+ * selection per parse, so this runs once per parse per trivia.
84
+ */
85
+ function prime(spec: LabeledTriviaSpec, triviaP: Combinator<unknown>, rootKinds: Readonly<Record<string, number>>): void {
86
+ const ranges: CharRange[] = []
87
+ let bounded = true
88
+ for (const arm of spec.arms) {
89
+ if (rootKinds[arm.label] === undefined) continue
90
+ const fs = arm.parser._meta.firstSet
91
+ if (fs.kind === 'any') bounded = false
92
+ else if (fs.kind === 'ranges') ranges.push(...fs.ranges)
93
+ }
94
+ const classified = triviaP._meta.triviaScanner
95
+ spec.rootKinds = rootKinds
96
+ spec.rootLeaders = ranges
97
+ spec.rootScan = !bounded ? null : classified === undefined ? fastTriviaScanner(triviaP) : classified
98
+ }
99
+
100
+ /** No character in `[from, to)` falls in `ranges`. A surrogate is never clear:
101
+ * the ranges are code points, and a code unit cannot be checked against them. */
102
+ function clearOf(ranges: readonly CharRange[], input: string, from: number, to: number): boolean {
103
+ for (let i = from; i < to; i++) {
104
+ const c = input.charCodeAt(i)
105
+ if (c >= 0xD800 && c <= 0xDFFF) return false
106
+ for (const r of ranges) if (c >= r.lo && c <= r.hi) return false
107
+ }
108
+ return true
109
+ }
package/src/types.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { LineView } from './combinators/lines.ts'
1
2
  import type { LabeledTriviaSpec } from './cst/trivia-kinds.ts'
2
3
 
3
4
  export type Span = {
@@ -113,6 +114,11 @@ export type ParserDef =
113
114
  // pre-ambient raw byte-walk.
114
115
  | { tag: 'scanTo'; sentinel: Combinator<unknown>; skip: Combinator<unknown>[]; raw: boolean; orEOF: boolean; recoverAt?: Combinator<unknown>; stopAt?: Combinator<unknown> }
115
116
  | { tag: 'keywords'; words: readonly string[]; caseInsensitive: boolean; boundary: string | undefined }
117
+ // `lines()`: `body` parses a run of lines with `prefix` (`first` on the first
118
+ // line) removed from each; `lazy` admits a line the prefix does not match.
119
+ | { tag: 'lines'; prefix: Combinator<unknown>; body: Combinator<unknown>; first?: Combinator<unknown>; lazy?: Combinator<unknown> }
120
+ // `columns(n)`: n columns of indentation, a tab reaching the next stop of four.
121
+ | { tag: 'columns'; n: number }
116
122
  | { tag: 'unknown' }
117
123
 
118
124
  export type Combinator<T> = {
@@ -280,6 +286,14 @@ export type ParseContext = {
280
286
  trackLines: boolean
281
287
  /** Grammar-author-provided state, scoped with withCtx() and read in guard(). */
282
288
  state?: unknown
289
+ /**
290
+ * Inside a `lines()` body, the run's lines: where each line's content sits in
291
+ * the body's input and in the run's own input, and the column it starts at.
292
+ * Undefined outside any run.
293
+ */
294
+ lineView?: LineView | undefined
295
+ /** Framework-internal: where `columns()` last stopped inside a tab, for `lines()`. */
296
+ _columnsSplit?: { at: number; split: number } | undefined
283
297
  /**
284
298
  * Mode host (RULE_ABI_PLAN §7): when set, a linkable/fused grammar's `node()`
285
299
  * rules build via `build(type, children, fields, span, rawChildren, triviaLog, state)`
@@ -384,6 +398,12 @@ export type ParseContext = {
384
398
  * selected markers into the document-root capture.
385
399
  */
386
400
  _rootTriviaCapture?: boolean | undefined
401
+ /**
402
+ * Framework-internal: the last deferred-commit trivia scan in this parse that
403
+ * recorded nothing (`table/trivia-gap.ts`). Held on the context so it is freed
404
+ * with the parse.
405
+ */
406
+ _gap?: TriviaGapMemo | undefined
387
407
  /**
388
408
  * Framework-internal: flat per-node trivia log for CST capture mode.
389
409
  * When set alongside _cstRawChildren, each trivia entry is recorded as three
@@ -592,3 +612,15 @@ export type CompiledParser<T> = {
592
612
  /** Present only when compiled with `{ coverage: true }`. */
593
613
  coverageDefinitions?: readonly import('./compiler/grammar-coverage-ids.ts').GrammarCoverageDefinition[]
594
614
  }
615
+
616
+ /** A trivia gap scanned from `at` to `end` that recorded nothing, and the key it holds for. */
617
+ export interface TriviaGapMemo {
618
+ at: number
619
+ end: number
620
+ input: string
621
+ of: Combinator<unknown> | undefined
622
+ root: boolean | undefined
623
+ rootKinds: Readonly<Record<string, number>> | undefined
624
+ log: number[] | undefined
625
+ state: unknown
626
+ }
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.55.4'
16
+ export const PARSEMAN_VERSION = '0.57.0'