parseman 0.45.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/README.md +8 -3
  2. package/dist/analysis/choice-cost-gate.d.ts +158 -0
  3. package/dist/analysis/choice-cost-gate.d.ts.map +1 -0
  4. package/dist/analysis/choice-cost-render.d.ts +96 -0
  5. package/dist/analysis/choice-cost-render.d.ts.map +1 -0
  6. package/dist/analysis/choice-cost.d.ts +383 -0
  7. package/dist/analysis/choice-cost.d.ts.map +1 -0
  8. package/dist/analysis/commitment.d.ts +131 -0
  9. package/dist/analysis/commitment.d.ts.map +1 -0
  10. package/dist/analysis/corpus.d.ts +73 -0
  11. package/dist/analysis/corpus.d.ts.map +1 -0
  12. package/dist/analysis/diagnose-render.d.ts +65 -0
  13. package/dist/analysis/diagnose-render.d.ts.map +1 -0
  14. package/dist/analysis/diagnose.d.ts +15 -0
  15. package/dist/analysis/diagnose.d.ts.map +1 -1
  16. package/dist/analysis/diagnostics.cjs +13736 -0
  17. package/dist/analysis/diagnostics.cjs.map +6 -0
  18. package/dist/analysis/diagnostics.d.ts +27 -0
  19. package/dist/analysis/diagnostics.d.ts.map +1 -0
  20. package/dist/analysis/diagnostics.js +13713 -0
  21. package/dist/analysis/diagnostics.js.map +6 -0
  22. package/dist/analysis/duplication.d.ts.map +1 -1
  23. package/dist/analysis/fix-render.d.ts +43 -0
  24. package/dist/analysis/fix-render.d.ts.map +1 -0
  25. package/dist/analysis/fix.d.ts +172 -0
  26. package/dist/analysis/fix.d.ts.map +1 -0
  27. package/dist/analysis/format-number.d.ts +23 -0
  28. package/dist/analysis/format-number.d.ts.map +1 -0
  29. package/dist/analysis/gating.d.ts +47 -2
  30. package/dist/analysis/gating.d.ts.map +1 -1
  31. package/dist/analysis/rebuild.d.ts +61 -0
  32. package/dist/analysis/rebuild.d.ts.map +1 -0
  33. package/dist/analysis/terminal.d.ts +145 -0
  34. package/dist/analysis/terminal.d.ts.map +1 -0
  35. package/dist/cli/index.cjs +14837 -0
  36. package/dist/cli/index.cjs.map +6 -0
  37. package/dist/cli/index.d.ts +3 -0
  38. package/dist/cli/index.d.ts.map +1 -0
  39. package/dist/cli/index.js +14814 -0
  40. package/dist/cli/index.js.map +6 -0
  41. package/dist/combinators/adjacency.d.ts +100 -0
  42. package/dist/combinators/adjacency.d.ts.map +1 -0
  43. package/dist/combinators/choice.d.ts.map +1 -1
  44. package/dist/combinators/completions.d.ts.map +1 -1
  45. package/dist/combinators/dispatch.d.ts +8 -1
  46. package/dist/combinators/dispatch.d.ts.map +1 -1
  47. package/dist/combinators/expect.d.ts.map +1 -1
  48. package/dist/combinators/first-set.d.ts.map +1 -1
  49. package/dist/combinators/gate.d.ts.map +1 -1
  50. package/dist/combinators/grammar.d.ts +22 -0
  51. package/dist/combinators/grammar.d.ts.map +1 -1
  52. package/dist/combinators/literal.d.ts +8 -0
  53. package/dist/combinators/literal.d.ts.map +1 -1
  54. package/dist/combinators/map.d.ts +0 -1
  55. package/dist/combinators/map.d.ts.map +1 -1
  56. package/dist/combinators/node.d.ts +13 -3
  57. package/dist/combinators/node.d.ts.map +1 -1
  58. package/dist/combinators/parser.d.ts +1 -1
  59. package/dist/combinators/parser.d.ts.map +1 -1
  60. package/dist/combinators/ref.d.ts.map +1 -1
  61. package/dist/combinators/regex.d.ts +1 -16
  62. package/dist/combinators/regex.d.ts.map +1 -1
  63. package/dist/combinators/repeat.d.ts +30 -2
  64. package/dist/combinators/repeat.d.ts.map +1 -1
  65. package/dist/combinators/scanTo.d.ts +71 -1
  66. package/dist/combinators/scanTo.d.ts.map +1 -1
  67. package/dist/combinators/sequence.d.ts.map +1 -1
  68. package/dist/combinators/token.d.ts.map +1 -1
  69. package/dist/combinators/trivia-boundary.d.ts +26 -0
  70. package/dist/combinators/trivia-boundary.d.ts.map +1 -0
  71. package/dist/combinators/trivia-skip.d.ts +40 -1
  72. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  73. package/dist/combinators/withCtx.d.ts.map +1 -1
  74. package/dist/compiler/build-arity.d.ts.map +1 -1
  75. package/dist/compiler/compile-linkable-table.d.ts +104 -0
  76. package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
  77. package/dist/compiler/degradation.d.ts +0 -2
  78. package/dist/compiler/degradation.d.ts.map +1 -1
  79. package/dist/compiler/fields.d.ts +13 -0
  80. package/dist/compiler/fields.d.ts.map +1 -1
  81. package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
  82. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  83. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  84. package/dist/compiler/linker.d.ts +79 -47
  85. package/dist/compiler/linker.d.ts.map +1 -1
  86. package/dist/compiler/token-alphabet.d.ts +81 -0
  87. package/dist/compiler/token-alphabet.d.ts.map +1 -0
  88. package/dist/compiler/token-dispatch.d.ts +95 -0
  89. package/dist/compiler/token-dispatch.d.ts.map +1 -0
  90. package/dist/compiler/token-scanner.d.ts +53 -0
  91. package/dist/compiler/token-scanner.d.ts.map +1 -0
  92. package/dist/compiler/value-usage.d.ts.map +1 -1
  93. package/dist/coverage.d.ts +1 -1
  94. package/dist/coverage.d.ts.map +1 -1
  95. package/dist/cst/capture-buffer.d.ts +35 -0
  96. package/dist/cst/capture-buffer.d.ts.map +1 -1
  97. package/dist/cst/reflection-attach.d.ts +8 -0
  98. package/dist/cst/reflection-attach.d.ts.map +1 -0
  99. package/dist/cst/reflection-symbols.d.ts +10 -0
  100. package/dist/cst/reflection-symbols.d.ts.map +1 -0
  101. package/dist/cst/reflection.d.ts +2 -4
  102. package/dist/cst/reflection.d.ts.map +1 -1
  103. package/dist/cst/root-trivia-scope.d.ts +26 -0
  104. package/dist/cst/root-trivia-scope.d.ts.map +1 -0
  105. package/dist/cst/trivia-charscan.d.ts +18 -0
  106. package/dist/cst/trivia-charscan.d.ts.map +1 -0
  107. package/dist/cst/trivia-kinds.d.ts +19 -0
  108. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  109. package/dist/functional/doc.d.ts.map +1 -1
  110. package/dist/functional/run-tabled.d.ts +12 -0
  111. package/dist/functional/run-tabled.d.ts.map +1 -0
  112. package/dist/functional/run.d.ts +25 -42
  113. package/dist/functional/run.d.ts.map +1 -1
  114. package/dist/grammar-metadata.d.ts +3 -0
  115. package/dist/grammar-metadata.d.ts.map +1 -0
  116. package/dist/index.cjs +10499 -7478
  117. package/dist/index.cjs.map +3 -4
  118. package/dist/index.d.ts +24 -9
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +10499 -7478
  121. package/dist/index.js.map +3 -4
  122. package/dist/language-service/index.cjs +311 -121
  123. package/dist/language-service/index.cjs.map +3 -4
  124. package/dist/language-service/index.js +311 -121
  125. package/dist/language-service/index.js.map +3 -4
  126. package/dist/oracle/index.cjs.map +0 -1
  127. package/dist/oracle/index.js.map +0 -1
  128. package/dist/parse-context.d.ts +49 -0
  129. package/dist/parse-context.d.ts.map +1 -0
  130. package/dist/plugin/evaluator.d.ts +41 -2
  131. package/dist/plugin/evaluator.d.ts.map +1 -1
  132. package/dist/plugin/free-identifiers.d.ts +23 -0
  133. package/dist/plugin/free-identifiers.d.ts.map +1 -0
  134. package/dist/plugin/index.cjs +11381 -9442
  135. package/dist/plugin/index.cjs.map +3 -4
  136. package/dist/plugin/index.d.ts.map +1 -1
  137. package/dist/plugin/index.js +11381 -9442
  138. package/dist/plugin/index.js.map +3 -4
  139. package/dist/plugin/reducer-resolver.d.ts +17 -1
  140. package/dist/plugin/reducer-resolver.d.ts.map +1 -1
  141. package/dist/regex/classes.d.ts +28 -0
  142. package/dist/regex/classes.d.ts.map +1 -1
  143. package/dist/regex/first-set.d.ts +14 -0
  144. package/dist/regex/first-set.d.ts.map +1 -1
  145. package/dist/run/index.cjs +136 -51
  146. package/dist/run/index.cjs.map +2 -3
  147. package/dist/run/index.js +136 -51
  148. package/dist/run/index.js.map +2 -3
  149. package/dist/spec/index.cjs +2898 -1622
  150. package/dist/spec/index.cjs.map +3 -4
  151. package/dist/spec/index.js +2898 -1622
  152. package/dist/spec/index.js.map +3 -4
  153. package/dist/spec/model.d.ts +8 -0
  154. package/dist/spec/model.d.ts.map +1 -1
  155. package/dist/table/assemble.d.ts +307 -0
  156. package/dist/table/assemble.d.ts.map +1 -0
  157. package/dist/table/cell.d.ts +42 -0
  158. package/dist/table/cell.d.ts.map +1 -0
  159. package/dist/table/child-slots.d.ts +30 -0
  160. package/dist/table/child-slots.d.ts.map +1 -0
  161. package/dist/table/compile-rule-map.d.ts +136 -0
  162. package/dist/table/compile-rule-map.d.ts.map +1 -0
  163. package/dist/table/compile.d.ts +56 -0
  164. package/dist/table/compile.d.ts.map +1 -0
  165. package/dist/table/duplication-hook.d.ts +32 -0
  166. package/dist/table/duplication-hook.d.ts.map +1 -0
  167. package/dist/table/emit-assembly.d.ts +139 -0
  168. package/dist/table/emit-assembly.d.ts.map +1 -0
  169. package/dist/table/emit.d.ts +113 -0
  170. package/dist/table/emit.d.ts.map +1 -0
  171. package/dist/table/encode-baseline.d.ts +16 -0
  172. package/dist/table/encode-baseline.d.ts.map +1 -0
  173. package/dist/table/encode.d.ts +71 -0
  174. package/dist/table/encode.d.ts.map +1 -0
  175. package/dist/table/exec-baseline.d.ts +9 -0
  176. package/dist/table/exec-baseline.d.ts.map +1 -0
  177. package/dist/table/exec.d.ts +72 -0
  178. package/dist/table/exec.d.ts.map +1 -0
  179. package/dist/table/fold.d.ts +25 -0
  180. package/dist/table/fold.d.ts.map +1 -0
  181. package/dist/table/index.cjs +11375 -0
  182. package/dist/table/index.cjs.map +6 -0
  183. package/dist/table/index.d.ts +100 -0
  184. package/dist/table/index.d.ts.map +1 -0
  185. package/dist/table/index.js +11352 -0
  186. package/dist/table/index.js.map +6 -0
  187. package/dist/table/inspect.d.ts +17 -0
  188. package/dist/table/inspect.d.ts.map +1 -0
  189. package/dist/table/ops.d.ts +446 -0
  190. package/dist/table/ops.d.ts.map +1 -0
  191. package/dist/table/program.d.ts +493 -0
  192. package/dist/table/program.d.ts.map +1 -0
  193. package/dist/table/run-support.d.ts +38 -0
  194. package/dist/table/run-support.d.ts.map +1 -0
  195. package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
  196. package/dist/table/scan-shapes.d.ts.map +1 -0
  197. package/dist/table/site-labels.d.ts +58 -0
  198. package/dist/table/site-labels.d.ts.map +1 -0
  199. package/dist/table/stamp.d.ts +70 -0
  200. package/dist/table/stamp.d.ts.map +1 -0
  201. package/dist/types.d.ts +80 -24
  202. package/dist/types.d.ts.map +1 -1
  203. package/dist/version.d.ts +1 -1
  204. package/package.json +41 -3
  205. package/src/analysis/choice-cost-gate.ts +359 -0
  206. package/src/analysis/choice-cost-render.ts +293 -0
  207. package/src/analysis/choice-cost.ts +1008 -0
  208. package/src/analysis/commitment.ts +557 -0
  209. package/src/analysis/corpus.ts +130 -0
  210. package/src/analysis/diagnose-render.ts +625 -0
  211. package/src/analysis/diagnose.ts +317 -0
  212. package/src/analysis/diagnostics.ts +33 -0
  213. package/src/analysis/duplication.ts +1955 -0
  214. package/src/analysis/fix-render.ts +217 -0
  215. package/src/analysis/fix.ts +669 -0
  216. package/src/analysis/format-number.ts +31 -0
  217. package/src/analysis/gating.ts +792 -0
  218. package/src/analysis/grammar.ts +72 -0
  219. package/src/analysis/rebuild.ts +287 -0
  220. package/src/analysis/terminal.ts +336 -0
  221. package/src/cli/index.ts +413 -0
  222. package/src/combinators/adjacency.ts +171 -0
  223. package/src/combinators/attempt.ts +41 -0
  224. package/src/combinators/case-fold.ts +90 -0
  225. package/src/combinators/choice.ts +466 -0
  226. package/src/combinators/completions.ts +64 -0
  227. package/src/combinators/dispatch.ts +358 -0
  228. package/src/combinators/expect.ts +159 -0
  229. package/src/combinators/first-set.ts +322 -0
  230. package/src/combinators/gate.ts +50 -0
  231. package/src/combinators/grammar.ts +272 -0
  232. package/src/combinators/keywords.ts +145 -0
  233. package/src/combinators/literal.ts +102 -0
  234. package/src/combinators/map.ts +106 -0
  235. package/src/combinators/node.ts +345 -0
  236. package/src/combinators/not.ts +55 -0
  237. package/src/combinators/parser.ts +266 -0
  238. package/src/combinators/peek.ts +63 -0
  239. package/src/combinators/probe.ts +25 -0
  240. package/src/combinators/ref.ts +109 -0
  241. package/src/combinators/regex.ts +170 -0
  242. package/src/combinators/repeat.ts +561 -0
  243. package/src/combinators/scanTo.ts +393 -0
  244. package/src/combinators/sequence.ts +221 -0
  245. package/src/combinators/token.ts +120 -0
  246. package/src/combinators/trivia-boundary.ts +48 -0
  247. package/src/combinators/trivia-skip.ts +821 -0
  248. package/src/combinators/withCtx.ts +45 -0
  249. package/src/compiler/build-arity.ts +330 -0
  250. package/src/compiler/compile-linkable-table.ts +209 -0
  251. package/src/compiler/degradation.ts +266 -0
  252. package/src/compiler/fields.ts +191 -0
  253. package/src/compiler/grammar-coverage-ids.ts +129 -0
  254. package/src/compiler/ir-serialize.ts +615 -0
  255. package/src/compiler/linker.ts +954 -0
  256. package/src/compiler/token-alphabet.ts +209 -0
  257. package/src/compiler/token-dispatch.ts +353 -0
  258. package/src/compiler/token-scanner.ts +318 -0
  259. package/src/compiler/value-usage.ts +147 -0
  260. package/src/coverage.ts +383 -0
  261. package/src/cst/capture-buffer.ts +297 -0
  262. package/src/cst/host-mode.ts +69 -0
  263. package/src/cst/reflection-attach.ts +11 -0
  264. package/src/cst/reflection-symbols.ts +9 -0
  265. package/src/cst/reflection.ts +149 -0
  266. package/src/cst/relative-spans.ts +207 -0
  267. package/src/cst/root-trivia-scope.ts +30 -0
  268. package/src/cst/trivia-charscan.ts +312 -0
  269. package/src/cst/trivia-entries.ts +285 -0
  270. package/src/cst/trivia-index.ts +131 -0
  271. package/src/cst/trivia-kinds.ts +358 -0
  272. package/src/cst/types.ts +76 -0
  273. package/src/cst/walk.ts +101 -0
  274. package/src/functional/doc.ts +959 -0
  275. package/src/functional/run-tabled.ts +153 -0
  276. package/src/functional/run.ts +558 -0
  277. package/src/grammar-metadata.ts +2 -0
  278. package/src/index.ts +189 -0
  279. package/src/language-service/index.ts +221 -0
  280. package/src/line-index.ts +131 -0
  281. package/src/oracle/digest.ts +620 -0
  282. package/src/oracle/index.ts +55 -0
  283. package/src/parse-context.ts +91 -0
  284. package/src/plugin/direct-builder-static.ts +102 -0
  285. package/src/plugin/evaluator.ts +1505 -0
  286. package/src/plugin/free-identifiers.ts +338 -0
  287. package/src/plugin/index.ts +2281 -0
  288. package/src/plugin/reducer-resolver.ts +606 -0
  289. package/src/recovery/scan.ts +155 -0
  290. package/src/regex/classes.ts +183 -0
  291. package/src/regex/first-set.ts +316 -0
  292. package/src/run/index.ts +31 -0
  293. package/src/spec/ebnf.ts +128 -0
  294. package/src/spec/index.ts +54 -0
  295. package/src/spec/model.ts +636 -0
  296. package/src/spec/railroad-lib.ts +559 -0
  297. package/src/spec/railroad.ts +256 -0
  298. package/src/table/assemble.ts +2994 -0
  299. package/src/table/cell.ts +42 -0
  300. package/src/table/child-slots.ts +113 -0
  301. package/src/table/compile-rule-map.ts +336 -0
  302. package/src/table/compile.ts +244 -0
  303. package/src/table/duplication-hook.ts +89 -0
  304. package/src/table/emit-assembly.ts +1766 -0
  305. package/src/table/emit.ts +418 -0
  306. package/src/table/encode-baseline.ts +340 -0
  307. package/src/table/encode.ts +1569 -0
  308. package/src/table/exec-baseline.ts +537 -0
  309. package/src/table/exec.ts +1672 -0
  310. package/src/table/fold.ts +38 -0
  311. package/src/table/index.ts +108 -0
  312. package/src/table/inspect.ts +52 -0
  313. package/src/table/ops.ts +459 -0
  314. package/src/table/program.ts +855 -0
  315. package/src/table/run-support.ts +66 -0
  316. package/src/table/scan-shapes.ts +1578 -0
  317. package/src/table/site-labels.ts +228 -0
  318. package/src/table/stamp.ts +229 -0
  319. package/src/types.ts +563 -0
  320. package/src/version.ts +16 -0
  321. package/dist/compiler/codegen.d.ts +0 -334
  322. package/dist/compiler/codegen.d.ts.map +0 -1
  323. package/dist/compiler/inline-build.d.ts +0 -11
  324. package/dist/compiler/inline-build.d.ts.map +0 -1
  325. package/dist/compiler/inline-callback.d.ts +0 -21
  326. package/dist/compiler/inline-callback.d.ts.map +0 -1
  327. package/dist/compiler/line-index.d.ts +0 -2
  328. package/dist/compiler/line-index.d.ts.map +0 -1
  329. package/dist/compiler/scannable-run.d.ts.map +0 -1
  330. package/dist/compiler/scannable-terminal.d.ts +0 -23
  331. package/dist/compiler/scannable-terminal.d.ts.map +0 -1
  332. package/dist/compiler/trivia-fast-path.d.ts +0 -38
  333. package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
