parseman 0.46.0 → 0.47.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/README.md +8 -3
  2. package/dist/analysis/choice-cost-render.d.ts +12 -4
  3. package/dist/analysis/choice-cost-render.d.ts.map +1 -1
  4. package/dist/analysis/choice-cost.d.ts +2 -2
  5. package/dist/analysis/choice-cost.d.ts.map +1 -1
  6. package/dist/analysis/commitment.d.ts +131 -0
  7. package/dist/analysis/commitment.d.ts.map +1 -0
  8. package/dist/analysis/diagnostics.cjs +10339 -7787
  9. package/dist/analysis/diagnostics.cjs.map +3 -4
  10. package/dist/analysis/diagnostics.d.ts +2 -2
  11. package/dist/analysis/diagnostics.js +10339 -7787
  12. package/dist/analysis/diagnostics.js.map +3 -4
  13. package/dist/analysis/duplication.d.ts.map +1 -1
  14. package/dist/analysis/fix.d.ts +3 -3
  15. package/dist/analysis/fix.d.ts.map +1 -1
  16. package/dist/analysis/format-number.d.ts +23 -0
  17. package/dist/analysis/format-number.d.ts.map +1 -0
  18. package/dist/analysis/gating.d.ts +39 -2
  19. package/dist/analysis/gating.d.ts.map +1 -1
  20. package/dist/analysis/rebuild.d.ts.map +1 -1
  21. package/dist/analysis/terminal.d.ts +5 -2
  22. package/dist/analysis/terminal.d.ts.map +1 -1
  23. package/dist/cli/index.cjs +10924 -8506
  24. package/dist/cli/index.cjs.map +3 -4
  25. package/dist/cli/index.js +10924 -8506
  26. package/dist/cli/index.js.map +3 -4
  27. package/dist/combinators/adjacency.d.ts +100 -0
  28. package/dist/combinators/adjacency.d.ts.map +1 -0
  29. package/dist/combinators/choice.d.ts.map +1 -1
  30. package/dist/combinators/completions.d.ts.map +1 -1
  31. package/dist/combinators/dispatch.d.ts +8 -1
  32. package/dist/combinators/dispatch.d.ts.map +1 -1
  33. package/dist/combinators/expect.d.ts.map +1 -1
  34. package/dist/combinators/first-set.d.ts.map +1 -1
  35. package/dist/combinators/gate.d.ts.map +1 -1
  36. package/dist/combinators/grammar.d.ts +22 -0
  37. package/dist/combinators/grammar.d.ts.map +1 -1
  38. package/dist/combinators/literal.d.ts +8 -0
  39. package/dist/combinators/literal.d.ts.map +1 -1
  40. package/dist/combinators/map.d.ts +0 -1
  41. package/dist/combinators/map.d.ts.map +1 -1
  42. package/dist/combinators/node.d.ts +13 -3
  43. package/dist/combinators/node.d.ts.map +1 -1
  44. package/dist/combinators/parser.d.ts +1 -1
  45. package/dist/combinators/parser.d.ts.map +1 -1
  46. package/dist/combinators/ref.d.ts.map +1 -1
  47. package/dist/combinators/regex.d.ts +1 -16
  48. package/dist/combinators/regex.d.ts.map +1 -1
  49. package/dist/combinators/repeat.d.ts +30 -2
  50. package/dist/combinators/repeat.d.ts.map +1 -1
  51. package/dist/combinators/scanTo.d.ts +71 -1
  52. package/dist/combinators/scanTo.d.ts.map +1 -1
  53. package/dist/combinators/sequence.d.ts.map +1 -1
  54. package/dist/combinators/token.d.ts.map +1 -1
  55. package/dist/combinators/trivia-boundary.d.ts +26 -0
  56. package/dist/combinators/trivia-boundary.d.ts.map +1 -0
  57. package/dist/combinators/trivia-skip.d.ts +45 -1
  58. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  59. package/dist/combinators/withCtx.d.ts.map +1 -1
  60. package/dist/compiler/compile-linkable-table.d.ts +104 -0
  61. package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
  62. package/dist/compiler/degradation.d.ts +0 -2
  63. package/dist/compiler/degradation.d.ts.map +1 -1
  64. package/dist/compiler/fields.d.ts +13 -0
  65. package/dist/compiler/fields.d.ts.map +1 -1
  66. package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
  67. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  68. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  69. package/dist/compiler/linker.d.ts +47 -53
  70. package/dist/compiler/linker.d.ts.map +1 -1
  71. package/dist/compiler/token-alphabet.d.ts +81 -0
  72. package/dist/compiler/token-alphabet.d.ts.map +1 -0
  73. package/dist/compiler/token-dispatch.d.ts +95 -0
  74. package/dist/compiler/token-dispatch.d.ts.map +1 -0
  75. package/dist/compiler/token-scanner.d.ts +53 -0
  76. package/dist/compiler/token-scanner.d.ts.map +1 -0
  77. package/dist/compiler/value-usage.d.ts.map +1 -1
  78. package/dist/coverage.d.ts +1 -1
  79. package/dist/coverage.d.ts.map +1 -1
  80. package/dist/cst/capture-buffer.d.ts +35 -0
  81. package/dist/cst/capture-buffer.d.ts.map +1 -1
  82. package/dist/cst/reflection-attach.d.ts +8 -0
  83. package/dist/cst/reflection-attach.d.ts.map +1 -0
  84. package/dist/cst/reflection-symbols.d.ts +10 -0
  85. package/dist/cst/reflection-symbols.d.ts.map +1 -0
  86. package/dist/cst/reflection.d.ts +2 -4
  87. package/dist/cst/reflection.d.ts.map +1 -1
  88. package/dist/cst/root-trivia-scope.d.ts +26 -0
  89. package/dist/cst/root-trivia-scope.d.ts.map +1 -0
  90. package/dist/cst/trivia-charscan.d.ts +18 -0
  91. package/dist/cst/trivia-charscan.d.ts.map +1 -0
  92. package/dist/cst/trivia-kinds.d.ts +19 -0
  93. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  94. package/dist/functional/doc.d.ts.map +1 -1
  95. package/dist/functional/run-tabled.d.ts +12 -0
  96. package/dist/functional/run-tabled.d.ts.map +1 -0
  97. package/dist/functional/run.d.ts +25 -42
  98. package/dist/functional/run.d.ts.map +1 -1
  99. package/dist/grammar-metadata.d.ts +3 -0
  100. package/dist/grammar-metadata.d.ts.map +1 -0
  101. package/dist/index.cjs +9290 -7181
  102. package/dist/index.cjs.map +3 -4
  103. package/dist/index.d.ts +18 -9
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +9300 -7191
  106. package/dist/index.js.map +3 -4
  107. package/dist/language-service/index.cjs +313 -121
  108. package/dist/language-service/index.cjs.map +3 -4
  109. package/dist/language-service/index.js +313 -121
  110. package/dist/language-service/index.js.map +3 -4
  111. package/dist/oracle/index.cjs.map +0 -1
  112. package/dist/oracle/index.js.map +0 -1
  113. package/dist/parse-context.d.ts +49 -0
  114. package/dist/parse-context.d.ts.map +1 -0
  115. package/dist/plugin/evaluator.d.ts +40 -1
  116. package/dist/plugin/evaluator.d.ts.map +1 -1
  117. package/dist/plugin/index.cjs +11175 -9534
  118. package/dist/plugin/index.cjs.map +3 -4
  119. package/dist/plugin/index.d.ts.map +1 -1
  120. package/dist/plugin/index.js +11175 -9534
  121. package/dist/plugin/index.js.map +3 -4
  122. package/dist/regex/classes.d.ts +28 -0
  123. package/dist/regex/classes.d.ts.map +1 -1
  124. package/dist/regex/first-set.d.ts +14 -0
  125. package/dist/regex/first-set.d.ts.map +1 -1
  126. package/dist/run/index.cjs +136 -51
  127. package/dist/run/index.cjs.map +2 -3
  128. package/dist/run/index.js +136 -51
  129. package/dist/run/index.js.map +2 -3
  130. package/dist/spec/index.cjs +2934 -1611
  131. package/dist/spec/index.cjs.map +3 -4
  132. package/dist/spec/index.js +2934 -1611
  133. package/dist/spec/index.js.map +3 -4
  134. package/dist/spec/model.d.ts +8 -0
  135. package/dist/spec/model.d.ts.map +1 -1
  136. package/dist/table/assemble.d.ts +307 -0
  137. package/dist/table/assemble.d.ts.map +1 -0
  138. package/dist/table/cell.d.ts +42 -0
  139. package/dist/table/cell.d.ts.map +1 -0
  140. package/dist/table/child-slots.d.ts +30 -0
  141. package/dist/table/child-slots.d.ts.map +1 -0
  142. package/dist/table/compile-rule-map.d.ts +136 -0
  143. package/dist/table/compile-rule-map.d.ts.map +1 -0
  144. package/dist/table/compile.d.ts +56 -0
  145. package/dist/table/compile.d.ts.map +1 -0
  146. package/dist/table/duplication-hook.d.ts +32 -0
  147. package/dist/table/duplication-hook.d.ts.map +1 -0
  148. package/dist/table/emit-assembly.d.ts +139 -0
  149. package/dist/table/emit-assembly.d.ts.map +1 -0
  150. package/dist/table/emit.d.ts +113 -0
  151. package/dist/table/emit.d.ts.map +1 -0
  152. package/dist/table/encode-baseline.d.ts +16 -0
  153. package/dist/table/encode-baseline.d.ts.map +1 -0
  154. package/dist/table/encode.d.ts +71 -0
  155. package/dist/table/encode.d.ts.map +1 -0
  156. package/dist/table/exec-baseline.d.ts +9 -0
  157. package/dist/table/exec-baseline.d.ts.map +1 -0
  158. package/dist/table/exec.d.ts +72 -0
  159. package/dist/table/exec.d.ts.map +1 -0
  160. package/dist/table/fold.d.ts +25 -0
  161. package/dist/table/fold.d.ts.map +1 -0
  162. package/dist/table/index.cjs +11463 -0
  163. package/dist/table/index.cjs.map +6 -0
  164. package/dist/table/index.d.ts +100 -0
  165. package/dist/table/index.d.ts.map +1 -0
  166. package/dist/table/index.js +11440 -0
  167. package/dist/table/index.js.map +6 -0
  168. package/dist/table/inspect.d.ts +17 -0
  169. package/dist/table/inspect.d.ts.map +1 -0
  170. package/dist/table/ops.d.ts +453 -0
  171. package/dist/table/ops.d.ts.map +1 -0
  172. package/dist/table/program.d.ts +496 -0
  173. package/dist/table/program.d.ts.map +1 -0
  174. package/dist/table/run-support.d.ts +38 -0
  175. package/dist/table/run-support.d.ts.map +1 -0
  176. package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
  177. package/dist/table/scan-shapes.d.ts.map +1 -0
  178. package/dist/table/site-labels.d.ts +58 -0
  179. package/dist/table/site-labels.d.ts.map +1 -0
  180. package/dist/table/stamp.d.ts +70 -0
  181. package/dist/table/stamp.d.ts.map +1 -0
  182. package/dist/types.d.ts +80 -24
  183. package/dist/types.d.ts.map +1 -1
  184. package/dist/version.d.ts +1 -1
  185. package/package.json +26 -2
  186. package/src/analysis/choice-cost-gate.ts +359 -0
  187. package/src/analysis/choice-cost-render.ts +293 -0
  188. package/src/analysis/choice-cost.ts +1008 -0
  189. package/src/analysis/commitment.ts +557 -0
  190. package/src/analysis/corpus.ts +130 -0
  191. package/src/analysis/diagnose-render.ts +625 -0
  192. package/src/analysis/diagnose.ts +317 -0
  193. package/src/analysis/diagnostics.ts +33 -0
  194. package/src/analysis/duplication.ts +1955 -0
  195. package/src/analysis/fix-render.ts +217 -0
  196. package/src/analysis/fix.ts +669 -0
  197. package/src/analysis/format-number.ts +31 -0
  198. package/src/analysis/gating.ts +792 -0
  199. package/src/analysis/grammar.ts +72 -0
  200. package/src/analysis/rebuild.ts +287 -0
  201. package/src/analysis/terminal.ts +336 -0
  202. package/src/cli/index.ts +413 -0
  203. package/src/combinators/adjacency.ts +171 -0
  204. package/src/combinators/attempt.ts +41 -0
  205. package/src/combinators/case-fold.ts +90 -0
  206. package/src/combinators/choice.ts +466 -0
  207. package/src/combinators/completions.ts +64 -0
  208. package/src/combinators/dispatch.ts +358 -0
  209. package/src/combinators/expect.ts +159 -0
  210. package/src/combinators/first-set.ts +348 -0
  211. package/src/combinators/gate.ts +50 -0
  212. package/src/combinators/grammar.ts +272 -0
  213. package/src/combinators/keywords.ts +145 -0
  214. package/src/combinators/literal.ts +102 -0
  215. package/src/combinators/map.ts +106 -0
  216. package/src/combinators/node.ts +345 -0
  217. package/src/combinators/not.ts +55 -0
  218. package/src/combinators/parser.ts +266 -0
  219. package/src/combinators/peek.ts +63 -0
  220. package/src/combinators/probe.ts +25 -0
  221. package/src/combinators/ref.ts +109 -0
  222. package/src/combinators/regex.ts +170 -0
  223. package/src/combinators/repeat.ts +561 -0
  224. package/src/combinators/scanTo.ts +393 -0
  225. package/src/combinators/sequence.ts +237 -0
  226. package/src/combinators/token.ts +120 -0
  227. package/src/combinators/trivia-boundary.ts +48 -0
  228. package/src/combinators/trivia-skip.ts +852 -0
  229. package/src/combinators/withCtx.ts +45 -0
  230. package/src/compiler/build-arity.ts +330 -0
  231. package/src/compiler/compile-linkable-table.ts +209 -0
  232. package/src/compiler/degradation.ts +266 -0
  233. package/src/compiler/fields.ts +191 -0
  234. package/src/compiler/grammar-coverage-ids.ts +129 -0
  235. package/src/compiler/ir-serialize.ts +615 -0
  236. package/src/compiler/linker.ts +954 -0
  237. package/src/compiler/token-alphabet.ts +209 -0
  238. package/src/compiler/token-dispatch.ts +353 -0
  239. package/src/compiler/token-scanner.ts +318 -0
  240. package/src/compiler/value-usage.ts +147 -0
  241. package/src/coverage.ts +383 -0
  242. package/src/cst/capture-buffer.ts +297 -0
  243. package/src/cst/host-mode.ts +69 -0
  244. package/src/cst/reflection-attach.ts +11 -0
  245. package/src/cst/reflection-symbols.ts +9 -0
  246. package/src/cst/reflection.ts +149 -0
  247. package/src/cst/relative-spans.ts +207 -0
  248. package/src/cst/root-trivia-scope.ts +30 -0
  249. package/src/cst/trivia-charscan.ts +312 -0
  250. package/src/cst/trivia-entries.ts +285 -0
  251. package/src/cst/trivia-index.ts +131 -0
  252. package/src/cst/trivia-kinds.ts +358 -0
  253. package/src/cst/types.ts +76 -0
  254. package/src/cst/walk.ts +101 -0
  255. package/src/functional/doc.ts +959 -0
  256. package/src/functional/run-tabled.ts +153 -0
  257. package/src/functional/run.ts +558 -0
  258. package/src/grammar-metadata.ts +2 -0
  259. package/src/index.ts +189 -0
  260. package/src/language-service/index.ts +221 -0
  261. package/src/line-index.ts +131 -0
  262. package/src/oracle/digest.ts +620 -0
  263. package/src/oracle/index.ts +55 -0
  264. package/src/parse-context.ts +91 -0
  265. package/src/plugin/direct-builder-static.ts +102 -0
  266. package/src/plugin/evaluator.ts +1505 -0
  267. package/src/plugin/free-identifiers.ts +338 -0
  268. package/src/plugin/index.ts +2281 -0
  269. package/src/plugin/reducer-resolver.ts +606 -0
  270. package/src/recovery/scan.ts +155 -0
  271. package/src/regex/classes.ts +183 -0
  272. package/src/regex/first-set.ts +316 -0
  273. package/src/run/index.ts +31 -0
  274. package/src/spec/ebnf.ts +128 -0
  275. package/src/spec/index.ts +54 -0
  276. package/src/spec/model.ts +636 -0
  277. package/src/spec/railroad-lib.ts +559 -0
  278. package/src/spec/railroad.ts +256 -0
  279. package/src/table/assemble.ts +3006 -0
  280. package/src/table/cell.ts +42 -0
  281. package/src/table/child-slots.ts +113 -0
  282. package/src/table/compile-rule-map.ts +336 -0
  283. package/src/table/compile.ts +244 -0
  284. package/src/table/duplication-hook.ts +89 -0
  285. package/src/table/emit-assembly.ts +1777 -0
  286. package/src/table/emit.ts +429 -0
  287. package/src/table/encode-baseline.ts +340 -0
  288. package/src/table/encode.ts +1569 -0
  289. package/src/table/exec-baseline.ts +537 -0
  290. package/src/table/exec.ts +1680 -0
  291. package/src/table/fold.ts +38 -0
  292. package/src/table/index.ts +108 -0
  293. package/src/table/inspect.ts +52 -0
  294. package/src/table/ops.ts +467 -0
  295. package/src/table/program.ts +876 -0
  296. package/src/table/run-support.ts +66 -0
  297. package/src/table/scan-shapes.ts +1578 -0
  298. package/src/table/site-labels.ts +228 -0
  299. package/src/table/stamp.ts +229 -0
  300. package/src/types.ts +563 -0
  301. package/src/version.ts +16 -0
  302. package/dist/compiler/codegen.d.ts +0 -334
  303. package/dist/compiler/codegen.d.ts.map +0 -1
  304. package/dist/compiler/inline-build.d.ts +0 -11
  305. package/dist/compiler/inline-build.d.ts.map +0 -1
  306. package/dist/compiler/inline-callback.d.ts +0 -21
  307. package/dist/compiler/inline-callback.d.ts.map +0 -1
  308. package/dist/compiler/line-index.d.ts +0 -2
  309. package/dist/compiler/line-index.d.ts.map +0 -1
  310. package/dist/compiler/module-hoist.d.ts +0 -106
  311. package/dist/compiler/module-hoist.d.ts.map +0 -1
  312. package/dist/compiler/scannable-run.d.ts.map +0 -1
  313. package/dist/compiler/scannable-terminal.d.ts +0 -23
  314. package/dist/compiler/scannable-terminal.d.ts.map +0 -1
  315. package/dist/compiler/trivia-fast-path.d.ts +0 -38
  316. package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
