parseman 0.46.0 → 0.47.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 (316) hide show
  1. package/README.md +8 -3
  2. package/dist/analysis/choice-cost-render.d.ts +12 -4
  3. package/dist/analysis/choice-cost-render.d.ts.map +1 -1
  4. package/dist/analysis/choice-cost.d.ts +2 -2
  5. package/dist/analysis/choice-cost.d.ts.map +1 -1
  6. package/dist/analysis/commitment.d.ts +131 -0
  7. package/dist/analysis/commitment.d.ts.map +1 -0
  8. package/dist/analysis/diagnostics.cjs +10339 -7787
  9. package/dist/analysis/diagnostics.cjs.map +3 -4
  10. package/dist/analysis/diagnostics.d.ts +2 -2
  11. package/dist/analysis/diagnostics.js +10339 -7787
  12. package/dist/analysis/diagnostics.js.map +3 -4
  13. package/dist/analysis/duplication.d.ts.map +1 -1
  14. package/dist/analysis/fix.d.ts +3 -3
  15. package/dist/analysis/fix.d.ts.map +1 -1
  16. package/dist/analysis/format-number.d.ts +23 -0
  17. package/dist/analysis/format-number.d.ts.map +1 -0
  18. package/dist/analysis/gating.d.ts +39 -2
  19. package/dist/analysis/gating.d.ts.map +1 -1
  20. package/dist/analysis/rebuild.d.ts.map +1 -1
  21. package/dist/analysis/terminal.d.ts +5 -2
  22. package/dist/analysis/terminal.d.ts.map +1 -1
  23. package/dist/cli/index.cjs +10924 -8506
  24. package/dist/cli/index.cjs.map +3 -4
  25. package/dist/cli/index.js +10924 -8506
  26. package/dist/cli/index.js.map +3 -4
  27. package/dist/combinators/adjacency.d.ts +100 -0
  28. package/dist/combinators/adjacency.d.ts.map +1 -0
  29. package/dist/combinators/choice.d.ts.map +1 -1
  30. package/dist/combinators/completions.d.ts.map +1 -1
  31. package/dist/combinators/dispatch.d.ts +8 -1
  32. package/dist/combinators/dispatch.d.ts.map +1 -1
  33. package/dist/combinators/expect.d.ts.map +1 -1
  34. package/dist/combinators/first-set.d.ts.map +1 -1
  35. package/dist/combinators/gate.d.ts.map +1 -1
  36. package/dist/combinators/grammar.d.ts +22 -0
  37. package/dist/combinators/grammar.d.ts.map +1 -1
  38. package/dist/combinators/literal.d.ts +8 -0
  39. package/dist/combinators/literal.d.ts.map +1 -1
  40. package/dist/combinators/map.d.ts +0 -1
  41. package/dist/combinators/map.d.ts.map +1 -1
  42. package/dist/combinators/node.d.ts +13 -3
  43. package/dist/combinators/node.d.ts.map +1 -1
  44. package/dist/combinators/parser.d.ts +1 -1
  45. package/dist/combinators/parser.d.ts.map +1 -1
  46. package/dist/combinators/ref.d.ts.map +1 -1
  47. package/dist/combinators/regex.d.ts +1 -16
  48. package/dist/combinators/regex.d.ts.map +1 -1
  49. package/dist/combinators/repeat.d.ts +30 -2
  50. package/dist/combinators/repeat.d.ts.map +1 -1
  51. package/dist/combinators/scanTo.d.ts +71 -1
  52. package/dist/combinators/scanTo.d.ts.map +1 -1
  53. package/dist/combinators/sequence.d.ts.map +1 -1
  54. package/dist/combinators/token.d.ts.map +1 -1
  55. package/dist/combinators/trivia-boundary.d.ts +26 -0
  56. package/dist/combinators/trivia-boundary.d.ts.map +1 -0
  57. package/dist/combinators/trivia-skip.d.ts +45 -1
  58. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  59. package/dist/combinators/withCtx.d.ts.map +1 -1
  60. package/dist/compiler/compile-linkable-table.d.ts +104 -0
  61. package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
  62. package/dist/compiler/degradation.d.ts +0 -2
  63. package/dist/compiler/degradation.d.ts.map +1 -1
  64. package/dist/compiler/fields.d.ts +13 -0
  65. package/dist/compiler/fields.d.ts.map +1 -1
  66. package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
  67. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  68. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  69. package/dist/compiler/linker.d.ts +47 -53
  70. package/dist/compiler/linker.d.ts.map +1 -1
  71. package/dist/compiler/token-alphabet.d.ts +81 -0
  72. package/dist/compiler/token-alphabet.d.ts.map +1 -0
  73. package/dist/compiler/token-dispatch.d.ts +95 -0
  74. package/dist/compiler/token-dispatch.d.ts.map +1 -0
  75. package/dist/compiler/token-scanner.d.ts +53 -0
  76. package/dist/compiler/token-scanner.d.ts.map +1 -0
  77. package/dist/compiler/value-usage.d.ts.map +1 -1
  78. package/dist/coverage.d.ts +1 -1
  79. package/dist/coverage.d.ts.map +1 -1
  80. package/dist/cst/capture-buffer.d.ts +35 -0
  81. package/dist/cst/capture-buffer.d.ts.map +1 -1
  82. package/dist/cst/reflection-attach.d.ts +8 -0
  83. package/dist/cst/reflection-attach.d.ts.map +1 -0
  84. package/dist/cst/reflection-symbols.d.ts +10 -0
  85. package/dist/cst/reflection-symbols.d.ts.map +1 -0
  86. package/dist/cst/reflection.d.ts +2 -4
  87. package/dist/cst/reflection.d.ts.map +1 -1
  88. package/dist/cst/root-trivia-scope.d.ts +26 -0
  89. package/dist/cst/root-trivia-scope.d.ts.map +1 -0
  90. package/dist/cst/trivia-charscan.d.ts +18 -0
  91. package/dist/cst/trivia-charscan.d.ts.map +1 -0
  92. package/dist/cst/trivia-kinds.d.ts +19 -0
  93. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  94. package/dist/functional/doc.d.ts.map +1 -1
  95. package/dist/functional/run-tabled.d.ts +12 -0
  96. package/dist/functional/run-tabled.d.ts.map +1 -0
  97. package/dist/functional/run.d.ts +25 -42
  98. package/dist/functional/run.d.ts.map +1 -1
  99. package/dist/grammar-metadata.d.ts +3 -0
  100. package/dist/grammar-metadata.d.ts.map +1 -0
  101. package/dist/index.cjs +9290 -7181
  102. package/dist/index.cjs.map +3 -4
  103. package/dist/index.d.ts +18 -9
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +9300 -7191
  106. package/dist/index.js.map +3 -4
  107. package/dist/language-service/index.cjs +313 -121
  108. package/dist/language-service/index.cjs.map +3 -4
  109. package/dist/language-service/index.js +313 -121
  110. package/dist/language-service/index.js.map +3 -4
  111. package/dist/oracle/index.cjs.map +0 -1
  112. package/dist/oracle/index.js.map +0 -1
  113. package/dist/parse-context.d.ts +49 -0
  114. package/dist/parse-context.d.ts.map +1 -0
  115. package/dist/plugin/evaluator.d.ts +40 -1
  116. package/dist/plugin/evaluator.d.ts.map +1 -1
  117. package/dist/plugin/index.cjs +11175 -9534
  118. package/dist/plugin/index.cjs.map +3 -4
  119. package/dist/plugin/index.d.ts.map +1 -1
  120. package/dist/plugin/index.js +11175 -9534
  121. package/dist/plugin/index.js.map +3 -4
  122. package/dist/regex/classes.d.ts +28 -0
  123. package/dist/regex/classes.d.ts.map +1 -1
  124. package/dist/regex/first-set.d.ts +14 -0
  125. package/dist/regex/first-set.d.ts.map +1 -1
  126. package/dist/run/index.cjs +136 -51
  127. package/dist/run/index.cjs.map +2 -3
  128. package/dist/run/index.js +136 -51
  129. package/dist/run/index.js.map +2 -3
  130. package/dist/spec/index.cjs +2934 -1611
  131. package/dist/spec/index.cjs.map +3 -4
  132. package/dist/spec/index.js +2934 -1611
  133. package/dist/spec/index.js.map +3 -4
  134. package/dist/spec/model.d.ts +8 -0
  135. package/dist/spec/model.d.ts.map +1 -1
  136. package/dist/table/assemble.d.ts +307 -0
  137. package/dist/table/assemble.d.ts.map +1 -0
  138. package/dist/table/cell.d.ts +42 -0
  139. package/dist/table/cell.d.ts.map +1 -0
  140. package/dist/table/child-slots.d.ts +30 -0
  141. package/dist/table/child-slots.d.ts.map +1 -0
  142. package/dist/table/compile-rule-map.d.ts +136 -0
  143. package/dist/table/compile-rule-map.d.ts.map +1 -0
  144. package/dist/table/compile.d.ts +56 -0
  145. package/dist/table/compile.d.ts.map +1 -0
  146. package/dist/table/duplication-hook.d.ts +32 -0
  147. package/dist/table/duplication-hook.d.ts.map +1 -0
  148. package/dist/table/emit-assembly.d.ts +139 -0
  149. package/dist/table/emit-assembly.d.ts.map +1 -0
  150. package/dist/table/emit.d.ts +113 -0
  151. package/dist/table/emit.d.ts.map +1 -0
  152. package/dist/table/encode-baseline.d.ts +16 -0
  153. package/dist/table/encode-baseline.d.ts.map +1 -0
  154. package/dist/table/encode.d.ts +71 -0
  155. package/dist/table/encode.d.ts.map +1 -0
  156. package/dist/table/exec-baseline.d.ts +9 -0
  157. package/dist/table/exec-baseline.d.ts.map +1 -0
  158. package/dist/table/exec.d.ts +72 -0
  159. package/dist/table/exec.d.ts.map +1 -0
  160. package/dist/table/fold.d.ts +25 -0
  161. package/dist/table/fold.d.ts.map +1 -0
  162. package/dist/table/index.cjs +11463 -0
  163. package/dist/table/index.cjs.map +6 -0
  164. package/dist/table/index.d.ts +100 -0
  165. package/dist/table/index.d.ts.map +1 -0
  166. package/dist/table/index.js +11440 -0
  167. package/dist/table/index.js.map +6 -0
  168. package/dist/table/inspect.d.ts +17 -0
  169. package/dist/table/inspect.d.ts.map +1 -0
  170. package/dist/table/ops.d.ts +453 -0
  171. package/dist/table/ops.d.ts.map +1 -0
  172. package/dist/table/program.d.ts +496 -0
  173. package/dist/table/program.d.ts.map +1 -0
  174. package/dist/table/run-support.d.ts +38 -0
  175. package/dist/table/run-support.d.ts.map +1 -0
  176. package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
  177. package/dist/table/scan-shapes.d.ts.map +1 -0
  178. package/dist/table/site-labels.d.ts +58 -0
  179. package/dist/table/site-labels.d.ts.map +1 -0
  180. package/dist/table/stamp.d.ts +70 -0
  181. package/dist/table/stamp.d.ts.map +1 -0
  182. package/dist/types.d.ts +80 -24
  183. package/dist/types.d.ts.map +1 -1
  184. package/dist/version.d.ts +1 -1
  185. package/package.json +26 -2
  186. package/src/analysis/choice-cost-gate.ts +359 -0
  187. package/src/analysis/choice-cost-render.ts +293 -0
  188. package/src/analysis/choice-cost.ts +1008 -0
  189. package/src/analysis/commitment.ts +557 -0
  190. package/src/analysis/corpus.ts +130 -0
  191. package/src/analysis/diagnose-render.ts +625 -0
  192. package/src/analysis/diagnose.ts +317 -0
  193. package/src/analysis/diagnostics.ts +33 -0
  194. package/src/analysis/duplication.ts +1955 -0
  195. package/src/analysis/fix-render.ts +217 -0
  196. package/src/analysis/fix.ts +669 -0
  197. package/src/analysis/format-number.ts +31 -0
  198. package/src/analysis/gating.ts +792 -0
  199. package/src/analysis/grammar.ts +72 -0
  200. package/src/analysis/rebuild.ts +287 -0
  201. package/src/analysis/terminal.ts +336 -0
  202. package/src/cli/index.ts +413 -0
  203. package/src/combinators/adjacency.ts +171 -0
  204. package/src/combinators/attempt.ts +41 -0
  205. package/src/combinators/case-fold.ts +90 -0
  206. package/src/combinators/choice.ts +466 -0
  207. package/src/combinators/completions.ts +64 -0
  208. package/src/combinators/dispatch.ts +358 -0
  209. package/src/combinators/expect.ts +159 -0
  210. package/src/combinators/first-set.ts +348 -0
  211. package/src/combinators/gate.ts +50 -0
  212. package/src/combinators/grammar.ts +272 -0
  213. package/src/combinators/keywords.ts +145 -0
  214. package/src/combinators/literal.ts +102 -0
  215. package/src/combinators/map.ts +106 -0
  216. package/src/combinators/node.ts +345 -0
  217. package/src/combinators/not.ts +55 -0
  218. package/src/combinators/parser.ts +266 -0
  219. package/src/combinators/peek.ts +63 -0
  220. package/src/combinators/probe.ts +25 -0
  221. package/src/combinators/ref.ts +109 -0
  222. package/src/combinators/regex.ts +170 -0
  223. package/src/combinators/repeat.ts +561 -0
  224. package/src/combinators/scanTo.ts +393 -0
  225. package/src/combinators/sequence.ts +237 -0
  226. package/src/combinators/token.ts +120 -0
  227. package/src/combinators/trivia-boundary.ts +48 -0
  228. package/src/combinators/trivia-skip.ts +852 -0
  229. package/src/combinators/withCtx.ts +45 -0
  230. package/src/compiler/build-arity.ts +330 -0
  231. package/src/compiler/compile-linkable-table.ts +209 -0
  232. package/src/compiler/degradation.ts +266 -0
  233. package/src/compiler/fields.ts +191 -0
  234. package/src/compiler/grammar-coverage-ids.ts +129 -0
  235. package/src/compiler/ir-serialize.ts +615 -0
  236. package/src/compiler/linker.ts +954 -0
  237. package/src/compiler/token-alphabet.ts +209 -0
  238. package/src/compiler/token-dispatch.ts +353 -0
  239. package/src/compiler/token-scanner.ts +318 -0
  240. package/src/compiler/value-usage.ts +147 -0
  241. package/src/coverage.ts +383 -0
  242. package/src/cst/capture-buffer.ts +297 -0
  243. package/src/cst/host-mode.ts +69 -0
  244. package/src/cst/reflection-attach.ts +11 -0
  245. package/src/cst/reflection-symbols.ts +9 -0
  246. package/src/cst/reflection.ts +149 -0
  247. package/src/cst/relative-spans.ts +207 -0
  248. package/src/cst/root-trivia-scope.ts +30 -0
  249. package/src/cst/trivia-charscan.ts +312 -0
  250. package/src/cst/trivia-entries.ts +285 -0
  251. package/src/cst/trivia-index.ts +131 -0
  252. package/src/cst/trivia-kinds.ts +358 -0
  253. package/src/cst/types.ts +76 -0
  254. package/src/cst/walk.ts +101 -0
  255. package/src/functional/doc.ts +959 -0
  256. package/src/functional/run-tabled.ts +153 -0
  257. package/src/functional/run.ts +558 -0
  258. package/src/grammar-metadata.ts +2 -0
  259. package/src/index.ts +189 -0
  260. package/src/language-service/index.ts +221 -0
  261. package/src/line-index.ts +131 -0
  262. package/src/oracle/digest.ts +620 -0
  263. package/src/oracle/index.ts +55 -0
  264. package/src/parse-context.ts +91 -0
  265. package/src/plugin/direct-builder-static.ts +102 -0
  266. package/src/plugin/evaluator.ts +1505 -0
  267. package/src/plugin/free-identifiers.ts +338 -0
  268. package/src/plugin/index.ts +2281 -0
  269. package/src/plugin/reducer-resolver.ts +606 -0
  270. package/src/recovery/scan.ts +155 -0
  271. package/src/regex/classes.ts +183 -0
  272. package/src/regex/first-set.ts +316 -0
  273. package/src/run/index.ts +31 -0
  274. package/src/spec/ebnf.ts +128 -0
  275. package/src/spec/index.ts +54 -0
  276. package/src/spec/model.ts +636 -0
  277. package/src/spec/railroad-lib.ts +559 -0
  278. package/src/spec/railroad.ts +256 -0
  279. package/src/table/assemble.ts +3006 -0
  280. package/src/table/cell.ts +42 -0
  281. package/src/table/child-slots.ts +113 -0
  282. package/src/table/compile-rule-map.ts +336 -0
  283. package/src/table/compile.ts +244 -0
  284. package/src/table/duplication-hook.ts +89 -0
  285. package/src/table/emit-assembly.ts +1777 -0
  286. package/src/table/emit.ts +429 -0
  287. package/src/table/encode-baseline.ts +340 -0
  288. package/src/table/encode.ts +1569 -0
  289. package/src/table/exec-baseline.ts +537 -0
  290. package/src/table/exec.ts +1680 -0
  291. package/src/table/fold.ts +38 -0
  292. package/src/table/index.ts +108 -0
  293. package/src/table/inspect.ts +52 -0
  294. package/src/table/ops.ts +467 -0
  295. package/src/table/program.ts +876 -0
  296. package/src/table/run-support.ts +66 -0
  297. package/src/table/scan-shapes.ts +1578 -0
  298. package/src/table/site-labels.ts +228 -0
  299. package/src/table/stamp.ts +229 -0
  300. package/src/types.ts +563 -0
  301. package/src/version.ts +16 -0
  302. package/dist/compiler/codegen.d.ts +0 -334
  303. package/dist/compiler/codegen.d.ts.map +0 -1
  304. package/dist/compiler/inline-build.d.ts +0 -11
  305. package/dist/compiler/inline-build.d.ts.map +0 -1
  306. package/dist/compiler/inline-callback.d.ts +0 -21
  307. package/dist/compiler/inline-callback.d.ts.map +0 -1
  308. package/dist/compiler/line-index.d.ts +0 -2
  309. package/dist/compiler/line-index.d.ts.map +0 -1
  310. package/dist/compiler/module-hoist.d.ts +0 -106
  311. package/dist/compiler/module-hoist.d.ts.map +0 -1
  312. package/dist/compiler/scannable-run.d.ts.map +0 -1
  313. package/dist/compiler/scannable-terminal.d.ts +0 -23
  314. package/dist/compiler/scannable-terminal.d.ts.map +0 -1
  315. package/dist/compiler/trivia-fast-path.d.ts +0 -38
  316. package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
