parseman 0.46.0 → 0.47.0

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