@@ -0,0 +1,3006 @@
1
+ /**
2
+ * THE ASSEMBLER — the table, LINKED into closures instead of interpreted.
3
+ *
4
+ * This is ledger row G5 as stated: *"build the grammar reference at run start,
5
+ * making the swaps on rules / sub-rules (leaves) at that point, then run with NO
6
+ * logic branching for that option input"*. `exec.ts` builds the same table and
7
+ * then interprets it: one `switch (code[ip])` over 29 opcodes, executed ONCE PER
8
+ * ROW — 497,360 rows for `benchmark.less` (`bench/jess/g5-rows.ts`) — each
9
+ * re-reading its opcode and re-decoding its operands from the `Int32Array`, and
10
+ * re-testing the same per-parse options. That is the per-node branching the
11
+ * design exists to remove.
12
+ *
13
+ * ## The shape
14
+ *
15
+ * Assembly walks the reachable table ONCE and lowers each site to a PIECE: a
16
+ * closure with its operands already captured as `const`s and its children bound
17
+ * as DIRECT references to their own pieces. At parse time there is no opcode
18
+ * read, no operand decode and no switch — a piece is called, and it calls the
19
+ * pieces it holds.
20
+ *
21
+ * The numbers that make this obviously the right trade, measured not assumed:
22
+ *
23
+ * 2,241 distinct reachable sites in the less table (`bench/jess/g5-sites.ts`)
24
+ * 497,360 rows executed for one parse of `benchmark.less`
25
+ *
26
+ * Pieces are GRAMMAR-sized; rows are INPUT-sized. Assembly allocates ~2.2k
27
+ * closures once per process and removes a dispatch plus an operand decode from
28
+ * ~497k executions — a 222x ratio. Assembly cost is paid once and is not the
29
+ * metric; it is measured anyway (`bench/jess/g5-ms.ts`, `bench/jess/g5-profile.ts`).
30
+ *
31
+ * ## Why this is not "just tuning the switch"
32
+ *
33
+ * A 29-case switch on an `Int32Array` load is a jump table whose successor V8
34
+ * cannot know. The interpreter loop is ONE basic block with 29 merge edges, so
35
+ * TurboFan cannot specialise any arm against its caller and every operand stays
36
+ * an untyped load. Measured on this branch: `exec` reaches TURBOFAN and is
37
+ * deoptimised back to MAGLEV repeatedly — 100 deopt events in a 20-parse run.
38
+ * The design was not paying dispatch overhead, it was opting its whole hot path
39
+ * out of the optimising compiler.
40
+ *
41
+ * A piece is created once and its call sites see one shape, so they stay
42
+ * monomorphic and TurboFan can inline them. That is the structural win, and it is
43
+ * why the target is codegen's number rather than a fraction of the gap.
44
+ *
45
+ * ## Why this keeps the artifact small
46
+ *
47
+ * The pieces ship ONCE, here, shared by every grammar and every variant. What a
48
+ * bundle carries is still the table — DATA. Codegen's 2.10 MB is recognition
49
+ * machinery inlined bespoke per rule; this stays at the table's 0.56 MB because
50
+ * the machinery is one copy in the runtime and the variation lives in the
51
+ * ASSEMBLY, not in duplicated piece bodies.
52
+ *
53
+ * ## Options are consumed by SELECTION, not by testing
54
+ *
55
+ * The piece set is a SUPERSET; an option set reaches a subset of it. Assembly
56
+ * walks from the entry rule and instantiates only what it touches, so a piece an
57
+ * option excludes is never allocated, never linked and costs zero at run — not a
58
+ * cheap branch, zero. Where a decision is knowable from the option set
59
+ * (`hostCst` below), assembly picks the piece rather than emitting one that
60
+ * tests. `scripts/check-invariants.mjs` enforces that no piece body reads a
61
+ * config field; see `CONFIG_FIELDS` there.
62
+ *
63
+ * Semantics are `exec.ts`'s, case for case. That file remains the reference and
64
+ * the three-way identity sweep gates this against it.
65
+ */
66
+ import type { Combinator, FieldMap, FirstSet, ParseContext, ParseResult, ParserDef } from '../types.ts'
67
+ import { balanced, scanTo } from '../combinators/scanTo.ts'
68
+ import { buildFieldMap } from '../compiler/fields.ts'
69
+ import { asciiFoldKey } from '../combinators/dispatch.ts'
70
+ import { projectChild, unwrapChild } from '../combinators/node.ts'
71
+ import { cstOutputHost } from '../compiler/build-arity.ts'
72
+ import { consumeTrivia } from '../combinators/trivia-skip.ts'
73
+ import {
74
+ advanceTrivia, needsDeferredTriviaCommit, rollbackScannedTriviaAt, rollbackTrivia, rollbackTriviaAt,
75
+ saveTriviaMark, scanTrivia, skipTriviaScanned, type FastTriviaScanner,
76
+ } from '../combinators/trivia-skip.ts'
77
+ import {
78
+ cstCaptureActive, cstLeavesLen, cstTlLen,
79
+ demoteCapturedToRaw, pushCstChild, pushCstLeaf,
80
+ type CstCaptureBuf,
81
+ } from '../cst/capture-buffer.ts'
82
+ import {
83
+ OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
84
+ OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SEQ, OP_SEQV, OP_XFORM,
85
+ OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_EXPECT, OP_SEQX, OP_SCAN,
86
+ OP_LIVE,
87
+ OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD, OP_ATTEMPT, OP_LABEL,
88
+ OP_COV,
89
+ OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE,
90
+ } from './ops.ts'
91
+ import { adjacencyHolds, adjacencyMisuse } from '../combinators/adjacency.ts'
92
+ import { failAt } from '../combinators/probe.ts'
93
+ /**
94
+ * U4 — the emitted engine, and the three pure helpers both engines share.
95
+ *
96
+ * `spanLines`/`rawEntry`/`lead` moved out of this function's closure so the
97
+ * emitted assembly can call the SAME definitions. A second copy of `rawEntry`
98
+ * is exactly how a CST leaf's span drifts between two engines that exist to be
99
+ * gated against each other.
100
+ */
101
+ import {
102
+ EMITTED_PARAMS, Unemittable, emitAssemblySource, rebuildPools,
103
+ type EmittedAssembly, type EmittedFactory,
104
+ } from './emit-assembly.ts'
105
+ import { lead, rawEntry, spanLines } from './run-support.ts'
106
+ /**
107
+ * THE COMPLETIONS PROBE, at the terminal fail sites and nowhere else.
108
+ *
109
+ * `failAt` (combinators/probe.ts) is called from exactly three places in the
110
+ * interpreter — `literal.ts`, `regex.ts`, `keywords.ts` — and codegen emits its
111
+ * mirror (`probeUpdate`) at the same leaf-fail sites. The table recorded
112
+ * NOTHING, so `completionsAt` on a table artifact saw only the top-level
113
+ * failure: a swallowed item failure inside a `sepBy` (`'{'` at offset 1 in a
114
+ * `{ decl; }` grammar) never reached the probe and the item's opener vanished
115
+ * from the completion set.
116
+ *
117
+ * Dormant unless `completionsAt` set `_probe`, so an ordinary parse pays one
118
+ * property read per terminal miss — the same price codegen pays.
119
+ */
120
+ import {
121
+ classHas, decodeClassSpec, expandCompact, resolveTable, validateDispatchSpec,
122
+ type CompactProgram, type ResolvedClass, type ResolvedTable,
123
+ type SubtreeRef, type TableProgram, type TableRule,
124
+ } from './program.ts'
125
+ import { reachableSites } from './site-labels.ts'
126
+ import { stampRuleMap } from './stamp.ts'
127
+ import { refuseUnclassifiedRootScope } from '../cst/root-trivia-scope.ts'
128
+ import { captureError, firstSetSentinel, matchesAt, orSentinel, recoverScan } from '../recovery/scan.ts'
129
+
130
+ /**
131
+ * Is the EMITTED engine (`emit-assembly.ts`) enabled for this process?
132
+ *
133
+ * Read ONCE, here, at module load — never on a parse path and never per
134
+ * assembly. It exists so the two engines can be A/B'd in ONE checkout: the
135
+ * bench guidance is explicit that a cross-worktree comparison carries a bias
136
+ * repetition does not remove.
137
+ */
138
+ const EMIT_ENABLED = (globalThis as { process?: { env?: Record<string, string | undefined> } })
139
+ .process?.env?.PM_TABLE_EMIT !== '0'
140
+
141
+ /** Failure sentinel — SHARED with the other two engines, see `cell.ts`. */
142
+ import { FAIL, newEndCell, type EndCell } from './cell.ts'
143
+
144
+ const EMPTY_TL: readonly number[] = Object.freeze([])
145
+ /** `finishCstBuf`'s two empty sentinels, held here so the node piece can inline it. */
146
+ const EMPTY_CH: unknown[] = []
147
+ const EMPTY_TLOG: number[] = []
148
+ const EMPTY_FX: string[] = []
149
+ const ROUTED_FX: string[] = ['routed()']
150
+ /** The sentinel pool a STRICT emitted assembly binds — it has no reader. */
151
+ const EMPTY_SENTS: readonly (Combinator<null> | undefined)[] = Object.freeze([])
152
+ /**
153
+ * One encoded matcher arm, LINKED into a predicate at assembly.
154
+ *
155
+ * The previous form rebuilt a `DispatchMatcherCase` per arm PER PARSE and handed
156
+ * it to `matchesDispatchMatcher` — an operand decode and an allocation in a piece
157
+ * body, and it passed a hardcoded `caseInsensitive: false`, which silently
158
+ * dropped `{ caseInsensitive: true }` on matcher arms and selected the wrong arm.
159
+ * The encoder now folds the case-insensitivity into the operands (kinds 3/4 are
160
+ * pre-folded startsWith/endsWith; a case-insensitive `matches` carries `i` in its
161
+ * flags), so the whole decision is a `const` closure chosen once.
162
+ */
163
+ function linkMatcher(m: readonly [number, string, string, number]): (key: string) => boolean {
164
+ const value = m[1]
165
+ switch (m[0]) {
166
+ case 0: return key => key.startsWith(value)
167
+ case 1: return key => key.endsWith(value)
168
+ case 3: return key => asciiFoldKey(key).startsWith(value)
169
+ case 4: return key => asciiFoldKey(key).endsWith(value)
170
+ // A fresh RegExp per test, as `matchesDispatchMatcher` builds — a cached one
171
+ // would carry `lastIndex` across parses whenever the author's pattern was
172
+ // sticky or global.
173
+ default: { const flags = m[2]; return key => new RegExp(value, flags).test(key) }
174
+ }
175
+ }
176
+
177
+ type Leaf = { _tag: 'leaf'; value: string; span: { start: number; end: number } }
178
+
179
+ /**
180
+ * The `COV` slot's value when a parse installs no collector. ONE shared function
181
+ * across every assembly, so the slot's type never widens to `| undefined` and no
182
+ * counter piece has to test it.
183
+ */
184
+ const NO_COVERAGE = (_id: string): void => {}
185
+
186
+ /**
187
+ * THE PIECE SIGNATURE — uniform, narrow, and the same for every one of the 29
188
+ * lowerings.
189
+ *
190
+ * Three arguments and one return, so every call site in the assembled graph sees
191
+ * one shape and stays monomorphic. A wider or varying signature (an out-param
192
+ * object, a per-op return shape) reintroduces the polymorphism this design
193
+ * exists to remove, and it would show up as exactly the megamorphic call sites
194
+ * `exec`'s switch already was.
195
+ *
196
+ * The end position travels in the assembly-scope `EC.e` slot, as it does in
197
+ * `exec.ts` and in codegen's `_pfEnd`, rather than in the return value — a
198
+ * `{ value, end }` pair would be an allocation per row.
199
+ */
200
+ type Piece = (input: string, pos: number, ctx: ParseContext) => unknown
201
+
202
+ /**
203
+ * ONE NON-FIRST SEQUENCE TERM, in a sequence that carries an adjacency
204
+ * assertion. Returns the new cursor or −1, with the value in `TERMV` —
205
+ * `nextTerm`'s protocol, so a bound term and a bound assertion are
206
+ * interchangeable in the loop and the loop tests neither.
207
+ */
208
+ type TermRunner = (input: string, cur: number, ctx: ParseContext) => number
209
+
210
+ /**
211
+ * The option set an assembly is specialised for.
212
+ *
213
+ * These are the facts `run()` fixes BEFORE the entry is called and that never
214
+ * change during a parse, so they are resolved by choosing a piece rather than by
215
+ * a test inside one. Anything that varies DURING a parse (position, the capture
216
+ * sinks a `node()` opens and closes, the error sink's contents) is runtime and
217
+ * stays where it is.
218
+ */
219
+ export type RunCfg = {
220
+ /** Is this parse's host a CST-output host? Fixes `OP_NODE`'s whole shape. */
221
+ readonly hostCst: boolean
222
+ /**
223
+ * Does the structural host consume the semantic `children` array? `false`
224
+ * selects raw-only node collectors. Collapse forces this back to true at the
225
+ * run boundary because its predicate consumes both child views.
226
+ */
227
+ readonly hostReadsChildren?: boolean
228
+ /**
229
+ * Structural node types whose host consumes trivia. Evaluated while an
230
+ * assembly is built, once per node site, never from the node execution path.
231
+ * Absent means the backwards-compatible default: capture every structural
232
+ * node's trivia.
233
+ */
234
+ readonly hostCaptureTrivia?: ((type: string) => boolean) | undefined
235
+ /** `ctx.trackLines` — decides whether the trivia leaf swap is legal at all. */
236
+ readonly trackLines: boolean
237
+ /**
238
+ * `ctx._tolerant` — is THIS parse allowed to recover?
239
+ *
240
+ * FIXED FOR THE LIFETIME OF A PARSE, which is what makes it legal here rather
241
+ * than a per-piece test. Every writer of `ctx._tolerant` sets it BEFORE the
242
+ * parse begins and never during — `compile.ts:119` (`parseWithErrors`),
243
+ * `functional/run.ts:399`, `functional/doc.ts:29`, `combinators/completions.ts:40`.
244
+ * The single mid-parse mutation is `recovery/scan.ts:36`, which CLEARS it for
245
+ * the duration of a sentinel probe and restores it in `finally`; a sentinel is
246
+ * a `firstSetSentinel` char-class combinator (or an `orSentinel` of two), never
247
+ * an assembled rule, so no piece runs under the cleared value. Even if one did,
248
+ * the selection would be the RIGHT one: a probe must not recover, and it would
249
+ * get the strict assembly.
250
+ *
251
+ * Contrast `cstCaptureActive`, which a previous lane proposed for the same
252
+ * treatment and which is per-NODE state — keying on that would have been
253
+ * incorrect, not merely redundant.
254
+ */
255
+ readonly tolerant: boolean
256
+ /**
257
+ * `ctx._grammarCoverage` — is THIS parse counting grammar coverage?
258
+ *
259
+ * FIXED FOR THE LIFETIME OF A PARSE, checked the same way `tolerant` was. There
260
+ * are exactly two writers — `createGrammarInstrumentationContext` (coverage.ts),
261
+ * which builds the field into a FRESH context object before any parse, and
262
+ * `functional/run.ts:403`, which installs it on the context it is about to run
263
+ * — and no reader anywhere mutates or clears it, mid-parse or otherwise.
264
+ * `createParseContext` initialises it to `undefined` once, at construction.
265
+ *
266
+ * So it is per-PARSE, unlike `ctx._cstBuf`, which a previous lane proposed
267
+ * keying on and which `beginCstNodeCapture`/`endCstNodeCapture` replace per
268
+ * NODE — that selection would have been incorrect, not merely redundant.
269
+ *
270
+ * An ordinary table has no `OP_COV` rows at all, so this bit selects a DIFFERENT
271
+ * assembly only for a table encoded with a coverage plan. For every other table
272
+ * the two assemblies are identical work and the extra bit costs one cache slot.
273
+ */
274
+ readonly coverage: boolean
275
+ /**
276
+ * `ctx._probe !== undefined` — is THIS parse feeding a completions probe?
277
+ *
278
+ * FIXED FOR THE LIFETIME OF A PARSE, on exactly the evidence `tolerant` is
279
+ * held to. The two writers both install it BEFORE the parse begins and never
280
+ * during — `combinators/grammar.ts:213` (built in `run()`'s prologue when
281
+ * `recover` is set) and `combinators/completions.ts:38`. The single mid-parse
282
+ * mutation is `recovery/scan.ts:29`, which CLEARS it for the duration of a
283
+ * sentinel probe and restores it in `finally`; a sentinel is a
284
+ * `firstSetSentinel` char-class combinator, never an assembled rule, so no
285
+ * piece runs under the cleared value. Even if one did the selection would be
286
+ * the RIGHT one — a sentinel probe must fail fast, and it would get the
287
+ * strict assembly.
288
+ *
289
+ * It is here for `OP_GATE`, which is the only piece that reads it on a
290
+ * SUCCESS path. The six leaf sites read it after their own `return`, on the
291
+ * failure path only, and are deliberately left alone: selecting a second body
292
+ * per literal length to remove a failure-path test would double the runtime's
293
+ * literal bodies to buy nothing measurable.
294
+ */
295
+ readonly probe: boolean
296
+ }
297
+
298
+ /** THE bit packing for an assembly cache key. Six bits, so at most sixty-four
299
+ * ordinary assemblies per table. Host trivia predicates use a host-identity
300
+ * cache in addition to this key because their per-type answer is not one bit.
301
+ *
302
+ * Taken as six booleans rather than as a `RunCfg` because `AssemblyCache.forCtx`
303
+ * runs once per entry invocation and must allocate nothing — it derives these bits
304
+ * from the `ctx` and builds a `RunCfg` only on the miss. That constraint is why the
305
+ * packing was written twice, 2500 lines apart in this file, and a bit added to one
306
+ * copy but not the other is a cache COLLISION: a parse served the wrong assembly.
307
+ * Splitting the packing from the object read satisfies both callers from one place. */
308
+ function cfgKeyOf(hostCst: boolean, trackLines: boolean, tolerant: boolean, coverage: boolean, probe: boolean, hostReadsChildren = true): number {
309
+ return (hostCst ? 1 : 0) | (trackLines ? 2 : 0) | (tolerant ? 4 : 0)
310
+ | (coverage ? 8 : 0) | (probe ? 16 : 0) | (hostReadsChildren ? 0 : 32)
311
+ }
312
+
313
+ /** The cfg key an assembly is cached under. */
314
+ export function cfgKey(c: RunCfg): number {
315
+ return cfgKeyOf(c.hostCst, c.trackLines, c.tolerant, c.coverage, c.probe, c.hostReadsChildren)
316
+ }
317
+
318
+ export type Assembly = {
319
+ /** One entry piece per rule name, already linked. */
320
+ readonly pieces: Readonly<Record<string, Piece>>
321
+ readonly end: () => number
322
+ /**
323
+ * Per-parse reset. What `exec.ts`'s `begin` DECIDED here (`trackLines`, the
324
+ * host mode) is exactly what assembly resolved, so all that is left is
325
+ * clearing the installed scanner and latching the host value.
326
+ */
327
+ readonly begin: (ctx: ParseContext) => void
328
+ /** Close an invocation and restore a suspended re-entrant frame, if any. */
329
+ readonly finish: () => void
330
+ readonly scanSkip: readonly (readonly Combinator<unknown>[])[]
331
+ /**
332
+ * The sites this option set actually REACHED. A strict subset of the table's
333
+ * reachable set whenever an option excludes anything, and the assertion
334
+ * `test/unit/table-assemble.test.ts` makes on that.
335
+ */
336
+ readonly reached: ReadonlySet<number>
337
+ /**
338
+ * WHY THIS ASSEMBLY IS RUNNING CLOSURES, when it is.
339
+ *
340
+ * `undefined` means the emitted engine (`emit-assembly.ts`) built this
341
+ * assembly. A string names the construct it refused. It is a field rather
342
+ * than a log line because a grammar that quietly drops to the closure path
343
+ * is a permanently slow path nobody would ever find — the same failure
344
+ * `encode.ts:1208-1213` refuses to allow for `OP_LIVE`.
345
+ */
346
+ readonly emitRefusal: string | undefined
347
+ }
348
+
349
+ /**
350
+ * Link one resolved table, for one option set, into a graph of closures.
351
+ *
352
+ * ONE walk. Each site is lowered at most once and memoised by its code offset,
353
+ * so a subtree shared by two parents is one piece with two references to it.
354
+ */
355
+ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Assembly {
356
+ const { code, k, fns, cc, fx, disp, dsp, trivia, triviaLabelled } = t
357
+ // The leaf swap is only legal when line tracking is off, and that is a property
358
+ // of the OPTION SET — so it is decided HERE, once, and the scope pieces below
359
+ // are chosen accordingly. `exec.ts` re-tested `FAST` at every scope entry.
360
+ const triviaScan = t.triviaScan
361
+ const swapLegal = !cfg.trackLines
362
+ const hostCst = cfg.hostCst
363
+ /**
364
+ * DOES THIS ASSEMBLY LOWER RECOVERY? Two facts, both known here.
365
+ *
366
+ * `prog.rec === 1` is TABLE DATA — `encodeTable({ recovery })` lays down the
367
+ * inferred sync operands, and their presence is what makes the recovery pieces
368
+ * lowerable at all. It is now always 1, since recovery is always lowered.
369
+ *
370
+ * `cfg.tolerant` is the OPTION half. Recovery being lowered into the table must
371
+ * not make a strict parse pay for it, and the way this design resolves an
372
+ * option is by CHOOSING A PIECE, not by testing a flag inside one. So the
373
+ * tolerant assembly holds the recovery pieces and the strict assembly holds the
374
+ * arity-specialised ones it always held — `RunCfg.tolerant` documents why that
375
+ * is sound. `parseWithErrors` and `completionsAt` set `_tolerant` before the
376
+ * entry runs, so they land on the tolerant assembly; every other parse gets a
377
+ * graph with no recovery in it anywhere.
378
+ *
379
+ * Dormancy is unchanged for the OTHER two engines: the source lowering
380
+ * (`codegen.ts:3153`) and the interpreter (`repeat.ts:163`) still gate on
381
+ * `ctx._tolerant` at runtime, and the recovery pieces below keep their own
382
+ * `_tolerant === true` failure-path gates so `exec.ts` stays the identity
383
+ * reference.
384
+ */
385
+ const REC = prog.rec === 1 && cfg.tolerant
386
+ /**
387
+ * The sync sentinel for a char-class index, built ONCE at assembly.
388
+ *
389
+ * `firstSetSentinel` is the interpreter's own constructor and codegen calls it
390
+ * per publish through `_ctx._rec.sentinel(...)`; the ranges are recoverable from
391
+ * the class spec, so the table builds the identical combinator here and the
392
+ * publish is a slot read. −1 means "no usable sentinel", which is the same
393
+ * answer `firstSetSentinel` gives for an `any`/`empty` first set.
394
+ */
395
+ const sentinels = new Map<number, Combinator<null> | undefined>()
396
+ function sentinelFor(cls: number): Combinator<null> | undefined {
397
+ if (cls < 0) return undefined
398
+ const hit = sentinels.get(cls)
399
+ if (hit !== undefined || sentinels.has(cls)) return hit
400
+ const spec = prog.cc[cls]!
401
+ const ranges = decodeClassSpec(spec)
402
+ const made = firstSetSentinel({ kind: 'ranges', ranges }) ?? undefined
403
+ sentinels.set(cls, made)
404
+ return made
405
+ }
406
+
407
+ /**
408
+ * THE ONE END-POSITION CELL FOR THIS ASSEMBLY (`_pfEnd` in emitted code).
409
+ *
410
+ * Whichever engines this assembly builds — the closure pieces below, the
411
+ * emitted pieces from `emit-assembly.ts`, an `exec.ts` driver — all write
412
+ * this slot, so a piece from one may be called by a piece from another. Per
413
+ * ASSEMBLY rather than per module: two grammars live in one process must not
414
+ * share it. See `cell.ts`.
415
+ */
416
+ const EC = newEndCell()
417
+
418
+ /**
419
+ * The INSTALLED trivia scanner for the scope currently running.
420
+ *
421
+ * Still a slot rather than a capture because `OP_SCOPE` and `OP_TOKEN` change
422
+ * it DURING a parse — it is runtime state, not configuration. What assembly
423
+ * removed is the per-scope re-test of `FAST` and `triviaLabelled`: which
424
+ * scanner a scope installs is now a `const` in that scope's piece.
425
+ */
426
+ let SCAN: FastTriviaScanner | null = null
427
+
428
+ const memo = new Map<number, Piece>()
429
+ const reached = new Set<number>()
430
+
431
+ function pushLeaf(ctx: ParseContext, value: string, s: number, e: number): void {
432
+ const lf: Leaf = { _tag: 'leaf', value, span: { start: s, end: e } }
433
+ pushCstLeaf(ctx, lf)
434
+ }
435
+
436
+ /** Mirrors `exec.ts`'s `rollbackNeeded` — a runtime question about live sinks. */
437
+ function rollbackNeeded(ctx: ParseContext): boolean {
438
+ return ctx._cstBuf !== undefined
439
+ || ctx._cstLeaves !== undefined
440
+ || ctx._cstRawChildren !== undefined
441
+ || ctx._cstTriviaLog !== undefined
442
+ || ctx._fields !== undefined
443
+ || ctx._errors !== undefined
444
+ || ctx._triviaLog !== undefined
445
+ || ctx._rootTriviaLog !== undefined
446
+ }
447
+
448
+ /**
449
+ * The three CST lengths of a rollback mark, as out-slots — see `markCst`.
450
+ *
451
+ * Slots for the same reason `EC.e` and `TERMV` are slots: a `{ raw, tl, lv }`
452
+ * return would be an allocation per mark, and marks are the single most
453
+ * executed thing in the driver.
454
+ *
455
+ * READ THEM IMMEDIATELY. They are only valid until the next `markCst`, which
456
+ * in practice means the three lines under the call.
457
+ */
458
+ let MRAW = 0
459
+ let MTL = 0
460
+ let MLV = 0
461
+
462
+ /**
463
+ * Take a rollback mark's three CST lengths with ONE `ctx._cstBuf` load, and
464
+ * answer `rollbackNeeded` on the way past.
465
+ *
466
+ * Every backtrackable construct marks before trying something it may roll
467
+ * back, and `nextTerm` alone reaches here >200,000 times per parse of
468
+ * `benchmark.less`. The previous shape called `rollbackNeeded` and then
469
+ * `cstRawLen`/`cstTlLen`/`cstLeavesLen` — four calls, three of them
470
+ * CROSS-MODULE into `../cst/capture-buffer.ts` and each independently
471
+ * re-loading `ctx._cstBuf`. V8 inlined the local `rollbackNeeded` and did NOT
472
+ * inline the other three: they cost 1.0% + 0.7% + 0.9% of the assembled
473
+ * parse in SELF time. Source lowering emits the same mark as inline direct
474
+ * loads, which is why it never paid this.
475
+ *
476
+ * The hoist is valid at EXACTLY this granularity and no wider. `_cstBuf` is
477
+ * per-NODE state, not per-parse config: `beginCstNodeCapture` sets it and
478
+ * `endCstNodeCapture` restores it, so it changes DURING a parse. The three
479
+ * lengths are read here at one instant with nothing running between them, so
480
+ * one load serves all three. It must NOT be cached in a piece or held across
481
+ * the child call — between the mark and the rollback the child RUNS and may
482
+ * replace or clear the buffer, so `rollbackCstCaptureAt` re-loads. A buffer
483
+ * held across that would roll back against a stale object, which does not
484
+ * throw: it silently keeps or drops CST children.
485
+ *
486
+ * `capture-buffer.ts`'s helpers stay exported and are still what `exec.ts`
487
+ * calls. `exec.ts` is the identity reference a divergence is bisected
488
+ * against; it is not inlined there.
489
+ */
490
+ function markCst(ctx: ParseContext): boolean {
491
+ const b = ctx._cstBuf
492
+ if (b !== undefined) {
493
+ // `cstRawLen`/`cstLeavesLen` collapse the lazy single-entry form; `cstTlLen`
494
+ // has no single form. `rollbackNeeded` short-circuits true on a live buffer,
495
+ // so the other seven sinks are not consulted — as before.
496
+ const raw = b.raw
497
+ MRAW = raw !== undefined ? raw.length : b.rawSingle !== undefined ? 1 : 0
498
+ const ch = b.ch
499
+ MLV = ch !== undefined ? ch.length : b.single !== undefined ? 1 : 0
500
+ const tl = b.tl
501
+ MTL = tl !== undefined ? tl.length : 0
502
+ return true
503
+ }
504
+ if (ctx._cstLeaves !== undefined
505
+ || ctx._cstRawChildren !== undefined
506
+ || ctx._cstTriviaLog !== undefined
507
+ || ctx._fields !== undefined
508
+ || ctx._errors !== undefined
509
+ || ctx._triviaLog !== undefined
510
+ || ctx._rootTriviaLog !== undefined) {
511
+ MRAW = ctx._cstRawChildren?.length ?? 0
512
+ MTL = ctx._cstTriviaLog?.length ?? 0
513
+ MLV = ctx._cstLeaves?.length ?? 0
514
+ return true
515
+ }
516
+ MRAW = 0
517
+ MTL = 0
518
+ MLV = 0
519
+ return false
520
+ }
521
+
522
+ /**
523
+ * Wrap a scope piece in its ROOT-CAPTURE POLICY, when it has one.
524
+ *
525
+ * Both bits are TABLE DATA, so the wrapping happens here and the common scope
526
+ * — every scope in every grammar that never asked for either — gets the bare
527
+ * piece back and pays nothing. `parser()` answers the same two questions in
528
+ * `combinators/grammar.ts`; the table answered neither.
529
+ */
530
+ function scopeRootPolicy(piece: Piece, flags: number): Piece {
531
+ let out = piece
532
+ if ((flags & 1) !== 0) {
533
+ // `grammar.ts:141` on a per-scope ctx COPY. One shared ctx here, so restore.
534
+ const inner = out
535
+ out = (input, pos, ctx) => {
536
+ const saved = ctx._rootTriviaCapture
537
+ ctx._rootTriviaCapture = false
538
+ const v = inner(input, pos, ctx)
539
+ ctx._rootTriviaCapture = saved
540
+ return v
541
+ }
542
+ }
543
+ if ((flags & 2) !== 0) {
544
+ const inner = out
545
+ out = (input, pos, ctx) => {
546
+ refuseUnclassifiedRootScope(ctx._rootTriviaStrictScopes)
547
+ return inner(input, pos, ctx)
548
+ }
549
+ }
550
+ return out
551
+ }
552
+
553
+ function skipTrivia(input: string, cur: number, ctx: ParseContext): number {
554
+ const s = SCAN
555
+ if (s !== null
556
+ && ctx._triviaLog === undefined
557
+ && !(ctx.captureTrivia === true && (ctx._cstBuf !== undefined || ctx._cstTriviaLog !== undefined))) {
558
+ return s(input, cur)
559
+ }
560
+ // CAPTURE IS NOT A REASON TO LEAVE THE SCANNER — see `skipTriviaScanned`.
561
+ // Appended rather than folded into the test above so the non-capturing path
562
+ // keeps the exact branch it had.
563
+ if (s !== null) return skipTriviaScanned(s, input, cur, ctx)
564
+ if (needsDeferredTriviaCommit(ctx)) {
565
+ const scan = scanTrivia(input, cur, ctx)
566
+ scan.commit()
567
+ return scan.end
568
+ }
569
+ return advanceTrivia(input, cur, ctx)
570
+ }
571
+
572
+ /**
573
+ * The value the last `nextTerm` produced.
574
+ *
575
+ * A second out-parameter slot beside `EC.e`, for the same reason `EC.e` is one:
576
+ * a `{ value, end }` pair would be an allocation per sequence TERM, which is
577
+ * the single most executed thing in any grammar here.
578
+ */
579
+ let TERMV: unknown
580
+
581
+ /**
582
+ * THE HOST, read ONCE PER PARSE.
583
+ *
584
+ * `ctx.build` is fixed by `run()` before the entry is called and cannot change
585
+ * during a parse, so it is configuration — but unlike `hostCst` it is a VALUE
586
+ * assembly cannot bake in, because two parses with the same option set can
587
+ * carry two different host functions. So it is hoisted to the boundary instead:
588
+ * `begin` reads it once, and the node piece reads this slot rather than going
589
+ * back to `ctx` on each of the 145,512 nodes `benchmark.less` builds.
590
+ *
591
+ * INV-6 in `scripts/check-invariants.mjs` is what caught this: the first
592
+ * version of the node piece opened with `const host = ctx.build`, which is a
593
+ * per-parse config read on the hottest non-terminal in the grammar.
594
+ */
595
+ let HOST: ParseContext['build']
596
+
597
+ /**
598
+ * THE COLLECTOR THIS PARSE IS COUNTING INTO, latched at the boundary exactly as
599
+ * `HOST` is and for the same reason: the assembly is SELECTED by whether one is
600
+ * present (`RunCfg.coverage`), but WHICH one it is is a per-parse VALUE, and two
601
+ * parses with the same option set can carry two different collectors.
602
+ *
603
+ * So no `OP_COV` piece reads `ctx` at all — it calls this slot. The initial
604
+ * no-op exists because `begin` is what installs the real function; a counter
605
+ * piece is only ever reachable from the coverage assembly, whose `begin` always
606
+ * finds one on the context that selected it.
607
+ */
608
+ let COV: (id: string) => void = NO_COVERAGE
609
+
610
+ /**
611
+ * Assembly-local slots are scalar on the ordinary path. User host/reducer
612
+ * code can synchronously invoke this same table map, though, so suspend them
613
+ * only for that cold nested case rather than letting the inner parse poison
614
+ * its caller's scanner, host, coverage sink, or end cell.
615
+ */
616
+ const frames: Array<{
617
+ scan: FastTriviaScanner | null
618
+ host: ParseContext['build']
619
+ coverage: (id: string) => void
620
+ end: number
621
+ }> = []
622
+ let depth = 0
623
+
624
+ /**
625
+ * A NON-FIRST sequence term: skip the installed trivia, run the child, and
626
+ * unrecord that trivia if the child matched nothing.
627
+ *
628
+ * Returns the new cursor, or −1 on failure; the term's value lands in `TERMV`.
629
+ * Split out of the loop because the FIRST term of a sequence never has trivia
630
+ * before it — that is a property of the POSITION, known at assembly, so the
631
+ * unrolled pieces call the first child directly and only reach here from term
632
+ * two onward.
633
+ *
634
+ * Small and monomorphic on purpose: every unrolled sequence piece calls this
635
+ * one function, so its call sites stay single-shape and TurboFan can inline it
636
+ * into them.
637
+ */
638
+ function nextTerm(child: Piece, input: string, cur: number, ctx: ParseContext): number {
639
+ if (ctx.trivia === undefined) {
640
+ const v = child(input, cur, ctx)
641
+ if (v === FAIL) return -1
642
+ TERMV = v
643
+ return EC.e
644
+ }
645
+ // The overwhelmingly common AST/value parse has no live rollback sink. Do
646
+ // not manufacture zero marks and then copy seven zero lengths for it: with
647
+ // no buffer, leaves, fields, diagnostics, or trivia logs, `skipTrivia()`
648
+ // cannot create anything that a failed following term would need to undo.
649
+ // This is exactly `markCst(ctx) === false`, spelled here so that the hot
650
+ // success path has neither its scalar writes nor its failure-only locals.
651
+ if (ctx._cstBuf === undefined
652
+ && ctx._cstLeaves === undefined
653
+ && ctx._cstRawChildren === undefined
654
+ && ctx._cstTriviaLog === undefined
655
+ && ctx._fields === undefined
656
+ && ctx._errors === undefined
657
+ && ctx._triviaLog === undefined
658
+ && ctx._rootTriviaLog === undefined) {
659
+ const scanEnd = skipTrivia(input, cur, ctx)
660
+ const v = child(input, scanEnd, ctx)
661
+ if (v === FAIL) return -1
662
+ TERMV = v
663
+ return EC.e > scanEnd ? EC.e : cur
664
+ }
665
+ // SCALAR MARKS — no per-term mark object, as `exec.ts` established.
666
+ const need = rollbackNeeded(ctx)
667
+ const mTl = need ? cstTlLen(ctx) : 0
668
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
669
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
670
+ const scanEnd = skipTrivia(input, cur, ctx)
671
+ const scanTl = need ? cstTlLen(ctx) : 0
672
+ const scanLog = need ? ctx._triviaLog?.length ?? 0 : 0
673
+ const scanRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
674
+ const v = child(input, scanEnd, ctx)
675
+ if (v === FAIL) return -1
676
+ TERMV = v
677
+ if (EC.e > scanEnd) return EC.e
678
+ // The term matched nothing, so the trivia in front of it was never consumed
679
+ // by anything — unrecord it and leave the cursor where it was.
680
+ if (need) rollbackScannedTriviaAt(ctx, mTl, scanTl, mLog, scanLog, mRoot, scanRoot)
681
+ return cur
682
+ }
683
+
684
+ function committed(c: ParseContext): boolean {
685
+ return c._fc === true
686
+ }
687
+
688
+ /**
689
+ * Accumulate one failed choice arm's expected set, in arm order.
690
+ *
691
+ * `choice.ts:167` is `expected.push(...result.expected)` per failed arm, and the
692
+ * accumulation — not the static union of the arms' OPENERS — is what a failing
693
+ * ordered choice reports. A left-factored or shared-prefix choice whose arms all
694
+ * failed PAST their common opener reported that opener alone: `['/::?/']` where
695
+ * the interpreter says `['"before"', '"hover"']`.
696
+ *
697
+ * Returns the accumulator so the common one-failing-arm case borrows the arm's
698
+ * own array (`slice` once) instead of building one per choice execution the way
699
+ * the interpreter's eager `const expected: string[] = []` does.
700
+ */
701
+ function accSet(ax: readonly string[] | undefined, acc: string[] | undefined): string[] | undefined {
702
+ if (ax === undefined || ax.length === 0) return acc
703
+ if (acc === undefined) return ax.slice()
704
+ for (const s of ax) acc.push(s)
705
+ return acc
706
+ }
707
+
708
+
709
+
710
+ function trackLinesInto(ctx: ParseContext, input: string, end: number): void {
711
+ const from = ctx._lineScannedTo ?? 0
712
+ if (end <= from) return
713
+ const starts = ctx._lineStarts
714
+ if (starts === undefined) return
715
+ for (let i = from; i < end; i++) if (input.charCodeAt(i) === 10) starts.push(i + 1)
716
+ ctx._lineScannedTo = end
717
+ }
718
+
719
+ /* ── the link step ──────────────────────────────────────────────────────── */
720
+
721
+ /**
722
+ * Lower one site, memoised.
723
+ *
724
+ * CYCLES. The encoder emits children before parents, so the only back-edges are
725
+ * the `OP_RULE` trampolines it patches for recursion (`encode.ts:228-241`). A
726
+ * site already in flight gets a STUB that forwards through a slot patched when
727
+ * the real piece exists, so recursive rules still hold direct references and
728
+ * nothing falls back to an index lookup. The stub costs one call, only on a
729
+ * genuine back-edge — 4 reachable `OP_RULE` sites in the less table.
730
+ */
731
+ const inFlight = new Map<number, { fwd: Piece; set: (p: Piece) => void }>()
732
+
733
+ function link(ip: number): Piece {
734
+ const done = memo.get(ip)
735
+ if (done !== undefined) return done
736
+ const flight = inFlight.get(ip)
737
+ if (flight !== undefined) return flight.fwd
738
+
739
+ let target: Piece | undefined
740
+ const fwd: Piece = (input, pos, ctx) => target!(input, pos, ctx)
741
+ const holder = { fwd, set: (p: Piece) => { target = p } }
742
+ inFlight.set(ip, holder)
743
+
744
+ const piece = lower(ip)
745
+
746
+ inFlight.delete(ip)
747
+ holder.set(piece)
748
+ memo.set(ip, piece)
749
+ reached.add(ip)
750
+ return piece
751
+ }
752
+
753
+ function lower(ip: number): Piece {
754
+ const op = code[ip]
755
+ switch (op) {
756
+ /* ── terminals ───────────────────────────────────────────────────────── */
757
+
758
+ case OP_LIT: {
759
+ const s = k[code[ip + 1]!] as string
760
+ const len = s.length
761
+ const xf = fx[code[ip + 2]!] as string[]
762
+ // LENGTH IS TABLE DATA, so the COMPARE is chosen here rather than
763
+ // delegated to a builtin that has to rediscover it. See `litBodyNote`.
764
+ if (len === 1) {
765
+ const c0 = s.charCodeAt(0)
766
+ return (input, pos, ctx) => {
767
+ if (input.charCodeAt(pos) === c0) {
768
+ const e = pos + 1
769
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
770
+ EC.e = e
771
+ return s
772
+ }
773
+ ctx._fe = pos; ctx._fx = xf
774
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
775
+ return FAIL
776
+ }
777
+ }
778
+ if (len === 2) {
779
+ const c0 = s.charCodeAt(0), c1 = s.charCodeAt(1)
780
+ return (input, pos, ctx) => {
781
+ if (input.charCodeAt(pos) === c0 && input.charCodeAt(pos + 1) === c1) {
782
+ const e = pos + 2
783
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
784
+ EC.e = e
785
+ return s
786
+ }
787
+ ctx._fe = pos; ctx._fx = xf
788
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
789
+ return FAIL
790
+ }
791
+ }
792
+ if (len === 3) {
793
+ const c0 = s.charCodeAt(0), c1 = s.charCodeAt(1), c2 = s.charCodeAt(2)
794
+ return (input, pos, ctx) => {
795
+ if (input.charCodeAt(pos) === c0 && input.charCodeAt(pos + 1) === c1
796
+ && input.charCodeAt(pos + 2) === c2) {
797
+ const e = pos + 3
798
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
799
+ EC.e = e
800
+ return s
801
+ }
802
+ ctx._fe = pos; ctx._fx = xf
803
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
804
+ return FAIL
805
+ }
806
+ }
807
+ return (input, pos, ctx) => {
808
+ if (input.startsWith(s, pos)) {
809
+ const e = pos + len
810
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
811
+ EC.e = e
812
+ return s
813
+ }
814
+ ctx._fe = pos; ctx._fx = xf
815
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
816
+ return FAIL
817
+ }
818
+ }
819
+
820
+ case OP_LIT_TRACK: {
821
+ const s = k[code[ip + 1]!] as string
822
+ const len = s.length
823
+ const xf = fx[code[ip + 2]!] as string[]
824
+ if (len === 1) {
825
+ const c0 = s.charCodeAt(0)
826
+ return (input, pos, ctx) => {
827
+ if (input.charCodeAt(pos) === c0) {
828
+ const e = pos + 1
829
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
830
+ trackLinesInto(ctx, input, e)
831
+ EC.e = e
832
+ return s
833
+ }
834
+ ctx._fe = pos; ctx._fx = xf
835
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
836
+ return FAIL
837
+ }
838
+ }
839
+ if (len === 2) {
840
+ const c0 = s.charCodeAt(0), c1 = s.charCodeAt(1)
841
+ return (input, pos, ctx) => {
842
+ if (input.charCodeAt(pos) === c0 && input.charCodeAt(pos + 1) === c1) {
843
+ const e = pos + 2
844
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
845
+ trackLinesInto(ctx, input, e)
846
+ EC.e = e
847
+ return s
848
+ }
849
+ ctx._fe = pos; ctx._fx = xf
850
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
851
+ return FAIL
852
+ }
853
+ }
854
+ if (len === 3) {
855
+ const c0 = s.charCodeAt(0), c1 = s.charCodeAt(1), c2 = s.charCodeAt(2)
856
+ return (input, pos, ctx) => {
857
+ if (input.charCodeAt(pos) === c0 && input.charCodeAt(pos + 1) === c1
858
+ && input.charCodeAt(pos + 2) === c2) {
859
+ const e = pos + 3
860
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
861
+ trackLinesInto(ctx, input, e)
862
+ EC.e = e
863
+ return s
864
+ }
865
+ ctx._fe = pos; ctx._fx = xf
866
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
867
+ return FAIL
868
+ }
869
+ }
870
+ return (input, pos, ctx) => {
871
+ if (input.startsWith(s, pos)) {
872
+ const e = pos + len
873
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, s, pos, e)
874
+ trackLinesInto(ctx, input, e)
875
+ EC.e = e
876
+ return s
877
+ }
878
+ ctx._fe = pos; ctx._fx = xf
879
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
880
+ return FAIL
881
+ }
882
+ }
883
+
884
+ case OP_RX: {
885
+ const re = k[code[ip + 1]!] as RegExp
886
+ const xf = fx[code[ip + 2]!] as string[]
887
+ return (input, pos, ctx) => {
888
+ re.lastIndex = pos
889
+ // `regex()` rows are sticky and expose only their whole match. `exec()`
890
+ // materialises a RegExpResult array just to read m[0]; `test()` performs
891
+ // the identical sticky match and leaves its end in `lastIndex`. This is
892
+ // the exact primitive the emitted table body uses, so the compact closure
893
+ // path must not retain an allocation on every regex token.
894
+ if (re.test(input)) {
895
+ const e = re.lastIndex
896
+ const v = input.slice(pos, e)
897
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, v, pos, e)
898
+ EC.e = e
899
+ return v
900
+ }
901
+ ctx._fe = pos; ctx._fx = xf
902
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
903
+ return FAIL
904
+ }
905
+ }
906
+
907
+ case OP_RX_TRACK: {
908
+ const re = k[code[ip + 1]!] as RegExp
909
+ const xf = fx[code[ip + 2]!] as string[]
910
+ return (input, pos, ctx) => {
911
+ re.lastIndex = pos
912
+ if (re.test(input)) {
913
+ const e = re.lastIndex
914
+ const v = input.slice(pos, e)
915
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, v, pos, e)
916
+ trackLinesInto(ctx, input, e)
917
+ EC.e = e
918
+ return v
919
+ }
920
+ ctx._fe = pos; ctx._fx = xf
921
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
922
+ return FAIL
923
+ }
924
+ }
925
+
926
+ case OP_LIT_CI:
927
+ case OP_LIT_CI_TRACK: {
928
+ const s = k[code[ip + 1]!] as string
929
+ const len = s.length
930
+ const xf = fx[code[ip + 2]!] as string[]
931
+ // The track/no-track choice was a per-row `code[ip] === OP_LIT_CI_TRACK`
932
+ // re-read in `exec.ts`. It is a property of the ROW, so it is resolved here.
933
+ // ASCII FOLD, one char at a time, WITHOUT the slice.
934
+ //
935
+ // `asciiFoldEq(input.slice(pos, e), s)` allocated a string on every
936
+ // ATTEMPT — including the failures, which is where a case-insensitive
937
+ // literal spends most of its executions (it is nearly always an arm of a
938
+ // choice). The chain compares in place and allocates only the value it
939
+ // is about to hand back, so the failure path allocates nothing at all.
940
+ //
941
+ // The semantics are `asciiFoldEq`'s, exactly: fold only A-Z (65-90) on
942
+ // BOTH sides, compare code unit by code unit. Past end-of-input
943
+ // `charCodeAt` is NaN, which folds to NaN and compares unequal — the
944
+ // same answer `asciiFoldEq` gave via its short-slice length test.
945
+ const fold = (c: number): number => (c >= 65 && c <= 90 ? c + 32 : c)
946
+ const foldedLit: number[] = []
947
+ for (let i = 0; i < len; i++) foldedLit.push(fold(s.charCodeAt(i)))
948
+ const matchesFolded = (input: string, pos: number): boolean => {
949
+ for (let i = 0; i < len; i++) {
950
+ if (fold(input.charCodeAt(pos + i)) !== foldedLit[i]!) return false
951
+ }
952
+ return true
953
+ }
954
+ if (op === OP_LIT_CI_TRACK) {
955
+ return (input, pos, ctx) => {
956
+ const e = pos + len
957
+ if (matchesFolded(input, pos)) {
958
+ // Yields the INPUT's casing (`literal.ts:86`), not the literal's.
959
+ const matched = input.slice(pos, e)
960
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, matched, pos, e)
961
+ trackLinesInto(ctx, input, e)
962
+ EC.e = e
963
+ return matched
964
+ }
965
+ ctx._fe = pos; ctx._fx = xf
966
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
967
+ return FAIL
968
+ }
969
+ }
970
+ return (input, pos, ctx) => {
971
+ const e = pos + len
972
+ if (matchesFolded(input, pos)) {
973
+ // Yields the INPUT's casing (`literal.ts:86`), not the literal's.
974
+ const matched = input.slice(pos, e)
975
+ if (cstCaptureActive(ctx)) pushLeaf(ctx, matched, pos, e)
976
+ EC.e = e
977
+ return matched
978
+ }
979
+ ctx._fe = pos; ctx._fx = xf
980
+ if (ctx._probe !== undefined) failAt(ctx, xf, pos)
981
+ return FAIL
982
+ }
983
+ }
984
+
985
+ case OP_EMPTY:
986
+ return (_input, pos) => { EC.e = pos; return '' }
987
+
988
+ /* ── transparent / structural ────────────────────────────────────────── */
989
+
990
+ case OP_GUARD: {
991
+ const pred = fns[code[ip + 1]!] as (s: unknown) => boolean
992
+ const xf = fx[code[ip + 2]!] as string[]
993
+ return (_input, pos, ctx) => {
994
+ if (pred(ctx.state)) { EC.e = pos; return null }
995
+ ctx._fe = pos; ctx._fx = xf
996
+ return FAIL
997
+ }
998
+ }
999
+
1000
+ case OP_ADJ: {
1001
+ // A row reached in its own right has NO boundary to test — the SEQ
1002
+ // pieces intercept the assertion wherever the question is answerable.
1003
+ // So this is the misuse the interpreter refuses (a bare choice arm, a
1004
+ // `node()` body, a repeat item), refused with the same sentence. Built
1005
+ // The POLARITY is bound here; the error itself is built where it is
1006
+ // thrown, so its stack points at the parse and not at assembly.
1007
+ const polarity = code[ip + 1] === 1 ? 'notAdjacent' : 'adjacent'
1008
+ return () => { throw adjacencyMisuse(polarity) }
1009
+ }
1010
+
1011
+ case OP_GATE: {
1012
+ const cls = cc[code[ip + 1]!]!
1013
+ const xf = fx[code[ip + 3]!] as string[]
1014
+ const child = link(code[ip + 2]!)
1015
+ // SKIPPED ENTIRELY UNDER A PROBE / TOLERANT RECOVERY, exactly as the
1016
+ // interpreter skips it (`node.ts:239`, `attempt.ts:22`): the swallowed
1017
+ // inner failure is what feeds a completions probe, so a fail-fast that
1018
+ // never enters the body silently narrows `completionsAt` to the openers
1019
+ // it could see.
1020
+ //
1021
+ // Both halves of that condition are per-PARSE options, so the gate is
1022
+ // resolved HERE. Under either the gate is a no-op that forwards to the
1023
+ // child, and an assembly that forwards is better expressed as the child
1024
+ // itself: those assemblies drop the closure AND its call frame. The
1025
+ // strict assembly gets a body whose only test is the `classHas` the gate
1026
+ // exists for — the success path no longer reads `_probe` or `_tolerant`
1027
+ // at all, which is two context loads per gate execution removed (css has
1028
+ // 13 `GATE` rows, less 22, json none).
1029
+ if (cfg.tolerant || cfg.probe) return child
1030
+ return (input, pos, ctx) => {
1031
+ if (!classHas(cls, lead(input, pos))) {
1032
+ ctx._fe = pos; ctx._fx = xf
1033
+ return FAIL
1034
+ }
1035
+ return child(input, pos, ctx)
1036
+ }
1037
+ }
1038
+
1039
+ case OP_LABEL: {
1040
+ const child = link(code[ip + 1]!)
1041
+ const xf = fx[code[ip + 2]!] as string[]
1042
+ return (input, pos, ctx) => {
1043
+ const v = child(input, pos, ctx)
1044
+ // `_fe` UNTOUCHED — `map.ts:84` keeps `result.span`.
1045
+ if (v === FAIL) ctx._fx = xf
1046
+ return v
1047
+ }
1048
+ }
1049
+
1050
+ case OP_COV: {
1051
+ const child = link(code[ip + 1]!)
1052
+ // NOT AN OPTION TEST IN A PIECE BODY — the option is resolved HERE, by
1053
+ // choosing which of three pieces to return. `cfg.coverage` false hands
1054
+ // back the child itself, so a coverage-encoded table run without a
1055
+ // collector is the same graph, with the same call depth, as an ordinary
1056
+ // one: the counter rows vanish at link time rather than short-circuiting
1057
+ // at parse time.
1058
+ if (!cfg.coverage) return child
1059
+ const def = prog.cov?.[code[ip + 2]!]
1060
+ // An `OP_COV` row with no pool behind it is a table whose `cov` field was
1061
+ // dropped somewhere between encode and load — the emitter, the compact
1062
+ // wire form, or a fold. Say so, rather than counting `undefined` as an id
1063
+ // and reporting a full denominator with no hits in it.
1064
+ if (def === undefined) {
1065
+ throw new TypeError(
1066
+ `assemble: OP_COV at ${ip} indexes coverage definition ${code[ip + 2]}, but this program `
1067
+ + 'carries no `cov` pool. The counter rows and the definition pool are one artifact.',
1068
+ )
1069
+ }
1070
+ const id = def[0]
1071
+ // ENTRY vs SUCCESS, decided once. `EC.e` is deliberately untouched on both
1072
+ // paths: the counter runs before the child (entry) or after the child's
1073
+ // value is already in hand (success), so nothing observes a stale end.
1074
+ if (code[ip + 3] === 0) {
1075
+ return (input, pos, ctx) => {
1076
+ COV(id)
1077
+ return child(input, pos, ctx)
1078
+ }
1079
+ }
1080
+ return (input, pos, ctx) => {
1081
+ const v = child(input, pos, ctx)
1082
+ if (v !== FAIL) COV(id)
1083
+ return v
1084
+ }
1085
+ }
1086
+
1087
+ case OP_RULE:
1088
+ // Pure indirection in the tape; in the assembled graph the parent simply
1089
+ // holds the target's piece. The trampoline row disappears entirely — this
1090
+ // is the only place a back-edge can occur, and `link` handles it.
1091
+ return link(code[ip + 1]!)
1092
+
1093
+ case OP_LIVE: {
1094
+ // A HAND-WRITTEN COMBINATOR, run through its own `.parse` — mirrors
1095
+ // codegen's `emitRuntimeFallback`. Its result IS the interpreter's, so
1096
+ // `expected`/`span` propagate verbatim and `committed` is copied onto the
1097
+ // ctx bit both drivers use for the cut.
1098
+ const c = fns[code[ip + 1]!] as Combinator<unknown>
1099
+ return (input, pos, ctx) => {
1100
+ const r = c.parse(input, pos, ctx)
1101
+ if (!r.ok) {
1102
+ ctx._fe = r.span.start
1103
+ ctx._fx = r.expected
1104
+ ctx._fc = r.committed === true
1105
+ return FAIL
1106
+ }
1107
+ EC.e = r.span.end
1108
+ return r.value
1109
+ }
1110
+ }
1111
+
1112
+ case OP_EXPECT: {
1113
+ const child = link(code[ip + 1]!)
1114
+ const xf = fx[code[ip + 2]!] as string[]
1115
+ // Line tracking is table data (`OP_EXPECT` has no tracked opcode variant).
1116
+ // Use the same helper as tracked node/root spans so an expect() recovery
1117
+ // diagnostic carries the interpreter's line/column contract in both
1118
+ // table engines. Do not use the incoming ctx bit: a grammar scope can
1119
+ // declare tracking even when the outer run context begins untracked.
1120
+ const expectSpan = (ctx: ParseContext, pos: number) =>
1121
+ prog.lines === 1 ? spanLines(ctx, pos, pos) : { start: pos, end: pos }
1122
+ // TOLERANT `expect()` EMBEDS ITS ERROR IN THE TREE, not only in the flat
1123
+ // `_errors` side-channel (`combinators/expect.ts:150`, and codegen's
1124
+ // `_ctx._rec.capture` at codegen.ts:4470) — so a tree walk finds every
1125
+ // diagnostic and the node survives incremental subtree reuse. The table
1126
+ // pushed to `_errors` and stopped, so a tolerant CST parse was missing the
1127
+ // `parseError` child the other two engines both produce.
1128
+ if (REC) {
1129
+ return (input, pos, ctx) => {
1130
+ const v = child(input, pos, ctx)
1131
+ if (v !== FAIL) return v
1132
+ const span = expectSpan(ctx, pos)
1133
+ const err = { _tag: 'parseError' as const, span, expected: xf }
1134
+ ctx._errors?.push(err)
1135
+ if (ctx._tolerant === true) captureError(ctx, err)
1136
+ // A RECOVERED FAILURE IS NO LONGER A FAILURE, so the commit bit the
1137
+ // child raised must not survive it. `_fc` is ctx-global for both table
1138
+ // drivers; the interpreter carries commitment on the RESULT object, so
1139
+ // returning a success drops it structurally and it has no bit to clear.
1140
+ // Leaving it set let a committed `dispatch`-tail failure that expect()
1141
+ // had already recovered CUT an enclosing choice, so the table rejected
1142
+ // input the interpreter accepts. Codegen has always cleared it
1143
+ // (`emitExpect`, `_ctx._fc = false`).
1144
+ ctx._fc = false
1145
+ EC.e = pos
1146
+ return err
1147
+ }
1148
+ }
1149
+ return (input, pos, ctx) => {
1150
+ const v = child(input, pos, ctx)
1151
+ if (v !== FAIL) return v
1152
+ const span = expectSpan(ctx, pos)
1153
+ const err = { _tag: 'parseError' as const, span, expected: xf }
1154
+ ctx._errors?.push(err)
1155
+ ctx._fc = false
1156
+ EC.e = pos
1157
+ return err
1158
+ }
1159
+ }
1160
+
1161
+ case OP_FIELD: {
1162
+ const name = k[code[ip + 1]!] as string
1163
+ const child = link(code[ip + 2]!)
1164
+ return (input, pos, ctx) => {
1165
+ const v = child(input, pos, ctx)
1166
+ if (v === FAIL) return FAIL
1167
+ ctx._fields?.push({ name, value: v, span: { start: pos, end: EC.e } })
1168
+ return v
1169
+ }
1170
+ }
1171
+
1172
+ case OP_XFORM: {
1173
+ const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
1174
+ const child = link(code[ip + 2]!)
1175
+ return (input, pos, ctx) => {
1176
+ const v = child(input, pos, ctx)
1177
+ if (v === FAIL) return FAIL
1178
+ return fn(v, { start: pos, end: EC.e })
1179
+ }
1180
+ }
1181
+
1182
+ case OP_SCAN: {
1183
+ // The scan POOL is built from subtrees, so it is linked after the graph
1184
+ // it references — a scan piece cannot capture its combinator at lowering
1185
+ // time without a cycle. It binds on first execution instead: one null
1186
+ // check, on 3 sites reached 6 times per parse of `benchmark.less`.
1187
+ const si = code[ip + 1]!
1188
+ let bound: Combinator<unknown> | undefined
1189
+ return (input, pos, ctx) => {
1190
+ const c = bound ?? (bound = scans[si]!)
1191
+ const r = c.parse(input, pos, ctx)
1192
+ if (!r.ok) {
1193
+ ctx._fe = r.span.start
1194
+ ctx._fx = (r.expected ?? EMPTY_FX) as string[]
1195
+ return FAIL
1196
+ }
1197
+ EC.e = r.span.end
1198
+ return r.value
1199
+ }
1200
+ }
1201
+
1202
+ case OP_WITHCTX: {
1203
+ // `extra` and the child are both bound HERE; the piece only swaps a field.
1204
+ const extra = k[code[ip + 1]!]
1205
+ const child = link(code[ip + 2]!)
1206
+ return (input, pos, ctx) => {
1207
+ const saved = ctx.state
1208
+ ctx.state = extra
1209
+ try { return child(input, pos, ctx) }
1210
+ finally { ctx.state = saved }
1211
+ }
1212
+ }
1213
+
1214
+ case OP_SCOPE:
1215
+ case OP_SCOPE_CAP:
1216
+ case OP_SCOPE_PLAIN: {
1217
+ const ki = code[ip + 1]!
1218
+ const scopeTrivia = ki < 0 ? undefined : (trivia[ki] as ParseContext['trivia'])
1219
+ const scopeLabels = scopeTrivia?._meta.triviaKindLabels
1220
+ // THE SWAP, RESOLVED AT ASSEMBLY. `exec.ts` computed
1221
+ // `FAST && ki >= 0 && !triviaLabelled[ki]` on every scope entry; `FAST` is
1222
+ // `!trackLines`, an option, and the other two are table data. All three are
1223
+ // known here, so the scope piece holds the scanner it installs as a `const`.
1224
+ const scanFor: FastTriviaScanner | null =
1225
+ swapLegal && ki >= 0 && !triviaLabelled[ki]! ? triviaScan[ki]! : null
1226
+ const child = link(code[ip + 2]!)
1227
+ // TWO PIECES, SELECTED HERE — not one piece that tests the opcode. Capture
1228
+ // is fixed for the whole parse, so `code[ip] === OP_SCOPE_CAP` is an
1229
+ // assembly-time fact and reading it per scope entry would be exactly the
1230
+ // config test INV-6 forbids.
1231
+ //
1232
+ // Both RESTORE rather than clear: capture is an OR with the enclosing
1233
+ // context (`grammar.ts:129`), so an inner scope must not switch an outer
1234
+ // capture off.
1235
+ const scopePiece: Piece = code[ip] === OP_SCOPE_CAP
1236
+ ? (input, pos, ctx) => {
1237
+ const saved = ctx.trivia
1238
+ const savedLabels = ctx.triviaKindLabels
1239
+ const savedScan = SCAN
1240
+ const savedCap = ctx.captureTrivia
1241
+ SCAN = scanFor
1242
+ ctx.trivia = scopeTrivia
1243
+ ctx.triviaKindLabels = scopeLabels
1244
+ ctx.captureTrivia = true
1245
+ const v = child(input, pos, ctx)
1246
+ ctx.captureTrivia = savedCap
1247
+ ctx.trivia = saved
1248
+ ctx.triviaKindLabels = savedLabels
1249
+ SCAN = savedScan
1250
+ return v
1251
+ }
1252
+ : (input, pos, ctx) => {
1253
+ const saved = ctx.trivia
1254
+ const savedLabels = ctx.triviaKindLabels
1255
+ const savedScan = SCAN
1256
+ SCAN = scanFor
1257
+ ctx.trivia = scopeTrivia
1258
+ ctx.triviaKindLabels = scopeLabels
1259
+ const v = child(input, pos, ctx)
1260
+ ctx.trivia = saved
1261
+ ctx.triviaKindLabels = savedLabels
1262
+ SCAN = savedScan
1263
+ return v
1264
+ }
1265
+ return scopeRootPolicy(scopePiece, code[ip] === OP_SCOPE_PLAIN ? 0 : code[ip + 3]!)
1266
+ }
1267
+
1268
+ case OP_ROUTED: {
1269
+ const fb = code[ip + 1]!
1270
+ const fallback = fb >= 0 ? link(fb) : undefined
1271
+ return (input, pos, ctx) => {
1272
+ const item = ctx._routed
1273
+ if (item === undefined || pos !== item.span.start) {
1274
+ if (fallback !== undefined) return fallback(input, pos, ctx)
1275
+ ctx._fe = pos; ctx._fx = ROUTED_FX
1276
+ return FAIL
1277
+ }
1278
+ if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value: item.value, span: item.span })
1279
+ EC.e = item.span.end
1280
+ return item.value
1281
+ }
1282
+ }
1283
+
1284
+ /* ── transaction ─────────────────────────────────────────────────────── */
1285
+
1286
+ case OP_ATTEMPT: {
1287
+ const child = link(code[ip + 1]!)
1288
+ return (input, pos, ctx) => {
1289
+ const need = markCst(ctx)
1290
+ const mRaw = MRAW
1291
+ const mTl = MTL
1292
+ const mLv = MLV
1293
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
1294
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
1295
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
1296
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1297
+ const v = child(input, pos, ctx)
1298
+ if (v !== FAIL) return v
1299
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1300
+ // A committed failure propagates VERBATIM — rolled back, not re-anchored.
1301
+ if (committed(ctx)) return FAIL
1302
+ ctx._fe = pos
1303
+ return FAIL
1304
+ }
1305
+ }
1306
+
1307
+ /* ── zero-width ──────────────────────────────────────────────────────── */
1308
+
1309
+ case OP_NOT: {
1310
+ const child = link(code[ip + 1]!)
1311
+ const xf = fx[code[ip + 2]!] as string[]
1312
+ return (input, pos, ctx) => {
1313
+ const need = markCst(ctx)
1314
+ const mRaw = MRAW
1315
+ const mTl = MTL
1316
+ const mLv = MLV
1317
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
1318
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
1319
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
1320
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1321
+ const v = child(input, pos, ctx)
1322
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1323
+ if (v === FAIL) { EC.e = pos; return null }
1324
+ // `not.ts:50` — the ASSERTION's own set, at the assertion's position.
1325
+ ctx._fe = pos
1326
+ ctx._fx = xf
1327
+ return FAIL
1328
+ }
1329
+ }
1330
+
1331
+ case OP_PEEK: {
1332
+ const child = link(code[ip + 1]!)
1333
+ const xf = fx[code[ip + 2]!] as string[]
1334
+ return (input, pos, ctx) => {
1335
+ const need = markCst(ctx)
1336
+ const mRaw = MRAW
1337
+ const mTl = MTL
1338
+ const mLv = MLV
1339
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
1340
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
1341
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
1342
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1343
+ const v = child(input, pos, ctx)
1344
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1345
+ // `peek.ts:60` — the ASSERTION's own set, at the assertion's position.
1346
+ if (v === FAIL) { ctx._fe = pos; ctx._fx = xf; return FAIL }
1347
+ EC.e = pos
1348
+ return null
1349
+ }
1350
+ }
1351
+
1352
+ case OP_OPT: {
1353
+ const child = link(code[ip + 1]!)
1354
+ return (input, pos, ctx) => {
1355
+ const need = markCst(ctx)
1356
+ const mRaw = MRAW
1357
+ const mTl = MTL
1358
+ const mLv = MLV
1359
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
1360
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
1361
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
1362
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1363
+ ctx._fc = false
1364
+ const v = child(input, pos, ctx)
1365
+ if (v === FAIL) {
1366
+ if (committed(ctx)) return FAIL
1367
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1368
+ EC.e = pos
1369
+ // NULL, not undefined — `repeat.ts:269,277`, and grammars TEST for it.
1370
+ return null
1371
+ }
1372
+ return v
1373
+ }
1374
+ }
1375
+
1376
+ /* ── boundaries ──────────────────────────────────────────────────────── */
1377
+
1378
+ case OP_TOKEN: {
1379
+ const child = link(code[ip + 1]!)
1380
+ return (input, pos, ctx) => {
1381
+ const sTrivia = ctx.trivia, sKinds = ctx.triviaKindLabels
1382
+ const sBuf = ctx._cstBuf, sChildren = ctx._cstChildren, sLeaves = ctx._cstLeaves
1383
+ const sRaw = ctx._cstRawChildren, sTl = ctx._cstTriviaLog
1384
+ const sOuterTl = ctx._triviaLog, sRootTl = ctx._rootTriviaLog
1385
+ const wasCapturing = cstCaptureActive(ctx)
1386
+
1387
+ const sScan = SCAN
1388
+ SCAN = null
1389
+ ctx.trivia = undefined
1390
+ ctx.triviaKindLabels = undefined
1391
+ ctx._cstBuf = undefined
1392
+ ctx._cstChildren = undefined
1393
+ ctx._cstLeaves = undefined
1394
+ ctx._cstRawChildren = undefined
1395
+ ctx._cstTriviaLog = undefined
1396
+ ctx._triviaLog = undefined
1397
+ ctx._rootTriviaLog = undefined
1398
+
1399
+ let v: unknown
1400
+ try {
1401
+ v = child(input, pos, ctx)
1402
+ } finally {
1403
+ SCAN = sScan
1404
+ ctx.trivia = sTrivia
1405
+ ctx.triviaKindLabels = sKinds
1406
+ ctx._cstBuf = sBuf
1407
+ ctx._cstChildren = sChildren
1408
+ ctx._cstLeaves = sLeaves
1409
+ ctx._cstRawChildren = sRaw
1410
+ ctx._cstTriviaLog = sTl
1411
+ ctx._triviaLog = sOuterTl
1412
+ ctx._rootTriviaLog = sRootTl
1413
+ }
1414
+ if (v === FAIL) return FAIL
1415
+ const end = EC.e
1416
+ const value = input.slice(pos, end)
1417
+ if (wasCapturing) pushCstLeaf(ctx, { _tag: 'leaf', value, span: { start: pos, end } })
1418
+ EC.e = end
1419
+ return value
1420
+ }
1421
+ }
1422
+
1423
+ case OP_LEAF: {
1424
+ const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
1425
+ const child = link(code[ip + 2]!)
1426
+ return (input, pos, ctx) => {
1427
+ const sBuf = ctx._cstBuf, sChildren = ctx._cstChildren, sLeaves = ctx._cstLeaves
1428
+ const sRaw = ctx._cstRawChildren, sTl = ctx._cstTriviaLog
1429
+ const sOuterTl = ctx._triviaLog
1430
+ const wasCapturing = cstCaptureActive(ctx)
1431
+
1432
+ ctx._cstBuf = undefined
1433
+ ctx._cstChildren = undefined
1434
+ ctx._cstLeaves = undefined
1435
+ ctx._cstRawChildren = undefined
1436
+ ctx._cstTriviaLog = undefined
1437
+ ctx._triviaLog = undefined
1438
+
1439
+ let v: unknown
1440
+ try {
1441
+ v = child(input, pos, ctx)
1442
+ } finally {
1443
+ ctx._cstBuf = sBuf
1444
+ ctx._cstChildren = sChildren
1445
+ ctx._cstLeaves = sLeaves
1446
+ ctx._cstRawChildren = sRaw
1447
+ ctx._cstTriviaLog = sTl
1448
+ ctx._triviaLog = sOuterTl
1449
+ }
1450
+ if (v === FAIL) return FAIL
1451
+ const end = EC.e
1452
+ const out = fn(v, { start: pos, end })
1453
+ if (wasCapturing) pushCstLeaf(ctx, { _tag: 'leaf', value: out, span: { start: pos, end } })
1454
+ EC.e = end
1455
+ return out
1456
+ }
1457
+ }
1458
+
1459
+ /* ── sequences ───────────────────────────────────────────────────────── */
1460
+
1461
+ case OP_SEQ:
1462
+ case OP_SEQV:
1463
+ case OP_SEQX: {
1464
+ const fused = op === OP_SEQX
1465
+ const base = fused ? ip + 3 : ip + 2
1466
+ const n = code[fused ? ip + 2 : ip + 1]!
1467
+ // Children bound as a plain array of DIRECT references, laid out once.
1468
+ const kids: Piece[] = new Array<Piece>(n)
1469
+ for (let i = 0; i < n; i++) kids[i] = link(code[base + i]!)
1470
+ const wantValues = op !== OP_SEQV
1471
+ const fn = fused
1472
+ ? fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
1473
+ : undefined
1474
+
1475
+ /**
1476
+ * The generic term loop, for arities the unrolled pieces below do not
1477
+ * cover.
1478
+ *
1479
+ * `exec.ts` open-coded a LIT/RX fast path here to dodge "a JS call frame
1480
+ * plus a switch dispatch". Half of that cost is gone by construction — a
1481
+ * piece has no switch — and the other half is what TurboFan removes by
1482
+ * INLINING a small monomorphic child. Re-emitting the terminal bodies
1483
+ * would instead grow this function past the inlining budget, which is the
1484
+ * defect being fixed, so they are not re-emitted.
1485
+ */
1486
+ const runTerms = (input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined): number => {
1487
+ const v0 = kids[0]!(input, pos, ctx)
1488
+ if (v0 === FAIL) return -1
1489
+ if (values !== undefined) values.push(v0)
1490
+ let cur = EC.e
1491
+ for (let i = 1; i < n; i++) {
1492
+ cur = nextTerm(kids[i]!, input, cur, ctx)
1493
+ if (cur < 0) return -1
1494
+ if (values !== undefined) values.push(TERMV)
1495
+ }
1496
+ return cur
1497
+ }
1498
+
1499
+ /**
1500
+ * SEQUENCES CARRYING AN ADJACENCY ASSERTION — the assembly-time twin of
1501
+ * `sequence()`'s `parseAdjacent` fork (combinators/sequence.ts:118).
1502
+ *
1503
+ * `adjacent()` / `notAdjacent()` are BOUNDARY tests, not terms: each asks
1504
+ * whether trivia sat between the previous term and here, so it has to be
1505
+ * answered at the cursor, BEFORE the ambient scan `nextTerm` performs.
1506
+ * Handed the post-scan position it would find the gap already consumed
1507
+ * and answer "adjacent" every time — `adjacent()` a no-op,
1508
+ * `notAdjacent()` a guaranteed failure, both behind a parse that still
1509
+ * succeeds. Which term holds an assertion is TABLE DATA, so the decision
1510
+ * is made here and the parse path holds a bound runner per term rather
1511
+ * than an opcode test per term.
1512
+ *
1513
+ * A runner returns the new cursor or −1, with the value in `TERMV` —
1514
+ * `nextTerm`'s protocol, so the two kinds are interchangeable in the
1515
+ * loop. The assertion's runner returns `cur` UNMOVED: the following term
1516
+ * re-scans the same gap and keeps its own commit/rewind decision, which
1517
+ * is what makes the tree, the spans and the trivia log identical to the
1518
+ * same sequence written without the marker.
1519
+ *
1520
+ * Index 0 is never an assertion — `sequence()` rejects it at
1521
+ * construction, since the gap before the first term belongs to the
1522
+ * caller — so the first child is still called directly.
1523
+ */
1524
+ /**
1525
+ * RECOVERY SEQUENCES PUBLISH `ctx._sync`, and that is the whole of
1526
+ * "recovery config" — no grammar carries any.
1527
+ *
1528
+ * Before term `i`, the sentinel for the union of every LATER term's first
1529
+ * set becomes the sync point a list nested in that term resyncs to, so a
1530
+ * `sepBy` inside `sequence('{', list, '}')` finds `}` with nothing
1531
+ * annotated. Where there is no usable local follow (the last term, an
1532
+ * `any` first set) the INHERITED sync stays published, which is how the
1533
+ * enclosing delimiter reaches across a rule boundary.
1534
+ *
1535
+ * ONE generic loop rather than the arity-specialised pieces: this is only
1536
+ * reached in a recovery table, where the arity unroll buys a fraction of a
1537
+ * cold-path parse and costs four more copies of the publish protocol to
1538
+ * keep in step with the other two engines.
1539
+ *
1540
+ * The restore is `sequence()`'s `finally` (combinators/sequence.ts:105).
1541
+ * Codegen does not restore and compensates by having every list capture
1542
+ * `_sync` at ENTRY (codegen.ts:3032); the table captures at entry too, so
1543
+ * this is unobservable either way — it is here because leaving a stale
1544
+ * sentinel published is the kind of state a later reader will trust.
1545
+ */
1546
+ if (REC) {
1547
+ const syncs: (Combinator<unknown> | undefined)[] = new Array<Combinator<unknown> | undefined>(n)
1548
+ for (let i = 0; i < n; i++) syncs[i] = sentinelFor(code[base + n + i]!)
1549
+ const runners: TermRunner[] = new Array<TermRunner>(n)
1550
+ for (let i = 1; i < n; i++) {
1551
+ const kidIp = code[base + i]!
1552
+ if (code[kidIp] !== OP_ADJ) {
1553
+ const kid = kids[i]!
1554
+ runners[i] = (input, cur, ctx) => nextTerm(kid, input, cur, ctx)
1555
+ continue
1556
+ }
1557
+ const negated = code[kidIp + 1] === 1
1558
+ const ki = code[kidIp + 2]!
1559
+ const kinds = ki < 0 ? undefined : k[ki] as readonly string[]
1560
+ const xf = fx[code[kidIp + 3]!] as string[]
1561
+ runners[i] = (input, cur, ctx) => {
1562
+ if (!adjacencyHolds(input, cur, ctx, negated, kinds)) {
1563
+ ctx._fe = cur; ctx._fx = xf
1564
+ return -1
1565
+ }
1566
+ TERMV = null
1567
+ return cur
1568
+ }
1569
+ }
1570
+ const runSyncTerms = (input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined): number => {
1571
+ const inherited = ctx._sync
1572
+ ctx._sync = syncs[0] ?? inherited
1573
+ const v0 = kids[0]!(input, pos, ctx)
1574
+ if (v0 === FAIL) { ctx._sync = inherited; return -1 }
1575
+ if (values !== undefined) values.push(v0)
1576
+ let cur = EC.e
1577
+ for (let i = 1; i < n; i++) {
1578
+ ctx._sync = syncs[i] ?? inherited
1579
+ cur = runners[i]!(input, cur, ctx)
1580
+ if (cur < 0) { ctx._sync = inherited; return -1 }
1581
+ if (values !== undefined) values.push(TERMV)
1582
+ }
1583
+ ctx._sync = inherited
1584
+ return cur
1585
+ }
1586
+ if (fused) {
1587
+ return (input, pos, ctx) => {
1588
+ const values: unknown[] = []
1589
+ const cur = runSyncTerms(input, pos, ctx, values)
1590
+ if (cur < 0) return FAIL
1591
+ EC.e = cur
1592
+ return fn!(values, { start: pos, end: cur })
1593
+ }
1594
+ }
1595
+ if (wantValues) {
1596
+ return (input, pos, ctx) => {
1597
+ const values: unknown[] = []
1598
+ const cur = runSyncTerms(input, pos, ctx, values)
1599
+ if (cur < 0) return FAIL
1600
+ EC.e = cur
1601
+ return values
1602
+ }
1603
+ }
1604
+ return (input, pos, ctx) => {
1605
+ const cur = runSyncTerms(input, pos, ctx, undefined)
1606
+ if (cur < 0) return FAIL
1607
+ EC.e = cur
1608
+ return undefined
1609
+ }
1610
+ }
1611
+
1612
+ let hasAdj = false
1613
+ for (let i = 1; i < n; i++) if (code[code[base + i]!] === OP_ADJ) { hasAdj = true; break }
1614
+ if (hasAdj) {
1615
+ const runners: TermRunner[] = new Array<TermRunner>(n)
1616
+ for (let i = 1; i < n; i++) {
1617
+ const kidIp = code[base + i]!
1618
+ if (code[kidIp] !== OP_ADJ) {
1619
+ const kid = kids[i]!
1620
+ runners[i] = (input, cur, ctx) => nextTerm(kid, input, cur, ctx)
1621
+ continue
1622
+ }
1623
+ const negated = code[kidIp + 1] === 1
1624
+ const ki = code[kidIp + 2]!
1625
+ // The FILTER is bound; the MASK is not. A scope can swap the trivia
1626
+ // table, so which bit a category name means is a parse-time fact —
1627
+ // `adjacencyHolds` resolves it against the active table, exactly as
1628
+ // the interpreter does.
1629
+ const kinds = ki < 0 ? undefined : k[ki] as readonly string[]
1630
+ const xf = fx[code[kidIp + 3]!] as string[]
1631
+ runners[i] = (input, cur, ctx) => {
1632
+ if (!adjacencyHolds(input, cur, ctx, negated, kinds)) {
1633
+ ctx._fe = cur; ctx._fx = xf
1634
+ return -1
1635
+ }
1636
+ TERMV = null
1637
+ return cur
1638
+ }
1639
+ }
1640
+ const runAdjTerms = (input: string, pos: number, ctx: ParseContext, values: unknown[] | undefined): number => {
1641
+ const v0 = kids[0]!(input, pos, ctx)
1642
+ if (v0 === FAIL) return -1
1643
+ if (values !== undefined) values.push(v0)
1644
+ let cur = EC.e
1645
+ for (let i = 1; i < n; i++) {
1646
+ cur = runners[i]!(input, cur, ctx)
1647
+ if (cur < 0) return -1
1648
+ if (values !== undefined) values.push(TERMV)
1649
+ }
1650
+ return cur
1651
+ }
1652
+ if (fused) {
1653
+ return (input, pos, ctx) => {
1654
+ const values: unknown[] = []
1655
+ const cur = runAdjTerms(input, pos, ctx, values)
1656
+ if (cur < 0) return FAIL
1657
+ EC.e = cur
1658
+ return fn!(values, { start: pos, end: cur })
1659
+ }
1660
+ }
1661
+ if (wantValues) {
1662
+ return (input, pos, ctx) => {
1663
+ const values: unknown[] = []
1664
+ const cur = runAdjTerms(input, pos, ctx, values)
1665
+ if (cur < 0) return FAIL
1666
+ EC.e = cur
1667
+ return values
1668
+ }
1669
+ }
1670
+ return (input, pos, ctx) => {
1671
+ const cur = runAdjTerms(input, pos, ctx, undefined)
1672
+ if (cur < 0) return FAIL
1673
+ EC.e = cur
1674
+ return undefined
1675
+ }
1676
+ }
1677
+
1678
+ /**
1679
+ * ARITY-SPECIALISED PIECES — the "pre-made, fully-formed" half of the
1680
+ * design, at the shape that dominates every grammar here.
1681
+ *
1682
+ * A sequence's ARITY is table data, so the loop over it is work assembly
1683
+ * can finish. Unrolling removes, per term: the counter, the bounds-checked
1684
+ * `kids[i]` load, and the `i > 0` test that decides whether trivia
1685
+ * precedes the term — the FIRST term never has trivia before it, and that
1686
+ * was being re-decided 166,842 times per parse for `SEQV` alone.
1687
+ *
1688
+ * The value array is built as a LITERAL of the right size rather than
1689
+ * grown by `push`, so a 2- or 3-term sequence allocates once at its final
1690
+ * capacity instead of reallocating as it fills.
1691
+ *
1692
+ * Arity 1-3 covers the overwhelming majority; above that the general loop
1693
+ * runs, so there is no arity this fails to lower.
1694
+ */
1695
+ if (n === 1) {
1696
+ const k0 = kids[0]!
1697
+ if (fused) {
1698
+ return (input, pos, ctx) => {
1699
+ const v = k0(input, pos, ctx)
1700
+ if (v === FAIL) return FAIL
1701
+ return fn!([v], { start: pos, end: EC.e })
1702
+ }
1703
+ }
1704
+ if (wantValues) {
1705
+ return (input, pos, ctx) => {
1706
+ const v = k0(input, pos, ctx)
1707
+ if (v === FAIL) return FAIL
1708
+ return [v]
1709
+ }
1710
+ }
1711
+ return (input, pos, ctx) => {
1712
+ const v = k0(input, pos, ctx)
1713
+ if (v === FAIL) return FAIL
1714
+ return undefined
1715
+ }
1716
+ }
1717
+
1718
+ if (n === 2) {
1719
+ const k0 = kids[0]!, k1 = kids[1]!
1720
+ if (fused) {
1721
+ return (input, pos, ctx) => {
1722
+ const v0 = k0(input, pos, ctx)
1723
+ if (v0 === FAIL) return FAIL
1724
+ const cur = nextTerm(k1, input, EC.e, ctx)
1725
+ if (cur < 0) return FAIL
1726
+ EC.e = cur
1727
+ return fn!([v0, TERMV], { start: pos, end: cur })
1728
+ }
1729
+ }
1730
+ if (wantValues) {
1731
+ return (input, pos, ctx) => {
1732
+ const v0 = k0(input, pos, ctx)
1733
+ if (v0 === FAIL) return FAIL
1734
+ const cur = nextTerm(k1, input, EC.e, ctx)
1735
+ if (cur < 0) return FAIL
1736
+ EC.e = cur
1737
+ return [v0, TERMV]
1738
+ }
1739
+ }
1740
+ return (input, pos, ctx) => {
1741
+ const v0 = k0(input, pos, ctx)
1742
+ if (v0 === FAIL) return FAIL
1743
+ const cur = nextTerm(k1, input, EC.e, ctx)
1744
+ if (cur < 0) return FAIL
1745
+ EC.e = cur
1746
+ return undefined
1747
+ }
1748
+ }
1749
+
1750
+ if (n === 3) {
1751
+ const k0 = kids[0]!, k1 = kids[1]!, k2 = kids[2]!
1752
+ if (fused) {
1753
+ return (input, pos, ctx) => {
1754
+ const v0 = k0(input, pos, ctx)
1755
+ if (v0 === FAIL) return FAIL
1756
+ let cur = nextTerm(k1, input, EC.e, ctx)
1757
+ if (cur < 0) return FAIL
1758
+ const v1 = TERMV
1759
+ cur = nextTerm(k2, input, cur, ctx)
1760
+ if (cur < 0) return FAIL
1761
+ EC.e = cur
1762
+ return fn!([v0, v1, TERMV], { start: pos, end: cur })
1763
+ }
1764
+ }
1765
+ if (wantValues) {
1766
+ return (input, pos, ctx) => {
1767
+ const v0 = k0(input, pos, ctx)
1768
+ if (v0 === FAIL) return FAIL
1769
+ let cur = nextTerm(k1, input, EC.e, ctx)
1770
+ if (cur < 0) return FAIL
1771
+ const v1 = TERMV
1772
+ cur = nextTerm(k2, input, cur, ctx)
1773
+ if (cur < 0) return FAIL
1774
+ EC.e = cur
1775
+ return [v0, v1, TERMV]
1776
+ }
1777
+ }
1778
+ return (input, pos, ctx) => {
1779
+ const v0 = k0(input, pos, ctx)
1780
+ if (v0 === FAIL) return FAIL
1781
+ let cur = nextTerm(k1, input, EC.e, ctx)
1782
+ if (cur < 0) return FAIL
1783
+ cur = nextTerm(k2, input, cur, ctx)
1784
+ if (cur < 0) return FAIL
1785
+ EC.e = cur
1786
+ return undefined
1787
+ }
1788
+ }
1789
+
1790
+ if (fused) {
1791
+ return (input, pos, ctx) => {
1792
+ const values: unknown[] = []
1793
+ const cur = runTerms(input, pos, ctx, values)
1794
+ if (cur < 0) return FAIL
1795
+ EC.e = cur
1796
+ return fn!(values, { start: pos, end: cur })
1797
+ }
1798
+ }
1799
+ if (wantValues) {
1800
+ return (input, pos, ctx) => {
1801
+ const values: unknown[] = []
1802
+ const cur = runTerms(input, pos, ctx, values)
1803
+ if (cur < 0) return FAIL
1804
+ EC.e = cur
1805
+ return values
1806
+ }
1807
+ }
1808
+ return (input, pos, ctx) => {
1809
+ const cur = runTerms(input, pos, ctx, undefined)
1810
+ if (cur < 0) return FAIL
1811
+ EC.e = cur
1812
+ return undefined
1813
+ }
1814
+ }
1815
+
1816
+ /* ── choice ──────────────────────────────────────────────────────────── */
1817
+
1818
+ case OP_CHOICE: {
1819
+ const table = disp[code[ip + 1]!]!
1820
+ const n = code[ip + 2]!
1821
+ const choiceFx = fx[code[ip + 3]!] as string[]
1822
+ const base = ip + 4
1823
+ const arms: Piece[] = new Array<Piece>(n)
1824
+ for (let i = 0; i < n; i++) arms[i] = link(code[base + i]!)
1825
+ // PER-ARM STATIC SETS, resolved once at assembly. An arm the char gate
1826
+ // declines to enter still owes its expectation: the interpreter has no
1827
+ // such gate, enters the arm, and its opener's failure is what lands in
1828
+ // the accumulation.
1829
+ const armFx: (string[])[] = new Array<string[]>(n)
1830
+ for (let i = 0; i < n; i++) armFx[i] = fx[code[base + n + i]!] as string[]
1831
+
1832
+ // THE TWO CHOICE SHAPES ARE TWO PIECES. `exec.ts` tested `table.exclusive`
1833
+ // on every choice execution; it is table data, so it selects here.
1834
+ if (table.exclusive) {
1835
+ const ascii = table.ascii
1836
+ const hi = table.hi
1837
+ // THERE ARE NO OPEN ARMS HERE, and the piece is written to say so. An
1838
+ // ungated arm is one whose class is −1, and `resolveDispatch` clears
1839
+ // `exclusive` for exactly those arms (program.ts) — so `table.open` is
1840
+ // empty whenever `exclusive` holds, and the open-arm fallback loop this
1841
+ // piece used to carry was unreachable code plus a per-parse length test.
1842
+ return (input, pos, ctx) => {
1843
+ const c = lead(input, pos)
1844
+ let arm = -1
1845
+ if (c >= 0 && c < 128) {
1846
+ const a = ascii[c]!
1847
+ if (a !== 0) arm = a - 1
1848
+ } else if (c >= 128) {
1849
+ for (let i = 0; i < hi.length; i += 3) {
1850
+ if (c >= hi[i]! && c <= hi[i + 1]!) { arm = hi[i + 2]!; break }
1851
+ }
1852
+ }
1853
+ if (arm >= 0) {
1854
+ ctx._fc = false
1855
+ const v = arms[arm]!(input, pos, ctx)
1856
+ if (v !== FAIL) return v
1857
+ // THE CUT — a committed failure fails the whole choice.
1858
+ if (committed(ctx)) return FAIL
1859
+ // THE DISPATCHED ARM'S OWN SET IS THE ANSWER, at the choice's own
1860
+ // position — the rule both shipped engines apply (choice.ts:105,
1861
+ // codegen.ts:1985). `_fx` already holds it; reporting the arm is
1862
+ // declining to overwrite it. See `exec.ts` for why this is NOT
1863
+ // furthest-failure merging and what the residual JSON gap is.
1864
+ const armFx = ctx._fx
1865
+ if (armFx !== undefined && armFx.length > 0) { ctx._fe = pos; return FAIL }
1866
+ }
1867
+ // THE UNION IS REPORTED ONLY ON A DISPATCH MISS, where the interpreter's
1868
+ // `parsers.flatMap` over non-nullable, char-excluded arms yields exactly
1869
+ // each arm's own opener — this set, without running the arms.
1870
+ ctx._fe = pos; ctx._fx = choiceFx
1871
+ return FAIL
1872
+ }
1873
+ }
1874
+
1875
+ // THE PER-ARM GATE. Arms in source order, each skipped when its own class
1876
+ // excludes the char at `pos`. `null` means nullable or unmappable, and
1877
+ // those arms are always entered.
1878
+ const armCls = table.armCls
1879
+ const gates: (ResolvedClass | null)[] = new Array<ResolvedClass | null>(n)
1880
+ for (let i = 0; i < n; i++) gates[i] = armCls[i] ?? null
1881
+
1882
+ /**
1883
+ * THE GATE, PRECOMPUTED. One `Uint32Array` load replaces `n` class tests.
1884
+ *
1885
+ * `exec.ts` asked, per arm per execution, "does this arm's class hold the
1886
+ * char at `pos`" — `n` calls into `classHas`, each a bounds check plus a
1887
+ * `Uint8Array` load, on 136,760 choice executions per parse of
1888
+ * `benchmark.less`. Every one of those answers is a function of the
1889
+ * CHARACTER and the TABLE, both of which are known here, so the whole
1890
+ * question is answered once at assembly: `cand[c]` is the bitmask of arms
1891
+ * that could match `c`, and the loop visits only set bits, IN ORDER, so
1892
+ * the source ordering the gate exists to preserve is preserved exactly.
1893
+ *
1894
+ * Slot 128 is EOF (`lead` returns −1): `classHas` rejects a negative code,
1895
+ * so only the always-enter arms are candidates there. Code points ≥ 128
1896
+ * keep the per-arm loop — the ranges make a table the wrong shape and
1897
+ * astral input is not the hot path.
1898
+ *
1899
+ * Bounded at 32 arms because the mask is one word. Above that the general
1900
+ * loop runs, which is the same code the ≥ 128 path takes, so there is no
1901
+ * second implementation to drift.
1902
+ */
1903
+ const maskable = n <= 32
1904
+ let cand: Uint32Array | undefined
1905
+ if (maskable) {
1906
+ const m = new Uint32Array(129)
1907
+ for (let i = 0; i < n; i++) {
1908
+ const cls = gates[i]!
1909
+ const bit = 1 << i
1910
+ if (cls === null) {
1911
+ for (let c = 0; c < 129; c++) m[c]! |= bit
1912
+ continue
1913
+ }
1914
+ for (let c = 0; c < 128; c++) if (cls.ascii[c] === 1) m[c]! |= bit
1915
+ }
1916
+ cand = m
1917
+ }
1918
+
1919
+ if (cand !== undefined) {
1920
+ const mask = cand
1921
+ return (input, pos, ctx) => {
1922
+ const c = lead(input, pos)
1923
+ const need = markCst(ctx)
1924
+ const mRaw = MRAW
1925
+ const mTl = MTL
1926
+ const mLv = MLV
1927
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
1928
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
1929
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
1930
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1931
+ let acc: string[] | undefined
1932
+ if (c < 128) {
1933
+ // `c` is −1 at EOF, which indexes slot 128 — the always-enter arms.
1934
+ const bits0 = mask[c < 0 ? 128 : c]!
1935
+ let bits = bits0
1936
+ // Arms below `prev` have already contributed. Only the FAILURE path
1937
+ // advances it, so a matching arm returns without paying for any of
1938
+ // this — the skipped arms' sets are read on no other exit.
1939
+ let prev = 0
1940
+ while (bits !== 0) {
1941
+ const i = 31 - Math.clz32(bits & -bits)
1942
+ bits &= bits - 1
1943
+ ctx._fc = false
1944
+ const v = arms[i]!(input, pos, ctx)
1945
+ if (v !== FAIL) return v
1946
+ for (let j = prev; j < i; j++) if ((bits0 & (1 << j)) === 0) acc = accSet(armFx[j]!, acc)
1947
+ prev = i + 1
1948
+ acc = accSet(ctx._fx, acc)
1949
+ // A committed arm cuts, and the interpreter reports the
1950
+ // accumulation at the ARM's span (choice.ts:170), not at `pos`.
1951
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1952
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1953
+ }
1954
+ for (let j = prev; j < n; j++) if ((bits0 & (1 << j)) === 0) acc = accSet(armFx[j]!, acc)
1955
+ ctx._fe = pos; ctx._fx = acc ?? choiceFx
1956
+ return FAIL
1957
+ }
1958
+ for (let i = 0; i < n; i++) {
1959
+ const cls = gates[i]!
1960
+ if (cls !== null && !classHas(cls, c)) { acc = accSet(armFx[i]!, acc); continue }
1961
+ ctx._fc = false
1962
+ const v = arms[i]!(input, pos, ctx)
1963
+ if (v !== FAIL) return v
1964
+ acc = accSet(ctx._fx, acc)
1965
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1966
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1967
+ }
1968
+ ctx._fe = pos; ctx._fx = acc ?? choiceFx
1969
+ return FAIL
1970
+ }
1971
+ }
1972
+
1973
+ return (input, pos, ctx) => {
1974
+ const c = lead(input, pos)
1975
+ const need = markCst(ctx)
1976
+ const mRaw = MRAW
1977
+ const mTl = MTL
1978
+ const mLv = MLV
1979
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
1980
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
1981
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
1982
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1983
+ let acc: string[] | undefined
1984
+ for (let i = 0; i < n; i++) {
1985
+ const cls = gates[i]!
1986
+ if (cls !== null && !classHas(cls, c)) { acc = accSet(armFx[i]!, acc); continue }
1987
+ ctx._fc = false
1988
+ const v = arms[i]!(input, pos, ctx)
1989
+ if (v !== FAIL) return v
1990
+ acc = accSet(ctx._fx, acc)
1991
+ if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1992
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1993
+ }
1994
+ ctx._fe = pos; ctx._fx = acc ?? choiceFx
1995
+ return FAIL
1996
+ }
1997
+ }
1998
+
1999
+ /* ── greedyClassify / autoNot ────────────────────────────────────────── */
2000
+
2001
+ case OP_GREEDY: {
2002
+ const sup = link(code[ip + 1]!)
2003
+ const n = code[ip + 2]!
2004
+ // THE CLASSIFICATION MAP, BUILT ONCE. `choice()` builds the same map once
2005
+ // per construction (choice.ts:64-70); nothing about it is per-parse.
2006
+ const byWord = new Map<string, Piece>()
2007
+ for (let i = 0; i < n; i++) byWord.set(k[code[ip + 3 + 2 * i]!] as string, link(code[ip + 4 + 2 * i]!))
2008
+ return (input, pos, ctx) => {
2009
+ const need = markCst(ctx)
2010
+ const mRaw = MRAW
2011
+ const mTl = MTL
2012
+ const mLv = MLV
2013
+ const mFl = need ? ctx._fields?.length ?? 0 : 0
2014
+ const mEr = need ? ctx._errors?.length ?? 0 : 0
2015
+ const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
2016
+ const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
2017
+ const v = sup(input, pos, ctx)
2018
+ // The super arm's failure propagates verbatim — its `_fe`/`_fx`, not
2019
+ // the union of the arms (choice.ts:126).
2020
+ if (v === FAIL) return FAIL
2021
+ const end = EC.e
2022
+ const lit = byWord.get(input.slice(pos, end))
2023
+ if (lit === undefined) { EC.e = end; return v }
2024
+ // Unwind the regex's leaf so the credited arm's own leaf is the only
2025
+ // one. Re-running the arm cannot fail: the word IS its literal.
2026
+ if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2027
+ return lit(input, pos, ctx)
2028
+ }
2029
+ }
2030
+
2031
+ case OP_ARMGATE: {
2032
+ // Predicate, arm and expected set all bound HERE — the body reads no
2033
+ // opcode, decodes no operand and tests no setting.
2034
+ const pred = fns[code[ip + 1]!] as (s: unknown) => boolean
2035
+ const child = link(code[ip + 2]!)
2036
+ const xf = fx[code[ip + 3]!] as string[]
2037
+ return (input, pos, ctx) => {
2038
+ if (!pred(ctx.state)) {
2039
+ // A `continue` in the interpreter (choice.ts:150), not a failure —
2040
+ // so no cut survives to the enclosing choice.
2041
+ ctx._fc = false
2042
+ ctx._fe = pos
2043
+ ctx._fx = xf
2044
+ return FAIL
2045
+ }
2046
+ return child(input, pos, ctx)
2047
+ }
2048
+ }
2049
+
2050
+ case OP_REJECT: {
2051
+ const child = link(code[ip + 1]!)
2052
+ const n = code[ip + 2]!
2053
+ // The checks SPLIT BY KIND here, so neither loop below tests which kind
2054
+ // it is holding. `autoNotFires` is a pure OR over independent lookahead
2055
+ // predicates, so grouping them changes nothing but the branch.
2056
+ const strs: string[] = []
2057
+ const clss: ResolvedClass[] = []
2058
+ for (let i = 0; i < n; i++) {
2059
+ const o = code[ip + 4 + 2 * i]!
2060
+ if (code[ip + 3 + 2 * i]! === 0) strs.push(k[o] as string)
2061
+ else clss.push(cc[o]!)
2062
+ }
2063
+ const ns = strs.length
2064
+ const nc = clss.length
2065
+ return (input, pos, ctx) => {
2066
+ const v = child(input, pos, ctx)
2067
+ if (v === FAIL) return FAIL
2068
+ const end = EC.e
2069
+ for (let i = 0; i < ns; i++) {
2070
+ if (!input.startsWith(strs[i]!, end)) continue
2071
+ // A `continue` in the interpreter, not a failure — so no cut survives
2072
+ // AND no expectation is contributed: `choice.ts:174` rolls back and
2073
+ // moves on without touching `expected`. `_fx` still holds whatever the
2074
+ // arm's last inner failure left, so it is cleared rather than left to
2075
+ // be accumulated as if this arm had failed wanting something.
2076
+ ctx._fc = false
2077
+ ctx._fx = undefined
2078
+ return FAIL
2079
+ }
2080
+ if (nc !== 0) {
2081
+ const c = lead(input, end)
2082
+ for (let i = 0; i < nc; i++) {
2083
+ if (!classHas(clss[i]!, c)) continue
2084
+ ctx._fc = false
2085
+ ctx._fx = undefined
2086
+ return FAIL
2087
+ }
2088
+ }
2089
+ EC.e = end
2090
+ return v
2091
+ }
2092
+ }
2093
+
2094
+ /* ── repetition ──────────────────────────────────────────────────────── */
2095
+
2096
+ case OP_REP:
2097
+ case OP_REPV: {
2098
+ const child = link(code[ip + 1]!)
2099
+ const min = code[ip + 2]!
2100
+ const max = code[ip + 3]!
2101
+ const sepIp = code[ip + 4]!
2102
+ const sep = sepIp >= 0 ? link(sepIp) : undefined
2103
+ const flags = code[ip + 5]!
2104
+ const keepSeparators = (flags & 2) !== 0
2105
+ const trailingAllowed = (flags & 1) !== 0
2106
+ const reportItem = (flags & 4) !== 0
2107
+ const itemFx = reportItem ? fx[code[ip + 6]!] as string[] : EMPTY_FX
2108
+ const collect = op === OP_REP
2109
+ // `many()` — the min-0, separator-less repeat — is the only shape that runs
2110
+ // its FIRST item through `repItem` and so skips leading trivia. The shape
2111
+ // identifies itself, and it is table data, so it is decided here.
2112
+ const skipBeforeFirst = sepIp < 0 && min === 0
2113
+
2114
+ /**
2115
+ * TOLERANT RECOVERY — the third implementation of `recoverScan`'s
2116
+ * protocol, and it calls the SAME functions the other two do
2117
+ * (`recovery/scan.ts`), so an error's span, its expected set and its CST
2118
+ * embedding cannot drift between engines.
2119
+ *
2120
+ * Reached only in a recovery table, and inside it only on the FAILURE of
2121
+ * an element, so a matching item pays a `_sync` read at entry and nothing
2122
+ * else. The `ctx._tolerant` gate is the same dormancy the source lowering
2123
+ * and the interpreter have.
2124
+ *
2125
+ * WHAT RECOVERS, AND WHERE THE THREE ENGINES AGREE:
2126
+ * - a MANDATORY item (`count < min`) of a separator-less repeat does NOT
2127
+ * recover: `oneOrMore`'s first item propagates its failure
2128
+ * (repeat.ts:221), and codegen inlines the `min` items ahead of the
2129
+ * loop with an early return. A `sepBy`'s first element DOES recover,
2130
+ * which is why the test is `sep !== undefined || count >= min`.
2131
+ * - the check is `at(mySync, itemStart)`: sitting ON the sync token is a
2132
+ * clean list end, not junk, and `itemStart` is past any leading trivia
2133
+ * so that trivia is never swallowed into the error span
2134
+ * (repeat.ts:167).
2135
+ * - a separated list scans to its OWN separator or the enclosing
2136
+ * delimiter (`orSentinel`, repeat.ts:454).
2137
+ * - `mySync` is captured at ENTRY. An element's own sequence publishes
2138
+ * over `_sync` while it runs; the interpreter restores in a `finally`
2139
+ * and codegen saves at entry (codegen.ts:3032) — same value, both ways.
2140
+ * - the separator-less path rolls the leading trivia back BEFORE
2141
+ * recovering (repeat.ts's `repItem`), and the separated path does NOT:
2142
+ * a consumed separator and the error after it both belong to the list
2143
+ * (repeat.ts:533).
2144
+ */
2145
+ if (REC) {
2146
+ const itemFx = fx[code[ip + 6]!] as string[]
2147
+ const sepSent = sentinelFor(code[ip + 7]!)
2148
+ return (input, pos, ctx) => {
2149
+ const out: unknown[] | undefined = collect ? [] : undefined
2150
+ const hasTrivia = ctx.trivia !== undefined
2151
+ const needMark = rollbackNeeded(ctx)
2152
+ const mySync = ctx._sync
2153
+ let cur = pos
2154
+ let count = 0
2155
+ for (;;) {
2156
+ if (max >= 0 && count >= max) break
2157
+ if (sep !== undefined && count > 0 && count >= min && cur >= input.length) break
2158
+ if (needMark) markCst(ctx)
2159
+ const mRaw = needMark ? MRAW : 0
2160
+ const mTl = needMark ? MTL : 0
2161
+ const mLv = needMark ? MLV : 0
2162
+ const mFl = needMark ? ctx._fields?.length ?? 0 : 0
2163
+ const mEr = needMark ? ctx._errors?.length ?? 0 : 0
2164
+ const mLog = needMark ? ctx._triviaLog?.length ?? 0 : 0
2165
+ const mRoot = needMark ? ctx._rootTriviaLog?.length ?? 0 : 0
2166
+ let itemStart = cur
2167
+ let sepEnd = -1
2168
+ const viaRepItem = sep === undefined && count >= min && (count > 0 || skipBeforeFirst)
2169
+ if (sep !== undefined && count > 0) {
2170
+ const leavesBefore = cstLeavesLen(ctx)
2171
+ let sp = cur
2172
+ if (hasTrivia) sp = skipTrivia(input, sp, ctx)
2173
+ ctx._fc = false
2174
+ const sv = sep(input, sp, ctx)
2175
+ if (sv === FAIL) {
2176
+ if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2177
+ if (committed(ctx)) return FAIL
2178
+ break
2179
+ }
2180
+ if (!keepSeparators) demoteCapturedToRaw(ctx, leavesBefore)
2181
+ sepEnd = EC.e
2182
+ itemStart = hasTrivia ? skipTrivia(input, EC.e, ctx) : EC.e
2183
+ } else if (hasTrivia && (count > 0 || skipBeforeFirst)) {
2184
+ itemStart = skipTrivia(input, itemStart, ctx)
2185
+ }
2186
+ if (itemStart >= input.length && viaRepItem) {
2187
+ if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2188
+ if (trailingAllowed && sepEnd >= 0) cur = sepEnd
2189
+ break
2190
+ }
2191
+ ctx._fc = false
2192
+ const v = child(input, itemStart, ctx)
2193
+ if (v === FAIL) {
2194
+ if (committed(ctx)) {
2195
+ if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2196
+ return FAIL
2197
+ }
2198
+ if (ctx._tolerant === true
2199
+ && mySync !== undefined
2200
+ && (sep !== undefined || count >= min)
2201
+ && !matchesAt(mySync, input, itemStart, ctx)) {
2202
+ if (sep === undefined && needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2203
+ const rr = recoverScan(
2204
+ input, itemStart, ctx,
2205
+ sep === undefined ? mySync : orSentinel(sepSent ?? mySync, sepSent === undefined ? undefined : mySync),
2206
+ itemFx,
2207
+ )
2208
+ if (out !== undefined) out.push(rr.error)
2209
+ captureError(ctx, rr.error)
2210
+ count++
2211
+ cur = rr.end
2212
+ continue
2213
+ }
2214
+ if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2215
+ if (trailingAllowed && sepEnd >= 0) cur = sepEnd
2216
+ break
2217
+ }
2218
+ if (EC.e === itemStart && viaRepItem) {
2219
+ if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2220
+ break
2221
+ }
2222
+ if (out !== undefined) out.push(v)
2223
+ cur = EC.e
2224
+ count++
2225
+ }
2226
+ if (count < min) {
2227
+ if (reportItem) { ctx._fe = cur; ctx._fx = itemFx }
2228
+ return FAIL
2229
+ }
2230
+ EC.e = cur
2231
+ return out
2232
+ }
2233
+ }
2234
+
2235
+ return (input, pos, ctx) => {
2236
+ const out: unknown[] | undefined = collect ? [] : undefined
2237
+ const hasTrivia = ctx.trivia !== undefined
2238
+ const needMark = rollbackNeeded(ctx)
2239
+ let cur = pos
2240
+ let count = 0
2241
+ for (;;) {
2242
+ if (max >= 0 && count >= max) break
2243
+ // A separated list is bounded by its SEPARATOR, so it stops at EOF at
2244
+ // the LOOP HEAD. Held to `count >= min` so an under-`min` list still
2245
+ // attempts the separator and reports its expected set.
2246
+ if (sep !== undefined && count > 0 && count >= min && cur >= input.length) break
2247
+ // Per ITERATION, never hoisted: a preceding item's `node()` opened and
2248
+ // closed a capture buffer, so `ctx._cstBuf` is not the object it was
2249
+ // at the loop head. `needMark` is the pre-existing loop-invariant.
2250
+ if (needMark) markCst(ctx)
2251
+ const mRaw = needMark ? MRAW : 0
2252
+ const mTl = needMark ? MTL : 0
2253
+ const mLv = needMark ? MLV : 0
2254
+ const mFl = needMark ? ctx._fields?.length ?? 0 : 0
2255
+ const mEr = needMark ? ctx._errors?.length ?? 0 : 0
2256
+ const mLog = needMark ? ctx._triviaLog?.length ?? 0 : 0
2257
+ const mRoot = needMark ? ctx._rootTriviaLog?.length ?? 0 : 0
2258
+ let itemStart = cur
2259
+ let sepEnd = -1
2260
+ const viaRepItem = sep === undefined && count >= min && (count > 0 || skipBeforeFirst)
2261
+ if (sep !== undefined && count > 0) {
2262
+ const leavesBefore = cstLeavesLen(ctx)
2263
+ let sp = cur
2264
+ if (hasTrivia) sp = skipTrivia(input, sp, ctx)
2265
+ ctx._fc = false
2266
+ const sv = sep(input, sp, ctx)
2267
+ if (sv === FAIL) {
2268
+ if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2269
+ if (committed(ctx)) return FAIL
2270
+ break
2271
+ }
2272
+ if (!keepSeparators) demoteCapturedToRaw(ctx, leavesBefore)
2273
+ sepEnd = EC.e
2274
+ itemStart = hasTrivia ? skipTrivia(input, EC.e, ctx) : EC.e
2275
+ } else if (hasTrivia && (count > 0 || skipBeforeFirst)) {
2276
+ itemStart = skipTrivia(input, itemStart, ctx)
2277
+ }
2278
+ if (itemStart >= input.length && viaRepItem) {
2279
+ if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2280
+ if (trailingAllowed && sepEnd >= 0) cur = sepEnd
2281
+ break
2282
+ }
2283
+ ctx._fc = false
2284
+ const v = child(input, itemStart, ctx)
2285
+ if (v === FAIL) {
2286
+ if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2287
+ if (committed(ctx)) return FAIL
2288
+ if (trailingAllowed && sepEnd >= 0) cur = sepEnd
2289
+ break
2290
+ }
2291
+ if (EC.e === itemStart && viaRepItem) {
2292
+ // Zero-width item: `repItem`'s TERMINATION device, not a semantic
2293
+ // filter — and it applies only where `repItem` runs.
2294
+ if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2295
+ break
2296
+ }
2297
+ if (out !== undefined) out.push(v)
2298
+ cur = EC.e
2299
+ count++
2300
+ }
2301
+ if (count < min) {
2302
+ if (reportItem) { ctx._fe = cur; ctx._fx = itemFx }
2303
+ return FAIL
2304
+ }
2305
+ EC.e = cur
2306
+ return out
2307
+ }
2308
+ }
2309
+
2310
+ /* ── dispatch ────────────────────────────────────────────────────────── */
2311
+
2312
+ case OP_DISPATCH: {
2313
+ const spec = dsp[code[ip + 2]!]!
2314
+ const selector = link(code[ip + 1]!)
2315
+ const otherIp = code[ip + 3]!
2316
+ const other = otherIp >= 0 ? link(otherIp) : undefined
2317
+ const otherRouted = code[ip + 4]! === 1
2318
+ const n = code[ip + 5]!
2319
+ const armBase = ip + 6
2320
+ const arms: Piece[] = new Array<Piece>(n)
2321
+ for (let i = 0; i < n; i++) arms[i] = link(code[armBase + i]!)
2322
+ const byKey = spec.byKey
2323
+ const byFold = spec.byFold
2324
+ const hasFold = spec.byFold.size > 0
2325
+ const match = spec.match
2326
+ const matchN = match.length
2327
+ const matchFn: Array<(key: string) => boolean> = new Array<(key: string) => boolean>(matchN)
2328
+ const matchArm: number[] = new Array<number>(matchN)
2329
+ for (let i = 0; i < matchN; i++) { matchFn[i] = linkMatcher(match[i]!); matchArm[i] = match[i]![3] }
2330
+ const routed = spec.routed
2331
+ const expected = spec.expected as string[]
2332
+
2333
+ return (input, pos, ctx) => {
2334
+ const selectorMark = saveTriviaMark(ctx)
2335
+ const selVal = selector(input, pos, ctx)
2336
+ if (selVal === FAIL) return FAIL
2337
+ const selEnd = EC.e
2338
+ const key = selVal as string
2339
+
2340
+ let arm = byKey.get(key)
2341
+ if (arm === undefined && hasFold) arm = byFold.get(asciiFoldKey(key))
2342
+ if (arm === undefined) {
2343
+ for (let i = 0; i < matchN; i++) {
2344
+ if (matchFn[i]!(key)) {
2345
+ arm = matchArm[i]!
2346
+ break
2347
+ }
2348
+ }
2349
+ }
2350
+
2351
+ let target: Piece
2352
+ let usesRouted: boolean
2353
+ if (arm === undefined) {
2354
+ if (other === undefined) {
2355
+ // No branch and no fallback: fail AT THE SELECTOR'S END.
2356
+ ctx._fe = selEnd
2357
+ ctx._fx = expected
2358
+ return FAIL
2359
+ }
2360
+ target = other
2361
+ usesRouted = otherRouted
2362
+ } else {
2363
+ target = arms[arm]!
2364
+ usesRouted = routed[arm] === 1
2365
+ }
2366
+
2367
+ let mark = saveTriviaMark(ctx)
2368
+ let v: unknown
2369
+ if (usesRouted) {
2370
+ const savedRouted = ctx._routed
2371
+ rollbackTrivia(ctx, selectorMark)
2372
+ mark = saveTriviaMark(ctx)
2373
+ ctx._routed = { value: key, span: { start: pos, end: selEnd } }
2374
+ try {
2375
+ v = target(input, pos, ctx)
2376
+ } finally {
2377
+ ctx._routed = savedRouted
2378
+ }
2379
+ } else {
2380
+ v = target(input, selEnd, ctx)
2381
+ }
2382
+ if (v === FAIL) {
2383
+ rollbackTrivia(ctx, mark)
2384
+ // A failed dispatch branch is COMMITTED: the selector already matched.
2385
+ ctx._fc = true
2386
+ return FAIL
2387
+ }
2388
+ return [key, v]
2389
+ }
2390
+ }
2391
+
2392
+ /* ── node ────────────────────────────────────────────────────────────── */
2393
+
2394
+ case OP_NODE:
2395
+ case OP_NODE_TRACK: {
2396
+ const flags = code[ip + 3]!
2397
+ const child = link(code[ip + 2]!)
2398
+ const proj = code[ip + 4]!
2399
+ const buildIdx = code[ip + 1]!
2400
+ const type = k[code[ip + 5]!] as string
2401
+ const tagIdx = code[ip + 6]!
2402
+ const tags = tagIdx < 0 ? undefined : k[tagIdx] as readonly string[]
2403
+ const tracked = op === OP_NODE_TRACK
2404
+ const readsTrivia = (flags & 4) !== 0
2405
+ const readsState = (flags & 8) !== 0
2406
+ const hasFields = (flags & 16) !== 0
2407
+ const collapse = (flags & 32) !== 0
2408
+ const unwrap = (flags & 64) !== 0
2409
+ const trailingTrivia = (flags & 128) !== 0
2410
+ // HOST MODE IS AN OPTION, and it decided five separate runtime ternaries
2411
+ // in `exec.ts`'s node case — the single most-executed non-terminal at
2412
+ // 145,512 executions per parse of `benchmark.less`. `cstOutputHost(ctx.build)`
2413
+ // is fixed by `run()` before the entry is called, so it selects the piece.
2414
+ const build = buildIdx >= 0
2415
+ ? fns[buildIdx] as (
2416
+ children: readonly unknown[], fields: FieldMap | undefined, span: { start: number; end: number },
2417
+ rawChildren: readonly unknown[], triviaLog: readonly number[], state: unknown,
2418
+ ) => unknown
2419
+ : undefined
2420
+
2421
+ // A STRUCTURAL node — no builder, no projection — is the only shape that
2422
+ // takes a per-node-type trivia-kind mask off the host (`node.ts:260`).
2423
+ // Assembly-time, so a node with a reducer pays nothing for the question.
2424
+ const structural = build === undefined && proj < 0
2425
+ const grammarCapture = (flags & 1) !== 0 || trailingTrivia
2426
+ const hostCapturesThisType = structural && cfg.hostCaptureTrivia !== undefined
2427
+ ? cfg.hostCaptureTrivia(type)
2428
+ : undefined
2429
+ const wantFields = hasFields || hostCst
2430
+ const captureWide = readsTrivia || hostCst
2431
+ ? !structural || grammarCapture || hostCapturesThisType !== false
2432
+ : hostCapturesThisType === true
2433
+ // Only a plain structural host call can omit semantic children. Every
2434
+ // operation that consumes them keeps the lazy buffer, including either
2435
+ // grammar collapse form and the host collapse predicate.
2436
+ const keepChildren = !structural || cfg.hostReadsChildren !== false || collapse || unwrap
2437
+ const rawOnly = !keepChildren
2438
+ return (input, pos, ctx) => {
2439
+ const host = HOST
2440
+ // `beginCstNodeCapture`/`endCstNodeCapture` INLINED, and the two objects
2441
+ // they exchange — the six-field `saved` and the three-field
2442
+ // `{children, rawChildren, triviaLog}` — held in locals instead.
2443
+ //
2444
+ // This is the same scalarisation `rollbackCstCaptureAt` already documents
2445
+ // for the five-field mark, applied to the node piece: `node()` runs
2446
+ // 145,512 times per parse of `benchmark.less`, so those were ~291k
2447
+ // allocations a parse that the compiled engine (which inlines its own
2448
+ // capture prologue and epilogue) never makes.
2449
+ const sCh = ctx._cstChildren
2450
+ const sLv = ctx._cstLeaves
2451
+ const sRaw = ctx._cstRawChildren
2452
+ const sTl = ctx._cstTriviaLog
2453
+ const sCap = ctx.captureTrivia
2454
+ const sBuf = ctx._cstBuf
2455
+ const buf: CstCaptureBuf = rawOnly ? { rawOnly: true } : {}
2456
+ ctx._cstBuf = buf
2457
+ ctx._cstChildren = undefined
2458
+ ctx._cstLeaves = undefined
2459
+ ctx._cstRawChildren = undefined
2460
+ ctx._cstTriviaLog = undefined
2461
+ // `begin` sets this true and the caller immediately cleared it when the
2462
+ // node does not capture wide. Net: the flag IS `captureWide`, a const.
2463
+ ctx.captureTrivia = captureWide
2464
+ const savedFields = ctx._fields
2465
+ ctx._fields = wantFields ? [] : undefined
2466
+ // PER-NODE-TYPE TRIVIA-KIND MASK, scoped to the body and restored —
2467
+ // `node.ts:259-273`, and codegen's `_triviaCaptureMask` save/install/
2468
+ // restore around the same scope (`codegen.ts:4130-4136`). The table
2469
+ // installed nothing, so a host that asked `Outer` for comments only got
2470
+ // every kind the scanner saw.
2471
+ const savedMask = structural ? ctx._triviaCaptureMask : undefined
2472
+ if (structural && host?._parsemanTriviaKinds !== undefined) {
2473
+ ctx._triviaCaptureMask = host._parsemanTriviaKinds(type)
2474
+ }
2475
+ const v = child(input, pos, ctx)
2476
+ if (v !== FAIL && trailingTrivia && ctx.trivia !== undefined) EC.e = consumeTrivia(input, EC.e, ctx)
2477
+ const fieldMap: FieldMap | undefined = wantFields ? buildFieldMap(ctx._fields) : undefined
2478
+ ctx._fields = savedFields
2479
+ if (structural) ctx._triviaCaptureMask = savedMask
2480
+ const kids = rawOnly ? EMPTY_CH : buf.ch ?? (buf.single !== undefined ? [buf.single] : EMPTY_CH)
2481
+ const hostKids = kids
2482
+ const rawKids = buf.raw ?? (buf.rawSingle !== undefined ? [buf.rawSingle] : EMPTY_CH)
2483
+ const tlog = buf.tl ?? EMPTY_TLOG
2484
+ ctx._cstBuf = sBuf
2485
+ ctx._cstChildren = sCh
2486
+ ctx._cstLeaves = sLv
2487
+ ctx._cstRawChildren = sRaw
2488
+ ctx._cstTriviaLog = sTl
2489
+ ctx.captureTrivia = sCap
2490
+ if (v === FAIL) return FAIL
2491
+ const end = EC.e
2492
+ const span = tracked ? spanLines(ctx, pos, end) : { start: pos, end }
2493
+ const st = readsState && ctx.state !== undefined
2494
+ ? Object.assign({}, ctx.state as Record<string, unknown>)
2495
+ : undefined
2496
+
2497
+ let nd: unknown
2498
+ if (unwrap && kids.length === 1) {
2499
+ nd = unwrapChild(kids[0])
2500
+ } else if (collapse && kids.length === 1) {
2501
+ nd = kids[0]
2502
+ } else if (
2503
+ // HOST COLLAPSE — applies wherever the node's VALUE comes from the
2504
+ // host, which is any node under a CST host, not only builder-less ones.
2505
+ (hostCst || (build === undefined && proj < 0))
2506
+ && keepChildren
2507
+ && host?._parsemanCstCollapse !== undefined
2508
+ && kids.length === 1
2509
+ && rawKids.length === 1
2510
+ && host._parsemanCstCollapse(type, kids[0], kids, rawKids)
2511
+ ) {
2512
+ nd = kids[0]
2513
+ } else if (proj >= 0) {
2514
+ nd = hostCst && host !== undefined
2515
+ ? host(type, kids, fieldMap, span, rawKids, tlog, readsState ? st : ctx.state !== undefined ? Object.assign({}, ctx.state as Record<string, unknown>) : undefined, tags)
2516
+ : projectChild(kids, proj, type)
2517
+ } else if (build !== undefined) {
2518
+ if (hostCst && host !== undefined) {
2519
+ // A direct builder is bypassed under a CST host.
2520
+ nd = host(type, kids, fieldMap, span, rawKids, tlog, readsState ? st : ctx.state !== undefined ? Object.assign({}, ctx.state as Record<string, unknown>) : undefined, tags)
2521
+ } else {
2522
+ nd = build(kids, fieldMap, span, rawKids, captureWide ? tlog : EMPTY_TL, st)
2523
+ }
2524
+ } else if (host !== undefined) {
2525
+ nd = host(type, hostKids, fieldMap, span, rawKids, tlog, st, tags)
2526
+ } else {
2527
+ nd = { _tag: 'node', type, span, state: st ?? null, children: kids }
2528
+ }
2529
+ // A ROOT NODE IS NOT A CHILD. `node()` pushes only when it was entered
2530
+ // from an enclosing collector — `saved.buf !== undefined || saved.ch !==
2531
+ // undefined` (node.ts:326). An outer `_cstLeaves` alone is a caller's
2532
+ // own sink, not a parent node, and pushing into it published the root
2533
+ // node as a leaf of itself.
2534
+ if (sBuf !== undefined || sCh !== undefined) {
2535
+ pushCstChild(ctx, nd, rawEntry(nd, input, pos, end))
2536
+ }
2537
+ EC.e = end
2538
+ return nd
2539
+ }
2540
+ }
2541
+
2542
+ default:
2543
+ throw new Error(`table assembler: unknown opcode ${String(op)} at ${ip}`)
2544
+ }
2545
+ }
2546
+
2547
+ /* ── scans, built once per assembly exactly as `exec.ts` builds them ─────── */
2548
+
2549
+ function subtreeComb(r: SubtreeRef, def?: ParserDef): Combinator<unknown> {
2550
+ const piece = pieceAt(r[0])
2551
+ // THE END SLOT IS ONE CELL FOR THE WHOLE ASSEMBLY (`cell.ts`), so this
2552
+ // reads the end of whichever engine ran the piece without asking which.
2553
+ // It used to select `emitted.end` or this file's own slot, which was
2554
+ // correct only because exactly one engine was ever live: an emitted piece
2555
+ // writing its own private slot left this file's at zero, and a scan
2556
+ // sentinel read that as an empty span.
2557
+ return {
2558
+ _tag: 'tableSubtree',
2559
+ _meta: { firstSet: refFirstSet(r[1]), canMatchNewline: true, isTrivia: false },
2560
+ _def: def ?? { tag: 'unknown' } as unknown as ParserDef,
2561
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<unknown> {
2562
+ const v = piece(input, pos, ctx)
2563
+ if (v === FAIL) {
2564
+ const fe = ctx._fe
2565
+ const at = fe === undefined || fe < 0 ? pos : fe
2566
+ return { ok: false, expected: (ctx._fx ?? EMPTY_FX) as string[], span: { start: at, end: at } }
2567
+ }
2568
+ return { ok: true, value: v, span: { start: pos, end: EC.e } }
2569
+ },
2570
+ }
2571
+ }
2572
+
2573
+ function refFirstSet(cls: number): FirstSet {
2574
+ if (cls === -2) return { kind: 'empty' }
2575
+ if (cls < 0) return { kind: 'any' }
2576
+ const spec = prog.cc[cls] ?? ''
2577
+ const ranges = decodeClassSpec(spec)
2578
+ return { kind: 'ranges', ranges }
2579
+ }
2580
+
2581
+ /* ── U4: the EMITTED assembly, tried before the closure one ─────────────── */
2582
+
2583
+ /**
2584
+ * Emitted source is attempted FIRST, and the closure walk above runs only for
2585
+ * what it refuses.
2586
+ *
2587
+ * The reason it has to be source at all is in `emit-assembly.ts`'s header:
2588
+ * V8's inline-cache feedback is per FUNCTION LITERAL, so every piece minted
2589
+ * from one literal here shares one feedback vector and every call site inside
2590
+ * it is megamorphic. That is not a property this file can rearrange away.
2591
+ *
2592
+ * The refusal is RECORDED, not swallowed. A grammar that silently drops to
2593
+ * the closure path is a permanently slow path nobody would ever find, which
2594
+ * is the same failure `encode.ts:1208-1213` refuses for `OP_LIVE`.
2595
+ */
2596
+ const scansArr: Combinator<unknown>[] = []
2597
+ let emitted: EmittedAssembly | undefined
2598
+ /** "The build already did this" — not a refusal, and never a `emitRefusal`. */
2599
+ class ServedByBuild extends Error {}
2600
+ let emitRefusal: string | undefined
2601
+ let emitReached: ReadonlySet<number> | undefined
2602
+ {
2603
+ // The scan pool and the scan-skip sets are built FROM subtrees, so their
2604
+ // sites need emitted names too or `subtreeComb` would have to fall back to
2605
+ // a closure for them and the two engines would both be live in one parse.
2606
+ const extraIps: number[] = []
2607
+ for (const s of prog.scans ?? []) {
2608
+ for (const r of s.skip) extraIps.push(r[0])
2609
+ if (s.sentinel !== undefined) extraIps.push(s.sentinel[0])
2610
+ }
2611
+ for (const set of prog.scanSkip ?? []) for (const r of set) extraIps.push(r[0])
2612
+ const validationRoots = [...Object.values(prog.rules), ...extraIps]
2613
+ for (const ip of reachableSites(code, validationRoots)) {
2614
+ if (code[ip] !== OP_DISPATCH) continue
2615
+ validateDispatchSpec(dsp[code[ip + 2]!], code[ip + 5]!, code[ip + 4]!)
2616
+ }
2617
+ /**
2618
+ * THE BUILD'S OWN ANSWER, TRIED BEFORE THE CONSTRUCTOR.
2619
+ *
2620
+ * `prog.asm` carries assemblies a build-time emitter already compiled, one
2621
+ * per option set. When this parse's option set is among them there is
2622
+ * nothing to compile: the factory is an ordinary function literal in the
2623
+ * emitted module, and the only work left is rebuilding the three data pools
2624
+ * from `plan` (see `PoolPlan` — allocation, no source, no eval).
2625
+ *
2626
+ * This is what makes `docs/guide/modes.md` and `docs/reference/api.md` TRUE.
2627
+ * Both said a macro build is the answer for a CSP environment without
2628
+ * `unsafe-eval`; until this existed, the first `parse()` of a macro-built
2629
+ * table called `new Function` like every other path and the promise was
2630
+ * decided by nothing. `test/unit/no-function-constructor.test.ts` decides it
2631
+ * now, by counting constructor calls across a real parse.
2632
+ */
2633
+ // A build-time assembly cannot know a runtime host's per-type trivia
2634
+ // predicate. Never serve the scalar-keyed default factory for a
2635
+ // predicate-specialised parse; lower a fresh host-owned assembly instead.
2636
+ const pre = cfg.hostCaptureTrivia === undefined
2637
+ ? prog.asm?.find(a => a.key === cfgKey(cfg))
2638
+ : undefined
2639
+ if (pre !== undefined) {
2640
+ const pools = rebuildPools(t.cc, t.fx, pre.plan)
2641
+ emitted = pre.factory(
2642
+ EC, FAIL, k, fx, fns, pools.masks, pools.classes, pools.armExpected, trivia,
2643
+ trivia.map(tv => tv?._meta.triviaKindLabels), triviaScan,
2644
+ scansArr, disp, dsp, EMPTY_FX, EMPTY_CH, EMPTY_TLOG, EMPTY_TL,
2645
+ cstCaptureActive, pushCstLeaf, pushCstChild, rollbackTriviaAt, rollbackScannedTriviaAt, failAt,
2646
+ classHas, consumeTrivia, buildFieldMap, projectChild, unwrapChild,
2647
+ demoteCapturedToRaw, cstLeavesLen, skipTriviaScanned, needsDeferredTriviaCommit,
2648
+ scanTrivia, advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
2649
+ asciiFoldKey, ROUTED_FX,
2650
+ REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
2651
+ matchesAt, recoverScan, orSentinel, captureError,
2652
+ )
2653
+ emitReached = new Set(pre.reached)
2654
+ }
2655
+ /**
2656
+ * ONCE A PROGRAM CARRIES ASSEMBLIES, THE CONSTRUCTOR IS OFF FOR IT — for
2657
+ * EVERY option set, not just the ones that were pre-compiled.
2658
+ *
2659
+ * "Prefer the pre-compiled factory, fall back to `new Function`" is how this
2660
+ * defect survives forever: the fallback becomes load-bearing, production
2661
+ * takes it, and nobody can tell which path ran. So a macro-built artifact
2662
+ * whose option set was not pre-compiled runs the CLOSURE engine, and the
2663
+ * reason is on `Assembly.emitRefusal` where `test/unit/table-assemble.test.ts`
2664
+ * and anyone debugging can read it. Slower, correct, and observable — the
2665
+ * three properties a silent eval had none of.
2666
+ *
2667
+ * A program with NO `asm` at all is an explicit low-level hand-built
2668
+ * `tableRules(prog)` input. Compiler-created and macro-created artifacts
2669
+ * always carry the field, including the canonical empty inventory.
2670
+ */
2671
+ try {
2672
+ // Served above by a pre-compiled factory. Nothing to compile, nothing to
2673
+ // refuse — and in particular no constructor call.
2674
+ if (emitted !== undefined) throw new ServedByBuild()
2675
+ if (prog.asm !== undefined) {
2676
+ throw new Unemittable(
2677
+ `option set ${cfgKey(cfg)} (hostCst=${cfg.hostCst} trackLines=${cfg.trackLines} `
2678
+ + `tolerant=${cfg.tolerant} coverage=${cfg.coverage} probe=${cfg.probe}), which this build `
2679
+ + 'did not pre-compile. The Function constructor is NOT used for an artifact that carries '
2680
+ + 'assemblies — see `EmitOptions.assemblies` to add this option set to the build',
2681
+ )
2682
+ }
2683
+ // THE A/B TOGGLE, read ONCE at module load and never on a parse path.
2684
+ // Two engines that can only be compared across two checkouts cannot be
2685
+ // compared at all: the bench harness's own guidance is that a
2686
+ // cross-worktree measurement carries a bias no repetition removes. Same
2687
+ // spelling as `PM_TABLE_COUNT` (`bench/table-opcode-gaps.ts`).
2688
+ if (!EMIT_ENABLED) throw new Unemittable('PM_TABLE_EMIT=0 (measurement toggle)')
2689
+ const em = emitAssemblySource(t, prog, cfg, extraIps)
2690
+ /**
2691
+ * COMPILING THE TEXT — and the two ways it can fail are NOT the same.
2692
+ *
2693
+ * An `EvalError` is the ENVIRONMENT refusing: a Content-Security-Policy
2694
+ * without `unsafe-eval` forbids the `Function` constructor. That is a
2695
+ * legitimate refusal and the closure engine is the correct answer.
2696
+ *
2697
+ * A `SyntaxError` is THIS EMITTER having generated invalid JavaScript.
2698
+ * That is a bug in `emit-assembly.ts`, and catching it would ship a
2699
+ * permanently slow path that is also permanently undiagnosed — every
2700
+ * grammar would quietly run closures because of one bad template. It
2701
+ * propagates, with the offending source attached.
2702
+ */
2703
+ let factory: EmittedFactory
2704
+ try {
2705
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
2706
+ factory = new Function(...EMITTED_PARAMS, em.source) as EmittedFactory
2707
+ } catch (e) {
2708
+ if (e instanceof SyntaxError) {
2709
+ throw new Error(
2710
+ `table emitter: generated invalid JavaScript (${e.message}). This is a defect in `
2711
+ + 'emit-assembly.ts, not a property of the grammar.\n' + em.source,
2712
+ )
2713
+ }
2714
+ throw new Unemittable(`the Function constructor (${String(e)})`)
2715
+ }
2716
+ emitted = factory(
2717
+ EC, FAIL, k, fx, fns, em.masks, em.classes, em.armExpected, trivia,
2718
+ trivia.map(tv => tv?._meta.triviaKindLabels), triviaScan,
2719
+ scansArr, disp, dsp, EMPTY_FX, EMPTY_CH, EMPTY_TLOG, EMPTY_TL,
2720
+ cstCaptureActive, pushCstLeaf, pushCstChild, rollbackTriviaAt, rollbackScannedTriviaAt, failAt,
2721
+ classHas, consumeTrivia, buildFieldMap, projectChild, unwrapChild,
2722
+ demoteCapturedToRaw, cstLeavesLen, skipTriviaScanned, needsDeferredTriviaCommit,
2723
+ scanTrivia, advanceTrivia, refuseUnclassifiedRootScope, spanLines, rawEntry, lead,
2724
+ asciiFoldKey, ROUTED_FX,
2725
+ // THE SENTINEL POOL, dense over char-class indices so the emitted text can
2726
+ // index it the way it indexes `SCANS`. Built only for a recovery assembly
2727
+ // — a strict one emits no reader — and through the SAME `sentinelFor` memo
2728
+ // the closure pieces use, so both engines resync to the same combinator
2729
+ // object rather than to two separately constructed ones.
2730
+ REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
2731
+ matchesAt, recoverScan, orSentinel, captureError,
2732
+ )
2733
+ emitReached = em.reached
2734
+ } catch (e) {
2735
+ if (e instanceof ServedByBuild) { /* already emitted; no refusal to record */ }
2736
+ else if (e instanceof Unemittable) emitRefusal = e.construct
2737
+ else throw e
2738
+ }
2739
+ }
2740
+
2741
+ /** One piece for a site, from whichever engine this assembly is running. */
2742
+ function pieceAt(ip: number): Piece {
2743
+ const em = emitted
2744
+ if (em !== undefined) {
2745
+ const p = em.byIp[ip]
2746
+ if (p === undefined) throw new Error(`table emitter: no emitted body for site ${ip}`)
2747
+ return p as Piece
2748
+ }
2749
+ return link(ip)
2750
+ }
2751
+
2752
+ for (const s of prog.scans ?? []) {
2753
+ const skip = s.skip.map(r => subtreeComb(r))
2754
+ const raw = (s.flags & 1) !== 0
2755
+ if (s.kind === 1) {
2756
+ scansArr.push(balanced(s.open!, s.close!, { skip, raw, strict: (s.flags & 4) !== 0 }) as Combinator<unknown>)
2757
+ continue
2758
+ }
2759
+ const sentDef: ParserDef | undefined = typeof s.sent === 'string'
2760
+ ? { tag: 'literal', value: s.sent, caseInsensitive: false } as unknown as ParserDef
2761
+ : undefined
2762
+ scansArr.push(scanTo(subtreeComb(s.sentinel!, sentDef), { skip, raw, orEOF: (s.flags & 2) !== 0 }) as Combinator<unknown>)
2763
+ }
2764
+ const scans: readonly Combinator<unknown>[] = scansArr
2765
+
2766
+ const scanSkip: readonly (readonly Combinator<unknown>[])[] =
2767
+ (prog.scanSkip ?? []).map(set => set.map(r => subtreeComb(r)))
2768
+
2769
+ /* ── link the rules ──────────────────────────────────────────────────────── */
2770
+
2771
+ if (emitted !== undefined) {
2772
+ const em = emitted
2773
+ return {
2774
+ pieces: em.pieces as Record<string, Piece>,
2775
+ end: em.end,
2776
+ begin: em.begin,
2777
+ finish: em.finish,
2778
+ scanSkip,
2779
+ reached: emitReached!,
2780
+ emitRefusal: undefined,
2781
+ }
2782
+ }
2783
+
2784
+ const pieces: Record<string, Piece> = {}
2785
+ for (const [name, entryIp] of Object.entries(prog.rules)) pieces[name] = link(entryIp)
2786
+
2787
+ /**
2788
+ * PER PARSE. `SCAN` starts null — a rule reached before any scope has no
2789
+ * installed trivia, and a stale one from a previous parse on a reused `ctx`
2790
+ * would skip trivia this grammar never declared.
2791
+ *
2792
+ * `FAST` and `HOSTCST` are gone: both were options, and both are now baked into
2793
+ * which pieces this assembly holds. What is left is the one piece of
2794
+ * configuration that is a per-parse VALUE rather than a per-parse FACT — the
2795
+ * host itself — and it is read here, once, instead of per node.
2796
+ */
2797
+ function begin(ctx: ParseContext): void {
2798
+ // Read user-owned values before touching the active frame. A getter that
2799
+ // throws must not leave an outer parse suspended.
2800
+ const nextHost = ctx.build
2801
+ const nextCoverage = ctx._grammarCoverage ?? NO_COVERAGE
2802
+ if (depth > 0) frames.push({ scan: SCAN, host: HOST, coverage: COV, end: EC.e })
2803
+ depth++
2804
+ SCAN = null
2805
+ HOST = nextHost
2806
+ COV = nextCoverage
2807
+ }
2808
+
2809
+ function finish(): void {
2810
+ if (depth <= 0) throw new Error('parseman table assembly frame underflow')
2811
+ depth--
2812
+ if (depth === 0) return
2813
+ const prior = frames.pop()!
2814
+ SCAN = prior.scan
2815
+ HOST = prior.host
2816
+ COV = prior.coverage
2817
+ EC.e = prior.end
2818
+ }
2819
+
2820
+ return { pieces, end: () => EC.e, begin, finish, scanSkip, reached, emitRefusal }
2821
+ }
2822
+
2823
+ /**
2824
+ * Assemblies for one resolved table, one per option set, built on demand.
2825
+ *
2826
+ * The option set is not known when the rule map is created — `ctx.build`,
2827
+ * `ctx.trackLines` and `ctx._tolerant` arrive with the parse — so the entry
2828
+ * computes the scalar key and takes the assembly for it, building it the first
2829
+ * time that combination is seen. That is the "assembled at run start" half of G5:
2830
+ * only the option combinations a process actually uses, each holding the pieces
2831
+ * its options reach. A process that never parses tolerantly never builds, and
2832
+ * never runs, a single recovery piece.
2833
+ */
2834
+ export class AssemblyCache {
2835
+ private readonly t: ResolvedTable
2836
+ private readonly prog: TableProgram
2837
+ private readonly byCfg: Array<Assembly | undefined> = Array.from({ length: 64 })
2838
+ /**
2839
+ * Monomorphic predicate inline cache. An assembly depends on the predicate
2840
+ * FUNCTION and the scalar cfg key, not on the host object that carries it, so
2841
+ * retaining the host would add neither correctness nor reuse. Stable parsing
2842
+ * is one identity comparison plus an array index; replacing the predicate
2843
+ * replaces this specialisation without retaining either host.
2844
+ */
2845
+ private hostPredicate: ((type: string) => boolean) | undefined
2846
+ private hostAssemblies: Array<Assembly | undefined> | undefined
2847
+
2848
+ constructor(prog: TableProgram, resolved: ResolvedTable = resolveTable(prog)) {
2849
+ this.prog = prog
2850
+ this.t = resolved
2851
+ }
2852
+
2853
+ for(cfg: RunCfg): Assembly {
2854
+ const key = cfgKey(cfg)
2855
+ // No host identity is available to own this predicate specialisation.
2856
+ if (cfg.hostCaptureTrivia !== undefined) return assemble(this.t, this.prog, cfg)
2857
+ const hit = this.byCfg[key]
2858
+ if (hit !== undefined) return hit
2859
+ const made = assemble(this.t, this.prog, cfg)
2860
+ this.byCfg[key] = made
2861
+ return made
2862
+ }
2863
+
2864
+ /**
2865
+ * The assembly for the option set this `ctx` implies.
2866
+ *
2867
+ * ─── WHY THIS IS NOT A VIOLATION OF "NO OPTION READS AT PARSE TIME" ─────────
2868
+ *
2869
+ * The rule that shaped 0.47 is that consulting an option PER RULE or PER
2870
+ * COMBINATOR is a fail, and the reason it is a fail is that such a consult
2871
+ * scales with the input. This one does not, and the previous defence of it —
2872
+ * "cheap, allocation-free, only once" — was the wrong argument, because
2873
+ * cheapness is not the criterion. The right argument is that the consult is
2874
+ * IRREDUCIBLE, and it is worth stating precisely so nobody re-opens it:
2875
+ *
2876
+ * A table entry has the artifact signature `(input, pos, ctx)`, shared with
2877
+ * codegen. All scalar option bits live on that `ctx`, and every one of them is
2878
+ * supplied PER CALL by the caller — `run()` takes `tolerant`, `build` and
2879
+ * `instrumentation` as options on an artifact it was handed;
2880
+ * `combinators/grammar.ts` sets `trackLines` on scope entry;
2881
+ * `completionsAt` installs `_probe`. So the option set is not knowable
2882
+ * before the call, and a selection that cannot happen before the call must
2883
+ * happen at it.
2884
+ *
2885
+ * That is not a hole in G5, it is G5's own first clause: "quickly building the
2886
+ * grammar reference ON RUN START, making some swaps on rules or sub-rules, and
2887
+ * then the run actually runs with no logic branching for that option input"
2888
+ * (`notes/TABLE-DRIVER.md`). This IS the run-start step. What the criterion
2889
+ * forbids is the second sentence, and past this call there is no option read
2890
+ * anywhere — `scripts/check-invariants.mjs` INV-6 decides that mechanically.
2891
+ *
2892
+ * MEASURED, not asserted: exactly ONE call per entry invocation, including
2893
+ * `benchmark.less` at 106,802 bytes. Eliminating it would require binding the
2894
+ * option set to the ARTIFACT rather than to the CALL, which is a change to the
2895
+ * public run API (the map would have to hand back a cfg-keyed family and
2896
+ * `run()` index it), not a change to this file.
2897
+ *
2898
+ * It allocates nothing: the key is packed from the ctx's own bits by `cfgKeyOf`,
2899
+ * which takes them as arguments precisely so no `RunCfg` need exist here — that
2900
+ * object is built only on the miss that builds an assembly. A host trivia
2901
+ * predicate additionally selects its identity-specialised inline cache
2902
+ * because a function cannot be represented by a scalar bit.
2903
+ *
2904
+ * DO NOT CACHE THE RESULT ACROSS CALLS. Keying it on anything but the `ctx`'s
2905
+ * own option bits is how `tableRules` handed a strict parse the PREVIOUS
2906
+ * parse's tolerant assembly (`test/unit/table-assemble.test.ts`).
2907
+ */
2908
+ forCtx(ctx: ParseContext): Assembly {
2909
+ const host = ctx.build
2910
+ const hostCst = host !== undefined && cstOutputHost(host)
2911
+ const hostReadsChildren = host?._parsemanReadsChildren !== false
2912
+ || host?._parsemanCstCollapse !== undefined
2913
+ const hostCaptureTrivia = host?._parsemanCaptureTrivia
2914
+ const trackLines = ctx.trackLines === true
2915
+ const tolerant = ctx._tolerant === true
2916
+ const coverage = ctx._grammarCoverage !== undefined
2917
+ const probe = ctx._probe !== undefined
2918
+ const key = cfgKeyOf(hostCst, trackLines, tolerant, coverage, probe, hostReadsChildren)
2919
+ let cache = this.byCfg
2920
+ if (hostCaptureTrivia !== undefined) {
2921
+ // The predicate is host configuration, not parse state. Specialising each
2922
+ // node once keeps it out of the hot path, so its behaviour must stay stable
2923
+ // for this function identity. Replacing the function is supported: the
2924
+ // identity check below installs a fresh cache when it is replaced.
2925
+ if (this.hostPredicate === hostCaptureTrivia) {
2926
+ cache = this.hostAssemblies!
2927
+ }
2928
+ else {
2929
+ cache = Array.from({ length: 64 })
2930
+ this.hostPredicate = hostCaptureTrivia
2931
+ this.hostAssemblies = cache
2932
+ }
2933
+ }
2934
+ const hit = cache[key]
2935
+ if (hit !== undefined) return hit
2936
+ const made = assemble(this.t, this.prog, {
2937
+ hostCst, hostReadsChildren, hostCaptureTrivia,
2938
+ trackLines, tolerant, coverage, probe,
2939
+ })
2940
+ cache[key] = made
2941
+ return made
2942
+ }
2943
+ }
2944
+
2945
+ /**
2946
+ * The ASSEMBLED rule map — the same artifact contract as `tableRules`, run
2947
+ * through linked closures instead of the bytecode interpreter.
2948
+ *
2949
+ * THE ONE config read is HERE, at the boundary, once per entry invocation:
2950
+ * `AssemblyCache.forCtx` turns the `ctx` into a scalar option set and takes the
2951
+ * assembly built for it. Everything past that point is pieces, and no piece body
2952
+ * reads an option — `scripts/check-invariants.mjs` INV-6 asserts it. `forCtx`
2953
+ * carries the argument for why that read is irreducible rather than merely cheap.
2954
+ */
2955
+ export function tableRules(
2956
+ source: TableProgram | CompactProgram,
2957
+ artifactMetadata: Readonly<Record<symbol, unknown>> = {},
2958
+ ): Record<string, TableRule> {
2959
+ const prog = expandCompact(source)
2960
+ const resolved = resolveTable(prog)
2961
+ const cache = new AssemblyCache(prog, resolved)
2962
+ const names = Object.keys(prog.rules)
2963
+ const skipOf = prog.scanSkipOf
2964
+ let last: unknown
2965
+ /**
2966
+ * THE ASSEMBLY THIS ENTRY INVOCATION SELECTED — handed from `scanSkipFor` to
2967
+ * the `runRule` that `stamp.ts`'s entry runs immediately afterwards, on the
2968
+ * SAME `ctx`, so the pair costs one `forCtx` between them rather than two.
2969
+ *
2970
+ * It used to live ACROSS parses ("the lookup is memoised, so this is an array
2971
+ * index after the first parse"), and that was wrong: `scanSkipFor` runs before
2972
+ * `runRule` has re-selected, so a strict parse following a tolerant one was
2973
+ * installed with the TOLERANT assembly's `scanSkip` — verified by object
2974
+ * identity, since the two assemblies wrap their own pieces. Amortising a
2975
+ * per-parse selection over parses is not amortisation, it is a stale answer.
2976
+ *
2977
+ * `runRule` CONSUMES it, so it can never outlive the invocation that set it:
2978
+ * an entry reached without the preceding `scanSkipFor` re-selects, and a
2979
+ * nested entry invocation sets and consumes its own inside the outer
2980
+ * `runRule`, after the outer read.
2981
+ */
2982
+ let selected: Assembly | undefined
2983
+ return stampRuleMap(prog, {
2984
+ runRule: (ri, input, pos, ctx) => {
2985
+ const a = selected ?? cache.forCtx(ctx)
2986
+ selected = undefined
2987
+ a.begin(ctx)
2988
+ try {
2989
+ const v = a.pieces[names[ri]!]!(input, pos, ctx)
2990
+ if (v === FAIL) return -1
2991
+ last = v
2992
+ return a.end()
2993
+ } finally {
2994
+ // A host callback or direct reducer can run this same map before its
2995
+ // outer entry returns. Restore the assembly frame on every exit.
2996
+ a.finish()
2997
+ }
2998
+ },
2999
+ lastValue: () => last,
3000
+ scanSkipFor: (ri, ctx) => {
3001
+ const a = cache.forCtx(ctx)
3002
+ selected = a
3003
+ return a.scanSkip[skipOf?.[ri] ?? -1]
3004
+ },
3005
+ }, artifactMetadata, resolved)
3006
+ }