@@ -0,0 +1,1680 @@
1
+ import type { Combinator, FieldMap, FirstSet, ParseContext, ParseResult, ParserDef } from '../types.ts'
2
+ import { balanced, scanTo } from '../combinators/scanTo.ts'
3
+ import { buildFieldMap } from '../compiler/fields.ts'
4
+ import { asciiFoldKey } from '../combinators/dispatch.ts'
5
+ import { projectChild, unwrapChild } from '../combinators/node.ts'
6
+ import { asciiFoldEq } from '../combinators/literal.ts'
7
+ import { cstOutputHost } from '../compiler/build-arity.ts'
8
+ import { consumeTrivia } from '../combinators/trivia-skip.ts'
9
+ import { advanceTrivia, needsDeferredTriviaCommit, rollbackScannedTriviaAt, rollbackTrivia, rollbackTriviaAt, saveTriviaMark, scanTrivia, skipTriviaScanned, type FastTriviaScanner } from '../combinators/trivia-skip.ts'
10
+ import {
11
+ beginCstNodeCapture, cstCaptureActive, cstLeavesLen, cstRawLen, cstTlLen,
12
+ demoteCapturedToRaw, endCstNodeCapture, pushCstChild, pushCstLeaf,
13
+ } from '../cst/capture-buffer.ts'
14
+ import {
15
+ OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
16
+ OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SEQ, OP_SEQV, OP_XFORM,
17
+ OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_EXPECT, OP_SEQX, OP_SCAN,
18
+ OP_LIVE,
19
+ OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD, OP_ATTEMPT, OP_LABEL,
20
+ OP_COV,
21
+ OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE,
22
+ } from './ops.ts'
23
+ import { adjacencyHolds, adjacencyMisuse } from '../combinators/adjacency.ts'
24
+ import { failAt } from '../combinators/probe.ts'
25
+ /**
26
+ * THE COMPLETIONS PROBE, at the terminal fail sites and nowhere else.
27
+ *
28
+ * `failAt` (combinators/probe.ts) is called from exactly three places in the
29
+ * interpreter — `literal.ts`, `regex.ts`, `keywords.ts` — and codegen emits its
30
+ * mirror (`probeUpdate`) at the same leaf-fail sites. The table recorded
31
+ * NOTHING, so `completionsAt` on a table artifact saw only the top-level
32
+ * failure: a swallowed item failure inside a `sepBy` (`'{'` at offset 1 in a
33
+ * `{ decl; }` grammar) never reached the probe and the item's opener vanished
34
+ * from the completion set.
35
+ *
36
+ * Dormant unless `completionsAt` set `_probe`, so an ordinary parse pays one
37
+ * property read per terminal miss — the same price codegen pays.
38
+ */
39
+ import { committed, spanLines, stampRuleMap } from './stamp.ts'
40
+ import { refuseUnclassifiedRootScope } from '../cst/root-trivia-scope.ts'
41
+ import { captureError, firstSetSentinel, matchesAt, orSentinel, recoverScan } from '../recovery/scan.ts'
42
+ import {
43
+ decodeClassSpec, expandCompact, resolveTable,
44
+ type CompactProgram, type ResolvedClass, type ResolvedDispatch, type ResolvedDispatchSpec,
45
+ type SubtreeRef, type TableProgram, type TableRule,
46
+ } from './program.ts'
47
+
48
+ /**
49
+ * THE SHARED DRIVER.
50
+ *
51
+ * One recognizer for every grammar, every rule and every variant — the thing
52
+ * codegen emits once PER RULE, and which therefore sets the per-rule byte floor.
53
+ * It uses the same zero-allocation protocol the emitted code uses (a sentinel
54
+ * for failure plus a shared end-position slot), so what it adds over open-coded
55
+ * recognition is the opcode read and the switch, not an allocation.
56
+ *
57
+ * The driver is instantiated ONCE PER RESOLVED TABLE, closing over that table's
58
+ * arrays. Instruction operands are then reads of context slots rather than
59
+ * property loads through an object, and — the point of G5 — an option can never
60
+ * be consulted here: two settings pairs are two tables, not two code paths.
61
+ */
62
+
63
+ /**
64
+ * Failure sentinel and end-position cell — SHARED with the other two engines,
65
+ * see `cell.ts`. Private copies were correct only while one engine ran a whole
66
+ * parse; a mixed assembly has two live at once and a private sentinel reads as
67
+ * a successful match to the other engine.
68
+ */
69
+ import { FAIL, newEndCell, type EndCell } from './cell.ts'
70
+
71
+ /**
72
+ * DIAGNOSTIC ROW COUNTER — off unless `PM_TABLE_COUNT=1` at process start.
73
+ *
74
+ * Read ONCE, at module load, into a module const, so this is not the per-parse
75
+ * option branch G5 forbids: with the variable false the whole thing folds away
76
+ * after tier-up. It exists because "too many rows" and "each row too slow" are
77
+ * different defects with the same symptom, and only a count separates them.
78
+ * No timing run in this repo may set it — see `bench/jess/table-rows.ts`.
79
+ */
80
+ export const tableCounters: {
81
+ rows: number
82
+ byOp: Int32Array
83
+ /** Distinct reducer/host functions actually reaching each shared call site. */
84
+ sites: Map<string, Set<unknown>>
85
+ /**
86
+ * SPECULATION AT UNGATED CHOICES. `encode.ts:368-376` gives a choice its
87
+ * first-char dispatch only when NO arm is nullable, ALL arms are pairwise
88
+ * disjoint and ALL map to a char class; any one failure ungates every arm of
89
+ * the site, which then runs the linear loop below. Codegen has no such
90
+ * all-or-nothing rule — `emitFirstMatch` guards each arm on its OWN first set
91
+ * — so these counts are work the table does and the compiled engine does not.
92
+ *
93
+ * `ungatedEntries` is arms entered there; `ungatedFails` is those that then
94
+ * failed, which is pure waste by construction: a failing arm consumed nothing
95
+ * the winning arm will not scan again.
96
+ */
97
+ ungatedEntries: number
98
+ ungatedFails: number
99
+ /** Rows executed INSIDE failed ungated arms — the subtree each wasted entry drags. */
100
+ ungatedFailRows: number
101
+ gatedEntries: number
102
+ /** Arm entries DECLINED by the per-arm class gate — the work this change removes. */
103
+ armGateSkips: number
104
+ } = { rows: 0, byOp: new Int32Array(64), sites: new Map(), ungatedEntries: 0, ungatedFails: 0, ungatedFailRows: 0, gatedEntries: 0, armGateSkips: 0 }
105
+
106
+ const COUNT = process.env.PM_TABLE_COUNT === '1'
107
+
108
+ function siteFn(site: string, fn: unknown): void {
109
+ let s = tableCounters.sites.get(site)
110
+ if (s === undefined) { s = new Set(); tableCounters.sites.set(site, s) }
111
+ s.add(fn)
112
+ }
113
+
114
+ export function resetTableCounters(): void {
115
+ tableCounters.rows = 0
116
+ tableCounters.byOp = new Int32Array(64)
117
+ tableCounters.sites = new Map()
118
+ tableCounters.ungatedEntries = 0
119
+ tableCounters.ungatedFails = 0
120
+ tableCounters.ungatedFailRows = 0
121
+ tableCounters.gatedEntries = 0
122
+ tableCounters.armGateSkips = 0
123
+ }
124
+
125
+ const EMPTY_TL: readonly number[] = Object.freeze([])
126
+ const EMPTY_FX: string[] = []
127
+ const ROUTED_FX: string[] = ['routed()']
128
+ /**
129
+ * Does an encoded matcher arm claim this selector key?
130
+ *
131
+ * Reads the ENCODED operands directly rather than rebuilding a
132
+ * `DispatchMatcherCase` and calling `matchesDispatchMatcher`. That detour
133
+ * allocated a matcher shape per arm per parse AND hardcoded
134
+ * `caseInsensitive: false`, silently discarding `{ caseInsensitive: true }` on
135
+ * matcher arms. Case-insensitivity is folded into the operands by the encoder:
136
+ * kinds 3/4 are pre-folded startsWith/endsWith, and a case-insensitive
137
+ * `matches` already carries `i` in its flags.
138
+ */
139
+ function matcherClaims(m: readonly [number, string, string, number], key: string): boolean {
140
+ switch (m[0]) {
141
+ case 0: return key.startsWith(m[1])
142
+ case 1: return key.endsWith(m[1])
143
+ case 3: return asciiFoldKey(key).startsWith(m[1])
144
+ case 4: return asciiFoldKey(key).endsWith(m[1])
145
+ // A fresh RegExp per test, as `matchesDispatchMatcher` builds — a cached one
146
+ // would carry `lastIndex` across parses whenever the author's pattern was
147
+ // sticky or global.
148
+ default: return new RegExp(m[1], m[2]).test(key)
149
+ }
150
+ }
151
+
152
+ type Leaf = { _tag: 'leaf'; value: string; span: { start: number; end: number } }
153
+
154
+ function rawEntry(v: unknown, input: string, s: number, e: number): unknown {
155
+ if (typeof v === 'object' && v !== null) {
156
+ const t = (v as { _tag?: string })._tag
157
+ if (t === 'node' || t === 'leaf' || t === 'parseError') return v
158
+ }
159
+ return { _tag: 'leaf', value: typeof v === 'string' ? v : (typeof v === 'object' && v !== null ? input.slice(s, e) : ''), span: { start: s, end: e } }
160
+ }
161
+
162
+ /** Same semantics as `trackLinesInto` in `src/table/assemble.ts` and `_trackLines`
163
+ * in `src/table/emit-assembly.ts` — three copies of one scan, and this is the one
164
+ * the emitted engine does not use. (Named for the source lowering before it, which
165
+ * spelled this `LINE_TRACK_DECL`; that module was deleted in `37c57b5`.) */
166
+ function trackLines(ctx: ParseContext, input: string, end: number): void {
167
+ const from = ctx._lineScannedTo ?? 0
168
+ if (end <= from) return
169
+ const starts = ctx._lineStarts
170
+ if (starts === undefined) return
171
+ for (let i = from; i < end; i++) if (input.charCodeAt(i) === 10) starts.push(i + 1)
172
+ ctx._lineScannedTo = end
173
+ }
174
+
175
+ function classHas(cls: ResolvedClass, code: number): boolean {
176
+ if (code < 0) return false
177
+ if (code < 128) return cls.ascii[code] === 1
178
+ const hi = cls.hi
179
+ for (let i = 0; i < hi.length; i += 2) if (code >= hi[i]! && code <= hi[i + 1]!) return true
180
+ return false
181
+ }
182
+
183
+ type Driver = {
184
+ exec: (ip: number, input: string, pos: number, ctx: ParseContext) => unknown
185
+ end: () => number
186
+ /** Per-parse reset of the trivia leaf swap. See `begin` in `makeDriver`. */
187
+ begin: (ctx: ParseContext) => void
188
+ /** Restore a suspended outer entry after a re-entrant parse. */
189
+ finish: () => void
190
+ /** Ambient `scanSkip` sets, rebuilt from `prog.scanSkip`, indexed as encoded. */
191
+ scanSkip: readonly (readonly Combinator<unknown>[])[]
192
+ }
193
+
194
+ function makeDriver(
195
+ code: Int32Array,
196
+ k: readonly unknown[],
197
+ fns: readonly unknown[],
198
+ cc: readonly ResolvedClass[],
199
+ fx: readonly (readonly string[])[],
200
+ disp: readonly ResolvedDispatch[],
201
+ dsp: readonly ResolvedDispatchSpec[],
202
+ trivia: readonly unknown[],
203
+ triviaScan: readonly (FastTriviaScanner | null)[],
204
+ triviaLabelled: readonly boolean[],
205
+ prog: TableProgram,
206
+ /**
207
+ * The assembly's end-position cell (`_pfEnd` in emitted code). INJECTED
208
+ * rather than owned, so a mixed assembly's driver and its emitted pieces
209
+ * write one slot — see `cell.ts`.
210
+ */
211
+ EC: EndCell,
212
+ ): Driver {
213
+
214
+ /**
215
+ * THE INSTALLED TRIVIA LEAF — G5's *"some swaps on rules or sub-rules
216
+ * (leafs)"*, which is the half of that sentence the driver had not honoured.
217
+ *
218
+ * `SCAN` is the SPECIALISED scanner for the trivia currently in scope, chosen
219
+ * once at `OP_SCOPE` from `triviaScan` and null when no swap is legal. The
220
+ * generic path it replaces called `advanceTrivia` per sequence term, and that
221
+ * function re-derived the scanner through a WeakMap and re-tested the same
222
+ * options on every call — for json that was 22.0% of the table's time against
223
+ * codegen's 6.1% for the identical work.
224
+ *
225
+ * `FAST` is the one condition that is a property of the PARSE rather than of
226
+ * the scope (`ctx.trackLines`, fixed by `run()` at entry), so it is read once
227
+ * per parse in the entry wrapper and folded into `SCAN` at each scope. Nothing
228
+ * on the term path consults the table or an option.
229
+ */
230
+ let SCAN: FastTriviaScanner | null = null
231
+ let FAST = false
232
+ /**
233
+ * IS THIS A RECOVERY TABLE? Table data, decided once when the driver is built,
234
+ * exactly as the assembler decides it — the two engines must agree here or a
235
+ * tolerant parse diverges on the one path no identity digest covers, since
236
+ * `errors` and `expected` are not in it.
237
+ *
238
+ * Recovery is DORMANT until a parse sets `ctx._tolerant`.
239
+ */
240
+ const REC = prog.rec === 1
241
+ // Line tracking is table data, selected once at encode time. `OP_EXPECT` has
242
+ // no tracked opcode variant, so it reads this assembly-wide constant when it
243
+ // constructs the zero-width recovery diagnostic.
244
+ const LINES = prog.lines === 1
245
+ /**
246
+ * Sync sentinels by char-class index, built at most once each. The ranges are
247
+ * recoverable from the class spec, and `firstSetSentinel` is the interpreter's
248
+ * own constructor — the same object codegen builds per publish through
249
+ * `_ctx._rec.sentinel(...)`.
250
+ */
251
+ const sentinels = new Map<number, Combinator<unknown> | undefined>()
252
+ function sentinelFor(cls: number): Combinator<unknown> | undefined {
253
+ if (cls < 0) return undefined
254
+ if (sentinels.has(cls)) return sentinels.get(cls)
255
+ const spec = prog.cc[cls]!
256
+ const ranges = decodeClassSpec(spec)
257
+ const made = firstSetSentinel({ kind: 'ranges', ranges }) ?? undefined
258
+ sentinels.set(cls, made)
259
+ return made
260
+ }
261
+ /**
262
+ * Is this parse's host a CST-output host? `ctx.build` is fixed by `run()` before
263
+ * the entry is called, so this is a PER-PARSE constant that `OP_NODE` was
264
+ * re-deriving on every node. Decided once in `begin`.
265
+ */
266
+ let HOSTCST = false
267
+ let HOSTREADSCHILDREN = true
268
+ let HOSTCAPTURETRIVIA: ((type: string) => boolean) | undefined
269
+ type DriverFrame = {
270
+ scan: FastTriviaScanner | null
271
+ fast: boolean
272
+ hostCst: boolean
273
+ hostReadsChildren: boolean
274
+ hostCaptureTrivia: ((type: string) => boolean) | undefined
275
+ end: number
276
+ }
277
+ // The ordinary path stays allocation-free. A frame exists only while a host
278
+ // callback re-enters this same reference driver before its outer parse ends.
279
+ const frames: DriverFrame[] = []
280
+ let depth = 0
281
+
282
+ /**
283
+ * Capture goes through the runtime's own buffer (`src/cst/capture-buffer.ts`),
284
+ * not a hand-rolled copy: it is already the shared mechanism the interpreter
285
+ * uses, and a second implementation of it is precisely the duplication G5 is
286
+ * about removing.
287
+ */
288
+ function pushLeaf(ctx: ParseContext, value: string, s: number, e: number): void {
289
+ const lf: Leaf = { _tag: 'leaf', value, span: { start: s, end: e } }
290
+ pushCstLeaf(ctx, lf)
291
+ }
292
+
293
+ /**
294
+ * Is there anything a failed branch could need to UNRECORD?
295
+ *
296
+ * `saveCstMark` allocates a 5-field object and `saveTriviaMark` allocates that
297
+ * plus a 7-field one; `rollbackTrivia` allocates a third to call through. In
298
+ * the driver those sat in the repetition and choice loops, so a parse
299
+ * allocated three-to-four objects PER ITEM that codegen never allocates (it
300
+ * emits scalar locals). That is a per-item cost, which is what the widening
301
+ * gap with input size was: +82% small, +275% large.
302
+ *
303
+ * When every sink either rollback touches is absent, nothing was recorded,
304
+ * so nothing needs unrecording and the mark need not exist. A grammar with no
305
+ * `node()` and no trivia log — json, csv — never allocates one.
306
+ */
307
+ function rollbackNeeded(ctx: ParseContext): boolean {
308
+ return ctx._cstBuf !== undefined
309
+ || ctx._cstLeaves !== undefined
310
+ || ctx._cstRawChildren !== undefined
311
+ || ctx._cstTriviaLog !== undefined
312
+ || ctx._fields !== undefined
313
+ || ctx._errors !== undefined
314
+ || ctx._triviaLog !== undefined
315
+ || ctx._rootTriviaLog !== undefined
316
+ }
317
+
318
+ /**
319
+ * The lead character at `pos`, as the first-set tables index it.
320
+ *
321
+ * `charCodeAt` is the hot read; only a high surrogate needs the code-point
322
+ * decode, so that branch is paid on astral input and nowhere else.
323
+ */
324
+ /**
325
+ * Skip trivia at `cur`, through the INSTALLED leaf when one is installed.
326
+ *
327
+ * The guard is the exact condition under which the generic functions take
328
+ * their own fast branch, restated over context fields so it costs loads rather
329
+ * than a call:
330
+ *
331
+ * - `advanceTrivia` (no deferred commit) runs `fast(input, cur)` whenever a
332
+ * scanner exists and `trackLines` is off — folded into `SCAN` already.
333
+ * - `scanTrivia` (deferred commit) runs it and returns a NO-OP commit when
334
+ * `_triviaLog` is unset and nothing is capturing trivia into a CST buffer.
335
+ * `_rootTriviaLog` is deliberately absent from this test: root rows are
336
+ * only ever written on the LABELLED scan path (see `OP_SCOPE`), and a
337
+ * labelled trivia never gets a `SCAN` installed.
338
+ *
339
+ * Anything outside that falls through to the shared implementations unchanged,
340
+ * so recording, labels and line tracking keep exactly one implementation.
341
+ */
342
+ function skipTrivia(input: string, cur: number, ctx: ParseContext): number {
343
+ const s = SCAN
344
+ if (s !== null
345
+ && ctx._triviaLog === undefined
346
+ && !(ctx.captureTrivia === true && (ctx._cstBuf !== undefined || ctx._cstTriviaLog !== undefined))) {
347
+ return s(input, cur)
348
+ }
349
+ // CAPTURE IS NOT A REASON TO LEAVE THE SCANNER. The recording an installed
350
+ // scanner's scope can owe is one `(cur, end)` row, so it is written here
351
+ // rather than through a deferred `{ end, commit }` pair per term. Appended
352
+ // rather than folded into the test above so the non-capturing path — every
353
+ // grammar that already had a scanner — keeps the exact branch it had.
354
+ if (s !== null) return skipTriviaScanned(s, input, cur, ctx)
355
+ if (needsDeferredTriviaCommit(ctx)) {
356
+ const scan = scanTrivia(input, cur, ctx)
357
+ scan.commit()
358
+ return scan.end
359
+ }
360
+ return advanceTrivia(input, cur, ctx)
361
+ }
362
+
363
+ function lead(input: string, pos: number): number {
364
+ if (pos >= input.length) return -1
365
+ const c = input.charCodeAt(pos)
366
+ if (c < 0xd800 || c > 0xdbff) return c
367
+ return input.codePointAt(pos) ?? c
368
+ }
369
+
370
+ function exec(ip: number, input: string, pos: number, ctx: ParseContext): unknown {
371
+ if (COUNT) { tableCounters.rows++; tableCounters.byOp[code[ip]!]!++ }
372
+ switch (code[ip]) {
373
+ case OP_LIT: {
374
+ const s = k[code[ip + 1]!] as string
375
+ if (input.startsWith(s, pos)) {
376
+ const e = pos + s.length
377
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
378
+ EC.e = e
379
+ return s
380
+ }
381
+ {
382
+ const xf = fx[code[ip + 2]!] as string[]
383
+ ctx._fe = pos; ctx._fx = xf
384
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
385
+ }
386
+ return FAIL
387
+ }
388
+ case OP_RX: {
389
+ const re = k[code[ip + 1]!] as RegExp
390
+ re.lastIndex = pos
391
+ const m = re.exec(input)
392
+ if (m !== null) {
393
+ const v = m[0]
394
+ const e = pos + v.length
395
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, v, pos, e)
396
+ EC.e = e
397
+ return v
398
+ }
399
+ {
400
+ const xf = fx[code[ip + 2]!] as string[]
401
+ ctx._fe = pos; ctx._fx = xf
402
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
403
+ }
404
+ return FAIL
405
+ }
406
+ case OP_LIT_TRACK: {
407
+ const s = k[code[ip + 1]!] as string
408
+ if (input.startsWith(s, pos)) {
409
+ const e = pos + s.length
410
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
411
+ trackLines(ctx, input, e)
412
+ EC.e = e
413
+ return s
414
+ }
415
+ {
416
+ const xf = fx[code[ip + 2]!] as string[]
417
+ ctx._fe = pos; ctx._fx = xf
418
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
419
+ }
420
+ return FAIL
421
+ }
422
+ case OP_RX_TRACK: {
423
+ const re = k[code[ip + 1]!] as RegExp
424
+ re.lastIndex = pos
425
+ const m = re.exec(input)
426
+ if (m !== null) {
427
+ const v = m[0]
428
+ const e = pos + v.length
429
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, v, pos, e)
430
+ trackLines(ctx, input, e)
431
+ EC.e = e
432
+ return v
433
+ }
434
+ {
435
+ const xf = fx[code[ip + 2]!] as string[]
436
+ ctx._fe = pos; ctx._fx = xf
437
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
438
+ }
439
+ return FAIL
440
+ }
441
+ case OP_LIT_CI:
442
+ case OP_LIT_CI_TRACK: {
443
+ const s = k[code[ip + 1]!] as string
444
+ const e = pos + s.length
445
+ // Yields the INPUT's casing (literal.ts:86), not the literal's — a node
446
+ // built from this carries the source text, and normalising it here would
447
+ // silently rewrite the user's CSS.
448
+ const matched = input.slice(pos, e)
449
+ if (asciiFoldEq(matched, s)) {
450
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, matched, pos, e)
451
+ if (code[ip] === OP_LIT_CI_TRACK) trackLines(ctx, input, e)
452
+ EC.e = e
453
+ return matched
454
+ }
455
+ {
456
+ const xf = fx[code[ip + 2]!] as string[]
457
+ ctx._fe = pos; ctx._fx = xf
458
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
459
+ }
460
+ return FAIL
461
+ }
462
+ case OP_EMPTY:
463
+ EC.e = pos
464
+ return ''
465
+
466
+ case OP_GUARD: {
467
+ if ((fns[code[ip + 1]!] as (s: unknown) => boolean)(ctx.state)) {
468
+ EC.e = pos
469
+ return null
470
+ }
471
+ ctx._fe = pos; ctx._fx = fx[code[ip + 2]!] as string[]
472
+ return FAIL
473
+ }
474
+
475
+ // Reached only where there is NO boundary to test — a bare choice arm, a
476
+ // `node()`'s whole body, a repeat item. The SEQ cases above intercept the
477
+ // row wherever it CAN be answered, so arriving here means the same misuse
478
+ // the interpreter refuses (adjacency.ts), and it refuses identically.
479
+ case OP_ADJ:
480
+ throw adjacencyMisuse(code[ip + 1] === 1 ? 'notAdjacent' : 'adjacent')
481
+
482
+ case OP_GATE: {
483
+ // Skipped under a probe / tolerant recovery — see `assemble.ts`.
484
+ if (ctx._probe === undefined && !ctx._tolerant && !classHas(cc[code[ip + 1]!]!, lead(input, pos))) {
485
+ ctx._fe = pos; ctx._fx = fx[code[ip + 3]!] as string[]
486
+ return FAIL
487
+ }
488
+ return exec(code[ip + 2]!, input, pos, ctx)
489
+ }
490
+
491
+ case OP_LABEL: {
492
+ const v = exec(code[ip + 1]!, input, pos, ctx)
493
+ // `_fe` untouched — `map.ts:84` keeps `result.span`.
494
+ if (v === FAIL) ctx._fx = fx[code[ip + 2]!] as string[]
495
+ return v
496
+ }
497
+ case OP_RULE:
498
+ return exec(code[ip + 1]!, input, pos, ctx)
499
+
500
+ /**
501
+ * A COUNTER ROW IS TRANSPARENT HERE, and that is the whole of its handling.
502
+ *
503
+ * This module is the VALUE-IDENTITY reference the assembler is bisected
504
+ * against, and a coverage counter is not a value: it changes nothing about
505
+ * what is parsed, what is built, where a failure lands or what it expected.
506
+ * Passing the row through keeps a coverage-encoded table runnable under the
507
+ * reference — which is what makes the identity sweep able to run one at all
508
+ * — and keeps this file exactly as behaviourally identical as it was.
509
+ *
510
+ * It does NOT record hits. Counting is the assembler's, on the product path;
511
+ * a second implementation of it here would be a second place for the phase
512
+ * semantics (entry for a rule, success for an arm) to drift.
513
+ */
514
+ case OP_COV:
515
+ return exec(code[ip + 1]!, input, pos, ctx)
516
+
517
+ case OP_LIVE: {
518
+ // A HAND-WRITTEN COMBINATOR, run through its own `.parse` — mirrors
519
+ // codegen's `emitRuntimeFallback`. Its result IS the interpreter's, so
520
+ // `expected`/`span` propagate verbatim and `committed` is copied onto the
521
+ // ctx bit both drivers use for the cut.
522
+ const c = fns[code[ip + 1]!] as Combinator<unknown>
523
+ const r = c.parse(input, pos, ctx)
524
+ if (!r.ok) {
525
+ ctx._fe = r.span.start
526
+ ctx._fx = r.expected
527
+ ctx._fc = r.committed === true
528
+ return FAIL
529
+ }
530
+ EC.e = r.span.end
531
+ return r.value
532
+ }
533
+
534
+ case OP_EXPECT: {
535
+ const v = exec(code[ip + 1]!, input, pos, ctx)
536
+ if (v !== FAIL) return v
537
+ // Mirrors src/combinators/expect.ts:135-150 — succeed at zero width with
538
+ // a ParseError value, and record it in the flat sink when present.
539
+ // The table-wide tracking bit selects the same shared span helper used
540
+ // for tracked node and root spans. The failed child has already scanned
541
+ // through `pos`, so its terminal rows have populated `_lineStarts` before
542
+ // this zero-width diagnostic is constructed.
543
+ const span = LINES ? spanLines(ctx, pos, pos) : { start: pos, end: pos }
544
+ const err = { _tag: 'parseError' as const, span, expected: fx[code[ip + 2]!] as string[] }
545
+ ctx._errors?.push(err)
546
+ // A TOLERANT `expect()` also embeds the error in the TREE (expect.ts:150,
547
+ // codegen.ts:4470), so a tree walk finds every diagnostic and it survives
548
+ // incremental subtree reuse — the flat sink alone is not the contract.
549
+ if (REC && ctx._tolerant === true) captureError(ctx, err)
550
+ // See assemble.ts OP_EXPECT: the recovered failure is a SUCCESS, so the
551
+ // child's commit bit must not survive to cut an enclosing choice.
552
+ ctx._fc = false
553
+ EC.e = pos
554
+ return err
555
+ }
556
+
557
+ case OP_ROUTED: {
558
+ // Mirrors src/combinators/dispatch.ts `routed()`.
559
+ const item = ctx._routed
560
+ if (item === undefined || pos !== item.span.start) {
561
+ const fb = code[ip + 1]!
562
+ if (fb >= 0) return exec(fb, input, pos, ctx)
563
+ ctx._fe = pos; ctx._fx = ROUTED_FX
564
+ return FAIL
565
+ }
566
+ if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value: item.value, span: item.span })
567
+ EC.e = item.span.end
568
+ return item.value
569
+ }
570
+
571
+ case OP_DISPATCH: {
572
+ // The selector runs ONCE; its value picks the arm. Resolution order is
573
+ // exact key -> ASCII-folded key -> matchers in declaration order ->
574
+ // otherwise, mirroring src/combinators/dispatch.ts:325.
575
+ const spec = dsp[code[ip + 2]!]!
576
+ const armBase = ip + 6
577
+ const selectorMark = saveTriviaMark(ctx)
578
+ const selVal = exec(code[ip + 1]!, input, pos, ctx)
579
+ if (selVal === FAIL) return FAIL
580
+ const selEnd = EC.e
581
+ const key = selVal as string
582
+
583
+ let arm = spec.byKey.get(key)
584
+ if (arm === undefined && spec.byFold.size > 0) arm = spec.byFold.get(asciiFoldKey(key))
585
+ if (arm === undefined) {
586
+ for (let i = 0; i < spec.match.length; i++) {
587
+ const m = spec.match[i]!
588
+ if (matcherClaims(m, key)) {
589
+ arm = m[3]
590
+ break
591
+ }
592
+ }
593
+ }
594
+
595
+ let target: number
596
+ let usesRouted: boolean
597
+ if (arm === undefined) {
598
+ const other = code[ip + 3]!
599
+ if (other < 0) {
600
+ // No branch and no fallback: fail AT THE SELECTOR'S END, not at pos.
601
+ ctx._fe = selEnd
602
+ ctx._fx = spec.expected as string[]
603
+ return FAIL
604
+ }
605
+ target = other
606
+ usesRouted = code[ip + 4]! === 1
607
+ } else {
608
+ target = code[armBase + arm]!
609
+ usesRouted = spec.routed[arm] === 1
610
+ }
611
+
612
+ let mark = saveTriviaMark(ctx)
613
+ let v: unknown
614
+ if (usesRouted) {
615
+ const savedRouted = ctx._routed
616
+ rollbackTrivia(ctx, selectorMark)
617
+ mark = saveTriviaMark(ctx)
618
+ ctx._routed = { value: key, span: { start: pos, end: selEnd } }
619
+ try {
620
+ v = exec(target, input, pos, ctx)
621
+ } finally {
622
+ ctx._routed = savedRouted
623
+ }
624
+ } else {
625
+ v = exec(target, input, selEnd, ctx)
626
+ }
627
+ if (v === FAIL) {
628
+ rollbackTrivia(ctx, mark)
629
+ // The interpreter marks a failed dispatch branch COMMITTED: the
630
+ // selector already matched, so an enclosing choice must not treat this
631
+ // as "try the next arm".
632
+ ctx._fc = true
633
+ return FAIL
634
+ }
635
+ // The end cell already holds the branch's end — dispatch's span runs from `pos`
636
+ // to there, which is what the caller reads.
637
+ return [key, v]
638
+ }
639
+
640
+ case OP_FIELD: {
641
+ const v = exec(code[ip + 2]!, input, pos, ctx)
642
+ if (v === FAIL) return FAIL
643
+ // Conditional on a live sink, exactly as src/combinators/map.ts has it:
644
+ // a `field()` outside any field-reading node costs nothing.
645
+ ctx._fields?.push({ name: k[code[ip + 1]!] as string, value: v, span: { start: pos, end: EC.e } })
646
+ return v
647
+ }
648
+
649
+ case OP_TOKEN: {
650
+ // Mirrors src/combinators/token.ts:21-65 exactly, including that
651
+ // `_triviaLog`/`_rootTriviaLog` are DELETED rather than set undefined
652
+ // and restored by presence, and that the leaf is pushed only when the
653
+ // caller was capturing BEFORE the sinks were cleared.
654
+ const sTrivia = ctx.trivia, sKinds = ctx.triviaKindLabels
655
+ const sBuf = ctx._cstBuf, sChildren = ctx._cstChildren, sLeaves = ctx._cstLeaves
656
+ const sRaw = ctx._cstRawChildren, sTl = ctx._cstTriviaLog
657
+ const sOuterTl = ctx._triviaLog, sRootTl = ctx._rootTriviaLog
658
+ const wasCapturing = cstCaptureActive(ctx)
659
+
660
+ const sScan = SCAN
661
+ SCAN = null
662
+ ctx.trivia = undefined
663
+ ctx.triviaKindLabels = undefined
664
+ ctx._cstBuf = undefined
665
+ ctx._cstChildren = undefined
666
+ ctx._cstLeaves = undefined
667
+ ctx._cstRawChildren = undefined
668
+ ctx._cstTriviaLog = undefined
669
+ ctx._triviaLog = undefined
670
+ ctx._rootTriviaLog = undefined
671
+
672
+ let v: unknown
673
+ try {
674
+ v = exec(code[ip + 1]!, input, pos, ctx)
675
+ } finally {
676
+ SCAN = sScan
677
+ ctx.trivia = sTrivia
678
+ ctx.triviaKindLabels = sKinds
679
+ ctx._cstBuf = sBuf
680
+ ctx._cstChildren = sChildren
681
+ ctx._cstLeaves = sLeaves
682
+ ctx._cstRawChildren = sRaw
683
+ ctx._cstTriviaLog = sTl
684
+ ctx._triviaLog = sOuterTl
685
+ ctx._rootTriviaLog = sRootTl
686
+ }
687
+ if (v === FAIL) return FAIL
688
+ const end = EC.e
689
+ const value = input.slice(pos, end)
690
+ if (wasCapturing) pushCstLeaf(ctx, { _tag: 'leaf', value, span: { start: pos, end } })
691
+ EC.e = end
692
+ return value
693
+ }
694
+
695
+ case OP_SCAN: {
696
+ const c = scans[code[ip + 1]!]!
697
+ const r = c.parse(input, pos, ctx)
698
+ if (!r.ok) {
699
+ ctx._fe = r.span.start
700
+ ctx._fx = (r.expected ?? EMPTY_FX) as string[]
701
+ return FAIL
702
+ }
703
+ EC.e = r.span.end
704
+ return r.value
705
+ }
706
+
707
+ case OP_WITHCTX: {
708
+ const saved = ctx.state
709
+ ctx.state = k[code[ip + 1]!]
710
+ try { return exec(code[ip + 2]!, input, pos, ctx) }
711
+ finally { ctx.state = saved }
712
+ }
713
+
714
+ case OP_SCOPE:
715
+ case OP_SCOPE_CAP:
716
+ case OP_SCOPE_PLAIN: {
717
+ const ki = code[ip + 1]!
718
+ const saved = ctx.trivia
719
+ const savedLabels = ctx.triviaKindLabels
720
+ const scopeTrivia = ki < 0 ? undefined : (trivia[ki] as ParseContext['trivia'])
721
+ const savedScan = SCAN
722
+ // THE SWAP. Chosen from table data at scope entry, never per term. A
723
+ // labelled trivia is excluded here rather than tested later: `scanTrivia`
724
+ // suppresses its own fast path when labels are present, so a swap there
725
+ // would drop the labelled records silently.
726
+ SCAN = FAST && ki >= 0 && !triviaLabelled[ki]! ? triviaScan[ki]! : null
727
+ ctx.trivia = scopeTrivia
728
+ // A scope installs its trivia's KIND LABELS too. Root-trivia rows are
729
+ // only ever written on the labelled scan path (trivia-skip.ts:212) — the
730
+ // unlabelled fast scanner returns before any root logging and does not
731
+ // even test `_rootTriviaLog` — so a scope that sets trivia without its
732
+ // labels captures NOTHING at the root, silently.
733
+ ctx.triviaKindLabels = scopeTrivia?._meta.triviaKindLabels
734
+ // SCOPE_CAP additionally turns capture ON for the child. It is an OR with
735
+ // whatever the enclosing context already asked for (`grammar.ts:129`), so
736
+ // the RESTORE puts the saved value back rather than writing `false` — an
737
+ // inner scope must not switch an outer capture off.
738
+ const savedCap = ctx.captureTrivia
739
+ if (code[ip] === OP_SCOPE_CAP) ctx.captureTrivia = true
740
+ // ROOT-CAPTURE POLICY (ip + 3). Bit 1 = `rootCapture: 'opaque'`, which is
741
+ // NOT inert: it suppresses selected root rows for the region
742
+ // (`grammar.ts:141`), and the flag is restored here because the table
743
+ // shares one ctx where `parser()` copies it. Bit 2 = the unclassified-scope
744
+ // refusal `grammar.ts:98` raises.
745
+ const policy = code[ip] === OP_SCOPE_PLAIN ? 0 : code[ip + 3]!
746
+ if ((policy & 2) !== 0) refuseUnclassifiedRootScope(ctx._rootTriviaStrictScopes)
747
+ const savedRootCap = ctx._rootTriviaCapture
748
+ if ((policy & 1) !== 0) ctx._rootTriviaCapture = false
749
+ const v = exec(code[ip + 2]!, input, pos, ctx)
750
+ if ((policy & 1) !== 0) ctx._rootTriviaCapture = savedRootCap
751
+ ctx.captureTrivia = savedCap
752
+ ctx.trivia = saved
753
+ ctx.triviaKindLabels = savedLabels
754
+ SCAN = savedScan
755
+ return v
756
+ }
757
+
758
+ case OP_SEQ:
759
+ case OP_SEQV:
760
+ case OP_SEQX: {
761
+ const fused = code[ip] === OP_SEQX
762
+ const base = fused ? ip + 3 : ip + 2
763
+ const n = code[fused ? ip + 2 : ip + 1]!
764
+ const values: unknown[] | undefined = code[ip] === OP_SEQV ? undefined : []
765
+ let cur = pos
766
+ // RECOVERY SYNC PUBLISH. A recovery table carries one follow-set class per
767
+ // term after the child slots; before each term the sentinel for it becomes
768
+ // `ctx._sync`, so a list nested in that term resyncs to this sequence's
769
+ // enclosing delimiter with nothing annotated (combinators/sequence.ts:75).
770
+ // Restored on every exit, as `sequence()`'s
771
+ // `finally` does. Strict tables never enter any of this.
772
+ const inheritedSync = REC ? ctx._sync : undefined
773
+ for (let i = 0; i < n; i++) {
774
+ if (REC) ctx._sync = sentinelFor(code[base + n + i]!) ?? inheritedSync
775
+ const child = code[base + i]!
776
+ // ADJACENCY IS TESTED AT `cur`, BEFORE THE BOUNDARY'S TRIVIA SCAN, and
777
+ // moves nothing — the next term re-scans the same gap and keeps its own
778
+ // commit/rewind decision, so spans, tree and trivia log are identical to
779
+ // the same sequence without the marker (combinators/sequence.ts:124).
780
+ // Running it through `exec` at the POST-scan position would find the gap
781
+ // already consumed and answer "adjacent" unconditionally.
782
+ if (code[child] === OP_ADJ) {
783
+ if (COUNT) { tableCounters.rows++; tableCounters.byOp[OP_ADJ]!++ }
784
+ const ki = code[child + 2]!
785
+ if (!adjacencyHolds(input, cur, ctx, code[child + 1] === 1, ki < 0 ? undefined : k[ki] as readonly string[])) {
786
+ ctx._fe = cur; ctx._fx = fx[code[child + 3]!] as string[]
787
+ { if (REC) ctx._sync = inheritedSync; return FAIL }
788
+ }
789
+ if (values !== undefined) values.push(null)
790
+ continue
791
+ }
792
+ if (i > 0 && ctx.trivia !== undefined) {
793
+ // SCALAR MARKS — `saveTriviaMark` allocated TWICE per term (its own
794
+ // seven-field object plus the five-field CST mark it delegates to).
795
+ const need = rollbackNeeded(ctx)
796
+ const mTl = need ? cstTlLen(ctx) : 0
797
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
798
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
799
+ const scanEnd = skipTrivia(input, cur, ctx)
800
+ const scanTl = need ? cstTlLen(ctx) : 0
801
+ const scanLog = need ? ctx._triviaLog?.length ?? 0 : 0
802
+ const scanRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
803
+ const v = exec(child, input, scanEnd, ctx)
804
+ if (v === FAIL) { if (REC) ctx._sync = inheritedSync; return FAIL }
805
+ if (EC.e > scanEnd) cur = EC.e
806
+ else if (need) rollbackScannedTriviaAt(
807
+ ctx, mTl, scanTl, mLog, scanLog, mRoot, scanRoot,
808
+ )
809
+ if (values !== undefined) values.push(v)
810
+ continue
811
+ }
812
+ // TERMINAL FAST PATH. Terminals are the majority of executed
813
+ // instructions, and reaching one through `exec` costs a JS call frame
814
+ // plus a switch dispatch that the emitted code does not pay. Running
815
+ // LIT/RX in place removes both. The duplication is in the DRIVER,
816
+ // which ships once for every grammar — the cost this design trades on.
817
+ const cop = code[child]
818
+ // The two inline terminals below execute a ROW without going through
819
+ // `exec`, so the counter has to see them or the row count understates
820
+ // exactly where the driver already won.
821
+ if (COUNT && (cop === OP_LIT || cop === OP_RX)) { tableCounters.rows++; tableCounters.byOp[cop]!++ }
822
+ if (cop === OP_LIT) {
823
+ const lit = k[code[child + 1]!] as string
824
+ if (!input.startsWith(lit, cur)) {
825
+ ctx._fe = cur; ctx._fx = fx[code[child + 2]!] as string[]
826
+ { if (REC) ctx._sync = inheritedSync; return FAIL }
827
+ }
828
+ const e = cur + lit.length
829
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, lit, cur, e)
830
+ if (values !== undefined) values.push(lit)
831
+ cur = e
832
+ continue
833
+ }
834
+ if (cop === OP_RX) {
835
+ const re = k[code[child + 1]!] as RegExp
836
+ re.lastIndex = cur
837
+ const m = re.exec(input)
838
+ if (m === null) {
839
+ ctx._fe = cur; ctx._fx = fx[code[child + 2]!] as string[]
840
+ { if (REC) ctx._sync = inheritedSync; return FAIL }
841
+ }
842
+ const mv = m[0]
843
+ const e = cur + mv.length
844
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, mv, cur, e)
845
+ if (values !== undefined) values.push(mv)
846
+ cur = e
847
+ continue
848
+ }
849
+ const v = exec(child, input, cur, ctx)
850
+ if (v === FAIL) { if (REC) ctx._sync = inheritedSync; return FAIL }
851
+ if (values !== undefined) values.push(v)
852
+ cur = EC.e
853
+ }
854
+ if (REC) ctx._sync = inheritedSync
855
+ EC.e = cur
856
+ if (fused) {
857
+ const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
858
+ if (COUNT) siteFn('SEQX fn()', fn)
859
+ return fn(values, { start: pos, end: cur })
860
+ }
861
+ return values
862
+ }
863
+
864
+ case OP_CHOICE: {
865
+ const d = code[ip + 1]!
866
+ const base = ip + 4
867
+ const choiceFx = fx[code[ip + 3]!] as string[]
868
+ // Report the union, at the choice's own position, on every exit that
869
+ // fails — matching both engines and, more importantly, never handing a
870
+ // user an empty expected set.
871
+ const failChoice = (): typeof FAIL => {
872
+ ctx._fe = pos
873
+ ctx._fx = choiceFx
874
+ return FAIL
875
+ }
876
+ const table = disp[d]!
877
+ const c = lead(input, pos)
878
+ if (table.exclusive) {
879
+ let arm = -1
880
+ if (c >= 0 && c < 128) {
881
+ const a = table.ascii[c]!
882
+ if (a !== 0) arm = a - 1
883
+ } else if (c >= 128) {
884
+ const hi = table.hi
885
+ for (let i = 0; i < hi.length; i += 3) {
886
+ if (c >= hi[i]! && c <= hi[i + 1]!) { arm = hi[i + 2]!; break }
887
+ }
888
+ }
889
+ if (arm >= 0) {
890
+ ctx._fc = false
891
+ if (COUNT) tableCounters.gatedEntries++
892
+ const v = exec(code[base + arm]!, input, pos, ctx)
893
+ if (v !== FAIL) return v
894
+ // THE CUT. `dispatch()` is the library's one true cut: once its
895
+ // selector matched, a failing branch must fail the whole choice
896
+ // rather than let a later arm re-recognise the same text. The flag
897
+ // was being SET by OP_DISPATCH and read by nobody, so the table
898
+ // accepted input both shipped engines reject.
899
+ if (committed(ctx)) return FAIL
900
+ // THE DISPATCHED ARM'S OWN SET IS THE ANSWER, at the choice's own
901
+ // position. This is the rule BOTH shipped engines apply — see the
902
+ // block comment below `failChoice` — and `_fx` already holds it, so
903
+ // the whole of "report the arm" is declining to overwrite it.
904
+ const armFx = ctx._fx
905
+ if (armFx !== undefined && armFx.length > 0) { ctx._fe = pos; return FAIL }
906
+ }
907
+ // THERE ARE NO OPEN ARMS TO FALL BACK TO. An open arm is one whose class
908
+ // is −1, and `resolveDispatch` clears `exclusive` for exactly those arms
909
+ // (program.ts), so `table.open` is empty whenever `exclusive` holds — the
910
+ // fallback loop this branch used to carry was unreachable code.
911
+ //
912
+ // THE UNION IS REPORTED ONLY WHEN NO ARM WAS SELECTED — a dispatch miss,
913
+ // where the interpreter runs `parsers.flatMap(p => p.parse(...))`
914
+ // (choice.ts:110-118) and, every arm being non-nullable and excluded by
915
+ // the char at `pos`, gets back exactly each arm's own opener. The static
916
+ // union IS that flatMap's answer, without running seven parsers to
917
+ // rediscover it.
918
+ //
919
+ // NOT furthest-failure merging, which no engine on the `expected` path
920
+ // performs. `failAt` (combinators/probe.ts:13-24) and its compiled mirror
921
+ // `probeUpdate` (codegen.ts:692) are the only positional merges in the
922
+ // library, they are gated on `_ctx._probe` and they surface as
923
+ // `RunResult.furthestFail` under `{recover:true}` — a different field.
924
+ // Every `_fe`/`_fx` site in all three drivers is last-write-wins.
925
+ //
926
+ // A residual JSON divergence survives this and is NOT a failure-reporting
927
+ // bug: on `{"a":]` this driver reports `["}"]` (the Obj arm's own
928
+ // propagated set) and both engines report seven tokens, because
929
+ // `choice()` froze `disjoint = false` for `Value` at CONSTRUCTION, when
930
+ // its `g.X` arms were unresolved refs (choice.ts:35 — the same staleness
931
+ // `encode.ts` recomputes around), so they firstMatch all seven arms where
932
+ // this driver dispatches to one. The arm sets differ, not the rule.
933
+ return failChoice()
934
+ }
935
+ // THE PER-ARM GATE. Arms in source order, each skipped when its own
936
+ // class excludes the char at `pos`. Order is untouched, so this is not a
937
+ // reordering: it only declines to enter arms that provably cannot match.
938
+ // A `null` class means nullable or unmappable, and those are always
939
+ // entered.
940
+ const n = code[ip + 2]!
941
+ const armCls = table.armCls
942
+ const need = rollbackNeeded(ctx)
943
+ const mRaw = need ? cstRawLen(ctx) : 0
944
+ const mTl = need ? cstTlLen(ctx) : 0
945
+ const mLv = need ? cstLeavesLen(ctx) : 0
946
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
947
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
948
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
949
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
950
+ // THE ORDERED PATH REPORTS THE ARMS THAT ACTUALLY RAN, accumulated in
951
+ // order — `choice.ts:167`'s `expected.push(...result.expected)` per failed
952
+ // arm, reported at the choice's own position. The static `choiceFx` union
953
+ // is a set of OPENERS, so a left-factored or shared-prefix choice whose
954
+ // arms all failed PAST their common opener reported that opener instead of
955
+ // the tokens the arms were actually waiting for: `['/::?/']` where the
956
+ // interpreter says `['"before"', '"hover"']`.
957
+ //
958
+ // `choiceFx` is still the answer when nothing ran or every arm reported
959
+ // nothing — the char gate can skip arms the interpreter would have entered,
960
+ // and the union is what those arms' start-failures would have contributed.
961
+ let acc: string[] | undefined
962
+ const push = (ax: readonly string[] | undefined): void => {
963
+ if (ax === undefined || ax.length === 0) return
964
+ if (acc === undefined) acc = ax.slice()
965
+ else for (const s of ax) acc.push(s)
966
+ }
967
+ const armFx = base + n
968
+ for (let i = 0; i < n; i++) {
969
+ const cls = armCls[i]
970
+ if (cls !== undefined && cls !== null && !classHas(cls, c)) {
971
+ if (COUNT) tableCounters.armGateSkips++
972
+ // THE GATED ARM STILL REPORTS. The interpreter has no char gate: it
973
+ // enters the arm, the arm's opener fails, and its static set is what
974
+ // lands in the accumulation. Declining to enter must not also decline
975
+ // to say what the arm wanted.
976
+ push(fx[code[armFx + i]!] as string[])
977
+ continue
978
+ }
979
+ ctx._fc = false
980
+ if (COUNT) tableCounters.ungatedEntries++
981
+ const rows0 = COUNT ? tableCounters.rows : 0
982
+ const v = exec(code[base + i]!, input, pos, ctx)
983
+ if (v !== FAIL) return v
984
+ if (COUNT) { tableCounters.ungatedFails++; tableCounters.ungatedFailRows += tableCounters.rows - rows0 }
985
+ push(ctx._fx)
986
+ // A committed arm cuts, and the interpreter reports the accumulation at
987
+ // the arm's OWN span (`choice.ts:170`), not at the choice's position.
988
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
989
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
990
+ }
991
+ if (acc !== undefined) { ctx._fe = pos; ctx._fx = acc; return FAIL }
992
+ return failChoice()
993
+ }
994
+
995
+ case OP_GREEDY: {
996
+ // The mark is taken BEFORE the super arm runs: the classified path
997
+ // unwinds the regex's leaf and lets the credited literal arm push its
998
+ // own, so exactly one leaf survives — same text, same span.
999
+ const need = rollbackNeeded(ctx)
1000
+ const mRaw = need ? cstRawLen(ctx) : 0
1001
+ const mTl = need ? cstTlLen(ctx) : 0
1002
+ const mLv = need ? cstLeavesLen(ctx) : 0
1003
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
1004
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
1005
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
1006
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1007
+ const sup = exec(code[ip + 1]!, input, pos, ctx)
1008
+ // choice.ts:126 — the super arm's failure is returned VERBATIM, so `_fe`
1009
+ // and `_fx` are left exactly as it set them.
1010
+ if (sup === FAIL) return FAIL
1011
+ const end = EC.e
1012
+ const word = input.slice(pos, end)
1013
+ const n = code[ip + 2]!
1014
+ for (let i = 0; i < n; i++) {
1015
+ if (k[code[ip + 3 + 2 * i]!] !== word) continue
1016
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1017
+ // Cannot fail: `word` IS this arm's case-sensitive literal at `pos`.
1018
+ return exec(code[ip + 4 + 2 * i]!, input, pos, ctx)
1019
+ }
1020
+ EC.e = end
1021
+ return sup
1022
+ }
1023
+
1024
+ case OP_ARMGATE: {
1025
+ if (!(fns[code[ip + 1]!] as (s: unknown) => boolean)(ctx.state)) {
1026
+ // choice.ts:150 is a `continue`, not a failure — the arm is treated as
1027
+ // never entered, so a cut it might have raised must not cut the choice.
1028
+ ctx._fc = false
1029
+ ctx._fe = pos
1030
+ ctx._fx = fx[code[ip + 3]!] as string[]
1031
+ return FAIL
1032
+ }
1033
+ return exec(code[ip + 2]!, input, pos, ctx)
1034
+ }
1035
+
1036
+ case OP_REJECT: {
1037
+ const v = exec(code[ip + 1]!, input, pos, ctx)
1038
+ if (v === FAIL) return FAIL
1039
+ const end = EC.e
1040
+ const n = code[ip + 2]!
1041
+ for (let i = 0; i < n; i++) {
1042
+ const o = code[ip + 4 + 2 * i]!
1043
+ const fires = code[ip + 3 + 2 * i]! === 0
1044
+ ? input.startsWith(k[o] as string, end)
1045
+ : classHas(cc[o]!, lead(input, end))
1046
+ if (!fires) continue
1047
+ // choice.ts:161-164 is a `continue`, not a failure: the arm is treated
1048
+ // as never entered, so a cut it raised must not survive to cut the
1049
+ // choice, and it contributes NO expectation. The choice does the
1050
+ // capture-sink rollback, as for any arm.
1051
+ ctx._fc = false
1052
+ ctx._fx = undefined
1053
+ return FAIL
1054
+ }
1055
+ EC.e = end
1056
+ return v
1057
+ }
1058
+
1059
+ case OP_OPT: {
1060
+ const need = rollbackNeeded(ctx)
1061
+ const mRaw = need ? cstRawLen(ctx) : 0
1062
+ const mTl = need ? cstTlLen(ctx) : 0
1063
+ const mLv = need ? cstLeavesLen(ctx) : 0
1064
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
1065
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
1066
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
1067
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1068
+ ctx._fc = false
1069
+ const v = exec(code[ip + 1]!, input, pos, ctx)
1070
+ if (v === FAIL) {
1071
+ // repeat.ts:277 — `optional()` propagates a committed failure rather
1072
+ // than reporting "absent".
1073
+ if (committed(ctx)) return FAIL
1074
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1075
+ EC.e = pos
1076
+ // NULL, not undefined. `optional()` yields `null` on no-match
1077
+ // (src/combinators/repeat.ts:269,277) and grammars TEST for it:
1078
+ // examples/lang's `call` reducer is `if (args === null) return callee`,
1079
+ // so `undefined` there turned a bare identifier into a call node with
1080
+ // `args: undefined`. The parse succeeded and only the tree moved.
1081
+ return null
1082
+ }
1083
+ return v
1084
+ }
1085
+
1086
+ case OP_REP:
1087
+ case OP_REPV: {
1088
+ const child = code[ip + 1]!
1089
+ const min = code[ip + 2]!
1090
+ const max = code[ip + 3]!
1091
+ const sep = code[ip + 4]!
1092
+ // bit 1 of the flags word: the author opted into keeping separators in
1093
+ // `children`. Absent, a list contributes its ITEMS and nothing else.
1094
+ const keepSeparators = (code[ip + 5]! & 2) !== 0
1095
+ // BIT 0 WAS WRITTEN AND NEVER READ. `sepBy({ trailing: 'allow' })` keeps
1096
+ // a separator that is not followed by an item, so `a,b,` consumes to 4;
1097
+ // ignoring the bit stopped at 3 while both shipped engines went to 4.
1098
+ const trailingAllowed = (code[ip + 5]! & 1) !== 0
1099
+ const out: unknown[] | undefined = code[ip] === OP_REP ? [] : undefined
1100
+ const hasTrivia = ctx.trivia !== undefined
1101
+ const needMark = rollbackNeeded(ctx)
1102
+ // WHO OWNS THE TRIVIA IN FRONT OF THE FIRST ITEM. Only `many()` — the
1103
+ // min-0, separator-less repeat — runs its first item through `repItem`
1104
+ // and therefore skips leading trivia (src/combinators/repeat.ts:130-137).
1105
+ // `oneOrMore`/`atLeast` (:203) and `sepBy` (:412) both parse the first
1106
+ // item AT `pos`, because leading trivia there is the ENCLOSING context's
1107
+ // responsibility. `many` is the only OP_REP with no separator and min 0,
1108
+ // so the shape identifies itself and no extra flag is needed.
1109
+ const skipBeforeFirst = sep < 0 && min === 0
1110
+ // TOLERANT RECOVERY, dormant unless `ctx._tolerant`. `mySync` is captured
1111
+ // at ENTRY because an element's own sequence publishes over `_sync` while
1112
+ // it runs — the interpreter restores in a `finally` (sequence.ts:105) and
1113
+ // codegen saves at entry (codegen.ts:3032); either way this is the list's
1114
+ // own sync. The item's expected set is at ip + 6 and the separator's
1115
+ // sentinel class at ip + 7, both laid down only for a recovery table.
1116
+ const mySync = REC ? ctx._sync : undefined
1117
+ const recFx = REC ? fx[code[ip + 6]!] as string[] : undefined
1118
+ const sepSent = REC ? sentinelFor(code[ip + 7]!) : undefined
1119
+ let cur = pos
1120
+ let count = 0
1121
+ for (;;) {
1122
+ if (max >= 0 && count >= max) break
1123
+ // A SEPARATED list is bounded by its SEPARATOR, so it stops at EOF at the
1124
+ // LOOP HEAD — `while (cur < input.length)` at repeat.ts's sepBy loop. The
1125
+ // `repItem` early-out below sits after the separator, a different
1126
+ // position, and standing in for this one dropped the item following a
1127
+ // final separator. Held to `count >= min`: a list still short of `min`
1128
+ // must attempt the separator so its failure sets the expected set, which
1129
+ // is the only thing an under-`min` list has to report.
1130
+ if (sep >= 0 && count > 0 && count >= min && cur >= input.length) break
1131
+ // One mark pair for the whole loop when a rollback is even possible,
1132
+ // refreshed per iteration rather than reallocated.
1133
+ // SCALAR MARKS. This loop took TWO allocations per item (a CST mark and
1134
+ // a trivia mark, the latter allocating a second one internally) — the
1135
+ // per-item allocation an earlier lane hunted on json and could not
1136
+ // replicate, because json builds almost no nodes and so never sets
1137
+ // `_cstBuf`, which is what makes `needMark` true for a whole parse.
1138
+ const mRaw = needMark ? cstRawLen(ctx) : 0
1139
+ const mTl = needMark ? cstTlLen(ctx) : 0
1140
+ const mLv = needMark ? cstLeavesLen(ctx) : 0
1141
+ const mFl = needMark ? ctx._fields?.length ?? 0 : 0
1142
+ const mEr = needMark ? ctx._errors?.length ?? 0 : 0
1143
+ const mLog = needMark ? ctx._triviaLog?.length ?? 0 : 0
1144
+ const mRoot = needMark ? ctx._rootTriviaLog?.length ?? 0 : 0
1145
+ let itemStart = cur
1146
+ let sepEnd = -1
1147
+ // WHICH ITEMS `repItem` ACTUALLY PARSES — the scope of every rule below
1148
+ // that was copied from it. `many` runs ALL its items through `repItem`;
1149
+ // `oneOrMore`/`atLeast` parse the mandatory first at `pos` themselves
1150
+ // (repeat.ts:203) and `sepBy` parses BOTH its first item (:412) and
1151
+ // every post-separator item (:481) itself, so a separated list never
1152
+ // reaches `repItem` at all. The trivia branch below already encodes this
1153
+ // (its `else if` is unreachable for `sep >= 0`); the two guards after it
1154
+ // did not, and applied `repItem`'s rules to items that never run it.
1155
+ //
1156
+ // `count >= min` extends that to ALL of `atLeast`'s mandatory items, not
1157
+ // just its first: items 2..min go through `repItem` (repeat.ts:213) but as
1158
+ // MANDATORY, which holds off both of its loop-termination stops. Without
1159
+ // it a `{ min: 2 }` repeat over a NULLABLE item stopped on the required
1160
+ // second item and then failed `count < min` — never satisfiable at all,
1161
+ // where the compiled engine yields the n-item derivation. `min === 0`
1162
+ // makes the clause vacuous, so `many()` is untouched.
1163
+ const viaRepItem = sep < 0 && count >= min && (count > 0 || skipBeforeFirst)
1164
+ if (sep >= 0 && count > 0) {
1165
+ // separator, with trivia on BOTH sides — mirrors repeat.ts's sepBy loop
1166
+ const leavesBefore = cstLeavesLen(ctx)
1167
+ let sp = cur
1168
+ if (hasTrivia) sp = skipTrivia(input, sp, ctx)
1169
+ ctx._fc = false
1170
+ const sv = exec(sep, input, sp, ctx)
1171
+ if (sv === FAIL) {
1172
+ if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1173
+ if (committed(ctx)) return FAIL
1174
+ break
1175
+ }
1176
+ // Demote the separator out of `children`, exactly where the
1177
+ // interpreter does it (src/combinators/repeat.ts, sepBy loop).
1178
+ if (!keepSeparators) demoteCapturedToRaw(ctx, leavesBefore)
1179
+ sepEnd = EC.e
1180
+ itemStart = hasTrivia ? skipTrivia(input, EC.e, ctx) : EC.e
1181
+ } else if (hasTrivia && (count > 0 || skipBeforeFirst)) {
1182
+ // Trivia precedes every item a `repItem` loop parses, the first of a
1183
+ // `many()` included — skipping it only for later items dropped
1184
+ // exactly one trivia-log entry per repetition, invisible in the
1185
+ // parse and visible in a node's `triviaLog`. It does NOT precede the
1186
+ // mandatory first item of `oneOrMore`/`sepBy`; see `skipBeforeFirst`.
1187
+ itemStart = skipTrivia(input, itemStart, ctx)
1188
+ }
1189
+ // Nothing but trivia left: don't speculatively parse an item at EOF.
1190
+ // This is `repItem`'s early-out, so it applies only where `repItem`
1191
+ // runs — never to a mandatory first item, which both other engines
1192
+ // attempt at `pos` whatever is there.
1193
+ if (itemStart >= input.length && viaRepItem) {
1194
+ if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1195
+ // A trailing separator at EOF is the COMMON case for
1196
+ // `trailing: 'allow'` (`a,b,`), and this early-out ran before the
1197
+ // item was ever attempted — so handling it only on the item-failure
1198
+ // path left the separator unconsumed.
1199
+ if (trailingAllowed && sepEnd >= 0) cur = sepEnd
1200
+ break
1201
+ }
1202
+ ctx._fc = false
1203
+ const v = exec(child, input, itemStart, ctx)
1204
+ if (v === FAIL) {
1205
+ // repeat.ts:141/215/233 — a committed item failure fails the WHOLE
1206
+ // repetition. Breaking here is what let `many(dispatch(...))` return
1207
+ // ok:true with a silently truncated document.
1208
+ if (committed(ctx)) {
1209
+ if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1210
+ return FAIL
1211
+ }
1212
+ // RESYNC. A mandatory item of a separator-less repeat does NOT recover
1213
+ // (`oneOrMore`'s first item propagates, repeat.ts:221); a `sepBy`'s
1214
+ // first element does. Sitting ON the sync token is a clean list end,
1215
+ // tested at `itemStart` so leading trivia is never swallowed into the
1216
+ // span. A separated list scans to its own separator OR the enclosing
1217
+ // delimiter, and does NOT roll back: the separator it consumed and the
1218
+ // error after it both belong to the list (repeat.ts:533).
1219
+ if (REC && ctx._tolerant === true
1220
+ && mySync !== undefined
1221
+ && (sep >= 0 || count >= min)
1222
+ && !matchesAt(mySync, input, itemStart, ctx)) {
1223
+ if (sep < 0 && needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1224
+ const rr = recoverScan(
1225
+ input, itemStart, ctx,
1226
+ sep < 0 ? mySync : orSentinel(sepSent ?? mySync, sepSent === undefined ? undefined : mySync),
1227
+ recFx!,
1228
+ )
1229
+ if (out !== undefined) out.push(rr.error)
1230
+ captureError(ctx, rr.error)
1231
+ count++
1232
+ cur = rr.end
1233
+ continue
1234
+ }
1235
+ if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1236
+ // `trailing: 'allow'` keeps a separator that no item followed.
1237
+ if (trailingAllowed && sepEnd >= 0) cur = sepEnd
1238
+ break
1239
+ }
1240
+ if (EC.e === itemStart && viaRepItem) {
1241
+ // Zero-width item: `repItem`'s stop, and a TERMINATION device, not a
1242
+ // semantic filter — a `many` loop whose only source of progress is the
1243
+ // item itself spins forever without it. That pressure does not exist
1244
+ // for a mandatory item (parsed once) or for a separated list (the
1245
+ // SEPARATOR advances the loop), and both shipped engines accordingly
1246
+ // take a zero-width item there: `sepBy(nullable, ',')` over `",a"` is
1247
+ // `["", "a"]`. Applying the stop to them made the table return `[]`
1248
+ // having consumed NOTHING, silently dropping real input.
1249
+ if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1250
+ break
1251
+ }
1252
+ if (out !== undefined) out.push(v)
1253
+ cur = EC.e
1254
+ count++
1255
+ }
1256
+ if (count < min) {
1257
+ // A list that ends under `min` is stuck at `cur` wanting another ITEM,
1258
+ // and reports what would have let it CONTINUE there — `failAt` in
1259
+ // repeat.ts and `deriveExpectedArr([def.parser])` in codegen's
1260
+ // emitSepBy both say the ITEM. Left alone, `_fx` still holds whatever
1261
+ // sub-parse failed last, which for a separated list is the SEPARATOR.
1262
+ // The index is present only when the encoder could not rule that out
1263
+ // (flags bit 2, `min >= 2`); at `min === 1` being under `min` means the
1264
+ // FIRST item failed and set the item's own set already.
1265
+ if ((code[ip + 5]! & 4) !== 0) { ctx._fe = cur; ctx._fx = fx[code[ip + 6]!] as string[] }
1266
+ return FAIL
1267
+ }
1268
+ EC.e = cur
1269
+ return out
1270
+ }
1271
+
1272
+ case OP_XFORM: {
1273
+ const v = exec(code[ip + 2]!, input, pos, ctx)
1274
+ if (v === FAIL) return FAIL
1275
+ const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
1276
+ if (COUNT) siteFn('XFORM fn()', fn)
1277
+ return fn(v, { start: pos, end: EC.e })
1278
+ }
1279
+
1280
+ case OP_LEAF: {
1281
+ // Mirrors src/combinators/token.ts:89-127. `leaf()` is a CAPTURE
1282
+ // BOUNDARY: it suppresses the interior's own CST captures and exposes
1283
+ // exactly ONE leaf carrying the reducer's value. Running the interior
1284
+ // with the parent's sinks still live leaked every interior terminal
1285
+ // into the parent's `children`, which moved arity in the enclosing
1286
+ // reducer with no error of its own.
1287
+ //
1288
+ // It differs from `token()` deliberately: trivia POLICY is untouched
1289
+ // (`ctx.trivia`, `ctx.triviaKindLabels`), and `_rootTriviaLog` stays
1290
+ // live so root-source trivia inside the leaf remains visible.
1291
+ const sBuf = ctx._cstBuf, sChildren = ctx._cstChildren, sLeaves = ctx._cstLeaves
1292
+ const sRaw = ctx._cstRawChildren, sTl = ctx._cstTriviaLog
1293
+ const sOuterTl = ctx._triviaLog
1294
+ const wasCapturing = cstCaptureActive(ctx)
1295
+
1296
+ ctx._cstBuf = undefined
1297
+ ctx._cstChildren = undefined
1298
+ ctx._cstLeaves = undefined
1299
+ ctx._cstRawChildren = undefined
1300
+ ctx._cstTriviaLog = undefined
1301
+ ctx._triviaLog = undefined
1302
+
1303
+ let v: unknown
1304
+ try {
1305
+ v = exec(code[ip + 2]!, input, pos, ctx)
1306
+ } finally {
1307
+ ctx._cstBuf = sBuf
1308
+ ctx._cstChildren = sChildren
1309
+ ctx._cstLeaves = sLeaves
1310
+ ctx._cstRawChildren = sRaw
1311
+ ctx._cstTriviaLog = sTl
1312
+ ctx._triviaLog = sOuterTl
1313
+ }
1314
+ if (v === FAIL) return FAIL
1315
+ const end = EC.e
1316
+ const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
1317
+ if (COUNT) siteFn('LEAF fn()', fn)
1318
+ const out = fn(v, { start: pos, end })
1319
+ if (wasCapturing) pushCstLeaf(ctx, { _tag: 'leaf', value: out, span: { start: pos, end } })
1320
+ EC.e = end
1321
+ return out
1322
+ }
1323
+
1324
+ case OP_NODE:
1325
+ case OP_NODE_TRACK: {
1326
+ const flags = code[ip + 3]!
1327
+ // `cstOutput` is a RUNTIME fact, not an encode-time one: it depends on
1328
+ // the host handed to this parse. Under a CST host the direct builder is
1329
+ // BYPASSED, so capture must widen regardless of what the reducer's arity
1330
+ // said — the encode-time flags under-approximate here by construction.
1331
+ const host = ctx.build
1332
+ const hostCst = HOSTCST
1333
+ const type = k[code[ip + 5]!] as string
1334
+ const structural = code[ip + 1]! < 0 && code[ip + 4]! < 0
1335
+ const grammarCapture = (flags & 1) !== 0 || (flags & 128) !== 0
1336
+ const hostCapturesThisType = structural && HOSTCAPTURETRIVIA !== undefined
1337
+ ? HOSTCAPTURETRIVIA(type)
1338
+ : undefined
1339
+ const captureWide = (flags & 4) !== 0 || hostCst
1340
+ ? !structural || grammarCapture || hostCapturesThisType !== false
1341
+ : hostCapturesThisType === true
1342
+ const keepChildren = !structural || HOSTREADSCHILDREN
1343
+ || (flags & 32) !== 0 || (flags & 64) !== 0
1344
+ const rawOnly = !keepChildren
1345
+ const saved = beginCstNodeCapture(ctx)
1346
+ if (rawOnly) {
1347
+ ctx._cstBuf = { rawOnly: true }
1348
+ }
1349
+ const savedFields = ctx._fields
1350
+ ctx._fields = (flags & 16) !== 0 || hostCst ? [] : undefined
1351
+ ctx.captureTrivia = captureWide
1352
+ // Per-node-type trivia-kind mask — see `assemble.ts`. Structural nodes
1353
+ // only (no builder, no projection), matching `node.ts:260`.
1354
+ const savedMask = structural ? ctx._triviaCaptureMask : undefined
1355
+ if (structural && host?._parsemanTriviaKinds !== undefined) {
1356
+ ctx._triviaCaptureMask = host._parsemanTriviaKinds(type)
1357
+ }
1358
+ const v = exec(code[ip + 2]!, input, pos, ctx)
1359
+ if (v !== FAIL && (flags & 128) !== 0 && ctx.trivia !== undefined) EC.e = consumeTrivia(input, EC.e, ctx)
1360
+ const fieldMap: FieldMap | undefined = (flags & 16) !== 0 || hostCst ? buildFieldMap(ctx._fields) : undefined
1361
+ ctx._fields = savedFields
1362
+ if (structural) ctx._triviaCaptureMask = savedMask
1363
+ const cap = endCstNodeCapture(ctx, saved)
1364
+ if (v === FAIL) return FAIL
1365
+ const end = EC.e
1366
+ const span = code[ip] === OP_NODE_TRACK ? spanLines(ctx, pos, end) : { start: pos, end }
1367
+ const st = (flags & 8) !== 0 && ctx.state !== undefined
1368
+ ? Object.assign({}, ctx.state as Record<string, unknown>)
1369
+ : undefined
1370
+ const tagIdx = code[ip + 6]!
1371
+ const tags = tagIdx < 0 ? undefined : k[tagIdx] as readonly string[]
1372
+
1373
+ const kids = cap.children
1374
+ const proj = code[ip + 4]!
1375
+ const buildIdx = code[ip + 1]!
1376
+ // A direct builder that never declared `state` still owes the HOST its
1377
+ // snapshot — node.ts builds it here, on a branch the eval path never takes.
1378
+ //
1379
+ // It is built INLINE AT THE HOST CALL, not before the branch and not
1380
+ // through a helper closure — either would be an allocation per node.
1381
+ // Computed eagerly it was an `Object.assign` clone per node, on every AST
1382
+ // parse of a grammar that uses `ctx.state` at all, thrown away unread: the
1383
+ // two host branches below are the only readers and neither runs on the AST
1384
+ // path with a direct builder.
1385
+ let nd: unknown
1386
+ if ((flags & 64) !== 0 && kids.length === 1) {
1387
+ nd = unwrapChild(kids[0])
1388
+ } else if ((flags & 32) !== 0 && kids.length === 1) {
1389
+ nd = kids[0]
1390
+ } else if (
1391
+ // HOST COLLAPSE. Applies wherever the node's VALUE comes from the host,
1392
+ // which is any node under a CST host — NOT only builder-less ones.
1393
+ // Gating on `!build` alone made `cstBuildHost({ collapse })` a silent
1394
+ // no-op for every grammar whose rules carry reducers (node.ts says so
1395
+ // in as many words). jess turns this on for `NamedColor`.
1396
+ (hostCst || (buildIdx < 0 && proj < 0))
1397
+ && keepChildren
1398
+ && host?._parsemanCstCollapse !== undefined
1399
+ && kids.length === 1
1400
+ && cap.rawChildren.length === 1
1401
+ && host._parsemanCstCollapse(type, kids[0], kids, cap.rawChildren)
1402
+ ) {
1403
+ nd = kids[0]
1404
+ } else if (proj >= 0) {
1405
+ nd = hostCst && host !== undefined
1406
+ ? 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)
1407
+ : projectChild(kids, proj, type)
1408
+ } else if (buildIdx >= 0) {
1409
+ if (hostCst && host !== undefined) {
1410
+ // A direct builder is bypassed under a CST host: the host must never
1411
+ // receive an arbitrary AST object as a child of a CST node.
1412
+ 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)
1413
+ } else {
1414
+ const build = fns[buildIdx] as (
1415
+ children: readonly unknown[], fields: FieldMap | undefined, span: { start: number; end: number },
1416
+ rawChildren: readonly unknown[], triviaLog: readonly number[], state: unknown,
1417
+ ) => unknown
1418
+ if (COUNT) siteFn('NODE build()', build)
1419
+ nd = build(kids, fieldMap, span, cap.rawChildren, (flags & 4) !== 0 || hostCst ? cap.triviaLog : EMPTY_TL, st)
1420
+ }
1421
+ } else if (host !== undefined) {
1422
+ // Structural node: the host owns the value.
1423
+ nd = host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, st, tags)
1424
+ } else {
1425
+ nd = { _tag: 'node', type, span, state: st ?? null, children: kids }
1426
+ }
1427
+ // A ROOT NODE IS NOT A CHILD — see the same guard in `assemble.ts`.
1428
+ if (saved.buf !== undefined || saved.ch !== undefined) {
1429
+ pushCstChild(ctx, nd, rawEntry(nd, input, pos, end))
1430
+ }
1431
+ EC.e = end
1432
+ return nd
1433
+ }
1434
+
1435
+ case OP_ATTEMPT: {
1436
+ // `attempt()` verbatim: mark, run, and on failure restore every capture
1437
+ // sink and re-anchor the report at the transaction's entry. A COMMITTED
1438
+ // failure is still rolled back and then propagated untouched — the
1439
+ // interpreter returns `result` itself on that branch.
1440
+ const need = rollbackNeeded(ctx)
1441
+ const mRaw = need ? cstRawLen(ctx) : 0
1442
+ const mTl = need ? cstTlLen(ctx) : 0
1443
+ const mLv = need ? cstLeavesLen(ctx) : 0
1444
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
1445
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
1446
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
1447
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1448
+ const v = exec(code[ip + 1]!, input, pos, ctx)
1449
+ if (v !== FAIL) return v
1450
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1451
+ if (ctx._fc === true) return FAIL
1452
+ ctx._fe = pos
1453
+ return FAIL
1454
+ }
1455
+
1456
+ case OP_NOT: {
1457
+ const need = rollbackNeeded(ctx)
1458
+ const mRaw = need ? cstRawLen(ctx) : 0
1459
+ const mTl = need ? cstTlLen(ctx) : 0
1460
+ const mLv = need ? cstLeavesLen(ctx) : 0
1461
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
1462
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
1463
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
1464
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1465
+ const v = exec(code[ip + 1]!, input, pos, ctx)
1466
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1467
+ if (v === FAIL) { EC.e = pos; return null }
1468
+ // `not.ts:50` — the ASSERTION's own set, at the assertion's position.
1469
+ ctx._fe = pos
1470
+ ctx._fx = fx[code[ip + 2]!] as string[]
1471
+ return FAIL
1472
+ }
1473
+
1474
+ case OP_PEEK: {
1475
+ const need = rollbackNeeded(ctx)
1476
+ const mRaw = need ? cstRawLen(ctx) : 0
1477
+ const mTl = need ? cstTlLen(ctx) : 0
1478
+ const mLv = need ? cstLeavesLen(ctx) : 0
1479
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
1480
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
1481
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
1482
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1483
+ const v = exec(code[ip + 1]!, input, pos, ctx)
1484
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1485
+ // `peek.ts:60` — the ASSERTION's own set, at the assertion's position.
1486
+ if (v === FAIL) { ctx._fe = pos; ctx._fx = fx[code[ip + 2]!] as string[]; return FAIL }
1487
+ EC.e = pos
1488
+ return null
1489
+ }
1490
+
1491
+ default:
1492
+ throw new Error(`table driver: unknown opcode ${String(code[ip])} at ${ip}`)
1493
+ }
1494
+ }
1495
+
1496
+ /**
1497
+ * A table SUBTREE, presented as a combinator.
1498
+ *
1499
+ * `scanTo()` and `balanced()` are rebuilt from their specs with the SHARED
1500
+ * constructors rather than re-implemented here, and those constructors take
1501
+ * COMBINATORS — a sentinel, a skipper list. This is the adapter: it runs the
1502
+ * subtree through this same driver and returns the combinator protocol's
1503
+ * `ParseResult`. Nothing else in the table crosses that boundary, and the cost
1504
+ * is one result object per scan probe, which the interpreter pays too.
1505
+ *
1506
+ * `_def` is deliberately opaque (`unknown`), so `matchesEmpty` answers `true`
1507
+ * and `firstSetOf` returns the carried set — the SAFE directions: a nullable
1508
+ * answer only costs a `many`/`choice` optimization, it never changes what is
1509
+ * accepted. The sentinel is the one exception: a `literal()` sentinel keeps its
1510
+ * def, because `scanTo` derives its expected set from exactly that
1511
+ * (`src/combinators/scanTo.ts:168`) and reporting `"sentinel"` where the
1512
+ * interpreter reports `"{"` is a real divergence.
1513
+ */
1514
+ function subtreeComb(r: SubtreeRef, def?: ParserDef): Combinator<unknown> {
1515
+ const ip = r[0]
1516
+ return {
1517
+ _tag: 'tableSubtree',
1518
+ _meta: { firstSet: refFirstSet(r[1]), canMatchNewline: true, isTrivia: false },
1519
+ _def: def ?? { tag: 'unknown' } as unknown as ParserDef,
1520
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<unknown> {
1521
+ const v = exec(ip, input, pos, ctx)
1522
+ if (v === FAIL) {
1523
+ const fe = ctx._fe
1524
+ const at = fe === undefined || fe < 0 ? pos : fe
1525
+ return { ok: false, expected: (ctx._fx ?? EMPTY_FX) as string[], span: { start: at, end: at } }
1526
+ }
1527
+ return { ok: true, value: v, span: { start: pos, end: EC.e } }
1528
+ },
1529
+ }
1530
+ }
1531
+
1532
+ /** The first set a `SubtreeRef` carries: −1 is `any`, −2 is `empty`. */
1533
+ function refFirstSet(cls: number): FirstSet {
1534
+ if (cls === -2) return { kind: 'empty' }
1535
+ if (cls < 0) return { kind: 'any' }
1536
+ const spec = prog.cc[cls] ?? ''
1537
+ const ranges = decodeClassSpec(spec)
1538
+ return { kind: 'ranges', ranges }
1539
+ }
1540
+
1541
+ /**
1542
+ * The scan pool: one real `scanTo()` / `balanced()` per spec.
1543
+ *
1544
+ * Built ONCE per resolved table, like the trivia pool, so a scan costs no
1545
+ * construction per parse. Rebuilding through the constructors is what keeps
1546
+ * `balanced`'s ambient re-resolution, its `expect()`-based recovery, its
1547
+ * `strict` failure and its one-leaf `token()` wrapper — none of which is
1548
+ * reconstructible from a driver-side re-implementation without a second copy to
1549
+ * drift.
1550
+ */
1551
+ const scans: readonly Combinator<unknown>[] = (prog.scans ?? []).map(s => {
1552
+ const skip = s.skip.map(r => subtreeComb(r))
1553
+ const raw = (s.flags & 1) !== 0
1554
+ if (s.kind === 1) {
1555
+ return balanced(s.open!, s.close!, { skip, raw, strict: (s.flags & 4) !== 0 }) as Combinator<unknown>
1556
+ }
1557
+ const sentDef: ParserDef | undefined = typeof s.sent === 'string'
1558
+ ? { tag: 'literal', value: s.sent, caseInsensitive: false } as unknown as ParserDef
1559
+ : undefined
1560
+ return scanTo(subtreeComb(s.sentinel!, sentDef), { skip, raw, orEOF: (s.flags & 2) !== 0 }) as Combinator<unknown>
1561
+ })
1562
+
1563
+ const scanSkip: readonly (readonly Combinator<unknown>[])[] =
1564
+ (prog.scanSkip ?? []).map(set => set.map(r => subtreeComb(r)))
1565
+
1566
+ /**
1567
+ * ONCE PER PARSE. `trackLines` is fixed by `run()` before the entry is called,
1568
+ * and it is the only leg of the swap's legality that is a property of the
1569
+ * PARSE rather than of the scope — so it is decided here and never re-asked on
1570
+ * the term path. `SCAN` starts null: a rule reached before any scope has no
1571
+ * installed trivia, and a stale one from a previous parse on a reused `ctx`
1572
+ * would skip trivia this grammar never declared.
1573
+ */
1574
+ function begin(ctx: ParseContext): void {
1575
+ const host = ctx.build
1576
+ // Read user-owned host properties before touching the active frame. A
1577
+ // throwing getter must not leave the driver one level deeper or half reset.
1578
+ const nextFast = ctx.trackLines !== true
1579
+ const nextHostCst = host !== undefined && cstOutputHost(host)
1580
+ const nextHostReadsChildren = host?._parsemanReadsChildren !== false
1581
+ || host?._parsemanCstCollapse !== undefined
1582
+ const nextHostCaptureTrivia = host?._parsemanCaptureTrivia
1583
+ if (depth > 0) {
1584
+ frames.push({
1585
+ scan: SCAN, fast: FAST,
1586
+ hostCst: HOSTCST, hostReadsChildren: HOSTREADSCHILDREN,
1587
+ hostCaptureTrivia: HOSTCAPTURETRIVIA,
1588
+ end: EC.e,
1589
+ })
1590
+ }
1591
+ depth++
1592
+ FAST = nextFast
1593
+ SCAN = null
1594
+ HOSTCST = nextHostCst
1595
+ HOSTREADSCHILDREN = nextHostReadsChildren
1596
+ HOSTCAPTURETRIVIA = nextHostCaptureTrivia
1597
+ }
1598
+
1599
+ function finish(): void {
1600
+ if (depth <= 0) throw new Error('parseman table reference driver frame underflow')
1601
+ depth--
1602
+ if (depth === 0) return
1603
+ const prior = frames.pop()!
1604
+ SCAN = prior.scan
1605
+ FAST = prior.fast
1606
+ HOSTCST = prior.hostCst
1607
+ HOSTREADSCHILDREN = prior.hostReadsChildren
1608
+ HOSTCAPTURETRIVIA = prior.hostCaptureTrivia
1609
+ EC.e = prior.end
1610
+ }
1611
+
1612
+ return { exec, end: () => EC.e, begin, finish, scanSkip }
1613
+ }
1614
+
1615
+ /**
1616
+ * THE REFERENCE DRIVER. Turn a program into a rule map, by INTERPRETING the
1617
+ * bytecode. Nothing ships on this — `tableRules` (`table/assemble.ts`) is
1618
+ * what `parseman/table` exports as `tableRules`, and what every artifact binds.
1619
+ *
1620
+ * IT IS CALLED `execRules` AND NOT `tableRules` FOR A MEASURED REASON. Until
1621
+ * this rename the two engines exported the SAME name with the SAME signature
1622
+ * across a module boundary, so `import { tableRules } from '…/table/exec.ts'`
1623
+ * and `import { tableRules } from 'parseman/table'` type-checked identically
1624
+ * and silently selected different engines. Three modules picked the wrong one:
1625
+ * `compiler/linker.ts` (the whole `compose()`/`fuse()` path), `table/fold.ts`
1626
+ * (every folded artifact's variant load), and `bench/jess/fixture.ts` — the
1627
+ * CANONICAL fixture harness, whose column printed as `table` was this function
1628
+ * for the entire cycle it was quoted in.
1629
+ *
1630
+ * A name that two engines can answer to is not a naming preference; it is a
1631
+ * defect with no diagnostic. Do not re-alias this to `tableRules` anywhere.
1632
+ *
1633
+ * The entries have the SAME signature as the assembler's, so `run()`, the
1634
+ * linker's public wrappers and every consumer are unchanged — which is exactly
1635
+ * why the substitution was invisible.
1636
+ */
1637
+ export function execRules(
1638
+ source: TableProgram | CompactProgram,
1639
+ /**
1640
+ * MEASUREMENT CONTROL, not a feature. `leafSwap: false` hands the driver a
1641
+ * `triviaScan` of all nulls, so `SCAN` is never installed and every skip takes
1642
+ * the shared generic functions — the exact pre-swap behaviour, from the SAME
1643
+ * driver code, differing only in TABLE DATA. That is what makes an in-process
1644
+ * A/B of the swap possible on a machine where cross-run comparison is not, and
1645
+ * it is G5-legal for the same reason `lines` is: it is read once, at rule-map
1646
+ * construction, and the parse path never sees an option.
1647
+ */
1648
+ opts: { leafSwap?: boolean } = {},
1649
+ artifactMetadata: Readonly<Record<symbol, unknown>> = {},
1650
+ ): Record<string, TableRule> {
1651
+ const prog = expandCompact(source)
1652
+ const t = resolveTable(prog)
1653
+ const scan = opts.leafSwap === false ? t.triviaScan.map(() => null) : t.triviaScan
1654
+ const d = makeDriver(t.code, t.k, t.fns, t.cc, t.fx, t.disp, t.dsp, t.trivia, scan, t.triviaLabelled, prog, newEndCell())
1655
+ const names = Object.keys(prog.rules)
1656
+ const entries = names.map(n => prog.rules[n]!)
1657
+ let last: unknown
1658
+ return stampRuleMap(prog, {
1659
+ runRule: (ri, input, pos, ctx) => {
1660
+ d.begin(ctx)
1661
+ try {
1662
+ const v = d.exec(entries[ri]!, input, pos, ctx)
1663
+ if (v === FAIL) return -1
1664
+ last = v
1665
+ return d.end()
1666
+ } finally {
1667
+ // Host reducers are ordinary user code and may re-enter this exact rule
1668
+ // map (or throw). Restore its scan/host/end frame on every exit.
1669
+ d.finish()
1670
+ }
1671
+ },
1672
+ lastValue: () => last,
1673
+ // Chosen PER RULE, from `scanSkipOf`: `run()` reads the ENTRY rule's own
1674
+ // `_meta.grammarScanSkip` (grammar.ts:203), and installing one program-wide
1675
+ // set instead gave a `composeLeaf` piece's rules a skip list the interpreter
1676
+ // never gives them — a divergence that is invisible, because the parse
1677
+ // succeeds having skipped over a delimiter it should have stopped at.
1678
+ scanSkipFor: ri => d.scanSkip[prog.scanSkipOf?.[ri] ?? -1],
1679
+ }, artifactMetadata, t)
1680
+ }