parseman 0.45.0 → 0.47.0

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