parseman 0.46.0 → 0.47.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/README.md +8 -3
  2. package/dist/analysis/choice-cost-render.d.ts +12 -4
  3. package/dist/analysis/choice-cost-render.d.ts.map +1 -1
  4. package/dist/analysis/choice-cost.d.ts +2 -2
  5. package/dist/analysis/choice-cost.d.ts.map +1 -1
  6. package/dist/analysis/commitment.d.ts +131 -0
  7. package/dist/analysis/commitment.d.ts.map +1 -0
  8. package/dist/analysis/diagnostics.cjs +10339 -7787
  9. package/dist/analysis/diagnostics.cjs.map +3 -4
  10. package/dist/analysis/diagnostics.d.ts +2 -2
  11. package/dist/analysis/diagnostics.js +10339 -7787
  12. package/dist/analysis/diagnostics.js.map +3 -4
  13. package/dist/analysis/duplication.d.ts.map +1 -1
  14. package/dist/analysis/fix.d.ts +3 -3
  15. package/dist/analysis/fix.d.ts.map +1 -1
  16. package/dist/analysis/format-number.d.ts +23 -0
  17. package/dist/analysis/format-number.d.ts.map +1 -0
  18. package/dist/analysis/gating.d.ts +39 -2
  19. package/dist/analysis/gating.d.ts.map +1 -1
  20. package/dist/analysis/rebuild.d.ts.map +1 -1
  21. package/dist/analysis/terminal.d.ts +5 -2
  22. package/dist/analysis/terminal.d.ts.map +1 -1
  23. package/dist/cli/index.cjs +10924 -8506
  24. package/dist/cli/index.cjs.map +3 -4
  25. package/dist/cli/index.js +10924 -8506
  26. package/dist/cli/index.js.map +3 -4
  27. package/dist/combinators/adjacency.d.ts +100 -0
  28. package/dist/combinators/adjacency.d.ts.map +1 -0
  29. package/dist/combinators/choice.d.ts.map +1 -1
  30. package/dist/combinators/completions.d.ts.map +1 -1
  31. package/dist/combinators/dispatch.d.ts +8 -1
  32. package/dist/combinators/dispatch.d.ts.map +1 -1
  33. package/dist/combinators/expect.d.ts.map +1 -1
  34. package/dist/combinators/first-set.d.ts.map +1 -1
  35. package/dist/combinators/gate.d.ts.map +1 -1
  36. package/dist/combinators/grammar.d.ts +22 -0
  37. package/dist/combinators/grammar.d.ts.map +1 -1
  38. package/dist/combinators/literal.d.ts +8 -0
  39. package/dist/combinators/literal.d.ts.map +1 -1
  40. package/dist/combinators/map.d.ts +0 -1
  41. package/dist/combinators/map.d.ts.map +1 -1
  42. package/dist/combinators/node.d.ts +13 -3
  43. package/dist/combinators/node.d.ts.map +1 -1
  44. package/dist/combinators/parser.d.ts +1 -1
  45. package/dist/combinators/parser.d.ts.map +1 -1
  46. package/dist/combinators/ref.d.ts.map +1 -1
  47. package/dist/combinators/regex.d.ts +1 -16
  48. package/dist/combinators/regex.d.ts.map +1 -1
  49. package/dist/combinators/repeat.d.ts +30 -2
  50. package/dist/combinators/repeat.d.ts.map +1 -1
  51. package/dist/combinators/scanTo.d.ts +71 -1
  52. package/dist/combinators/scanTo.d.ts.map +1 -1
  53. package/dist/combinators/sequence.d.ts.map +1 -1
  54. package/dist/combinators/token.d.ts.map +1 -1
  55. package/dist/combinators/trivia-boundary.d.ts +26 -0
  56. package/dist/combinators/trivia-boundary.d.ts.map +1 -0
  57. package/dist/combinators/trivia-skip.d.ts +45 -1
  58. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  59. package/dist/combinators/withCtx.d.ts.map +1 -1
  60. package/dist/compiler/compile-linkable-table.d.ts +104 -0
  61. package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
  62. package/dist/compiler/degradation.d.ts +0 -2
  63. package/dist/compiler/degradation.d.ts.map +1 -1
  64. package/dist/compiler/fields.d.ts +13 -0
  65. package/dist/compiler/fields.d.ts.map +1 -1
  66. package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
  67. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  68. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  69. package/dist/compiler/linker.d.ts +47 -53
  70. package/dist/compiler/linker.d.ts.map +1 -1
  71. package/dist/compiler/token-alphabet.d.ts +81 -0
  72. package/dist/compiler/token-alphabet.d.ts.map +1 -0
  73. package/dist/compiler/token-dispatch.d.ts +95 -0
  74. package/dist/compiler/token-dispatch.d.ts.map +1 -0
  75. package/dist/compiler/token-scanner.d.ts +53 -0
  76. package/dist/compiler/token-scanner.d.ts.map +1 -0
  77. package/dist/compiler/value-usage.d.ts.map +1 -1
  78. package/dist/coverage.d.ts +1 -1
  79. package/dist/coverage.d.ts.map +1 -1
  80. package/dist/cst/capture-buffer.d.ts +35 -0
  81. package/dist/cst/capture-buffer.d.ts.map +1 -1
  82. package/dist/cst/reflection-attach.d.ts +8 -0
  83. package/dist/cst/reflection-attach.d.ts.map +1 -0
  84. package/dist/cst/reflection-symbols.d.ts +10 -0
  85. package/dist/cst/reflection-symbols.d.ts.map +1 -0
  86. package/dist/cst/reflection.d.ts +2 -4
  87. package/dist/cst/reflection.d.ts.map +1 -1
  88. package/dist/cst/root-trivia-scope.d.ts +26 -0
  89. package/dist/cst/root-trivia-scope.d.ts.map +1 -0
  90. package/dist/cst/trivia-charscan.d.ts +18 -0
  91. package/dist/cst/trivia-charscan.d.ts.map +1 -0
  92. package/dist/cst/trivia-kinds.d.ts +19 -0
  93. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  94. package/dist/functional/doc.d.ts.map +1 -1
  95. package/dist/functional/run-tabled.d.ts +12 -0
  96. package/dist/functional/run-tabled.d.ts.map +1 -0
  97. package/dist/functional/run.d.ts +25 -42
  98. package/dist/functional/run.d.ts.map +1 -1
  99. package/dist/grammar-metadata.d.ts +3 -0
  100. package/dist/grammar-metadata.d.ts.map +1 -0
  101. package/dist/index.cjs +9290 -7181
  102. package/dist/index.cjs.map +3 -4
  103. package/dist/index.d.ts +18 -9
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +9300 -7191
  106. package/dist/index.js.map +3 -4
  107. package/dist/language-service/index.cjs +313 -121
  108. package/dist/language-service/index.cjs.map +3 -4
  109. package/dist/language-service/index.js +313 -121
  110. package/dist/language-service/index.js.map +3 -4
  111. package/dist/oracle/index.cjs.map +0 -1
  112. package/dist/oracle/index.js.map +0 -1
  113. package/dist/parse-context.d.ts +49 -0
  114. package/dist/parse-context.d.ts.map +1 -0
  115. package/dist/plugin/evaluator.d.ts +40 -1
  116. package/dist/plugin/evaluator.d.ts.map +1 -1
  117. package/dist/plugin/index.cjs +11175 -9534
  118. package/dist/plugin/index.cjs.map +3 -4
  119. package/dist/plugin/index.d.ts.map +1 -1
  120. package/dist/plugin/index.js +11175 -9534
  121. package/dist/plugin/index.js.map +3 -4
  122. package/dist/regex/classes.d.ts +28 -0
  123. package/dist/regex/classes.d.ts.map +1 -1
  124. package/dist/regex/first-set.d.ts +14 -0
  125. package/dist/regex/first-set.d.ts.map +1 -1
  126. package/dist/run/index.cjs +136 -51
  127. package/dist/run/index.cjs.map +2 -3
  128. package/dist/run/index.js +136 -51
  129. package/dist/run/index.js.map +2 -3
  130. package/dist/spec/index.cjs +2934 -1611
  131. package/dist/spec/index.cjs.map +3 -4
  132. package/dist/spec/index.js +2934 -1611
  133. package/dist/spec/index.js.map +3 -4
  134. package/dist/spec/model.d.ts +8 -0
  135. package/dist/spec/model.d.ts.map +1 -1
  136. package/dist/table/assemble.d.ts +307 -0
  137. package/dist/table/assemble.d.ts.map +1 -0
  138. package/dist/table/cell.d.ts +42 -0
  139. package/dist/table/cell.d.ts.map +1 -0
  140. package/dist/table/child-slots.d.ts +30 -0
  141. package/dist/table/child-slots.d.ts.map +1 -0
  142. package/dist/table/compile-rule-map.d.ts +136 -0
  143. package/dist/table/compile-rule-map.d.ts.map +1 -0
  144. package/dist/table/compile.d.ts +56 -0
  145. package/dist/table/compile.d.ts.map +1 -0
  146. package/dist/table/duplication-hook.d.ts +32 -0
  147. package/dist/table/duplication-hook.d.ts.map +1 -0
  148. package/dist/table/emit-assembly.d.ts +139 -0
  149. package/dist/table/emit-assembly.d.ts.map +1 -0
  150. package/dist/table/emit.d.ts +113 -0
  151. package/dist/table/emit.d.ts.map +1 -0
  152. package/dist/table/encode-baseline.d.ts +16 -0
  153. package/dist/table/encode-baseline.d.ts.map +1 -0
  154. package/dist/table/encode.d.ts +71 -0
  155. package/dist/table/encode.d.ts.map +1 -0
  156. package/dist/table/exec-baseline.d.ts +9 -0
  157. package/dist/table/exec-baseline.d.ts.map +1 -0
  158. package/dist/table/exec.d.ts +72 -0
  159. package/dist/table/exec.d.ts.map +1 -0
  160. package/dist/table/fold.d.ts +25 -0
  161. package/dist/table/fold.d.ts.map +1 -0
  162. package/dist/table/index.cjs +11463 -0
  163. package/dist/table/index.cjs.map +6 -0
  164. package/dist/table/index.d.ts +100 -0
  165. package/dist/table/index.d.ts.map +1 -0
  166. package/dist/table/index.js +11440 -0
  167. package/dist/table/index.js.map +6 -0
  168. package/dist/table/inspect.d.ts +17 -0
  169. package/dist/table/inspect.d.ts.map +1 -0
  170. package/dist/table/ops.d.ts +453 -0
  171. package/dist/table/ops.d.ts.map +1 -0
  172. package/dist/table/program.d.ts +496 -0
  173. package/dist/table/program.d.ts.map +1 -0
  174. package/dist/table/run-support.d.ts +38 -0
  175. package/dist/table/run-support.d.ts.map +1 -0
  176. package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
  177. package/dist/table/scan-shapes.d.ts.map +1 -0
  178. package/dist/table/site-labels.d.ts +58 -0
  179. package/dist/table/site-labels.d.ts.map +1 -0
  180. package/dist/table/stamp.d.ts +70 -0
  181. package/dist/table/stamp.d.ts.map +1 -0
  182. package/dist/types.d.ts +80 -24
  183. package/dist/types.d.ts.map +1 -1
  184. package/dist/version.d.ts +1 -1
  185. package/package.json +26 -2
  186. package/src/analysis/choice-cost-gate.ts +359 -0
  187. package/src/analysis/choice-cost-render.ts +293 -0
  188. package/src/analysis/choice-cost.ts +1008 -0
  189. package/src/analysis/commitment.ts +557 -0
  190. package/src/analysis/corpus.ts +130 -0
  191. package/src/analysis/diagnose-render.ts +625 -0
  192. package/src/analysis/diagnose.ts +317 -0
  193. package/src/analysis/diagnostics.ts +33 -0
  194. package/src/analysis/duplication.ts +1955 -0
  195. package/src/analysis/fix-render.ts +217 -0
  196. package/src/analysis/fix.ts +669 -0
  197. package/src/analysis/format-number.ts +31 -0
  198. package/src/analysis/gating.ts +792 -0
  199. package/src/analysis/grammar.ts +72 -0
  200. package/src/analysis/rebuild.ts +287 -0
  201. package/src/analysis/terminal.ts +336 -0
  202. package/src/cli/index.ts +413 -0
  203. package/src/combinators/adjacency.ts +171 -0
  204. package/src/combinators/attempt.ts +41 -0
  205. package/src/combinators/case-fold.ts +90 -0
  206. package/src/combinators/choice.ts +466 -0
  207. package/src/combinators/completions.ts +64 -0
  208. package/src/combinators/dispatch.ts +358 -0
  209. package/src/combinators/expect.ts +159 -0
  210. package/src/combinators/first-set.ts +348 -0
  211. package/src/combinators/gate.ts +50 -0
  212. package/src/combinators/grammar.ts +272 -0
  213. package/src/combinators/keywords.ts +145 -0
  214. package/src/combinators/literal.ts +102 -0
  215. package/src/combinators/map.ts +106 -0
  216. package/src/combinators/node.ts +345 -0
  217. package/src/combinators/not.ts +55 -0
  218. package/src/combinators/parser.ts +266 -0
  219. package/src/combinators/peek.ts +63 -0
  220. package/src/combinators/probe.ts +25 -0
  221. package/src/combinators/ref.ts +109 -0
  222. package/src/combinators/regex.ts +170 -0
  223. package/src/combinators/repeat.ts +561 -0
  224. package/src/combinators/scanTo.ts +393 -0
  225. package/src/combinators/sequence.ts +237 -0
  226. package/src/combinators/token.ts +120 -0
  227. package/src/combinators/trivia-boundary.ts +48 -0
  228. package/src/combinators/trivia-skip.ts +852 -0
  229. package/src/combinators/withCtx.ts +45 -0
  230. package/src/compiler/build-arity.ts +330 -0
  231. package/src/compiler/compile-linkable-table.ts +209 -0
  232. package/src/compiler/degradation.ts +266 -0
  233. package/src/compiler/fields.ts +191 -0
  234. package/src/compiler/grammar-coverage-ids.ts +129 -0
  235. package/src/compiler/ir-serialize.ts +615 -0
  236. package/src/compiler/linker.ts +954 -0
  237. package/src/compiler/token-alphabet.ts +209 -0
  238. package/src/compiler/token-dispatch.ts +353 -0
  239. package/src/compiler/token-scanner.ts +318 -0
  240. package/src/compiler/value-usage.ts +147 -0
  241. package/src/coverage.ts +383 -0
  242. package/src/cst/capture-buffer.ts +297 -0
  243. package/src/cst/host-mode.ts +69 -0
  244. package/src/cst/reflection-attach.ts +11 -0
  245. package/src/cst/reflection-symbols.ts +9 -0
  246. package/src/cst/reflection.ts +149 -0
  247. package/src/cst/relative-spans.ts +207 -0
  248. package/src/cst/root-trivia-scope.ts +30 -0
  249. package/src/cst/trivia-charscan.ts +312 -0
  250. package/src/cst/trivia-entries.ts +285 -0
  251. package/src/cst/trivia-index.ts +131 -0
  252. package/src/cst/trivia-kinds.ts +358 -0
  253. package/src/cst/types.ts +76 -0
  254. package/src/cst/walk.ts +101 -0
  255. package/src/functional/doc.ts +959 -0
  256. package/src/functional/run-tabled.ts +153 -0
  257. package/src/functional/run.ts +558 -0
  258. package/src/grammar-metadata.ts +2 -0
  259. package/src/index.ts +189 -0
  260. package/src/language-service/index.ts +221 -0
  261. package/src/line-index.ts +131 -0
  262. package/src/oracle/digest.ts +620 -0
  263. package/src/oracle/index.ts +55 -0
  264. package/src/parse-context.ts +91 -0
  265. package/src/plugin/direct-builder-static.ts +102 -0
  266. package/src/plugin/evaluator.ts +1505 -0
  267. package/src/plugin/free-identifiers.ts +338 -0
  268. package/src/plugin/index.ts +2281 -0
  269. package/src/plugin/reducer-resolver.ts +606 -0
  270. package/src/recovery/scan.ts +155 -0
  271. package/src/regex/classes.ts +183 -0
  272. package/src/regex/first-set.ts +316 -0
  273. package/src/run/index.ts +31 -0
  274. package/src/spec/ebnf.ts +128 -0
  275. package/src/spec/index.ts +54 -0
  276. package/src/spec/model.ts +636 -0
  277. package/src/spec/railroad-lib.ts +559 -0
  278. package/src/spec/railroad.ts +256 -0
  279. package/src/table/assemble.ts +3006 -0
  280. package/src/table/cell.ts +42 -0
  281. package/src/table/child-slots.ts +113 -0
  282. package/src/table/compile-rule-map.ts +336 -0
  283. package/src/table/compile.ts +244 -0
  284. package/src/table/duplication-hook.ts +89 -0
  285. package/src/table/emit-assembly.ts +1777 -0
  286. package/src/table/emit.ts +429 -0
  287. package/src/table/encode-baseline.ts +340 -0
  288. package/src/table/encode.ts +1569 -0
  289. package/src/table/exec-baseline.ts +537 -0
  290. package/src/table/exec.ts +1680 -0
  291. package/src/table/fold.ts +38 -0
  292. package/src/table/index.ts +108 -0
  293. package/src/table/inspect.ts +52 -0
  294. package/src/table/ops.ts +467 -0
  295. package/src/table/program.ts +876 -0
  296. package/src/table/run-support.ts +66 -0
  297. package/src/table/scan-shapes.ts +1578 -0
  298. package/src/table/site-labels.ts +228 -0
  299. package/src/table/stamp.ts +229 -0
  300. package/src/types.ts +563 -0
  301. package/src/version.ts +16 -0
  302. package/dist/compiler/codegen.d.ts +0 -334
  303. package/dist/compiler/codegen.d.ts.map +0 -1
  304. package/dist/compiler/inline-build.d.ts +0 -11
  305. package/dist/compiler/inline-build.d.ts.map +0 -1
  306. package/dist/compiler/inline-callback.d.ts +0 -21
  307. package/dist/compiler/inline-callback.d.ts.map +0 -1
  308. package/dist/compiler/line-index.d.ts +0 -2
  309. package/dist/compiler/line-index.d.ts.map +0 -1
  310. package/dist/compiler/module-hoist.d.ts +0 -106
  311. package/dist/compiler/module-hoist.d.ts.map +0 -1
  312. package/dist/compiler/scannable-run.d.ts.map +0 -1
  313. package/dist/compiler/scannable-terminal.d.ts +0 -23
  314. package/dist/compiler/scannable-terminal.d.ts.map +0 -1
  315. package/dist/compiler/trivia-fast-path.d.ts +0 -38
  316. package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
