parseman 0.46.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/README.md +8 -3
  2. package/dist/analysis/choice-cost-render.d.ts +12 -4
  3. package/dist/analysis/choice-cost-render.d.ts.map +1 -1
  4. package/dist/analysis/choice-cost.d.ts +2 -2
  5. package/dist/analysis/choice-cost.d.ts.map +1 -1
  6. package/dist/analysis/commitment.d.ts +131 -0
  7. package/dist/analysis/commitment.d.ts.map +1 -0
  8. package/dist/analysis/diagnostics.cjs +9929 -7465
  9. package/dist/analysis/diagnostics.cjs.map +3 -4
  10. package/dist/analysis/diagnostics.d.ts +2 -2
  11. package/dist/analysis/diagnostics.js +9929 -7465
  12. package/dist/analysis/diagnostics.js.map +3 -4
  13. package/dist/analysis/duplication.d.ts.map +1 -1
  14. package/dist/analysis/fix.d.ts +3 -3
  15. package/dist/analysis/fix.d.ts.map +1 -1
  16. package/dist/analysis/format-number.d.ts +23 -0
  17. package/dist/analysis/format-number.d.ts.map +1 -0
  18. package/dist/analysis/gating.d.ts +39 -2
  19. package/dist/analysis/gating.d.ts.map +1 -1
  20. package/dist/analysis/rebuild.d.ts.map +1 -1
  21. package/dist/analysis/terminal.d.ts +5 -2
  22. package/dist/analysis/terminal.d.ts.map +1 -1
  23. package/dist/cli/index.cjs +10865 -8535
  24. package/dist/cli/index.cjs.map +3 -4
  25. package/dist/cli/index.js +10865 -8535
  26. package/dist/cli/index.js.map +3 -4
  27. package/dist/combinators/adjacency.d.ts +100 -0
  28. package/dist/combinators/adjacency.d.ts.map +1 -0
  29. package/dist/combinators/choice.d.ts.map +1 -1
  30. package/dist/combinators/completions.d.ts.map +1 -1
  31. package/dist/combinators/dispatch.d.ts +8 -1
  32. package/dist/combinators/dispatch.d.ts.map +1 -1
  33. package/dist/combinators/expect.d.ts.map +1 -1
  34. package/dist/combinators/first-set.d.ts.map +1 -1
  35. package/dist/combinators/gate.d.ts.map +1 -1
  36. package/dist/combinators/grammar.d.ts +22 -0
  37. package/dist/combinators/grammar.d.ts.map +1 -1
  38. package/dist/combinators/literal.d.ts +8 -0
  39. package/dist/combinators/literal.d.ts.map +1 -1
  40. package/dist/combinators/map.d.ts +0 -1
  41. package/dist/combinators/map.d.ts.map +1 -1
  42. package/dist/combinators/node.d.ts +13 -3
  43. package/dist/combinators/node.d.ts.map +1 -1
  44. package/dist/combinators/parser.d.ts +1 -1
  45. package/dist/combinators/parser.d.ts.map +1 -1
  46. package/dist/combinators/ref.d.ts.map +1 -1
  47. package/dist/combinators/regex.d.ts +1 -16
  48. package/dist/combinators/regex.d.ts.map +1 -1
  49. package/dist/combinators/repeat.d.ts +30 -2
  50. package/dist/combinators/repeat.d.ts.map +1 -1
  51. package/dist/combinators/scanTo.d.ts +71 -1
  52. package/dist/combinators/scanTo.d.ts.map +1 -1
  53. package/dist/combinators/sequence.d.ts.map +1 -1
  54. package/dist/combinators/token.d.ts.map +1 -1
  55. package/dist/combinators/trivia-boundary.d.ts +26 -0
  56. package/dist/combinators/trivia-boundary.d.ts.map +1 -0
  57. package/dist/combinators/trivia-skip.d.ts +40 -1
  58. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  59. package/dist/combinators/withCtx.d.ts.map +1 -1
  60. package/dist/compiler/compile-linkable-table.d.ts +104 -0
  61. package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
  62. package/dist/compiler/degradation.d.ts +0 -2
  63. package/dist/compiler/degradation.d.ts.map +1 -1
  64. package/dist/compiler/fields.d.ts +13 -0
  65. package/dist/compiler/fields.d.ts.map +1 -1
  66. package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
  67. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  68. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  69. package/dist/compiler/linker.d.ts +47 -53
  70. package/dist/compiler/linker.d.ts.map +1 -1
  71. package/dist/compiler/token-alphabet.d.ts +81 -0
  72. package/dist/compiler/token-alphabet.d.ts.map +1 -0
  73. package/dist/compiler/token-dispatch.d.ts +95 -0
  74. package/dist/compiler/token-dispatch.d.ts.map +1 -0
  75. package/dist/compiler/token-scanner.d.ts +53 -0
  76. package/dist/compiler/token-scanner.d.ts.map +1 -0
  77. package/dist/compiler/value-usage.d.ts.map +1 -1
  78. package/dist/coverage.d.ts +1 -1
  79. package/dist/coverage.d.ts.map +1 -1
  80. package/dist/cst/capture-buffer.d.ts +35 -0
  81. package/dist/cst/capture-buffer.d.ts.map +1 -1
  82. package/dist/cst/reflection-attach.d.ts +8 -0
  83. package/dist/cst/reflection-attach.d.ts.map +1 -0
  84. package/dist/cst/reflection-symbols.d.ts +10 -0
  85. package/dist/cst/reflection-symbols.d.ts.map +1 -0
  86. package/dist/cst/reflection.d.ts +2 -4
  87. package/dist/cst/reflection.d.ts.map +1 -1
  88. package/dist/cst/root-trivia-scope.d.ts +26 -0
  89. package/dist/cst/root-trivia-scope.d.ts.map +1 -0
  90. package/dist/cst/trivia-charscan.d.ts +18 -0
  91. package/dist/cst/trivia-charscan.d.ts.map +1 -0
  92. package/dist/cst/trivia-kinds.d.ts +19 -0
  93. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  94. package/dist/functional/doc.d.ts.map +1 -1
  95. package/dist/functional/run-tabled.d.ts +12 -0
  96. package/dist/functional/run-tabled.d.ts.map +1 -0
  97. package/dist/functional/run.d.ts +25 -42
  98. package/dist/functional/run.d.ts.map +1 -1
  99. package/dist/grammar-metadata.d.ts +3 -0
  100. package/dist/grammar-metadata.d.ts.map +1 -0
  101. package/dist/index.cjs +9224 -7203
  102. package/dist/index.cjs.map +3 -4
  103. package/dist/index.d.ts +18 -9
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +9219 -7198
  106. package/dist/index.js.map +3 -4
  107. package/dist/language-service/index.cjs +311 -121
  108. package/dist/language-service/index.cjs.map +3 -4
  109. package/dist/language-service/index.js +311 -121
  110. package/dist/language-service/index.js.map +3 -4
  111. package/dist/oracle/index.cjs.map +0 -1
  112. package/dist/oracle/index.js.map +0 -1
  113. package/dist/parse-context.d.ts +49 -0
  114. package/dist/parse-context.d.ts.map +1 -0
  115. package/dist/plugin/evaluator.d.ts +40 -1
  116. package/dist/plugin/evaluator.d.ts.map +1 -1
  117. package/dist/plugin/index.cjs +11146 -9593
  118. package/dist/plugin/index.cjs.map +3 -4
  119. package/dist/plugin/index.d.ts.map +1 -1
  120. package/dist/plugin/index.js +11146 -9593
  121. package/dist/plugin/index.js.map +3 -4
  122. package/dist/regex/classes.d.ts +28 -0
  123. package/dist/regex/classes.d.ts.map +1 -1
  124. package/dist/regex/first-set.d.ts +14 -0
  125. package/dist/regex/first-set.d.ts.map +1 -1
  126. package/dist/run/index.cjs +136 -51
  127. package/dist/run/index.cjs.map +2 -3
  128. package/dist/run/index.js +136 -51
  129. package/dist/run/index.js.map +2 -3
  130. package/dist/spec/index.cjs +2898 -1623
  131. package/dist/spec/index.cjs.map +3 -4
  132. package/dist/spec/index.js +2898 -1623
  133. package/dist/spec/index.js.map +3 -4
  134. package/dist/spec/model.d.ts +8 -0
  135. package/dist/spec/model.d.ts.map +1 -1
  136. package/dist/table/assemble.d.ts +307 -0
  137. package/dist/table/assemble.d.ts.map +1 -0
  138. package/dist/table/cell.d.ts +42 -0
  139. package/dist/table/cell.d.ts.map +1 -0
  140. package/dist/table/child-slots.d.ts +30 -0
  141. package/dist/table/child-slots.d.ts.map +1 -0
  142. package/dist/table/compile-rule-map.d.ts +136 -0
  143. package/dist/table/compile-rule-map.d.ts.map +1 -0
  144. package/dist/table/compile.d.ts +56 -0
  145. package/dist/table/compile.d.ts.map +1 -0
  146. package/dist/table/duplication-hook.d.ts +32 -0
  147. package/dist/table/duplication-hook.d.ts.map +1 -0
  148. package/dist/table/emit-assembly.d.ts +139 -0
  149. package/dist/table/emit-assembly.d.ts.map +1 -0
  150. package/dist/table/emit.d.ts +113 -0
  151. package/dist/table/emit.d.ts.map +1 -0
  152. package/dist/table/encode-baseline.d.ts +16 -0
  153. package/dist/table/encode-baseline.d.ts.map +1 -0
  154. package/dist/table/encode.d.ts +71 -0
  155. package/dist/table/encode.d.ts.map +1 -0
  156. package/dist/table/exec-baseline.d.ts +9 -0
  157. package/dist/table/exec-baseline.d.ts.map +1 -0
  158. package/dist/table/exec.d.ts +72 -0
  159. package/dist/table/exec.d.ts.map +1 -0
  160. package/dist/table/fold.d.ts +25 -0
  161. package/dist/table/fold.d.ts.map +1 -0
  162. package/dist/table/index.cjs +11375 -0
  163. package/dist/table/index.cjs.map +6 -0
  164. package/dist/table/index.d.ts +100 -0
  165. package/dist/table/index.d.ts.map +1 -0
  166. package/dist/table/index.js +11352 -0
  167. package/dist/table/index.js.map +6 -0
  168. package/dist/table/inspect.d.ts +17 -0
  169. package/dist/table/inspect.d.ts.map +1 -0
  170. package/dist/table/ops.d.ts +446 -0
  171. package/dist/table/ops.d.ts.map +1 -0
  172. package/dist/table/program.d.ts +493 -0
  173. package/dist/table/program.d.ts.map +1 -0
  174. package/dist/table/run-support.d.ts +38 -0
  175. package/dist/table/run-support.d.ts.map +1 -0
  176. package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
  177. package/dist/table/scan-shapes.d.ts.map +1 -0
  178. package/dist/table/site-labels.d.ts +58 -0
  179. package/dist/table/site-labels.d.ts.map +1 -0
  180. package/dist/table/stamp.d.ts +70 -0
  181. package/dist/table/stamp.d.ts.map +1 -0
  182. package/dist/types.d.ts +80 -24
  183. package/dist/types.d.ts.map +1 -1
  184. package/dist/version.d.ts +1 -1
  185. package/package.json +25 -2
  186. package/src/analysis/choice-cost-gate.ts +359 -0
  187. package/src/analysis/choice-cost-render.ts +293 -0
  188. package/src/analysis/choice-cost.ts +1008 -0
  189. package/src/analysis/commitment.ts +557 -0
  190. package/src/analysis/corpus.ts +130 -0
  191. package/src/analysis/diagnose-render.ts +625 -0
  192. package/src/analysis/diagnose.ts +317 -0
  193. package/src/analysis/diagnostics.ts +33 -0
  194. package/src/analysis/duplication.ts +1955 -0
  195. package/src/analysis/fix-render.ts +217 -0
  196. package/src/analysis/fix.ts +669 -0
  197. package/src/analysis/format-number.ts +31 -0
  198. package/src/analysis/gating.ts +792 -0
  199. package/src/analysis/grammar.ts +72 -0
  200. package/src/analysis/rebuild.ts +287 -0
  201. package/src/analysis/terminal.ts +336 -0
  202. package/src/cli/index.ts +413 -0
  203. package/src/combinators/adjacency.ts +171 -0
  204. package/src/combinators/attempt.ts +41 -0
  205. package/src/combinators/case-fold.ts +90 -0
  206. package/src/combinators/choice.ts +466 -0
  207. package/src/combinators/completions.ts +64 -0
  208. package/src/combinators/dispatch.ts +358 -0
  209. package/src/combinators/expect.ts +159 -0
  210. package/src/combinators/first-set.ts +322 -0
  211. package/src/combinators/gate.ts +50 -0
  212. package/src/combinators/grammar.ts +272 -0
  213. package/src/combinators/keywords.ts +145 -0
  214. package/src/combinators/literal.ts +102 -0
  215. package/src/combinators/map.ts +106 -0
  216. package/src/combinators/node.ts +345 -0
  217. package/src/combinators/not.ts +55 -0
  218. package/src/combinators/parser.ts +266 -0
  219. package/src/combinators/peek.ts +63 -0
  220. package/src/combinators/probe.ts +25 -0
  221. package/src/combinators/ref.ts +109 -0
  222. package/src/combinators/regex.ts +170 -0
  223. package/src/combinators/repeat.ts +561 -0
  224. package/src/combinators/scanTo.ts +393 -0
  225. package/src/combinators/sequence.ts +221 -0
  226. package/src/combinators/token.ts +120 -0
  227. package/src/combinators/trivia-boundary.ts +48 -0
  228. package/src/combinators/trivia-skip.ts +821 -0
  229. package/src/combinators/withCtx.ts +45 -0
  230. package/src/compiler/build-arity.ts +330 -0
  231. package/src/compiler/compile-linkable-table.ts +209 -0
  232. package/src/compiler/degradation.ts +266 -0
  233. package/src/compiler/fields.ts +191 -0
  234. package/src/compiler/grammar-coverage-ids.ts +129 -0
  235. package/src/compiler/ir-serialize.ts +615 -0
  236. package/src/compiler/linker.ts +954 -0
  237. package/src/compiler/token-alphabet.ts +209 -0
  238. package/src/compiler/token-dispatch.ts +353 -0
  239. package/src/compiler/token-scanner.ts +318 -0
  240. package/src/compiler/value-usage.ts +147 -0
  241. package/src/coverage.ts +383 -0
  242. package/src/cst/capture-buffer.ts +297 -0
  243. package/src/cst/host-mode.ts +69 -0
  244. package/src/cst/reflection-attach.ts +11 -0
  245. package/src/cst/reflection-symbols.ts +9 -0
  246. package/src/cst/reflection.ts +149 -0
  247. package/src/cst/relative-spans.ts +207 -0
  248. package/src/cst/root-trivia-scope.ts +30 -0
  249. package/src/cst/trivia-charscan.ts +312 -0
  250. package/src/cst/trivia-entries.ts +285 -0
  251. package/src/cst/trivia-index.ts +131 -0
  252. package/src/cst/trivia-kinds.ts +358 -0
  253. package/src/cst/types.ts +76 -0
  254. package/src/cst/walk.ts +101 -0
  255. package/src/functional/doc.ts +959 -0
  256. package/src/functional/run-tabled.ts +153 -0
  257. package/src/functional/run.ts +558 -0
  258. package/src/grammar-metadata.ts +2 -0
  259. package/src/index.ts +189 -0
  260. package/src/language-service/index.ts +221 -0
  261. package/src/line-index.ts +131 -0
  262. package/src/oracle/digest.ts +620 -0
  263. package/src/oracle/index.ts +55 -0
  264. package/src/parse-context.ts +91 -0
  265. package/src/plugin/direct-builder-static.ts +102 -0
  266. package/src/plugin/evaluator.ts +1505 -0
  267. package/src/plugin/free-identifiers.ts +338 -0
  268. package/src/plugin/index.ts +2281 -0
  269. package/src/plugin/reducer-resolver.ts +606 -0
  270. package/src/recovery/scan.ts +155 -0
  271. package/src/regex/classes.ts +183 -0
  272. package/src/regex/first-set.ts +316 -0
  273. package/src/run/index.ts +31 -0
  274. package/src/spec/ebnf.ts +128 -0
  275. package/src/spec/index.ts +54 -0
  276. package/src/spec/model.ts +636 -0
  277. package/src/spec/railroad-lib.ts +559 -0
  278. package/src/spec/railroad.ts +256 -0
  279. package/src/table/assemble.ts +2994 -0
  280. package/src/table/cell.ts +42 -0
  281. package/src/table/child-slots.ts +113 -0
  282. package/src/table/compile-rule-map.ts +336 -0
  283. package/src/table/compile.ts +244 -0
  284. package/src/table/duplication-hook.ts +89 -0
  285. package/src/table/emit-assembly.ts +1766 -0
  286. package/src/table/emit.ts +418 -0
  287. package/src/table/encode-baseline.ts +340 -0
  288. package/src/table/encode.ts +1569 -0
  289. package/src/table/exec-baseline.ts +537 -0
  290. package/src/table/exec.ts +1672 -0
  291. package/src/table/fold.ts +38 -0
  292. package/src/table/index.ts +108 -0
  293. package/src/table/inspect.ts +52 -0
  294. package/src/table/ops.ts +459 -0
  295. package/src/table/program.ts +855 -0
  296. package/src/table/run-support.ts +66 -0
  297. package/src/table/scan-shapes.ts +1578 -0
  298. package/src/table/site-labels.ts +228 -0
  299. package/src/table/stamp.ts +229 -0
  300. package/src/types.ts +563 -0
  301. package/src/version.ts +16 -0
  302. package/dist/compiler/codegen.d.ts +0 -334
  303. package/dist/compiler/codegen.d.ts.map +0 -1
  304. package/dist/compiler/inline-build.d.ts +0 -11
  305. package/dist/compiler/inline-build.d.ts.map +0 -1
  306. package/dist/compiler/inline-callback.d.ts +0 -21
  307. package/dist/compiler/inline-callback.d.ts.map +0 -1
  308. package/dist/compiler/line-index.d.ts +0 -2
  309. package/dist/compiler/line-index.d.ts.map +0 -1
  310. package/dist/compiler/module-hoist.d.ts +0 -106
  311. package/dist/compiler/module-hoist.d.ts.map +0 -1
  312. package/dist/compiler/scannable-run.d.ts.map +0 -1
  313. package/dist/compiler/scannable-terminal.d.ts +0 -23
  314. package/dist/compiler/scannable-terminal.d.ts.map +0 -1
  315. package/dist/compiler/trivia-fast-path.d.ts +0 -38
  316. package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
