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,855 @@
1
+ import type { Combinator, ParseContext, ParseResult } from '../types.ts'
2
+ import { classifiedTrivia, trivia as triviaOf } from '../combinators/map.ts'
3
+ import { fastTriviaScanner, type FastTriviaScanner } from '../combinators/trivia-skip.ts'
4
+ import { regex } from '../combinators/regex.ts'
5
+ import { choice } from '../combinators/choice.ts'
6
+ import { many } from '../combinators/repeat.ts'
7
+ import type { EmittedFactory, PoolPlan } from './emit-assembly.ts'
8
+
9
+ /**
10
+ * One assembly the BUILD already compiled, so the run does not have to.
11
+ *
12
+ * `key` is `cfgKey(RunCfg)` — the scalar option bits an assembly is specialised
13
+ * for. `factory` is the emitted scope as a real function literal, taking
14
+ * `EMITTED_PARAMS` in order. `plan` rebuilds the three data pools the factory
15
+ * expects (see `PoolPlan`), and `reached` is the emitter's site set, which
16
+ * `Assembly.reached` publishes and `test/unit/table-assemble.test.ts`
17
+ * asserts on.
18
+ */
19
+ export type PrecompiledAssembly = {
20
+ readonly key: number
21
+ readonly factory: EmittedFactory
22
+ readonly plan: PoolPlan
23
+ readonly reached: readonly number[]
24
+ }
25
+
26
+ /**
27
+ * The emitted form of a grammar under the table lowering.
28
+ *
29
+ * This object is ALL a grammar contributes to a bundle. Every line of
30
+ * recognition logic lives in `exec.ts`, which ships once in the runtime and is
31
+ * shared by every grammar and every variant. A rule costs its row, not a
32
+ * function.
33
+ *
34
+ * Serialized shape is deliberately JSON-ish (numbers, strings, and a pool of
35
+ * author callbacks) so the emitter can print it as a literal.
36
+ */
37
+ export type TableProgram = {
38
+ /** Flat instruction stream. See `ops.ts` for operand layouts. */
39
+ readonly code: readonly number[]
40
+ /** Const pool: literal strings and STICKY regexes. */
41
+ readonly k: readonly unknown[]
42
+ /** Author callbacks: `transform`/`leaf` reducers and `node()` builders. */
43
+ readonly fns: readonly unknown[]
44
+ /**
45
+ * Char classes, one string per class, as consecutive `[lo, hi]` code-point
46
+ * pairs. Compact in source and expanded to an ASCII lookup at load.
47
+ */
48
+ readonly cc: readonly string[]
49
+ /** Expected-set pool for failure reporting; indices appear on terminals. */
50
+ readonly fx: readonly (readonly string[])[]
51
+ /** Dispatch tables: per choice, the arms' char-class indices (−1 = no gate). */
52
+ readonly disp: readonly (readonly number[])[]
53
+ /**
54
+ * `dispatch()` tables. Deliberately plain arrays of strings and numbers rather
55
+ * than prepared `Map`s: the emitted module has to be able to PRINT this, and a
56
+ * Map in the const pool would fail `emitConst` closed. The Maps are built once
57
+ * at `resolveTable`, exactly like the char-class ASCII lookups.
58
+ */
59
+ readonly dsp: readonly DispatchSpec[]
60
+ /** Rule name → entry offset in `code`. */
61
+ readonly rules: Readonly<Record<string, number>>
62
+ /**
63
+ * 1 when this table's terminals are the line-tracking rows. It is TABLE DATA,
64
+ * read once when the rule map is built, so the entry wrapper is chosen at load
65
+ * rather than branched on per parse.
66
+ */
67
+ readonly lines?: 0 | 1
68
+ /**
69
+ * The grammar trivia's kind labels, and whether it is root-classified.
70
+ *
71
+ * `run()` reads these off the ENTRY (`triviaKindLabelsFromRunnable`,
72
+ * run.ts:220) and takes the `typeof r === 'function'` branch for a compiled
73
+ * entry, which codegen stamps with `_meta`. A table entry is also a function,
74
+ * so without carrying and stamping these, `run({ rootTrivia })` throws
75
+ * "requires labeled grammar trivia" for a grammar that plainly has them.
76
+ */
77
+ readonly labels?: readonly string[]
78
+ readonly classified?: 0 | 1
79
+ /**
80
+ * Grammar-level ambient `scanSkip` sets, as SUBTREE REFERENCES. Same
81
+ * entry-metadata trap as `labels`: `run.ts:270` reads
82
+ * `entry._meta.grammarScanSkip` only for a NON-function entry, so a table entry
83
+ * never got it and `ctx.scanSkip` stayed empty — silently changing what
84
+ * `scanTo`/`balanced` skip over.
85
+ *
86
+ * WAS a list of live combinators: unemittable, and not even listed in
87
+ * `runtimeOnly`, so a module would have parsed with an empty skip list. It was
88
+ * sound only because `scanTo()`/`balanced()` — the sole readers of
89
+ * `ctx.scanSkip` — were themselves emit-blocked. They no longer are, so this is
90
+ * data.
91
+ *
92
+ * A POOL, not one set, and `scanSkipOf` says which rules carry which. The set is
93
+ * a property of the RULE, not of the program: `rules({ scanSkip }, …)` stamps
94
+ * `_meta.grammarScanSkip` on each of ITS rules
95
+ * (`src/combinators/parser.ts:210`), a `parser()` scope has no `scanSkip` field
96
+ * at all, and `run()` installs the set belonging to the ENTRY rule
97
+ * (`src/combinators/grammar.ts:203`). In a `composeLeaf` grammar the pieces do
98
+ * not agree — 67 of jess's 195 css rules carry no ambient set — so installing
99
+ * one program-wide set gave those entries a skip list the interpreter does not
100
+ * give them.
101
+ */
102
+ readonly scanSkip?: readonly (readonly SubtreeRef[])[]
103
+ /**
104
+ * Per rule, the index into `scanSkip` its entry installs, or −1. Parallel to
105
+ * `Object.keys(rules)`.
106
+ */
107
+ readonly scanSkipOf?: readonly number[]
108
+ /** Scan specs, referenced by index from `OP_SCAN`. */
109
+ readonly scans?: readonly ScanSpec[]
110
+ /**
111
+ * The host mode this table was BUILT for.
112
+ *
113
+ * `run()` reads it off the entry via `FUSED_HOST_MODE` and
114
+ * `assertHostModeCompatible` throws when a `'cst'` artifact is run without a
115
+ * CST host. Without the stamp a `hostMode: 'cst'` table returned the
116
+ * grammar's own AST objects with `ok: true` while paying full CST capture —
117
+ * the compiled engine throws on the same input.
118
+ */
119
+ readonly hostMode?: 'ast' | 'cst'
120
+ /**
121
+ * Why this program cannot be EMITTED, if it cannot. Populated at encode time
122
+ * so the failure names the construct instead of surfacing as a generic
123
+ * "cannot serialise [object Object]" from deep inside the printer.
124
+ */
125
+ readonly runtimeOnly?: readonly string[]
126
+ /**
127
+ * Trivia specs, referenced by index from `OP_SCOPE` and from the rule entries.
128
+ *
129
+ * `classifiedTrivia()` is `trivia(oneOrMore(choice(label(name, arm)…)))` with
130
+ * regex arms — entirely structural, so it lowers to DATA and is rebuilt at
131
+ * load with the shared `classifiedTrivia`/`trivia`/`regex`. That keeps the one
132
+ * trivia implementation (labels, root log, cst log, masks, the fast scanner)
133
+ * instead of growing a second one over the table, while making the program
134
+ * printable. Pooling the live combinator was what broke emit for every
135
+ * `rules({ trivia }, …)` grammar.
136
+ */
137
+ readonly triviaSpecs?: readonly TriviaSpec[]
138
+ /**
139
+ * 1 when this table was encoded with `TableSettings.recovery` — the sequences
140
+ * carry their inferred follow-set classes and the repetitions their item
141
+ * expected-set and separator sentinel class.
142
+ *
143
+ * TABLE DATA, read once when the driver/assembly is built. It selects the
144
+ * recovery pieces; it is not a per-parse option, and a recovery table still
145
+ * recovers only when a parse sets `ctx._tolerant`.
146
+ */
147
+ readonly rec?: 0 | 1
148
+ /**
149
+ * THE GRAMMAR-COVERAGE DEFINITION POOL, present only on a table encoded with
150
+ * `TableSettings.coverage`. `[id, kind]` per definition, in the order
151
+ * `buildGrammarPlan` produces (sorted by id), and `OP_COV`'s `id` operand
152
+ * indexes it.
153
+ *
154
+ * DEFINITIONS SHIP AS TABLE DATA rather than being scraped back out of the
155
+ * emitted text. The source lowering's macro path recovers its denominator by
156
+ * regex-scanning the generated hooks (`plugin/index.ts` `emittedCoverageDefinitions`),
157
+ * which it has to do because its IDs only exist inside generated statements. A
158
+ * table has no statements, so there is nothing to scan — and an empty scrape is
159
+ * NO MEASUREMENT reported as 100% coverage, which is the exact failure
160
+ * `'coverage-definitions-unavailable'` was declared for. Carrying the pool is
161
+ * what lets `compileRuleMap` hand back `coverageDefinitions` directly.
162
+ *
163
+ * THE POOL IS THE WHOLE DENOMINATOR, not merely the instrumented sites. A
164
+ * definition with no `OP_COV` row can never be hit, so it drags the ratio DOWN.
165
+ * That is the direction this project requires an instrumentation gap to fail in.
166
+ */
167
+ readonly cov?: readonly (readonly [id: string, kind: 0 | 1 | 2 | 3])[]
168
+ /**
169
+ * Optional emitted-factory inventory, keyed by `cfgKey(RunCfg)`.
170
+ *
171
+ * Normal `compile()` and macro output both carry an explicit empty inventory
172
+ * (`a:[]`) and select the compact closure assembler. Presence of this field
173
+ * permanently disables runtime `Function` construction: an unserved option
174
+ * set uses closures. A missing field is reserved for an explicit low-level
175
+ * hand-built `tableRules(program)` caller, never a compiler-created artifact.
176
+ */
177
+ readonly asm?: readonly PrecompiledAssembly[]
178
+ }
179
+
180
+ /**
181
+ * The one compact artifact shape produced by the table compiler.
182
+ *
183
+ * Compiler and macro artifacts are explicitly closure-backed: an empty
184
+ * inventory is materially different from an absent one, which is reserved for
185
+ * a caller that deliberately hands `tableRules()` a live runtime program.
186
+ */
187
+ export function closureArtifact(prog: TableProgram): TableProgram {
188
+ return { ...prog, asm: [] }
189
+ }
190
+
191
+ /**
192
+ * `GrammarCoverageDefinition['kind']` as the small integer `prog.cov` carries,
193
+ * and back. Two functions rather than a shared array literal so the mapping is
194
+ * exhaustive over the union in BOTH directions — a fifth kind added to
195
+ * `grammar-coverage-ids.ts` fails to compile here instead of silently encoding
196
+ * as `undefined` and reading back as `'rule'`.
197
+ */
198
+ export function covKindCode(kind: 'rule' | 'choice-arm' | 'dispatch-arm' | 'label'): 0 | 1 | 2 | 3 {
199
+ switch (kind) {
200
+ case 'rule': return 0
201
+ case 'choice-arm': return 1
202
+ case 'dispatch-arm': return 2
203
+ case 'label': return 3
204
+ }
205
+ }
206
+
207
+ export function covKindName(code: 0 | 1 | 2 | 3): 'rule' | 'choice-arm' | 'dispatch-arm' | 'label' {
208
+ switch (code) {
209
+ case 0: return 'rule'
210
+ case 1: return 'choice-arm'
211
+ case 2: return 'dispatch-arm'
212
+ case 3: return 'label'
213
+ }
214
+ }
215
+
216
+ /** The pool read back as the public definition shape `compiledGrammarCoverageDefinitions` validates. */
217
+ export function covDefinitions(prog: TableProgram): readonly { id: string; kind: 'rule' | 'choice-arm' | 'dispatch-arm' | 'label' }[] {
218
+ return (prog.cov ?? []).map(([id, kind]) => ({ id, kind: covKindName(kind) }))
219
+ }
220
+
221
+ /**
222
+ * A reference to a table SUBTREE, standing in for a combinator.
223
+ *
224
+ * `[ip, cls]`: `ip` is the subtree's offset in `code`; `cls` indexes its first set
225
+ * in `cc`, −1 for `any` and −2 for `empty`. The first set is carried because
226
+ * `buildBalancedInterior` reads each skipper's own first set to decide whether its
227
+ * content run can be a bounded regex (`src/combinators/scanTo.ts:280`) — a
228
+ * reference that reported `any` would silently rebuild a DIFFERENT, slower
229
+ * interior than the grammar's own.
230
+ */
231
+ export type SubtreeRef = readonly [ip: number, cls: number]
232
+
233
+ /**
234
+ * A scanning construct as its CONSTRUCTOR ARGUMENTS.
235
+ *
236
+ * `kind` 0 is `scanTo`, 1 is `balanced`. `flags` bit 0 = `raw`, bit 1 = `orEOF`
237
+ * (scanTo), bit 2 = `strict` (balanced). `sent` is the sentinel's literal text
238
+ * when it is a `literal()`, so the rebuilt scan reports the same expected set as
239
+ * the interpreter, and `null` otherwise (which reports `"sentinel"`).
240
+ */
241
+ export type ScanSpec = {
242
+ readonly kind: 0 | 1
243
+ readonly flags: number
244
+ readonly skip: readonly SubtreeRef[]
245
+ readonly sentinel?: SubtreeRef
246
+ readonly sent?: string | null
247
+ readonly open?: string
248
+ readonly close?: string
249
+ }
250
+
251
+ /** A trivia combinator as data. `arms` empty means a plain `trivia(regex)`. */
252
+ export type TriviaSpec = {
253
+ /** `[label, regexSource, regexFlags]` per classified arm. */
254
+ readonly arms: readonly (readonly [string, string, string])[]
255
+ /** Set for a plain `trivia(regex)`: `[source, flags]`. */
256
+ readonly plain?: readonly [string, string]
257
+ /**
258
+ * Set for an UNLABELLED alternation body — `trivia(oneOrMore(choice(ws,
259
+ * comment)))`, which is what a grammar that does not need trivia CATEGORIES
260
+ * writes and what `examples/css/parser.ts` has always written. `[source,
261
+ * flags]` per arm, in order, because a PEG choice is ordered.
262
+ *
263
+ * Distinct from `arms` rather than folded into it with a synthetic label:
264
+ * `classifiedTrivia()` puts its labels in the CST trivia log, so a made-up name
265
+ * would appear in a consumer's tree for a category the grammar never declared.
266
+ */
267
+ readonly alts?: readonly (readonly [string, string])[]
268
+ /** The `alts` repetition floor — 1 for `oneOrMore`, 0 for `many`. */
269
+ readonly min?: number
270
+ /**
271
+ * A trivia combinator whose shape this encoder cannot express as data, kept
272
+ * live so the program still RUNS. Its presence is recorded in `runtimeOnly`,
273
+ * so emit refuses and names it rather than failing inside the printer.
274
+ */
275
+ readonly live?: unknown
276
+ }
277
+
278
+ /**
279
+ * The wire form an artifact actually prints — same table, short keys. Kept
280
+ * separate from `TableProgram` so the encoder stays readable and the emitted
281
+ * module stays small.
282
+ */
283
+ export type CompactProgram = {
284
+ readonly c: readonly number[]
285
+ readonly k: readonly unknown[]
286
+ readonly x: readonly string[]
287
+ readonly e: readonly (readonly string[])[]
288
+ readonly d: readonly (readonly number[])[]
289
+ readonly r: Readonly<Record<string, number>>
290
+ readonly f: readonly unknown[]
291
+ readonly l?: 0 | 1
292
+ readonly p?: readonly DispatchSpec[]
293
+ readonly lb?: readonly string[]
294
+ readonly rc?: 0 | 1
295
+ readonly h?: 'ast' | 'cst'
296
+ readonly tv?: readonly TriviaSpec[]
297
+ readonly sc?: readonly ScanSpec[]
298
+ readonly ss?: readonly (readonly SubtreeRef[])[]
299
+ readonly so?: readonly number[]
300
+ readonly rv?: 0 | 1
301
+ readonly cv?: readonly (readonly [string, 0 | 1 | 2 | 3])[]
302
+ /** Pre-compiled assemblies — see `TableProgram.asm`. */
303
+ readonly a?: readonly PrecompiledAssembly[]
304
+ }
305
+
306
+ export function expandCompact(p: TableProgram | CompactProgram): TableProgram {
307
+ if ('code' in p) return p
308
+ return ownTableProgram({
309
+ code: p.c, k: p.k, cc: p.x, fx: p.e, disp: p.d, rules: p.r, fns: p.f,
310
+ lines: p.l ?? 0, dsp: p.p ?? [],
311
+ ...(p.lb === undefined ? {} : { labels: p.lb }),
312
+ ...(p.rc === undefined ? {} : { classified: p.rc }),
313
+ ...(p.h === undefined ? {} : { hostMode: p.h }),
314
+ ...(p.tv === undefined ? {} : { triviaSpecs: p.tv }),
315
+ ...(p.sc === undefined ? {} : { scans: p.sc }),
316
+ ...(p.ss === undefined ? {} : { scanSkip: p.ss }),
317
+ ...(p.so === undefined ? {} : { scanSkipOf: p.so }),
318
+ ...(p.rv === undefined ? {} : { rec: p.rv }),
319
+ ...(p.cv === undefined ? {} : { cov: p.cv }),
320
+ ...(p.a === undefined ? {} : { asm: p.a }),
321
+ })
322
+ }
323
+
324
+ /** One `dispatch()`'s routing tables, in serialisable form. */
325
+ export type DispatchSpec = {
326
+ /** Exact keys, parallel to `keyArm`. */
327
+ readonly key: readonly string[]
328
+ readonly keyArm: readonly number[]
329
+ /** ASCII-folded keys for case-insensitive cases, parallel to `foldArm`. */
330
+ readonly fold: readonly string[]
331
+ readonly foldArm: readonly number[]
332
+ /**
333
+ * `[kind, value, flags, arm]` per matcher. `kind` is 0/1/2 for
334
+ * startsWith/endsWith/matches, and 3/4 for the ASCII-FOLDED startsWith/endsWith
335
+ * a `{ caseInsensitive: true }` matcher arm encodes to — `value` is pre-folded
336
+ * for those and the driver folds the selector key before comparing. A
337
+ * case-insensitive `matches` needs no kind of its own: it folds into the
338
+ * regex's own `i` flag.
339
+ */
340
+ readonly match: readonly (readonly [number, string, string, number])[]
341
+ /** 1 when that arm consumes the routed token. */
342
+ readonly routed: readonly number[]
343
+ /** Expected set when nothing matches — every case key, JSON-quoted. */
344
+ readonly expected: readonly string[]
345
+ }
346
+
347
+ export type ResolvedDispatchSpec = {
348
+ readonly byKey: ReadonlyMap<string, number>
349
+ readonly byFold: ReadonlyMap<string, number>
350
+ readonly match: readonly (readonly [number, string, string, number])[]
351
+ readonly routed: readonly number[]
352
+ readonly expected: readonly string[]
353
+ }
354
+
355
+ /** A char class expanded for execution: O(1) for ASCII, ranges above it. */
356
+ export type ResolvedClass = {
357
+ readonly ascii: Uint8Array
358
+ readonly hi: readonly number[]
359
+ }
360
+
361
+ /** A dispatch table expanded for execution. */
362
+ export type ResolvedDispatch = {
363
+ /** charCode → arm index + 1, 0 = no arm claims it. Read only when `exclusive`. */
364
+ readonly ascii: Uint8Array
365
+ /** `[lo, hi, arm]` triples for code points ≥ 128. Read only when `exclusive`. */
366
+ readonly hi: readonly number[]
367
+ /** Arms with no gate at all — tried in order after a dispatch miss. */
368
+ readonly open: readonly number[]
369
+ /**
370
+ * The arms' OWN classes, positionally — `null` where the arm is nullable or
371
+ * its first set does not map to a class, and so cannot be gated.
372
+ *
373
+ * This is the per-arm gate, and it is what the compiled engine has always had:
374
+ * `emitFirstMatch` guards each arm on its own first set, which is sound
375
+ * whatever the other arms do. The table used to demand that ALL arms be
376
+ * disjoint, non-nullable and mappable before it would gate ANY of them, so a
377
+ * single unmappable arm made the whole site speculative — 98.6% of arm entries
378
+ * on `benchmark.less` were at such sites, and 88.8% of those failed.
379
+ */
380
+ readonly armCls: readonly (ResolvedClass | null)[]
381
+ /**
382
+ * True when the arms' classes are pairwise disjoint and every arm has one, so
383
+ * "the first arm whose class holds this char" IS "the first arm that matches"
384
+ * and the O(1) `ascii`/`hi` selection is sound. When false the arms are tried
385
+ * in order, each skipped by its own `armCls` entry — same order, same result,
386
+ * fewer entries.
387
+ *
388
+ * Computed from the RESOLVED classes rather than read off the encoder's
389
+ * `disjointSets` on first sets: the classes are what selection actually reads,
390
+ * so testing them is both tighter and the property that has to hold.
391
+ */
392
+ readonly exclusive: boolean
393
+ }
394
+
395
+ /**
396
+ * The `(grammar, settings)` reference table, built ONCE and cached.
397
+ *
398
+ * This is the object G5 names: the run path reads it and never branches on an
399
+ * option. Two settings pairs produce two of these; the driver is the same code.
400
+ */
401
+ export type ResolvedTable = {
402
+ readonly prog: TableProgram
403
+ readonly code: Int32Array
404
+ readonly k: readonly unknown[]
405
+ readonly fns: readonly unknown[]
406
+ readonly cc: readonly ResolvedClass[]
407
+ readonly fx: readonly (readonly string[])[]
408
+ readonly disp: readonly ResolvedDispatch[]
409
+ readonly dsp: readonly ResolvedDispatchSpec[]
410
+ /** Trivia combinators rebuilt from `triviaSpecs`, once per table. */
411
+ readonly trivia: readonly Combinator<unknown>[]
412
+ /**
413
+ * THE TRIVIA LEAF SWAP (G5: "some swaps on rules or sub-rules (leafs)").
414
+ *
415
+ * Each trivia entry's SPECIALISED scanner, resolved here rather than looked up
416
+ * per sequence term, and `null` where the shape has no lowering. Parallel to
417
+ * `trivia`, so a scope installs both by the same index.
418
+ */
419
+ readonly triviaScan: readonly (FastTriviaScanner | null)[]
420
+ /** Per trivia entry: does it carry kind labels? Decided here, never per parse. */
421
+ readonly triviaLabelled: readonly boolean[]
422
+ readonly rules: Readonly<Record<string, number>>
423
+ }
424
+
425
+ /**
426
+ * Runtime state owned by a table object from the moment that object is made.
427
+ * The symbol stays out of wire data; the nested cell lets resolution fill the
428
+ * cache without changing the program object's V8 shape.
429
+ */
430
+ const TABLE_RUNTIME = Symbol('parseman.tableRuntime')
431
+ type RuntimeTableProgram = TableProgram & {
432
+ readonly [TABLE_RUNTIME]?: { resolved: ResolvedTable | undefined }
433
+ }
434
+
435
+ /** Give an internally-created program its fixed-shape runtime owner. */
436
+ export function ownTableProgram(prog: TableProgram, resolved?: ResolvedTable): TableProgram {
437
+ return { ...prog, [TABLE_RUNTIME]: { resolved } } as RuntimeTableProgram
438
+ }
439
+
440
+ /** A compiled entry, shaped exactly like a codegen rule function. */
441
+ export type TableRule = (input: string, pos: number, ctx: ParseContext) => ParseResult<unknown>
442
+
443
+ // A valid range always has lo <= hi, so this pair cannot be the first encoded
444
+ // range. It marks the wide format without stealing or escaping any BMP code
445
+ // point (notably U+FFFF, which appears often in generated negated classes).
446
+ const WIDE_CLASS_PREFIX_LO = 0xffff
447
+ const WIDE_CLASS_PREFIX_HI = 0
448
+
449
+ /** Fail closed on a class range before it reaches either wire format. */
450
+ function validateClassRange(lo: number, hi: number): void {
451
+ for (const cp of [lo, hi]) {
452
+ if (!Number.isInteger(cp) || cp < 0 || cp > 0x10ffff) {
453
+ throw new TypeError(`table char-class endpoint is outside Unicode: ${cp}`)
454
+ }
455
+ }
456
+ if (lo > hi) {
457
+ throw new TypeError(`table char-class range is descending: ${lo} > ${hi}`)
458
+ }
459
+ }
460
+
461
+ /** Encode consecutive `[lo, hi]` code-point pairs without truncating astral endpoints. */
462
+ export function encodeClassSpec(ranges: readonly { lo: number; hi: number }[]): string {
463
+ let wide = false
464
+ for (const range of ranges) {
465
+ validateClassRange(range.lo, range.hi)
466
+ if (range.hi > 0xffff) wide = true
467
+ }
468
+ if (!wide) {
469
+ let spec = ''
470
+ for (const range of ranges) spec += String.fromCharCode(range.lo, range.hi)
471
+ return spec
472
+ }
473
+
474
+ let spec = String.fromCharCode(WIDE_CLASS_PREFIX_LO, WIDE_CLASS_PREFIX_HI)
475
+ for (const range of ranges) {
476
+ spec += String.fromCharCode(
477
+ range.lo >>> 16, range.lo & 0xffff,
478
+ range.hi >>> 16, range.hi & 0xffff,
479
+ )
480
+ }
481
+ return spec
482
+ }
483
+
484
+ /** Decode the consecutive `[lo, hi]` code-point pairs in a class spec. */
485
+ export function decodeClassSpec(spec: string): Array<{ lo: number; hi: number }> {
486
+ if (spec.charCodeAt(0) === WIDE_CLASS_PREFIX_LO && spec.charCodeAt(1) === WIDE_CLASS_PREFIX_HI) {
487
+ if ((spec.length - 2) % 4 !== 0) throw new TypeError('table char-class spec has a truncated wide range')
488
+ if (spec.length === 2) throw new TypeError('table char-class spec has a wide prefix but no ranges')
489
+ const ranges: Array<{ lo: number; hi: number }> = []
490
+ for (let i = 2; i < spec.length; i += 4) {
491
+ const lo = spec.charCodeAt(i) * 0x10000 + spec.charCodeAt(i + 1)
492
+ const hi = spec.charCodeAt(i + 2) * 0x10000 + spec.charCodeAt(i + 3)
493
+ validateClassRange(lo, hi)
494
+ ranges.push({ lo, hi })
495
+ }
496
+ return ranges
497
+ }
498
+
499
+ if (spec.length % 2 !== 0) throw new TypeError('table char-class spec has an unmatched range endpoint')
500
+ const ranges: Array<{ lo: number; hi: number }> = []
501
+ for (let i = 0; i < spec.length; i += 2) {
502
+ const lo = spec.charCodeAt(i)
503
+ const hi = spec.charCodeAt(i + 1)
504
+ validateClassRange(lo, hi)
505
+ ranges.push({ lo, hi })
506
+ }
507
+ return ranges
508
+ }
509
+
510
+ export function resolveClass(spec: string): ResolvedClass {
511
+ const ascii = new Uint8Array(128)
512
+ const hi: number[] = []
513
+ for (const { lo, hi: up } of decodeClassSpec(spec)) {
514
+ if (lo < 128) for (let c = lo; c <= Math.min(up, 127); c++) ascii[c] = 1
515
+ if (up >= 128) hi.push(Math.max(lo, 128), up)
516
+ }
517
+ return { ascii, hi }
518
+ }
519
+
520
+ export function classHas(cls: ResolvedClass, code: number): boolean {
521
+ if (code < 0) return false
522
+ if (code < 128) return cls.ascii[code] === 1
523
+ const hi = cls.hi
524
+ for (let i = 0; i < hi.length; i += 2) if (code >= hi[i]! && code <= hi[i + 1]!) return true
525
+ return false
526
+ }
527
+
528
+ function resolveDispatch(arms: readonly number[], cc: readonly ResolvedClass[]): ResolvedDispatch {
529
+ const ascii = new Uint8Array(128)
530
+ const hi: number[] = []
531
+ const open: number[] = []
532
+ const armCls: (ResolvedClass | null)[] = []
533
+ // Disjointness over the ASCII plane, accumulated as the arms are laid in: a
534
+ // second arm claiming a char another already claims makes O(1) selection
535
+ // unsound, because "first class that holds c" stops being "first arm that
536
+ // matches". Above ASCII the `hi` ranges are not indexed, so any arm with a
537
+ // high range beyond the first is treated as overlapping — conservative, and
538
+ // the per-arm path below is correct for it either way.
539
+ let exclusive = true
540
+ let hiOwners = 0
541
+ for (let a = 0; a < arms.length; a++) {
542
+ const ci = arms[a]!
543
+ if (ci < 0) { open.push(a); armCls.push(null); exclusive = false; continue }
544
+ const cls = cc[ci]!
545
+ armCls.push(cls)
546
+ for (let c = 0; c < 128; c++) {
547
+ if (cls.ascii[c] !== 1) continue
548
+ if (ascii[c] === 0) ascii[c] = a + 1
549
+ else exclusive = false
550
+ }
551
+ if (cls.hi.length > 0 && ++hiOwners > 1) exclusive = false
552
+ for (let i = 0; i < cls.hi.length; i += 2) hi.push(cls.hi[i]!, cls.hi[i + 1]!, a)
553
+ }
554
+ return { ascii, hi, open, armCls, exclusive }
555
+ }
556
+
557
+ /**
558
+ * Build the reference table for a program. Memoized per program OBJECT, which
559
+ * is the `(grammar, settings)` pair: the emitter produces one program per pair,
560
+ * so two variants are two cache entries and the run path never sees an option.
561
+ */
562
+ /** Rebuild a trivia combinator from its spec using the SHARED constructors. */
563
+ function buildTrivia(spec: TriviaSpec): Combinator<unknown> {
564
+ if (spec.live !== undefined) return spec.live as Combinator<unknown>
565
+ if (spec.plain !== undefined) return triviaOf(regex(new RegExp(spec.plain[0], spec.plain[1])))
566
+ if (spec.alts !== undefined) {
567
+ const [head, ...rest] = spec.alts.map(([source, flags]) => regex(new RegExp(source, flags)))
568
+ if (head === undefined) throw new TypeError('trivia spec has an EMPTY alternation, which matches nothing')
569
+ const body = rest.length === 0 ? head : choice(head, ...rest)
570
+ // `many(x, { min: 1 })` and `oneOrMore(x)` are the IDENTICAL combinator, not
571
+ // merely equivalent — min>=1 routes to the same `atLeast` implementation
572
+ // (combinators/repeat.ts:126-131). So this rebuilds the source shape exactly
573
+ // for both floors and there is no second constructor to keep in step.
574
+ return triviaOf(many(body, { min: spec.min ?? 1 }))
575
+ }
576
+ const arms: Record<string, Combinator<unknown>> = {}
577
+ for (const [name, source, flags] of spec.arms) arms[name] = regex(new RegExp(source, flags))
578
+ return classifiedTrivia(arms)
579
+ }
580
+
581
+ export function resolveTable(prog: TableProgram): ResolvedTable {
582
+ const owner = (prog as RuntimeTableProgram)[TABLE_RUNTIME]
583
+ const hit = owner?.resolved
584
+ if (hit !== undefined) return hit
585
+ const cc = prog.cc.map(resolveClass)
586
+ const triviaBuilt = (prog.triviaSpecs ?? []).map(buildTrivia)
587
+ const built: ResolvedTable = {
588
+ prog,
589
+ code: Int32Array.from(prog.code),
590
+ k: prog.k,
591
+ fns: prog.fns,
592
+ cc,
593
+ fx: prog.fx,
594
+ disp: prog.disp.map(d => resolveDispatch(d, cc)),
595
+ trivia: triviaBuilt,
596
+ triviaScan: triviaBuilt.map(fastTriviaScanner),
597
+ triviaLabelled: triviaBuilt.map(t => t._meta.triviaKindLabels !== undefined),
598
+ dsp: prog.dsp.map(d => ({
599
+ byKey: new Map(d.key.map((x, i) => [x, d.keyArm[i]!])),
600
+ byFold: new Map(d.fold.map((x, i) => [x, d.foldArm[i]!])),
601
+ match: d.match,
602
+ routed: d.routed,
603
+ expected: d.expected,
604
+ })),
605
+ rules: prog.rules,
606
+ }
607
+ if (owner !== undefined) owner.resolved = built
608
+ return built
609
+ }
610
+
611
+ /* ── THE VARIANT FOLD (ledger row G4) ────────────────────────────────────── */
612
+
613
+ /**
614
+ * One variant's whole difference from the base table, as SPARSE ROW EDITS.
615
+ *
616
+ * G5 says the variants *"differ in table contents, not in code"*, and G4 asks
617
+ * for ONE compiled output per input grammar. Measured across jess's four
618
+ * dialects, the four `trackLines` x `hostMode` tables differ in `code` and in
619
+ * two scalars and in NOTHING else — `k`, `cc`, `fx`, `disp`, `dsp`, `rules`,
620
+ * `fns`, `labels`, `classified`, `scanSkip`, `scanSkipOf`, `scans` and
621
+ * `triviaSpecs` are byte-identical in all four, and the code streams are the
622
+ * same LENGTH. So a variant is not another table. It is a list of words to
623
+ * overwrite.
624
+ *
625
+ * `at` is stored as ASCENDING GAPS, not absolute offsets: the edits are dense
626
+ * enough (mean gap under 10 on every dialect) that gaps are one or two digits
627
+ * where offsets are four, and the load-time cost of the difference is one `+=`.
628
+ */
629
+ export type TableDelta = {
630
+ /** Gaps between successive edited word offsets; the first is absolute. */
631
+ readonly at: readonly number[]
632
+ /** Replacement words, parallel to `at`. */
633
+ readonly to: readonly number[]
634
+ readonly lines?: 0 | 1
635
+ readonly hostMode?: 'ast' | 'cst'
636
+ }
637
+
638
+ /** A base table plus the row edits that turn it into each named variant. */
639
+ export type FoldedProgram = {
640
+ readonly base: TableProgram
641
+ /** Variant name → its edits. The base's own name maps to an empty delta. */
642
+ readonly variants: Readonly<Record<string, TableDelta>>
643
+ }
644
+
645
+ const FOLDED_RUNTIME = Symbol('parseman.foldedRuntime')
646
+ type RuntimeFoldedProgram = FoldedProgram & {
647
+ readonly [FOLDED_RUNTIME]?: { byName: Map<string, TableProgram> }
648
+ }
649
+
650
+ function ownFoldedProgram(
651
+ base: TableProgram,
652
+ variants: Readonly<Record<string, TableDelta>>,
653
+ ): FoldedProgram {
654
+ return { base, variants, [FOLDED_RUNTIME]: { byName: new Map() } } as RuntimeFoldedProgram
655
+ }
656
+
657
+ /** The wire form a folded artifact prints — short keys, as `CompactProgram`. */
658
+ export type CompactFolded = {
659
+ readonly b: CompactProgram
660
+ readonly v: Readonly<Record<string, { a?: readonly number[]; t?: readonly number[]; l?: 0 | 1; h?: 'ast' | 'cst' }>>
661
+ }
662
+
663
+ /**
664
+ * The fields a fold requires to be IDENTICAL across every variant.
665
+ *
666
+ * Named rather than derived so the refusal can say which one broke.
667
+ */
668
+ const SHARED_FIELDS = [
669
+ 'k', 'cc', 'fx', 'disp', 'dsp', 'rules', 'labels', 'classified',
670
+ 'scanSkip', 'scanSkipOf', 'scans', 'triviaSpecs', 'runtimeOnly',
671
+ // `rec` is a property of the GRAMMAR BUILD, not of the trackLines/hostMode
672
+ // axis a fold varies — every variant of one export is encoded with the same
673
+ // recovery setting, and a mismatch is two tables, which is what the refusal says.
674
+ 'rec',
675
+ // `cov` is a property of the GRAMMAR and its coverage plan, for the same reason
676
+ // `rec` is a property of the build: the plan is built from the rule map, which
677
+ // every variant of one export shares. It ships ONCE, and a variant that
678
+ // disagrees is two tables — which is what the refusal says.
679
+ 'cov',
680
+ ] as const satisfies readonly (keyof TableProgram)[]
681
+
682
+ /**
683
+ * Every field of `TableProgram`, classified: shared once, compared by identity
684
+ * (`fns`), overwritten per variant (`code`), or carried on the delta as a
685
+ * scalar (`lines`, `hostMode`).
686
+ *
687
+ * `FoldExhaustive` fails to COMPILE the moment a field is added to
688
+ * `TableProgram` without a decision, which is the only way this list can be kept
689
+ * honest. A new variant-dependent field landing in `SHARED_FIELDS`' blind spot
690
+ * would ship one variant's data under every variant's name, and every parse
691
+ * would succeed.
692
+ */
693
+ /**
694
+ * `asm` is classified as DROPPED, and it is the only field in that class.
695
+ *
696
+ * A pre-compiled assembly is emitted FROM a `code` stream, and `code` is exactly
697
+ * what a fold overwrites per variant — so a base assembly is wrong for every
698
+ * variant but the base, and silently so: it would parse, and parse the base
699
+ * grammar. Folding therefore happens BEFORE pre-compiling, and `foldPrograms`
700
+ * refuses an input that already carries one rather than dropping it quietly.
701
+ */
702
+ type FoldDropped = 'asm'
703
+ type FoldClassified = 'code' | 'fns' | 'lines' | 'hostMode' | FoldDropped | (typeof SHARED_FIELDS)[number]
704
+ type AssertNever<T extends never> = T
705
+ export type FoldExhaustive = AssertNever<Exclude<keyof TableProgram, FoldClassified>>
706
+
707
+ /**
708
+ * Fold N encoded programs into ONE table plus per-variant row edits.
709
+ *
710
+ * FAILS CLOSED, and that is the point. Every assumption the fold rests on is
711
+ * asserted here against the actual programs, so a grammar or a setting that
712
+ * breaks one produces a build error naming the field — not an artifact that
713
+ * loads and parses with the wrong table. The alternative, trusting a measurement
714
+ * taken once on four grammars, is how a variant axis acquires a silent third
715
+ * dimension.
716
+ *
717
+ * `fns` is compared by IDENTITY, not by source text. Two closures can print the
718
+ * same and capture different scopes; sharing one pool between variants is only
719
+ * sound when they are literally the same functions, which they are when the
720
+ * variants are encodings of ONE grammar export.
721
+ */
722
+ export function foldPrograms(
723
+ programs: Readonly<Record<string, TableProgram>>,
724
+ baseName: string,
725
+ ): FoldedProgram {
726
+ const suppliedBase = programs[baseName]
727
+ if (suppliedBase === undefined) throw new TypeError(`foldPrograms: no program named ${JSON.stringify(baseName)}`)
728
+ // The in-memory fold and its emitted form must be the same artifact: the
729
+ // emitter prints `a:[]`, so preserve that policy when a caller folds tables
730
+ // directly instead of loading a module.
731
+ const base = closureArtifact(suppliedBase)
732
+ const variants: Record<string, TableDelta> = {}
733
+ for (const name of Object.keys(programs)) {
734
+ const p = programs[name]!
735
+ // See `FoldDropped`. Refused rather than dropped: a base assembly carried
736
+ // onto a variant parses the BASE grammar under the variant's name, which no
737
+ // test that only asks "did it parse" would ever see.
738
+ if (p.asm !== undefined && p.asm.length > 0) {
739
+ throw new TypeError(
740
+ `foldPrograms: program ${JSON.stringify(name)} already carries pre-compiled assemblies. `
741
+ + 'An assembly is emitted from a `code` stream and a fold overwrites `code` per variant, so '
742
+ + 'pre-compiling must happen AFTER the fold, per variant — not before it.',
743
+ )
744
+ }
745
+ /**
746
+ * AN EMPTY `asm` IS NOT AN ASSEMBLY, it is the artifact saying "a build made
747
+ * me" — the flag that switches the runtime `Function` constructor off. It
748
+ * carries through the fold, because a folded artifact is no less a build
749
+ * product than an unfolded one. `unfoldVariant` spreads the base, so the
750
+ * variant inherits it.
751
+ *
752
+ * This mattered the moment `65fc9a4` moved `fold.ts` off the bytecode
753
+ * interpreter onto `tableRules`: the interpreter never evaluated
754
+ * anything, so the fold path was trivially clean, and swapping the driver
755
+ * silently handed every folded artifact a `new Function` on its first parse.
756
+ * `test/unit/no-function-constructor.test.ts` caught it on the merge.
757
+ */
758
+ if (p.code.length !== base.code.length) {
759
+ throw new TypeError(
760
+ `foldPrograms: variant ${JSON.stringify(name)} has ${p.code.length} code words, base `
761
+ + `${JSON.stringify(baseName)} has ${base.code.length}. A fold overwrites words; it cannot `
762
+ + 'resize the stream. These are two different tables, not one table and its edits.',
763
+ )
764
+ }
765
+ for (const f of SHARED_FIELDS) {
766
+ if (JSON.stringify(p[f]) !== JSON.stringify(base[f])) {
767
+ throw new TypeError(
768
+ `foldPrograms: variant ${JSON.stringify(name)} differs from base ${JSON.stringify(baseName)} `
769
+ + `in ${JSON.stringify(f)}, which the fold ships ONCE. Either the variants are not encodings `
770
+ + 'of one grammar, or this field has become variant-dependent and needs its own delta.',
771
+ )
772
+ }
773
+ }
774
+ if (p.fns.length !== base.fns.length || p.fns.some((f, i) => f !== base.fns[i])) {
775
+ throw new TypeError(
776
+ `foldPrograms: variant ${JSON.stringify(name)} does not share the base's reducer pool BY `
777
+ + 'IDENTITY. Equal source text is not enough — two closures can print alike and capture '
778
+ + 'different scopes. Encode every variant from ONE grammar export.',
779
+ )
780
+ }
781
+ const at: number[] = []
782
+ const to: number[] = []
783
+ let prev = 0
784
+ for (let i = 0; i < p.code.length; i++) {
785
+ if (p.code[i] === base.code[i]) continue
786
+ at.push(i - prev)
787
+ to.push(p.code[i]!)
788
+ prev = i
789
+ }
790
+ variants[name] = {
791
+ at, to,
792
+ ...(p.lines === undefined ? {} : { lines: p.lines }),
793
+ ...(p.hostMode === undefined ? {} : { hostMode: p.hostMode }),
794
+ }
795
+ }
796
+ return ownFoldedProgram(base, variants)
797
+ }
798
+
799
+ /**
800
+ * Materialise ONE variant of a folded table, at load, when it is selected.
801
+ *
802
+ * Every field but `code` is shared BY REFERENCE with the base — no copying, no
803
+ * re-parsing. The resolved table is derived from the base's, so N variants cost
804
+ * ONE char-class expansion, ONE dispatch build and ONE trivia rebuild between
805
+ * them instead of N. Selecting a variant is strictly LESS load work than
806
+ * building an Nth independent table, which is the opposite of the usual
807
+ * size-versus-load-time trade.
808
+ *
809
+ * The driver still sees nothing but a `TableProgram`. There is no variant flag
810
+ * on the parse path and nothing here is consulted again after load — the option
811
+ * half of G5 is untouched.
812
+ */
813
+ export function unfoldVariant(folded: FoldedProgram, name: string): TableProgram {
814
+ const byName = (folded as RuntimeFoldedProgram)[FOLDED_RUNTIME]?.byName
815
+ const hit = byName?.get(name)
816
+ if (hit !== undefined) return hit
817
+ const d = folded.variants[name]
818
+ if (d === undefined) {
819
+ throw new TypeError(
820
+ `unfoldVariant: no variant ${JSON.stringify(name)}. This table carries: `
821
+ + `${Object.keys(folded.variants).map(n => JSON.stringify(n)).join(', ')}.`,
822
+ )
823
+ }
824
+ const base = folded.base
825
+ const code = [...base.code]
826
+ let ip = 0
827
+ for (let i = 0; i < d.at.length; i++) { ip += d.at[i]!; code[ip] = d.to[i]! }
828
+ const raw: TableProgram = {
829
+ ...base,
830
+ code,
831
+ ...(d.lines === undefined ? {} : { lines: d.lines }),
832
+ ...(d.hostMode === undefined ? {} : { hostMode: d.hostMode }),
833
+ }
834
+ // Seeded from the BASE's resolved table: `cc`, `disp`, `dsp` and the rebuilt
835
+ // trivia are identical by `foldPrograms`' own assertion, so N variants cost
836
+ // ONE of each between them. Only the code stream is per variant.
837
+ const prog = ownTableProgram(raw)
838
+ const owner = (prog as RuntimeTableProgram)[TABLE_RUNTIME]!
839
+ owner.resolved = { ...resolveTable(base), prog, code: Int32Array.from(code) }
840
+ byName?.set(name, prog)
841
+ return prog
842
+ }
843
+
844
+ export function expandCompactFolded(f: CompactFolded): FoldedProgram {
845
+ const variants: Record<string, TableDelta> = {}
846
+ for (const name of Object.keys(f.v)) {
847
+ const d = f.v[name]!
848
+ variants[name] = {
849
+ at: d.a ?? [], to: d.t ?? [],
850
+ ...(d.l === undefined ? {} : { lines: d.l }),
851
+ ...(d.h === undefined ? {} : { hostMode: d.h }),
852
+ }
853
+ }
854
+ return ownFoldedProgram(expandCompact(f.b), variants)
855
+ }