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,358 @@
1
+ import type { Combinator, DispatchCase, DispatchMatcherCase, ParseContext, ParseResult, ParserMeta } from '../types.ts'
2
+ import { cstCaptureActive, pushCstLeaf } from '../cst/capture-buffer.ts'
3
+ import { rollbackTrivia, saveTriviaMark } from './trivia-skip.ts'
4
+
5
+ export type DispatchWhen<T> = {
6
+ readonly kind: 'when'
7
+ readonly keys: readonly string[]
8
+ readonly parser: Combinator<T>
9
+ readonly caseInsensitive: boolean
10
+ readonly usesRouted?: boolean
11
+ }
12
+
13
+ export type DispatchWhenMatcher<T> = {
14
+ readonly kind: 'whenMatcher'
15
+ readonly matcher: DispatchStringMatcher
16
+ readonly parser: Combinator<T>
17
+ readonly caseInsensitive: boolean
18
+ readonly usesRouted?: boolean
19
+ }
20
+
21
+ export type DispatchStringMatcher =
22
+ | { readonly kind: 'startsWith'; readonly value: string }
23
+ | { readonly kind: 'endsWith'; readonly value: string }
24
+ | { readonly kind: 'matches'; readonly value: string; readonly flags: string }
25
+
26
+ export type DispatchOtherwise<T> = {
27
+ readonly kind: 'otherwise'
28
+ readonly parser: Combinator<T>
29
+ readonly usesRouted?: boolean
30
+ }
31
+
32
+ export type DispatchArm<T = unknown> = DispatchWhen<T> | DispatchWhenMatcher<T> | DispatchOtherwise<T>
33
+ export type DispatchWhenOptions = {
34
+ caseInsensitive?: boolean
35
+ }
36
+ export type DispatchWhenFactory = <T>(
37
+ key: string | readonly string[],
38
+ parser: Combinator<T>,
39
+ ) => DispatchWhen<T>
40
+ export type DispatchWhenKey = string | readonly string[]
41
+ export type DispatchWhenSelector = DispatchWhenKey | DispatchStringMatcher
42
+
43
+ type ArmValue<T> = T extends DispatchWhen<infer U>
44
+ ? U
45
+ : T extends DispatchWhenMatcher<infer U>
46
+ ? U
47
+ : T extends DispatchOtherwise<infer U>
48
+ ? U
49
+ : never
50
+ type UnionArms<T extends readonly DispatchArm<unknown>[]> = {
51
+ [K in keyof T]: ArmValue<T[K]>
52
+ }[number]
53
+
54
+ export function parserUsesRouted(parser: Combinator<unknown>, seen: Set<Combinator<unknown>> = new Set()): boolean {
55
+ if (seen.has(parser)) return false
56
+ seen.add(parser)
57
+ const def = parser._def
58
+ switch (def.tag) {
59
+ case 'routed':
60
+ return true
61
+ case 'sequence':
62
+ case 'choice':
63
+ return def.parsers.some(entry => parserUsesRouted(entry, seen))
64
+ case 'dispatch':
65
+ return false
66
+ case 'sepBy':
67
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.separator, seen)
68
+ case 'grammar':
69
+ return parserUsesRouted(def.parser, seen) || (def.triviaParser ? parserUsesRouted(def.triviaParser, seen) : false)
70
+ case 'scanTo':
71
+ return parserUsesRouted(def.sentinel, seen) || def.skip.some(entry => parserUsesRouted(entry, seen))
72
+ case 'recover':
73
+ return parserUsesRouted(def.parser, seen) || parserUsesRouted(def.sentinel, seen)
74
+ case 'lazy':
75
+ try { return parserUsesRouted(def.thunk(), seen) } catch { return false }
76
+ case 'many':
77
+ case 'oneOrMore':
78
+ case 'optional':
79
+ case 'attempt':
80
+ case 'transform':
81
+ case 'trivia':
82
+ case 'token':
83
+ case 'leaf':
84
+ case 'label':
85
+ case 'field':
86
+ case 'not':
87
+ case 'peek':
88
+ case 'node':
89
+ case 'withCtx':
90
+ case 'expect':
91
+ return parserUsesRouted(def.parser, seen)
92
+ default:
93
+ return false
94
+ }
95
+ }
96
+
97
+ export function branchUsesRouted(branch: { parser: Combinator<unknown>; usesRouted?: boolean | undefined }): boolean {
98
+ return branch.usesRouted === true || parserUsesRouted(branch.parser)
99
+ }
100
+
101
+ export function asciiFoldKey(key: string): string {
102
+ let out = ''
103
+ for (let i = 0; i < key.length; i++) {
104
+ const c = key.charCodeAt(i)
105
+ out += String.fromCharCode(c >= 65 && c <= 90 ? c + 32 : c)
106
+ }
107
+ return out
108
+ }
109
+
110
+ export function matchesDispatchMatcher(value: string, matcher: DispatchMatcherCase): boolean {
111
+ const candidate = matcher.caseInsensitive ? asciiFoldKey(value) : value
112
+ const expected = matcher.caseInsensitive ? asciiFoldKey(matcher.value) : matcher.value
113
+ switch (matcher.kind) {
114
+ case 'startsWith':
115
+ return candidate.startsWith(expected)
116
+ case 'endsWith':
117
+ return candidate.endsWith(expected)
118
+ case 'matches': {
119
+ const flags = matcher.caseInsensitive && !matcher.flags?.includes('i')
120
+ ? `${matcher.flags ?? ''}i`
121
+ : matcher.flags ?? ''
122
+ return new RegExp(matcher.value, flags).test(value)
123
+ }
124
+ }
125
+ }
126
+
127
+ function isStringMatcher(value: unknown): value is DispatchStringMatcher {
128
+ if (typeof value !== 'object' || value === null) return false
129
+ const kind = (value as { kind?: unknown }).kind
130
+ return kind === 'startsWith' || kind === 'endsWith' || kind === 'matches'
131
+ }
132
+
133
+ function fixedMatcher(kind: 'startsWith' | 'endsWith', value: string): DispatchStringMatcher {
134
+ if (typeof value !== 'string') throw new TypeError(`parseman: ${kind}() value must be a string`)
135
+ if (value.length === 0) throw new RangeError(`parseman: ${kind}() requires a non-empty value`)
136
+ return { kind, value }
137
+ }
138
+
139
+ export function startsWith(prefix: string): DispatchStringMatcher {
140
+ return fixedMatcher('startsWith', prefix)
141
+ }
142
+
143
+ export function endsWith(suffix: string): DispatchStringMatcher {
144
+ return fixedMatcher('endsWith', suffix)
145
+ }
146
+
147
+ export function matches(pattern: RegExp): DispatchStringMatcher {
148
+ if (!(pattern instanceof RegExp)) throw new TypeError('parseman: matches() requires a RegExp')
149
+ if (pattern.flags.includes('g') || pattern.flags.includes('y')) {
150
+ throw new TypeError('parseman: matches() does not accept global or sticky regex flags')
151
+ }
152
+ return { kind: 'matches', value: pattern.source, flags: pattern.flags }
153
+ }
154
+
155
+ export function when<T>(
156
+ key: DispatchWhenKey,
157
+ parser: Combinator<T>,
158
+ opts?: DispatchWhenOptions,
159
+ ): DispatchWhen<T>
160
+ export function when<T>(
161
+ matcher: DispatchStringMatcher,
162
+ parser: Combinator<T>,
163
+ opts?: DispatchWhenOptions,
164
+ ): DispatchWhenMatcher<T>
165
+ export function when<T>(
166
+ key: DispatchWhenSelector,
167
+ parser: Combinator<T>,
168
+ opts: DispatchWhenOptions = {},
169
+ ): DispatchWhen<T> | DispatchWhenMatcher<T> {
170
+ const usesRouted = parserUsesRouted(parser as Combinator<unknown>)
171
+ if (isStringMatcher(key)) {
172
+ return { kind: 'whenMatcher', matcher: key, parser, caseInsensitive: opts.caseInsensitive ?? false, usesRouted }
173
+ }
174
+ const keys = Array.isArray(key) ? [...key] : [key]
175
+ if (keys.length === 0) throw new RangeError('parseman: when() requires at least one key')
176
+ for (const item of keys) {
177
+ if (typeof item !== 'string') throw new TypeError('parseman: when() keys must be strings')
178
+ }
179
+ return { kind: 'when', keys, parser, caseInsensitive: opts.caseInsensitive ?? false, usesRouted }
180
+ }
181
+
182
+ export function makeWhen(opts: DispatchWhenOptions = {}): DispatchWhenFactory {
183
+ return (key, parser) => when(key, parser, opts)
184
+ }
185
+
186
+ export function otherwise<T>(parser: Combinator<T>): DispatchOtherwise<T> {
187
+ return { kind: 'otherwise', parser, usesRouted: parserUsesRouted(parser as Combinator<unknown>) }
188
+ }
189
+
190
+ /**
191
+ * Reuse the token the enclosing `dispatch()` already consumed to select this branch,
192
+ * instead of re-recognizing it.
193
+ *
194
+ * With no argument this is dispatch-only: outside a dispatch branch (or at a position
195
+ * other than the selector's) it fails with `expected: ['routed()']`.
196
+ *
197
+ * `routed(fallback)` makes the SAME production usable in both contexts: inside a
198
+ * dispatch branch it reuses the routed token; anywhere else it parses `fallback` in
199
+ * place. That is what collapses a `Routed<X>` twin production into its original —
200
+ * `sequence(routed(Name), Prelude, ';')` replaces the pair
201
+ * `sequence(Name, Prelude, ';')` / `sequence(routed(), Prelude, ';')`, which
202
+ * otherwise differ by exactly one element and duplicate a whole production (and a
203
+ * whole compiled emission) for a one-token difference.
204
+ *
205
+ * A `routed()` in a dispatch SELECTOR is still an error with or without a fallback:
206
+ * the selector is what produces the routed token, so reading it there is misuse.
207
+ */
208
+ export function routed<T = string>(fallback?: Combinator<T>): Combinator<T> {
209
+ return {
210
+ _tag: 'routed',
211
+ _meta: {
212
+ // The routed token is whatever the selector matched, so the first set stays
213
+ // `any` even with a fallback — `any` already subsumes the fallback's.
214
+ firstSet: { kind: 'any' },
215
+ canMatchNewline: fallback?._meta.canMatchNewline ?? false,
216
+ isTrivia: false,
217
+ },
218
+ _def: fallback === undefined
219
+ ? { tag: 'routed' }
220
+ : { tag: 'routed', fallback: fallback as Combinator<unknown> },
221
+ parse(_input: string, pos: number, ctx: ParseContext): ParseResult<T> {
222
+ const item = ctx._routed
223
+ if (item === undefined || pos !== item.span.start) {
224
+ if (fallback !== undefined) return fallback.parse(_input, pos, ctx)
225
+ return { ok: false, expected: ['routed()'], span: { start: pos, end: pos } }
226
+ }
227
+ if (cstCaptureActive(ctx)) {
228
+ pushCstLeaf(ctx, { _tag: 'leaf', value: item.value, span: item.span })
229
+ }
230
+ return { ok: true, value: item.value as T, span: item.span }
231
+ },
232
+ }
233
+ }
234
+
235
+ export function dispatch<S extends string, T extends readonly DispatchArm<unknown>[]>(
236
+ selector: Combinator<S>,
237
+ ...arms: T
238
+ ): Combinator<[S, UnionArms<T>]> {
239
+ if (parserUsesRouted(selector as Combinator<unknown>)) {
240
+ throw new Error('parseman: routed() can only appear inside a dispatch() branch')
241
+ }
242
+ let fallback: Combinator<unknown> | undefined
243
+ let fallbackUsesRouted = false
244
+ const matchers: DispatchMatcherCase[] = []
245
+ const cases: DispatchCase[] = []
246
+ const seen: Array<{ raw: string; folded: string; caseInsensitive: boolean }> = []
247
+
248
+ for (let i = 0; i < arms.length; i++) {
249
+ const arm = arms[i]!
250
+ if (arm.kind === 'otherwise') {
251
+ if (fallback !== undefined) throw new Error('parseman: dispatch() accepts at most one otherwise() arm')
252
+ if (i !== arms.length - 1) throw new Error('parseman: otherwise() must be the last dispatch() arm')
253
+ fallback = arm.parser
254
+ fallbackUsesRouted = arm.usesRouted === true
255
+ continue
256
+ }
257
+ if (arm.kind === 'whenMatcher') {
258
+ const matcher = arm.matcher
259
+ matchers.push({
260
+ kind: matcher.kind,
261
+ value: matcher.value,
262
+ ...(matcher.kind === 'matches' ? { flags: matcher.flags } : {}),
263
+ parser: arm.parser as Combinator<unknown>,
264
+ caseInsensitive: arm.caseInsensitive,
265
+ ...(arm.usesRouted === true ? { usesRouted: true } : {}),
266
+ })
267
+ continue
268
+ }
269
+ for (const key of arm.keys) {
270
+ const folded = asciiFoldKey(key)
271
+ for (const prior of seen) {
272
+ const overlaps = arm.caseInsensitive || prior.caseInsensitive
273
+ ? folded === prior.folded
274
+ : key === prior.raw
275
+ if (overlaps) throw new Error(`parseman: duplicate dispatch key ${JSON.stringify(key)}`)
276
+ }
277
+ seen.push({ raw: key, folded, caseInsensitive: arm.caseInsensitive })
278
+ }
279
+ cases.push({
280
+ keys: arm.keys,
281
+ parser: arm.parser as Combinator<unknown>,
282
+ caseInsensitive: arm.caseInsensitive,
283
+ ...(arm.usesRouted === true ? { usesRouted: true } : {}),
284
+ })
285
+ }
286
+
287
+ const meta: ParserMeta = {
288
+ firstSet: selector._meta.firstSet,
289
+ canMatchNewline: selector._meta.canMatchNewline ||
290
+ cases.some(entry => entry.parser._meta.canMatchNewline) ||
291
+ matchers.some(entry => entry.parser._meta.canMatchNewline) ||
292
+ (fallback?._meta.canMatchNewline ?? false),
293
+ isTrivia: false,
294
+ }
295
+
296
+ const byKey = new Map<string, DispatchCase>()
297
+ const byFoldedKey = new Map<string, DispatchCase>()
298
+ for (const entry of cases) {
299
+ for (const key of entry.keys) {
300
+ if (entry.caseInsensitive) byFoldedKey.set(asciiFoldKey(key), entry)
301
+ else byKey.set(key, entry)
302
+ }
303
+ }
304
+
305
+ return {
306
+ _tag: 'dispatch',
307
+ _meta: meta,
308
+ _def: {
309
+ tag: 'dispatch',
310
+ selector: selector as Combinator<string>,
311
+ cases,
312
+ ...(matchers.length === 0 ? {} : { matchers }),
313
+ ...(fallback === undefined ? {} : { otherwise: fallback, ...(fallbackUsesRouted ? { otherwiseUsesRouted: true } : {}) }),
314
+ },
315
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<[S, UnionArms<T>]> {
316
+ if (parserUsesRouted(selector as Combinator<unknown>)) {
317
+ throw new Error('parseman: routed() can only appear inside a dispatch() branch')
318
+ }
319
+ const selectorMark = saveTriviaMark(ctx)
320
+ const selected = selector.parse(input, pos, ctx)
321
+ if (!selected.ok) return selected
322
+
323
+ const branch = byKey.get(selected.value) ??
324
+ (byFoldedKey.size === 0 ? undefined : byFoldedKey.get(asciiFoldKey(selected.value))) ??
325
+ matchers.find(matcher => matchesDispatchMatcher(selected.value, matcher)) ??
326
+ (fallback === undefined ? undefined : { keys: [], parser: fallback, caseInsensitive: false, usesRouted: fallbackUsesRouted })
327
+ if (branch === undefined) {
328
+ const expected = cases.flatMap(entry => entry.keys.map(key => JSON.stringify(key)))
329
+ return { ok: false, expected, span: { start: selected.span.end, end: selected.span.end } }
330
+ }
331
+
332
+ const savedRouted = ctx._routed
333
+ const usesRouted = branchUsesRouted(branch)
334
+ let mark = saveTriviaMark(ctx)
335
+ if (usesRouted) {
336
+ rollbackTrivia(ctx, selectorMark)
337
+ mark = saveTriviaMark(ctx)
338
+ ctx._routed = { value: selected.value, span: selected.span }
339
+ }
340
+ let result: ParseResult<unknown>
341
+ try {
342
+ result = branch.parser.parse(input, usesRouted ? pos : selected.span.end, ctx)
343
+ } finally {
344
+ if (usesRouted) ctx._routed = savedRouted
345
+ }
346
+ if (!result.ok) {
347
+ rollbackTrivia(ctx, mark)
348
+ return { ...result, committed: true }
349
+ }
350
+
351
+ return {
352
+ ok: true,
353
+ value: [selected.value, result.value as UnionArms<T>],
354
+ span: { start: pos, end: result.span.end },
355
+ }
356
+ },
357
+ }
358
+ }
@@ -0,0 +1,159 @@
1
+ import type { Combinator, ParseContext, ParseResult, ParserMeta, ParseError } from '../types.ts'
2
+ import { captureError } from '../recovery/scan.ts'
3
+ import { annotateSpanFromLineContext } from '../line-index.ts'
4
+ import { matchesEmpty, isZeroWidthAssertion } from './first-set.ts'
5
+
6
+ export type { ParseError }
7
+
8
+ /**
9
+ * Statically derive the "expected" token set from a combinator's structure,
10
+ * so a failure can name what it wanted without running anything:
11
+ *
12
+ * literal('}') -> ['"}"']
13
+ * keywords(['a','b']) -> ['"a"', '"b"']
14
+ * choice(x, y) -> expected(x) ++ expected(y) (all alternatives)
15
+ * sequence(a, b, …) -> expected(a) ++ … through the NULLABLE prefix
16
+ * label('s', x) -> ['s'] (an explicit name wins)
17
+ *
18
+ * This is what lets expect() report a meaningful, IDENTICAL expectation in both
19
+ * the interpreter and the compiled output (the compiled path does not rebuild the
20
+ * runtime `expected` array, so it reads this precomputed set instead).
21
+ *
22
+ * The result is DEDUPLICATED. Deriving through a nullable prefix re-reaches the
23
+ * same token from several terms — a value grammar whose leading terms are all
24
+ * optional names its value-start set once per term — and an expectation list that
25
+ * offers `"@{"` five times is wrong for a reader and expensive for a parser: a
26
+ * choice that loses every arm materializes `[...arm0, ...arm1, …]` at runtime, and
27
+ * those arrays feed the enclosing choice's concat in turn, so duplicates compound
28
+ * multiplicatively up the nesting. On jess's Less grammar the un-deduped sets cost
29
+ * 32% of parse time on `benchmark.less` (medians 20.7 → 14.2 ms over 100
30
+ * interleaved pairs, 99/100 wins, min 19.1 → 13.0), which is the whole of the Less
31
+ * regression 0.35.0 shipped. Dedup is not an optimisation bolted onto the
32
+ * derivation — a SET is what the derivation always meant.
33
+ */
34
+ export function deriveExpected(c: Combinator<unknown>): string[] {
35
+ return [...new Set(derive(c, new Set()))]
36
+ }
37
+
38
+ function derive(c: Combinator<unknown>, seen: Set<Combinator<unknown>>): string[] {
39
+ const deriveExpected = (p: Combinator<unknown>): string[] => derive(p, seen)
40
+ const def = c._def
41
+ switch (def.tag) {
42
+ case 'literal': return [JSON.stringify(def.value)]
43
+ case 'regex': return [`/${def.source}/`]
44
+ case 'keywords': return def.words.map(w => JSON.stringify(w))
45
+ case 'label': return [def.label]
46
+ case 'choice': return def.parsers.flatMap(deriveExpected)
47
+ case 'dispatch': return deriveExpected(def.selector)
48
+ // Union through the NULLABLE prefix, mirroring `sequenceFirstSet`. A leading
49
+ // `optional(…)`/`many(…)` can match nothing, so the term after it is equally
50
+ // able to fail first — deriving from term 0 alone named a token the parse
51
+ // never actually required. A leading `not(…)` is zero-width and contributes
52
+ // nothing (its own expectation is about what must NOT be here), but it is
53
+ // nullable, so keep scanning past it. Stop at the first term that must match.
54
+ case 'sequence': {
55
+ const out: string[] = []
56
+ for (const term of def.parsers) {
57
+ if (!isZeroWidthAssertion(term)) out.push(...deriveExpected(term))
58
+ if (!matchesEmpty(term)) break
59
+ }
60
+ return out
61
+ }
62
+ case 'attempt': return deriveExpected(def.parser)
63
+ // Delegating wrappers whose first-token failure is their inner parser's — must
64
+ // pass through so a start-failure (and the first-set-miss fast-path in attempt/
65
+ // node, which reads this) names the real expected token, not the wrapper tag.
66
+ case 'field':
67
+ case 'withCtx':
68
+ case 'recover': return deriveExpected(def.parser)
69
+ case 'expect': return def.expected
70
+ case 'node':
71
+ case 'grammar':
72
+ case 'trivia':
73
+ case 'token':
74
+ case 'leaf':
75
+ case 'optional':
76
+ case 'many':
77
+ case 'oneOrMore':
78
+ case 'transform':
79
+ case 'not':
80
+ case 'peek': return deriveExpected(def.parser)
81
+ // A bare routed() can only fail as "there is no routed token here". With a
82
+ // fallback, THAT is the branch that can fail, so its expectations are the useful
83
+ // ones — the routed path either succeeds or is not taken.
84
+ case 'routed': return def.fallback === undefined ? ['routed()'] : deriveExpected(def.fallback)
85
+ // A bare adjacency assertion names the JOIN it requires. Inside a sequence it is
86
+ // skipped entirely (isZeroWidthAssertion, above), which is the normal path.
87
+ case 'adjacency': return [def.polarity]
88
+ case 'lazy': {
89
+ // An EXTERNAL ref (a rule from a composed base grammar) has no local
90
+ // definition yet — its `thunk()` throws until fusion supplies it. Fall back
91
+ // to the rule name as the expected label instead of descending.
92
+ const name = (c as { _ruleName?: string })._ruleName
93
+ // Cycle guard: a RECURSIVE rule re-enters its own definition forever. Reaching
94
+ // one is newly possible now that `sequence` derives through a nullable prefix
95
+ // (`list = choice('end', sequence(optional(item), list))` — term-0-only
96
+ // derivation used to stop short of the self-reference). Falling back to the rule
97
+ // name, exactly as for an unresolved external ref, cuts the cycle. Removed again
98
+ // on the way out, so a rule referenced twice NON-cyclically still derives fully.
99
+ if (seen.has(c)) return name ? [name] : []
100
+ seen.add(c)
101
+ try { return deriveExpected(def.thunk()) }
102
+ catch { return name ? [name] : [] }
103
+ finally { seen.delete(c) }
104
+ }
105
+ default: return []
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Required-token combinator. Tries `combinator`; on success returns it verbatim.
111
+ * On failure it does NOT fail — it records a {@link ParseError} (the statically
112
+ * derived expected set, or `label` when given) into `ctx._errors` and RECOVERS IN
113
+ * PLACE, returning a zero-width success so the enclosing sequence continues as if
114
+ * the token were present.
115
+ *
116
+ * Use it to mark required delimiters/terminators (`}` `)` `;`, a selector before a
117
+ * block) so a missing one is reported with position + expectation rather than
118
+ * aborting the whole parse or being silently swallowed by a catch-all.
119
+ *
120
+ * sequence(literal('{'), declList, expect(literal('}')))
121
+ *
122
+ * Errors are only collected when the parse runs with `{ recover: true }`; without
123
+ * it, expect() still recovers in place but records nothing (zero overhead beyond
124
+ * the inner attempt).
125
+ */
126
+ export function expect<T>(combinator: Combinator<T>, label?: string): Combinator<T | ParseError> {
127
+ const expected = label !== undefined ? [label] : deriveExpected(combinator)
128
+ const meta: ParserMeta = {
129
+ firstSet: combinator._meta.firstSet,
130
+ canMatchNewline: combinator._meta.canMatchNewline,
131
+ isTrivia: false,
132
+ }
133
+ return {
134
+ _tag: 'expect',
135
+ _meta: meta,
136
+ _def: { tag: 'expect', parser: combinator as Combinator<unknown>, label, expected },
137
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<T | ParseError> {
138
+ const result = combinator.parse(input, pos, ctx)
139
+ if (result.ok) return result as ParseResult<T | ParseError>
140
+ const span = { start: pos, end: pos }
141
+ const error: ParseError = { _tag: 'parseError', span: ctx.trackLines ? annotateSpanFromLineContext(span, ctx) : span, expected }
142
+ ctx._errors?.push(error)
143
+ // Embed the missing-token error as a `parseError` CST child (no-op when CST
144
+ // capture is off), so it rides the tree exactly like list-recovery errors —
145
+ // a tree walk finds every diagnostic and the error survives incremental
146
+ // reuse, rather than living only in the flat ctx._errors side-channel. Gated
147
+ // on tolerant like the list-recovery embed sites, so a strict CST build is
148
+ // byte-identical to before (and stays in parity with the compiled path,
149
+ // which can only embed when the recovery bundle `_rec` is installed).
150
+ if (ctx._tolerant) captureError(ctx, error)
151
+ return { ok: true, value: error, span: { start: pos, end: pos } }
152
+ },
153
+ }
154
+ }
155
+
156
+ /** True when `value` is a recovery {@link ParseError} node. */
157
+ export function isParseError(value: unknown): value is ParseError {
158
+ return typeof value === 'object' && value !== null && (value as ParseError)._tag === 'parseError'
159
+ }