parseman 0.45.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/README.md +8 -3
  2. package/dist/analysis/choice-cost-gate.d.ts +158 -0
  3. package/dist/analysis/choice-cost-gate.d.ts.map +1 -0
  4. package/dist/analysis/choice-cost-render.d.ts +96 -0
  5. package/dist/analysis/choice-cost-render.d.ts.map +1 -0
  6. package/dist/analysis/choice-cost.d.ts +383 -0
  7. package/dist/analysis/choice-cost.d.ts.map +1 -0
  8. package/dist/analysis/commitment.d.ts +131 -0
  9. package/dist/analysis/commitment.d.ts.map +1 -0
  10. package/dist/analysis/corpus.d.ts +73 -0
  11. package/dist/analysis/corpus.d.ts.map +1 -0
  12. package/dist/analysis/diagnose-render.d.ts +65 -0
  13. package/dist/analysis/diagnose-render.d.ts.map +1 -0
  14. package/dist/analysis/diagnose.d.ts +15 -0
  15. package/dist/analysis/diagnose.d.ts.map +1 -1
  16. package/dist/analysis/diagnostics.cjs +13736 -0
  17. package/dist/analysis/diagnostics.cjs.map +6 -0
  18. package/dist/analysis/diagnostics.d.ts +27 -0
  19. package/dist/analysis/diagnostics.d.ts.map +1 -0
  20. package/dist/analysis/diagnostics.js +13713 -0
  21. package/dist/analysis/diagnostics.js.map +6 -0
  22. package/dist/analysis/duplication.d.ts.map +1 -1
  23. package/dist/analysis/fix-render.d.ts +43 -0
  24. package/dist/analysis/fix-render.d.ts.map +1 -0
  25. package/dist/analysis/fix.d.ts +172 -0
  26. package/dist/analysis/fix.d.ts.map +1 -0
  27. package/dist/analysis/format-number.d.ts +23 -0
  28. package/dist/analysis/format-number.d.ts.map +1 -0
  29. package/dist/analysis/gating.d.ts +47 -2
  30. package/dist/analysis/gating.d.ts.map +1 -1
  31. package/dist/analysis/rebuild.d.ts +61 -0
  32. package/dist/analysis/rebuild.d.ts.map +1 -0
  33. package/dist/analysis/terminal.d.ts +145 -0
  34. package/dist/analysis/terminal.d.ts.map +1 -0
  35. package/dist/cli/index.cjs +14837 -0
  36. package/dist/cli/index.cjs.map +6 -0
  37. package/dist/cli/index.d.ts +3 -0
  38. package/dist/cli/index.d.ts.map +1 -0
  39. package/dist/cli/index.js +14814 -0
  40. package/dist/cli/index.js.map +6 -0
  41. package/dist/combinators/adjacency.d.ts +100 -0
  42. package/dist/combinators/adjacency.d.ts.map +1 -0
  43. package/dist/combinators/choice.d.ts.map +1 -1
  44. package/dist/combinators/completions.d.ts.map +1 -1
  45. package/dist/combinators/dispatch.d.ts +8 -1
  46. package/dist/combinators/dispatch.d.ts.map +1 -1
  47. package/dist/combinators/expect.d.ts.map +1 -1
  48. package/dist/combinators/first-set.d.ts.map +1 -1
  49. package/dist/combinators/gate.d.ts.map +1 -1
  50. package/dist/combinators/grammar.d.ts +22 -0
  51. package/dist/combinators/grammar.d.ts.map +1 -1
  52. package/dist/combinators/literal.d.ts +8 -0
  53. package/dist/combinators/literal.d.ts.map +1 -1
  54. package/dist/combinators/map.d.ts +0 -1
  55. package/dist/combinators/map.d.ts.map +1 -1
  56. package/dist/combinators/node.d.ts +13 -3
  57. package/dist/combinators/node.d.ts.map +1 -1
  58. package/dist/combinators/parser.d.ts +1 -1
  59. package/dist/combinators/parser.d.ts.map +1 -1
  60. package/dist/combinators/ref.d.ts.map +1 -1
  61. package/dist/combinators/regex.d.ts +1 -16
  62. package/dist/combinators/regex.d.ts.map +1 -1
  63. package/dist/combinators/repeat.d.ts +30 -2
  64. package/dist/combinators/repeat.d.ts.map +1 -1
  65. package/dist/combinators/scanTo.d.ts +71 -1
  66. package/dist/combinators/scanTo.d.ts.map +1 -1
  67. package/dist/combinators/sequence.d.ts.map +1 -1
  68. package/dist/combinators/token.d.ts.map +1 -1
  69. package/dist/combinators/trivia-boundary.d.ts +26 -0
  70. package/dist/combinators/trivia-boundary.d.ts.map +1 -0
  71. package/dist/combinators/trivia-skip.d.ts +40 -1
  72. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  73. package/dist/combinators/withCtx.d.ts.map +1 -1
  74. package/dist/compiler/build-arity.d.ts.map +1 -1
  75. package/dist/compiler/compile-linkable-table.d.ts +104 -0
  76. package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
  77. package/dist/compiler/degradation.d.ts +0 -2
  78. package/dist/compiler/degradation.d.ts.map +1 -1
  79. package/dist/compiler/fields.d.ts +13 -0
  80. package/dist/compiler/fields.d.ts.map +1 -1
  81. package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
  82. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  83. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  84. package/dist/compiler/linker.d.ts +79 -47
  85. package/dist/compiler/linker.d.ts.map +1 -1
  86. package/dist/compiler/token-alphabet.d.ts +81 -0
  87. package/dist/compiler/token-alphabet.d.ts.map +1 -0
  88. package/dist/compiler/token-dispatch.d.ts +95 -0
  89. package/dist/compiler/token-dispatch.d.ts.map +1 -0
  90. package/dist/compiler/token-scanner.d.ts +53 -0
  91. package/dist/compiler/token-scanner.d.ts.map +1 -0
  92. package/dist/compiler/value-usage.d.ts.map +1 -1
  93. package/dist/coverage.d.ts +1 -1
  94. package/dist/coverage.d.ts.map +1 -1
  95. package/dist/cst/capture-buffer.d.ts +35 -0
  96. package/dist/cst/capture-buffer.d.ts.map +1 -1
  97. package/dist/cst/reflection-attach.d.ts +8 -0
  98. package/dist/cst/reflection-attach.d.ts.map +1 -0
  99. package/dist/cst/reflection-symbols.d.ts +10 -0
  100. package/dist/cst/reflection-symbols.d.ts.map +1 -0
  101. package/dist/cst/reflection.d.ts +2 -4
  102. package/dist/cst/reflection.d.ts.map +1 -1
  103. package/dist/cst/root-trivia-scope.d.ts +26 -0
  104. package/dist/cst/root-trivia-scope.d.ts.map +1 -0
  105. package/dist/cst/trivia-charscan.d.ts +18 -0
  106. package/dist/cst/trivia-charscan.d.ts.map +1 -0
  107. package/dist/cst/trivia-kinds.d.ts +19 -0
  108. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  109. package/dist/functional/doc.d.ts.map +1 -1
  110. package/dist/functional/run-tabled.d.ts +12 -0
  111. package/dist/functional/run-tabled.d.ts.map +1 -0
  112. package/dist/functional/run.d.ts +25 -42
  113. package/dist/functional/run.d.ts.map +1 -1
  114. package/dist/grammar-metadata.d.ts +3 -0
  115. package/dist/grammar-metadata.d.ts.map +1 -0
  116. package/dist/index.cjs +10499 -7478
  117. package/dist/index.cjs.map +3 -4
  118. package/dist/index.d.ts +24 -9
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +10499 -7478
  121. package/dist/index.js.map +3 -4
  122. package/dist/language-service/index.cjs +311 -121
  123. package/dist/language-service/index.cjs.map +3 -4
  124. package/dist/language-service/index.js +311 -121
  125. package/dist/language-service/index.js.map +3 -4
  126. package/dist/oracle/index.cjs.map +0 -1
  127. package/dist/oracle/index.js.map +0 -1
  128. package/dist/parse-context.d.ts +49 -0
  129. package/dist/parse-context.d.ts.map +1 -0
  130. package/dist/plugin/evaluator.d.ts +41 -2
  131. package/dist/plugin/evaluator.d.ts.map +1 -1
  132. package/dist/plugin/free-identifiers.d.ts +23 -0
  133. package/dist/plugin/free-identifiers.d.ts.map +1 -0
  134. package/dist/plugin/index.cjs +11381 -9442
  135. package/dist/plugin/index.cjs.map +3 -4
  136. package/dist/plugin/index.d.ts.map +1 -1
  137. package/dist/plugin/index.js +11381 -9442
  138. package/dist/plugin/index.js.map +3 -4
  139. package/dist/plugin/reducer-resolver.d.ts +17 -1
  140. package/dist/plugin/reducer-resolver.d.ts.map +1 -1
  141. package/dist/regex/classes.d.ts +28 -0
  142. package/dist/regex/classes.d.ts.map +1 -1
  143. package/dist/regex/first-set.d.ts +14 -0
  144. package/dist/regex/first-set.d.ts.map +1 -1
  145. package/dist/run/index.cjs +136 -51
  146. package/dist/run/index.cjs.map +2 -3
  147. package/dist/run/index.js +136 -51
  148. package/dist/run/index.js.map +2 -3
  149. package/dist/spec/index.cjs +2898 -1622
  150. package/dist/spec/index.cjs.map +3 -4
  151. package/dist/spec/index.js +2898 -1622
  152. package/dist/spec/index.js.map +3 -4
  153. package/dist/spec/model.d.ts +8 -0
  154. package/dist/spec/model.d.ts.map +1 -1
  155. package/dist/table/assemble.d.ts +307 -0
  156. package/dist/table/assemble.d.ts.map +1 -0
  157. package/dist/table/cell.d.ts +42 -0
  158. package/dist/table/cell.d.ts.map +1 -0
  159. package/dist/table/child-slots.d.ts +30 -0
  160. package/dist/table/child-slots.d.ts.map +1 -0
  161. package/dist/table/compile-rule-map.d.ts +136 -0
  162. package/dist/table/compile-rule-map.d.ts.map +1 -0
  163. package/dist/table/compile.d.ts +56 -0
  164. package/dist/table/compile.d.ts.map +1 -0
  165. package/dist/table/duplication-hook.d.ts +32 -0
  166. package/dist/table/duplication-hook.d.ts.map +1 -0
  167. package/dist/table/emit-assembly.d.ts +139 -0
  168. package/dist/table/emit-assembly.d.ts.map +1 -0
  169. package/dist/table/emit.d.ts +113 -0
  170. package/dist/table/emit.d.ts.map +1 -0
  171. package/dist/table/encode-baseline.d.ts +16 -0
  172. package/dist/table/encode-baseline.d.ts.map +1 -0
  173. package/dist/table/encode.d.ts +71 -0
  174. package/dist/table/encode.d.ts.map +1 -0
  175. package/dist/table/exec-baseline.d.ts +9 -0
  176. package/dist/table/exec-baseline.d.ts.map +1 -0
  177. package/dist/table/exec.d.ts +72 -0
  178. package/dist/table/exec.d.ts.map +1 -0
  179. package/dist/table/fold.d.ts +25 -0
  180. package/dist/table/fold.d.ts.map +1 -0
  181. package/dist/table/index.cjs +11375 -0
  182. package/dist/table/index.cjs.map +6 -0
  183. package/dist/table/index.d.ts +100 -0
  184. package/dist/table/index.d.ts.map +1 -0
  185. package/dist/table/index.js +11352 -0
  186. package/dist/table/index.js.map +6 -0
  187. package/dist/table/inspect.d.ts +17 -0
  188. package/dist/table/inspect.d.ts.map +1 -0
  189. package/dist/table/ops.d.ts +446 -0
  190. package/dist/table/ops.d.ts.map +1 -0
  191. package/dist/table/program.d.ts +493 -0
  192. package/dist/table/program.d.ts.map +1 -0
  193. package/dist/table/run-support.d.ts +38 -0
  194. package/dist/table/run-support.d.ts.map +1 -0
  195. package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
  196. package/dist/table/scan-shapes.d.ts.map +1 -0
  197. package/dist/table/site-labels.d.ts +58 -0
  198. package/dist/table/site-labels.d.ts.map +1 -0
  199. package/dist/table/stamp.d.ts +70 -0
  200. package/dist/table/stamp.d.ts.map +1 -0
  201. package/dist/types.d.ts +80 -24
  202. package/dist/types.d.ts.map +1 -1
  203. package/dist/version.d.ts +1 -1
  204. package/package.json +41 -3
  205. package/src/analysis/choice-cost-gate.ts +359 -0
  206. package/src/analysis/choice-cost-render.ts +293 -0
  207. package/src/analysis/choice-cost.ts +1008 -0
  208. package/src/analysis/commitment.ts +557 -0
  209. package/src/analysis/corpus.ts +130 -0
  210. package/src/analysis/diagnose-render.ts +625 -0
  211. package/src/analysis/diagnose.ts +317 -0
  212. package/src/analysis/diagnostics.ts +33 -0
  213. package/src/analysis/duplication.ts +1955 -0
  214. package/src/analysis/fix-render.ts +217 -0
  215. package/src/analysis/fix.ts +669 -0
  216. package/src/analysis/format-number.ts +31 -0
  217. package/src/analysis/gating.ts +792 -0
  218. package/src/analysis/grammar.ts +72 -0
  219. package/src/analysis/rebuild.ts +287 -0
  220. package/src/analysis/terminal.ts +336 -0
  221. package/src/cli/index.ts +413 -0
  222. package/src/combinators/adjacency.ts +171 -0
  223. package/src/combinators/attempt.ts +41 -0
  224. package/src/combinators/case-fold.ts +90 -0
  225. package/src/combinators/choice.ts +466 -0
  226. package/src/combinators/completions.ts +64 -0
  227. package/src/combinators/dispatch.ts +358 -0
  228. package/src/combinators/expect.ts +159 -0
  229. package/src/combinators/first-set.ts +322 -0
  230. package/src/combinators/gate.ts +50 -0
  231. package/src/combinators/grammar.ts +272 -0
  232. package/src/combinators/keywords.ts +145 -0
  233. package/src/combinators/literal.ts +102 -0
  234. package/src/combinators/map.ts +106 -0
  235. package/src/combinators/node.ts +345 -0
  236. package/src/combinators/not.ts +55 -0
  237. package/src/combinators/parser.ts +266 -0
  238. package/src/combinators/peek.ts +63 -0
  239. package/src/combinators/probe.ts +25 -0
  240. package/src/combinators/ref.ts +109 -0
  241. package/src/combinators/regex.ts +170 -0
  242. package/src/combinators/repeat.ts +561 -0
  243. package/src/combinators/scanTo.ts +393 -0
  244. package/src/combinators/sequence.ts +221 -0
  245. package/src/combinators/token.ts +120 -0
  246. package/src/combinators/trivia-boundary.ts +48 -0
  247. package/src/combinators/trivia-skip.ts +821 -0
  248. package/src/combinators/withCtx.ts +45 -0
  249. package/src/compiler/build-arity.ts +330 -0
  250. package/src/compiler/compile-linkable-table.ts +209 -0
  251. package/src/compiler/degradation.ts +266 -0
  252. package/src/compiler/fields.ts +191 -0
  253. package/src/compiler/grammar-coverage-ids.ts +129 -0
  254. package/src/compiler/ir-serialize.ts +615 -0
  255. package/src/compiler/linker.ts +954 -0
  256. package/src/compiler/token-alphabet.ts +209 -0
  257. package/src/compiler/token-dispatch.ts +353 -0
  258. package/src/compiler/token-scanner.ts +318 -0
  259. package/src/compiler/value-usage.ts +147 -0
  260. package/src/coverage.ts +383 -0
  261. package/src/cst/capture-buffer.ts +297 -0
  262. package/src/cst/host-mode.ts +69 -0
  263. package/src/cst/reflection-attach.ts +11 -0
  264. package/src/cst/reflection-symbols.ts +9 -0
  265. package/src/cst/reflection.ts +149 -0
  266. package/src/cst/relative-spans.ts +207 -0
  267. package/src/cst/root-trivia-scope.ts +30 -0
  268. package/src/cst/trivia-charscan.ts +312 -0
  269. package/src/cst/trivia-entries.ts +285 -0
  270. package/src/cst/trivia-index.ts +131 -0
  271. package/src/cst/trivia-kinds.ts +358 -0
  272. package/src/cst/types.ts +76 -0
  273. package/src/cst/walk.ts +101 -0
  274. package/src/functional/doc.ts +959 -0
  275. package/src/functional/run-tabled.ts +153 -0
  276. package/src/functional/run.ts +558 -0
  277. package/src/grammar-metadata.ts +2 -0
  278. package/src/index.ts +189 -0
  279. package/src/language-service/index.ts +221 -0
  280. package/src/line-index.ts +131 -0
  281. package/src/oracle/digest.ts +620 -0
  282. package/src/oracle/index.ts +55 -0
  283. package/src/parse-context.ts +91 -0
  284. package/src/plugin/direct-builder-static.ts +102 -0
  285. package/src/plugin/evaluator.ts +1505 -0
  286. package/src/plugin/free-identifiers.ts +338 -0
  287. package/src/plugin/index.ts +2281 -0
  288. package/src/plugin/reducer-resolver.ts +606 -0
  289. package/src/recovery/scan.ts +155 -0
  290. package/src/regex/classes.ts +183 -0
  291. package/src/regex/first-set.ts +316 -0
  292. package/src/run/index.ts +31 -0
  293. package/src/spec/ebnf.ts +128 -0
  294. package/src/spec/index.ts +54 -0
  295. package/src/spec/model.ts +636 -0
  296. package/src/spec/railroad-lib.ts +559 -0
  297. package/src/spec/railroad.ts +256 -0
  298. package/src/table/assemble.ts +2994 -0
  299. package/src/table/cell.ts +42 -0
  300. package/src/table/child-slots.ts +113 -0
  301. package/src/table/compile-rule-map.ts +336 -0
  302. package/src/table/compile.ts +244 -0
  303. package/src/table/duplication-hook.ts +89 -0
  304. package/src/table/emit-assembly.ts +1766 -0
  305. package/src/table/emit.ts +418 -0
  306. package/src/table/encode-baseline.ts +340 -0
  307. package/src/table/encode.ts +1569 -0
  308. package/src/table/exec-baseline.ts +537 -0
  309. package/src/table/exec.ts +1672 -0
  310. package/src/table/fold.ts +38 -0
  311. package/src/table/index.ts +108 -0
  312. package/src/table/inspect.ts +52 -0
  313. package/src/table/ops.ts +459 -0
  314. package/src/table/program.ts +855 -0
  315. package/src/table/run-support.ts +66 -0
  316. package/src/table/scan-shapes.ts +1578 -0
  317. package/src/table/site-labels.ts +228 -0
  318. package/src/table/stamp.ts +229 -0
  319. package/src/types.ts +563 -0
  320. package/src/version.ts +16 -0
  321. package/dist/compiler/codegen.d.ts +0 -334
  322. package/dist/compiler/codegen.d.ts.map +0 -1
  323. package/dist/compiler/inline-build.d.ts +0 -11
  324. package/dist/compiler/inline-build.d.ts.map +0 -1
  325. package/dist/compiler/inline-callback.d.ts +0 -21
  326. package/dist/compiler/inline-callback.d.ts.map +0 -1
  327. package/dist/compiler/line-index.d.ts +0 -2
  328. package/dist/compiler/line-index.d.ts.map +0 -1
  329. package/dist/compiler/scannable-run.d.ts.map +0 -1
  330. package/dist/compiler/scannable-terminal.d.ts +0 -23
  331. package/dist/compiler/scannable-terminal.d.ts.map +0 -1
  332. package/dist/compiler/trivia-fast-path.d.ts +0 -38
  333. package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