@@ -0,0 +1,1569 @@
1
+ import type { AutoNotCheck, Combinator, FirstSet, ParserDef } from '../types.ts'
2
+ import { firstSetOf, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
3
+ import { childrenOf } from '../analysis/gating.ts'
4
+ import { getCoreLiteralValue } from '../combinators/choice.ts'
5
+ import { deriveExpected } from '../combinators/expect.ts'
6
+ import { buildReadsState, buildReadsTrivia } from '../compiler/build-arity.ts'
7
+ import { buildReadsFields, parserHasOwnFields } from '../compiler/fields.ts'
8
+ import { asciiFoldKey, branchUsesRouted, parserUsesRouted } from '../combinators/dispatch.ts'
9
+ import {
10
+ OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
11
+ OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SEQ, OP_SEQV, OP_XFORM,
12
+ OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_EXPECT, OP_SEQX, OP_SCAN,
13
+ OP_LIVE, OP_ATTEMPT, OP_LABEL,
14
+ OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD,
15
+ OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_COV,
16
+ } from './ops.ts'
17
+ import { adjacencyExpected } from '../combinators/adjacency.ts'
18
+ import { resolveAdjacencyKindMask } from '../cst/trivia-kinds.ts'
19
+ import { missingInferredType } from '../combinators/node.ts'
20
+ import { hasOwnTriviaBoundary } from '../combinators/trivia-boundary.ts'
21
+ import type { BalancedSpec } from '../combinators/scanTo.ts'
22
+ import type { DispatchSpec, ScanSpec, SubtreeRef, TableProgram, TriviaSpec } from './program.ts'
23
+ import { covKindCode, encodeClassSpec, ownTableProgram } from './program.ts'
24
+ import type { GrammarCoveragePlan } from '../compiler/grammar-coverage-ids.ts'
25
+
26
+ /**
27
+ * Can `emitConst` print this? Mirrors the guard in `emit.ts` — scalars, arrays
28
+ * of scalars, and plain objects of those. Kept as a predicate here so the
29
+ * encoder can record a NAMED runtime-only reason instead of letting the printer
30
+ * throw a bare TypeError at emit time.
31
+ */
32
+ function emittableConst(v: unknown): boolean {
33
+ const scalar = (x: unknown): boolean =>
34
+ x === null || typeof x === 'string' || typeof x === 'number' || typeof x === 'boolean'
35
+ if (scalar(v) || v instanceof RegExp) return true
36
+ if (Array.isArray(v)) return v.every(scalar)
37
+ if (typeof v === 'object' && v !== null) {
38
+ const proto = Object.getPrototypeOf(v)
39
+ if (proto !== Object.prototype && proto !== null) return false
40
+ return Object.values(v).every(x => scalar(x) || (Array.isArray(x) && x.every(scalar)))
41
+ }
42
+ return false
43
+ }
44
+
45
+ /**
46
+ * Does the rule-map entry `winner` bottom out AT the reference `p`?
47
+ *
48
+ * The question `Encoder.winners` has to ask before resolving a `g.X` by name. A
49
+ * map entry that IS the reference — directly, or under the scope wrappers that
50
+ * `rules()` puts there — is not an override to resolve to; it is the reference's
51
+ * own binding, and the only thing that reaches the rule's BODY from there is the
52
+ * ref's thunk. Resolving by name instead hands back the row already in flight for
53
+ * that entry, which is an alias cycle rather than a parser (see `winners`).
54
+ *
55
+ * ONLY WRAPPERS `rules()` ITSELF INTRODUCES are unwrapped — `parser()`/`noTrivia`
56
+ * scopes (`grammar`) and `trivia()`. Both are single-child and both can encode to
57
+ * NO ROW, which is what makes the cycle degenerate. Walking further, into a
58
+ * `seq()` or a `choice()` arm, would answer a different question: a rule whose
59
+ * body legitimately contains a reference to itself is ordinary recursion and must
60
+ * keep resolving by name.
61
+ */
62
+ function wrapsRef(winner: Combinator<unknown>, p: Combinator<unknown>): boolean {
63
+ let cur = winner
64
+ // Bounded rather than `while (true)`: `_def.parser` is author-supplied and a
65
+ // hand-built cycle of scopes would otherwise hang the encoder. Nothing legal
66
+ // nests these more than a couple deep.
67
+ for (let n = 0; n < 16; n++) {
68
+ if (cur === p) return true
69
+ const d = cur._def as ParserDef
70
+ if (d.tag !== 'grammar' && d.tag !== 'trivia') return false
71
+ cur = d.parser
72
+ }
73
+ return false
74
+ }
75
+
76
+ /** Raised when a construct has no opcode yet. Prototype scope is explicit. */
77
+ export class UnsupportedConstruct extends Error {
78
+ readonly tag: string
79
+ constructor(tag: string) { super(`table lowering: no opcode for '${tag}'`); this.tag = tag }
80
+ }
81
+
82
+ /** Settings that select the TABLE'S CONTENTS. They never reach the driver. */
83
+ export type TableSettings = {
84
+ readonly hostMode?: 'ast' | 'cst'
85
+ readonly trackLines?: boolean
86
+ /**
87
+ * ACCEPTED AND IGNORED. RECOVERY IS ALWAYS LOWERED (owner ruling).
88
+ *
89
+ * It used to be opt-in because the sync sentinel a list resyncs to is derived
90
+ * from grammar structure and so has to be known before the table exists — a
91
+ * BUILD setting for the same reason `hostMode` is. The consequence was a
92
+ * `CompiledParser` whose `parseWithErrors()` THREW unless the caller had
93
+ * happened to pass a flag `compile()` does not require, which is a contract
94
+ * break against the source lowering, not a trade.
95
+ *
96
+ * The cost of always lowering it is the extra rows, measured on the emitted
97
+ * module: json 1,081 → 1,214 B (+12.3%), graphql 2,925 → 3,397 B (+16.1%).
98
+ * Against codegen's 15,138 B for the same json root the table is still ~12×
99
+ * smaller, so the size argument for making it optional no longer holds.
100
+ *
101
+ * LOWERING RECOVERY IS NOT RUNNING IT. Every recovery path stays gated on
102
+ * `ctx._tolerant` — the same dormancy the source lowering emits
103
+ * (`codegen.ts:3153`) and the interpreter tests (`repeat.ts:163`) — so a strict
104
+ * parse is strict, and `test/parity/table-recovery-always.test.ts` pins that.
105
+ *
106
+ * The field is kept rather than deleted so `compile` stays signature-
107
+ * compatible with `compile()`, whose `{ recovery: true }` is real. Passing it
108
+ * is harmless; passing `false` does NOT switch recovery off, and saying so here
109
+ * is the point — a setting that silently means nothing is the defect class this
110
+ * project keeps finding.
111
+ */
112
+ readonly recovery?: boolean
113
+ /**
114
+ * THE COVERAGE PLAN, when this table is being encoded for grammar coverage.
115
+ *
116
+ * Its maps are keyed by COMBINATOR IDENTITY, which is what makes threading the
117
+ * plan through the encoder exact: at the moment a choice, dispatch, label or
118
+ * rule body is laid down, the combinator being encoded IS the key, so no code
119
+ * offset has to be matched back to a structural path after the fact. `OP_COV`
120
+ * rows go in beside those sites and the plan's definitions become `prog.cov`.
121
+ *
122
+ * A plan is a BUILD input for the same reason `hostMode` is: the rows either
123
+ * exist in the table or they do not, and no per-parse flag can conjure them.
124
+ * Which of the resulting assemblies actually counts is the separate,
125
+ * assembly-selected question `RunCfg.coverage` answers.
126
+ */
127
+ readonly coverage?: GrammarCoveragePlan
128
+ }
129
+
130
+ type Emitted = { readonly ip: number }
131
+
132
+
133
+ class Encoder {
134
+ code: number[] = []
135
+ k: unknown[] = []
136
+ fns: unknown[] = []
137
+ /**
138
+ * SOURCE TEXT per `fns` entry, or `null` where the author's callback reached
139
+ * the encoder as a live closure with no source (the runtime path).
140
+ *
141
+ * Parallel to `fns`, in the same order, because that is the order the emitters
142
+ * print `f:[…]` in. Without it a macro build has no way to print an author
143
+ * reducer at all: `emitTableModule` falls back to `() => {}` per entry, which
144
+ * produces a module that LOADS and returns the wrong tree — the exact class of
145
+ * failure that a `null` return from `compileRuleMap` (its `mfCovered &&
146
+ * buildCovered` gate) exists to prevent on the source-lowering side. The table
147
+ * counterpart cannot honour that same all-or-nothing contract without this.
148
+ *
149
+ * NOT part of `TableProgram`: it is build-time metadata for the printer, never
150
+ * data the driver reads, and it must not travel in an emitted artifact.
151
+ */
152
+ fnSrcs: (string | null)[] = []
153
+ cc: string[] = []
154
+ fx: (readonly string[])[] = []
155
+ disp: (readonly number[])[] = []
156
+ dsp: DispatchSpec[] = []
157
+ labels: readonly string[] | undefined = undefined
158
+ /** Trivia table in scope at the row being encoded — codegen's `ctx.activeTrivia`. */
159
+ activeTrivia: Combinator<unknown> | undefined = undefined
160
+ classified = false
161
+ scans: ScanSpec[] = []
162
+ /** Ambient `scanSkip` sets, pooled by the ARRAY's identity (stable per grammar). */
163
+ scanSkipSets: (readonly SubtreeRef[])[] = []
164
+ private scanSkipIndex = new Map<readonly Combinator<unknown>[], number>()
165
+ /** Per rule, in `rules` order: which pooled set its entry installs, or −1. */
166
+ scanSkipOf: number[] = []
167
+ /** Reasons this program can RUN but not be EMITTED. */
168
+ runtimeOnly = new Set<string>()
169
+ /**
170
+ * The rule map being encoded, so a named reference resolves BY NAME.
171
+ *
172
+ * This is what makes a MERGED rule map — the shape composition produces, where
173
+ * a later piece overrides an earlier piece's rule — encode to the parser
174
+ * `compose()` means. Without it `case 'lazy'` resolves a reference by calling
175
+ * its thunk, and a thunk closes over the piece that DEFINED the reference: a
176
+ * base piece's internal `g.Atom` keeps reaching the base's own `Atom` even
177
+ * after the merged map binds that name to an override. The encode succeeds,
178
+ * the table parses, and it silently returns the base's tree — open recursion,
179
+ * which `compose()` is largely FOR, quietly absent.
180
+ *
181
+ * The source lowering never had this problem because it compiles every rule to
182
+ * a canonical `_r_<Name>` and fusion drops them into one scope, so a reference
183
+ * resolves by name for free (`compiler/linker.ts:1`). The table has no shared
184
+ * scope, so the name lookup has to be explicit and is done here.
185
+ *
186
+ * For a NON-merged map this is a no-op: the name resolves to the same
187
+ * combinator the thunk returns. The self-resolution guard is load-bearing — a
188
+ * rule map entry may BE the named lazy proxy for its own body, and resolving
189
+ * that to itself would return the in-progress memoized row and never encode
190
+ * the body at all.
191
+ *
192
+ * THAT GUARD CANNOT BE OBJECT IDENTITY. `rules({ trackLines: true }, …)`
193
+ * REPLACES every map entry with `parser({ trackLines: true }, entry)` carrying
194
+ * the SAME `_ruleName` (`combinators/parser.ts:228-241`), so for all four
195
+ * `*PositionsGrammar` exports the entry is a WRAPPER around the rule's own lazy
196
+ * proxy. `winner !== p` is then trivially true, the proxy resolves by name to
197
+ * the wrapper that is already in flight, `node()` hands back the recursion
198
+ * trampoline, and `case 'grammar'` — a `parser()` scope with no trivia, no
199
+ * capture and no root policy emits NO ROW — passes that trampoline straight out
200
+ * as the wrapper's own offset. The trampoline is patched to itself, the rule's
201
+ * real body is never encoded, and every parse dies on its first byte with
202
+ * `Maximum call stack size exceeded`: 87/87 css, 314/314 less, 24/24 jess and
203
+ * 2408/2408 scss files, all four `-lines` variants, from `dccb7fa` (which added
204
+ * this map) to `90aa867`. So the question is not "is the winner this object"
205
+ * but "does the winner bottom out AT this reference", and `wrapsRef` asks that.
206
+ */
207
+ winners: Readonly<Record<string, Combinator<unknown>>> | undefined = undefined
208
+ triviaSpecs: TriviaSpec[] = []
209
+ private triviaIndex = new Map<Combinator<unknown>, number>()
210
+
211
+ /**
212
+ * A trivia combinator as DATA where its shape allows.
213
+ *
214
+ * `classifiedTrivia()` builds exactly `trivia(oneOrMore(choice(label(name,
215
+ * arm)…)))` (src/combinators/map.ts) and the four shipping grammars give it
216
+ * regex arms, so the whole thing round-trips through `[label, source, flags]`
217
+ * triples. Anything else is kept LIVE and recorded, so the program runs and
218
+ * emit refuses by name.
219
+ */
220
+ private triviaSlot(t: Combinator<unknown>): number {
221
+ const hit = this.triviaIndex.get(t)
222
+ if (hit !== undefined) return hit
223
+ const idx = this.triviaSpecs.length
224
+ this.triviaIndex.set(t, idx)
225
+ this.triviaSpecs.push(this.triviaSpecOf(t))
226
+ return idx
227
+ }
228
+
229
+ private triviaSpecOf(t: Combinator<unknown>): TriviaSpec {
230
+ const unlowered = (why: string): TriviaSpec => {
231
+ this.runtimeOnly.add(`rules({ trivia }) — ${why}`)
232
+ return { arms: [], live: t }
233
+ }
234
+ const d = t._def
235
+ // THE `trivia()` WRAPPER IS A MARKER, NOT THE SHAPE. It sets `_meta.isTrivia`
236
+ // and delegates; `parser({ trivia })` / `rules({ trivia })` store whatever they
237
+ // are handed verbatim (`combinators/grammar.ts:71`), and the only consumers —
238
+ // `advanceTrivia` / `scanTrivia` — read `span.end` off it without ever looking
239
+ // at the tag. So demanding the wrapper refused grammars every other engine runs:
240
+ // `parser({ trivia: regex(/ +/) })` is ordinary authoring and it fell all the way
241
+ // back to the interpreter, silently, with the parseman import still in place.
242
+ // Unwrap it WHEN PRESENT and classify the body either way.
243
+ let inner = d.tag === 'trivia' ? d.parser._def : d
244
+ // A `transform()` over a trivia body is a VALUE map, and a trivia VALUE is
245
+ // never observed by anything: `advanceTrivia` and `scanTrivia`
246
+ // (combinators/trivia-skip.ts:189, :219) are the only consumers of `ctx.trivia`
247
+ // and both read `span.end` alone — the parsed value is dropped on every path.
248
+ // So the wrapper is UNWRAPPED rather than refused. This is not "close enough":
249
+ // the recognition it wraps is the entire observable contract of trivia, and
250
+ // dropping a reducer whose result provably cannot escape loses nothing.
251
+ // (`examples/json/jsonc.ts` writes exactly `transform(many(…), () => '')`.)
252
+ while (inner.tag === 'transform') inner = inner.parser._def
253
+ if (inner.tag === 'regex') return { arms: [], plain: [inner.source, inner.flags] }
254
+ // The repetition around the alternation. `oneOrMore` is the tag min>=1 always
255
+ // carries (repeat.ts:201) and `many` is the nullable min-0 loop; both lower,
256
+ // and the floor travels in the spec so the rebuild is the same combinator
257
+ // rather than a same-shaped one. A BOUNDED repeat does not lower — `max` has
258
+ // nowhere to go in the spec and silently unbounding it would accept input the
259
+ // grammar rejects.
260
+ if (inner.tag !== 'oneOrMore' && inner.tag !== 'many') {
261
+ return unlowered(`unrecognised trivia body '${inner.tag}'`)
262
+ }
263
+ if (inner.max !== undefined) {
264
+ return unlowered(`trivia body is a BOUNDED repeat (max ${inner.max}), which the spec cannot carry`)
265
+ }
266
+ const min = inner.min
267
+ const rep = inner.parser._def
268
+ // A single un-alternated body (`trivia(many(regex))`) is the one-arm case.
269
+ const parsers = rep.tag === 'choice' ? rep.parsers : [inner.parser]
270
+ // Two shapes, and an arm set must be wholly one or the other. LABELLED is
271
+ // `classifiedTrivia()`'s exact output. UNLABELLED is the plain
272
+ // `trivia(oneOrMore(choice(ws, comment)))` that `examples/css/parser.ts`
273
+ // writes and that had no lowering at all. Mixing them is refused rather than
274
+ // half-labelled: `classifiedTrivia` requires a label per arm, so there is no
275
+ // rebuild that keeps both, and inventing labels would put names in the CST
276
+ // trivia log that the grammar never wrote.
277
+ const labelled: Array<readonly [string, string, string]> = []
278
+ const bare: Array<readonly [string, string]> = []
279
+ for (const arm of parsers) {
280
+ const a = arm._def
281
+ if (a.tag === 'regex') {
282
+ bare.push([a.source, a.flags])
283
+ continue
284
+ }
285
+ if (a.tag !== 'label') return unlowered(`trivia arm is '${a.tag}', not a labelled arm or a regex`)
286
+ const body = a.parser._def
287
+ if (body.tag !== 'regex') return unlowered(`trivia arm ${JSON.stringify(a.label)} is '${body.tag}', not a regex`)
288
+ labelled.push([a.label, body.source, body.flags])
289
+ }
290
+ if (labelled.length > 0 && bare.length > 0) {
291
+ return unlowered('trivia arms are a MIX of labelled and bare regexes, which has no single rebuild')
292
+ }
293
+ if (labelled.length > 0) {
294
+ // `classifiedTrivia()` builds min-1 by construction, so a min-0 labelled
295
+ // body cannot be rebuilt through it and is not quietly promoted.
296
+ if (min !== 1) return unlowered(`labelled trivia arms need a min-1 repeat, got min ${min}`)
297
+ return { arms: labelled }
298
+ }
299
+ return { arms: [], alts: bare, min }
300
+ }
301
+ rules: Record<string, number> = {}
302
+
303
+ /**
304
+ * THE COVERAGE DEFINITION POOL, in `plan.definitions` order, or `undefined`
305
+ * when this is an ordinary encode. Ordinary is the default and it must stay
306
+ * byte-for-byte the table it always was, so every coverage member is absent —
307
+ * not empty — unless a plan was passed.
308
+ */
309
+ private readonly cov: (readonly [string, 0 | 1 | 2 | 3])[] | undefined
310
+ private readonly covIndex: ReadonlyMap<string, number>
311
+ readonly plan: GrammarCoveragePlan | undefined
312
+
313
+ /**
314
+ * Wrap one site in its counter row.
315
+ *
316
+ * `id` comes from the plan and is therefore already a definition; an id the
317
+ * pool does not know is a plan/encoder disagreement and is refused rather than
318
+ * appended, because a hit on an id outside the denominator is a hit the
319
+ * collector silently drops (`createGrammarCoverageCollector.hit` tests `known`)
320
+ * — a site that reads as instrumented and counts nothing.
321
+ */
322
+ private covWrap(child: number, id: string | undefined, on: 0 | 1): number {
323
+ if (id === undefined || this.cov === undefined) return child
324
+ const slot = this.covIndex.get(id)
325
+ if (slot === undefined) {
326
+ throw new UnsupportedConstruct(`coverage(id ${JSON.stringify(id)} is not in the plan's definitions)`)
327
+ }
328
+ return this.emit(OP_COV, child, slot, on)
329
+ }
330
+
331
+ private kIndex = new Map<unknown, number>()
332
+ private ccIndex = new Map<string, number>()
333
+ private fxIndex = new Map<string, number>()
334
+ /** Memoized by combinator identity: a shared sub-combinator is ONE row. */
335
+ memo = new Map<Combinator<unknown>, number>()
336
+ /**
337
+ * Memo for `lazy` REFERENCES, keyed by the trivia scope the reference is made
338
+ * from. A cross-rule reference's row depends on that scope (`scopedRef`), so the
339
+ * single `memo` slot every other construct uses would freeze the FIRST
340
+ * reference's scope onto every later one and undo the fix.
341
+ */
342
+ refMemo = new Map<Combinator<unknown>, Map<Combinator<unknown> | undefined, number>>()
343
+ pending = new Map<Combinator<unknown>, number[]>()
344
+
345
+ readonly settings: TableSettings
346
+ /** Resolved once, HERE, at table-build time — never consulted at run time. */
347
+ readonly track: boolean
348
+ /**
349
+ * Decides the extra operands laid down below. ALWAYS TRUE — see
350
+ * `TableSettings.recovery`. Kept as a field rather than folded away because it
351
+ * is what every emit site reads, and the emit sites are where the shape of the
352
+ * row is stated.
353
+ */
354
+ readonly rec: boolean
355
+ constructor(settings: TableSettings) {
356
+ this.settings = settings
357
+ this.track = settings.trackLines === true
358
+ this.rec = true
359
+ this.plan = settings.coverage
360
+ this.cov = settings.coverage === undefined
361
+ ? undefined
362
+ : settings.coverage.definitions.map(d => [d.id, covKindCode(d.kind)] as const)
363
+ this.covIndex = new Map((this.cov ?? []).map((d, i) => [d[0], i]))
364
+ }
365
+
366
+ /**
367
+ * The class of the follow set of term `i` in a sequence — the sync sentinel a
368
+ * list nested in that term resyncs to, INFERRED from structure exactly as
369
+ * `sequence()` (combinators/sequence.ts:58) and `emitSeqValues`
370
+ * (compiler/codegen.ts:1756) infer it: the union of every LATER term's first
371
+ * set, not merely up to the first non-nullable one, so a mandatory middle term
372
+ * cannot hide a later close. −1 where there is no usable sentinel, which is the
373
+ * same set of cases `firstSetSentinel` answers `null` for.
374
+ */
375
+ private followClass(parsers: readonly Combinator<unknown>[], i: number): number {
376
+ let fs: FirstSet = { kind: 'empty' }
377
+ const rr = this.refResolver()
378
+ for (let j = i + 1; j < parsers.length; j++) fs = union(fs, firstSetOf(parsers[j]!, new Set(), rr))
379
+ return this.charClass(fs)
380
+ }
381
+
382
+ private constant(v: unknown): number {
383
+ const key = v instanceof RegExp ? `re:${JSON.stringify([v.source, v.flags])}` : v
384
+ const hit = this.kIndex.get(key)
385
+ if (hit !== undefined) return hit
386
+ const i = this.k.length
387
+ this.k.push(v)
388
+ this.kIndex.set(key, i)
389
+ return i
390
+ }
391
+
392
+ /**
393
+ * A `notAdjacent({ kinds })` filter, pooled by its CONTENTS.
394
+ *
395
+ * `constant()` keys an array by identity, and every `notAdjacent()` call
396
+ * builds a fresh one — so `calc()`'s four `{ kinds: ['whitespace'] }` sites
397
+ * would each park an identical array in the pool.
398
+ */
399
+ private kindsIndex = new Map<string, number>()
400
+ private kindsSlot(kinds: readonly string[]): number {
401
+ const key = JSON.stringify(kinds)
402
+ const hit = this.kindsIndex.get(key)
403
+ if (hit !== undefined) return hit
404
+ const i = this.k.length
405
+ this.k.push([...kinds])
406
+ this.kindsIndex.set(key, i)
407
+ return i
408
+ }
409
+
410
+ /**
411
+ * Park an author callback, WITH its source when the caller has it.
412
+ *
413
+ * `src` is required at every call site rather than optional so that adding a
414
+ * new `fn()`-bearing lowering forces a decision about its source instead of
415
+ * silently defaulting to "unprintable" — which reads as a working encode and
416
+ * emits a `() => {}` placeholder downstream.
417
+ */
418
+ private fn(v: unknown, src: string | null): number {
419
+ const i = this.fns.length
420
+ this.fns.push(v)
421
+ this.fnSrcs.push(src)
422
+ return i
423
+ }
424
+
425
+ private expected(list: readonly string[]): number {
426
+ const key = JSON.stringify(list)
427
+ const hit = this.fxIndex.get(key)
428
+ if (hit !== undefined) return hit
429
+ const i = this.fx.length
430
+ this.fx.push(list)
431
+ this.fxIndex.set(key, i)
432
+ return i
433
+ }
434
+
435
+ /**
436
+ * THE GATING ANALYSIS'S VIEW OF A CROSS-PIECE HOLE — `winners`, the same map
437
+ * emission already resolves those holes against.
438
+ *
439
+ * `firstSetOf` and `matchesEmpty` degrade a `lazy` whose thunk THROWS to `any`
440
+ * / nullable. A grammar loaded at runtime never hits that: `composeLeaf` binds
441
+ * every `g.X` before the encoder sees the map, so the thunks resolve. A grammar
442
+ * lowered by the MACRO does hit it — `plugin/evaluator.ts` mints an
443
+ * `externalRefs` slot for any `g.X` this `rules()` call does not itself define
444
+ * and never `.define()`s it, because the definition lives in another piece and
445
+ * is merged afterwards. The ref still ENCODES correctly (`case 'lazy'` resolves
446
+ * it through `winners` at `:1169`), so the artifact parses the same input — it
447
+ * simply parses it with the gate switched off at every choice arm that reaches
448
+ * a hole.
449
+ *
450
+ * Measured on jess's less grammar (`bench/jess/macro-program-diff.ts`): 195 of
451
+ * 562 reachable choice arms carried NO first set in the shipped artifact
452
+ * against 103 of 540 in the runtime encode, and 10 dispatch sites lost the O(1)
453
+ * `exclusive` piece (`assemble.ts:1758`) for the candidate-mask loop. With this
454
+ * resolver both figures land exactly on the runtime encode's: 103 and 41.
455
+ *
456
+ * SOUNDNESS. `firstSetOf` warns that deep resolution is sound only where refs
457
+ * are FINAL, because a compose OVERRIDE could widen a referenced rule's set
458
+ * after the fact. `winners` IS the final map — it is the merged rule map being
459
+ * encoded, the one `case 'lazy'` points the arm's CALL at. Gating an arm on the
460
+ * first set of the very row it will call cannot skip a match that row would
461
+ * accept. And this only ever runs in the CATCH branch: where a thunk resolves,
462
+ * nothing consults the resolver and nothing changes, which is why the runtime
463
+ * leg's counts are byte-for-byte what they were.
464
+ */
465
+ private refResolver(): RefResolver | undefined {
466
+ const w = this.winners
467
+ return w === undefined ? undefined : (name: string) => w[name]
468
+ }
469
+
470
+ /** A first set becomes a char class string, or −1 for `any`. */
471
+ private charClass(fs: FirstSet): number {
472
+ if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
473
+ // `fromCharCode` truncates every endpoint to 16 bits. That silently turned
474
+ // U+1F600 into U+F600 in the dispatch table: `lead()` correctly produced the
475
+ // astral code point, but no encoded class could claim it. The shared codec
476
+ // keeps BMP endpoints compact and escapes astral endpoints unambiguously.
477
+ const spec = encodeClassSpec(fs.ranges)
478
+ const hit = this.ccIndex.get(spec)
479
+ if (hit !== undefined) return hit
480
+ const i = this.cc.length
481
+ this.cc.push(spec)
482
+ this.ccIndex.set(spec, i)
483
+ return i
484
+ }
485
+
486
+ /**
487
+ * Wrap one choice arm in its `autoNot` checks (`OP_REJECT`).
488
+ *
489
+ * A `firstSet` check's class comes from `codesToFirstSet` (choice.ts), which
490
+ * only ever builds a non-empty `ranges` set over codes 1..127 — so `charClass`
491
+ * cannot answer −1 here. It is asserted rather than assumed, because a −1 would
492
+ * index past `cc` and silently stop rejecting.
493
+ */
494
+ private reject(child: number, checks: readonly AutoNotCheck[]): number {
495
+ const ops: number[] = []
496
+ for (const c of checks) {
497
+ if (c.kind === 'startsWith') { ops.push(0, this.constant(c.value)); continue }
498
+ const cls = this.charClass(c.set)
499
+ if (cls < 0) throw new UnsupportedConstruct('choice(autoNot: unmappable first set)')
500
+ ops.push(1, cls)
501
+ }
502
+ const head = this.emitHead(OP_REJECT, 2 + ops.length)
503
+ this.code[head + 1] = child
504
+ this.code[head + 2] = ops.length >> 1
505
+ for (let i = 0; i < ops.length; i++) this.code[head + 3 + i] = ops[i]!
506
+ return head
507
+ }
508
+
509
+ private emit(...words: number[]): number {
510
+ const ip = this.code.length
511
+ for (const w of words) this.code.push(w)
512
+ return ip
513
+ }
514
+
515
+ /** Reserve `n` operand slots to patch once children are laid out. */
516
+ private emitHead(op: number, n: number): number {
517
+ const ip = this.code.length
518
+ this.code.push(op)
519
+ for (let i = 0; i < n; i++) this.code.push(0)
520
+ return ip
521
+ }
522
+
523
+ /**
524
+ * A combinator carried as a table SUBTREE plus its first set.
525
+ *
526
+ * The first set is not decoration: `buildBalancedInterior` reads each skipper's
527
+ * to decide whether the interior content run can be one bounded regex
528
+ * (scanTo.ts:280). A reference that reported `any` would rebuild a different —
529
+ * one-character-at-a-time — interior than the grammar's own.
530
+ */
531
+ private subtree(c: Combinator<unknown>): SubtreeRef {
532
+ const ip = this.node(c).ip
533
+ const fs = firstSetOf(c, new Set(), this.refResolver())
534
+ return [ip, fs.kind === 'empty' ? -2 : this.charClass(fs)]
535
+ }
536
+
537
+ /** Pool one scan spec. One per combinator: `node()` memoizes by identity. */
538
+ private scanSlot(spec: ScanSpec): number {
539
+ const i = this.scans.length
540
+ this.scans.push(spec)
541
+ return i
542
+ }
543
+
544
+ private scanSkipSlot(set: readonly Combinator<unknown>[]): number {
545
+ const hit = this.scanSkipIndex.get(set)
546
+ if (hit !== undefined) return hit
547
+ const i = this.scanSkipSets.length
548
+ // Reserve the slot BEFORE encoding: an ambient skipper can be a `balanced()`
549
+ // whose own encoding reaches back here through the same array.
550
+ this.scanSkipIndex.set(set, i)
551
+ this.scanSkipSets.push([])
552
+ this.scanSkipSets[i] = set.map(c => this.subtree(c))
553
+ return i
554
+ }
555
+
556
+ /** Take trivia labels / classification off one carrier, first one wins. */
557
+ private carryTriviaMeta(c: Combinator<unknown> | undefined): void {
558
+ if (c === undefined) return
559
+ this.labels ??= c._meta.triviaKindLabels
560
+ if (c._meta.rootTriviaClassified === true) this.classified = true
561
+ }
562
+
563
+ encodeRule(name: string, p: Combinator<unknown>): void {
564
+ // `rules({ trivia }, …)` is ambient for the whole rule body, so it is in
565
+ // scope for a `notAdjacent({ kinds })` anywhere inside it — see the same
566
+ // save/restore in `case 'grammar'`.
567
+ this.activeTrivia = p._meta.grammarTrivia
568
+ const body = this.node(p).ip
569
+ this.activeTrivia = undefined
570
+ // GRAMMAR-LEVEL AMBIENT TRIVIA (`rules({ trivia }, …)`).
571
+ //
572
+ // `run()` installs this from `entry._meta.grammarTrivia`, but ONLY for a
573
+ // combinator entry — a compiled entry is a FUNCTION and is expected to have
574
+ // baked it in (src/functional/run.ts:269). A table entry is also a function,
575
+ // so without this the whole grammar parses with NO ambient trivia and every
576
+ // whitespace-bearing input silently fails to match the same way in every
577
+ // path — which makes a three-way identity check AGREE while proving nothing.
578
+ // Bake it, exactly as the compiled path does.
579
+ // AMBIENT `scanSkip`, PER RULE — the granularity `run()` uses, because it
580
+ // reads the ENTRY rule's own `_meta.grammarScanSkip` (grammar.ts:203). It is
581
+ // stamped by `rules({ scanSkip }, …)` on that map's rules only
582
+ // (parser.ts:210); a `parser()` scope has no `scanSkip` at all, so a rule is
583
+ // the finest scope that can carry one. In a `composeLeaf` grammar the pieces
584
+ // disagree — 67 of jess's 195 css rules carry no set — and taking the first
585
+ // one for the whole program handed those entries a skip list the interpreter
586
+ // does not give them.
587
+ const gss = p._meta.grammarScanSkip
588
+ this.scanSkipOf.push(gss === undefined ? -1 : this.scanSkipSlot(gss))
589
+ const amb = p._meta.grammarTrivia
590
+ // Carried so `tableRules` can stamp the entry — see TableProgram.labels.
591
+ //
592
+ // THREE PLACES, because that is where `run()` looks (`triviaKindLabelsFromRunnable`
593
+ // / `rootTriviaClassifiedFromRunnable`, functional/run.ts): `rules({ trivia }, …)`
594
+ // leaves it on `_meta.grammarTrivia`, `classifiedTrivia()` leaves it on the
595
+ // combinator's OWN `_meta`, and `parser({ trivia }, …)` leaves it on
596
+ // `_def.triviaParser` with nothing on `_meta` at all.
597
+ //
598
+ // Reading only the first meant a table lowered from a `parser({ trivia:
599
+ // classifiedTrivia(…) }, …)` root PARSED correctly and then reported no
600
+ // labels, so `run({ rootTrivia: { select } })` rejected a grammar that plainly
601
+ // has labelled trivia — and a bare `classifiedTrivia()` passed as
602
+ // `options.trivia` lost its labels the same way. Neither is visible to a
603
+ // value-identity sweep: the tree is the same, only the metadata is gone.
604
+ this.carryTriviaMeta(amb)
605
+ this.carryTriviaMeta(p)
606
+ if (p._def.tag === 'grammar') this.carryTriviaMeta(p._def.triviaParser)
607
+ // `body` is ALREADY the counter row when this rule has one — `node()` wraps
608
+ // it there, not here. Wrapping the rule ENTRY instead would credit the rule
609
+ // only when it is the start rule: an internal `g.X` reference resolves
610
+ // straight to the body's offset (`case 'lazy'`), so every recursive call and
611
+ // every cross-rule reference would jump past the counter, and a grammar with
612
+ // one entry would report one hit rule however much of it ran.
613
+ this.rules[name] = amb === undefined ? body : this.emit(OP_SCOPE, this.triviaSlot(amb), body)
614
+ }
615
+
616
+ /**
617
+ * The row a cross-rule `g.X` reference jumps to — the target's body, RE-WRAPPED
618
+ * in the target rule's own ambient trivia when that differs from the trivia
619
+ * lexically active at the reference.
620
+ *
621
+ * `encodeRule` wraps a rule ENTRY in `OP_SCOPE`, but only the entry: a `g.X`
622
+ * reference resolves straight to the body's offset and jumps past it (the note
623
+ * at `encodeRule`). So a rule referenced from inside a `noTrivia(...)` region ran
624
+ * with NO ambient trivia of its own — the table scoped trivia DYNAMICALLY, by
625
+ * caller, where codegen scopes it LEXICALLY, per rule, by binding each rule's
626
+ * trivia scanner at compile time.
627
+ *
628
+ * jess's Less grammar sits on that seam: `StandardDeclaration` wraps its value in
629
+ * `noTrivia(...)` while the `!important` tail lives in the referenced rule
630
+ * `ValueListWithPriority`, so `color: red !important` could not cross the space
631
+ * before `!` — and a 107 KB stylesheet stopped at 68.5% reporting `ok: true`.
632
+ *
633
+ * ONLY WHERE THE SCOPE WAS CLEARED. `activeTrivia` is the encoder's lexical scope
634
+ * tracker (maintained by `case 'grammar'` across `parser()` / `noTrivia`), and a
635
+ * reference made under ANY live trivia stays a bare jump, so the table gains no
636
+ * rows anywhere except inside a cleared region. The broader form — restore
637
+ * wherever the scope merely DIFFERS — also overrides a caller that set its own
638
+ * live trivia, and that regressed `@less/test-data`'s `selectors.less` from 3791
639
+ * bytes to 1784 while repairing nothing extra.
640
+ *
641
+ * ONLY WHERE THE TARGET HAS A BOUNDARY TO REPAIR — `hasOwnTriviaBoundary`, the
642
+ * same gate `ref.ts` applies, because the two engines must ask the same question
643
+ * or they parse different languages again. A rule whose body is a bare
644
+ * alternation, a dispatch, or a single terminal never consults an ambient
645
+ * scanner itself, so the scope row cannot repair it and only leaks past its arms.
646
+ * jess's SCSS `MathUnary` — `choice(noTrivia(…), noTrivia(…), g.ValueAtom)` — is
647
+ * exactly that shape: the row it gained handed its third arm a live scope, and
648
+ * `KeywordOrInterpolatedValue`'s `many()` then skipped the space between two
649
+ * identifiers. `gen-workload.scss` stopped at byte 218 of 287543 and `a{b: c d}`
650
+ * silently produced the ONE keyword `bc`.
651
+ */
652
+ private scopedRef(p: Combinator<unknown>, target: Combinator<unknown>): number {
653
+ const ip = this.node(target).ip
654
+ // `rules()` stamps `grammarTrivia` on the map entry, which for a proxied rule
655
+ // is the REF itself; a composed `winners` entry carries it on the target.
656
+ const amb = p._meta.grammarTrivia ?? target._meta.grammarTrivia
657
+ if (amb === undefined || this.activeTrivia !== undefined) return ip
658
+ if (!hasOwnTriviaBoundary(target)) return ip
659
+ return this.emit(OP_SCOPE, this.triviaSlot(amb), ip)
660
+ }
661
+
662
+ node(p: Combinator<unknown>): Emitted {
663
+ // See `refMemo`: a reference is memoised per REFERENCING trivia scope, every
664
+ // other construct once for the program.
665
+ const lazyRef = (p._def as ParserDef).tag === 'lazy'
666
+ const scopeKey = lazyRef ? this.activeTrivia : undefined
667
+ const refMemo = this.refMemo
668
+ const hit = lazyRef ? refMemo.get(p)?.get(scopeKey) : this.memo.get(p)
669
+ if (hit !== undefined) return { ip: hit }
670
+ // Recursion: reserve a trampoline row now, patch its target when the real
671
+ // body lands. One extra indirection per recursive rule, zero per call site.
672
+ const inFlight = this.pending.get(p)
673
+ if (inFlight !== undefined) {
674
+ const ip = this.emitHead(OP_RULE, 1)
675
+ inFlight.push(ip + 1)
676
+ return { ip }
677
+ }
678
+ const patches: number[] = []
679
+ this.pending.set(p, patches)
680
+ // A RULE IS CREDITED ON ENTRY, not on success — `codegen.ts:4529` emits the
681
+ // hook as the first statement of the named function body, so a rule that is
682
+ // reached and then fails still counts as exercised. The counter goes on the
683
+ // MEMOISED offset, which is the one every reference to this combinator lands
684
+ // on, and the recursion trampolines below are patched with it too.
685
+ const ip = this.covWrap(this.encodeDef(p), this.plan?.rules.get(p), 0)
686
+ this.pending.delete(p)
687
+ if (lazyRef) {
688
+ let byScope = refMemo.get(p)
689
+ if (byScope === undefined) { byScope = new Map(); refMemo.set(p, byScope) }
690
+ byScope.set(scopeKey, ip)
691
+ } else {
692
+ this.memo.set(p, ip)
693
+ }
694
+ for (const slot of patches) {
695
+ // A TRAMPOLINE PATCHED TO ITSELF IS NOT A TABLE. `OP_RULE ip → ip` makes no
696
+ // progress in either driver: `exec.ts`'s `case OP_RULE` recurses on the same
697
+ // `ip`, and `assemble.ts` builds a piece that calls itself, so the first byte
698
+ // of the first file is `Maximum call stack size exceeded`. It is produced
699
+ // when a combinator's whole encoding emits NO row of its own and resolves
700
+ // back to the trampoline reserved for it — an ALIAS CYCLE, never a grammar
701
+ // this table can run. Raised here, at the one line that can write it, so the
702
+ // encoder names the construct instead of every parse dying at run time.
703
+ if (slot - 1 === ip) {
704
+ const rule = (p as unknown as { _ruleName?: string })._ruleName
705
+ throw new Error(
706
+ `table lowering: alias cycle — ${p._tag}${rule === undefined ? '' : ` (rule '${rule}')`} encodes to its own recursion trampoline at ${ip}`,
707
+ )
708
+ }
709
+ this.code[slot] = ip
710
+ }
711
+ return { ip }
712
+ }
713
+
714
+ private encodeDef(p: Combinator<unknown>): number {
715
+ const d = p._def as ParserDef
716
+ // THE SCANNING CONSTRUCTS, as their constructor arguments.
717
+ //
718
+ // Neither is recoverable from `_def` alone — `balanced()` overrides `.parse`
719
+ // and leaves `_def` as its EAGER interior, so a structural encoding builds a
720
+ // parser that skips the wrong things and reports nothing. Both are fully
721
+ // described by what they were CONSTRUCTED with, which is what `ScanSpec`
722
+ // carries; `resolveTable` hands that back to `scanTo()`/`balanced()`.
723
+ //
724
+ // The `balanced()` test reads `_balancedSpec` on the object `balanced()`
725
+ // RETURNS. That object is a `token()` and the ambient marker lives on the
726
+ // combinator INSIDE it, so testing `_balancedAmbient` here would either miss
727
+ // the construct or reach past the `token()` that gives it its one leaf.
728
+ const bal = (p as BalancedSpec)._balancedSpec
729
+ if (bal !== undefined) return this.emit(OP_SCAN, this.scanSlot({
730
+ kind: 1,
731
+ flags: (bal.raw ? 1 : 0) | (bal.strict ? 4 : 0),
732
+ skip: bal.ownSkip.map(c => this.subtree(c)),
733
+ open: bal.open,
734
+ close: bal.close,
735
+ }))
736
+ if (d.tag === 'scanTo') {
737
+ const sentDef = d.sentinel._def
738
+ return this.emit(OP_SCAN, this.scanSlot({
739
+ kind: 0,
740
+ flags: (d.raw ? 1 : 0) | (d.orEOF ? 2 : 0),
741
+ skip: d.skip.map(c => this.subtree(c)),
742
+ sentinel: this.subtree(d.sentinel),
743
+ sent: sentDef.tag === 'literal' ? sentDef.value : null,
744
+ }))
745
+ }
746
+ switch (d.tag) {
747
+ case 'literal': {
748
+ if (d.caseInsensitive) {
749
+ return this.emit(this.track ? OP_LIT_CI_TRACK : OP_LIT_CI, this.constant(d.value), this.expected(deriveExpected(p)))
750
+ }
751
+ return this.emit(this.track ? OP_LIT_TRACK : OP_LIT, this.constant(d.value), this.expected(deriveExpected(p)))
752
+ }
753
+ case 'regex': {
754
+ const flags = d.flags.includes('y') ? d.flags : `${d.flags}y`
755
+ return this.emit(this.track ? OP_RX_TRACK : OP_RX, this.constant(new RegExp(d.source, flags)), this.expected(deriveExpected(p)))
756
+ }
757
+ case 'keywords': {
758
+ // `keywords()` compiles to ONE sticky regex and pushes a leaf — which is
759
+ // exactly what OP_RX does. Rebuilding the same regex here needs no new
760
+ // opcode. Construction mirrors src/combinators/keywords.ts:87-106; the
761
+ // words arrive already sorted longest-first on the def.
762
+ const alt = d.words.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')
763
+ const boundary = d.boundary ? `(?![${d.boundary}])` : ''
764
+ const flags = d.caseInsensitive ? 'iy' : 'uy'
765
+ return this.emit(
766
+ this.track ? OP_RX_TRACK : OP_RX,
767
+ this.constant(new RegExp(`(?:${alt})${boundary}`, flags)),
768
+ // `['keyword']`, NOT `deriveExpected(p)`. `keywords.ts:137` reports the
769
+ // one word `keyword` — the whole point of the combinator is that a
770
+ // failing keyword arm names the CATEGORY rather than reciting every
771
+ // literal in the family. `deriveExpected` reports the literals, so the
772
+ // table said `'"media"'` where both other engines say `'keyword'`.
773
+ this.expected(['keyword']),
774
+ )
775
+ }
776
+ case 'expect': {
777
+ // `expect()` NEVER fails: on a failed child it yields a ParseError value
778
+ // at zero width. That is a distinct outcome, so it gets its own row.
779
+ return this.emit(OP_EXPECT, this.node(d.parser).ip, this.expected([...d.expected]))
780
+ }
781
+ case 'sequence': {
782
+ const kids = d.parsers.map(c => this.node(c).ip)
783
+ // A RECOVERY table appends one follow-set class per term AFTER the child
784
+ // slots. Appending keeps every existing read (`ip + 2 + i`) and the
785
+ // `collapseIndirection` slot map exactly as they were, so a strict table
786
+ // is word-for-word the table it always was.
787
+ const head = this.emitHead(d.valueUnused ? OP_SEQV : OP_SEQ, 1 + kids.length + (this.rec ? kids.length : 0))
788
+ this.code[head + 1] = kids.length
789
+ for (let i = 0; i < kids.length; i++) this.code[head + 2 + i] = kids[i]!
790
+ if (this.rec) {
791
+ for (let i = 0; i < kids.length; i++) this.code[head + 2 + kids.length + i] = this.followClass(d.parsers, i)
792
+ }
793
+ return head
794
+ }
795
+ case 'choice': {
796
+ // `greedyClassify` is NOT a choice at all — one arm runs and another arm
797
+ // is credited — so it gets its own row rather than an arm ordering.
798
+ if (d.strategy.tag === 'greedyClassify') {
799
+ const superIndex = d.strategy.superIndex
800
+ const sup = this.node(d.parsers[superIndex]!).ip
801
+ // Same construction as `choice()`'s own `greedyLitMap` (choice.ts:64-70):
802
+ // every NON-super arm that has a core literal value, keyed by that value.
803
+ // An arm without one is unreachable through classification and is left
804
+ // to the super arm exactly as the interpreter leaves it.
805
+ // COVERAGE CREDITS THE CLASSIFIED ARM AND NOT THE SUPER ARM, which is
806
+ // what the interpreter reference does: `coverage.ts:230` suppresses the
807
+ // per-arm hit for a `greedyClassify` site precisely because the super
808
+ // arm runs as an implementation detail, and only the arm the
809
+ // classification SELECTS is a semantic winner. Here the selected arm is
810
+ // re-run (`assemble.ts` `OP_GREEDY`), so wrapping it is exact.
811
+ //
812
+ // THE SUPER ARM'S OWN ID IS NOT INSTRUMENTED, and that is a stated gap,
813
+ // not an oversight: it is credited when classification finds nothing,
814
+ // and that outcome is decided INSIDE the greedy row rather than by any
815
+ // child of it. Instrumenting the super arm's offset would credit it on
816
+ // every invocation, including the ones the interpreter credits to a
817
+ // literal arm — an over-count, which inflates the ratio. Leaving it
818
+ // uninstrumented under-counts, which is the direction every other
819
+ // coverage decision in this codebase fails in.
820
+ const covIds = this.plan?.choices.get(p)
821
+ const pairs: number[] = []
822
+ for (let i = 0; i < d.parsers.length; i++) {
823
+ if (i === superIndex) continue
824
+ const lit = getCoreLiteralValue(d.parsers[i]!)
825
+ if (lit === null) continue
826
+ pairs.push(this.constant(lit), this.covWrap(this.node(d.parsers[i]!).ip, covIds?.[i], 1))
827
+ }
828
+ const head = this.emitHead(OP_GREEDY, 2 + pairs.length)
829
+ this.code[head + 1] = sup
830
+ this.code[head + 2] = pairs.length >> 1
831
+ for (let i = 0; i < pairs.length; i++) this.code[head + 3 + i] = pairs[i]!
832
+ return head
833
+ }
834
+ // Arm ORDER is semantics. `literalsLongestFirst` / `sharedPrefix` reorder
835
+ // the arms, so encoding them as a declared-order choice would pick a
836
+ // different arm and build a different tree while still parsing. ORDER is
837
+ // the only thing they change, though, and order is table data:
838
+ // `literalsLongestFirst` carries its order explicitly, and `sharedPrefix`
839
+ // is documented in choice.ts:52 as a firstMatch specialization, so
840
+ // declared order is already correct for it.
841
+ const order = d.strategy.tag === 'literalsLongestFirst'
842
+ ? d.strategy.sortedIndices
843
+ : d.parsers.map((_, i) => i)
844
+ const arms = order.map(i => d.parsers[i]!)
845
+ // `autoNot` rejects an arm that ALREADY MATCHED when a later arm would
846
+ // have consumed more, so a matched arm loses. It wraps the arm's row and
847
+ // does not touch the arm's identity in `memo`: the same combinator used
848
+ // at a site with no `autoNot` still reaches the bare row.
849
+ //
850
+ // A GATED arm (`choice({ gate, combinator })`) is wrapped OUTSIDE its
851
+ // `autoNot` wrapper, mirroring the interpreter's order: the gate is
852
+ // consulted before the arm runs (choice.ts:150) and `autoNot` only after
853
+ // it has already matched (choice.ts:160-164). Wrapping — rather than
854
+ // splicing the predicate into the arm — is what preserves the arm's own
855
+ // first set for `classes` below, and with it the site's O(1) dispatch.
856
+ // ARM IDS ARE INDEXED BY DECLARED POSITION, not by encoded position:
857
+ // `buildGrammarPlan` mints `choice:…/arm:i` off `def.parsers`, and `order`
858
+ // is a permutation of that. `src` is the declared index, so the id and the
859
+ // `autoNot`/`gates` entries are all taken with the same subscript.
860
+ const armCovIds = this.plan?.choices.get(p)
861
+ const kids = arms.map((c, k) => {
862
+ const src = order[k]!
863
+ const checks = d.autoNot[src]
864
+ const inner = this.node(c).ip
865
+ const ip = checks === null || checks === undefined || checks.length === 0
866
+ ? inner
867
+ : this.reject(inner, checks)
868
+ const gate = d.gates[src]
869
+ const gated = gate === null || gate === undefined
870
+ ? ip
871
+ : this.emit(OP_ARMGATE, this.fn(gate, d.gateSrcs?.[src] ?? null), ip, this.expected(deriveExpected(c)))
872
+ // OUTSIDE both wrappers, because an arm is credited once it has WON —
873
+ // after its gate admitted it and after `autoNot` declined to reject it.
874
+ // Inside either, a gate-rejected or auto-rejected arm would count as
875
+ // covered on a parse where the choice picked somebody else.
876
+ return this.covWrap(gated, armCovIds?.[src], 1)
877
+ })
878
+ // O(1) first-char dispatch is only SOUND when the arms are disjoint.
879
+ // With overlapping first sets, "the first arm whose class contains this
880
+ // char" is not "the first arm that matches": a `Keyword` arm whose first
881
+ // set over-approximates to include digits was selected ahead of `Num`
882
+ // for the input `0` — the parse still succeeded, and only the TREE moved.
883
+ // Disjointness is computed HERE, not read off `d.disjoint`. That flag is
884
+ // set when `choice()` is CONSTRUCTED, at which point `g.X` arms are
885
+ // unresolved refs whose first set is `any` — so every recursive grammar
886
+ // reports non-disjoint and loses its dispatch. (Codegen has the same
887
+ // problem and solves it the same way: a placeholder resolved at fuse.)
888
+ // EVERY choice now carries its arms' classes. The old code recorded them
889
+ // only when the whole site qualified for O(1) selection — all arms
890
+ // non-nullable, pairwise disjoint and mappable — and a single failure
891
+ // left the site with no gate of any kind, so all its arms were entered
892
+ // at every position it was reached.
893
+ //
894
+ // An arm's class is its OWN gate and is sound on its own: a
895
+ // non-nullable arm whose first set excludes the char at `pos` cannot
896
+ // match there, whatever the other arms do. `firstSetOf` already skips a
897
+ // leading zero-width assertion so `not(X) Y` reports firstSet(Y) rather
898
+ // than `any` (`first-set.ts` `isZeroWidthAssertion`), and `charClass`
899
+ // returns −1 for anything it cannot represent — both directions are the
900
+ // safe one. A NULLABLE arm is never gated: it can match at a position
901
+ // its first set does not contain, by consuming nothing.
902
+ //
903
+ // `resolveDispatch` decides from these classes whether the site keeps
904
+ // the O(1) table (`exclusive`) or falls to the ordered per-arm path.
905
+ // Arm ORDER is preserved on both, which is what makes this a PEG-safe
906
+ // change rather than a reordering.
907
+ const rr = this.refResolver()
908
+ const classes = arms.map(a => matchesEmpty(a, new Set(), rr) ? -1 : this.charClass(firstSetOf(a, new Set(), rr)))
909
+ const dispIdx = this.disp.length
910
+ this.disp.push(classes)
911
+ // PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. The ordered
912
+ // path reports the CONCATENATION of the arms' sets (`choice.ts:167`), and
913
+ // it must include the arms the driver's char gate declined to enter — the
914
+ // interpreter has no such gate, runs them, and gets exactly their static
915
+ // opener set back. Without these words a partially-gated site reported
916
+ // only the arms that happened to run.
917
+ const head = this.emitHead(OP_CHOICE, 3 + 2 * kids.length)
918
+ this.code[head + 1] = dispIdx
919
+ this.code[head + 2] = kids.length
920
+ // THE CHOICE'S OWN SET IS THE ARMS' FLATMAP, NOT `deriveExpected(p)`.
921
+ // `deriveExpected` DEDUPES, and neither engine does: `choice.ts:110-118`
922
+ // is `parsers.flatMap(p => p.parse(...).expected)`, so a shared-prefix
923
+ // choice whose two arms both open with `/::?/` reports it TWICE. The
924
+ // deduped union silently collapsed that to one.
925
+ this.code[head + 3] = this.expected(arms.flatMap(a => deriveExpected(a)))
926
+ for (let i = 0; i < kids.length; i++) {
927
+ this.code[head + 4 + i] = kids[i]!
928
+ this.code[head + 4 + kids.length + i] = this.expected(deriveExpected(arms[i]!))
929
+ }
930
+ return head
931
+ }
932
+ case 'many':
933
+ case 'oneOrMore': {
934
+ const child = this.node(d.parser).ip
935
+ const op = d.valueUnused ? OP_REPV : OP_REP
936
+ const min = d.tag === 'many' ? 0 : d.min
937
+ // ip + 6 is the ITEM's expected set and ip + 7 the separator's sentinel
938
+ // class (−1: there is no separator). Both are only laid down for a
939
+ // recovery table, where the resync error's payload has to be the ITEM's
940
+ // set — `deriveExpected(combinator)` in repeat.ts:170, and
941
+ // `deriveExpectedArr([def.parser])` in codegen's emitMany.
942
+ return this.rec
943
+ ? this.emit(op, child, min, d.max ?? -1, -1, 0, this.expected(deriveExpected(d.parser)), -1)
944
+ : this.emit(op, child, min, d.max ?? -1, -1, 0)
945
+ }
946
+ case 'sepBy': {
947
+ const child = this.node(d.parser).ip
948
+ const sep = this.node(d.separator).ip
949
+ // A list contributes its ITEMS and nothing else (release/0.47.0
950
+ // `7cb528e feat(lists)!`). The separator is demoted out of `children`
951
+ // after it matches unless the author opted in with `keepSeparator()`.
952
+ // Bit 2: an ITEM-expected fx index follows at `ip + 6`. A list that ends
953
+ // under `min` reports what would have let it CONTINUE — the ITEM, which
954
+ // is what `failAt` (repeat.ts) and `deriveExpectedArr([def.parser])`
955
+ // (codegen emitSepBy) both use; the driver otherwise reports whatever
956
+ // sub-parse failed last, which is the SEPARATOR. Only `min >= 2` can
957
+ // reach that end under `min` with items already taken — at `min === 1`
958
+ // it means the FIRST item failed, and that failure already set the
959
+ // item's own set. So no committed grammar grows by a word.
960
+ const flags = (d.trailing === 'allow' ? 1 : 0) | (d.keepSeparators === true ? 2 : 0) | (d.min >= 2 ? 4 : 0)
961
+ // A RECOVERY table always carries ip + 6 (the ITEM's expected set, which
962
+ // the resync error reports) and ip + 7 (the SEPARATOR's sentinel class, so
963
+ // a tolerant list resyncs to its own separator OR the enclosing
964
+ // delimiter — `orSentinel(separator, term)` in repeat.ts:454 and
965
+ // `_rec.or(sepSent, mySync)` in codegen's emitSepBy).
966
+ if (this.rec) {
967
+ return this.emit(
968
+ OP_REP, child, d.min, d.max ?? -1, sep, flags,
969
+ this.expected(deriveExpected(d.parser)), this.charClass(firstSetOf(d.separator, new Set(), this.refResolver())),
970
+ )
971
+ }
972
+ return d.min >= 2
973
+ ? this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags, this.expected(deriveExpected(d.parser)))
974
+ : this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags)
975
+ }
976
+ case 'optional':
977
+ return this.emit(OP_OPT, this.node(d.parser).ip)
978
+ case 'transform': {
979
+ // Declared on the def and not lowered here. Refuse rather than assume it is
980
+ // inert: a recognition-only transform suppresses its value, and a table that
981
+ // produced one anyway would differ from both other engines.
982
+ // FUSE `transform(sequence(...))` — the dominant shape — into one row.
983
+ // Emitted separately it costs two dispatches and two call frames per
984
+ // rule invocation. The inner sequence must not be shared with anything
985
+ // else, or fusing it here would steal it from the other reference.
986
+ const inner = d.parser._def as ParserDef
987
+ if (inner.tag === 'sequence' && !this.memo.has(d.parser) && !this.pending.has(d.parser)) {
988
+ const kids = inner.parsers.map(c => this.node(c).ip)
989
+ const head = this.emitHead(OP_SEQX, 2 + kids.length + (this.rec ? kids.length : 0))
990
+ this.code[head + 1] = this.fn(d.fn, d.fnSrc ?? null)
991
+ this.code[head + 2] = kids.length
992
+ for (let i = 0; i < kids.length; i++) this.code[head + 3 + i] = kids[i]!
993
+ if (this.rec) {
994
+ for (let i = 0; i < kids.length; i++) this.code[head + 3 + kids.length + i] = this.followClass(inner.parsers, i)
995
+ }
996
+ return head
997
+ }
998
+ const child = this.node(d.parser).ip
999
+ return this.emit(OP_XFORM, this.fn(d.fn, d.fnSrc ?? null), child)
1000
+ }
1001
+ case 'leaf': {
1002
+ const child = this.node(d.parser).ip
1003
+ return this.emit(OP_LEAF, this.fn(d.fn, d.fnSrc ?? null), child)
1004
+ }
1005
+ case 'node': {
1006
+ // A node legally has NO builder when its value comes from a selection:
1007
+ // `project` replaces the builder outright, and `collapse`/`unwrap` make
1008
+ // the single captured child the value. A node with none of those and no
1009
+ // builder is STRUCTURAL — its value comes from a `ctx.build` host.
1010
+ //
1011
+ // This USED to refuse, on the belief that the driver had no host. It
1012
+ // does: `assemble.ts` reads `ctx.build` once per parse in `begin()` and
1013
+ // bakes host-ness into which pieces the assembly holds, and `exec.ts`
1014
+ // has the same branch. The refusal was over-broad, not protective —
1015
+ // verified by differential against the interpreter, with a host and
1016
+ // without, on match and on failure.
1017
+ // An AUTHORING error, not a table gap — raise what the other two engines do.
1018
+ if (d.type === undefined) missingInferredType()
1019
+ const child = this.node(d.parser).ip
1020
+ // Capture flags, resolved HERE from the reducer's declared arity using the
1021
+ // same analysis codegen runs (`src/compiler/build-arity.ts`). `hostMode:
1022
+ // 'cst'` forces them on, exactly as the emitted `cstOut` path does. The
1023
+ // driver reads a bit; it re-derives nothing and sees no setting.
1024
+ const cstOut = this.settings.hostMode === 'cst'
1025
+ // THE THREE DERIVED CAPTURE BITS, mirroring `node.ts:215` term for term:
1026
+ //
1027
+ // capturesTrivia = captureTrivia || trailingTrivia
1028
+ // || (build ? buildReadsTrivia(def) : project === undefined)
1029
+ //
1030
+ // `cstOut` is the static stand-in for "a CST host is coming", which the
1031
+ // interpreter reaches dynamically off `ctx.build`.
1032
+ //
1033
+ // Two of these terms were MISSING and each was a real divergence:
1034
+ //
1035
+ // `captureTrivia` — an explicit request the arity analysis cannot
1036
+ // express (an author can ask for capture on a 3-argument reducer).
1037
+ // This used to REFUSE rather than lower, so a documented option no
1038
+ // grammar could use through the table.
1039
+ // `trailingTrivia` — trivia consumed INSIDE the node's capture scope
1040
+ // lands in THIS node's log. The interpreter counts it; the table did
1041
+ // not, so a node with `trailingTrivia` and a non-trivia-reading
1042
+ // reducer captured under the interpreter and dropped under the table.
1043
+ //
1044
+ // The `build ? … : project === undefined` split matters now that a
1045
+ // STRUCTURAL node lowers: with no builder the interpreter captures unless
1046
+ // a `project` replaces the value outright.
1047
+ const noBuildCaptures = d.project === undefined
1048
+ const derivedTrivia = d.build !== undefined ? buildReadsTrivia(d) : noBuildCaptures
1049
+ const derivedState = d.build !== undefined ? buildReadsState(d) : noBuildCaptures
1050
+ const derivedFields = d.build !== undefined ? buildReadsFields(d) : noBuildCaptures
1051
+ // Field capture additionally requires the body to CONTAIN `field()`
1052
+ // captures: a node that reads fields but has none allocates nothing.
1053
+ const wantsFields = parserHasOwnFields(d.parser) && (cstOut || derivedFields)
1054
+ const flags = (cstOut || d.captureTrivia === true || d.trailingTrivia === true || derivedTrivia ? 4 : 0)
1055
+ | (cstOut || derivedState ? 8 : 0)
1056
+ | (wantsFields ? 16 : 0)
1057
+ | (d.collapse === true ? 32 : 0)
1058
+ | (d.unwrap === true ? 64 : 0)
1059
+ | (d.trailingTrivia === true ? 128 : 0)
1060
+ // Distinguish grammar-owned capture from a structural node's default
1061
+ // capture. A runtime host predicate may narrow only the latter.
1062
+ | (d.captureTrivia === true ? 1 : 0)
1063
+ const body = this.emit(
1064
+ this.track ? OP_NODE_TRACK : OP_NODE,
1065
+ d.build === undefined ? -1 : this.fn(d.build, d.buildSrc ?? null),
1066
+ child, flags,
1067
+ d.project ?? -1,
1068
+ this.constant(d.type),
1069
+ // `tags` reaches a `ctx.build` host as its 8th argument. jess's
1070
+ // `cssCstBuildHost` is built with `{ tags: true }` and puts them on
1071
+ // every CST node, so this is load-bearing, not reflection trivia.
1072
+ d.tags === undefined ? -1 : this.constant(d.tags),
1073
+ )
1074
+ // The rule's own first-set gate — the emitted code's `_ngc` test, as data.
1075
+ // A NULLABLE rule has no gate: it succeeds on input its first set does
1076
+ // not contain (including EOF), so gating it would reject a legal empty
1077
+ // match. Caught by the ladder's `empty` and `garbage` cases.
1078
+ // THE BODY'S first set, not the node's. `node.ts:232` reads
1079
+ // `combinator._meta.firstSet` where `combinator` is the BODY; a `node()`
1080
+ // reports `any` for itself, so `firstSetOf(p)` was −1 here and NO node
1081
+ // ever got the gate the interpreter applies. That is not just a lost
1082
+ // fast path: when the guard fires it reports the STATIC
1083
+ // `deriveExpected(body)`, which for a body with nullable leading terms
1084
+ // names tokens the run would have skipped — `['"@"', '"(", '"x"']` where
1085
+ // running the body reports `['"x"']` alone.
1086
+ if (matchesEmpty(d.parser)) return body
1087
+ const fs = d.parser._meta.firstSet
1088
+ if (fs.kind === 'any') return body
1089
+ const cls = this.charClass(fs)
1090
+ if (cls < 0) return body
1091
+ const e = deriveExpected(d.parser)
1092
+ return this.emit(OP_GATE, cls, body, this.expected(e.length > 0 ? e : [d.parser._tag]))
1093
+ }
1094
+ case 'token':
1095
+ return this.emit(OP_TOKEN, this.node(d.parser).ip)
1096
+ case 'routed':
1097
+ return this.emit(OP_ROUTED, d.fallback === undefined ? -1 : this.node(d.fallback).ip)
1098
+ case 'dispatch': {
1099
+ // A `routed()` in the SELECTOR is a hard error in the interpreter
1100
+ // (dispatch.ts:318) — it would ask for a token that has not been read yet.
1101
+ if (parserUsesRouted(d.selector as Combinator<unknown>)) {
1102
+ throw new UnsupportedConstruct('dispatch(routed() in selector)')
1103
+ }
1104
+ const sel = this.node(d.selector as Combinator<unknown>).ip
1105
+ // `dispatchArmIds` mints ids in RESOLUTION ORDER — every case (one id per
1106
+ // case, not per key), then every matcher, then `otherwise` — which is the
1107
+ // order the two loops below push arms in, with the fallback last. A
1108
+ // running index is what keeps the two in step; taking `ids[armIndex]`
1109
+ // inside the key loop would consume one id per KEY and slide every
1110
+ // subsequent arm onto somebody else's identity.
1111
+ const dispCovIds = this.plan?.dispatches.get(p)
1112
+ let dispCov = 0
1113
+ const arms: number[] = []
1114
+ const routed: number[] = []
1115
+ const key: string[] = [], keyArm: number[] = []
1116
+ const fold: string[] = [], foldArm: number[] = []
1117
+ const match: Array<readonly [number, string, string, number]> = []
1118
+ const expected: string[] = []
1119
+ // ARM ORDER IS RESOLUTION ORDER: exact key, then ASCII-folded key, then
1120
+ // matchers in declaration order, then otherwise. Mirrors dispatch.ts:325.
1121
+ for (const c of d.cases) {
1122
+ const arm = arms.length
1123
+ arms.push(this.covWrap(this.node(c.parser).ip, dispCovIds?.[dispCov++], 1))
1124
+ routed.push(branchUsesRouted(c) ? 1 : 0)
1125
+ for (const kk of c.keys) {
1126
+ expected.push(JSON.stringify(kk))
1127
+ if (c.caseInsensitive) { fold.push(asciiFoldKey(kk)); foldArm.push(arm) }
1128
+ else { key.push(kk); keyArm.push(arm) }
1129
+ }
1130
+ }
1131
+ const KIND = { startsWith: 0, endsWith: 1, matches: 2 } as const
1132
+ for (const m of d.matchers ?? []) {
1133
+ const arm = arms.length
1134
+ arms.push(this.covWrap(this.node(m.parser).ip, dispCovIds?.[dispCov++], 1))
1135
+ routed.push(branchUsesRouted(m) ? 1 : 0)
1136
+ // `{ caseInsensitive: true }` ON A MATCHER ARM IS FOLDED INTO THE
1137
+ // OPERANDS HERE, not tested per parse in the driver. It was dropped
1138
+ // entirely — both drivers built the matcher shape with a hardcoded
1139
+ // `caseInsensitive: false` — so `when(startsWith('pre'), …, { caseInsensitive: true })`
1140
+ // never claimed `PRElude` and the parse fell through to `otherwise()`.
1141
+ // A wrong ARM is a wrong parse, and no key-path test covered it because
1142
+ // the folded-KEY path (`fold`/`foldArm` above) is a different mechanism.
1143
+ //
1144
+ // `matches` folds into the regex's own `i` flag, which is what
1145
+ // `matchesDispatchMatcher` does (it tests the RAW value with `i`
1146
+ // appended). `startsWith`/`endsWith` fold BOTH sides, so the stored
1147
+ // value is pre-folded and kinds 3/4 tell the driver to fold the key.
1148
+ if (!m.caseInsensitive) { match.push([KIND[m.kind], m.value, m.flags ?? '', arm]); continue }
1149
+ if (m.kind === 'matches') {
1150
+ const f = m.flags ?? ''
1151
+ match.push([2, m.value, f.includes('i') ? f : `${f}i`, arm])
1152
+ continue
1153
+ }
1154
+ match.push([m.kind === 'startsWith' ? 3 : 4, asciiFoldKey(m.value), '', arm])
1155
+ }
1156
+ const other = d.otherwise === undefined
1157
+ ? -1
1158
+ : this.covWrap(this.node(d.otherwise).ip, dispCovIds?.[dispCov++], 1)
1159
+ const dspIdx = this.dsp.length
1160
+ this.dsp.push({ key, keyArm, fold, foldArm, match, routed, expected })
1161
+ const head = this.emitHead(OP_DISPATCH, 5 + arms.length)
1162
+ this.code[head + 1] = sel
1163
+ this.code[head + 2] = dspIdx
1164
+ this.code[head + 3] = other
1165
+ // THE FALLBACK'S ROUTED BIT IS WALKED, NOT READ OFF THE DEF.
1166
+ //
1167
+ // `d.otherwiseUsesRouted` is computed when `otherwise()` is CONSTRUCTED
1168
+ // (dispatch.ts:189), and at that moment a `g.X` inside the arm is an
1169
+ // unresolved ref whose thunk throws — `parserUsesRouted` catches and
1170
+ // answers `false`. The interpreter never reads the stored flag alone: it
1171
+ // calls `branchUsesRouted` at PARSE time (dispatch.ts:335), which ORs the
1172
+ // flag with a live walk of the now-resolved graph.
1173
+ //
1174
+ // Reading only the flag here is why `@charset "UTF-8";` — one line of
1175
+ // plain CSS — failed under the table with `expected: ["routed()"]`: the
1176
+ // fallback ran at the selector's END with no routed token, so the
1177
+ // `routed()` inside it had nothing to yield. Every case arm was already
1178
+ // walked (`branchUsesRouted(c)` above); the fallback was the one branch
1179
+ // that was not.
1180
+ const otherRouted = d.otherwise === undefined
1181
+ ? false
1182
+ : branchUsesRouted({ parser: d.otherwise as Combinator<unknown>, usesRouted: d.otherwiseUsesRouted === true })
1183
+ this.code[head + 4] = otherRouted ? 1 : 0
1184
+ this.code[head + 5] = arms.length
1185
+ for (let i = 0; i < arms.length; i++) this.code[head + 6 + i] = arms[i]!
1186
+ return head
1187
+ }
1188
+ case 'field':
1189
+ return this.emit(OP_FIELD, this.constant(d.name), this.node(d.parser).ip)
1190
+ case 'lazy': {
1191
+ // A named reference is not a hop: it resolves to the target's row.
1192
+ // Emitting a trampoline here cost one dispatch per reference for nothing.
1193
+ //
1194
+ // AN UNDEFINED `ref()` IS TOLERATED, not refused. `ref<T>()` throws from
1195
+ // its thunk until `.define()` runs, and codegen catches exactly this and
1196
+ // falls back to running the ref live (`emitLazy`'s two `catch` arms ->
1197
+ // `emitRuntimeFallback`), so a grammar assembled before every slot is
1198
+ // filled still COMPILES and throws only if that slot is actually parsed
1199
+ // through. The encoder threw at build time instead, which made the table
1200
+ // lowering refuse a grammar the source lowering accepts. Deferring to the
1201
+ // ref's own `.parse` reproduces codegen's timing and its message.
1202
+ // BY NAME first, when this reference names a rule of the map being
1203
+ // encoded — see `winners` for why a thunk is the wrong resolver for a
1204
+ // merged (composed) map, and why the identity guard is required.
1205
+ const refName = (p as unknown as { _ruleName?: string })._ruleName
1206
+ const winner = refName === undefined ? undefined : this.winners?.[refName]
1207
+ if (winner !== undefined && !wrapsRef(winner, p)) return this.scopedRef(p, winner)
1208
+ let resolved: Combinator<unknown>
1209
+ try { resolved = d.thunk() }
1210
+ catch {
1211
+ this.runtimeOnly.add('ref() used before .define() — the slot is run live')
1212
+ return this.emit(OP_LIVE, this.fn(p, null))
1213
+ }
1214
+ return this.scopedRef(p, resolved)
1215
+ }
1216
+ // `not.ts:50` fails with EXACTLY `not(<child tag>)`, at the assertion's own
1217
+ // position — the same shape `OP_PEEK` below carries. The table emitted no
1218
+ // expected operand at all, so a `not()` failure reported `[]` where every
1219
+ // other engine reported `['not(literal)']`. It hid because `expected` is a
1220
+ // TOP-LEVEL field on `RunResult` and is NOT part of the identity digest.
1221
+ case 'not':
1222
+ return this.emit(OP_NOT, this.node(d.parser).ip, this.expected([`not(${d.parser._tag})`]))
1223
+ case 'guard':
1224
+ // `'gate'`, the public name — see gate.ts. The def tag stays `'guard'`.
1225
+ return this.emit(OP_GUARD, this.fn(d.predicate, d.predSrc ?? null), this.expected(['gate']))
1226
+ case 'adjacency':
1227
+ // A `kinds` LIST IS VALIDATED HERE, at encode time, against the trivia
1228
+ // table in scope — the same `resolveAdjacencyKindMask` the interpreter
1229
+ // calls on first reach and codegen calls while emitting the `_akN` probe
1230
+ // (`codegen.ts:1034`). Deferring it to the driver's parse-time resolution
1231
+ // left `compile()` accepting a grammar whose kind name no active table
1232
+ // declares: an unlabelled table or a typo'd category compiled clean and
1233
+ // threw only when an input happened to reach the assertion.
1234
+ if (d.kinds !== undefined) resolveAdjacencyKindMask(this.activeTrivia, d.kinds)
1235
+ // The expected label is `adjacencyExpected`'s, not `deriveExpected`'s:
1236
+ // the interpreter fails with exactly `adjacent` / `notAdjacent` /
1237
+ // `notAdjacent(a|b)` (adjacency.ts `adjacencyFail`) and codegen emits the
1238
+ // same string, so this is the one the identity sweep compares.
1239
+ return this.emit(
1240
+ OP_ADJ,
1241
+ d.polarity === 'notAdjacent' ? 1 : 0,
1242
+ d.kinds === undefined ? -1 : this.kindsSlot(d.kinds),
1243
+ this.expected([adjacencyExpected(d)]),
1244
+ )
1245
+ case 'withCtx': {
1246
+ // `extra` is arbitrary user data in the const pool. `emitConst` takes
1247
+ // scalars, arrays of scalars, and plain objects of those — anything
1248
+ // richer (a class instance, a function value) RUNS fine but cannot be
1249
+ // printed into a module. Record it as a runtime-only reason so emit
1250
+ // refuses BY NAME rather than throwing a raw TypeError from the printer.
1251
+ if (!emittableConst(d.extra)) {
1252
+ this.runtimeOnly.add('withCtx(extra) — the state object is not serialisable')
1253
+ }
1254
+ return this.emit(OP_WITHCTX, this.constant(d.extra), this.node(d.parser).ip)
1255
+ }
1256
+ case 'peek':
1257
+ // The expected set is the ASSERTION's, not the body's: `peek.ts:60`
1258
+ // reports `peek(<child tag>)` and DISCARDS what the body wanted — a
1259
+ // lookahead's failure is "the guard did not hold", and naming the body's
1260
+ // internals offers a token the parse never asked the author for. The
1261
+ // table propagated the body's set instead.
1262
+ return this.emit(OP_PEEK, this.node(d.parser).ip, this.expected([`peek(${d.parser._tag})`]))
1263
+ // A TRANSACTION IS A ROW. See `OP_ATTEMPT` for why the transparent
1264
+ // lowering was correct only for a choice arm.
1265
+ case 'attempt': {
1266
+ const inner = this.emit(OP_ATTEMPT, this.node(d.parser).ip)
1267
+ // THE FIRST-SET FAIL-FAST GUARD, lowered as the `OP_GATE` row the `node()`
1268
+ // case already uses — it is the same guard, written twice in the
1269
+ // interpreter (`attempt.ts:22`, `node.ts:239`). It is NOT merely an
1270
+ // optimisation: when it fires it reports the STATIC `deriveExpected` of
1271
+ // the inner, which for a body with nullable leading terms names tokens the
1272
+ // run itself would have skipped past. Running the inner instead reported a
1273
+ // strictly smaller set, so omitting the guard was an observable divergence,
1274
+ // not a slower equivalent.
1275
+ const fs = d.parser._meta.firstSet
1276
+ if (fs.kind === 'any' || matchesEmpty(d.parser)) return inner
1277
+ const cls = this.charClass(fs)
1278
+ if (cls < 0) return inner
1279
+ const e = deriveExpected(d.parser)
1280
+ return this.emit(OP_GATE, cls, inner, this.expected(e.length > 0 ? e : [d.parser._tag]))
1281
+ }
1282
+ // A LABEL IS A ROW — see `OP_LABEL`. It replaces the child's expected set,
1283
+ // which is the entire combinator.
1284
+ // A LABEL IS CREDITED ON SUCCESS, as `codegen.ts:4530` credits it: its hook
1285
+ // is emitted AFTER the child's statements, where a failed child has already
1286
+ // broken out of the labelled block.
1287
+ case 'label':
1288
+ return this.covWrap(
1289
+ this.emit(OP_LABEL, this.node(d.parser).ip, this.expected([d.label])),
1290
+ this.plan?.labels.get(p)?.[0],
1291
+ 1,
1292
+ )
1293
+ // Transparent wrapper: no row of its own, no dispatch at run time.
1294
+ case 'trivia':
1295
+ return this.node(d.parser).ip
1296
+ case 'grammar': {
1297
+ // Fail closed on the scope switches this encoder does not carry. `trackLines`
1298
+ // is the one field here that is RECONCILED rather than refused: the driver
1299
+ // takes it from TableSettings, so a scope asking for something different is a
1300
+ // silent disagreement between the grammar and the artifact.
1301
+ // `rootCapture: 'opaque'` is INERT for parsing. grammar.ts reads it only
1302
+ // inside a `_rootTriviaStrictScopes` validation throw; it declares that
1303
+ // this scope's trivia is opaque to root capture, which `run()` consumes.
1304
+ // Nothing in the parse changes, so there is nothing for the table to
1305
+ // lower — accepting it is not a gap, it is the correct lowering.
1306
+ // ONLY `true` IS MEANINGFUL HERE. `grammar.ts:94` stores
1307
+ // `opts.trackLines ?? false`, so an UNSET scope and an explicitly-false
1308
+ // one are the same value on the def — the distinction is lost. At parse
1309
+ // time `:103` reads `opts.trackLines ?? _ctx?.trackLines ?? false`, i.e.
1310
+ // unset INHERITS. So stored-false cannot be read as "force off", and
1311
+ // comparing it against the setting refused every inner scope of a
1312
+ // trackLines:true grammar — all four `*PositionsGrammar`s.
1313
+ //
1314
+ // A SCOPE THAT ASKS FOR TRACKING NO LONGER REFUSES. `encodeTable` folds
1315
+ // every such scope into the table's own `track` before encoding begins
1316
+ // (see `hasScopedTrackLines`), exactly as codegen folds them into one
1317
+ // `ctx.lineTracking` for the whole compile. The refusal here was reachable
1318
+ // only because the table read the SETTING alone, and it made the table
1319
+ // lowering reject `parser({ trackLines: true }, …)` — a grammar
1320
+ // `compile()` accepts and annotates.
1321
+ // A trivia scope is a ROW, not a lowering decision: the scope's trivia
1322
+ // combinator goes in the const pool and the driver installs it.
1323
+ // `captureTrivia: true` picks the SCOPE_CAP piece. It is an OR with the
1324
+ // inherited context in the interpreter (`grammar.ts:129`), never a
1325
+ // switch-off, so a scope without it emits plain SCOPE and leaves an
1326
+ // outer capture alone. A scope that ONLY asks for capture still needs a
1327
+ // row — hence the `cap` branch before the `triviaParser === undefined`
1328
+ // shortcut, which would otherwise drop the request on the floor.
1329
+ const cap = d.captureTrivia === true
1330
+ const op = cap ? OP_SCOPE_CAP : OP_SCOPE
1331
+ // ROOT-CAPTURE POLICY, as two bits on the row (ip + 3).
1332
+ //
1333
+ // `rootCapture: 'opaque'` is NOT inert — that claim was wrong, and it is
1334
+ // what let a table parse hand back root markers from a region the grammar
1335
+ // declared opaque. `grammar.ts:141` sets `ctx._rootTriviaCapture = false`
1336
+ // for the scope, and the scope's ctx is a COPY there, so the flag reverts
1337
+ // by construction; the table shares one ctx, so the piece has to restore it.
1338
+ //
1339
+ // Bit 1 is the STRICT-SCOPE refusal `grammar.ts:98` raises: with selected
1340
+ // root trivia active, a local scope whose trivia is not classified and is
1341
+ // not declared opaque cannot be reported faithfully, so it throws rather
1342
+ // than silently dropping markers. Codegen emits the same throw
1343
+ // (codegen.ts:4705); the table emitted nothing at all.
1344
+ const flags = (d.rootCapture === 'opaque' ? 1 : 0)
1345
+ | (d.triviaParser !== undefined
1346
+ && d.triviaParser._meta.rootTriviaClassified !== true
1347
+ && d.rootCapture !== 'opaque' ? 2 : 0)
1348
+ // THE SCOPE'S TRIVIA IS IN SCOPE FOR ITS BODY. Tracked across the child
1349
+ // descent so `case 'adjacency'` can resolve a `kinds` list against the
1350
+ // table that will actually be active there, exactly as codegen's
1351
+ // `ctx.activeTrivia` does. Saved and restored rather than assigned,
1352
+ // because a scope is nestable and an inner one does not leak outward.
1353
+ const savedTrivia = this.activeTrivia
1354
+ if (d.clearTrivia === true) this.activeTrivia = undefined
1355
+ else if (d.triviaParser !== undefined) this.activeTrivia = d.triviaParser
1356
+ const bodyIp = this.node(d.parser).ip
1357
+ this.activeTrivia = savedTrivia
1358
+ if (d.clearTrivia === true) return this.emit(op, -1, bodyIp, flags)
1359
+ if (d.triviaParser === undefined) {
1360
+ return cap || flags !== 0 ? this.emit(op, -1, bodyIp, flags) : bodyIp
1361
+ }
1362
+ return this.emit(op, this.triviaSlot(d.triviaParser), bodyIp, flags)
1363
+ }
1364
+ /**
1365
+ * A HAND-WRITTEN COMBINATOR. `_def: { tag: 'unknown' }` is the designated
1366
+ * escape for a parser built outside the library, so there is no structure
1367
+ * to lower — the behaviour is a closure. Codegen accepts one and delegates
1368
+ * to `.parse` at run time (`emitRuntimeFallback`); refusing it here made
1369
+ * the table lowering reject a grammar the source lowering compiles.
1370
+ *
1371
+ * Runtime-only BY NAME, because a live combinator cannot be printed —
1372
+ * codegen degrades the same way (`runtimeParsers` non-empty ⇒ no
1373
+ * `inlineExpression`).
1374
+ */
1375
+ case 'unknown':
1376
+ this.runtimeOnly.add(`${p._tag} — a hand-written combinator, run live through its own .parse`)
1377
+ return this.emit(OP_LIVE, this.fn(p, null))
1378
+ default:
1379
+ // NOT widened to `OP_LIVE`. A LIBRARY tag reaching here is a missing
1380
+ // lowering, and silently running it live would trade a build error for a
1381
+ // permanent slow path nobody would ever find.
1382
+ throw new UnsupportedConstruct(d.tag)
1383
+ }
1384
+ }
1385
+
1386
+ /**
1387
+ * Peephole: an `OP_RULE` row is pure indirection — a switch dispatch and a
1388
+ * call frame to do nothing but jump. Rewrite every child slot that points at
1389
+ * one to point at its target instead. Transitive, and cycle-safe by bounding
1390
+ * the walk at the number of rows.
1391
+ */
1392
+ private collapseIndirection(): void {
1393
+ const resolve = (ip: number): number => {
1394
+ let t = ip
1395
+ for (let n = 0; n < this.code.length && this.code[t] === OP_RULE; n++) t = this.code[t + 1]!
1396
+ return t
1397
+ }
1398
+ const slots = (ip: number): number[] => {
1399
+ switch (this.code[ip]) {
1400
+ case OP_GATE: return [ip + 2]
1401
+ case OP_RULE: case OP_OPT: case OP_NOT: case OP_PEEK: case OP_EXPECT: case OP_ATTEMPT: case OP_LABEL: case OP_COV: return [ip + 1]
1402
+ case OP_SCOPE: case OP_SCOPE_CAP: case OP_WITHCTX: case OP_XFORM: case OP_LEAF: case OP_NODE: case OP_NODE_TRACK: return [ip + 2]
1403
+ case OP_SEQ: case OP_SEQV: return Array.from({ length: this.code[ip + 1]! }, (_, i) => ip + 2 + i)
1404
+ case OP_SEQX: return Array.from({ length: this.code[ip + 2]! }, (_, i) => ip + 3 + i)
1405
+ // ARMS START AT ip+4. `ip+3` is the choice's own EXPECTED-SET index, and
1406
+ // rewriting it here treated an `fx` index as a code offset: when the row
1407
+ // at that offset happened to be `OP_RULE` the index was replaced by that
1408
+ // row's target, so a failing choice reported a different expected set —
1409
+ // and the bogus "target" was then pushed onto the walk and its operands
1410
+ // rewritten as if they were child slots. It also left the LAST arm
1411
+ // uncollapsed, which is the only part of this that was merely slow.
1412
+ case OP_CHOICE: return Array.from({ length: this.code[ip + 2]! }, (_, i) => ip + 4 + i)
1413
+ // GREEDY's operands INTERLEAVE a const index with an arm offset, so only
1414
+ // the odd slots are children — rewriting a const-pool index as if it were
1415
+ // a code offset is the same class of bug the OP_CHOICE note above records.
1416
+ case OP_GREEDY: return [ip + 1, ...Array.from({ length: this.code[ip + 2]! }, (_, i) => ip + 4 + 2 * i)]
1417
+ // REJECT's check operands are const/class indices, never offsets.
1418
+ case OP_REJECT: return [ip + 1]
1419
+ case OP_REP: case OP_REPV: return this.code[ip + 4]! >= 0 ? [ip + 1, ip + 4] : [ip + 1]
1420
+ default: return []
1421
+ }
1422
+ }
1423
+ const seen = new Set<number>()
1424
+ // Scan subtrees are ROOTS too: a sentinel or skipper is reached through
1425
+ // `prog.scans`, not through any code slot, so leaving them out left their
1426
+ // interiors uncollapsed and — worse — left the spec's own offset pointing at
1427
+ // a trampoline whose target had been rewritten around it.
1428
+ const refs: SubtreeRef[] = []
1429
+ for (const s of this.scans) {
1430
+ if (s.sentinel !== undefined) refs.push(s.sentinel)
1431
+ refs.push(...s.skip)
1432
+ }
1433
+ for (const set of this.scanSkipSets) refs.push(...set)
1434
+ const stack = [...Object.values(this.rules), ...refs.map(r => r[0])]
1435
+ while (stack.length > 0) {
1436
+ const ip = stack.pop()!
1437
+ if (seen.has(ip)) continue
1438
+ seen.add(ip)
1439
+ for (const slot of slots(ip)) {
1440
+ const target = resolve(this.code[slot]!)
1441
+ this.code[slot] = target
1442
+ stack.push(target)
1443
+ }
1444
+ }
1445
+ for (const name of Object.keys(this.rules)) this.rules[name] = resolve(this.rules[name]!)
1446
+ const res = (r: SubtreeRef): SubtreeRef => [resolve(r[0]), r[1]]
1447
+ this.scans = this.scans.map(s => ({
1448
+ ...s,
1449
+ skip: s.skip.map(res),
1450
+ ...(s.sentinel === undefined ? {} : { sentinel: res(s.sentinel) }),
1451
+ }))
1452
+ this.scanSkipSets = this.scanSkipSets.map(set => set.map(res))
1453
+ }
1454
+
1455
+ finish(): TableProgram {
1456
+ if (this.code.length === 0) this.emit(OP_EMPTY)
1457
+ this.collapseIndirection()
1458
+ return ownTableProgram({
1459
+ code: this.code, k: this.k, fns: this.fns, cc: this.cc,
1460
+ fx: this.fx, disp: this.disp, dsp: this.dsp, rules: this.rules,
1461
+ ...(this.labels === undefined ? {} : { labels: this.labels }),
1462
+ ...(this.classified ? { classified: 1 as const } : {}),
1463
+ ...(this.scanSkipSets.length === 0
1464
+ ? {}
1465
+ : { scanSkip: this.scanSkipSets, scanSkipOf: this.scanSkipOf }),
1466
+ ...(this.scans.length === 0 ? {} : { scans: this.scans }),
1467
+ ...(this.settings.hostMode === undefined ? {} : { hostMode: this.settings.hostMode }),
1468
+ ...(this.runtimeOnly.size === 0 ? {} : { runtimeOnly: [...this.runtimeOnly].sort() }),
1469
+ ...(this.triviaSpecs.length === 0 ? {} : { triviaSpecs: this.triviaSpecs }),
1470
+ ...(this.rec ? { rec: 1 as const } : {}),
1471
+ ...(this.cov === undefined ? {} : { cov: this.cov }),
1472
+ lines: this.track ? 1 : 0,
1473
+ })
1474
+ }
1475
+ }
1476
+
1477
+ /**
1478
+ * Encode a rule map into ONE table for ONE settings pair.
1479
+ *
1480
+ * Two settings pairs give two programs and therefore two cached reference
1481
+ * tables; the driver that reads them is the same function in both cases and
1482
+ * never sees `settings`.
1483
+ */
1484
+ /**
1485
+ * Does this grammar ask for line tracking anywhere in its own structure?
1486
+ *
1487
+ * TRACKING IS A WHOLE-TABLE DECISION, as it is a whole-compile decision for the
1488
+ * source lowering: `ctx.lineTracking` is `opts.trackLines || grammarTrackLines ||
1489
+ * hasLineTrackingDef(combinator)` (`compiler/codegen.ts`). The table read the
1490
+ * SETTING alone and refused any `parser({ trackLines: true }, …)` scope it found
1491
+ * underneath, so a grammar that carries its own tracking compiled through
1492
+ * codegen and was rejected by the encoder. Same question, same answer.
1493
+ *
1494
+ * A `lazy` is followed, because a rule reference is how a grammar reaches most of
1495
+ * itself; an undefined one answers `false` rather than throwing, as codegen's does.
1496
+ */
1497
+ function hasScopedTrackLines(p: Combinator<unknown>, seen: Set<Combinator<unknown>>): boolean {
1498
+ if (seen.has(p)) return false
1499
+ seen.add(p)
1500
+ if (p._meta.grammarTrackLines === true) return true
1501
+ const d = p._def
1502
+ switch (d.tag) {
1503
+ case 'grammar':
1504
+ if (d.trackLines === true) return true
1505
+ break
1506
+ case 'lazy':
1507
+ try { return hasScopedTrackLines(d.thunk(), seen) }
1508
+ catch { return false }
1509
+ // Neither is covered by the shared `childrenOf`: it drops `dispatch` matcher
1510
+ // arms and treats `routed()` as a leaf.
1511
+ case 'dispatch':
1512
+ for (const m of d.matchers ?? []) if (hasScopedTrackLines(m.parser, seen)) return true
1513
+ break
1514
+ case 'routed':
1515
+ return d.fallback === undefined ? false : hasScopedTrackLines(d.fallback, seen)
1516
+ default:
1517
+ break
1518
+ }
1519
+ for (const c of childrenOf(d)) if (hasScopedTrackLines(c, seen)) return true
1520
+ return false
1521
+ }
1522
+
1523
+ export function encodeTable(
1524
+ ruleMap: Record<string, Combinator<unknown>>,
1525
+ settings: TableSettings = {},
1526
+ ): TableProgram {
1527
+ return encodeTableProgram(ruleMap, settings).prog
1528
+ }
1529
+
1530
+ /**
1531
+ * `encodeTable` PLUS the reducer sources, for a caller that must PRINT the table.
1532
+ *
1533
+ * The sources are not on `TableProgram` because they are not data the driver
1534
+ * reads and must never travel in an emitted artifact; they are the printer's
1535
+ * input, in `prog.fns` order. A build that lowers macro-evaluated combinators
1536
+ * gets a real source per entry (`fnSrc` / `buildSrc` / `predSrc` / `gateSrcs`,
1537
+ * set by the macro evaluator); a runtime caller gets `null`s and must therefore
1538
+ * refuse to print rather than emit `() => {}` placeholders.
1539
+ */
1540
+ export function encodeTableProgram(
1541
+ ruleMap: Record<string, Combinator<unknown>>,
1542
+ settings: TableSettings = {},
1543
+ ): { prog: TableProgram; fnSrcs: (string | null)[] } {
1544
+ // Line tracking is decided ONCE for the whole artifact, as codegen does
1545
+ // (`opts.trackLines || grammarTrackLines || hasLineTrackingDef(combinator)`).
1546
+ // Reading only the SETTING made the table narrower than codegen and refused a
1547
+ // `parser({ trackLines: true })` scope that codegen simply accepts.
1548
+ const names = Object.keys(ruleMap)
1549
+ const seen = new Set<Combinator<unknown>>()
1550
+ const track = settings.trackLines === true
1551
+ || names.some(n => hasScopedTrackLines(ruleMap[n]!, seen))
1552
+ // Same precedence as compileRuleMap/compose: an explicit lowering option wins;
1553
+ // otherwise a `rules({ hostMode: 'cst' })` declaration stamped on its entries
1554
+ // selects CST. (`'ast'` is the unstamped default.) Reading only `settings`
1555
+ // produced two opposite contracts for one grammar: compose refused a hostless
1556
+ // run while public encodeTable silently emitted and stamped an AST table.
1557
+ const hostMode = settings.hostMode
1558
+ ?? names.map(n => ruleMap[n]!._meta.grammarHostMode).find(Boolean)
1559
+ const resolvedSettings = {
1560
+ ...settings,
1561
+ ...(hostMode === undefined ? {} : { hostMode }),
1562
+ ...(track ? { trackLines: true } : {}),
1563
+ }
1564
+ const enc = new Encoder(resolvedSettings)
1565
+ enc.winners = ruleMap
1566
+ for (const name of names) enc.encodeRule(name, ruleMap[name]!)
1567
+ const prog = enc.finish()
1568
+ return { prog, fnSrcs: enc.fnSrcs }
1569
+ }