@@ -0,0 +1,821 @@
1
+ import type { Combinator, ParseContext, ParseFail, ParseResult } from '../types.ts'
2
+ import { literalCodePoints, parseClassOperand, parseClassRanges } from '../regex/classes.ts'
3
+ import {
4
+ analyzeLabeledTrivia,
5
+ recordTriviaChunks,
6
+ scanLabeledTriviaEnd,
7
+ scanLabeledTriviaChunks,
8
+ visitLabeledTrivia,
9
+ } from '../cst/trivia-kinds.ts'
10
+ import {
11
+ pushCstTriviaEntry,
12
+ pushTriviaLogEntry,
13
+ rollbackCstCapture,
14
+ rollbackCstCaptureAt,
15
+ saveCstMark,
16
+ } from '../cst/capture-buffer.ts'
17
+ import { recordLineRangeFromContext } from '../line-index.ts'
18
+ import { createDetachedParseContext } from '../parse-context.ts'
19
+
20
+ /**
21
+ * Result of scanning trivia: the position after it, plus a `commit()` that
22
+ * records the matched trivia tokens into the active rawChildren collector.
23
+ * Recording is deferred so callers that may reject the following item (e.g.
24
+ * many()/oneOrMore() retries) can advance speculatively without recording
25
+ * trivia that doesn't actually sit between two accepted items.
26
+ */
27
+ export type TriviaScan = { end: number; commit: () => void }
28
+
29
+ /** Saved lengths for rolling back speculative trivia commits. */
30
+ export type TriviaRollbackMark = { raw: number; tlog: number; leaves: number; fields: number; errors: number; log: number; rootLog: number }
31
+
32
+ const NOOP_COMMIT = () => {}
33
+ export type FastTriviaScanner = (input: string, cur: number) => number
34
+ /**
35
+ * Bounded structural cache for the handful of trivia grammars a process actively
36
+ * parses. A module-global WeakMap used to allocate on every table-runtime
37
+ * import, even though emitted artifacts only need this during construction.
38
+ *
39
+ * Four scalar slots keep the common grammar switches hot without a collection,
40
+ * a per-parser property transition, or an unbounded process-retaining cache.
41
+ * Cache misses merely rebuild a construction-time scanner and remain correct.
42
+ */
43
+ let fastTrivia0: Combinator<unknown> | undefined
44
+ let fastTrivia1: Combinator<unknown> | undefined
45
+ let fastTrivia2: Combinator<unknown> | undefined
46
+ let fastTrivia3: Combinator<unknown> | undefined
47
+ let fastScanner0: FastTriviaScanner | null | undefined
48
+ let fastScanner1: FastTriviaScanner | null | undefined
49
+ let fastScanner2: FastTriviaScanner | null | undefined
50
+ let fastScanner3: FastTriviaScanner | null | undefined
51
+
52
+ /** True when trivia recording must be deferred until the following term commits. */
53
+ export function needsDeferredTriviaCommit(ctx: ParseContext): boolean {
54
+ return ctx._triviaLog !== undefined || ctx._rootTriviaLog !== undefined || ctx._cstBuf !== undefined || ctx._cstTriviaLog !== undefined
55
+ }
56
+
57
+ export function saveTriviaMark(ctx: ParseContext): TriviaRollbackMark {
58
+ const m = saveCstMark(ctx)
59
+ return {
60
+ raw: m.raw,
61
+ tlog: m.tlog,
62
+ leaves: m.leaves,
63
+ fields: m.fields,
64
+ errors: m.errors,
65
+ log: ctx._triviaLog ? ctx._triviaLog.length : 0,
66
+ rootLog: ctx._rootTriviaLog ? ctx._rootTriviaLog.length : 0,
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Trivia rollback from SCALAR marks — the allocation-free twin of
72
+ * `rollbackTrivia`. See `rollbackCstCaptureAt`; `saveTriviaMark` allocates twice
73
+ * (this wrapper plus the CST mark it delegates to) and the table driver took one
74
+ * per sequence term and per repetition item.
75
+ */
76
+ export function rollbackTriviaAt(
77
+ ctx: ParseContext,
78
+ raw: number,
79
+ tlog: number,
80
+ leaves: number,
81
+ fields: number,
82
+ errors: number,
83
+ log: number,
84
+ rootLog: number,
85
+ ): void {
86
+ rollbackCstCaptureAt(ctx, raw, tlog, leaves, fields, errors)
87
+ if (ctx._triviaLog && ctx._triviaLog.length !== log) ctx._triviaLog.length = log
88
+ if (ctx._rootTriviaLog && ctx._rootTriviaLog.length !== rootLog) ctx._rootTriviaLog.length = rootLog
89
+ }
90
+
91
+ export function rollbackTrivia(ctx: ParseContext, mark: TriviaRollbackMark): void {
92
+ rollbackCstCapture(ctx, { raw: mark.raw, tlog: mark.tlog, leaves: mark.leaves, fields: mark.fields, errors: mark.errors })
93
+ // Guarded like every other truncation — see rollbackCstCapture.
94
+ if (ctx._triviaLog && ctx._triviaLog.length !== mark.log) ctx._triviaLog.length = mark.log
95
+ if (ctx._rootTriviaLog && ctx._rootTriviaLog.length !== mark.rootLog) ctx._rootTriviaLog.length = mark.rootLog
96
+ }
97
+
98
+ function parseTriviaNoCapture(
99
+ triviaP: Combinator<unknown>,
100
+ input: string,
101
+ cur: number,
102
+ ctx: ParseContext,
103
+ ): ParseResult<unknown> {
104
+ const probeCtx = createDetachedParseContext(ctx.trackLines, ctx.state)
105
+ probeCtx._lineIndex = ctx._lineIndex
106
+ probeCtx._lineStarts = ctx._lineStarts
107
+ probeCtx._lineScannedTo = ctx._lineScannedTo
108
+ const result = triviaP.parse(input, cur, probeCtx)
109
+ ctx._lineScannedTo = probeCtx._lineScannedTo
110
+ return result
111
+ }
112
+
113
+ /**
114
+ * A ZERO-WIDTH LOOKAHEAD's mark: everything `saveTriviaMark` covers, plus the
115
+ * completions probe's current best failure.
116
+ *
117
+ * `_probe` is a sink like any other — `failAt` (probe.ts) replaces
118
+ * `ctx._probe.best` whenever a failure lands at or before the cursor — but it is
119
+ * deliberately NOT part of `rollbackTrivia`, because most rollbacks must leave it
120
+ * alone. A failed `choice` arm or `sequence` term SHOULD keep its contribution:
121
+ * merging the expectations of alternatives that failed at the cursor is exactly
122
+ * how `completionsAt` builds its set.
123
+ *
124
+ * A lookahead is the exception. `peek`/`not` consume nothing on EITHER outcome
125
+ * and promise to leave no observable trace, so expectations raised INSIDE the
126
+ * probe are not reachable from the enclosing grammar at the cursor:
127
+ * `peek(sequence(literal('a'), literal('zzz')))` over `"ab"` offered `"zzz"` as
128
+ * the ONLY completion, though no input can ever reach it there.
129
+ */
130
+ export type LookaheadMark = TriviaRollbackMark & { probeBest: ParseFail | null }
131
+
132
+ export function saveLookaheadMark(ctx: ParseContext): LookaheadMark {
133
+ return { ...saveTriviaMark(ctx), probeBest: ctx._probe ? ctx._probe.best : null }
134
+ }
135
+
136
+ export function rollbackLookahead(ctx: ParseContext, mark: LookaheadMark): void {
137
+ rollbackTrivia(ctx, mark)
138
+ if (ctx._probe) ctx._probe.best = mark.probeBest
139
+ }
140
+
141
+ function scanWithLabels(input: string, cur: number, ctx: ParseContext): TriviaScan {
142
+ const triviaP = ctx.trivia!
143
+ const spec = analyzeLabeledTrivia(triviaP)
144
+ if (!spec) return { end: cur, commit: NOOP_COMMIT }
145
+
146
+ const log = ctx._triviaLog
147
+ const rootLog = ctx._rootTriviaLog
148
+ const rootKinds = ctx._rootTriviaKindIndex
149
+ const captureTl = ctx.captureTrivia && (ctx._cstBuf !== undefined || ctx._cstTriviaLog !== undefined)
150
+ const mask = ctx._triviaCaptureMask
151
+ let fullRows: number[] | undefined
152
+ let rootRows: number[] | undefined
153
+ let cstRows: number[] | undefined
154
+ const visitedEnd = visitLabeledTrivia(input, cur, spec, ctx.state, (start, matchEnd, kindIndex) => {
155
+ if (log !== undefined) (fullRows ??= []).push(start, matchEnd, kindIndex)
156
+ const selectedKind = rootLog === undefined || rootKinds === undefined || ctx._rootTriviaCapture === false
157
+ ? undefined
158
+ : rootKinds[spec.labels[kindIndex] ?? '']
159
+ if (selectedKind !== undefined) (rootRows ??= []).push(start, matchEnd, selectedKind)
160
+ if (captureTl && (mask === undefined || (mask & (1 << kindIndex)) !== 0)) {
161
+ (cstRows ??= []).push(start, matchEnd, kindIndex)
162
+ }
163
+ })
164
+ if (visitedEnd !== undefined) {
165
+ if (visitedEnd === cur) return { end: cur, commit: NOOP_COMMIT }
166
+ return {
167
+ end: visitedEnd,
168
+ commit: () => {
169
+ if (fullRows !== undefined && log !== undefined) log.push(...fullRows)
170
+ if (rootRows !== undefined && rootLog !== undefined) {
171
+ for (let i = 0; i < rootRows.length; i += 3) {
172
+ rootLog.push(cur, visitedEnd, rootRows[i]!, rootRows[i + 1]!, rootRows[i + 2]!)
173
+ }
174
+ }
175
+ if (cstRows !== undefined) {
176
+ for (let i = 0; i < cstRows.length; i += 3) {
177
+ pushCstTriviaEntry(ctx, cstRows[i]!, cstRows[i + 1]!, cstRows[i + 2]!)
178
+ }
179
+ }
180
+ },
181
+ }
182
+ }
183
+
184
+ const { end, chunks } = scanLabeledTriviaChunks(input, cur, spec, ctx.state)
185
+ if (end === cur) return { end: cur, commit: NOOP_COMMIT }
186
+
187
+ return {
188
+ end,
189
+ commit: () => recordTriviaChunks(ctx, chunks),
190
+ }
191
+ }
192
+
193
+ /** Skip classified trivia without constructing retained chunk records. */
194
+ function skipWithLabels(input: string, cur: number, ctx: ParseContext): number {
195
+ const spec = analyzeLabeledTrivia(ctx.trivia!)
196
+ if (spec) return scanLabeledTriviaEnd(input, cur, spec, ctx.state)
197
+ const tr = parseTriviaNoCapture(ctx.trivia!, input, cur, ctx)
198
+ return tr.ok && tr.span.end > cur ? tr.span.end : cur
199
+ }
200
+
201
+ /** End of the trivia run at `cur` via the interpreter — the no-scanner fallback. */
202
+ function triviaEndByParse(
203
+ triviaP: Combinator<unknown>,
204
+ input: string,
205
+ cur: number,
206
+ ctx: ParseContext,
207
+ trackLines: boolean,
208
+ ): number {
209
+ const tr = triviaP.parse(input, cur, createDetachedParseContext(trackLines, ctx.state))
210
+ return tr.ok ? tr.span.end : cur
211
+ }
212
+
213
+ /**
214
+ * Skip trivia at `cur` and return the new position. No recording, no wrapper
215
+ * object — use between sequence/repeat terms when CST trivia capture is off.
216
+ */
217
+ export function advanceTrivia(input: string, cur: number, ctx: ParseContext): number {
218
+ const triviaP = ctx.trivia
219
+ if (!triviaP) return cur
220
+ if (!ctx.trackLines) {
221
+ const fast = fastTriviaScanner(triviaP)
222
+ if (fast) return fast(input, cur)
223
+ if (ctx.triviaKindLabels) return skipWithLabels(input, cur, ctx)
224
+ const tr = triviaP.parse(input, cur, createDetachedParseContext(ctx.trackLines, ctx.state))
225
+ return tr.ok && tr.span.end > cur ? tr.span.end : cur
226
+ }
227
+ const trackTriviaLines = ctx.trackLines && triviaP._meta.canMatchNewline
228
+ const fast = fastTriviaScanner(triviaP)
229
+ if (fast) {
230
+ const end = fast(input, cur)
231
+ if (trackTriviaLines) recordLineRangeFromContext(ctx, input, cur, end)
232
+ return end
233
+ }
234
+ if (ctx.triviaKindLabels) {
235
+ const end = skipWithLabels(input, cur, ctx)
236
+ if (trackTriviaLines) recordLineRangeFromContext(ctx, input, cur, end)
237
+ return end
238
+ }
239
+ const tr = parseTriviaNoCapture(triviaP, input, cur, ctx)
240
+ return tr.ok && tr.span.end > cur ? tr.span.end : cur
241
+ }
242
+
243
+ /**
244
+ * Scan trivia at `cur` using `ctx.trivia`, WITHOUT recording it. Returns the
245
+ * position after the trivia (or `cur` if none) and a `commit()` to record it.
246
+ */
247
+ export function scanTrivia(input: string, cur: number, ctx: ParseContext): TriviaScan {
248
+ const triviaP = ctx.trivia
249
+ if (!triviaP) return { end: cur, commit: NOOP_COMMIT }
250
+
251
+ const log = ctx._triviaLog
252
+ const rootLog = ctx._rootTriviaLog
253
+ const captureTl = ctx.captureTrivia && (ctx._cstBuf !== undefined || ctx._cstTriviaLog !== undefined)
254
+ if (!ctx.trackLines) {
255
+ const fast = !ctx.triviaKindLabels ? fastTriviaScanner(triviaP) : null
256
+ if (fast && log === undefined && !captureTl) {
257
+ return { end: fast(input, cur), commit: NOOP_COMMIT }
258
+ }
259
+
260
+ if (ctx.triviaKindLabels && (log !== undefined || rootLog !== undefined || captureTl)) {
261
+ return scanWithLabels(input, cur, ctx)
262
+ }
263
+
264
+ // LABELLED TRIVIA WITH NOTHING TO RECORD. The branch above took every sink,
265
+ // so no log, no root log and no capture sink is live here: the only thing
266
+ // that can escape this call is `end`. `advanceTrivia:205` has always answered
267
+ // that with the fused scanner, labels or not; this is the same answer in the
268
+ // twin that returns a `TriviaScan`, and it is the difference between one
269
+ // regex-shaped run and a per-character classify whose kind is then dropped.
270
+ if (ctx.triviaKindLabels) {
271
+ const labelledFast = fastTriviaScanner(triviaP)
272
+ return { end: labelledFast ? labelledFast(input, cur) : skipWithLabels(input, cur, ctx), commit: NOOP_COMMIT }
273
+ }
274
+
275
+ if (log !== undefined || rootLog !== undefined || captureTl) {
276
+ // CAPTURE DOES NOT NEED THE INTERPRETER. What lands in either sink is the
277
+ // one `(cur, end)` row below — the trivia's internal structure is never
278
+ // read — so a scanner that agrees with `triviaP` on `end` produces byte-
279
+ // identical capture. Delegating to `triviaP.parse` cost a detached
280
+ // `ParseContext` plus a full re-entry into the combinator interpreter PER
281
+ // SEQUENCE TERM, and bought nothing that survived the call.
282
+ const end = fast
283
+ ? fast(input, cur)
284
+ : triviaEndByParse(triviaP, input, cur, ctx, log !== undefined ? false : ctx.trackLines)
285
+ if (end === cur) return { end: cur, commit: NOOP_COMMIT }
286
+ return {
287
+ end,
288
+ commit: () => {
289
+ pushTriviaLogEntry(ctx, cur, end)
290
+ if (captureTl) pushCstTriviaEntry(ctx, cur, end)
291
+ },
292
+ }
293
+ }
294
+
295
+ const tr = triviaP.parse(input, cur, createDetachedParseContext(ctx.trackLines, ctx.state))
296
+ return { end: tr.ok ? tr.span.end : cur, commit: NOOP_COMMIT }
297
+ }
298
+ const trackTriviaLines = ctx.trackLines && triviaP._meta.canMatchNewline
299
+
300
+ const fast = !ctx.triviaKindLabels ? fastTriviaScanner(triviaP) : null
301
+ if (fast && log === undefined && !captureTl) {
302
+ const end = fast(input, cur)
303
+ if (trackTriviaLines) recordLineRangeFromContext(ctx, input, cur, end)
304
+ return { end, commit: NOOP_COMMIT }
305
+ }
306
+
307
+ if (ctx.triviaKindLabels && (log !== undefined || rootLog !== undefined || captureTl)) {
308
+ const scan = scanWithLabels(input, cur, ctx)
309
+ if (trackTriviaLines) recordLineRangeFromContext(ctx, input, cur, scan.end)
310
+ return scan
311
+ }
312
+
313
+ // The `trackLines` twin of the branch above, same reasoning: nothing records,
314
+ // so `end` is the whole result and the fused scanner produces it. The line
315
+ // range is recorded from the SPAN either way, which is why the scanner loses
316
+ // nothing here that `skipWithLabels` was providing.
317
+ if (ctx.triviaKindLabels) {
318
+ const labelledFast = fastTriviaScanner(triviaP)
319
+ const end = labelledFast ? labelledFast(input, cur) : skipWithLabels(input, cur, ctx)
320
+ if (trackTriviaLines) recordLineRangeFromContext(ctx, input, cur, end)
321
+ return { end, commit: NOOP_COMMIT }
322
+ }
323
+
324
+ if (log !== undefined || rootLog !== undefined || captureTl) {
325
+ // Same equivalence as the `trackLines`-off twin above; line ranges are
326
+ // recorded from the SPAN either way, so the scanner loses nothing.
327
+ let end: number
328
+ if (fast) {
329
+ end = fast(input, cur)
330
+ if (trackTriviaLines) recordLineRangeFromContext(ctx, input, cur, end)
331
+ } else {
332
+ const tr = parseTriviaNoCapture(triviaP, input, cur, ctx)
333
+ end = tr.ok ? tr.span.end : cur
334
+ }
335
+ if (end === cur) return { end: cur, commit: NOOP_COMMIT }
336
+ return {
337
+ end,
338
+ commit: () => {
339
+ pushTriviaLogEntry(ctx, cur, end)
340
+ if (captureTl) pushCstTriviaEntry(ctx, cur, end)
341
+ },
342
+ }
343
+ }
344
+
345
+ const tr = parseTriviaNoCapture(triviaP, input, cur, ctx)
346
+ return { end: tr.ok ? tr.span.end : cur, commit: NOOP_COMMIT }
347
+ }
348
+
349
+ /**
350
+ * Skip trivia at `cur` through an INSTALLED scanner, recording it immediately.
351
+ *
352
+ * The table drivers install a scanner only for UNLABELLED trivia with
353
+ * `trackLines` off, and that is exactly the case where the deferred path's whole
354
+ * commit is the single row `(cur, end)` — so there is nothing to defer, and the
355
+ * `{ end, commit }` pair plus its closure that `scanTrivia(…).commit()` allocated
356
+ * on EVERY sequence term of EVERY capturing grammar buy nothing. This is that
357
+ * branch with the allocations removed, kept here so recording has one home.
358
+ */
359
+ export function skipTriviaScanned(
360
+ s: FastTriviaScanner,
361
+ input: string,
362
+ cur: number,
363
+ ctx: ParseContext,
364
+ ): number {
365
+ const end = s(input, cur)
366
+ if (end !== cur) {
367
+ // `pushTriviaLogEntry`'s kind-index branch is unreachable without labels.
368
+ if (ctx._triviaLog !== undefined) ctx._triviaLog.push(cur, end)
369
+ if (ctx.captureTrivia === true && (ctx._cstBuf !== undefined || ctx._cstTriviaLog !== undefined)) {
370
+ pushCstTriviaEntry(ctx, cur, end)
371
+ }
372
+ }
373
+ return end
374
+ }
375
+
376
+ /**
377
+ * Consume trivia at `cur`, recording it immediately. For callers that always
378
+ * accept the trivia between two committed terms (e.g. sequence/sepBy).
379
+ */
380
+ export function consumeTrivia(input: string, cur: number, ctx: ParseContext): number {
381
+ if (!needsDeferredTriviaCommit(ctx)) return advanceTrivia(input, cur, ctx)
382
+ const scan = scanTrivia(input, cur, ctx)
383
+ scan.commit()
384
+ return scan.end
385
+ }
386
+
387
+ /**
388
+ * End of the trivia run at `cur`, WITHOUT recording any of it.
389
+ *
390
+ * The read-only twin of `consumeTrivia`, for a zero-width ASSERTION that wants the
391
+ * adjacency fact and must leave no trace: the deferred path returns a scan whose
392
+ * `commit()` is simply never called, so nothing lands in any buffer and there is
393
+ * nothing to roll back. The following term re-scans the same gap and owns the
394
+ * commit/rewind decision exactly as if the assertion were not there.
395
+ */
396
+ export function probeTriviaEnd(input: string, cur: number, ctx: ParseContext): number {
397
+ if (!ctx.trivia) return cur
398
+ // "WITHOUT recording any of it" has to include LINE ranges, and it did not.
399
+ // Both paths call `recordLineRangeFromContext` whenever
400
+ // `ctx.trackLines && trivia.canMatchNewline`, so a rejected `notAdjacent()`
401
+ // left a line record behind for a gap the parse then re-scanned — the probe was
402
+ // read-only for buffers and read-write for line data. Probing through a context
403
+ // with `trackLines` off makes `trackTriviaLines` false in both scanners, so
404
+ // there is nothing to record and nothing to undo. Everything the scanners
405
+ // actually need to find the end (`trivia`, `state`, the deferred-commit sinks)
406
+ // is carried through unchanged.
407
+ const probeCtx: ParseContext = { ...ctx, trackLines: false }
408
+ if (needsDeferredTriviaCommit(probeCtx)) return scanTrivia(input, cur, probeCtx).end
409
+ return advanceTrivia(input, cur, probeCtx)
410
+ }
411
+
412
+ /**
413
+ * The specialised scanner for a trivia combinator, or null when its shape has no
414
+ * lowering. Memoized on the combinator.
415
+ *
416
+ * EXPORTED because G5's leaf swap needs it at TABLE-BUILD time: the table driver
417
+ * resolves the scanner once, per trivia entry, and installs the closure at scope
418
+ * entry — where the generic path called this (a WeakMap lookup) plus a chain of
419
+ * option branches on EVERY sequence term.
420
+ */
421
+ export function fastTriviaScanner(trivia: Combinator<unknown>): FastTriviaScanner | null {
422
+ if (trivia === fastTrivia0) return fastScanner0!
423
+ if (trivia === fastTrivia1) return fastScanner1!
424
+ if (trivia === fastTrivia2) return fastScanner2!
425
+ if (trivia === fastTrivia3) return fastScanner3!
426
+ const scanner = buildFastTriviaScanner(trivia)
427
+ fastTrivia3 = fastTrivia2
428
+ fastScanner3 = fastScanner2
429
+ fastTrivia2 = fastTrivia1
430
+ fastScanner2 = fastScanner1
431
+ fastTrivia1 = fastTrivia0
432
+ fastScanner1 = fastScanner0
433
+ fastTrivia0 = trivia
434
+ fastScanner0 = scanner
435
+ return scanner
436
+ }
437
+
438
+ function buildFastTriviaScanner(trivia: Combinator<unknown>): FastTriviaScanner | null {
439
+ const core = trivia._def.tag === 'trivia' ? trivia._def.parser : trivia
440
+ const direct = regexTriviaScanner(core)
441
+ if (direct) return direct
442
+
443
+ const repeat = core._def.tag === 'oneOrMore' || (core._def.tag === 'many' && core._def.min >= 1)
444
+ ? core._def.parser
445
+ : null
446
+ if (!repeat) return null
447
+
448
+ const one = regexTriviaScanner(repeat)
449
+ if (one) return loopScanner([one])
450
+
451
+ if (repeat._def.tag !== 'choice') return null
452
+ const arms = repeat._def.parsers.map(regexTriviaScanner)
453
+ if (arms.some(s => s === null)) return null
454
+ return loopScanner(arms as FastTriviaScanner[])
455
+ }
456
+
457
+ function loopScanner(arms: FastTriviaScanner[]): FastTriviaScanner {
458
+ return (input, cur) => {
459
+ let pos = cur
460
+ scan: while (pos < input.length) {
461
+ for (const arm of arms) {
462
+ const end = arm(input, pos)
463
+ if (end > pos) {
464
+ pos = end
465
+ continue scan
466
+ }
467
+ }
468
+ break
469
+ }
470
+ return pos
471
+ }
472
+ }
473
+
474
+ function regexTriviaScanner(parser: Combinator<unknown>): FastTriviaScanner | null {
475
+ // A `label()` WRAPPER IS A NAME, NOT A SHAPE. `classifiedTrivia()` builds
476
+ // `trivia(oneOrMore(choice(label(name, regex)…)))` (combinators/map.ts), and
477
+ // this declined the whole thing on the wrapper alone — every arm BODY is a
478
+ // plain regex, and `encode.ts:288` already reads them straight back out as
479
+ // `[label, source, flags]` triples.
480
+ //
481
+ // What a label carries is which KIND matched, and this scanner never answered
482
+ // that question for an unlabelled grammar either: it returns an end offset and
483
+ // nothing else. So unwrapping loses no information the result could hold. The
484
+ // caller decides whether kinds are needed and only reaches here when they are
485
+ // not — see `scanTrivia`, which still routes every recording path through
486
+ // `scanWithLabels`.
487
+ //
488
+ // The cost of declining was not a slower scan, it was NO scan: with
489
+ // `fastTriviaScanner` null for all four of jess's trivia slots, `program.ts:475`
490
+ // filled `triviaScan` with nulls, `advanceTrivia:205` fell past its fast arm,
491
+ // and every trivia gap in every dialect went through the labelled char scanner.
492
+ const d = parser._def.tag === 'label' ? parser._def.parser._def : parser._def
493
+ if (d.tag !== 'regex' || d.flags) return null
494
+ const source = d.source
495
+ return classRunSource(source)
496
+ ?? altStarSource(source)
497
+ ?? prefixRunSource(source)
498
+ ?? delimitedSource(source)
499
+ }
500
+
501
+ /**
502
+ * Literal fragments are compared with `charCodeAt`, which reads CODE UNITS — the
503
+ * same granularity the non-`u` engine matches at (a `u` flag is rejected upstream
504
+ * with every other flag). An astral literal would have been folded to one code
505
+ * POINT by `literalCodePoints`, so it could never compare equal; declining keeps
506
+ * the scanner exact rather than silently under-matching.
507
+ */
508
+ function bmpString(cps: number[] | null): string | null {
509
+ if (!cps) return null
510
+ for (let i = 0; i < cps.length; i++) if (cps[i]! > 0xffff) return null
511
+ return String.fromCharCode(...cps)
512
+ }
513
+
514
+ function inRanges(cp: number, ranges: Array<[number, number]>): boolean {
515
+ for (let i = 0; i < ranges.length; i++) {
516
+ const r = ranges[i]!
517
+ if (cp >= r[0] && cp <= r[1]) return true
518
+ }
519
+ return false
520
+ }
521
+
522
+ /** Greedy scanner for a positive char-class body (`^`-negated bodies are rejected upstream). */
523
+ function classScanner(classBody: string): FastTriviaScanner | null {
524
+ const ranges = parseClassRanges(classBody)
525
+ if (!ranges) return null
526
+ return (input, cur) => {
527
+ let pos = cur
528
+ while (pos < input.length && inRanges(input.charCodeAt(pos), ranges)) pos++
529
+ return pos
530
+ }
531
+ }
532
+
533
+ /** Scanner for a `C[^\n\r]*` arm — consumes its leader through a line break. */
534
+ function untilLineBreakScanner(leaderCode: number): FastTriviaScanner {
535
+ return (input, cur) => {
536
+ if (input.charCodeAt(cur) !== leaderCode) return cur
537
+ let pos = cur + 1
538
+ while (pos < input.length) {
539
+ const cc = input.charCodeAt(pos)
540
+ if (cc === 10 || cc === 13) break
541
+ pos++
542
+ }
543
+ return pos
544
+ }
545
+ }
546
+
547
+ /** A bare positive char-class run `[class]*` / `[class]+` (`[ \t\n\r,]*`, …). */
548
+ function classRunSource(source: string): FastTriviaScanner | null {
549
+ const m = /^\[([^\]^](?:[^\]]|\\.)*)\][*+]$/.exec(source)
550
+ return m ? classScanner(m[1]!) : null
551
+ }
552
+
553
+ /**
554
+ * `<literal><class-run>` — a literal opener followed by a single char-class run,
555
+ * `//[^\n]*` and `#[^\n\r]*` being the line-comment spellings of it. The class may
556
+ * be negated or positive; `*` admits the bare opener, `+` requires one class char.
557
+ *
558
+ * This is the whole-arm generalisation of `classifyTriviaArm`'s `untilLineBreak`,
559
+ * which only ever saw ONE leader char inside a `(?:…)*` group — a two-char `//`
560
+ * opener had no lowering at all, so every Less trivia scan fell back to the
561
+ * interpreter.
562
+ */
563
+ function prefixRunSource(source: string): FastTriviaScanner | null {
564
+ const m = /^((?:\\.|[^\\[\]()*+?|^$.{}])*)(\[(?:\\.|[^\]])+\])([*+])$/.exec(source)
565
+ if (!m) return null
566
+ const prefix = bmpString(literalCodePoints(m[1]!))
567
+ if (prefix === null) return null
568
+ const cls = parseClassOperand(m[2]!)
569
+ if (!cls) return null
570
+ const { ranges, negated } = cls
571
+ const minOne = m[3] === '+'
572
+ return (input, cur) => {
573
+ if (!input.startsWith(prefix, cur)) return cur
574
+ const start = cur + prefix.length
575
+ const len = input.length
576
+ let pos = start
577
+ while (pos < len && inRanges(input.charCodeAt(pos), ranges) !== negated) pos++
578
+ return minOne && pos === start ? cur : pos
579
+ }
580
+ }
581
+
582
+ /**
583
+ * `<open>(?:body)*<close>` scanned as "advance to the first `close`" — the block
584
+ * comment shape (`/*…*\/`), which no other recognizer here can classify, so css
585
+ * and Less trivia had NO fast scanner at all.
586
+ *
587
+ * The lowering ignores the body, so it is only legal when `(?:body)*` provably
588
+ * cannot span the `close` literal; `delimitedBodySound` is the proof, and an
589
+ * unproven body declines rather than scanning wrong. Ported from the archived
590
+ * `scannable-run.ts`, whose coverage is the specification for this shape.
591
+ */
592
+ function delimitedSource(source: string): FastTriviaScanner | null {
593
+ // An escaped backslash in the body makes this a STRING shape, where "advance to
594
+ // the first close" would stop at an escaped delimiter. Not modelled; decline.
595
+ if (source.includes('\\\\')) return null
596
+ let i = 0
597
+ while (i < source.length && source[i] !== '(') i += source[i] === '\\' ? 2 : 1
598
+ if (source[i] !== '(' || source[i + 1] !== '?' || source[i + 2] !== ':') return null
599
+ let k = i + 3
600
+ let depth = 1
601
+ while (k < source.length && depth > 0) {
602
+ const ch = source[k]
603
+ if (ch === '\\') { k += 2; continue }
604
+ if (ch === '[') {
605
+ let j = k + 1
606
+ if (source[j] === '^') j++
607
+ while (j < source.length && source[j] !== ']') j += source[j] === '\\' ? 2 : 1
608
+ if (source[j] !== ']') return null
609
+ k = j + 1
610
+ continue
611
+ }
612
+ if (ch === '(') depth++
613
+ else if (ch === ')') depth--
614
+ k++
615
+ }
616
+ if (depth !== 0 || source[k] !== '*') return null
617
+ const body = source.slice(i + 3, k - 1)
618
+ const openCps = literalCodePoints(source.slice(0, i))
619
+ const closeCps = literalCodePoints(source.slice(k + 1))
620
+ if (!openCps || !closeCps || !delimitedBodySound(body, closeCps)) return null
621
+ const open = bmpString(openCps)
622
+ const close = bmpString(closeCps)
623
+ if (open === null || close === null) return null
624
+ return (input, cur) => {
625
+ if (!input.startsWith(open, cur)) return cur
626
+ const hit = input.indexOf(close, cur + open.length)
627
+ // Unterminated: the regex requires the close literal, so it does not match.
628
+ return hit === -1 ? cur : hit + close.length
629
+ }
630
+ }
631
+
632
+ /** Split a `(?:…)` body on top-level `|`, respecting classes and nested groups. */
633
+ function splitDelimArms(body: string): string[] | null {
634
+ const arms: string[] = []
635
+ let depth = 0
636
+ let last = 0
637
+ let i = 0
638
+ while (i < body.length) {
639
+ const ch = body[i]
640
+ if (ch === '\\') { i += 2; continue }
641
+ if (ch === '[') {
642
+ let j = i + 1
643
+ if (body[j] === '^') j++
644
+ while (j < body.length && body[j] !== ']') j += body[j] === '\\' ? 2 : 1
645
+ if (body[j] !== ']') return null
646
+ i = j + 1
647
+ continue
648
+ }
649
+ if (ch === '(') { depth++; i++; continue }
650
+ if (ch === ')') { depth--; if (depth < 0) return null; i++; continue }
651
+ if (ch === '|' && depth === 0) { arms.push(body.slice(last, i)); last = i + 1 }
652
+ i++
653
+ }
654
+ if (depth !== 0) return null
655
+ arms.push(body.slice(last))
656
+ return arms
657
+ }
658
+
659
+ /**
660
+ * Prove "scan to the first `close`" EQUIVALENT to `<open>(?:body)*<close>`. That
661
+ * holds iff `(?:body)*` can never match text containing `close`: greedy repetition
662
+ * then stops exactly before the first `close`, which is where the raw scan lands.
663
+ *
664
+ * Proven only for the delimiter-safe idiom — the sole body shape that says "any
665
+ * char that does not form the close":
666
+ *
667
+ * close is one char `l0`: body must be exactly `[^l0]`
668
+ * close is `l0 l1`: body must be exactly `[^l0] | l0(?!l1)`
669
+ *
670
+ * The bulk arm takes every char but `l0`; the guard arm takes `l0` only when `l1`
671
+ * does not follow. So the body admits `l0`, never `l0 l1`, and rejects nothing
672
+ * else — a body NARROWER than that would stop early while the raw scan ran on to
673
+ * the next close, and the two would diverge. Anything outside the template is
674
+ * declined. Sound by construction: true is only ever returned for a body proven
675
+ * not to contain `close`.
676
+ */
677
+ function delimitedBodySound(body: string, close: number[]): boolean {
678
+ if (close.length > 2) return false
679
+ const l0 = close[0]!
680
+ const arms = splitDelimArms(body)
681
+ if (!arms) return false
682
+ let bulkSeen = false
683
+ let guardSeen = false
684
+ for (const arm of arms) {
685
+ const bulk = /^\[\^((?:\\.|[^\]])+)\]$/.exec(arm)
686
+ if (bulk) {
687
+ if (bulkSeen) return false
688
+ const ranges = parseClassRanges(bulk[1]!)
689
+ if (!ranges || ranges.length !== 1 || ranges[0]![0] !== l0 || ranges[0]![1] !== l0) return false
690
+ bulkSeen = true
691
+ continue
692
+ }
693
+ if (close.length === 2 && arm.endsWith(')')) {
694
+ const idx = arm.indexOf('(?!')
695
+ if (idx === -1 || guardSeen) return false
696
+ const prefix = literalCodePoints(arm.slice(0, idx))
697
+ const op = parseClassOperand(arm.slice(idx + 3, -1))
698
+ if (prefix?.length !== 1 || prefix[0] !== l0) return false
699
+ if (!op || op.negated || op.ranges.length !== 1
700
+ || op.ranges[0]![0] !== close[1] || op.ranges[0]![1] !== close[1]) return false
701
+ guardSeen = true
702
+ continue
703
+ }
704
+ return false
705
+ }
706
+ return bulkSeen && guardSeen === (close.length === 2)
707
+ }
708
+
709
+ /**
710
+ * One alternation arm of a `(?:…)*` trivia group: a positive char-class run
711
+ * `[class]` (a trailing `+`/`*` is redundant under the enclosing loop) or a line
712
+ * line-terminated arm `C[^\n\r]*` (`C` = one literal leader char, maybe escaped). Anything
713
+ * else is unclassifiable → no fast path, never a wrong one.
714
+ */
715
+ type TriviaArm =
716
+ | { kind: 'class'; ranges: Array<[number, number]> }
717
+ | { kind: 'untilLineBreak'; code: number }
718
+
719
+ function classifyTriviaArm(arm: string): TriviaArm | null {
720
+ const cls = /^\[([^\]^](?:[^\]]|\\.)*)\][*+]?$/.exec(arm)
721
+ if (cls) {
722
+ const ranges = parseClassRanges(cls[1]!)
723
+ return ranges ? { kind: 'class', ranges } : null
724
+ }
725
+ const lc = /^(\\?.)\[\^\\n\\r\]\*$/.exec(arm)
726
+ if (lc) {
727
+ const marker = lc[1]!
728
+ return { kind: 'untilLineBreak', code: (marker.length === 2 ? marker[1]! : marker[0]!).charCodeAt(0) }
729
+ }
730
+ return null
731
+ }
732
+
733
+ function armScanner(arm: TriviaArm): FastTriviaScanner {
734
+ if (arm.kind === 'untilLineBreak') return untilLineBreakScanner(arm.code)
735
+ const ranges = arm.ranges
736
+ return (input, cur) => {
737
+ let pos = cur
738
+ while (pos < input.length && inRanges(input.charCodeAt(pos), ranges)) pos++
739
+ return pos
740
+ }
741
+ }
742
+
743
+ /**
744
+ * A single tight loop over a merged char-class range list plus (usually one)
745
+ * line-terminated leader — the fast form of `(?:[class]|C[^\n\r]*)*`. Requires the
746
+ * caller to have checked marker/class disjointness (so a bare merged scan
747
+ * matches the regex regardless of arm order).
748
+ */
749
+ function fusedTriviaScanner(ranges: Array<[number, number]>, leaders: number[]): FastTriviaScanner {
750
+ const c0 = leaders[0]!
751
+ const single = leaders.length === 1
752
+ return (input, cur) => {
753
+ let pos = cur
754
+ const len = input.length
755
+ for (;;) {
756
+ const c = input.charCodeAt(pos)
757
+ if (pos < len && inRanges(c, ranges)) { pos++; continue }
758
+ if (single ? c === c0 : leaders.includes(c)) {
759
+ pos++
760
+ while (pos < len) {
761
+ const cc = input.charCodeAt(pos)
762
+ if (cc === 10 || cc === 13) break
763
+ pos++
764
+ }
765
+ continue
766
+ }
767
+ break
768
+ }
769
+ return pos
770
+ }
771
+ }
772
+
773
+ /** Split a `(?:…)*` body on top-level `|`, respecting `[…]`; bail on nested groups. */
774
+ function splitTopLevelAlts(body: string): string[] | null {
775
+ const arms: string[] = []
776
+ let inClass = false
777
+ let start = 0
778
+ for (let i = 0; i < body.length; i++) {
779
+ const c = body[i]
780
+ if (c === '\\') { i++; continue }
781
+ if (inClass) { if (c === ']') inClass = false; continue }
782
+ if (c === '[') inClass = true
783
+ else if (c === '(') return null // nested group — leave it to RegExp.exec
784
+ else if (c === '|') { arms.push(body.slice(start, i)); start = i + 1 }
785
+ }
786
+ arms.push(body.slice(start))
787
+ return arms
788
+ }
789
+
790
+ /**
791
+ * Trivia written as a single regex alternation-star, `(?:arm|arm|…)*` — e.g.
792
+ * `(?:[ \t\n\r,]|#[^\n\r]*)*`. Arms are classified independently and
793
+ * order-independently: char-class arms merge into one range list and line-terminated
794
+ * leaders are collected, so `(?:#…|[class])*` scans the same as `(?:[class]|#…)*`.
795
+ * The common (disjoint) case compiles to one fused loop; a leader that also sits
796
+ * inside a class is the one spot where arm order matters, so that falls back to
797
+ * the ordered `loopScanner`.
798
+ */
799
+ function altStarSource(source: string): FastTriviaScanner | null {
800
+ const m = /^\(\?:(.*)\)[*+]$/.exec(source)
801
+ if (!m) return null
802
+ const armSrcs = splitTopLevelAlts(m[1]!)
803
+ if (!armSrcs || armSrcs.length < 2) return null
804
+ const arms: TriviaArm[] = []
805
+ for (const src of armSrcs) {
806
+ const arm = classifyTriviaArm(src)
807
+ if (!arm) return null
808
+ arms.push(arm)
809
+ }
810
+ const ranges: Array<[number, number]> = []
811
+ const leaders: number[] = []
812
+ for (const arm of arms) {
813
+ if (arm.kind === 'class') ranges.push(...arm.ranges)
814
+ else leaders.push(arm.code)
815
+ }
816
+ if (leaders.some(code => inRanges(code, ranges))) {
817
+ return loopScanner(arms.map(armScanner)) // order-significant overlap
818
+ }
819
+ if (leaders.length === 0) return armScanner({ kind: 'class', ranges })
820
+ return fusedTriviaScanner(ranges, leaders)
821
+ }