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,63 @@
1
+ import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
2
+ import { any, matchesEmpty } from './first-set.ts'
3
+ import { saveLookaheadMark, rollbackLookahead } from './trivia-skip.ts'
4
+
5
+ /**
6
+ * Positive lookahead. Succeeds (consuming nothing) when `combinator` matches at
7
+ * this position; fails when it doesn't. The mirror of `not()`.
8
+ *
9
+ * // "a mixin reference, not a class selector": require the punctuation ahead,
10
+ * // then let the real production consume it.
11
+ * const mixinRef = sequence(peek(regex(/[.#]/)), mixinCall)
12
+ *
13
+ * WHY THIS EXISTS AS A PRIMITIVE (not `not(not(X))`):
14
+ *
15
+ * `not(not(X))` is behaviourally a positive lookahead, but `not()` reports
16
+ * `firstSet: any()` — it cannot know what it forbids — so `not(not(X))` reports
17
+ * `any()` too. An arm leading with it therefore POISONS its choice's first-char
18
+ * dispatch (`analyzeGating` flags it as the `double-not` anti-pattern), and among
19
+ * sibling arms that share a first char the hand-rolled gate miscompiles.
20
+ *
21
+ * `peek(X)` carries X's first-set instead, so a leading `peek()` GATES its arm:
22
+ * `choice(sequence(peek(regex(/[.#]/)), …), …)` still emits O(1) dispatch.
23
+ *
24
+ * The first-set is exact only when X is NON-NULLABLE. A nullable `peek(X)`
25
+ * succeeds on the empty string, so it constrains no first character at all — it
26
+ * reports `any()`, the sound over-approximation. (Same rule the sequence
27
+ * first-set uses; see `isPositiveLookahead` in `first-set.ts`.)
28
+ */
29
+ export function peek(combinator: Combinator<unknown>): Combinator<null> {
30
+ const meta: ParserMeta = {
31
+ // A nullable body imposes no first-char constraint — `any()`, not its set.
32
+ firstSet: matchesEmpty(combinator) ? any() : combinator._meta.firstSet,
33
+ canMatchNewline: false,
34
+ isTrivia: false,
35
+ }
36
+
37
+ return {
38
+ _tag: 'peek',
39
+ _meta: meta,
40
+ _def: { tag: 'peek', parser: combinator },
41
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<null> {
42
+ // Zero-width on BOTH outcomes, so the probe must leave NO observable trace:
43
+ // whatever the inner attempt captured (CST leaves/rawChildren/per-node
44
+ // trivia/fields), recovered (`_errors`), or committed to the GLOBAL
45
+ // `_triviaLog` is rolled back either way.
46
+ //
47
+ // `rollbackTrivia`, not `rollbackCstCapture`: the latter's mark omits
48
+ // `_triviaLog`, which is a SEPARATE sink `scanTrivia().commit()` writes when
49
+ // the body skips ambient trivia between terms. Missing it duplicates every
50
+ // probed trivia span once the region is parsed for real — `_triviaLog` has no
51
+ // dedup anywhere (`triviaEntries()` is a positional view over the flat array),
52
+ // so the duplicate reaches output. Same failure mode as the one recorded in
53
+ // `test/parity/trivia-log-regression.test.ts`. The compiled `peek` emits its
54
+ // body under a non-capturing ctx and so never writes these sinks at all; this
55
+ // is what keeps the two engines at parity.
56
+ const mark = saveLookaheadMark(ctx)
57
+ const result = combinator.parse(input, pos, ctx)
58
+ rollbackLookahead(ctx, mark)
59
+ if (result.ok) return { ok: true, value: null, span: { start: pos, end: pos } }
60
+ return { ok: false, expected: [`peek(${combinator._tag})`], span: { start: pos, end: pos } }
61
+ },
62
+ }
63
+ }
@@ -0,0 +1,25 @@
1
+ import type { ParseContext, ParseFail } from '../types.ts'
2
+
3
+ /**
4
+ * Creates a ParseFail and, if a completions probe is active, records it.
5
+ *
6
+ * Rules:
7
+ * - Only failures at or before the probe's target offset are eligible.
8
+ * - A deeper failure (higher pos) replaces the current best.
9
+ * - A tie (same pos) merges expected arrays — this is what makes choice arms
10
+ * aggregate their alternatives rather than each overwriting the last.
11
+ * - Shallower failures are ignored.
12
+ */
13
+ export function failAt(ctx: ParseContext, expected: string[], pos: number): ParseFail {
14
+ const r: ParseFail = { ok: false, expected, span: { start: pos, end: pos } }
15
+ const probe = ctx._probe
16
+ if (probe !== undefined && pos <= probe.offset) {
17
+ const best = probe.best
18
+ if (best === null || pos > best.span.start) {
19
+ probe.best = r
20
+ } else if (pos === best.span.start) {
21
+ probe.best = { ...best, expected: [...best.expected, ...expected] }
22
+ }
23
+ }
24
+ return r
25
+ }
@@ -0,0 +1,109 @@
1
+ import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
2
+ import { any } from './first-set.ts'
3
+ import { hasOwnTriviaBoundary } from './trivia-boundary.ts'
4
+
5
+ /**
6
+ * Create a forward-declared parser slot for mutually recursive grammars.
7
+ *
8
+ * Because JS evaluates arguments eagerly, you can't reference a variable
9
+ * before it's declared. ref() creates a placeholder you fill in later:
10
+ *
11
+ * const value = ref<JSONValue>()
12
+ * const array = transform(sequence(literal('['), sepBy(value, literal(',')), literal(']')), ...)
13
+ * const object = transform(sequence(literal('{'), sepBy(pair, literal(',')), literal('}')), ...)
14
+ * value.define(choice(object, array, string, number, bool, nullVal))
15
+ *
16
+ * Unlike lazy(() => x), you use the ref directly — no wrapping at each call site.
17
+ */
18
+ export function ref<T>(): Combinator<T> & { define(p: Combinator<T>): void } {
19
+ let resolved: Combinator<T> | null = null
20
+ /** `hasOwnTriviaBoundary(resolved)`, resolved at `define()`. See `parse`. */
21
+ let ownBoundary = false
22
+
23
+ const meta: ParserMeta = {
24
+ firstSet: any(),
25
+ canMatchNewline: true,
26
+ isTrivia: false,
27
+ }
28
+
29
+ const slot = {
30
+ _tag: 'lazy' as const,
31
+ _meta: meta,
32
+ _def: {
33
+ tag: 'lazy' as const,
34
+ thunk: () => {
35
+ if (!resolved) throw new Error('ref<T>() used before .define() was called')
36
+ return resolved as Combinator<unknown>
37
+ },
38
+ },
39
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<T> {
40
+ if (!resolved) throw new Error('ref<T>() used before .define() was called')
41
+ // A RULE CARRIES ITS OWN AMBIENT TRIVIA — see `rules()` in parser.ts, which
42
+ // stamps `grammarTrivia` on EVERY rule precisely so that entering any one of
43
+ // them installs it. Reading it only at the parse ENTRY (grammar.ts) made the
44
+ // interpreter scope trivia DYNAMICALLY: a rule referenced from underneath a
45
+ // `noTrivia(...)` ran without trivia, so its meaning depended on its caller.
46
+ // Codegen never had that behaviour — it binds each rule's trivia scanner at
47
+ // COMPILE time (the emitted `_tf0(input, cur, ctx, 1)` before every sequence
48
+ // boundary), so a `g.Foo` reference always runs under Foo's own scope.
49
+ //
50
+ // The two engines therefore parsed DIFFERENT LANGUAGES, and jess's Less
51
+ // grammar sits exactly on the seam: `StandardDeclaration` wraps its value in
52
+ // `noTrivia(...)` and the `!important` tail lives in the referenced rule
53
+ // `ValueListWithPriority`, so the interpreter and both table drivers could not
54
+ // cross the space in `color: red !important`. That one construct truncated a
55
+ // 107 KB stylesheet at 68.5% while reporting `ok: true` with no errors.
56
+ // Codegen shipped through 0.44-0.46 and was right; 0.47 made the table the
57
+ // shipping engine and the divergence became the product.
58
+ //
59
+ // ONLY WHERE THE SCOPE WAS CLEARED, not wherever it merely differs. The
60
+ // broader form (`ctx.trivia !== gt`) also OVERRIDES a caller that set a
61
+ // different live trivia, and that measurably broke recognition: over jess's
62
+ // four corpora it regressed `@less/test-data`'s `selectors.less` from 3791
63
+ // bytes to 1784. A caller running under its own live trivia is making a
64
+ // deliberate choice; a caller that CLEARED it is the `noTrivia(...)` case this
65
+ // fixes. Restricting to the cleared case keeps every repair and drops that
66
+ // regression.
67
+ //
68
+ // ONLY WHERE THE RULE HAS A BOUNDARY TO REPAIR. `ownBoundary` is resolved
69
+ // once in `define()` (see `hasOwnTriviaBoundary`): a rule whose body is a
70
+ // bare ALTERNATION, a dispatch, or a single terminal has no position of its
71
+ // own at which ambient trivia is consulted, so re-establishing the scope
72
+ // there cannot repair anything — it can only leak past the arms into
73
+ // whatever they delegate to.
74
+ //
75
+ // That leak is what jess's SCSS value grammar hit. `ValueTerm` clears, and
76
+ // `MathUnary` — `choice(noTrivia(…), noTrivia(…), g.ValueAtom)` — restored
77
+ // for a body that never uses it, handing its third arm a live scope.
78
+ // `ValueAtom` → `KeywordOrInterpolatedValue`, whose `many()` concatenates
79
+ // identifier chunks, then skipped the space between its terms:
80
+ // `gen-workload.scss` stopped at byte 218 of 287543, and `a{b: c d}` silently
81
+ // produced the ONE keyword `bc` with `ok: true` and no errors.
82
+ //
83
+ // Hot path is one boolean check.
84
+ const gt = meta.grammarTrivia
85
+ if (gt !== undefined && ownBoundary && ctx.trivia === undefined) {
86
+ const savedLabels = ctx.triviaKindLabels
87
+ ctx.trivia = gt
88
+ ctx.triviaKindLabels = gt._meta.triviaKindLabels
89
+ try { return resolved.parse(input, pos, ctx) }
90
+ finally { ctx.trivia = undefined; ctx.triviaKindLabels = savedLabels }
91
+ }
92
+ return resolved.parse(input, pos, ctx)
93
+ },
94
+ define(p: Combinator<T>): void {
95
+ if (resolved) throw new Error('ref<T>() already defined')
96
+ resolved = p
97
+ ownBoundary = hasOwnTriviaBoundary(p as Combinator<unknown>)
98
+ meta.firstSet = p._meta.firstSet
99
+ meta.canMatchNewline = p._meta.canMatchNewline
100
+ meta.isTrivia = p._meta.isTrivia
101
+ if (p._meta.triviaKindLabels !== undefined) meta.triviaKindLabels = p._meta.triviaKindLabels
102
+ else delete meta.triviaKindLabels
103
+ if (p._meta.disjoint !== undefined) meta.disjoint = p._meta.disjoint
104
+ else delete meta.disjoint
105
+ },
106
+ }
107
+
108
+ return slot
109
+ }
@@ -0,0 +1,170 @@
1
+ import type { Combinator, ParseContext, ParseResult, ParserMeta, FirstSet } from '../types.ts'
2
+ import { any } from './first-set.ts'
3
+ import { failAt } from './probe.ts'
4
+ import { pushCstLeaf, cstCaptureActive } from '../cst/capture-buffer.ts'
5
+ import { recordLineRangeFromContext } from '../line-index.ts'
6
+ import { shorthandRanges, parseClassRanges } from '../regex/classes.ts'
7
+ import { firstSetFromRegex } from '../regex/first-set.ts'
8
+
9
+ /**
10
+ * A regex terminal's first-set (for choice-dispatch fast paths) is derived by
11
+ * `firstSetFromRegex` in `../regex/first-set.ts` — a small, dependency-free
12
+ * hand-rolled regex parser, imported DIRECTLY and unconditionally.
13
+ *
14
+ * ── WHY NOT INJECTED ────────────────────────────────────────────────────────────
15
+ * It used to be registered at run time by the library entry (`index.ts`) via a
16
+ * `registerRegexAnalyzer` seam, so that a bundle holding `regex()` without the
17
+ * library entry never pulled the analyzer in. That saved ~3 KB and cost a shipped
18
+ * export: `parseman/table` is its OWN module graph, so its private copy of this
19
+ * module never saw the registration, `regex()` there returned the permissive
20
+ * `any()` first-set, and `classifiedTrivia()` — which REQUIRES a concrete finite
21
+ * first set per arm — rejected every arm of every table-lowered grammar
22
+ * (`buildTrivia` in `../table/program.ts`). The failure was invisible from source,
23
+ * where `src/index.ts` is always in the graph.
24
+ *
25
+ * A mutable module-global that a *different* entry has to remember to write makes
26
+ * `regex()`'s result depend on which bundle it landed in. Deriving the first set
27
+ * intrinsically makes every module graph agree by construction. The analyzer adds
28
+ * no new leaf modules — it imports only `./first-set.ts` and `../regex/classes.ts`,
29
+ * both already reachable from here — so the cost is the analyzer's own bytes.
30
+ */
31
+ const SCAN_BAIL_AT = 64
32
+ type ShortScanner = (input: string, pos: number) => number | null | undefined
33
+
34
+ function inRanges(cp: number, ranges: Array<[number, number]>): boolean {
35
+ for (let i = 0; i < ranges.length; i++) {
36
+ const [lo, hi] = ranges[i]!
37
+ if (cp >= lo && cp <= hi) return true
38
+ }
39
+ return false
40
+ }
41
+
42
+ /**
43
+ * The positive char-class body (chars between `[` and `]`) as ranges for the
44
+ * short-scan fast path, or null if it can't be lowered — including a NEGATED
45
+ * class (`[^…]`), which the scan can't express. The class parsing itself is the
46
+ * shared `parseClassRanges` (see `../regex/classes.ts`).
47
+ */
48
+ function readClassRanges(body: string): Array<[number, number]> | null {
49
+ if (body.startsWith('^')) return null
50
+ return parseClassRanges(body)
51
+ }
52
+
53
+ function shortScanner(source: string, flags: string): ShortScanner | null {
54
+ if (/[imsuvy]/.test(flags)) return null
55
+ let ranges: Array<[number, number]> | null = null
56
+ let quant = ''
57
+ if (source[0] === '[') {
58
+ let end = 1
59
+ while (end < source.length && source[end] !== ']') {
60
+ if (source[end] === '\\') end += 2
61
+ else end++
62
+ }
63
+ if (source[end] !== ']') return null
64
+ ranges = readClassRanges(source.slice(1, end))
65
+ quant = source.slice(end + 1)
66
+ } else if (source[0] === '\\' && (source[1] === 'd' || source[1] === 'w' || source[1] === 's')) {
67
+ ranges = shorthandRanges(source[1])
68
+ quant = source.slice(2)
69
+ }
70
+ if (!ranges || (quant !== '+' && quant !== '*')) return null
71
+ const minOne = quant === '+'
72
+ return (input, pos) => {
73
+ let end = pos
74
+ while (end < input.length && inRanges(input.charCodeAt(end), ranges)) {
75
+ end++
76
+ if (end - pos >= SCAN_BAIL_AT) return undefined
77
+ }
78
+ return minOne && end === pos ? null : end
79
+ }
80
+ }
81
+
82
+ /**
83
+ * ASCII-case-fold a first-set: for every ASCII letter reachable as a leading char,
84
+ * also admit its opposite-case twin (A–Z ↔ a–z). Non-letter code points and `any`/
85
+ * `empty` are unchanged. Only widens the set (sound superset) — used to correct a
86
+ * case-insensitive regex's flag-agnostic first-set for first-char dispatch. Unicode
87
+ * case-folding beyond ASCII is deliberately NOT applied (parseman first-sets are
88
+ * ASCII-BMP for dispatch); a non-ASCII letter keeps its own code point only, which
89
+ * stays sound (the interpreter's real `/i` match still runs).
90
+ */
91
+ function asciiCaseFold(fs: FirstSet): FirstSet {
92
+ if (fs.kind !== 'ranges') return fs
93
+ const ranges: { lo: number; hi: number }[] = fs.ranges.map(r => ({ lo: r.lo, hi: r.hi }))
94
+ const add = (lo: number, hi: number): void => { if (lo <= hi) ranges.push({ lo, hi }) }
95
+ for (const r of fs.ranges) {
96
+ // Uppercase portion [A–Z] → lowercase twin (+32).
97
+ const uLo = Math.max(r.lo, 65), uHi = Math.min(r.hi, 90)
98
+ add(uLo + 32, uHi + 32)
99
+ // Lowercase portion [a–z] → uppercase twin (−32).
100
+ const lLo = Math.max(r.lo, 97), lHi = Math.min(r.hi, 122)
101
+ add(lLo - 32, lHi - 32)
102
+ }
103
+ // Normalize: sort + coalesce overlapping/adjacent ranges.
104
+ ranges.sort((a, b) => a.lo - b.lo)
105
+ const merged: { lo: number; hi: number }[] = []
106
+ for (const r of ranges) {
107
+ const last = merged[merged.length - 1]
108
+ if (last && r.lo <= last.hi + 1) last.hi = Math.max(last.hi, r.hi)
109
+ else merged.push({ lo: r.lo, hi: r.hi })
110
+ }
111
+ return { kind: 'ranges', ranges: merged }
112
+ }
113
+
114
+ export function regex(pattern: string | RegExp, flags = ''): Combinator<string> {
115
+ const source = typeof pattern === 'string' ? pattern : pattern.source
116
+ const resolvedFlags = typeof pattern === 'string' ? flags : pattern.flags
117
+
118
+ const anchored = new RegExp(source, 'y' + resolvedFlags.replace(/[gy]/g, ''))
119
+ const scan = shortScanner(source, resolvedFlags)
120
+
121
+ const raw = firstSetFromRegex(source)
122
+ // The first-set analyzer is flag-agnostic, so for a CASE-INSENSITIVE pattern it
123
+ // returns only the literal-case leading chars — e.g. `/red|blue/i` → `{r,b}`, NOT
124
+ // `{r,R,b,B}`. Using that narrow set for first-char DISPATCH would false-EXCLUDE the
125
+ // opposite-case input (`ReD` gated out of a `/(?:red|…)/i` arm), so widen it under
126
+ // `i`. The widening is FLAG-AWARE because `i` alone and `i`+`u` fold different sets:
127
+ // - `/i` WITHOUT `u`: ECMAScript case-insensitive matching folds ONLY the ASCII
128
+ // Basic-Latin case pairs (`a`↔`A` … `z`↔`Z`) — a plain `/[a-z]/i` does NOT match
129
+ // `ſ`(U+017F) or `K`(U+212A). So `asciiCaseFold` is a SOUND, tight superset, and
130
+ // it preserves at-keyword dispatch (`/@media…/i` still gates on `@`).
131
+ // - `/ui` or `/iv` (Unicode mode — the `u` OR the ES2024 `v` Unicode-sets flag):
132
+ // matching uses Unicode *simple case folding*, so `/[a-z]/ui` (and `/[a-z]/iv`)
133
+ // ALSO match `ſ`→s and `K`→k. ASCII-folding can't enumerate those astral/BMP case
134
+ // pairs, so gating on the ASCII set would false-exclude them. Fall back to `any()`
135
+ // (always-try) — sound, and only forfeits gating for the rare Unicode-mode `/i`
136
+ // recognizer, never the ASCII-only `/i` at-keywords the fix targets.
137
+ const firstSet = !resolvedFlags.includes('i')
138
+ ? raw.firstSet
139
+ : (resolvedFlags.includes('u') || resolvedFlags.includes('v'))
140
+ ? any()
141
+ : asciiCaseFold(raw.firstSet)
142
+ const canMatchNewline = raw.canMatchNewline
143
+ const meta: ParserMeta = { firstSet, canMatchNewline, isTrivia: false }
144
+
145
+ return {
146
+ _tag: 'regex',
147
+ _meta: meta,
148
+ _def: { tag: 'regex', source, flags: resolvedFlags },
149
+ parse(input: string, pos: number, ctx: ParseContext): ParseResult<string> {
150
+ const scanEnd = scan?.(input, pos)
151
+ if (scanEnd !== undefined) {
152
+ if (scanEnd === null) return failAt(ctx, [`/${source}/`], pos)
153
+ const value = input.slice(pos, scanEnd)
154
+ const span = { start: pos, end: scanEnd }
155
+ if (canMatchNewline && ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, scanEnd)
156
+ if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value, span })
157
+ return { ok: true, value, span }
158
+ }
159
+ anchored.lastIndex = pos
160
+ const m = anchored.exec(input)
161
+ if (m === null) {
162
+ return failAt(ctx, [`/${source}/`], pos)
163
+ }
164
+ const span = { start: pos, end: pos + m[0]!.length }
165
+ if (canMatchNewline && ctx.trackLines) recordLineRangeFromContext(ctx, input, pos, span.end)
166
+ if (cstCaptureActive(ctx)) pushCstLeaf(ctx, { _tag: 'leaf', value: m[0]!, span })
167
+ return { ok: true, value: m[0]!, span }
168
+ },
169
+ }
170
+ }