parseman 0.46.0 → 0.47.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/README.md +8 -3
  2. package/dist/analysis/choice-cost-render.d.ts +12 -4
  3. package/dist/analysis/choice-cost-render.d.ts.map +1 -1
  4. package/dist/analysis/choice-cost.d.ts +2 -2
  5. package/dist/analysis/choice-cost.d.ts.map +1 -1
  6. package/dist/analysis/commitment.d.ts +131 -0
  7. package/dist/analysis/commitment.d.ts.map +1 -0
  8. package/dist/analysis/diagnostics.cjs +10339 -7787
  9. package/dist/analysis/diagnostics.cjs.map +3 -4
  10. package/dist/analysis/diagnostics.d.ts +2 -2
  11. package/dist/analysis/diagnostics.js +10339 -7787
  12. package/dist/analysis/diagnostics.js.map +3 -4
  13. package/dist/analysis/duplication.d.ts.map +1 -1
  14. package/dist/analysis/fix.d.ts +3 -3
  15. package/dist/analysis/fix.d.ts.map +1 -1
  16. package/dist/analysis/format-number.d.ts +23 -0
  17. package/dist/analysis/format-number.d.ts.map +1 -0
  18. package/dist/analysis/gating.d.ts +39 -2
  19. package/dist/analysis/gating.d.ts.map +1 -1
  20. package/dist/analysis/rebuild.d.ts.map +1 -1
  21. package/dist/analysis/terminal.d.ts +5 -2
  22. package/dist/analysis/terminal.d.ts.map +1 -1
  23. package/dist/cli/index.cjs +10924 -8506
  24. package/dist/cli/index.cjs.map +3 -4
  25. package/dist/cli/index.js +10924 -8506
  26. package/dist/cli/index.js.map +3 -4
  27. package/dist/combinators/adjacency.d.ts +100 -0
  28. package/dist/combinators/adjacency.d.ts.map +1 -0
  29. package/dist/combinators/choice.d.ts.map +1 -1
  30. package/dist/combinators/completions.d.ts.map +1 -1
  31. package/dist/combinators/dispatch.d.ts +8 -1
  32. package/dist/combinators/dispatch.d.ts.map +1 -1
  33. package/dist/combinators/expect.d.ts.map +1 -1
  34. package/dist/combinators/first-set.d.ts.map +1 -1
  35. package/dist/combinators/gate.d.ts.map +1 -1
  36. package/dist/combinators/grammar.d.ts +22 -0
  37. package/dist/combinators/grammar.d.ts.map +1 -1
  38. package/dist/combinators/literal.d.ts +8 -0
  39. package/dist/combinators/literal.d.ts.map +1 -1
  40. package/dist/combinators/map.d.ts +0 -1
  41. package/dist/combinators/map.d.ts.map +1 -1
  42. package/dist/combinators/node.d.ts +13 -3
  43. package/dist/combinators/node.d.ts.map +1 -1
  44. package/dist/combinators/parser.d.ts +1 -1
  45. package/dist/combinators/parser.d.ts.map +1 -1
  46. package/dist/combinators/ref.d.ts.map +1 -1
  47. package/dist/combinators/regex.d.ts +1 -16
  48. package/dist/combinators/regex.d.ts.map +1 -1
  49. package/dist/combinators/repeat.d.ts +30 -2
  50. package/dist/combinators/repeat.d.ts.map +1 -1
  51. package/dist/combinators/scanTo.d.ts +71 -1
  52. package/dist/combinators/scanTo.d.ts.map +1 -1
  53. package/dist/combinators/sequence.d.ts.map +1 -1
  54. package/dist/combinators/token.d.ts.map +1 -1
  55. package/dist/combinators/trivia-boundary.d.ts +26 -0
  56. package/dist/combinators/trivia-boundary.d.ts.map +1 -0
  57. package/dist/combinators/trivia-skip.d.ts +45 -1
  58. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  59. package/dist/combinators/withCtx.d.ts.map +1 -1
  60. package/dist/compiler/compile-linkable-table.d.ts +104 -0
  61. package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
  62. package/dist/compiler/degradation.d.ts +0 -2
  63. package/dist/compiler/degradation.d.ts.map +1 -1
  64. package/dist/compiler/fields.d.ts +13 -0
  65. package/dist/compiler/fields.d.ts.map +1 -1
  66. package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
  67. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  68. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  69. package/dist/compiler/linker.d.ts +47 -53
  70. package/dist/compiler/linker.d.ts.map +1 -1
  71. package/dist/compiler/token-alphabet.d.ts +81 -0
  72. package/dist/compiler/token-alphabet.d.ts.map +1 -0
  73. package/dist/compiler/token-dispatch.d.ts +95 -0
  74. package/dist/compiler/token-dispatch.d.ts.map +1 -0
  75. package/dist/compiler/token-scanner.d.ts +53 -0
  76. package/dist/compiler/token-scanner.d.ts.map +1 -0
  77. package/dist/compiler/value-usage.d.ts.map +1 -1
  78. package/dist/coverage.d.ts +1 -1
  79. package/dist/coverage.d.ts.map +1 -1
  80. package/dist/cst/capture-buffer.d.ts +35 -0
  81. package/dist/cst/capture-buffer.d.ts.map +1 -1
  82. package/dist/cst/reflection-attach.d.ts +8 -0
  83. package/dist/cst/reflection-attach.d.ts.map +1 -0
  84. package/dist/cst/reflection-symbols.d.ts +10 -0
  85. package/dist/cst/reflection-symbols.d.ts.map +1 -0
  86. package/dist/cst/reflection.d.ts +2 -4
  87. package/dist/cst/reflection.d.ts.map +1 -1
  88. package/dist/cst/root-trivia-scope.d.ts +26 -0
  89. package/dist/cst/root-trivia-scope.d.ts.map +1 -0
  90. package/dist/cst/trivia-charscan.d.ts +18 -0
  91. package/dist/cst/trivia-charscan.d.ts.map +1 -0
  92. package/dist/cst/trivia-kinds.d.ts +19 -0
  93. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  94. package/dist/functional/doc.d.ts.map +1 -1
  95. package/dist/functional/run-tabled.d.ts +12 -0
  96. package/dist/functional/run-tabled.d.ts.map +1 -0
  97. package/dist/functional/run.d.ts +25 -42
  98. package/dist/functional/run.d.ts.map +1 -1
  99. package/dist/grammar-metadata.d.ts +3 -0
  100. package/dist/grammar-metadata.d.ts.map +1 -0
  101. package/dist/index.cjs +9290 -7181
  102. package/dist/index.cjs.map +3 -4
  103. package/dist/index.d.ts +18 -9
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +9300 -7191
  106. package/dist/index.js.map +3 -4
  107. package/dist/language-service/index.cjs +313 -121
  108. package/dist/language-service/index.cjs.map +3 -4
  109. package/dist/language-service/index.js +313 -121
  110. package/dist/language-service/index.js.map +3 -4
  111. package/dist/oracle/index.cjs.map +0 -1
  112. package/dist/oracle/index.js.map +0 -1
  113. package/dist/parse-context.d.ts +49 -0
  114. package/dist/parse-context.d.ts.map +1 -0
  115. package/dist/plugin/evaluator.d.ts +40 -1
  116. package/dist/plugin/evaluator.d.ts.map +1 -1
  117. package/dist/plugin/index.cjs +11175 -9534
  118. package/dist/plugin/index.cjs.map +3 -4
  119. package/dist/plugin/index.d.ts.map +1 -1
  120. package/dist/plugin/index.js +11175 -9534
  121. package/dist/plugin/index.js.map +3 -4
  122. package/dist/regex/classes.d.ts +28 -0
  123. package/dist/regex/classes.d.ts.map +1 -1
  124. package/dist/regex/first-set.d.ts +14 -0
  125. package/dist/regex/first-set.d.ts.map +1 -1
  126. package/dist/run/index.cjs +136 -51
  127. package/dist/run/index.cjs.map +2 -3
  128. package/dist/run/index.js +136 -51
  129. package/dist/run/index.js.map +2 -3
  130. package/dist/spec/index.cjs +2934 -1611
  131. package/dist/spec/index.cjs.map +3 -4
  132. package/dist/spec/index.js +2934 -1611
  133. package/dist/spec/index.js.map +3 -4
  134. package/dist/spec/model.d.ts +8 -0
  135. package/dist/spec/model.d.ts.map +1 -1
  136. package/dist/table/assemble.d.ts +307 -0
  137. package/dist/table/assemble.d.ts.map +1 -0
  138. package/dist/table/cell.d.ts +42 -0
  139. package/dist/table/cell.d.ts.map +1 -0
  140. package/dist/table/child-slots.d.ts +30 -0
  141. package/dist/table/child-slots.d.ts.map +1 -0
  142. package/dist/table/compile-rule-map.d.ts +136 -0
  143. package/dist/table/compile-rule-map.d.ts.map +1 -0
  144. package/dist/table/compile.d.ts +56 -0
  145. package/dist/table/compile.d.ts.map +1 -0
  146. package/dist/table/duplication-hook.d.ts +32 -0
  147. package/dist/table/duplication-hook.d.ts.map +1 -0
  148. package/dist/table/emit-assembly.d.ts +139 -0
  149. package/dist/table/emit-assembly.d.ts.map +1 -0
  150. package/dist/table/emit.d.ts +113 -0
  151. package/dist/table/emit.d.ts.map +1 -0
  152. package/dist/table/encode-baseline.d.ts +16 -0
  153. package/dist/table/encode-baseline.d.ts.map +1 -0
  154. package/dist/table/encode.d.ts +71 -0
  155. package/dist/table/encode.d.ts.map +1 -0
  156. package/dist/table/exec-baseline.d.ts +9 -0
  157. package/dist/table/exec-baseline.d.ts.map +1 -0
  158. package/dist/table/exec.d.ts +72 -0
  159. package/dist/table/exec.d.ts.map +1 -0
  160. package/dist/table/fold.d.ts +25 -0
  161. package/dist/table/fold.d.ts.map +1 -0
  162. package/dist/table/index.cjs +11463 -0
  163. package/dist/table/index.cjs.map +6 -0
  164. package/dist/table/index.d.ts +100 -0
  165. package/dist/table/index.d.ts.map +1 -0
  166. package/dist/table/index.js +11440 -0
  167. package/dist/table/index.js.map +6 -0
  168. package/dist/table/inspect.d.ts +17 -0
  169. package/dist/table/inspect.d.ts.map +1 -0
  170. package/dist/table/ops.d.ts +453 -0
  171. package/dist/table/ops.d.ts.map +1 -0
  172. package/dist/table/program.d.ts +496 -0
  173. package/dist/table/program.d.ts.map +1 -0
  174. package/dist/table/run-support.d.ts +38 -0
  175. package/dist/table/run-support.d.ts.map +1 -0
  176. package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
  177. package/dist/table/scan-shapes.d.ts.map +1 -0
  178. package/dist/table/site-labels.d.ts +58 -0
  179. package/dist/table/site-labels.d.ts.map +1 -0
  180. package/dist/table/stamp.d.ts +70 -0
  181. package/dist/table/stamp.d.ts.map +1 -0
  182. package/dist/types.d.ts +80 -24
  183. package/dist/types.d.ts.map +1 -1
  184. package/dist/version.d.ts +1 -1
  185. package/package.json +26 -2
  186. package/src/analysis/choice-cost-gate.ts +359 -0
  187. package/src/analysis/choice-cost-render.ts +293 -0
  188. package/src/analysis/choice-cost.ts +1008 -0
  189. package/src/analysis/commitment.ts +557 -0
  190. package/src/analysis/corpus.ts +130 -0
  191. package/src/analysis/diagnose-render.ts +625 -0
  192. package/src/analysis/diagnose.ts +317 -0
  193. package/src/analysis/diagnostics.ts +33 -0
  194. package/src/analysis/duplication.ts +1955 -0
  195. package/src/analysis/fix-render.ts +217 -0
  196. package/src/analysis/fix.ts +669 -0
  197. package/src/analysis/format-number.ts +31 -0
  198. package/src/analysis/gating.ts +792 -0
  199. package/src/analysis/grammar.ts +72 -0
  200. package/src/analysis/rebuild.ts +287 -0
  201. package/src/analysis/terminal.ts +336 -0
  202. package/src/cli/index.ts +413 -0
  203. package/src/combinators/adjacency.ts +171 -0
  204. package/src/combinators/attempt.ts +41 -0
  205. package/src/combinators/case-fold.ts +90 -0
  206. package/src/combinators/choice.ts +466 -0
  207. package/src/combinators/completions.ts +64 -0
  208. package/src/combinators/dispatch.ts +358 -0
  209. package/src/combinators/expect.ts +159 -0
  210. package/src/combinators/first-set.ts +348 -0
  211. package/src/combinators/gate.ts +50 -0
  212. package/src/combinators/grammar.ts +272 -0
  213. package/src/combinators/keywords.ts +145 -0
  214. package/src/combinators/literal.ts +102 -0
  215. package/src/combinators/map.ts +106 -0
  216. package/src/combinators/node.ts +345 -0
  217. package/src/combinators/not.ts +55 -0
  218. package/src/combinators/parser.ts +266 -0
  219. package/src/combinators/peek.ts +63 -0
  220. package/src/combinators/probe.ts +25 -0
  221. package/src/combinators/ref.ts +109 -0
  222. package/src/combinators/regex.ts +170 -0
  223. package/src/combinators/repeat.ts +561 -0
  224. package/src/combinators/scanTo.ts +393 -0
  225. package/src/combinators/sequence.ts +237 -0
  226. package/src/combinators/token.ts +120 -0
  227. package/src/combinators/trivia-boundary.ts +48 -0
  228. package/src/combinators/trivia-skip.ts +852 -0
  229. package/src/combinators/withCtx.ts +45 -0
  230. package/src/compiler/build-arity.ts +330 -0
  231. package/src/compiler/compile-linkable-table.ts +209 -0
  232. package/src/compiler/degradation.ts +266 -0
  233. package/src/compiler/fields.ts +191 -0
  234. package/src/compiler/grammar-coverage-ids.ts +129 -0
  235. package/src/compiler/ir-serialize.ts +615 -0
  236. package/src/compiler/linker.ts +954 -0
  237. package/src/compiler/token-alphabet.ts +209 -0
  238. package/src/compiler/token-dispatch.ts +353 -0
  239. package/src/compiler/token-scanner.ts +318 -0
  240. package/src/compiler/value-usage.ts +147 -0
  241. package/src/coverage.ts +383 -0
  242. package/src/cst/capture-buffer.ts +297 -0
  243. package/src/cst/host-mode.ts +69 -0
  244. package/src/cst/reflection-attach.ts +11 -0
  245. package/src/cst/reflection-symbols.ts +9 -0
  246. package/src/cst/reflection.ts +149 -0
  247. package/src/cst/relative-spans.ts +207 -0
  248. package/src/cst/root-trivia-scope.ts +30 -0
  249. package/src/cst/trivia-charscan.ts +312 -0
  250. package/src/cst/trivia-entries.ts +285 -0
  251. package/src/cst/trivia-index.ts +131 -0
  252. package/src/cst/trivia-kinds.ts +358 -0
  253. package/src/cst/types.ts +76 -0
  254. package/src/cst/walk.ts +101 -0
  255. package/src/functional/doc.ts +959 -0
  256. package/src/functional/run-tabled.ts +153 -0
  257. package/src/functional/run.ts +558 -0
  258. package/src/grammar-metadata.ts +2 -0
  259. package/src/index.ts +189 -0
  260. package/src/language-service/index.ts +221 -0
  261. package/src/line-index.ts +131 -0
  262. package/src/oracle/digest.ts +620 -0
  263. package/src/oracle/index.ts +55 -0
  264. package/src/parse-context.ts +91 -0
  265. package/src/plugin/direct-builder-static.ts +102 -0
  266. package/src/plugin/evaluator.ts +1505 -0
  267. package/src/plugin/free-identifiers.ts +338 -0
  268. package/src/plugin/index.ts +2281 -0
  269. package/src/plugin/reducer-resolver.ts +606 -0
  270. package/src/recovery/scan.ts +155 -0
  271. package/src/regex/classes.ts +183 -0
  272. package/src/regex/first-set.ts +316 -0
  273. package/src/run/index.ts +31 -0
  274. package/src/spec/ebnf.ts +128 -0
  275. package/src/spec/index.ts +54 -0
  276. package/src/spec/model.ts +636 -0
  277. package/src/spec/railroad-lib.ts +559 -0
  278. package/src/spec/railroad.ts +256 -0
  279. package/src/table/assemble.ts +3006 -0
  280. package/src/table/cell.ts +42 -0
  281. package/src/table/child-slots.ts +113 -0
  282. package/src/table/compile-rule-map.ts +336 -0
  283. package/src/table/compile.ts +244 -0
  284. package/src/table/duplication-hook.ts +89 -0
  285. package/src/table/emit-assembly.ts +1777 -0
  286. package/src/table/emit.ts +429 -0
  287. package/src/table/encode-baseline.ts +340 -0
  288. package/src/table/encode.ts +1569 -0
  289. package/src/table/exec-baseline.ts +537 -0
  290. package/src/table/exec.ts +1680 -0
  291. package/src/table/fold.ts +38 -0
  292. package/src/table/index.ts +108 -0
  293. package/src/table/inspect.ts +52 -0
  294. package/src/table/ops.ts +467 -0
  295. package/src/table/program.ts +876 -0
  296. package/src/table/run-support.ts +66 -0
  297. package/src/table/scan-shapes.ts +1578 -0
  298. package/src/table/site-labels.ts +228 -0
  299. package/src/table/stamp.ts +229 -0
  300. package/src/types.ts +563 -0
  301. package/src/version.ts +16 -0
  302. package/dist/compiler/codegen.d.ts +0 -334
  303. package/dist/compiler/codegen.d.ts.map +0 -1
  304. package/dist/compiler/inline-build.d.ts +0 -11
  305. package/dist/compiler/inline-build.d.ts.map +0 -1
  306. package/dist/compiler/inline-callback.d.ts +0 -21
  307. package/dist/compiler/inline-callback.d.ts.map +0 -1
  308. package/dist/compiler/line-index.d.ts +0 -2
  309. package/dist/compiler/line-index.d.ts.map +0 -1
  310. package/dist/compiler/module-hoist.d.ts +0 -106
  311. package/dist/compiler/module-hoist.d.ts.map +0 -1
  312. package/dist/compiler/scannable-run.d.ts.map +0 -1
  313. package/dist/compiler/scannable-terminal.d.ts +0 -23
  314. package/dist/compiler/scannable-terminal.d.ts.map +0 -1
  315. package/dist/compiler/trivia-fast-path.d.ts +0 -38
  316. package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
