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