parseman 0.46.0 → 0.47.0

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