@@ -0,0 +1,557 @@
1
+ /**
2
+ * COMMITMENT ANALYSIS — where a construct can still fail after it has consumed
3
+ * input or dirtied the capture buffers.
4
+ *
5
+ * This is a whole-grammar property of the rule graph, and it has exactly two
6
+ * consumers: the compiler, which bakes the decisions into the emitted source,
7
+ * and the interpreter, which must reach the SAME decisions at load time. It
8
+ * lives here, in one module, for that reason. Two hand-written copies of a
9
+ * predicate like `mayFail` would drift, and a drift here is not a size
10
+ * regression — it is the compiled and interpreted parsers accepting different
11
+ * languages. One module, two consumers.
12
+ *
13
+ * Every predicate in this file is a sound OVER-approximation of "something can
14
+ * go wrong": each MUST err toward `true`. A `false` is a proof obligation
15
+ * discharged on the caller's behalf, and callers delete code on the strength of
16
+ * it. Elide only where non-failure is PROVEN; when in doubt, say `true` and keep
17
+ * the machinery.
18
+ */
19
+ import type { Combinator, ParserDef } from '../types.ts'
20
+ import { regexCanMatchEmpty } from '../regex/first-set.ts'
21
+ import { childrenOf } from './gating.ts'
22
+
23
+ /**
24
+ * Can `p` FAIL at all?
25
+ *
26
+ * `false` needs a construct whose definition is total: the empty literal,
27
+ * trivia (a scan of width 0 is a match), and the repeats — but the repeats only
28
+ * conditionally, see below.
29
+ *
30
+ * Everything else stays `true`, including two cases that look elidable and are
31
+ * not. A `choice` whose last arm is infallible is NOT infallible, because
32
+ * first-set gating decides at runtime which arms are entered at all and the
33
+ * total arm may be skipped. And a re-entered cycle is `true` rather than
34
+ * `false`: a recursive rule's own fallibility is exactly what the recursion is
35
+ * being asked about, so the safe fixpoint seed is the conservative one.
36
+ *
37
+ * TODO(0.47): `expect` is conservative here. It converts its inner parser's
38
+ * failure into a zero-width success carrying a parse-error node and therefore
39
+ * never fails, but it currently reports its inner parser's fallibility. Making
40
+ * it `false` would elide further; it needs its own pass through the tree-diff
41
+ * gate first, because `expect` sits at exactly the closing-delimiter positions
42
+ * where a wrong answer is most visible.
43
+ */
44
+ export function mayFail(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set()): boolean {
45
+ if (seen.has(p)) return true
46
+ seen.add(p)
47
+ const d = p._def as ParserDef
48
+ switch (d.tag) {
49
+ case 'literal': return d.value.length > 0
50
+ case 'trivia': return false
51
+ // `optional`/`many`/a min-0 `sepBy` swallow an ORDINARY failure — zero
52
+ // repetitions is a match. They do NOT swallow a COMMITTED one: a dispatch
53
+ // that has selected an arm and then failed inside it breaks out past every
54
+ // enclosing repeat, straight to the fallible boundary. So these are total
55
+ // exactly when their body cannot commit. (Measured: treating them as
56
+ // unconditionally total left an extra zero-width leaf in
57
+ // `BareInterpolatedSelector` on benchmark.less — a rollback the legacy
58
+ // build fired and this one had deleted.)
59
+ case 'optional':
60
+ case 'many': return mayCommitFailure(d.parser)
61
+ case 'sepBy': return d.min >= 1
62
+ ? mayFail(d.parser, seen)
63
+ : mayCommitFailure(d.parser) || mayCommitFailure(d.separator)
64
+ case 'oneOrMore': return mayFail(d.parser, seen)
65
+ case 'sequence': return d.parsers.some(x => mayFail(x, seen))
66
+ // Delegating wrappers: as fallible as what they wrap.
67
+ case 'node':
68
+ case 'transform':
69
+ case 'label':
70
+ case 'field':
71
+ case 'expect':
72
+ case 'withCtx':
73
+ case 'grammar':
74
+ case 'token':
75
+ case 'leaf': return mayFail(d.parser, seen)
76
+ case 'lazy': { try { return mayFail(d.thunk(), seen) } catch { return true } }
77
+ // An adjacency assertion is a TEST — failing is its whole job.
78
+ case 'adjacency': return true
79
+ default: return true
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Does this regex source consume at least one character on every match?
85
+ *
86
+ * `exec('')` alone is NOT this test, and the difference is not academic. A pure
87
+ * lookahead like `/(?=[ \t\n\r\f]*(?:[,{]))/` — the Less grammar's
88
+ * `bareInterpolatedSelectorEnd` — never matches the EMPTY STRING (there is no
89
+ * `,` or `{` to look at) yet matches ZERO-WIDTH wherever it succeeds. Reading
90
+ * `exec('') === null` as "always consumes" put an extra empty leaf inside
91
+ * `BareInterpolatedSelector` on benchmark.less.
92
+ *
93
+ * So the test is in two parts. A pattern built only from character-consuming
94
+ * constructs is position-INdependent: if it can match ε anywhere it can match ε
95
+ * in `''`, which `exec('')` detects. The constructs that break that equivalence
96
+ * — matching zero-width at a position inside a string but not at position 0 of
97
+ * `''` — are exactly lookaround and the word boundaries; `^`/`$` still match in
98
+ * `''`. Reject those syntactically and `exec('')` covers the rest.
99
+ */
100
+ function regexAlwaysConsumes(source: string): boolean {
101
+ // STRUCTURAL, not a probe. The previous form rejected any pattern CONTAINING a
102
+ // lookaround and then asked `new RegExp(source).exec('')`. Both halves were
103
+ // imprecise in the same direction:
104
+ // - the blanket lookaround test fails a pattern whose lookaround is buried
105
+ // inside mandatory content — `/\/\*(?:[^*]|\*(?!\/))*\*\//` must consume
106
+ // `/*` and cannot match empty, yet was reported nullable;
107
+ // - `exec('')` cannot see a pattern that matches zero-width only at a
108
+ // NON-empty position, which is why the blanket test had to exist.
109
+ // `regexCanMatchEmpty` walks the same AST `firstSetFromRegex` uses, where
110
+ // anchors, lookaround and word boundaries are already `EMPTY`, so it answers
111
+ // both cases correctly and neither guard is needed.
112
+ return !regexCanMatchEmpty(source)
113
+ }
114
+
115
+ /**
116
+ * Does `p`, WHEN IT SUCCEEDS, always consume at least one character?
117
+ *
118
+ * The mirror of `mayFail`, and the opposite polarity from
119
+ * `first-set.ts`'s `matchesEmpty`: this one MUST err toward `false`. A `true`
120
+ * is a proof that the success path advanced the position, and the sequence
121
+ * emitter deletes a trivia rewind on the strength of it.
122
+ *
123
+ * It cannot be written as `!matchesEmpty(p)`. `matchesEmpty` answers a
124
+ * first-set question and is allowed to under-report nullability; for `expect`
125
+ * it does, and that case is exactly the one that matters here. `expect(X)`
126
+ * turns X's FAILURE into a zero-width SUCCESS carrying a parse-error node —
127
+ * so `expect(literal('}'))` succeeds having consumed nothing, while
128
+ * `matchesEmpty` looks through to the non-nullable literal and says "always
129
+ * consumes". Eliding on that answer moves a rule's span end past trailing
130
+ * trivia (measured: 38 tree mismatches across the css corpus, spans absorbing
131
+ * whitespace their legacy build excluded). `recover` manufactures the same
132
+ * zero-width success, and both are `false` here.
133
+ *
134
+ * `dispatch` is `false` rather than deferring to its selector: the selector's
135
+ * consumption is not unconditionally part of the dispatch's own span, and the
136
+ * few bytes are not worth the proof obligation.
137
+ */
138
+ export function alwaysConsumes(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set()): boolean {
139
+ if (seen.has(p)) return false
140
+ seen.add(p)
141
+ const d = p._def as ParserDef
142
+ switch (d.tag) {
143
+ case 'literal': return d.value.length > 0
144
+ case 'keywords': return true
145
+ case 'regex': return regexAlwaysConsumes(d.source)
146
+ case 'routed': return d.fallback === undefined ? true : alwaysConsumes(d.fallback, seen)
147
+ // One consuming term is enough for the whole sequence to consume.
148
+ case 'sequence': return d.parsers.some(x => alwaysConsumes(x, seen))
149
+ // A choice consumes only if EVERY arm it could take consumes.
150
+ case 'choice': return d.parsers.every(x => alwaysConsumes(x, seen))
151
+ case 'oneOrMore': return alwaysConsumes(d.parser, seen)
152
+ case 'sepBy': return d.min >= 1 && alwaysConsumes(d.parser, seen)
153
+ case 'node':
154
+ case 'transform':
155
+ case 'label':
156
+ case 'field':
157
+ case 'withCtx':
158
+ case 'grammar':
159
+ case 'token':
160
+ case 'leaf': return alwaysConsumes(d.parser, seen)
161
+ case 'lazy': { try { return alwaysConsumes(d.thunk(), seen) } catch { return false } }
162
+ // Zero-width by definition — it asserts over the gap and moves nothing. This
163
+ // `false` is load-bearing: it is what keeps the TRIVIA REWIND in place for the
164
+ // term that follows, so the assertion changes no span it does not mean to.
165
+ case 'adjacency': return false
166
+ // `expect`/`recover` synthesise a zero-width success; `optional`/`many`/
167
+ // `not`/`peek`/`trivia` are zero-width by definition; `dispatch`/`scanTo`/
168
+ // `guard`/`attempt`/`unknown` are unproven.
169
+ default: return false
170
+ }
171
+ }
172
+
173
+ /**
174
+ * True when parsing `p` may push a capture (leaf/child/trivia) into the active
175
+ * buffers and THEN fail, leaving partial state that an enclosing node() would
176
+ * wrongly absorb. Used to decide whether a fallible block needs CST-rollback.
177
+ *
178
+ * Sound over-approximation: the ONLY constructs that capture-then-fail are
179
+ * - a sequence whose non-final term captures before a later term can fail
180
+ * - a sepBy/oneOrMore item-then-separator partial (handled by their own
181
+ * dedicated rollback, so still covered conservatively here)
182
+ * Atomic terminals (literal/regex/keywords/charClass/guard/not) fail without
183
+ * having captured. node() buffers into a private sub-scope and discards it on
184
+ * failure, so it never leaks. choice/firstMatch roll back each failed arm
185
+ * internally. optional/many never "fail" with partial output. Delegating
186
+ * wrappers (transform/label/grammar/withCtx/expect) pass through to inner.
187
+ */
188
+ export function mayLeavePartialCapture(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set(), triviaActive = true): boolean {
189
+ if (seen.has(p)) return false
190
+ seen.add(p)
191
+ const d = p._def
192
+ switch (d.tag) {
193
+ // Atomic / non-capturing-then-failing: a failure happens before any push.
194
+ case 'literal':
195
+ case 'regex':
196
+ case 'keywords':
197
+ case 'guard':
198
+ // Adjacency probes with `_cap = 0` / no `commit()`: it cannot push anything.
199
+ case 'adjacency':
200
+ case 'not':
201
+ // peek(): emitted under a non-capturing probe ctx and zero-width on both
202
+ // outcomes, so it can never leave a partial capture behind.
203
+ case 'peek':
204
+ case 'trivia':
205
+ case 'token':
206
+ case 'leaf':
207
+ case 'scanTo':
208
+ case 'unknown':
209
+ return false
210
+ // node() captures into its own private buffers and rolls them back on
211
+ // failure (emitNode restores _ctx.* and never pushes on the !ok path).
212
+ case 'node':
213
+ return false
214
+ // choice/firstMatch already roll back each failed arm; on overall failure
215
+ // nothing committed remains.
216
+ case 'choice':
217
+ return false
218
+ case 'dispatch':
219
+ // These are independent graph predicates. Sharing one mutable `seen` set
220
+ // between them made the first walk hide the arm from the second: an atomic
221
+ // literal arm, for example, was visited by `mayLeavePartialCapture` (false)
222
+ // and then skipped by `capturesLeaf` (also false because already seen).
223
+ // Fork the ancestor set for each proof so cycle protection remains, while
224
+ // one proof cannot erase evidence needed by another.
225
+ return capturesLeaf(d.selector, new Set(seen)) ||
226
+ d.cases.some(x => mayLeavePartialCapture(x.parser, new Set(seen), triviaActive) || capturesLeaf(x.parser, new Set(seen))) ||
227
+ (d.matchers ? d.matchers.some(x => mayLeavePartialCapture(x.parser, new Set(seen), triviaActive) || capturesLeaf(x.parser, new Set(seen))) : false) ||
228
+ (d.otherwise ? mayLeavePartialCapture(d.otherwise, new Set(seen), triviaActive) || capturesLeaf(d.otherwise, new Set(seen)) : false)
229
+ case 'attempt':
230
+ return false
231
+ // optional never fails; many/oneOrMore only "fail" with zero captured items.
232
+ case 'optional':
233
+ case 'many':
234
+ case 'oneOrMore':
235
+ return false
236
+ // sepBy emits its own per-iteration rollback in emitSepBy.
237
+ case 'sepBy':
238
+ return false
239
+ // A sequence is the real case: an earlier capturing term followed by a term
240
+ // that can fail leaves the earlier captures buffered.
241
+ //
242
+ // BOTH halves of that sentence are load-bearing, and the second half is what
243
+ // this walk exists to exploit. "Some earlier term captures" alone was the old
244
+ // test, and it is true of very nearly every sequence in a real grammar — a
245
+ // sequence almost always opens with a literal or a node. The rollback is only
246
+ // reachable if something AFTER the capture can still fail; once the remainder
247
+ // is total (`optional`/`many`/trivia/a min-0 `sepBy`), the sequence has no
248
+ // failure path left to roll back from, and the save+restore is dead code.
249
+ //
250
+ // `captured` tracks whether anything is in the buffers yet as the scan moves
251
+ // left to right. Two things put it there:
252
+ // - a term that captures (`capturesLeaf`/`hasNodeDef`), and
253
+ // - the inter-term TRIVIA scan, which under `capturing` pushes into
254
+ // `_cstRawChildren`/`_cstTriviaLog` BEFORE the term at each boundary
255
+ // i >= 1. `emitSeqValues` only rewinds that push on the term's SUCCESS
256
+ // path (the "matched empty, so leave the whitespace out of the span"
257
+ // branch); a term that FAILS breaks straight out with the trivia still
258
+ // buffered, so from boundary 1 onward the buffers are dirty regardless of
259
+ // what the earlier terms did.
260
+ // Each term is also asked about itself: a nested sequence can leave its own
261
+ // partial capture behind, and its terms are emitted inline into this same
262
+ // block with no boundary of their own to roll them back.
263
+ case 'sequence': {
264
+ const parts = d.parsers
265
+ let captured = false
266
+ for (let i = 0; i < parts.length; i++) {
267
+ const part = parts[i]!
268
+ if (i > 0 && triviaActive) captured = true
269
+ if (captured && mayFail(part)) return true
270
+ if (mayLeavePartialCapture(part, seen, triviaActive)) return true
271
+ if (capturesLeaf(part) || hasNodeDef(part)) captured = true
272
+ }
273
+ return false
274
+ }
275
+ // Delegating wrappers: defer to the wrapped parser.
276
+ case 'transform':
277
+ case 'label':
278
+ case 'field':
279
+ case 'expect':
280
+ case 'withCtx':
281
+ case 'grammar':
282
+ return mayLeavePartialCapture(d.parser, seen, triviaActive)
283
+ case 'recover':
284
+ return mayLeavePartialCapture(d.parser, seen, triviaActive)
285
+ case 'lazy': {
286
+ try { return mayLeavePartialCapture(d.thunk(), seen, triviaActive) } catch { return true }
287
+ }
288
+ // Unknown shapes: be safe and keep the rollback.
289
+ default:
290
+ return true
291
+ }
292
+ }
293
+
294
+ /** True when `p` can push a leaf/node into the capture buffers on success. */
295
+ export function capturesLeaf(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set()): boolean {
296
+ if (seen.has(p)) return false
297
+ seen.add(p)
298
+ const d = p._def
299
+ switch (d.tag) {
300
+ case 'literal':
301
+ case 'regex':
302
+ case 'keywords':
303
+ case 'node':
304
+ case 'token':
305
+ case 'routed':
306
+ case 'leaf':
307
+ return true
308
+ case 'not':
309
+ case 'peek':
310
+ case 'guard':
311
+ case 'adjacency':
312
+ case 'trivia':
313
+ case 'unknown':
314
+ return false
315
+ case 'sequence':
316
+ case 'choice':
317
+ return d.parsers.some(x => capturesLeaf(x, seen))
318
+ case 'dispatch':
319
+ return capturesLeaf(d.selector, seen) ||
320
+ d.cases.some(x => capturesLeaf(x.parser, seen)) ||
321
+ (d.matchers ? d.matchers.some(entry => capturesLeaf(entry.parser, seen)) : false) ||
322
+ (d.otherwise ? capturesLeaf(d.otherwise, seen) : false)
323
+ case 'sepBy':
324
+ return capturesLeaf(d.parser, seen) || capturesLeaf(d.separator, seen)
325
+ case 'many':
326
+ case 'oneOrMore':
327
+ case 'optional':
328
+ case 'attempt':
329
+ case 'transform':
330
+ case 'label':
331
+ case 'field':
332
+ case 'expect':
333
+ case 'withCtx':
334
+ case 'grammar':
335
+ case 'recover':
336
+ return capturesLeaf(d.parser, seen)
337
+ case 'scanTo':
338
+ return true
339
+ case 'lazy': {
340
+ try { return capturesLeaf(d.thunk(), seen) } catch { return true }
341
+ }
342
+ default:
343
+ return true
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Does this combinator tree contain a node() anywhere (following ref/lazy
349
+ * thunks)? Determines whether the compile emits CST capture — so non-node
350
+ * grammars stay byte-identical. `seen` guards against recursion cycles.
351
+ */
352
+ export function hasNodeDef(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set()): boolean {
353
+ if (seen.has(p)) return false
354
+ seen.add(p)
355
+ const d = p._def
356
+ switch (d.tag) {
357
+ case 'node': return true
358
+ case 'lazy': { try { return hasNodeDef(d.thunk(), seen) } catch { return false } }
359
+ case 'grammar':
360
+ case 'trivia':
361
+ case 'token':
362
+ case 'leaf':
363
+ case 'label':
364
+ case 'field':
365
+ case 'optional':
366
+ case 'many':
367
+ case 'oneOrMore':
368
+ case 'not':
369
+ case 'peek':
370
+ case 'transform': return hasNodeDef(d.parser, seen)
371
+ case 'sequence':
372
+ case 'choice': return d.parsers.some(x => hasNodeDef(x, seen))
373
+ case 'dispatch': return hasNodeDef(d.selector, seen) || d.cases.some(x => hasNodeDef(x.parser, seen)) || (d.matchers ? d.matchers.some(entry => hasNodeDef(entry.parser, seen)) : false) || (d.otherwise ? hasNodeDef(d.otherwise, seen) : false)
374
+ case 'sepBy': return hasNodeDef(d.parser, seen) || hasNodeDef(d.separator, seen)
375
+ case 'scanTo': return hasNodeDef(d.sentinel, seen) || d.skip.some(x => hasNodeDef(x, seen))
376
+ case 'recover': return hasNodeDef(d.parser, seen) || hasNodeDef(d.sentinel, seen)
377
+ case 'expect': return hasNodeDef(d.parser, seen)
378
+ case 'withCtx': return hasNodeDef(d.parser, seen)
379
+ default: return false
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Whether a grammar tree owns a DIRECT semantic node reduction — a `node(..., build)`
385
+ * whose callback produces the value itself, as opposed to a purely structural
386
+ * `node(parser)`.
387
+ *
388
+ * It is the predicate behind `hostBranchElided`: an artifact only drops a
389
+ * positioned-CST branch if there was a direct builder to drop, so an all-structural
390
+ * grammar stays usable with either host (`cst/host-mode.ts`).
391
+ *
392
+ * It belongs in THIS module for the reason stated at the top of it: both lowerings
393
+ * must reach the same answer, and a stamp they disagree about is a driver that
394
+ * accepts a host it should refuse. It previously lived in `compiler/codegen.ts`,
395
+ * which forced `table/compile-rule-map.ts` to import the engine the table replaced.
396
+ *
397
+ * The descent mirrors the source lowering's own `childrenOf` exactly — including a
398
+ * `routed()` fallback and a `dispatch` matcher arm, both of which are real emit
399
+ * sites and neither of which `hasNodeDef` above walks.
400
+ */
401
+ export function hasDirectBuildDef(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set()): boolean {
402
+ if (seen.has(p)) return false
403
+ seen.add(p)
404
+ const d = p._def
405
+ switch (d.tag) {
406
+ case 'node': return d.build !== undefined || hasDirectBuildDef(d.parser, seen)
407
+ case 'lazy': { try { return hasDirectBuildDef(d.thunk(), seen) } catch { return false } }
408
+ case 'grammar':
409
+ case 'trivia':
410
+ case 'token':
411
+ case 'leaf':
412
+ case 'label':
413
+ case 'field':
414
+ case 'optional':
415
+ case 'many':
416
+ case 'oneOrMore':
417
+ case 'attempt':
418
+ case 'not':
419
+ case 'peek':
420
+ case 'withCtx':
421
+ case 'expect':
422
+ case 'transform': return hasDirectBuildDef(d.parser, seen)
423
+ case 'sequence':
424
+ case 'choice': return d.parsers.some(x => hasDirectBuildDef(x, seen))
425
+ case 'dispatch': return hasDirectBuildDef(d.selector, seen)
426
+ || d.cases.some(x => hasDirectBuildDef(x.parser, seen))
427
+ || (d.matchers ? d.matchers.some(x => hasDirectBuildDef(x.parser, seen)) : false)
428
+ || (d.otherwise ? hasDirectBuildDef(d.otherwise, seen) : false)
429
+ case 'sepBy': return hasDirectBuildDef(d.parser, seen) || hasDirectBuildDef(d.separator, seen)
430
+ case 'scanTo': return hasDirectBuildDef(d.sentinel, seen) || d.skip.some(x => hasDirectBuildDef(x, seen))
431
+ case 'recover': return hasDirectBuildDef(d.parser, seen) || hasDirectBuildDef(d.sentinel, seen)
432
+ case 'routed': return d.fallback ? hasDirectBuildDef(d.fallback, seen) : false
433
+ default: return false
434
+ }
435
+ }
436
+
437
+ /** True when `p` can report a committed failure through emitFallible's failure channel. */
438
+ export function mayCommitFailure(p: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set()): boolean {
439
+ if (seen.has(p)) return false
440
+ seen.add(p)
441
+ const d = p._def
442
+ switch (d.tag) {
443
+ case 'dispatch':
444
+ return true
445
+ case 'choice':
446
+ case 'sequence':
447
+ return d.parsers.some(x => mayCommitFailure(x, seen))
448
+ case 'many':
449
+ case 'oneOrMore':
450
+ case 'optional':
451
+ case 'attempt':
452
+ case 'transform':
453
+ case 'label':
454
+ case 'field':
455
+ case 'grammar':
456
+ case 'node':
457
+ return mayCommitFailure(d.parser, seen)
458
+ case 'sepBy':
459
+ return mayCommitFailure(d.parser, seen) || mayCommitFailure(d.separator, seen)
460
+ case 'token':
461
+ case 'leaf':
462
+ return mayCommitFailure(d.parser, seen)
463
+ case 'withCtx':
464
+ return mayCommitFailure(d.parser, seen)
465
+ case 'scanTo':
466
+ return mayCommitFailure(d.sentinel, seen) || d.skip.some(x => mayCommitFailure(x, seen))
467
+ case 'lazy': {
468
+ try { return mayCommitFailure(d.thunk(), seen) } catch { return true }
469
+ }
470
+ case 'unknown':
471
+ return true
472
+ default:
473
+ return false
474
+ }
475
+ }
476
+
477
+ /** A leaf-composed imported piece may carry Parseman's own structural balanced
478
+ * text reconstruction, but never a grammar-authored semantic callback.
479
+ *
480
+ * `externalRefs` are the unresolved NAMED refs `compileLinkable`'s pre-pass already
481
+ * classified as external (`g.Value` naming a rule this artifact doesn't define). They
482
+ * are the one case that fails OPEN: the ref is a HOLE, it holds no callback of its
483
+ * own, and codegen emits it as a by-name `_r_<Name>` call bound at fuse time by
484
+ * whichever piece supplies the name — either another pre-final piece (itself put
485
+ * through this same gate) or the local leaf (allowed to be semantic by design). So
486
+ * an artifact whose only "unknown" is a hole is genuinely recognition-only.
487
+ *
488
+ * EVERY other lazy failure still fails CLOSED. In particular an UNNAMED `ref()` that
489
+ * was never `.define()`d is NOT external — nobody can bind it by name — so it stays
490
+ * an opaque subtree of unknown semantics and the answer is "semantic". Catching all
491
+ * errors here instead would let that (and any future thunk failure) pass the
492
+ * recognition-only gate. */
493
+ function hasSemanticReduction(
494
+ roots: readonly Combinator<unknown>[],
495
+ externalRefs?: ReadonlySet<Combinator<unknown>>,
496
+ ): boolean {
497
+ const seen = new Set<Combinator<unknown>>()
498
+ const visit = (parser: Combinator<unknown>): boolean => {
499
+ if (seen.has(parser)) return false
500
+ seen.add(parser)
501
+ const def = parser._def
502
+ if (def.tag === 'transform' && !def.recognitionOnly) return true
503
+ if (def.tag === 'choice' && def.gates.some(Boolean)) return true
504
+ if (def.tag === 'guard' || def.tag === 'withCtx') return true
505
+ if (def.tag === 'node' && def.build !== undefined) return true
506
+ if (def.tag === 'lazy') {
507
+ if (externalRefs?.has(parser)) return false
508
+ try { return visit(def.thunk()) } catch { return true }
509
+ }
510
+ return childrenOf(def).some(visit)
511
+ }
512
+ return roots.some(visit)
513
+ }
514
+
515
+ /**
516
+ * `hasDirectBuilders` / `isRecognitionOnly` for a rule map WITHOUT lowering it.
517
+ *
518
+ * `composeLeaf()` gates on both: every pre-final grammar must prove recognition-only,
519
+ * and the local leaf's direct builders decide whether the recognition pieces need
520
+ * terminal capture. Both were only ever available as fields on `LinkablePieces`, so
521
+ * the gate forced a full `compileLinkable()` of every piece purely to read two
522
+ * booleans off the result — which is why the table lowering appeared to be blocked on
523
+ * porting the source lowering wholesale.
524
+ *
525
+ * They are not lowering products. Both are predicates over the COMBINATOR GRAPH, and
526
+ * this computes them from the graph directly, with the SAME `externalRefs` rule the
527
+ * lowering applies (`:6008`): a named `lazy` whose thunk throws is a HOLE, bound by
528
+ * name at fuse time, and is therefore not evidence of unknown semantics. An UNNAMED
529
+ * unresolved `ref()` stays semantic — nobody can bind it, so it fails closed.
530
+ */
531
+ export function classifyRuleMap(
532
+ ruleMap: ReadonlyArray<readonly [string, Combinator<unknown>]>,
533
+ ): { hasDirectBuilders: boolean; isRecognitionOnly: boolean } {
534
+ const externalRefs = new Set<Combinator<unknown>>()
535
+ const scanned = new Set<Combinator<unknown>>()
536
+ const scanExternal = (p: Combinator<unknown>): void => {
537
+ if (scanned.has(p)) return
538
+ scanned.add(p)
539
+ const def = p._def
540
+ if (def.tag === 'lazy') {
541
+ let resolved: Combinator<unknown> | undefined
542
+ try { resolved = def.thunk() } catch { resolved = undefined }
543
+ if (resolved === undefined) {
544
+ if ((p as unknown as { _ruleName?: string })._ruleName) externalRefs.add(p)
545
+ return
546
+ }
547
+ scanExternal(resolved)
548
+ return
549
+ }
550
+ for (const child of childrenOf(def)) scanExternal(child)
551
+ }
552
+ for (const [, rule] of ruleMap) scanExternal(rule)
553
+ return {
554
+ hasDirectBuilders: ruleMap.some(([, rule]) => hasDirectBuildDef(rule)),
555
+ isRecognitionOnly: !hasSemanticReduction(ruleMap.map(([, rule]) => rule), externalRefs),
556
+ }
557
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * The SECOND world.
3
+ *
4
+ * A type error has one source to point at. A grammar finding has two — the ordered
5
+ * choice whose arms cost the time, and the input that pays for it — and relating them is
6
+ * the whole diagnostic. `gating.ts` owns the first world and is entirely static. This
7
+ * module owns the second, and it is deliberately the smallest honest thing that can be
8
+ * said about a corpus without instrumenting a parse.
9
+ *
10
+ * WHAT IS MEASURED, EXACTLY
11
+ * -------------------------
12
+ * For a choice, every corpus POSITION whose character is in some arm's first set. That
13
+ * is a count of characters, not of times the choice was reached, and the difference
14
+ * matters: a choice nested three rules deep is reached far less often than its first
15
+ * characters occur. So the number is reported as what it is — an upper bound on entries,
16
+ * derived from the same first sets the compiler dispatches on — and never as "this
17
+ * choice ran N times". Naming it precisely is what keeps it useful; a number that
18
+ * overstates itself is worse than no number, because the reader stops trusting all of
19
+ * them.
20
+ *
21
+ * The one thing this DOES settle, exactly and without any modelling: an arm whose first
22
+ * set is ANY is entered at every one of those positions, because no first character can
23
+ * exclude it. That is the cost the fix removes, and it is arithmetic, not a guess.
24
+ *
25
+ * Determinism: counts and the located example are a pure function of (report, corpus).
26
+ * No timings, no paths beyond the caller's own sample names.
27
+ */
28
+ import type { Combinator, FirstSet } from '../types.ts'
29
+ import { firstSetOf } from '../combinators/first-set.ts'
30
+ import type { ChoiceGating } from './gating.ts'
31
+
32
+ export type CorpusSample = { name: string; text: string }
33
+
34
+ /** A located input position: the caller's sample name plus 1-based line/column. */
35
+ export type CorpusSite = {
36
+ sample: string
37
+ line: number
38
+ column: number
39
+ /** The source line, untrimmed, for a caret rendering. */
40
+ lineText: string
41
+ }
42
+
43
+ export type ArmCorpusCost = {
44
+ index: number
45
+ /** ANY first set — no character excludes this arm. */
46
+ any: boolean
47
+ /**
48
+ * Corpus positions whose character this arm's first set accepts. For an `any` arm this
49
+ * equals the choice total, which is the point.
50
+ */
51
+ positions: number
52
+ /** The first such position, for the two-world rendering. Absent when there are none. */
53
+ firstSite?: CorpusSite
54
+ }
55
+
56
+ export type ChoiceCorpusCost = {
57
+ choiceId: string
58
+ /** Positions whose character SOME arm accepts — the choice's own reach, bounded. */
59
+ positions: number
60
+ /** Every position in the corpus, so a share can be read. */
61
+ corpusPositions: number
62
+ arms: ArmCorpusCost[]
63
+ }
64
+
65
+ const inSet = (fs: FirstSet, cp: number): boolean => {
66
+ if (fs.kind === 'any') return true
67
+ if (fs.kind === 'empty') return false
68
+ for (const r of fs.ranges) if (cp >= r.lo && cp <= r.hi) return true
69
+ return false
70
+ }
71
+
72
+ function siteAt(samples: readonly CorpusSample[], flatIndex: number): CorpusSite {
73
+ let i = flatIndex
74
+ for (const s of samples) {
75
+ if (i < s.text.length) {
76
+ let line = 1
77
+ let lineStart = 0
78
+ for (let j = 0; j < i; j++) if (s.text.charCodeAt(j) === 10) { line++; lineStart = j + 1 }
79
+ let lineEnd = s.text.indexOf('\n', lineStart)
80
+ if (lineEnd === -1) lineEnd = s.text.length
81
+ return { sample: s.name, line, column: i - lineStart + 1, lineText: s.text.slice(lineStart, lineEnd) }
82
+ }
83
+ i -= s.text.length
84
+ }
85
+ return { sample: '<eof>', line: 0, column: 0, lineText: '' }
86
+ }
87
+
88
+ /**
89
+ * Measure one choice against a corpus. `choice.anyArms` already knows which arms are
90
+ * broad; this adds how much input that costs.
91
+ */
92
+ export function measureChoiceCost(c: ChoiceGating, samples: readonly CorpusSample[], arms: readonly { firstSet: FirstSet }[]): ChoiceCorpusCost {
93
+ const counts = arms.map(() => 0)
94
+ const first = arms.map<number>(() => -1)
95
+ let positions = 0
96
+ let corpusPositions = 0
97
+ let flat = 0
98
+ for (const s of samples) {
99
+ for (let i = 0; i < s.text.length; i++) {
100
+ const cp = s.text.charCodeAt(i)
101
+ let hitAny = false
102
+ for (let a = 0; a < arms.length; a++) {
103
+ if (inSet(arms[a]!.firstSet, cp)) {
104
+ counts[a]!++
105
+ if (first[a] === -1) first[a] = flat + i
106
+ hitAny = true
107
+ }
108
+ }
109
+ if (hitAny) positions++
110
+ }
111
+ flat += s.text.length
112
+ corpusPositions += s.text.length
113
+ }
114
+ return {
115
+ choiceId: c.id,
116
+ positions,
117
+ corpusPositions,
118
+ arms: arms.map((a, i) => ({
119
+ index: i,
120
+ any: a.firstSet.kind === 'any',
121
+ positions: counts[i]!,
122
+ ...(first[i]! >= 0 ? { firstSite: siteAt(samples, first[i]!) } : {}),
123
+ })),
124
+ }
125
+ }
126
+
127
+ /** The per-arm first sets a choice dispatches on, in arm order. */
128
+ export function armFirstSets(choiceArms: readonly Combinator<unknown>[]): { firstSet: FirstSet }[] {
129
+ return choiceArms.map(a => ({ firstSet: firstSetOf(a) }))
130
+ }