@@ -0,0 +1,1777 @@
1
+ /**
2
+ * U4 — THE ASSEMBLY, AS EMITTED SOURCE.
3
+ *
4
+ * `assemble.ts` lowers each reachable site to a closure. This file lowers the
5
+ * same sites to TEXT, compiled once per (grammar × option set) at run start.
6
+ * Semantics are `assemble.ts`'s, site for site; the identity sweep
7
+ * (`bench/table-lowering-identity.ts`) gates the two against each other and
8
+ * against the interpreter, and `expected` is inside the digest it compares.
9
+ *
10
+ * ## Why text, and not a rearrangement of `assemble.ts`
11
+ *
12
+ * V8 attaches inline-cache feedback per FUNCTION LITERAL, not per closure. The
13
+ * second closure minted from a `CreateClosure` site moves its `FeedbackCell` to
14
+ * `kManyClosures`, and the vector is shared from then on. `assemble.ts` mints 18
15
+ * arity-2 `SEQV` closures for `less/stylesheet` from ONE literal, so the two
16
+ * child call sites inside that literal see 18 unrelated callees and go
17
+ * megamorphic — a hash lookup in the megamorphic stub cache, no inlining, no
18
+ * type feedback for the callee body. No rearrangement of a file that MINTS
19
+ * closures can produce a distinct literal per site. Emitting one
20
+ * `function _pf<N>` per site can: inside `_pf12` the call `_pf7(…)` names ONE
21
+ * binding and stays monomorphic.
22
+ *
23
+ * ## What may be shared, and what must be emitted
24
+ *
25
+ * A body containing a call to another PIECE must be emitted per site, or the
26
+ * megamorphism relocates from `assemble.ts` into a shared helper and nothing
27
+ * changes. That is why `nextTerm` is INLINED at every sequence term here rather
28
+ * than called.
29
+ *
30
+ * The rule this file applies is narrower than "no helpers", and it is the whole
31
+ * criterion: **a shared emitted-scope helper is sound exactly when it takes no
32
+ * piece as an argument**, because then it has no call site whose feedback a
33
+ * second caller could pollute. `_skipTrivia` qualifies — it calls the installed
34
+ * trivia scanner, which is runtime state in either engine. `nextTerm` does not.
35
+ *
36
+ * ## Cycles
37
+ *
38
+ * `assemble.ts` needs one forwarding stub per recursive site,
39
+ * `const fwd = (input, pos, ctx) => target!(input, pos, ctx)` — a SINGLE
40
+ * function literal that every back-edge in the process funnels through.
41
+ * Emitted `function` declarations hoist, so a back-edge here is a direct name
42
+ * reference and the stub does not exist.
43
+ *
44
+ * ## Refusal
45
+ *
46
+ * Any construct not lowered raises `Unemittable`, naming it. `assemble.ts`
47
+ * catches that, RECORDS it on the assembly, and falls back to the closure path.
48
+ * The fallback is observable (`Assembly.emitRefusal`): a silent one would make
49
+ * a permanently slow path indistinguishable from a fast one, which is exactly
50
+ * what `encode.ts:1208-1213` refuses to allow for `OP_LIVE`.
51
+ */
52
+ import type { ParseContext } from '../types.ts'
53
+ import {
54
+ OP_ADJ, OP_ATTEMPT, OP_CHOICE, OP_DISPATCH, OP_EMPTY, OP_EXPECT, OP_FIELD, OP_GATE,
55
+ OP_LABEL, OP_LEAF, OP_LIT, OP_LIT_CI, OP_LIT_CI_TRACK, OP_LIT_TRACK, OP_NAMES,
56
+ OP_NODE, OP_NODE_TRACK, OP_NOT, OP_OPT, OP_PEEK, OP_REP, OP_REPV, OP_ROUTED, OP_RULE, OP_RX,
57
+ OP_RX_TRACK, OP_SCAN, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ, OP_SEQV, OP_SEQX, OP_TOKEN, OP_XFORM,
58
+ } from './ops.ts'
59
+ import { validateDispatchSpec, type ResolvedClass, type ResolvedTable, type TableProgram } from './program.ts'
60
+ import { emitShapeMatch, scanShapeFromRegex } from './scan-shapes.ts'
61
+ import {
62
+ CAP_OFF, CAP_ON, TRI_NONE, TRI_UNKNOWN, TOP, computeSiteLabels, reachableSites, type SiteLabel,
63
+ } from './site-labels.ts'
64
+
65
+ /** What the compiled factory hands back — the emitted twin of `Assembly`. */
66
+ export type EmittedPiece = (input: string, pos: number, ctx: ParseContext) => unknown
67
+ export type EmittedAssembly = {
68
+ readonly pieces: Record<string, EmittedPiece>
69
+ /** Sites named for `subtreeComb` — the scan pool and the scan-skip sets. */
70
+ readonly byIp: Record<number, EmittedPiece | undefined>
71
+ readonly end: () => number
72
+ readonly begin: (ctx: ParseContext) => void
73
+ readonly finish: () => void
74
+ }
75
+ /** The `new Function` result. Its parameters are `EMITTED_PARAMS`, in order. */
76
+ export type EmittedFactory = (...args: readonly unknown[]) => EmittedAssembly
77
+
78
+ /** A construct the emitter does not lower. Names the CONSTRUCT, never a type. */
79
+ export class Unemittable extends Error {
80
+ readonly construct: string
81
+ constructor(construct: string) {
82
+ super(`table emitter: cannot emit ${construct}`)
83
+ this.name = 'Unemittable'
84
+ this.construct = construct
85
+ }
86
+ }
87
+
88
+ /**
89
+ * The names the emitted scope is closed over, in the order the factory takes
90
+ * them. ONE list, so the parameter list and the argument list cannot drift — a
91
+ * mismatch binds the wrong function to the wrong name and yields a parser that
92
+ * runs and is wrong, which no type in this file would catch.
93
+ */
94
+ export const EMITTED_PARAMS = [
95
+ 'EC', 'FAIL', 'K', 'FX', 'FNS', 'MASK', 'CLS', 'AFX', 'TRIVIA', 'TRIVIALABELS', 'TRIVIASCAN',
96
+ 'SCANS', 'DISP', 'DSP', 'EMPTY_FX', 'EMPTY_CH', 'EMPTY_TLOG', 'EMPTY_TL',
97
+ 'cstCaptureActive', 'pushCstLeaf', 'pushCstChild', 'rollbackTriviaAt', 'rollbackScannedTriviaAt', 'failAt',
98
+ 'classHas', 'consumeTrivia', 'buildFieldMap', 'projectChild', 'unwrapChild',
99
+ 'demoteCapturedToRaw', 'cstLeavesLen', 'skipTriviaScanned', 'needsDeferredTriviaCommit',
100
+ 'scanTrivia', 'advanceTrivia', 'refuseUnclassifiedRootScope', 'spanLines', 'rawEntry', 'lead',
101
+ 'asciiFoldKey', 'ROUTED_FX',
102
+ // RECOVERY, bound rather than reimplemented. `SENTS` is the sync sentinel per
103
+ // char-class index — the emitted twin of `assemble.ts`'s `sentinelFor` memo,
104
+ // and an interpreted `Combinator` indexed out of an array for the same reason
105
+ // `SCANS` is. The four functions are `recovery/scan.ts`'s own, so an error's
106
+ // span, expected set and CST embedding are produced by the SAME code in every
107
+ // engine and cannot drift.
108
+ 'SENTS', 'matchesAt', 'recoverScan', 'orSentinel', 'captureError',
109
+ ] as const
110
+
111
+ /**
112
+ * The state the emitted scope owns, and the helpers that touch it.
113
+ *
114
+ * `EC` is the assembly's end-position cell, INJECTED (see `cell.ts`) rather
115
+ * than a slot this scope owns: a mixed assembly runs emitted pieces beside
116
+ * closure pieces or an `exec.ts` driver, and all three must write one slot or a
117
+ * cross-engine call reports the end of whatever the other engine last did.
118
+ * `_pfScan` is the
119
+ * installed trivia scanner, which `OP_SCOPE` swaps mid-parse and so cannot be a
120
+ * constant in either engine.
121
+ *
122
+ * Every function here is shared by the rule in this file's header: none takes a
123
+ * piece as an argument.
124
+ */
125
+ const RUNTIME_PRELUDE = `
126
+ let _pfScan=null
127
+ let _pfHost
128
+ let _pfDepth=0
129
+ const _pfFrames=[]
130
+ function _skipTrivia(input,cur,ctx){
131
+ const s=_pfScan
132
+ if(s!==null&&ctx._triviaLog===undefined&&!(ctx.captureTrivia===true&&(ctx._cstBuf!==undefined||ctx._cstTriviaLog!==undefined)))return s(input,cur)
133
+ if(s!==null)return skipTriviaScanned(s,input,cur,ctx)
134
+ if(needsDeferredTriviaCommit(ctx)){const sc=scanTrivia(input,cur,ctx);sc.commit();return sc.end}
135
+ return advanceTrivia(input,cur,ctx)
136
+ }
137
+ function _pushLeaf(ctx,value,s,e){pushCstLeaf(ctx,{_tag:'leaf',value,span:{start:s,end:e}})}
138
+ function _pushLeafBuf(ctx,value,s,e){
139
+ const l={_tag:'leaf',value,span:{start:s,end:e}}
140
+ const b=ctx._cstBuf
141
+ if(b.ch!==undefined)b.ch.push(l)
142
+ else if(b.single!==undefined){b.ch=[b.single,l];b.single=undefined}
143
+ else b.single=l
144
+ if(b.raw!==undefined)b.raw.push(l)
145
+ else if(b.rawSingle!==undefined){b.raw=[b.rawSingle,l];b.rawSingle=undefined}
146
+ else b.rawSingle=l
147
+ }
148
+ function _accSet(ax,acc){
149
+ if(ax===undefined||ax.length===0)return acc
150
+ if(acc===undefined)return ax.slice()
151
+ for(const s of ax)acc.push(s)
152
+ return acc
153
+ }
154
+ function _trackLines(ctx,input,end){
155
+ const from=ctx._lineScannedTo??0
156
+ if(end<=from)return
157
+ const starts=ctx._lineStarts
158
+ if(starts===undefined)return
159
+ for(let i=from;i<end;i++)if(input.charCodeAt(i)===10)starts.push(i+1)
160
+ ctx._lineScannedTo=end
161
+ }
162
+ function _rollbackNeeded(ctx){
163
+ return ctx._cstBuf!==undefined||ctx._cstLeaves!==undefined||ctx._cstRawChildren!==undefined||ctx._cstTriviaLog!==undefined||ctx._fields!==undefined||ctx._errors!==undefined||ctx._triviaLog!==undefined||ctx._rootTriviaLog!==undefined
164
+ }
165
+ function _rbBuf(ctx,raw,tl,lv,lg,rt){
166
+ const b=ctx._cstBuf
167
+ const ra=b.raw
168
+ if(ra!==undefined){
169
+ if(raw===0)b.raw=undefined
170
+ else if(raw===1){b.rawSingle=ra[0];b.raw=undefined}
171
+ else if(ra.length!==raw)ra.length=raw
172
+ }else if(raw===0)b.rawSingle=undefined
173
+ const ch=b.ch
174
+ if(ch!==undefined){
175
+ if(lv===0)b.ch=undefined
176
+ else if(lv===1){b.single=ch[0];b.ch=undefined}
177
+ else if(ch.length!==lv)ch.length=lv
178
+ }else if(lv===0)b.single=undefined
179
+ const bt=b.tl
180
+ if(bt!==undefined){
181
+ if(tl===0)b.tl=undefined
182
+ else if(bt.length!==tl)bt.length=tl
183
+ }
184
+ if(ctx._triviaLog!==undefined&&ctx._triviaLog.length!==lg)ctx._triviaLog.length=lg
185
+ if(ctx._rootTriviaLog!==undefined&&ctx._rootTriviaLog.length!==rt)ctx._rootTriviaLog.length=rt
186
+ }
187
+ `
188
+
189
+ function q(s: string): string {
190
+ return JSON.stringify(s)
191
+ }
192
+
193
+ /**
194
+ * WHICH SIDE SINKS A MARK IN THIS ASSEMBLY HAS TO COVER.
195
+ *
196
+ * `_fields` and `_errors` used to be free: `emitRollback`'s own comment argued
197
+ * that neither could grow in an emitted assembly, because `OP_FIELD` and
198
+ * `OP_EXPECT` were both unemittable and a table carrying one fell back whole.
199
+ * Emitting them retires that argument, so the marks come back — but ONLY for a
200
+ * table that actually contains the writer, which is an emit-time fact about the
201
+ * program and not a per-parse test.
202
+ *
203
+ * Per TABLE and not per SITE, deliberately. A site's subtree reaches through
204
+ * `OP_RULE` into the rule graph, so "can a field run under this mark" is very
205
+ * nearly "does the grammar have a field" for every mark that is not a leaf's —
206
+ * and a per-site answer would buy a handful of sites at the cost of a second
207
+ * fixpoint walk over the code array.
208
+ */
209
+ type Sinks = { readonly fd: boolean; readonly er: boolean }
210
+ const NO_SINKS: Sinks = { fd: false, er: false }
211
+
212
+ /**
213
+ * The rollback-mark prologue as SSA locals, rather than the eight shared slots
214
+ * `assemble.ts` writes and reads back.
215
+ *
216
+ * `markCst` exists in the closure engine because a `{ raw, tl, lv }` return
217
+ * would allocate per mark and marks are the most-executed thing in the driver.
218
+ * Emitted, the question does not arise: a mark is seven locals in a frame the
219
+ * body already has. Same scalarisation `rollbackCstCaptureAt` documents, one
220
+ * step further because the source form can take it.
221
+ */
222
+ function sinkSlots(t: string, s: Sinks): string {
223
+ return `${s.fd ? `,${t}fd=0` : ''}${s.er ? `,${t}er=0` : ''}`
224
+ }
225
+ function sinkReads(t: string, s: Sinks): string {
226
+ return `${s.fd ? `\n${t}fd=ctx._fields!==undefined?ctx._fields.length:0` : ''}${s.er ? `\n${t}er=ctx._errors!==undefined?ctx._errors.length:0` : ''}`
227
+ }
228
+
229
+ function emitMark(t: string, buf: boolean, s: Sinks = NO_SINKS, decl = true): string {
230
+ // THE SITE IS INSIDE A NODE. `OP_NODE` opens `ctx._cstBuf` unconditionally and
231
+ // closes it on the way out, so the whole discriminating chain below — which
232
+ // sink is live, and whether a mark is needed at all — has ONE answer here, and
233
+ // the pass knows it. What is left is the five loads a mark actually is.
234
+ //
235
+ // `decl === false` RE-TAKES a mark into locals that already exist —
236
+ // `OP_DISPATCH` marks, rolls back to the selector's mark, and marks again.
237
+ if (buf) {
238
+ return `${decl ? `let ${t}raw=0,${t}tl=0,${t}lv=0,${t}lg=0,${t}rt=0${sinkSlots(t, s)}\n` : ''}{const b=ctx._cstBuf
239
+ const r=b.raw;${t}raw=r!==undefined?r.length:b.rawSingle!==undefined?1:0
240
+ const h=b.ch;${t}lv=h!==undefined?h.length:b.single!==undefined?1:0
241
+ const l=b.tl;${t}tl=l!==undefined?l.length:0
242
+ ${t}lg=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
243
+ ${t}rt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0${sinkReads(t, s)}}`
244
+ }
245
+ return `${decl ? `let ${t}n=false,${t}raw=0,${t}tl=0,${t}lv=0,${t}lg=0,${t}rt=0${sinkSlots(t, s)}\n` : `${t}n=false\n`}{const b=ctx._cstBuf
246
+ if(b!==undefined){
247
+ const r=b.raw;${t}raw=r!==undefined?r.length:b.rawSingle!==undefined?1:0
248
+ const h=b.ch;${t}lv=h!==undefined?h.length:b.single!==undefined?1:0
249
+ const l=b.tl;${t}tl=l!==undefined?l.length:0
250
+ ${t}n=true
251
+ }else if(ctx._cstLeaves!==undefined||ctx._cstRawChildren!==undefined||ctx._cstTriviaLog!==undefined||ctx._fields!==undefined||ctx._errors!==undefined||ctx._triviaLog!==undefined||ctx._rootTriviaLog!==undefined){
252
+ ${t}raw=ctx._cstRawChildren!==undefined?ctx._cstRawChildren.length:0
253
+ ${t}tl=ctx._cstTriviaLog!==undefined?ctx._cstTriviaLog.length:0
254
+ ${t}lv=ctx._cstLeaves!==undefined?ctx._cstLeaves.length:0
255
+ ${t}n=true
256
+ }
257
+ if(${t}n){
258
+ ${t}lg=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
259
+ ${t}rt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0${sinkReads(t, s)}
260
+ }}`
261
+ }
262
+
263
+ /**
264
+ * The matching rollback for a mark emitted with prefix `t`.
265
+ *
266
+ * THE `_fields` AND `_errors` MARKS ARE NOT TAKEN, because in an EMITTED
267
+ * assembly neither sink can grow. `OP_FIELD` is the only writer of `_fields`
268
+ * and `OP_EXPECT`/`recoverScan` the only writers of `_errors`, and none of the
269
+ * three is lowered by this file — a table containing one raises `Unemittable`
270
+ * and the WHOLE assembly falls back to `assemble.ts`'s closures, so no emitted
271
+ * body ever runs beside one. That is what makes this an emit-time fact rather
272
+ * than a per-parse guess, and it is why `assemble.ts`'s `nextTerm` still takes
273
+ * all seven: it is the engine those constructs actually run in.
274
+ *
275
+ * The two sinks are passed differently because `rollbackCstCaptureAt` guards
276
+ * them differently. `_errors` is guarded on `errors !== undefined`, so
277
+ * `undefined` is its established "no mark taken" sentinel and is correct even
278
+ * for a caller-supplied context that arrived with errors already on it.
279
+ * `_fields` has no such guard — `length = undefined` would throw — so it takes
280
+ * the literal `0` that a per-node `_fields` no `OP_FIELD` can push to always has.
281
+ */
282
+ function emitRollback(t: string, buf: boolean, s: Sinks = NO_SINKS): string {
283
+ // `_rbBuf` is the `_cstBuf` arm of `rollbackCstCaptureAt` plus the two trivia
284
+ // truncations, with the sink discrimination the label already answered removed.
285
+ // It takes no piece, so the header's sharing rule admits it.
286
+ //
287
+ // The two side sinks stay OUT of `_rbBuf` rather than growing its parameter
288
+ // list: they are absent from most tables, and a fixed seven-argument helper
289
+ // would make every grammar pay the two extra pushes to serve the ones that
290
+ // carry a field. Guarded exactly as `rollbackCstCaptureAt:242,233` guards them
291
+ // — `_errors` on a defined mark, because `undefined` is its "no mark taken"
292
+ // sentinel, and `_fields` on the array alone.
293
+ const fd = s.fd ? `\nif(ctx._fields!==undefined&&ctx._fields.length!==${t}fd)ctx._fields.length=${t}fd` : ''
294
+ const er = s.er ? `\nif(ctx._errors!==undefined&&ctx._errors.length!==${t}er)ctx._errors.length=${t}er` : ''
295
+ if (buf) return `_rbBuf(ctx,${t}raw,${t}tl,${t}lv,${t}lg,${t}rt)${fd}${er}`
296
+ if (!s.fd && !s.er) return `if(${t}n)rollbackTriviaAt(ctx,${t}raw,${t}tl,${t}lv,0,undefined,${t}lg,${t}rt)`
297
+ return `if(${t}n){rollbackTriviaAt(ctx,${t}raw,${t}tl,${t}lv,${s.fd ? `${t}fd` : '0'},${s.er ? `${t}er` : 'undefined'},${t}lg,${t}rt)}`
298
+ }
299
+
300
+ /**
301
+ * ONE SEQUENCE TERM — `assemble.ts`'s `nextTerm`, inlined at the site.
302
+ *
303
+ * Reads and writes `cur`; the term's value lands in `dst`; failure returns
304
+ * `FAIL` directly, because emitted here there is no shared return channel to
305
+ * encode a −1 sentinel into.
306
+ *
307
+ * The `ctx.trivia === undefined` test is GONE wherever the site label answers
308
+ * it. It was never an option — `OP_SCOPE` swaps `ctx.trivia` mid-parse, which is
309
+ * why it could not be lifted into `RunCfg` — but it IS a property of where the
310
+ * term sits, and `site-labels.ts` resolves it at encode time. `TRI_NONE` keeps
311
+ * only the first branch, a known slot keeps only the second (with the scope's own
312
+ * scanner already bound into `skip`), and `TRI_UNKNOWN` keeps both.
313
+ */
314
+ function emitTerm(callee: string, dst: string, t: string, l: SiteLabel, skip: string): string {
315
+ const fast = `const ${t}v=${callee}(input,cur,ctx)
316
+ if(${t}v===FAIL)return FAIL
317
+ ${dst}=${t}v
318
+ cur=EC.e`
319
+ if (l.tri === TRI_NONE) return fast
320
+ const scanned = `const ${t}tl=ctx._cstBuf!==undefined?(ctx._cstBuf.tl!==undefined?ctx._cstBuf.tl.length:0):(ctx._cstTriviaLog!==undefined?ctx._cstTriviaLog.length:0)
321
+ const ${t}lg=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
322
+ const ${t}rt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0
323
+ const ${t}s=${skip}(input,cur,ctx)
324
+ const ${t}stl=ctx._cstBuf!==undefined?(ctx._cstBuf.tl!==undefined?ctx._cstBuf.tl.length:0):(ctx._cstTriviaLog!==undefined?ctx._cstTriviaLog.length:0)
325
+ const ${t}slg=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
326
+ const ${t}srt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0
327
+ const ${t}v=${callee}(input,${t}s,ctx)
328
+ if(${t}v===FAIL)return FAIL
329
+ ${dst}=${t}v
330
+ if(EC.e>${t}s)cur=EC.e
331
+ else{rollbackScannedTriviaAt(ctx,${t}tl,${t}stl,${t}lg,${t}slg,${t}rt,${t}srt)}`
332
+ if (l.tri !== TRI_UNKNOWN) return scanned
333
+ return `if(ctx.trivia===undefined){
334
+ ${fast}
335
+ }else{
336
+ ${scanned}
337
+ }`
338
+ }
339
+
340
+ /**
341
+ * The repetition's LEAD-TRIVIA skip, as a BLOCK.
342
+ *
343
+ * A block and not a bare statement because the separator form spells it
344
+ * `} else <this>`, and a label that erases the skip entirely would otherwise
345
+ * leave a dangling `else` to swallow the statement after it.
346
+ */
347
+ function leadSkip(hasTrivia: string, leadTrivia: string, skip: string): string {
348
+ if (hasTrivia === 'false') return '{}'
349
+ const call = `itemStart=${skip}(input,itemStart,ctx)`
350
+ if (hasTrivia === 'true') return leadTrivia === 'true' ? `{${call}}` : `{if(${leadTrivia})${call}}`
351
+ return `{if(hasTrivia&&${leadTrivia})${call}}`
352
+ }
353
+
354
+ /**
355
+ * THE THREE POOLS, SAID AS INDICES INTO THE PROGRAM.
356
+ *
357
+ * The pools themselves are `Uint32Array`s, `{ascii, hi}` class objects and
358
+ * string arrays — printable, but at 129 words per mask and 128 bytes per class
359
+ * they would dwarf the table they belong to. Every entry is already IN the
360
+ * program: a class is `cc[i]`, an arm's expected set is `fx[i]`, and a mask is a
361
+ * pure function of its class row. So the plan is three arrays of small integers,
362
+ * and `rebuildPools` turns it back into the pools with allocation only — no
363
+ * string building, and in particular no `Function` constructor.
364
+ *
365
+ * This is what lets the macro pre-compile an assembly: the FACTORY is printed as
366
+ * a real function literal, and its data arguments are rebuilt from this.
367
+ */
368
+ export type PoolPlan = {
369
+ /** Per `CLS` row: the `cc` index of each entry, `-1` for a null (ungated) arm. */
370
+ readonly classes: readonly (readonly number[])[]
371
+ /** Per `AFX` row: the `fx` index of each arm's expected set. */
372
+ readonly armExpected: readonly (readonly number[])[]
373
+ /** Per `MASK` row: the `CLS` index the mask is computed from. */
374
+ readonly masks: readonly number[]
375
+ }
376
+
377
+ /** Everything the compiled factory needs bound, beside the emitted text. */
378
+ export type EmitResult = {
379
+ readonly source: string
380
+ /** Site offsets the emitter reached — the emitted twin of `Assembly.reached`. */
381
+ readonly reached: ReadonlySet<number>
382
+ /** Hoisted per-choice candidate masks, in `MASK` order. */
383
+ readonly masks: readonly Uint32Array[]
384
+ /** Hoisted per-arm class gates, in `CLS` order. */
385
+ readonly classes: readonly (ResolvedClass | null)[][]
386
+ /** Hoisted per-arm expected sets, in `AFX` order. */
387
+ readonly armExpected: readonly (readonly string[])[][]
388
+ /** The same three pools as indices, for a build-time emitter. */
389
+ readonly plan: PoolPlan
390
+ }
391
+
392
+ /** The candidate mask for one `CLS` row — the ONE definition, shared with the emitter. */
393
+ function maskForClassRow(gates: readonly (ResolvedClass | null)[]): Uint32Array {
394
+ const m = new Uint32Array(129)
395
+ for (let i = 0; i < gates.length; i++) {
396
+ const cls = gates[i]!
397
+ const bit = 1 << i
398
+ if (cls === null) { for (let c = 0; c < 129; c++) m[c]! |= bit; continue }
399
+ for (let c = 0; c < 128; c++) if (cls.ascii[c] === 1) m[c]! |= bit
400
+ }
401
+ return m
402
+ }
403
+
404
+ /**
405
+ * Rebuild the three pools a pre-compiled factory takes, from the resolved table
406
+ * and the plan the emitter printed. Allocation only.
407
+ */
408
+ export function rebuildPools(
409
+ cc: readonly ResolvedClass[],
410
+ fx: readonly (readonly string[])[],
411
+ plan: PoolPlan,
412
+ ): { masks: Uint32Array[]; classes: (ResolvedClass | null)[][]; armExpected: (readonly string[])[][] } {
413
+ const classes = plan.classes.map(row => row.map(i => (i < 0 ? null : cc[i] ?? null)))
414
+ return {
415
+ classes,
416
+ armExpected: plan.armExpected.map(row => row.map(i => fx[i] ?? [])),
417
+ masks: plan.masks.map(ci => maskForClassRow(classes[ci]!)),
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Emit the whole assembly for one resolved table and one option set.
423
+ *
424
+ * Throws `Unemittable` for any construct not lowered. It does NOT compile the
425
+ * text — `assemble.ts` does — so a refusal and a compile failure stay two
426
+ * distinguishable outcomes at the call site.
427
+ */
428
+ export function emitAssemblySource(
429
+ t: ResolvedTable,
430
+ prog: TableProgram,
431
+ cfg: {
432
+ hostCst: boolean
433
+ hostReadsChildren?: boolean
434
+ hostCaptureTrivia?: ((type: string) => boolean) | undefined
435
+ trackLines: boolean
436
+ tolerant: boolean
437
+ coverage: boolean
438
+ probe: boolean
439
+ },
440
+ extraIps: readonly number[] = [],
441
+ ): EmitResult {
442
+ const { code, k, disp, dsp, triviaLabelled } = t
443
+ const swapLegal = !cfg.trackLines
444
+ const hostCst = cfg.hostCst
445
+
446
+ /**
447
+ * RECOVERY IS EMITTED, and the refusal this replaces was wrong in SCOPE.
448
+ *
449
+ * The refusal read: *"`recoverScan`'s protocol has three implementations
450
+ * already and a fourth is how an error span drifts... It is also cold by
451
+ * construction — reached only on the failure of an element — so the speed
452
+ * argument for emitting it is absent."*
453
+ *
454
+ * The drift half is answered by construction, not by argument: NOTHING of the
455
+ * protocol is reimplemented here. `recoverScan`, `matchesAt`, `orSentinel` and
456
+ * `captureError` are BOUND and CALLED, exactly as the closure engine calls
457
+ * them, so there is no fourth implementation to drift — the same reason
458
+ * `OP_SCAN` may bind an interpreted combinator and index it (`SCANS[i]`).
459
+ *
460
+ * The cold half was true of recovery and FALSE of the refusal. This throw sat
461
+ * at the top of the emitter, so it did not decline the cold recovery rows — it
462
+ * declined THE ENTIRE ASSEMBLY. Every tolerant parse in the product therefore
463
+ * ran the array-indexed closure walk for its whole hot path: every literal,
464
+ * every choice, every repetition, none of which is recovery and all of which is
465
+ * as hot as the strict path that is emitted. The speed argument is absent for
466
+ * `recoverScan` and was never absent for the table around it.
467
+ *
468
+ * What stays interpreted is what was already interpreted for the strict path.
469
+ */
470
+ const REC = prog.rec === 1 && cfg.tolerant
471
+ if (cfg.coverage) throw new Unemittable('a coverage assembly')
472
+
473
+ // THE DOWNWARD PASS, BEFORE ANY LOWERING. The roots are exactly the sites
474
+ // `link` is called on from outside a body — the rule entries and the scan
475
+ // pool's `extraIps` — and each starts at `TOP`, because a caller outside the
476
+ // emitted scope supplies a context this pass cannot see.
477
+ const roots = [...Object.values(prog.rules), ...extraIps]
478
+ const labels = computeSiteLabels(code, roots, hostCst)
479
+
480
+ // THE SIDE-SINK CENSUS, over the same reachable set the labels are computed
481
+ // from. `OP_FIELD` is the only ROW that writes `_fields` and `OP_EXPECT` the
482
+ // only emittable row that writes `_errors` — `recoverScan` is the other, and a
483
+ // recovery assembly is refused above. A table with neither pays for neither.
484
+ //
485
+ // `OP_SCAN` RAISES BOTH, and that is not caution — it is a measured defect.
486
+ // The row runs an INTERPRETED combinator (`scanTo`/`balanced`, rebuilt by
487
+ // `resolveTable`'s pool from subtrees), so its interior is not in this code
488
+ // array and this walk cannot see what it writes. `balanced()`'s unclosed-group
489
+ // failure pushes a `parseError` carrying its closer, and less's `at-rules.less`
490
+ // and `css-3.less` each left several of them on `RunResult.errors` — errors
491
+ // that the CLOSURE engine rolls back, because its mark covers `_errors`
492
+ // unconditionally. The two engines disagreed on the errors facet of a parse
493
+ // that otherwise matched byte for byte.
494
+ //
495
+ // Any future row whose child is not an offset in `code` belongs here for the
496
+ // same reason. `OP_LIVE` is the other such row, and it is unemittable.
497
+ const sinks: Sinks = (() => {
498
+ let fd = false
499
+ // A RECOVERY ASSEMBLY IS AN `_errors` WRITER EVERYWHERE. `recoverScan` pushes
500
+ // through `captureError` from inside any `OP_REP` that recovers, and the
501
+ // recovering row is the repetition itself rather than a distinguishable
502
+ // opcode — so there is no site to census and the answer is the assembly's.
503
+ //
504
+ // This is the `OP_SCAN` lesson applied before it costs anything rather than
505
+ // after: that row raised both sinks because its interior is not in `code`,
506
+ // and the defect it was raised for was precisely an `_errors` rollback the
507
+ // two engines disagreed on while matching byte for byte on the tree.
508
+ let er = REC
509
+ for (const ip of reachableSites(code, roots)) {
510
+ const op = code[ip]
511
+ if (op === OP_SCAN) { fd = true; er = true; break }
512
+ if (op === OP_FIELD) fd = true
513
+ else if (op === OP_EXPECT) er = true
514
+ }
515
+ return { fd, er }
516
+ })()
517
+
518
+ const bodies: string[] = []
519
+ const byIp = new Map<number, string>()
520
+ const reached = new Set<number>()
521
+ const prelude: string[] = []
522
+ const skipDefs: string[] = []
523
+ const skipPool = new Map<string, string>()
524
+ const pool = new Map<string, string>()
525
+ const masks: Uint32Array[] = []
526
+ const classes: (ResolvedClass | null)[][] = []
527
+ const armExpected: (readonly string[])[][] = []
528
+ /**
529
+ * THE POOLS, SAID AS INDICES — see `PoolPlan`. Written in lockstep with the
530
+ * three arrays above so a build-time emitter can print the plan instead of the
531
+ * pools, and `rebuildPools` can reproduce them from the resolved table alone.
532
+ * Kept adjacent to each `push` for exactly the reason `EMITTED_PARAMS` is one
533
+ * list: two orders that must agree cannot be allowed to live apart.
534
+ */
535
+ const classPlan: number[][] = []
536
+ const armExpectedPlan: number[][] = []
537
+ const maskPlan: number[] = []
538
+
539
+ /** `codegen.ts`'s hoisted pools, same spelling: `_k<N>` / `_fx<N>` / `_fn<N>`. */
540
+ function hoist(kind: string, expr: string): string {
541
+ const hit = pool.get(expr)
542
+ if (hit !== undefined) return hit
543
+ const nm = `_${kind}${pool.size}`
544
+ pool.set(expr, nm)
545
+ prelude.push(`const ${nm}=${expr}`)
546
+ return nm
547
+ }
548
+ const kRef = (i: number): string => hoist('k', `K[${i}]`)
549
+ const fxRef = (i: number): string => hoist('fx', `FX[${i}]`)
550
+ const fnRef = (i: number): string => hoist('fn', `FNS[${i}]`)
551
+ /**
552
+ * The sync sentinel for a char-class index, hoisted once per class.
553
+ *
554
+ * −1 is "no usable sentinel" — the same answer `firstSetSentinel` gives for an
555
+ * `any`/`empty` first set — and it lowers to the literal `undefined` so the
556
+ * publish below collapses to the inherited sentinel with no array read.
557
+ */
558
+ const sentRef = (cls: number): string => cls < 0 ? 'undefined' : hoist('sent', `SENTS[${cls}]`)
559
+
560
+ /** A fresh local prefix, so two inlined marks in one body cannot collide. */
561
+ let uid = 0
562
+ const tmp = (): string => `_t${uid++}_`
563
+
564
+ /**
565
+ * THE TRIVIA SCAN FOR ONE SITE LABEL.
566
+ *
567
+ * `_skipTrivia` answers four questions on every call — is a scanner installed,
568
+ * is the global trivia log live, is capture on, is a capture sink open — and
569
+ * three of them are what the label already knows. This mints the arm that
570
+ * survives, once per distinct label, and every site with that label calls it by
571
+ * name.
572
+ *
573
+ * Shared, and the header's rule is why it may be: none of these takes a PIECE.
574
+ * The installed scanner arrives as a hoisted `TRIVIASCAN[ki]` const, not as an
575
+ * argument, so a second caller cannot pollute a call site's feedback. That is
576
+ * the same test `_skipTrivia` passed and `nextTerm` failed.
577
+ *
578
+ * `_triviaLog` is deliberately NOT resolved here. It is neither an option nor a
579
+ * site property — nothing in `src` writes it except a caller-supplied
580
+ * `ParseContext` (`types.ts:524`) — so it stays the one runtime read.
581
+ */
582
+ function skipFor(l: SiteLabel): string {
583
+ if (l.tri < 0) return '_skipTrivia'
584
+ const ki = l.tri
585
+ // `TRIVIASCAN[ki]` IS NULLABLE. `program.ts:475` maps every trivia through
586
+ // `fastTriviaScanner`, which declines any shape it cannot lower, so the slot
587
+ // holds `null` for those — which is why `OP_SCOPE` installs a scanner it
588
+ // still has to null-test. The presence of a lowering is table data like the
589
+ // label bit beside it, so it belongs in the key rather than in a branch.
590
+ const hasScan = swapLegal && !triviaLabelled[ki]! && t.triviaScan[ki] != null
591
+ const key = `${ki}|${hasScan ? 1 : 0}|${l.buf ? 1 : 0}|${l.cap}`
592
+ const hit = skipPool.get(key)
593
+ if (hit !== undefined) return hit
594
+ const nm = `_sk${skipPool.size}`
595
+ skipPool.set(key, nm)
596
+ if (!hasScan) {
597
+ // No installed scanner: the labelled/`trackLines` path, which records
598
+ // through `scanTrivia`. `needsDeferredTriviaCommit` is implied by an open
599
+ // buffer, so an in-node site skips the call that asks.
600
+ skipDefs.push(l.buf
601
+ ? `function ${nm}(input,cur,ctx){const s=scanTrivia(input,cur,ctx);s.commit();return s.end}`
602
+ : `function ${nm}(input,cur,ctx){
603
+ if(needsDeferredTriviaCommit(ctx)){const s=scanTrivia(input,cur,ctx);s.commit();return s.end}
604
+ return advanceTrivia(input,cur,ctx)}`)
605
+ return nm
606
+ }
607
+ const sc = hoist('ts', `TRIVIASCAN[${ki}]`)
608
+ // `ctx.captureTrivia === true && (a sink is open)`, with both conjuncts
609
+ // resolved as far as the label takes them.
610
+ const sink = l.buf ? 'true' : '(ctx._cstBuf!==undefined||ctx._cstTriviaLog!==undefined)'
611
+ const capturing = l.cap === CAP_OFF ? 'false' : l.cap === CAP_ON ? sink : `(ctx.captureTrivia===true&&${sink})`
612
+ if (capturing === 'true') {
613
+ // Capture is on and a sink is open: the bare-scanner arm is unreachable.
614
+ skipDefs.push(`function ${nm}(input,cur,ctx){return skipTriviaScanned(${sc},input,cur,ctx)}`)
615
+ return nm
616
+ }
617
+ const guard = capturing === 'false'
618
+ ? 'ctx._triviaLog===undefined'
619
+ : `ctx._triviaLog===undefined&&!${capturing}`
620
+ skipDefs.push(`function ${nm}(input,cur,ctx){
621
+ if(${guard})return ${sc}(input,cur)
622
+ return skipTriviaScanned(${sc},input,cur,ctx)}`)
623
+ return nm
624
+ }
625
+
626
+ /**
627
+ * The BODY OF A LOWERED REGEX ROW, or `undefined` when the shape declines.
628
+ *
629
+ * Declining is the safe direction and the only one this can take: a shape it
630
+ * fails to recognise falls through to the regex, which is what the row did
631
+ * before. A shape it recognises WRONGLY is a defect, and the gate for that is
632
+ * `bench/scan-shape-oracle.ts` — every regex constant of every workload
633
+ * grammar driven at every position of its own corpus against the sticky
634
+ * `exec` it replaces, end for end.
635
+ *
636
+ * The value is `input.slice(pos, end)`, which for a STICKY regex is `m[0]`
637
+ * character for character. The `u`/`i`/`m`/`s` flags are handled inside
638
+ * `scanShapeFromRegex`; `u` declines outright, because a code-unit scan is not
639
+ * a code-point one.
640
+ *
641
+ * `captureLeaf` is the SITE'S — it arrives from `lower`, already resolved
642
+ * against that site's label, so a lowered row pushes its leaf through exactly
643
+ * the form `OP_LIT` and `OP_LIT_CI` push theirs. Re-deriving the capture test
644
+ * here would put a second answer to a settled question inside the one body
645
+ * that runs most.
646
+ */
647
+ function emitScan(
648
+ ki: number,
649
+ xf: string,
650
+ track: boolean,
651
+ captureLeaf: (value: string) => string,
652
+ ): string | undefined {
653
+ const re = k[ki]
654
+ if (!(re instanceof RegExp)) return undefined
655
+ const shape = scanShapeFromRegex(re.source, re.flags)
656
+ if (shape === null) return undefined
657
+ let n = 0
658
+ const p = tmp()
659
+ const m = emitShapeMatch(shape, 'pos', (prefix = '_v') => `${p}${prefix}${n++}`, '')
660
+ return `
661
+ ${m.setup.join('\n')}
662
+ if(${m.ok}){
663
+ const e=${m.end}
664
+ const v=input.slice(pos,e)
665
+ ${captureLeaf('v')}
666
+ ${track ? '_trackLines(ctx,input,e)\n' : ''}EC.e=e
667
+ return v
668
+ }
669
+ ctx._fe=pos;ctx._fx=${xf}
670
+ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
671
+ `
672
+ }
673
+
674
+ function link(ip: number): string {
675
+ const hit = byIp.get(ip)
676
+ if (hit !== undefined) return hit
677
+ // An ALIAS site forwards to its child with no body of its own. Resolved
678
+ // before reservation so no name is minted for a function that would only
679
+ // add a call frame — `assemble.ts` makes the same choice by returning the
680
+ // child piece itself.
681
+ const target = resolveAlias(ip)
682
+ const done = byIp.get(target)
683
+ if (done !== undefined) return done
684
+ const fname = `_pf${target}`
685
+ // Reserved BEFORE lowering, so a back-edge into a site still in flight binds
686
+ // to the hoisted declaration rather than to a forwarding stub. This is the
687
+ // whole of `assemble.ts`'s `inFlight` map and its one shared closure.
688
+ byIp.set(target, fname)
689
+ bodies.push(lower(target, fname))
690
+ return fname
691
+ }
692
+
693
+ /**
694
+ * FOLLOW THE ALIAS CHAIN TO THE FIRST SITE WITH A BODY — iteratively, and
695
+ * BEFORE anything is reserved.
696
+ *
697
+ * The previous form recursed through `link` and parked a PROVISIONAL name
698
+ * `_pf<ip>` in the alias map so that a cycle would terminate. It terminated,
699
+ * and it emitted a call to a function that does not exist: `OP_RULE` is the
700
+ * only back-edge in a table, `OP_RULE` is an alias, so a recursive rule whose
701
+ * cycle re-enters through the alias row still in flight bound to the
702
+ * provisional name, and every grammar with that shape compiled to
703
+ * `_pf1100 is not defined` — a ReferenceError at parse time, from a body that
704
+ * the `new Function` call above accepts as syntactically valid.
705
+ *
706
+ * Resolving the chain first removes the window: no name is ever handed out for
707
+ * a site that will not get a body, because the only name handed out is the
708
+ * CHAIN'S END, which is always lowered.
709
+ *
710
+ * A cycle of alias-only rows — `A` aliasing to `B` aliasing back to `A`, with
711
+ * no body anywhere in between — is a rule that expands to itself and consumes
712
+ * nothing. Refused BY NAME rather than looped on: it is a defect in the
713
+ * grammar, and the closure engine meets it as a stack overflow at parse time.
714
+ */
715
+ function resolveAlias(ip: number): number {
716
+ let cur = ip
717
+ let seen: Set<number> | undefined
718
+ for (;;) {
719
+ reached.add(cur)
720
+ const next = aliasOf(cur)
721
+ if (next === undefined) return cur
722
+ if (seen === undefined) seen = new Set([cur])
723
+ else if (seen.has(cur)) throw new Unemittable('a cycle of alias-only sites (a rule that expands to itself)')
724
+ else seen.add(cur)
725
+ cur = next
726
+ }
727
+ }
728
+
729
+ /** Sites that forward to a child with no body, decided by option, data, or LABEL. */
730
+ function aliasOf(ip: number): number | undefined {
731
+ const op = code[ip]
732
+ // `OP_GATE` under a probe or a tolerant recovery is a no-op that forwards to
733
+ // its child, exactly as `assemble.ts:952` resolves it.
734
+ if (op === OP_GATE && (cfg.tolerant || cfg.probe)) return code[ip + 2]!
735
+ if (op === OP_RULE) return code[ip + 1]!
736
+ // A SCOPE THAT INSTALLS WHAT IS ALREADY INSTALLED. `encode.ts:520` wraps
737
+ // EVERY rule of a `rules({ trivia }, …)` map in its own `OP_SCOPE`, so a
738
+ // grammar with one ambient trivia re-installs the same slot at every rule
739
+ // entry — six context stores, a scanner swap and their six restores, per
740
+ // call, to arrive at the values already there.
741
+ //
742
+ // The label is what makes this decidable: `tri >= 0` can ONLY have come from
743
+ // an enclosing `OP_SCOPE` carrying that slot, and that scope set
744
+ // `ctx.triviaKindLabels` and `_pfScan` from the same slot, so all three are
745
+ // already the values this row would write. Restricted to plain `OP_SCOPE`
746
+ // with no root-capture policy: `OP_SCOPE_CAP` also raises `captureTrivia`,
747
+ // and the two flag bits are a real refusal and a real save/restore.
748
+ //
749
+ // `ki >= 0` is required rather than implied: `TRI_NONE` and `TRI_UNKNOWN` are
750
+ // themselves negative, so comparing a negative operand against a lattice
751
+ // element would read "unknown" as a match.
752
+ if ((op === OP_SCOPE_PLAIN || (op === OP_SCOPE && code[ip + 3]! === 0)) && code[ip + 1]! >= 0
753
+ && labels.at(ip).tri === code[ip + 1]!) {
754
+ return code[ip + 2]!
755
+ }
756
+ return undefined
757
+ }
758
+
759
+ function lower(ip: number, fname: string): string {
760
+ const op = code[ip]
761
+ const head = `function ${fname}(input,pos,ctx){`
762
+ const L = labels.at(ip)
763
+ /**
764
+ * THE LEAF CAPTURE TEST, and only the test.
765
+ *
766
+ * `cstCaptureActive` is `_cstBuf !== undefined || _cstLeaves !== undefined`,
767
+ * and `OP_NODE` opens `_cstBuf` on entry regardless of host mode — so under a
768
+ * node the answer is a constant `true`. What that licenses is dropping the
769
+ * CALL, never the capture: the captured leaves feed `kids` → `build(...)`,
770
+ * and eliding them on `hostCst === false` would be a wrong tree, not a fast
771
+ * one. Off-label the test is INLINED rather than called, which is sound
772
+ * everywhere and costs nothing.
773
+ *
774
+ * THE PUSH ITSELF is two cross-module calls — `pushCstLeaf` to decide on
775
+ * `trackLines`, then `pushCstChild` to decide which collector is live — on
776
+ * the most-executed path there is. Both decisions are settled here:
777
+ * `trackLines` is `RunCfg`, and an open `_cstBuf` is the label. `_pushLeafBuf`
778
+ * is that pair with both branches taken, and it takes no piece.
779
+ */
780
+ const pushLeaf = L.buf && !cfg.trackLines ? '_pushLeafBuf' : '_pushLeaf'
781
+ const captureLeaf = (value: string): string => {
782
+ const call = `${pushLeaf}(ctx,${value},pos,e)`
783
+ return L.buf ? call : `if(ctx._cstBuf!==undefined||ctx._cstLeaves!==undefined)${call}`
784
+ }
785
+ switch (op) {
786
+ case OP_LIT:
787
+ case OP_LIT_TRACK: {
788
+ const s = k[code[ip + 1]!] as string
789
+ const xf = fxRef(code[ip + 2]!)
790
+ const track = op === OP_LIT_TRACK
791
+ // LENGTH IS TABLE DATA, so the compare is chosen here — the emitted twin
792
+ // of the closure engine's length-keyed literal bodies.
793
+ const test = s.length === 1
794
+ ? `input.charCodeAt(pos)===${s.charCodeAt(0)}`
795
+ : s.length === 2
796
+ ? `input.charCodeAt(pos)===${s.charCodeAt(0)}&&input.charCodeAt(pos+1)===${s.charCodeAt(1)}`
797
+ : `input.startsWith(${q(s)},pos)`
798
+ return `${head}
799
+ if(${test}){
800
+ const e=pos+${s.length}
801
+ ${captureLeaf(q(s))}
802
+ ${track ? '_trackLines(ctx,input,e)\n' : ''}EC.e=e
803
+ return ${q(s)}
804
+ }
805
+ ctx._fe=pos;ctx._fx=${xf}
806
+ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
807
+ }`
808
+ }
809
+
810
+ case OP_LIT_CI:
811
+ case OP_LIT_CI_TRACK: {
812
+ const s = k[code[ip + 1]!] as string
813
+ const xf = fxRef(code[ip + 2]!)
814
+ const track = op === OP_LIT_CI_TRACK
815
+ // THE FOLD, UNROLLED AGAINST CONSTANTS.
816
+ //
817
+ // `assemble.ts:866-873` holds the folded literal in an ARRAY and walks it
818
+ // with a loop and a ternary per character — a load, a bounds check and a
819
+ // branch for a comparison whose right-hand side is known here. Emitted, the
820
+ // literal is not data at all: each character is one or two compares
821
+ // against integers, straight-line, no array and no closure.
822
+ //
823
+ // Semantics are `asciiFoldEq`'s exactly. `foldedLit[i]` is the literal
824
+ // folded, so it is never in A-Z; and `fold` maps A-Z INTO a-z and is the
825
+ // identity everywhere else. So for a folded character `c`:
826
+ // - `c` in a-z — the input matches iff it is `c` or `c - 32`
827
+ // - otherwise — the input matches iff it is exactly `c`, because the
828
+ // only characters fold moves land in a-z
829
+ // Past end of input `charCodeAt` is NaN, which compares unequal to both —
830
+ // the same answer the length test gave.
831
+ //
832
+ // The match VALUE is `input.slice(pos, e)`, the INPUT's casing and not the
833
+ // literal's, exactly as `literal.ts:86` has it — returning the literal
834
+ // would silently normalise case in any node built from it.
835
+ const tests: string[] = []
836
+ const reads: string[] = []
837
+ for (let i = 0; i < s.length; i++) {
838
+ const raw = s.charCodeAt(i)
839
+ const f = raw >= 65 && raw <= 90 ? raw + 32 : raw
840
+ const at = i === 0 ? 'pos' : `pos+${i}`
841
+ if (f >= 97 && f <= 122) {
842
+ reads.push(`const c${i}=input.charCodeAt(${at})`)
843
+ tests.push(`(c${i}===${f}||c${i}===${f - 32})`)
844
+ } else {
845
+ tests.push(`input.charCodeAt(${at})===${f}`)
846
+ }
847
+ }
848
+ return `${head}
849
+ ${reads.length > 0 ? `${reads.join('\n')}\n` : ''}if(${tests.join('&&')}){
850
+ const e=pos+${s.length}
851
+ const v=input.slice(pos,e)
852
+ ${captureLeaf('v')}
853
+ ${track ? '_trackLines(ctx,input,e)\n' : ''}EC.e=e
854
+ return v
855
+ }
856
+ ctx._fe=pos;ctx._fx=${xf}
857
+ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
858
+ }`
859
+ }
860
+
861
+ case OP_RX:
862
+ case OP_RX_TRACK: {
863
+ const xf = fxRef(code[ip + 2]!)
864
+ const track = op === OP_RX_TRACK
865
+ // THE MATCH ARRAY IS THE COST, not the matching. `re.exec` allocates one
866
+ // per row — 6,005 rows per `json/document` parse, 12.9% of everything
867
+ // executed — and every field of it but `[0]` is discarded here. A shape
868
+ // that lowers replaces the call with a straight-line scan over
869
+ // `charCodeAt`, and the ranges are FOLDED INTO THE SOURCE by
870
+ // `classCond`/`litCond`/`foldEq` rather than read from a table at run
871
+ // time: emitted as a static body consulting `inRanges`, this would
872
+ // reproduce the per-character loop it exists to remove.
873
+ const scanned = emitScan(code[ip + 1]!, xf, track, captureLeaf)
874
+ if (scanned !== undefined) return `${head}${scanned}}`
875
+ // STICKINESS IS THE PRECONDITION OF BOTH FORMS BELOW, and it is checked
876
+ // rather than assumed. `encode.ts:588` appends `y` to every regex row it
877
+ // emits, so a non-sticky constant here would be a new encoder path — and
878
+ // `lastIndex=pos` would already have been meaningless for it, silently
879
+ // matching from wherever the last row left off.
880
+ const rxk = k[code[ip + 1]!]
881
+ if (!(rxk instanceof RegExp) || !rxk.sticky) throw new Unemittable('a non-sticky regex row')
882
+ // A ROW THAT DOES NOT LOWER STILL NEED NOT ALLOCATE. `test` and `exec`
883
+ // run the identical match; `exec` additionally materialises a
884
+ // JSRegExpResult, and V8's `test` fast path does not. For a STICKY
885
+ // regex `lastIndex` IS the match end on success (RegExpBuiltinExec
886
+ // step 15), so `input.slice(pos, lastIndex)` is `m[0]` character for
887
+ // character, and the array was the only thing dropped. Counted, not
888
+ // reasoned: 400×~2k sticky matches of json's unlowered string body take
889
+ // 99 scavenges through `exec` and 10 through `test`, reproduced.
890
+ const re = kRef(code[ip + 1]!)
891
+ return `${head}
892
+ ${re}.lastIndex=pos
893
+ if(${re}.test(input)){
894
+ const e=${re}.lastIndex
895
+ const v=input.slice(pos,e)
896
+ ${captureLeaf('v')}
897
+ ${track ? '_trackLines(ctx,input,e)\n' : ''}EC.e=e
898
+ return v
899
+ }
900
+ ctx._fe=pos;ctx._fx=${xf}
901
+ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
902
+ }`
903
+ }
904
+
905
+ case OP_EMPTY:
906
+ // `''`, not `null` — the zero-width match's value. The other three engines
907
+ // (exec.ts, exec-baseline.ts, assemble.ts) all return `''`; this one returned
908
+ // `null`, so the SAME program yielded a different `value` depending only on
909
+ // which engine ran. Unreached from the combinator API today — `OP_EMPTY` is
910
+ // emitted only as `finish()` padding for an EMPTY rule map, which then has no
911
+ // walk roots for the emitter to visit — but that is an accident of two
912
+ // unguarded facts, not an invariant, and it is already reachable through the
913
+ // hand-built-program idiom this file's driver tests use.
914
+ return `${head}EC.e=pos;return ''}`
915
+
916
+ case OP_GATE: {
917
+ const child = link(code[ip + 2]!)
918
+ const ci = classes.push([t.cc[code[ip + 1]!]!]) - 1
919
+ classPlan.push([code[ip + 1]!])
920
+ const cls = hoist('cl', `CLS[${ci}][0]`)
921
+ const xf = fxRef(code[ip + 3]!)
922
+ return `${head}
923
+ if(!classHas(${cls},lead(input,pos))){ctx._fe=pos;ctx._fx=${xf};return FAIL}
924
+ return ${child}(input,pos,ctx)
925
+ }`
926
+ }
927
+
928
+ case OP_XFORM: {
929
+ const fn = fnRef(code[ip + 1]!)
930
+ const child = link(code[ip + 2]!)
931
+ return `${head}
932
+ const v=${child}(input,pos,ctx)
933
+ if(v===FAIL)return FAIL
934
+ return ${fn}(v,{start:pos,end:EC.e})
935
+ }`
936
+ }
937
+
938
+ case OP_LABEL: {
939
+ const child = link(code[ip + 1]!)
940
+ const xf = fxRef(code[ip + 2]!)
941
+ // `_fe` IS NOT TOUCHED — `map.ts:84` keeps the child's span and replaces
942
+ // only the expected set. A label that moved the failure position would
943
+ // report the diagnostic at the label's start rather than where the input
944
+ // actually stopped.
945
+ return `${head}
946
+ const v=${child}(input,pos,ctx)
947
+ if(v===FAIL)ctx._fx=${xf}
948
+ return v
949
+ }`
950
+ }
951
+
952
+ case OP_FIELD: {
953
+ const name = q(k[code[ip + 1]!] as string)
954
+ const child = link(code[ip + 2]!)
955
+ // `ctx._fields?.push(…)` — conditional on a field-reading node being
956
+ // open, exactly as `map.ts` has it, so a field outside one costs a load
957
+ // and a branch. The ARRAY is what `OP_NODE` hands to `buildFieldMap`.
958
+ return `${head}
959
+ const v=${child}(input,pos,ctx)
960
+ if(v===FAIL)return FAIL
961
+ const f=ctx._fields
962
+ if(f!==undefined)f.push({name:${name},value:v,span:{start:pos,end:EC.e}})
963
+ return v
964
+ }`
965
+ }
966
+
967
+ case OP_EXPECT: {
968
+ const child = link(code[ip + 1]!)
969
+ const xf = fxRef(code[ip + 2]!)
970
+ // TWO ARMS, mirroring `assemble.ts:1050`. A TOLERANT `expect()` EMBEDS
971
+ // ITS ERROR IN THE TREE and not only in the flat `_errors` side-channel
972
+ // (`combinators/expect.ts:150`, codegen's `_ctx._rec.capture` at
973
+ // codegen.ts:4470) — so a tree walk finds every diagnostic and the node
974
+ // survives incremental subtree reuse. `captureError` is the shared
975
+ // function, called; the embedding is not restated here.
976
+ //
977
+ // `_fc` IS CLEARED IN BOTH: a recovered failure is no longer a failure,
978
+ // and the commit bit the child raised must not survive it and cut an
979
+ // enclosing choice (`assemble.ts:1058-1066`).
980
+ return `${head}
981
+ const v=${child}(input,pos,ctx)
982
+ if(v!==FAIL)return v
983
+ const err={_tag:'parseError',span:${prog.lines === 1 ? 'spanLines(ctx,pos,pos)' : '{start:pos,end:pos}'},expected:${xf}}
984
+ const es=ctx._errors
985
+ if(es!==undefined)es.push(err)
986
+ ${REC ? 'if(ctx._tolerant===true)captureError(ctx,err)\n' : ''}ctx._fc=false
987
+ EC.e=pos
988
+ return err
989
+ }`
990
+ }
991
+
992
+ case OP_ROUTED: {
993
+ const fb = code[ip + 1]!
994
+ const fallback = fb >= 0 ? link(fb) : undefined
995
+ // The SPAN is the routed item's own object, not a copy — `assemble.ts`
996
+ // hands the same one to the leaf, and a fresh `{start,end}` here would
997
+ // be a different tree for any consumer comparing by identity.
998
+ const push = `pushCstLeaf(ctx,{_tag:'leaf',value:it.value,span:it.span})`
999
+ return `${head}
1000
+ const it=ctx._routed
1001
+ if(it===undefined||pos!==it.span.start){
1002
+ ${fallback !== undefined ? `return ${fallback}(input,pos,ctx)` : 'ctx._fe=pos;ctx._fx=ROUTED_FX;return FAIL'}
1003
+ }
1004
+ ${L.buf ? push : `if(ctx._cstBuf!==undefined||ctx._cstLeaves!==undefined)${push}`}
1005
+ EC.e=it.span.end
1006
+ return it.value
1007
+ }`
1008
+ }
1009
+
1010
+ case OP_SCAN: {
1011
+ const si = code[ip + 1]!
1012
+ // The scan pool is built FROM subtrees, so it is populated after this
1013
+ // text compiles. The array is passed by reference and indexed on
1014
+ // execution, which is what the closure engine's lazy bind amounts to.
1015
+ return `${head}
1016
+ const r=SCANS[${si}].parse(input,pos,ctx)
1017
+ if(!r.ok){ctx._fe=r.span.start;ctx._fx=r.expected??EMPTY_FX;return FAIL}
1018
+ EC.e=r.span.end
1019
+ return r.value
1020
+ }`
1021
+ }
1022
+
1023
+ case OP_SCOPE:
1024
+ case OP_SCOPE_CAP:
1025
+ case OP_SCOPE_PLAIN: {
1026
+ const ki = code[ip + 1]!
1027
+ const cap = op === OP_SCOPE_CAP
1028
+ const flags = op === OP_SCOPE_PLAIN ? 0 : code[ip + 3]!
1029
+ const child = link(code[ip + 2]!)
1030
+ // THE SWAP, RESOLVED AT EMIT. `swapLegal` is `!trackLines`, an option;
1031
+ // the other two are table data. All three are known here, so the body
1032
+ // holds the scanner it installs as a hoisted const.
1033
+ const scanFor = swapLegal && ki >= 0 && !triviaLabelled[ki]! ? hoist('ts', `TRIVIASCAN[${ki}]`) : 'null'
1034
+ const tri = ki < 0 ? 'undefined' : hoist('tv', `TRIVIA[${ki}]`)
1035
+ const lab = ki < 0 ? 'undefined' : hoist('tl', `TRIVIA[${ki}]._meta.triviaKindLabels`)
1036
+ // The two root-capture policies are TABLE DATA, so they are emitted INTO
1037
+ // the body. `scopeRootPolicy`'s two wrapper closures and their two extra
1038
+ // call frames do not exist here.
1039
+ const strict = (flags & 2) !== 0 ? 'refuseUnclassifiedRootScope(ctx._rootTriviaStrictScopes)\n' : ''
1040
+ const rootCap = (flags & 1) !== 0
1041
+ return `${head}
1042
+ ${strict}${rootCap ? 'const sR=ctx._rootTriviaCapture\nctx._rootTriviaCapture=false\n' : ''}const sT=ctx.trivia,sL=ctx.triviaKindLabels,sS=_pfScan${cap ? ',sC=ctx.captureTrivia' : ''}
1043
+ _pfScan=${scanFor}
1044
+ ctx.trivia=${tri}
1045
+ ctx.triviaKindLabels=${lab}
1046
+ ${cap ? 'ctx.captureTrivia=true\n' : ''}const v=${child}(input,pos,ctx)
1047
+ ${cap ? 'ctx.captureTrivia=sC\n' : ''}ctx.trivia=sT
1048
+ ctx.triviaKindLabels=sL
1049
+ _pfScan=sS
1050
+ ${rootCap ? 'ctx._rootTriviaCapture=sR\n' : ''}return v
1051
+ }`
1052
+ }
1053
+
1054
+ /* ── boundaries ──────────────────────────────────────────────────────── */
1055
+
1056
+ case OP_TOKEN:
1057
+ case OP_LEAF: {
1058
+ const isToken = op === OP_TOKEN
1059
+ const fn = isToken ? undefined : fnRef(code[ip + 1]!)
1060
+ const child = link(isToken ? code[ip + 1]! : code[ip + 2]!)
1061
+ // `try`/`finally`, AS `assemble.ts` HAS IT. `OP_SCOPE` restores linearly
1062
+ // and this does not, and the difference is not an oversight in either:
1063
+ // `test/unit/token.test.ts:198` pins CONTEXT RESTORATION ON A THROWING
1064
+ // BODY across the interpreter and `compile`, and jess's reducers
1065
+ // throw on purpose — that is how its dialects reject illegal constructs
1066
+ // (`bench/jess/digest.ts:64`). A boundary that leaked `ctx.trivia` and
1067
+ // five capture sinks on the way out would corrupt every subsequent parse
1068
+ // through the same context, so the handler IS the semantics here.
1069
+ //
1070
+ // `cstCaptureActive` IS THE SAVED STATE, read BEFORE the sinks are
1071
+ // cleared — the leaf this body contributes goes to the OUTER collector.
1072
+ // Inside a node the answer is the label's, and the call disappears.
1073
+ const wasCap = L.buf ? 'true' : '(ctx._cstBuf!==undefined||ctx._cstLeaves!==undefined)'
1074
+ // `token()` additionally clears the TRIVIA scope and the root trivia
1075
+ // log: its whole point is that the child sees no ambient skipping, so
1076
+ // the match is one contiguous run of input (`ops.ts:180-190`).
1077
+ return `${head}
1078
+ const sBuf=ctx._cstBuf,sCh=ctx._cstChildren,sLv=ctx._cstLeaves,sRaw=ctx._cstRawChildren,sTl=ctx._cstTriviaLog
1079
+ const sOtl=ctx._triviaLog
1080
+ ${isToken ? 'const sTri=ctx.trivia,sKinds=ctx.triviaKindLabels,sScan=_pfScan,sRtl=ctx._rootTriviaLog\n' : ''}const wasCap=${wasCap}
1081
+ ${isToken ? `_pfScan=null
1082
+ ctx.trivia=undefined
1083
+ ctx.triviaKindLabels=undefined
1084
+ ` : ''}ctx._cstBuf=undefined
1085
+ ctx._cstChildren=undefined
1086
+ ctx._cstLeaves=undefined
1087
+ ctx._cstRawChildren=undefined
1088
+ ctx._cstTriviaLog=undefined
1089
+ ctx._triviaLog=undefined
1090
+ ${isToken ? 'ctx._rootTriviaLog=undefined\n' : ''}let v
1091
+ try{v=${child}(input,pos,ctx)}finally{
1092
+ ${isToken ? `_pfScan=sScan
1093
+ ctx.trivia=sTri
1094
+ ctx.triviaKindLabels=sKinds
1095
+ ctx._rootTriviaLog=sRtl
1096
+ ` : ''}ctx._cstBuf=sBuf
1097
+ ctx._cstChildren=sCh
1098
+ ctx._cstLeaves=sLv
1099
+ ctx._cstRawChildren=sRaw
1100
+ ctx._cstTriviaLog=sTl
1101
+ ctx._triviaLog=sOtl
1102
+ }
1103
+ if(v===FAIL)return FAIL
1104
+ const e=EC.e
1105
+ const out=${isToken ? 'input.slice(pos,e)' : `${fn}(v,{start:pos,end:e})`}
1106
+ if(wasCap)pushCstLeaf(ctx,{_tag:'leaf',value:out,span:{start:pos,end:e}})
1107
+ EC.e=e
1108
+ return out
1109
+ }`
1110
+ }
1111
+
1112
+ /* ── transaction ─────────────────────────────────────────────────────── */
1113
+
1114
+ case OP_ATTEMPT: {
1115
+ const child = link(code[ip + 1]!)
1116
+ const p = tmp()
1117
+ return `${head}
1118
+ ${emitMark(p, L.buf, sinks)}
1119
+ const v=${child}(input,pos,ctx)
1120
+ if(v!==FAIL)return v
1121
+ ${emitRollback(p, L.buf, sinks)}
1122
+ if(ctx._fc===true)return FAIL
1123
+ ctx._fe=pos
1124
+ return FAIL
1125
+ }`
1126
+ }
1127
+
1128
+ case OP_NOT: {
1129
+ const child = link(code[ip + 1]!)
1130
+ const xf = fxRef(code[ip + 2]!)
1131
+ const p = tmp()
1132
+ return `${head}
1133
+ ${emitMark(p, L.buf, sinks)}
1134
+ const v=${child}(input,pos,ctx)
1135
+ ${emitRollback(p, L.buf, sinks)}
1136
+ if(v===FAIL){EC.e=pos;return null}
1137
+ ctx._fe=pos
1138
+ ctx._fx=${xf}
1139
+ return FAIL
1140
+ }`
1141
+ }
1142
+
1143
+ case OP_PEEK: {
1144
+ const child = link(code[ip + 1]!)
1145
+ const xf = fxRef(code[ip + 2]!)
1146
+ const p = tmp()
1147
+ return `${head}
1148
+ ${emitMark(p, L.buf, sinks)}
1149
+ const v=${child}(input,pos,ctx)
1150
+ ${emitRollback(p, L.buf, sinks)}
1151
+ if(v===FAIL){ctx._fe=pos;ctx._fx=${xf};return FAIL}
1152
+ EC.e=pos
1153
+ return null
1154
+ }`
1155
+ }
1156
+
1157
+ case OP_OPT: {
1158
+ const child = link(code[ip + 1]!)
1159
+ const p = tmp()
1160
+ return `${head}
1161
+ ${emitMark(p, L.buf, sinks)}
1162
+ ctx._fc=false
1163
+ const v=${child}(input,pos,ctx)
1164
+ if(v===FAIL){
1165
+ if(ctx._fc===true)return FAIL
1166
+ ${emitRollback(p, L.buf, sinks)}
1167
+ EC.e=pos
1168
+ return null
1169
+ }
1170
+ return v
1171
+ }`
1172
+ }
1173
+
1174
+ case OP_SEQ:
1175
+ case OP_SEQV:
1176
+ case OP_SEQX: {
1177
+ const fused = op === OP_SEQX
1178
+ const base = fused ? ip + 3 : ip + 2
1179
+ const n = code[fused ? ip + 2 : ip + 1]!
1180
+ const wantValues = op !== OP_SEQV
1181
+ // ADJACENCY changes the PARENT's term-boundary emission: the assertion
1182
+ // must be answered at the cursor, BEFORE the ambient trivia scan.
1183
+ // Refused rather than lowered wrong — a piece handed the post-scan
1184
+ // position finds the gap already consumed and answers "adjacent" every
1185
+ // time, SILENTLY (`ops.ts:226-251`).
1186
+ for (let i = 1; i < n; i++) {
1187
+ if (code[code[base + i]!] === OP_ADJ) {
1188
+ throw new Unemittable('a sequence carrying an adjacency assertion (OP_ADJ)')
1189
+ }
1190
+ }
1191
+ const fn = fused ? fnRef(code[ip + 1]!) : undefined
1192
+ const kids: string[] = []
1193
+ for (let i = 0; i < n; i++) kids.push(link(code[base + i]!))
1194
+ /**
1195
+ * A RECOVERY SEQUENCE PUBLISHES `ctx._sync`, and that is the WHOLE of
1196
+ * "recovery config" — no grammar carries any (`assemble.ts:1446`).
1197
+ *
1198
+ * Before term `i`, the sentinel for the union of every LATER term's first
1199
+ * set becomes the sync point a list nested in that term resyncs to, so a
1200
+ * `sepBy` inside `sequence('{', list, '}')` finds `}` with nothing
1201
+ * annotated. Where there is no usable local follow — the last term, an
1202
+ * `any` first set — the INHERITED sentinel stays published, which is how
1203
+ * an enclosing delimiter reaches across a rule boundary. `sentRef` emits
1204
+ * the literal `undefined` for that case, so it costs no array read.
1205
+ *
1206
+ * THE `n` SYNC OPERANDS SIT AFTER THE `n` CHILD OPERANDS, laid down by
1207
+ * `encodeTable({ recovery })` — the same operands `assemble.ts:1468`
1208
+ * reads, at the same offsets.
1209
+ *
1210
+ * RESTORED IN A `finally`, matching `sequence()`'s own
1211
+ * (`combinators/sequence.ts:105`) and the closure engine's. `emitTerm`
1212
+ * emits `return FAIL` INLINE at several points, so a restore placed on
1213
+ * the fall-through paths alone would leak a stale sentinel out of exactly
1214
+ * the failure the recovery path is about to read. `finally` is the only
1215
+ * placement that covers an inlined return, and it is on the tolerant
1216
+ * assembly only — the strict one emits none of this.
1217
+ */
1218
+ const sy = REC ? `${tmp()}sy` : ''
1219
+ const pub = (i: number): string => {
1220
+ const s = sentRef(code[base + n + i]!)
1221
+ return s === 'undefined' ? `ctx._sync=${sy}` : `ctx._sync=${s}??${sy}`
1222
+ }
1223
+ const parts: string[] = [head]
1224
+ if (REC) parts.push(`const ${sy}=ctx._sync`, 'try{', pub(0))
1225
+ parts.push(`const v0=${kids[0]}(input,pos,ctx)`, 'if(v0===FAIL)return FAIL')
1226
+ const close = (): string => REC ? `${parts.join('\n')}\n}finally{ctx._sync=${sy}}\n}` : `${parts.join('\n')}\n}`
1227
+ if (n === 1) {
1228
+ parts.push(fused ? `return ${fn}([v0],{start:pos,end:EC.e})` : wantValues ? 'return [v0]' : 'return undefined')
1229
+ return close()
1230
+ }
1231
+ parts.push('let cur=EC.e')
1232
+ const names: string[] = ['v0']
1233
+ for (let i = 1; i < n; i++) {
1234
+ const vn = `v${i}`
1235
+ parts.push(`let ${vn}`)
1236
+ if (REC) parts.push(pub(i))
1237
+ parts.push(emitTerm(kids[i]!, vn, tmp(), L, skipFor(L)))
1238
+ names.push(vn)
1239
+ }
1240
+ parts.push('EC.e=cur')
1241
+ parts.push(fused
1242
+ ? `return ${fn}([${names.join(',')}],{start:pos,end:cur})`
1243
+ : wantValues ? `return [${names.join(',')}]` : 'return undefined')
1244
+ return close()
1245
+ }
1246
+
1247
+ case OP_CHOICE: {
1248
+ const table = disp[code[ip + 1]!]!
1249
+ const n = code[ip + 2]!
1250
+ const choiceFx = fxRef(code[ip + 3]!)
1251
+ const base = ip + 4
1252
+ const arms: string[] = []
1253
+ for (let i = 0; i < n; i++) arms.push(link(code[base + i]!))
1254
+ const axi = armExpected.push(
1255
+ Array.from({ length: n }, (_, i) => t.fx[code[base + n + i]!] as readonly string[]),
1256
+ ) - 1
1257
+ armExpectedPlan.push(Array.from({ length: n }, (_, i) => code[base + n + i]!))
1258
+ const afx = hoist('afx', `AFX[${axi}]`)
1259
+
1260
+ if (table.exclusive) {
1261
+ // NO OPEN ARMS EXIST under `exclusive` — `resolveDispatch` clears the
1262
+ // flag for any arm whose class is −1 — so there is no fallback loop.
1263
+ const di = code[ip + 1]!
1264
+ const asc = hoist('as', `DISP[${di}].ascii`)
1265
+ const hiArr = table.hi
1266
+ // Arms named, one `case` each: the interpreter's `arms[arm](…)` is an
1267
+ // array index feeding ONE call site, which is the megamorphic site
1268
+ // this unit exists to remove.
1269
+ const armSwitch = arms.map((a, i) => `case ${i}:v=${a}(input,pos,ctx);break`).join('\n')
1270
+ return `${head}
1271
+ const c=lead(input,pos)
1272
+ let arm=-1
1273
+ if(c>=0&&c<128){const a=${asc}[c];if(a!==0)arm=a-1}
1274
+ ${hiArr.length === 0 ? '' : `else if(c>=128){const h=${hoist('hi', `DISP[${di}].hi`)}
1275
+ for(let i=0;i<h.length;i+=3){if(c>=h[i]&&c<=h[i+1]){arm=h[i+2];break}}}`}
1276
+ if(arm>=0){
1277
+ ctx._fc=false
1278
+ let v
1279
+ switch(arm){
1280
+ ${armSwitch}
1281
+ }
1282
+ if(v!==FAIL)return v
1283
+ if(ctx._fc===true)return FAIL
1284
+ const af=ctx._fx
1285
+ if(af!==undefined&&af.length>0){ctx._fe=pos;return FAIL}
1286
+ }
1287
+ ctx._fe=pos;ctx._fx=${choiceFx}
1288
+ return FAIL
1289
+ }`
1290
+ }
1291
+
1292
+ // THE PER-ARM GATE. `assemble.ts` precomputes a candidate bitmask so one
1293
+ // `Uint32Array` load replaces `n` class tests, then walks the set bits
1294
+ // through `arms[i]`. The mask survives here; the indexed CALL does not.
1295
+ const ci = classes.push(Array.from({ length: n }, (_, i) => table.armCls[i] ?? null)) - 1
1296
+ // `resolveDispatch` builds `armCls[a]` as `cc[disp[di][a]]`, or null when
1297
+ // that index is negative (program.ts:419-423), so the arm's CLASS INDEX
1298
+ // is the dispatch row itself. Nothing is re-derived here.
1299
+ classPlan.push(Array.from({ length: n }, (_, i) => prog.disp[code[ip + 1]!]![i] ?? -1))
1300
+ const gates = classes[ci]!
1301
+ const gRef = hoist('g', `CLS[${ci}]`)
1302
+ const maskable = n <= 32
1303
+ let maskName = ''
1304
+ if (maskable) {
1305
+ maskName = hoist('mk', `MASK[${masks.push(maskForClassRow(gates)) - 1}]`)
1306
+ // The mask is a pure function of `gates`, i.e. of `CLS[ci]`, so the
1307
+ // plan records the class-pool index rather than 129 words per site.
1308
+ maskPlan.push(ci)
1309
+ }
1310
+ const p = tmp()
1311
+ // THE SKIPPED ARMS' SETS STAY LAZY. `prev` and the catch-up loop are the
1312
+ // closure engine's, kept verbatim: they run only on a FAILURE path, and
1313
+ // an arm that MATCHES returns without paying for any of it. Unrolling
1314
+ // them would put an `_accSet` — and its `slice()` — on the success path.
1315
+ const maskArms = arms.map((a, i) => `
1316
+ if((bits&${1 << i})!==0){
1317
+ ctx._fc=false
1318
+ {const v=${a}(input,pos,ctx)
1319
+ if(v!==FAIL)return v}
1320
+ for(let j=prev;j<${i};j++)if((bits&(1<<j))===0)acc=_accSet(${afx}[j],acc)
1321
+ prev=${i + 1}
1322
+ acc=_accSet(ctx._fx,acc)
1323
+ if(ctx._fc===true){if(acc!==undefined)ctx._fx=acc;return FAIL}
1324
+ ${emitRollback(p, L.buf, sinks)}
1325
+ }`).join('')
1326
+
1327
+ const generalArms = arms.map((a, i) => `
1328
+ if(${gRef}[${i}]===null||classHas(${gRef}[${i}],c)){
1329
+ ctx._fc=false
1330
+ {const v=${a}(input,pos,ctx)
1331
+ if(v!==FAIL)return v}
1332
+ acc=_accSet(ctx._fx,acc)
1333
+ if(ctx._fc===true){if(acc!==undefined)ctx._fx=acc;return FAIL}
1334
+ ${emitRollback(p, L.buf, sinks)}
1335
+ }else acc=_accSet(${afx}[${i}],acc)`).join('')
1336
+
1337
+ return `${head}
1338
+ const c=lead(input,pos)
1339
+ ${emitMark(p, L.buf, sinks)}
1340
+ let acc
1341
+ ${maskable ? `if(c<128){
1342
+ const bits=${maskName}[c<0?128:c]
1343
+ let prev=0
1344
+ ${maskArms}
1345
+ for(let j=prev;j<${n};j++)if((bits&(1<<j))===0)acc=_accSet(${afx}[j],acc)
1346
+ ctx._fe=pos;ctx._fx=acc??${choiceFx}
1347
+ return FAIL
1348
+ }
1349
+ ` : ''}${generalArms}
1350
+ ctx._fe=pos;ctx._fx=acc??${choiceFx}
1351
+ return FAIL
1352
+ }`
1353
+ }
1354
+
1355
+ case OP_DISPATCH: {
1356
+ const di = code[ip + 2]!
1357
+ const spec = dsp[di]
1358
+ const selector = link(code[ip + 1]!)
1359
+ const otherIp = code[ip + 3]!
1360
+ const other = otherIp >= 0 ? link(otherIp) : undefined
1361
+ const otherRouted = code[ip + 4]! === 1
1362
+ const n = code[ip + 5]!
1363
+ validateDispatchSpec(spec, n, code[ip + 4]!)
1364
+ const armBase = ip + 6
1365
+ const arms: string[] = []
1366
+ for (let i = 0; i < n; i++) arms.push(link(code[armBase + i]!))
1367
+ const bk = hoist('bk', `DSP[${di}].byKey`)
1368
+ const dx = hoist('dx', `DSP[${di}].expected`)
1369
+ // THE MATCHER ARMS, AS SOURCE. `exec.ts`'s `linkMatcher` mints one
1370
+ // closure per arm from four literals it already has in hand; the four
1371
+ // are TABLE DATA, so here they are the test itself and no closure, no
1372
+ // pool and no indexed call exist. Kinds are `matcherClaims`'s exactly
1373
+ // — 0/1 raw prefix/suffix, 3/4 the pre-folded pair, anything else a
1374
+ // regex. The regex is built PER TEST, deliberately: a hoisted one
1375
+ // carries `lastIndex` across parses whenever the author's pattern is
1376
+ // sticky or global (`exec.ts:140-144`).
1377
+ const claims = (m: readonly [number, string, string, number]): string => {
1378
+ switch (m[0]) {
1379
+ case 0: return `key.startsWith(${q(m[1])})`
1380
+ case 1: return `key.endsWith(${q(m[1])})`
1381
+ case 3: return `asciiFoldKey(key).startsWith(${q(m[1])})`
1382
+ case 4: return `asciiFoldKey(key).endsWith(${q(m[1])})`
1383
+ default: return `new RegExp(${q(m[1])},${q(m[2])}).test(key)`
1384
+ }
1385
+ }
1386
+ const chain = spec.match.length === 0
1387
+ ? ''
1388
+ : `if(arm===undefined){\n${spec.match.map((m, i) => `${i === 0 ? '' : 'else '}if(${claims(m)})arm=${m[3]}`).join('\n')}\n}\n`
1389
+ const fold = spec.byFold.size > 0
1390
+ ? `if(arm===undefined)arm=${hoist('bf', `DSP[${di}].byFold`)}.get(asciiFoldKey(key))\n`
1391
+ : ''
1392
+ const m1 = tmp()
1393
+ const m2 = tmp()
1394
+ const routedCall = (target: string): string => `{const savedRouted=ctx._routed
1395
+ ${emitRollback(m1, L.buf, sinks)}
1396
+ ${emitMark(m2, L.buf, sinks, false)}
1397
+ ctx._routed={value:key,span:{start:pos,end:selEnd}}
1398
+ try{v=${target}(input,pos,ctx)}finally{ctx._routed=savedRouted}
1399
+ break}`
1400
+ const plainCall = (target: string): string => `v=${target}(input,selEnd,ctx);break`
1401
+ const armCases = arms.map((target, i) => `case ${i}:${spec.routed[i] === 1
1402
+ ? routedCall(target)
1403
+ : plainCall(target)}`).join('\n')
1404
+ const fallbackCase = other === undefined ? '' : `default:${otherRouted
1405
+ ? routedCall(other)
1406
+ : plainCall(other)}`
1407
+ // THE SELECTOR RUNS ONCE and the key it returns picks the arm — that is
1408
+ // what `dispatch()` buys over a choice of arms that each re-parse the
1409
+ // opener. A routed arm rewinds the selector's trivia capture and gets
1410
+ // the token handed to it (`OP_ROUTED`) instead of re-matching it.
1411
+ return `${head}
1412
+ ${emitMark(m1, L.buf, sinks)}
1413
+ const sv=${selector}(input,pos,ctx)
1414
+ if(sv===FAIL)return FAIL
1415
+ const selEnd=EC.e
1416
+ const key=sv
1417
+ let arm=${bk}.get(key)
1418
+ ${fold}${chain}
1419
+ if(arm===undefined){
1420
+ ${other === undefined ? `ctx._fe=selEnd;ctx._fx=${dx};return FAIL` : ''}
1421
+ }
1422
+ ${emitMark(m2, L.buf, sinks)}
1423
+ let v
1424
+ switch(arm){
1425
+ ${armCases}
1426
+ ${fallbackCase}
1427
+ }
1428
+ if(v===FAIL){
1429
+ ${emitRollback(m2, L.buf, sinks)}
1430
+ ctx._fc=true
1431
+ return FAIL
1432
+ }
1433
+ return [key,v]
1434
+ }`
1435
+ }
1436
+
1437
+ case OP_REP:
1438
+ case OP_REPV: {
1439
+ const child = link(code[ip + 1]!)
1440
+ const min = code[ip + 2]!
1441
+ const max = code[ip + 3]!
1442
+ const sepIp = code[ip + 4]!
1443
+ const sep = sepIp >= 0 ? link(sepIp) : undefined
1444
+ const flags = code[ip + 5]!
1445
+ const keepSeparators = (flags & 2) !== 0
1446
+ const trailingAllowed = (flags & 1) !== 0
1447
+ const reportItem = (flags & 4) !== 0
1448
+ const itemFx = reportItem ? fxRef(code[ip + 6]!) : 'EMPTY_FX'
1449
+ const collect = op === OP_REP
1450
+ const skipBeforeFirst = sepIp < 0 && min === 0
1451
+ const p = tmp()
1452
+ // `viaRepItem` is `sep === undefined && count >= min && (count > 0 ||
1453
+ // skipBeforeFirst)`. Two of its three conjuncts are table data, so only
1454
+ // what survives them is emitted.
1455
+ const via = sep !== undefined
1456
+ ? 'false'
1457
+ : min === 0
1458
+ ? (skipBeforeFirst ? 'true' : 'count>0')
1459
+ : `count>=${min}&&count>0`
1460
+ const leadTrivia = skipBeforeFirst ? 'true' : 'count>0'
1461
+ // THE TWO LOOP-INVARIANT HOISTS, RESOLVED AT EMIT WHERE THE LABEL ANSWERS
1462
+ // THEM. `hasTrivia` is the site's trivia scope; `needMark` is implied by
1463
+ // an open `_cstBuf`. Both were read once per repetition SITE, which is
1464
+ // cheap on its own and is not the point — what they gated was a branch
1465
+ // per item and a branch at every one of the five rollback points below.
1466
+ const skip = skipFor(L)
1467
+ const knownTrivia = L.tri === TRI_NONE ? false : L.tri !== TRI_UNKNOWN ? true : undefined
1468
+ const hasTrivia = knownTrivia === undefined ? 'hasTrivia' : String(knownTrivia)
1469
+ const needMark = L.buf ? 'true' : 'needMark'
1470
+ // THE `_fields` AND `_errors` MARKS ARE TAKEN ONLY WHERE THE TABLE HAS A
1471
+ // WRITER, which is the same emit-time census `emitMark` applies to the
1472
+ // non-loop marks. Before `OP_FIELD` and `OP_EXPECT` were emittable the
1473
+ // answer was "never"; it is now "per table", and a grammar with no field
1474
+ // still pays neither the two loads nor the two stores per item.
1475
+ const pfd = sinks.fd ? `${p}fd` : '0'
1476
+ const per = sinks.er ? `${p}er` : 'undefined'
1477
+ const rb = L.buf
1478
+ ? `_rbBuf(ctx,${p}raw,${p}tl,${p}lv,${p}lg,${p}rt)${sinks.fd ? `\nif(ctx._fields!==undefined&&ctx._fields.length!==${p}fd)ctx._fields.length=${p}fd` : ''}${sinks.er ? `\nif(ctx._errors!==undefined&&ctx._errors.length!==${p}er)ctx._errors.length=${p}er` : ''}`
1479
+ : `if(needMark)rollbackTriviaAt(ctx,${p}raw,${p}tl,${p}lv,${pfd},${per},${p}lg,${p}rt)`
1480
+ const markSinks = `${sinks.fd ? `\n${p}fd=ctx._fields!==undefined?ctx._fields.length:0` : ''}${sinks.er ? `\n${p}er=ctx._errors!==undefined?ctx._errors.length:0` : ''}`
1481
+ const markBody = L.buf
1482
+ ? `const b=ctx._cstBuf
1483
+ const r=b.raw;${p}raw=r!==undefined?r.length:b.rawSingle!==undefined?1:0
1484
+ const h=b.ch;${p}lv=h!==undefined?h.length:b.single!==undefined?1:0
1485
+ const l=b.tl;${p}tl=l!==undefined?l.length:0
1486
+ ${p}lg=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
1487
+ ${p}rt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0${markSinks}`
1488
+ : `if(needMark){
1489
+ const b=ctx._cstBuf
1490
+ if(b!==undefined){
1491
+ const r=b.raw;${p}raw=r!==undefined?r.length:b.rawSingle!==undefined?1:0
1492
+ const h=b.ch;${p}lv=h!==undefined?h.length:b.single!==undefined?1:0
1493
+ const l=b.tl;${p}tl=l!==undefined?l.length:0
1494
+ }else{
1495
+ ${p}raw=ctx._cstRawChildren!==undefined?ctx._cstRawChildren.length:0
1496
+ ${p}tl=ctx._cstTriviaLog!==undefined?ctx._cstTriviaLog.length:0
1497
+ ${p}lv=ctx._cstLeaves!==undefined?ctx._cstLeaves.length:0
1498
+ }
1499
+ ${p}lg=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
1500
+ ${p}rt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0${markSinks}
1501
+ }`
1502
+ /**
1503
+ * TOLERANT RECOVERY — the SAME functions the other three engines call
1504
+ * (`recovery/scan.ts`), so an error's span, its expected set and its CST
1505
+ * embedding are produced once and cannot drift between engines.
1506
+ *
1507
+ * Reached only in a recovery table, and inside it only on the FAILURE of
1508
+ * an element, so a matching item pays one `_sync` read at entry and
1509
+ * nothing else. Everything else in this loop is the emitted strict loop
1510
+ * unchanged — which is the whole point, and what the top-level refusal
1511
+ * this replaces was giving up.
1512
+ *
1513
+ * THE FOUR AGREEMENTS WITH `assemble.ts:2036`, each load-bearing:
1514
+ * - a MANDATORY item of a separator-less repeat does NOT recover, so the
1515
+ * gate is `sep !== undefined || count >= min`. Both conjuncts are
1516
+ * table data here, so the test is CONSTANT-FOLDED rather than run.
1517
+ * - the check is `matchesAt(mySync, itemStart)`: sitting ON the sync
1518
+ * token is a clean list end, not junk, and `itemStart` is past any
1519
+ * leading trivia so trivia is never swallowed into the error span.
1520
+ * - a separated list scans to its OWN separator or the enclosing
1521
+ * delimiter (`orSentinel`); a separator-less one to the inherited
1522
+ * sentinel alone.
1523
+ * - the separator-less path rolls leading trivia back BEFORE recovering
1524
+ * and the separated path does NOT — a consumed separator and the error
1525
+ * after it both belong to the list (repeat.ts:533). This is why the
1526
+ * strict loop's unconditional `${'${rb}'}` could not simply be reused.
1527
+ *
1528
+ * `mySync` IS CAPTURED AT ENTRY, not read per item: an element's own
1529
+ * sequence publishes over `_sync` while it runs. The interpreter restores
1530
+ * in a `finally`, codegen saves at entry — same value, and so does this.
1531
+ */
1532
+ const my = `${p}my`
1533
+ // `reportItem` gates the FAILURE report only. `recoverScan`'s expected set
1534
+ // is taken unconditionally by the closure engine (`assemble.ts:2067`), so
1535
+ // it is resolved separately from `itemFx` — reusing that one would hand
1536
+ // `EMPTY_FX` to every recovered error in a grammar that does not report.
1537
+ const recFx = REC ? fxRef(code[ip + 6]!) : ''
1538
+ const sepSent = REC ? sentRef(code[ip + 7]!) : 'undefined'
1539
+ const recSent = sep === undefined
1540
+ ? my
1541
+ : sepSent === 'undefined'
1542
+ ? `orSentinel(${my},undefined)`
1543
+ : `orSentinel(${sepSent}??${my},${sepSent}===undefined?undefined:${my})`
1544
+ const recGate = sep !== undefined || min === 0 ? '' : `count>=${min}&&`
1545
+ const recBranch = !REC ? '' : `if(ctx._tolerant===true&&${my}!==undefined&&${recGate}!matchesAt(${my},input,itemStart,ctx)){
1546
+ ${sep === undefined ? `${rb}\n` : ''}const rr=recoverScan(input,itemStart,ctx,${recSent},${recFx})
1547
+ ${collect ? 'out.push(rr.error)\n' : ''}captureError(ctx,rr.error)
1548
+ count++
1549
+ cur=rr.end
1550
+ continue
1551
+ }
1552
+ `
1553
+ return `${head}
1554
+ const out=${collect ? '[]' : 'undefined'}
1555
+ ${knownTrivia === undefined ? 'const hasTrivia=ctx.trivia!==undefined\n' : ''}${L.buf ? '' : 'const needMark=_rollbackNeeded(ctx)\n'}${REC ? `const ${my}=ctx._sync\n` : ''}let cur=pos
1556
+ let count=0
1557
+ for(;;){
1558
+ ${max >= 0 ? `if(count>=${max})break\n` : ''}${sep !== undefined ? `if(count>0&&count>=${min}&&cur>=input.length)break\n` : ''}let ${p}raw=0,${p}tl=0,${p}lv=0,${p}lg=0,${p}rt=0${sinks.fd ? `,${p}fd=0` : ''}${sinks.er ? `,${p}er=0` : ''}
1559
+ ${markBody}
1560
+ let itemStart=cur
1561
+ let sepEnd=-1
1562
+ ${sep !== undefined ? `if(count>0){
1563
+ const lb=cstLeavesLen(ctx)
1564
+ let sp=cur
1565
+ ${hasTrivia === 'false' ? '' : hasTrivia === 'true' ? `sp=${skip}(input,sp,ctx)\n` : `if(hasTrivia)sp=${skip}(input,sp,ctx)\n`}ctx._fc=false
1566
+ const sv=${sep}(input,sp,ctx)
1567
+ if(sv===FAIL){
1568
+ ${rb}
1569
+ if(ctx._fc===true)return FAIL
1570
+ break
1571
+ }
1572
+ ${keepSeparators ? '' : 'demoteCapturedToRaw(ctx,lb)\n'}sepEnd=EC.e
1573
+ itemStart=${hasTrivia === 'false' ? 'EC.e' : hasTrivia === 'true' ? `${skip}(input,EC.e,ctx)` : `hasTrivia?${skip}(input,EC.e,ctx):EC.e`}
1574
+ }else ${leadSkip(hasTrivia, leadTrivia, skip)}
1575
+ ` : `${leadSkip(hasTrivia, leadTrivia, skip)}
1576
+ `}if(itemStart>=input.length&&${via}){
1577
+ ${rb}
1578
+ ${trailingAllowed ? 'if(sepEnd>=0)cur=sepEnd\n' : ''}break
1579
+ }
1580
+ ctx._fc=false
1581
+ const v=${child}(input,itemStart,ctx)
1582
+ if(v===FAIL){
1583
+ ${REC
1584
+ // THE ROLLBACK MOVES INSIDE THE ARMS. The strict loop rolls back
1585
+ // unconditionally before testing `_fc`, which is sound because every path out
1586
+ // of it rolls back. The recovery arm is the one path that must NOT: a
1587
+ // separated list keeps the consumed separator and the trivia after it, both of
1588
+ // which belong to the list (repeat.ts:533). So `committed` is tested first and
1589
+ // each arm takes its own rollback — the same order as `assemble.ts:2113`.
1590
+ ? `if(ctx._fc===true){
1591
+ ${rb}
1592
+ return FAIL
1593
+ }
1594
+ ${recBranch}${rb}`
1595
+ : `${rb}
1596
+ if(ctx._fc===true)return FAIL`}
1597
+ ${trailingAllowed ? 'if(sepEnd>=0)cur=sepEnd\n' : ''}break
1598
+ }
1599
+ if(EC.e===itemStart&&${via}){
1600
+ ${rb}
1601
+ break
1602
+ }
1603
+ ${collect ? 'out.push(v)\n' : ''}cur=EC.e
1604
+ count++
1605
+ }
1606
+ ${min > 0 ? `if(count<${min}){
1607
+ ${reportItem ? `ctx._fe=cur;ctx._fx=${itemFx}\n` : ''}return FAIL
1608
+ }
1609
+ ` : ''}EC.e=cur
1610
+ return out
1611
+ }`
1612
+ }
1613
+
1614
+ case OP_NODE:
1615
+ case OP_NODE_TRACK: {
1616
+ const flags = code[ip + 3]!
1617
+ const child = link(code[ip + 2]!)
1618
+ const proj = code[ip + 4]!
1619
+ const buildIdx = code[ip + 1]!
1620
+ const type = k[code[ip + 5]!] as string
1621
+ const tagIdx = code[ip + 6]!
1622
+ const tags = tagIdx < 0 ? 'undefined' : kRef(tagIdx)
1623
+ const tracked = op === OP_NODE_TRACK
1624
+ const readsTrivia = (flags & 4) !== 0
1625
+ const readsState = (flags & 8) !== 0
1626
+ const hasFields = (flags & 16) !== 0
1627
+ const collapse = (flags & 32) !== 0
1628
+ const unwrap = (flags & 64) !== 0
1629
+ const trailingTrivia = (flags & 128) !== 0
1630
+ // HOST MODE IS AN OPTION, and it decided five runtime ternaries in the
1631
+ // interpreter's node case — the most-executed non-terminal in any of
1632
+ // these grammars. It selects the emitted shape instead.
1633
+ const build = buildIdx >= 0 ? fnRef(buildIdx) : undefined
1634
+ const structural = build === undefined && proj < 0
1635
+ const grammarCapture = (flags & 1) !== 0 || trailingTrivia
1636
+ const hostCapturesThisType = structural && cfg.hostCaptureTrivia !== undefined
1637
+ ? cfg.hostCaptureTrivia(type)
1638
+ : undefined
1639
+ const wantFields = hasFields || hostCst
1640
+ const captureWide = readsTrivia || hostCst
1641
+ ? !structural || grammarCapture || hostCapturesThisType !== false
1642
+ : hostCapturesThisType === true
1643
+ const keepChildren = !structural || cfg.hostReadsChildren !== false || collapse || unwrap
1644
+ const ty = q(type)
1645
+ const stArg = readsState ? 'st' : '(ctx.state!==undefined?Object.assign({},ctx.state):undefined)'
1646
+ const hostCall = `_pfHost(${ty},hostKids,fieldMap,span,rawKids,tlog,${stArg},${tags})`
1647
+ let value: string
1648
+ if (proj >= 0) {
1649
+ value = hostCst
1650
+ ? `nd=_pfHost!==undefined?${hostCall}:projectChild(kids,${proj},${ty})`
1651
+ : `nd=projectChild(kids,${proj},${ty})`
1652
+ } else if (build !== undefined) {
1653
+ // A direct builder is bypassed under a CST host.
1654
+ const direct = `${build}(kids,fieldMap,span,rawKids,${captureWide ? 'tlog' : 'EMPTY_TL'},st)`
1655
+ value = hostCst ? `nd=_pfHost!==undefined?${hostCall}:${direct}` : `nd=${direct}`
1656
+ } else {
1657
+ value = `nd=_pfHost!==undefined?_pfHost(${ty},hostKids,fieldMap,span,rawKids,tlog,st,${tags}):{_tag:'node',type:${ty},span,state:st??null,children:kids}`
1658
+ }
1659
+ // HOST COLLAPSE applies wherever the node's VALUE comes from the host —
1660
+ // any node under a CST host, not only the builder-less ones.
1661
+ const collapsible = keepChildren && (hostCst || (build === undefined && proj < 0))
1662
+ const openCapture = keepChildren
1663
+ ? `const buf={}
1664
+ ctx._cstBuf=buf
1665
+ ctx._cstChildren=undefined
1666
+ ctx._cstLeaves=undefined
1667
+ ctx._cstRawChildren=undefined
1668
+ ctx._cstTriviaLog=undefined`
1669
+ : `const buf={rawOnly:true}
1670
+ ctx._cstBuf=buf
1671
+ ctx._cstChildren=undefined
1672
+ ctx._cstLeaves=undefined
1673
+ ctx._cstRawChildren=undefined
1674
+ ctx._cstTriviaLog=undefined`
1675
+ const finishCapture = keepChildren
1676
+ ? `const kids=buf.ch??(buf.single!==undefined?[buf.single]:EMPTY_CH)
1677
+ const hostKids=kids
1678
+ const rawKids=buf.raw??(buf.rawSingle!==undefined?[buf.rawSingle]:EMPTY_CH)
1679
+ const tlog=buf.tl??EMPTY_TLOG`
1680
+ : `const kids=EMPTY_CH
1681
+ const hostKids=kids
1682
+ const rawKids=buf.raw??(buf.rawSingle!==undefined?[buf.rawSingle]:EMPTY_CH)
1683
+ const tlog=buf.tl??EMPTY_TLOG`
1684
+ return `${head}
1685
+ const sCh=ctx._cstChildren,sLv=ctx._cstLeaves,sRaw=ctx._cstRawChildren,sTl=ctx._cstTriviaLog
1686
+ const sCap=ctx.captureTrivia,sBuf=ctx._cstBuf
1687
+ ${openCapture}
1688
+ ctx.captureTrivia=${captureWide}
1689
+ const savedFields=ctx._fields
1690
+ ctx._fields=${wantFields ? '[]' : 'undefined'}
1691
+ ${structural ? `const savedMask=ctx._triviaCaptureMask
1692
+ if(_pfHost!==undefined&&_pfHost._parsemanTriviaKinds!==undefined)ctx._triviaCaptureMask=_pfHost._parsemanTriviaKinds(${ty})
1693
+ ` : ''}const v=${child}(input,pos,ctx)
1694
+ ${trailingTrivia && L.tri !== TRI_NONE
1695
+ ? `if(v!==FAIL${L.tri === TRI_UNKNOWN ? '&&ctx.trivia!==undefined' : ''})EC.e=consumeTrivia(input,EC.e,ctx)\n`
1696
+ : ''}const fieldMap=${wantFields ? 'buildFieldMap(ctx._fields)' : 'undefined'}
1697
+ ctx._fields=savedFields
1698
+ ${structural ? 'ctx._triviaCaptureMask=savedMask\n' : ''}${finishCapture}
1699
+ ctx._cstBuf=sBuf
1700
+ ctx._cstChildren=sCh
1701
+ ctx._cstLeaves=sLv
1702
+ ctx._cstRawChildren=sRaw
1703
+ ctx._cstTriviaLog=sTl
1704
+ ctx.captureTrivia=sCap
1705
+ if(v===FAIL)return FAIL
1706
+ const end=EC.e
1707
+ const span=${tracked ? 'spanLines(ctx,pos,end)' : '{start:pos,end}'}
1708
+ const st=${readsState ? '(ctx.state!==undefined?Object.assign({},ctx.state):undefined)' : 'undefined'}
1709
+ let nd
1710
+ ${unwrap ? 'if(kids.length===1)nd=unwrapChild(kids[0])\nelse ' : ''}${collapse ? 'if(kids.length===1)nd=kids[0]\nelse ' : ''}${collapsible ? `if(_pfHost!==undefined&&_pfHost._parsemanCstCollapse!==undefined&&kids.length===1&&rawKids.length===1&&_pfHost._parsemanCstCollapse(${ty},kids[0],kids,rawKids))nd=kids[0]
1711
+ else ` : ''}{${value}}
1712
+ ${L.buf
1713
+ // The OUTER buffer, which this body saved into `sBuf` before opening its own —
1714
+ // so an in-node site's parent collector is present by the same fact.
1715
+ ? 'pushCstChild(ctx,nd,rawEntry(nd,input,pos,end))'
1716
+ : 'if(sBuf!==undefined||sCh!==undefined)pushCstChild(ctx,nd,rawEntry(nd,input,pos,end))'}
1717
+ EC.e=end
1718
+ return nd
1719
+ }`
1720
+ }
1721
+
1722
+ default:
1723
+ throw new Unemittable(`OP_${OP_NAMES[op!] ?? String(op)}`)
1724
+ }
1725
+ }
1726
+
1727
+ const ruleEntries: string[] = []
1728
+ for (const [rname, entryIp] of Object.entries(prog.rules)) {
1729
+ // `_r_<Name>` — the composition surface, in `codegen.ts`'s own spelling and
1730
+ // deliberately NOT namespaced, so a sibling calls it by name.
1731
+ const target = link(entryIp)
1732
+ const rn = `_r_${rname.replace(/[^A-Za-z0-9_$]/g, '_')}`
1733
+ bodies.push(`const ${rn}=${target}`)
1734
+ ruleEntries.push(`${q(rname)}:${rn}`)
1735
+ }
1736
+ // Sites the SCAN pool and the scan-skip sets reference. They are linked
1737
+ // through `subtreeComb` outside the emitted scope, so they need names.
1738
+ const extra: string[] = []
1739
+ for (const ip of extraIps) extra.push(`${ip}:${link(ip)}`)
1740
+
1741
+ // The per-label trivia scans sit AFTER the hoisted pool they close over: they
1742
+ // are `function` declarations, so a body may call one that is textually below
1743
+ // it, but the `const _ts<N>` each one reads must be initialised before any
1744
+ // parse runs, not merely before the declaration is evaluated.
1745
+ const source = `${RUNTIME_PRELUDE}
1746
+ ${prelude.join('\n')}
1747
+ ${skipDefs.join('\n')}
1748
+ ${bodies.join('\n')}
1749
+ function _begin(ctx){
1750
+ const host=ctx.build
1751
+ if(_pfDepth>0)_pfFrames.push([_pfScan,_pfHost,EC.e])
1752
+ _pfDepth++
1753
+ _pfScan=null
1754
+ _pfHost=host
1755
+ }
1756
+ function _finish(){
1757
+ if(_pfDepth<=0)throw new Error('parseman emitted table assembly frame underflow')
1758
+ _pfDepth--
1759
+ if(_pfDepth===0)return
1760
+ const prior=_pfFrames.pop()
1761
+ _pfScan=prior[0]
1762
+ _pfHost=prior[1]
1763
+ EC.e=prior[2]
1764
+ }
1765
+ return{
1766
+ pieces:{${ruleEntries.join(',')}},
1767
+ byIp:{${extra.join(',')}},
1768
+ end:function(){return EC.e},
1769
+ begin:_begin,
1770
+ finish:_finish
1771
+ }`
1772
+
1773
+ return {
1774
+ source, reached, masks, classes, armExpected,
1775
+ plan: { classes: classPlan, armExpected: armExpectedPlan, masks: maskPlan },
1776
+ }
1777
+ }