@@ -0,0 +1,155 @@
1
+ import type { Combinator, FirstSet, ParseContext, ParseError, ParseResult, RecoveryHelpers } from '../types.ts'
2
+ import { cstCaptureActive, pushCstChild } from '../cst/capture-buffer.ts'
3
+ import { annotateSpanFromLineContext, recordLineRangeFromContext } from '../line-index.ts'
4
+
5
+ /**
6
+ * Recovery MECHANISM — sync-source-agnostic. These primitives implement "scan
7
+ * forward to a sync token and emit a ParseError" and "is the sync token here?".
8
+ * They are deliberately independent of WHERE the sync sentinel comes from: the
9
+ * grammar never carries recovery config. Sync points are inferred automatically
10
+ * from grammar structure (see `infer.ts`) and may be overridden externally by the
11
+ * language service via `ctx._listSync`. This is the salvaged core of the old
12
+ * `combinators/recover-scan.ts`.
13
+ */
14
+
15
+ /**
16
+ * Run `sentinel` at `pos` as PURE lookahead: succeed/fail only, with every
17
+ * side-effecting sink suppressed (CST leaves/children/trivia, the trivia log,
18
+ * the recovery error sink, and the completions probe). A recovery scan probes the
19
+ * sentinel at many positions and must not capture leaves, log trivia, or pollute
20
+ * `completionsAt`'s furthest-failure set as a byproduct of looking ahead.
21
+ */
22
+ function probeAt(sentinel: Combinator<unknown>, input: string, pos: number, ctx: ParseContext): boolean {
23
+ const s = {
24
+ _probe: ctx._probe, _errors: ctx._errors,
25
+ _cstLeaves: ctx._cstLeaves, _cstChildren: ctx._cstChildren, _cstRawChildren: ctx._cstRawChildren,
26
+ _cstTriviaLog: ctx._cstTriviaLog, _triviaLog: ctx._triviaLog, _rootTriviaLog: ctx._rootTriviaLog, _fields: ctx._fields, _cstBuf: ctx._cstBuf,
27
+ _tolerant: ctx._tolerant, _sync: ctx._sync,
28
+ }
29
+ ctx._probe = undefined; ctx._errors = undefined
30
+ ctx._cstLeaves = undefined; ctx._cstChildren = undefined; ctx._cstRawChildren = undefined
31
+ ctx._cstTriviaLog = undefined; ctx._triviaLog = undefined; ctx._rootTriviaLog = undefined; ctx._fields = undefined; ctx._cstBuf = undefined
32
+ // A sentinel is pure lookahead: it must not itself enter tolerant recovery (nor
33
+ // read a stale outer `_sync`). Otherwise a sentinel that composes many/sepBy would
34
+ // run recovery on every probed position — wasted work, and it could resync to the
35
+ // enclosing list's sync token. Cleared here and restored in `finally`.
36
+ ctx._tolerant = undefined; ctx._sync = undefined
37
+ try {
38
+ return sentinel.parse(input, pos, ctx).ok
39
+ } finally {
40
+ ctx._probe = s._probe; ctx._errors = s._errors
41
+ ctx._cstLeaves = s._cstLeaves; ctx._cstChildren = s._cstChildren; ctx._cstRawChildren = s._cstRawChildren
42
+ ctx._cstTriviaLog = s._cstTriviaLog; ctx._triviaLog = s._triviaLog; ctx._rootTriviaLog = s._rootTriviaLog; ctx._fields = s._fields; ctx._cstBuf = s._cstBuf
43
+ ctx._tolerant = s._tolerant; ctx._sync = s._sync
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Combine two recovery sentinels into one that matches when either does — used by
49
+ * tolerant `sepBy` to resync to its own separator OR the enclosing delimiter. Cold
50
+ * path only; a bare wrapper so we don't pull `choice()` (and its analysis) in.
51
+ */
52
+ export function orSentinel(
53
+ a: Combinator<unknown>,
54
+ b: Combinator<unknown> | undefined,
55
+ ): Combinator<unknown> {
56
+ if (b === undefined) return a
57
+ return {
58
+ _tag: 'orSentinel',
59
+ _meta: { firstSet: { kind: 'any' }, canMatchNewline: false, isTrivia: false },
60
+ _def: { tag: 'unknown' },
61
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<unknown> {
62
+ const ra = a.parse(input, pos, ctx)
63
+ if (ra.ok) return ra
64
+ return b.parse(input, pos, ctx)
65
+ },
66
+ }
67
+ }
68
+
69
+ /**
70
+ * A lightweight zero-width sentinel that "matches" (succeeds, consuming nothing)
71
+ * when the input code point at `pos` starts the given first set. Sync inference
72
+ * builds one from a list's follow set so a nested list can resync to the enclosing
73
+ * delimiter with no grammar annotation. Returns `null` when the first set is `any`
74
+ * or `empty` (no usable inference).
75
+ */
76
+ export function firstSetSentinel(fs: FirstSet): Combinator<null> | null {
77
+ if (fs.kind !== 'ranges' || fs.ranges.length === 0) return null
78
+ const ranges = fs.ranges
79
+ return {
80
+ _tag: 'firstSetSentinel',
81
+ _meta: { firstSet: fs, canMatchNewline: false, isTrivia: false },
82
+ _def: { tag: 'unknown' },
83
+ parse(input: string, pos: number): ParseResult<null> {
84
+ const code = pos < input.length ? input.codePointAt(pos) : undefined
85
+ if (code !== undefined) {
86
+ for (const r of ranges) {
87
+ if (code >= r.lo && code <= r.hi) return { ok: true, value: null, span: { start: pos, end: pos } }
88
+ }
89
+ }
90
+ return { ok: false, expected: [], span: { start: pos, end: pos } }
91
+ },
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Cold-path recovery scan shared by tolerant `many()` / `sepBy()`. Scans forward
97
+ * from `from` until `sync` matches (or EOF) — probing the sentinel as pure
98
+ * lookahead — emits a {@link ParseError} spanning the skipped range, and pushes it
99
+ * to `ctx._errors`. The sync token is NOT consumed; the caller's loop resumes from
100
+ * `end`. Only ever reached after an element has failed, so entirely off the hot path.
101
+ *
102
+ * The caller owns the loop guard: `sepBy` progress is driven by its separator (a
103
+ * zero-width error for a missing element is fine); `many` checks `matchesAt(sync)`
104
+ * itself and breaks when sitting on the terminator, so a zero-width failure can
105
+ * never spin.
106
+ */
107
+ export function recoverScan(
108
+ input: string,
109
+ from: number,
110
+ ctx: ParseContext,
111
+ sync: Combinator<unknown>,
112
+ expected: string[],
113
+ ): { error: ParseError; end: number } {
114
+ let scanPos = from
115
+ while (scanPos < input.length && !probeAt(sync, input, scanPos, ctx)) scanPos++
116
+ const span = { start: from, end: scanPos }
117
+ if (ctx.trackLines) recordLineRangeFromContext(ctx, input, from, scanPos)
118
+ const error: ParseError = { _tag: 'parseError', span: ctx.trackLines ? annotateSpanFromLineContext(span, ctx) : span, expected }
119
+ ctx._errors?.push(error)
120
+ return { error, end: scanPos }
121
+ }
122
+
123
+ /**
124
+ * Does `sentinel` match at `pos` (pure lookahead)? Used by the tolerant list loops
125
+ * to detect "we are sitting on the sync/terminator token" without consuming it or
126
+ * capturing anything.
127
+ */
128
+ export function matchesAt(
129
+ sentinel: Combinator<unknown>,
130
+ input: string,
131
+ pos: number,
132
+ ctx: ParseContext,
133
+ ): boolean {
134
+ return probeAt(sentinel, input, pos, ctx)
135
+ }
136
+
137
+ /**
138
+ * Embed a recovered error as a `parseError` CST child at the recovery point, so the
139
+ * error lives IN the tree (rides reused subtrees across incremental edits; a tree
140
+ * walk finds every diagnostic) — not only in the flat `ctx._errors` side-channel.
141
+ * No-op when CST capture is off (value/AST mode). Both the interpreter list loops
142
+ * and the compiled recovery branches call this (the latter via `_ctx._rec.capture`),
143
+ * so the embedded node is identical on both paths.
144
+ */
145
+ export function captureError(ctx: ParseContext, error: ParseError): void {
146
+ if (cstCaptureActive(ctx)) pushCstChild(ctx, error, error)
147
+ }
148
+
149
+ /**
150
+ * The recovery-helper bundle handed to a tolerant parse via `ctx._rec`. A COMPILED
151
+ * grammar can't import these, so the driver (`run`, `parseDoc`) injects them; the
152
+ * interpreter list loops read the same bundle, guaranteeing byte-identical recovery
153
+ * across paths. Shared here so every driver installs the exact same instance.
154
+ */
155
+ export const REC: RecoveryHelpers = { scan: recoverScan, at: matchesAt, or: orSentinel, sentinel: firstSetSentinel, capture: captureError }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Low-level regex char-class primitives, shared by every hand-rolled regex
3
+ * analysis in the codebase — the interpreter's first-set walker
4
+ * (`./first-set.ts`), `regex()`'s short-scan fast path, and codegen's scannable
5
+ * lowering (`../compiler/scannable-run.ts`). Kept dependency-free (no combinator,
6
+ * no codegen imports) so it lands in even the leanest interpreter bundle.
7
+ *
8
+ * Everything here parses a regex STRUCTURE to code-point ranges; nothing encodes
9
+ * a specific byte meaning ("this is whitespace"). `\d`/`\w` lower to their ASCII
10
+ * ranges (correct for the default, non-`u` engine); `\s` uses the fixed
11
+ * `SPACE_RANGES` set (unaffected by the `u` flag).
12
+ */
13
+
14
+ /** Single-char escapes whose code point is fixed regardless of context. */
15
+ export const CLASS_ESCAPES: Record<string, number> = { t: 9, n: 10, r: 13, f: 12, v: 11, '0': 0 }
16
+
17
+ /**
18
+ * `\s`'s code-point set per the spec's `WhiteSpace` + `LineTerminator`
19
+ * productions — TAB/LF/VT/FF/CR, SPACE, NBSP, and the Unicode `Zs` space
20
+ * separators. Fixed regardless of the `u` flag, so always safe to lower.
21
+ */
22
+ export const SPACE_RANGES: Array<[number, number]> = [
23
+ [9, 13], [32, 32], [160, 160], [5760, 5760], [8192, 8202],
24
+ [8232, 8232], [8233, 8233], [8239, 8239], [8287, 8287], [12288, 12288], [65279, 65279],
25
+ ]
26
+
27
+ /**
28
+ * ASCII code-point ranges for the shorthand classes we lower. `\d`/`\w` are
29
+ * ASCII-only in the default (non-`u`) engine; `\s` maps to `SPACE_RANGES`.
30
+ */
31
+ export function shorthandRanges(ch: 'd' | 'w' | 's'): Array<[number, number]> {
32
+ if (ch === 'd') return [[48, 57]]
33
+ if (ch === 's') return SPACE_RANGES
34
+ // Order matches the canonical `scannable-run` lowering (underscore last), so
35
+ // `ScanShape` output and generated code stay byte-identical. Membership is
36
+ // order-independent (ranges are iterated / the FirstSet union sorts), so the
37
+ // non-sorted order is purely representational — see PR #16 discussion.
38
+ return [[48, 57], [65, 90], [97, 122], [95, 95]]
39
+ }
40
+
41
+ /** `\uXXXX` at `body[i]` → its code point and the index past it, or null. */
42
+ export function readUnicodeEscape(body: string, i: number): { cp: number; next: number } | null {
43
+ if (body[i] !== '\\' || body[i + 1] !== 'u') return null
44
+ const hex = body.slice(i + 2, i + 6)
45
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) return null
46
+ return { cp: Number.parseInt(hex, 16), next: i + 6 }
47
+ }
48
+
49
+ /** Regex syntax characters that cannot appear in a LITERAL fragment unescaped. */
50
+ const META = new Set('()[]{}*+?|^$.'.split(''))
51
+
52
+ /**
53
+ * A regex fragment that is a plain run of literal characters → its code points,
54
+ * or null when the fragment contains any operator (so a caller can never mistake
55
+ * `a*` for the two-char literal `a*`). An empty fragment returns null, because
56
+ * every caller wants "at least one char to match".
57
+ */
58
+ export function literalCodePoints(frag: string): number[] | null {
59
+ const out: number[] = []
60
+ let i = 0
61
+ while (i < frag.length) {
62
+ const ch = frag[i]!
63
+ if (ch === '\\') {
64
+ const e = frag[i + 1]
65
+ if (e === undefined) return null
66
+ out.push(e in CLASS_ESCAPES ? CLASS_ESCAPES[e]! : e.codePointAt(0)!)
67
+ i += 2
68
+ continue
69
+ }
70
+ if (META.has(ch)) return null
71
+ out.push(ch.codePointAt(0)!)
72
+ i += 1
73
+ }
74
+ return out.length ? out : null
75
+ }
76
+
77
+ /**
78
+ * Index of the `]` that CLOSES the class opening at `body[0]`, honouring `\]`,
79
+ * or −1 if the class is unterminated. A leading `^` is negation, never a member;
80
+ * every other char up to the first unescaped `]` is. This is JS's own non-`u`
81
+ * reading, under which `[]` is the empty class — so in `[]]` the FIRST `]`
82
+ * closes and a literal `]` follows, and this returns 1, not 2.
83
+ */
84
+ function classCloseIndex(body: string): number {
85
+ let i = 1
86
+ if (body[i] === '^') i++
87
+ while (i < body.length) {
88
+ const ch = body[i]
89
+ if (ch === '\\') { i += 2; continue }
90
+ if (ch === ']') return i
91
+ i++
92
+ }
93
+ return -1
94
+ }
95
+
96
+ /**
97
+ * A single-character MATCHER fragment as a (possibly negated) range set: a
98
+ * bracketed class `[…]`/`[^…]`, a `\d`/`\w`/`\s` shorthand, or one literal char.
99
+ * Anything wider (a group, a multi-char literal, `.`) returns null.
100
+ *
101
+ * "Bracketed class" means the WHOLE fragment is ONE class. Testing only that it
102
+ * opens with `[` and ends with `]` is a different, weaker question, and
103
+ * `[ \t\n\r\f]*[\$(]` answers it while being a SEQUENCE — a whitespace run, then
104
+ * one of `$(`. Read as a single class its members become the garbage union of
105
+ * everything between the OUTER brackets, whitespace and `*` and `[` included, so
106
+ * scss's `\+(?=[ \t\n\r\f]*[\$(])` matched a `+` before a space; the shape oracle
107
+ * caught it at 203 positions of the scss corpus. Every caller here asks "is this
108
+ * ONE char matcher", so a fragment that is not gets null — declining costs a
109
+ * lowering or widens a first-set to `any()`, both of which only forgo a fast
110
+ * path, whereas accepting yields a wrong member set, which is a wrong scan or a
111
+ * wrong dispatch.
112
+ */
113
+ export function parseClassOperand(body: string): { ranges: Array<[number, number]>; negated: boolean } | null {
114
+ if (body === '\\d' || body === '\\w' || body === '\\s') {
115
+ return { ranges: shorthandRanges(body[1] as 'd' | 'w' | 's'), negated: false }
116
+ }
117
+ if (body.length >= 2 && body[0] === '[') {
118
+ // The class must close at the LAST char; anything after it is a second token
119
+ // (a quantifier, another class, a literal) that this fragment cannot express.
120
+ if (classCloseIndex(body) !== body.length - 1) return null
121
+ let inner = body.slice(1, -1)
122
+ const negated = inner.startsWith('^')
123
+ if (negated) inner = inner.slice(1)
124
+ const ranges = parseClassRanges(inner)
125
+ return ranges ? { ranges, negated } : null
126
+ }
127
+ const cps = literalCodePoints(body)
128
+ if (cps && cps.length === 1) return { ranges: [[cps[0]!, cps[0]!]], negated: false }
129
+ return null
130
+ }
131
+
132
+ type ClassAtom = { cp: number } | { set: Array<[number, number]> }
133
+
134
+ /**
135
+ * Parse a regex char-class body (the chars BETWEEN `[` and `]`, negation `^`
136
+ * already stripped by the caller) to code-point ranges. `\d`/`\w`/`\s` expand to
137
+ * their ranges; `\uXXXX` and the fixed single-char escapes resolve to their code
138
+ * point; any other letter escape (`\D`, `\W`, `\S`, `\b`, …) returns null rather
139
+ * than being mis-read as a literal letter, so callers fall back to a safe
140
+ * over-approximation instead of a wrong set.
141
+ */
142
+ export function parseClassRanges(body: string): Array<[number, number]> | null {
143
+ const ranges: Array<[number, number]> = []
144
+ let i = 0
145
+ const readAtom = (): ClassAtom | null => {
146
+ const ch = body[i]
147
+ if (ch === undefined) return null
148
+ if (ch === '\\') {
149
+ const uni = readUnicodeEscape(body, i)
150
+ if (uni) {
151
+ i = uni.next
152
+ return { cp: uni.cp }
153
+ }
154
+ const e = body[i + 1]
155
+ if (e === undefined) return null
156
+ i += 2
157
+ if (e in CLASS_ESCAPES) return { cp: CLASS_ESCAPES[e]! }
158
+ if (e === 'd' || e === 'w' || e === 's') return { set: shorthandRanges(e) }
159
+ // Any other letter escape is a class we can't safely lower (\D, \W, \S, …).
160
+ if ((e >= 'a' && e <= 'z') || (e >= 'A' && e <= 'Z')) return null
161
+ return { cp: e.codePointAt(0)! }
162
+ }
163
+ i += ch.length
164
+ return { cp: ch.codePointAt(0)! }
165
+ }
166
+ while (i < body.length) {
167
+ const lo = readAtom()
168
+ if (lo === null) return null
169
+ if ('set' in lo) {
170
+ ranges.push(...lo.set)
171
+ continue
172
+ }
173
+ if (body[i] === '-' && body[i + 1] !== undefined && body[i + 1] !== ']') {
174
+ i += 1
175
+ const hi = readAtom()
176
+ if (hi === null || 'set' in hi) return null
177
+ ranges.push([lo.cp, hi.cp])
178
+ } else {
179
+ ranges.push([lo.cp, lo.cp])
180
+ }
181
+ }
182
+ return ranges.length ? ranges : null
183
+ }
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Hand-rolled first-set analysis for `regex()` terminals — the interpreter's
3
+ * replacement for the old `regexp-tree`-backed analyzer (~264 KB). A `regex()`
4
+ * terminal's first-set is used ONLY to let `choice()` do first-char dispatch
5
+ * (see `choice.ts`); it is never consulted for whether a match succeeds. So this
6
+ * analyzer only has to be SOUND as an OVER-approximation: every char the pattern
7
+ * can actually start with must be in the returned set. Any construct it can't
8
+ * pin down widens to `any()` (— "could start with anything" —), which only
9
+ * disables a dispatch fast path, never changes a parse. Under-approximating
10
+ * (dropping a real start char) WOULD be a bug: dispatch would skip a matching
11
+ * arm. The soundness fuzz test (`test/unit/regex-first-set.test.ts`) checks this
12
+ * against the real `RegExp` engine as an oracle.
13
+ *
14
+ * It parses the regex into a tiny AST (single-char matchers, sequence,
15
+ * alternation, repetition, zero-width) reusing the shared char-class primitives
16
+ * in `./classes.ts`, then walks the AST unioning first-sets through the nullable
17
+ * prefix — the same shape the `regexp-tree` walker used.
18
+ *
19
+ * Note: flags are not consulted (matching the prior `regexp-tree` seam, which
20
+ * parsed `/${source}/` with no flags), so an `/i` pattern's first-set is its
21
+ * literal-case set. Sound for dispatch because the interpreter's `/i` regex arms
22
+ * are compared case-sensitively at the same seam.
23
+ */
24
+ import type { FirstSet } from '../types.ts'
25
+ import { any, empty, fromRange, union } from '../combinators/first-set.ts'
26
+ import { shorthandRanges, parseClassRanges } from './classes.ts'
27
+
28
+ // A minimal regex AST. Each node answers two questions: what first chars can a
29
+ // non-empty match begin with (`firstSet`), and can the node match zero chars
30
+ // (`nullable`). `any` is the over-approximation sentinel for anything we don't
31
+ // model precisely.
32
+ type Node =
33
+ | { t: 'char'; set: FirstSet; nl: boolean } // matches exactly one char from `set`
34
+ | { t: 'any' } // unknown 1-char-ish construct: could start with anything
35
+ | { t: 'empty' } // zero-width: anchors, lookaround, word boundary
36
+ | { t: 'seq'; parts: Node[] }
37
+ | { t: 'alt'; arms: Node[] }
38
+ | { t: 'rep'; node: Node; min: number } // quantified; min 0 ⇒ nullable
39
+
40
+ const ANY: Node = { t: 'any' }
41
+ const EMPTY: Node = { t: 'empty' }
42
+
43
+ function charNode(set: FirstSet, nl: boolean): Node {
44
+ return { t: 'char', set, nl }
45
+ }
46
+
47
+ function rangesToSet(ranges: Array<[number, number]>): FirstSet {
48
+ let fs: FirstSet = empty()
49
+ for (const [lo, hi] of ranges) fs = union(fs, fromRange(lo, hi))
50
+ return fs
51
+ }
52
+
53
+ function rangesHaveNewline(ranges: Array<[number, number]>): boolean {
54
+ return ranges.some(([lo, hi]) => lo <= 0x0a && 0x0a <= hi)
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Parser: recursive descent over a raw regex source (no surrounding delimiters).
59
+ // Throws `Bail` on a structure it can't parse (unbalanced group/class); the
60
+ // entry point catches it and degrades the WHOLE pattern to `any()`. A construct
61
+ // that parses but isn't modeled (a backreference, an exotic escape) becomes an
62
+ // `any` NODE instead — sound and more precise, since it only pollutes the
63
+ // first-set if reached in the nullable prefix.
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /** Thrown on a structure the parser can't handle (unbalanced group/class); the
67
+ * entry point catches it and degrades the whole pattern to `any()`. */
68
+ const BAIL = Symbol('regex-bail')
69
+
70
+ /**
71
+ * Parse a raw regex `src` (no delimiters) into a `Node`, or throw `BAIL`.
72
+ * Closure over a single cursor `i` — recursive-descent: alt → seq → quantified
73
+ * → atom.
74
+ */
75
+ function parseRegex(src: string): Node {
76
+ let i = 0
77
+
78
+ const parseAlt = (): Node => {
79
+ const arms: Node[] = [parseSeq()]
80
+ while (src[i] === '|') { i++; arms.push(parseSeq()) }
81
+ return arms.length === 1 ? arms[0]! : { t: 'alt', arms }
82
+ }
83
+
84
+ const parseSeq = (): Node => {
85
+ const parts: Node[] = []
86
+ while (i < src.length) {
87
+ const ch = src[i]!
88
+ if (ch === '|' || ch === ')') break
89
+ parts.push(parseQuantified())
90
+ }
91
+ if (parts.length === 0) return EMPTY
92
+ return parts.length === 1 ? parts[0]! : { t: 'seq', parts }
93
+ }
94
+
95
+ const parseQuantified = (): Node => {
96
+ const atom = parseAtom()
97
+ const ch = src[i]
98
+ let min: number | null = null
99
+ if (ch === '*') { min = 0; i++ }
100
+ else if (ch === '+') { min = 1; i++ }
101
+ else if (ch === '?') { min = 0; i++ }
102
+ else if (ch === '{') { min = parseBraceQuantifier() }
103
+ if (min === null) return atom
104
+ // Consume a lazy `?` on the quantifier — it changes greediness, not first-set.
105
+ if (src[i] === '?') i++
106
+ return { t: 'rep', node: atom, min }
107
+ }
108
+
109
+ // Parse `{n}` / `{n,}` / `{n,m}` at `i` (on `{`), returning the minimum; null
110
+ // (leaving `{` unconsumed) if it isn't a well-formed brace quantifier, so the
111
+ // caller keeps the `{` as the literal atom it already parsed.
112
+ const parseBraceQuantifier = (): number | null => {
113
+ const close = src.indexOf('}', i)
114
+ if (close === -1) return null
115
+ const m = /^(\d+)(?:,(\d*))?$/.exec(src.slice(i + 1, close))
116
+ if (!m) return null
117
+ i = close + 1
118
+ return Number.parseInt(m[1]!, 10)
119
+ }
120
+
121
+ const parseAtom = (): Node => {
122
+ const ch = src[i]!
123
+ switch (ch) {
124
+ case '(': return parseGroup()
125
+ case '[': return parseClass()
126
+ case '.': i++; return charNode(any(), false) // `.` matches any char except line terminators
127
+ case '\\': return parseEscape()
128
+ case '^':
129
+ case '$': i++; return EMPTY // anchors are zero-width
130
+ case '{': i++; return charNode(fromRange(0x7b, 0x7b), false) // a `{` with nothing to quantify is a literal brace
131
+ default:
132
+ i += ch.length
133
+ return charNode(fromRange(ch.codePointAt(0)!, ch.codePointAt(0)!), ch === '\n')
134
+ }
135
+ }
136
+
137
+ const parseGroup = (): Node => {
138
+ i++ // consume '('
139
+ // Group prefixes: (?:…) (?=…) (?!…) (?<=…) (?<!…) (?<name>…)
140
+ let lookaround = false
141
+ if (src[i] === '?') {
142
+ const c1 = src[i + 1]
143
+ if (c1 === ':') i += 2
144
+ else if (c1 === '=' || c1 === '!') { i += 2; lookaround = true }
145
+ else if (c1 === '<') {
146
+ const c2 = src[i + 2]
147
+ if (c2 === '=' || c2 === '!') { i += 3; lookaround = true } // lookbehind
148
+ else { // named group (?<name>…): skip to '>'
149
+ const gt = src.indexOf('>', i)
150
+ if (gt === -1) throw BAIL
151
+ i = gt + 1
152
+ }
153
+ } else throw BAIL // (? followed by something unexpected
154
+ }
155
+ const inner = parseAlt()
156
+ if (src[i] !== ')') throw BAIL
157
+ i++ // consume ')'
158
+ return lookaround ? EMPTY : inner // lookaround is zero-width
159
+ }
160
+
161
+ const parseClass = (): Node => {
162
+ // Read `[ ... ]`, honoring `\]`; detect leading `^` negation.
163
+ let k = i + 1
164
+ const negated = src[k] === '^'
165
+ if (negated) k++
166
+ let body = ''
167
+ while (k < src.length && src[k] !== ']') {
168
+ if (src[k] === '\\') { body += src[k]! + (src[k + 1] ?? ''); k += 2 }
169
+ else { body += src[k]!; k++ }
170
+ }
171
+ if (src[k] !== ']') throw BAIL
172
+ i = k + 1
173
+ const ranges = parseClassRanges(body)
174
+ // A negated class can start with (almost) any char, so the first-set widens to
175
+ // `any`, but its newline capability is still cheap and useful to know:
176
+ // `[^,\r\n]*` is a workhorse "field until line end" regex and must not force
177
+ // line-tracking code to rescan every field.
178
+ if (negated) return charNode(any(), !ranges || !rangesHaveNewline(ranges))
179
+ if (!ranges) return charNode(any(), true) // unparseable class → widen
180
+ return charNode(rangesToSet(ranges), rangesHaveNewline(ranges))
181
+ }
182
+
183
+ const parseEscape = (): Node => {
184
+ const e = src[i + 1]
185
+ if (e === undefined) throw BAIL
186
+ // \uXXXX
187
+ if (e === 'u' && /^[0-9a-fA-F]{4}$/.test(src.slice(i + 2, i + 6))) {
188
+ const cp = Number.parseInt(src.slice(i + 2, i + 6), 16)
189
+ i += 6
190
+ return charNode(fromRange(cp, cp), cp === 0x0a)
191
+ }
192
+ // \xHH
193
+ if (e === 'x' && /^[0-9a-fA-F]{2}$/.test(src.slice(i + 2, i + 4))) {
194
+ const cp = Number.parseInt(src.slice(i + 2, i + 4), 16)
195
+ i += 4
196
+ return charNode(fromRange(cp, cp), cp === 0x0a)
197
+ }
198
+ i += 2
199
+ switch (e) {
200
+ case 'd': return charNode(rangesToSet(shorthandRanges('d')), false)
201
+ case 'w': return charNode(rangesToSet(shorthandRanges('w')), false)
202
+ case 's': return charNode(rangesToSet(shorthandRanges('s')), true)
203
+ case 'D': case 'W': case 'S': return charNode(any(), true) // negated shorthand → widen
204
+ case 'b': case 'B': return EMPTY // word boundary: zero-width
205
+ case 'n': return charNode(fromRange(0x0a, 0x0a), true)
206
+ case 'r': return charNode(fromRange(0x0d, 0x0d), false)
207
+ case 't': return charNode(fromRange(0x09, 0x09), false)
208
+ case 'f': return charNode(fromRange(0x0c, 0x0c), false)
209
+ case 'v': return charNode(fromRange(0x0b, 0x0b), false)
210
+ case '0': return charNode(fromRange(0, 0), false)
211
+ default:
212
+ // A digit escape is a backreference we don't resolve → widen.
213
+ if (e >= '1' && e <= '9') return ANY
214
+ // Any other escape is a literal char (escaped metachar, `\/`, etc.).
215
+ return charNode(fromRange(e.codePointAt(0)!, e.codePointAt(0)!), e === '\n')
216
+ }
217
+ }
218
+
219
+ const node = parseAlt()
220
+ if (i < src.length) throw BAIL // leftover (e.g. an unbalanced `)`)
221
+ return node
222
+ }
223
+
224
+ // ---------------------------------------------------------------------------
225
+ // Walkers over the AST.
226
+ // ---------------------------------------------------------------------------
227
+
228
+ /** Can `n` succeed consuming zero characters? Err toward `true` when unsure —
229
+ * over-estimating nullability only widens the (over-approximated) first-set. */
230
+ function nullable(n: Node): boolean {
231
+ switch (n.t) {
232
+ case 'char': return false
233
+ case 'any': return true
234
+ case 'empty': return true
235
+ case 'seq': return n.parts.every(nullable)
236
+ case 'alt': return n.arms.some(nullable)
237
+ case 'rep': return n.min === 0 || nullable(n.node)
238
+ }
239
+ }
240
+
241
+ /** First chars a non-empty match of `n` can begin with (over-approximated). */
242
+ function firstSet(n: Node): FirstSet {
243
+ switch (n.t) {
244
+ case 'char': return n.set
245
+ case 'any': return any()
246
+ case 'empty': return empty()
247
+ case 'rep': return firstSet(n.node)
248
+ case 'alt': {
249
+ let fs: FirstSet = empty()
250
+ for (const arm of n.arms) fs = union(fs, firstSet(arm))
251
+ return fs
252
+ }
253
+ case 'seq': {
254
+ // Union each part's first-set through the nullable prefix: a leading
255
+ // nullable part lets a LATER part's first chars start the sequence.
256
+ let fs: FirstSet = empty()
257
+ for (const part of n.parts) {
258
+ fs = union(fs, firstSet(part))
259
+ if (!nullable(part)) break
260
+ }
261
+ return fs
262
+ }
263
+ }
264
+ }
265
+
266
+ /** Whether a successful match can consume a newline anywhere. This is tracked
267
+ * separately from `firstSet`: dispatch only cares about the first consumed char,
268
+ * while optional line tracking cares whether the matched span needs scanning. */
269
+ function canConsumeNewline(n: Node): boolean {
270
+ switch (n.t) {
271
+ case 'char': return n.nl
272
+ case 'any': return true
273
+ case 'empty': return false
274
+ case 'rep': return canConsumeNewline(n.node)
275
+ case 'alt': return n.arms.some(canConsumeNewline)
276
+ case 'seq': return n.parts.some(canConsumeNewline)
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Compute a regex terminal's first-set and newline-start flag from its SOURCE
282
+ * (no delimiters, no flags). Over-approximates: an unparseable pattern degrades
283
+ * to `{ any, canMatchNewline: true }` — the same conservative value the prior
284
+ * `regexp-tree` seam returned on a parse failure.
285
+ */
286
+ /**
287
+ * Can `source` match ZERO characters?
288
+ *
289
+ * Structural, over the same AST `firstSetFromRegex` uses — which already models
290
+ * every zero-width construct as `EMPTY`: anchors (`^`/`$`), lookaround
291
+ * (`(?=…)`/`(?!…)`/`(?<=…)`/`(?<!…)`) and word boundaries. That is what makes
292
+ * this usable as a proof of consumption: a pattern is zero-width exactly when
293
+ * the AST says it is nullable, so a lookaround BURIED inside an otherwise
294
+ * mandatory pattern no longer taints the whole answer.
295
+ *
296
+ * Errs toward `true` (nullable) — an unparseable pattern is assumed able to
297
+ * match empty, so callers that delete code on a `false` keep their proof.
298
+ */
299
+ export function regexCanMatchEmpty(source: string): boolean {
300
+ try { return nullable(parseRegex(source)) } catch { return true }
301
+ }
302
+
303
+ export function firstSetFromRegex(source: string): { firstSet: FirstSet; canMatchNewline: boolean } {
304
+ let ast: Node
305
+ try {
306
+ ast = parseRegex(source)
307
+ } catch {
308
+ return { firstSet: any(), canMatchNewline: true }
309
+ }
310
+ // A NULLABLE pattern (can match zero chars) must widen to `any()`: as a
311
+ // `choice()` arm it can succeed with an empty match at ANY position, so a
312
+ // narrow first-set would let first-char dispatch wrongly skip it. Only a
313
+ // pattern that must consume ≥1 char has a meaningful first-set.
314
+ if (nullable(ast)) return { firstSet: any(), canMatchNewline: canConsumeNewline(ast) }
315
+ return { firstSet: firstSet(ast), canMatchNewline: canConsumeNewline(ast) }
316
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `parseman/run` — the minimal entry for EXECUTING an already-compiled grammar.
3
+ *
4
+ * The macro removes the combinators, the compiler and the codegen from your
5
+ * bundle, but not the driver: running a compiled grammar is still a `run()` call,
6
+ * so a package that SHIPS a parser keeps a runtime import of parseman. Importing
7
+ * that driver from the main entry drags the whole library along — the combinator
8
+ * set, `compile()`, the first-set analysis, the CST builders — none of which a
9
+ * compiled parser touches.
10
+ *
11
+ * This entry exists so it doesn't have to. `run()`'s complete module closure is
12
+ * intentionally tiny (this, `functional/run.ts`, recovery helpers, CST capture /
13
+ * trivia views, host-mode checks, and the line-index helper) — a fraction of the
14
+ * main entry.
15
+ * `test/unit/run-entry-closure.test.ts` asserts that stays true, so a future
16
+ * import into `functional/run.ts` that widens the closure fails the suite rather
17
+ * than quietly re-inflating everyone's bundle.
18
+ *
19
+ * Use the main entry when you BUILD grammars; use this one when you only run
20
+ * output the macro (or `compile()`) already produced.
21
+ *
22
+ * import { run } from 'parseman/run'
23
+ * import { myGrammar } from './grammar.js' // macro-compiled
24
+ *
25
+ * const result = run(myGrammar, source, { trivia })
26
+ */
27
+ export { buildRootTriviaIndex } from '../cst/trivia-entries.ts'
28
+ export { run } from '../functional/run.ts'
29
+ export type { RootTriviaCapture, Runnable, RunOptions, RunResult } from '../functional/run.ts'
30
+ export type { RootTriviaGap, RootTriviaIndex, TriviaEntriesView } from '../cst/trivia-entries.ts'
31
+ export type { ParseContext, ParseError, ParseResult } from '../types.ts'