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,318 @@
1
+ /**
2
+ * Derived tokenization, part 2: emit the SCANNER for an alphabet.
3
+ *
4
+ * Shape, and why each piece is what it is (all measured on the css grammar):
5
+ *
6
+ * - Literal and keyword terminals become a DATA TRIE walked by a fixed-size
7
+ * driver. The trie walk is also the macro-time SEARCH for a distinguishing
8
+ * position set: it walks until the node is uniquely accepting, so it does not
9
+ * depend on a fixed discriminator. A fixed one does not work — `(len,c1,c2)`
10
+ * collides on `@counter-style`/`@color-profile` and
11
+ * `@font-feature-values`/`@font-palette-values`, and widening to the last
12
+ * char still leaves 11 buckets for 13 keys.
13
+ * Emitting the trie as CODE (nested switches) was measured at 25,223 B for 13
14
+ * css at-keywords; as DATA it is 2,057 B for the same set and 1.4x the speed
15
+ * of today's slice-plus-char-chain rather than 2.5x. Data also means the
16
+ * driver's size is independent of how many keys the grammar has.
17
+ *
18
+ * - Regex terminals stay sticky regexes, consulted ONLY when the position's
19
+ * first char is in their first set. Ungated, a 42-regex core alphabet scans
20
+ * benchmark.css at 14.5 MB/s; first-char gated, 30.4 MB/s.
21
+ *
22
+ * - The candidate set consulted is LOCAL to the decision point (a packed table
23
+ * of id lists). The id space is global. Global maximal munch over the whole
24
+ * alphabet produces seven tokens for a 123 KB stylesheet.
25
+ *
26
+ * - `tight` is set AT SCAN TIME: 1 when no trivia preceded the token. This is
27
+ * what carries `noTrivia` adjacency (262 sites across the four dialects, 50 in
28
+ * css) rather than the hand-spelled `boundary` classes, which are not even
29
+ * self-consistent today: the 26 css `keywords(boundary:)` sites use THREE
30
+ * different spellings and 16 of them omit `€-￿` and/or `\\`.
31
+ *
32
+ * - Whitespace is a token in `MODE_SELECTOR` (the descendant combinator is
33
+ * significant there) and skipped otherwise. Mode is part of the memo key.
34
+ */
35
+ import type { Alphabet } from './token-alphabet.ts'
36
+ import { TOK_EOF, TOK_UNKNOWN, TOK_WS } from './token-alphabet.ts'
37
+ import { firstSetFromRegex } from '../regex/first-set.ts'
38
+ import { packInts, sharedHelperDecl } from './token-dispatch.ts'
39
+ import { foldCode, foldExpr } from './token-dispatch.ts'
40
+
41
+ /**
42
+ * First CHARS a regex terminal can start with, or null when it is unbounded.
43
+ * Reuses the analyzer the interpreter's first-set gating already trusts — the
44
+ * gate must never be narrower than the real first set or a valid token is
45
+ * skipped, and `firstSetFromRegex` is the same over-approximation used there.
46
+ */
47
+ function firstSetOfRegexSource(source: string, flags: string): Set<number> | null {
48
+ let fs
49
+ try { fs = firstSetFromRegex(source).firstSet } catch { return null }
50
+ if (fs.kind === 'any') return null
51
+ const out = new Set<number>()
52
+ if (fs.kind === 'empty') return out
53
+ for (const r of fs.ranges) {
54
+ // An unbounded tail (non-ASCII ranges are wide) collapses to the non-ASCII flag.
55
+ if (r.hi - r.lo > 512) return null
56
+ for (let c = r.lo; c <= r.hi; c++) out.add(c)
57
+ }
58
+ // Under `/i` the analyzer's ranges may name only one case; admit both, since a
59
+ // gate narrower than the real first set would skip a valid token.
60
+ if (flags.includes('i')) {
61
+ for (const c of [...out]) {
62
+ if (c >= 65 && c <= 90) out.add(c + 32)
63
+ else if (c >= 97 && c <= 122) out.add(c - 32)
64
+ }
65
+ }
66
+ return out
67
+ }
68
+
69
+ type TrieNode = { edges: Map<number, number>; accept: number }
70
+
71
+ /**
72
+ * Build the literal/keyword trie. Case-insensitive terminals fold ASCII LETTERS
73
+ * which is exact for ASCII letters and identity for everything else, so one
74
+ * folded walk serves both. A case-SENSITIVE terminal whose text contains an
75
+ * ASCII letter cannot share the folded walk and is left to the regex path.
76
+ */
77
+ function buildTrie(alphabet: Alphabet): { nodes: TrieNode[]; covered: Set<number> } {
78
+ const nodes: TrieNode[] = [{ edges: new Map(), accept: 0 }]
79
+ const covered = new Set<number>()
80
+ const hasAsciiLetter = (s: string): boolean => /[a-zA-Z]/.test(s)
81
+
82
+ const insert = (text: string, id: number): boolean => {
83
+ if (text.length === 0) return false
84
+ let n = 0
85
+ for (let i = 0; i < text.length; i++) {
86
+ // LETTERS ONLY. `| 32` also folds non-letter ASCII pairs -- [ / {, ] / },
87
+ // ^ / ~, @ / ` -- so a case-insensitive scanner would collide on those,
88
+ // skip later terminals, and fire the EARLIER terminal's accept id for the
89
+ // later terminal's text. Same defect `e8612eb` fixed in dispatch; the one
90
+ // implementation is imported rather than written a third time.
91
+ const c = foldCode(text.charCodeAt(i))
92
+ let t = nodes[n]!.edges.get(c)
93
+ if (t === undefined) {
94
+ t = nodes.length
95
+ nodes.push({ edges: new Map(), accept: 0 })
96
+ nodes[n]!.edges.set(c, t)
97
+ }
98
+ n = t
99
+ }
100
+ // A collision means two terminals fold to the same text; keep the first.
101
+ if (nodes[n]!.accept !== 0) return false
102
+ nodes[n]!.accept = id
103
+ return true
104
+ }
105
+
106
+ for (const t of alphabet.terminals) {
107
+ if (t.kind === 'literal') {
108
+ if (!t.caseInsensitive && hasAsciiLetter(t.value)) continue
109
+ if (insert(t.value, t.id)) covered.add(t.id)
110
+ } else if (t.kind === 'keywords') {
111
+ // One id per SET: every word in the set accepts the same id, which is
112
+ // exactly what the dispatch needs (the set is the terminal).
113
+ let any = false
114
+ for (const w of t.words) {
115
+ if (!t.caseInsensitive && hasAsciiLetter(w)) continue
116
+ if (insert(w, t.id)) any = true
117
+ }
118
+ if (any) covered.add(t.id)
119
+ }
120
+ }
121
+ return { nodes, covered }
122
+ }
123
+
124
+ export type ScannerEmission = {
125
+ /** Module-scope declarations for the artifact prelude. */
126
+ decls: string[]
127
+ /** Name of the cursor entry point. */
128
+ nextFn: string
129
+ /** Register a candidate set, returning its table index. */
130
+ candidateIndexOf: (ids: readonly number[]) => number
131
+ /** True when the alphabet produced a usable scanner at all. */
132
+ usable: boolean
133
+ }
134
+
135
+ /**
136
+ * Emit the scanner for `alphabet`. `ns` namespaces every hoisted name exactly
137
+ * like `_re`/`_fx`/`_pf` so two fused pieces never collide.
138
+ */
139
+ export function emitScanner(alphabet: Alphabet, ns: string): ScannerEmission {
140
+ const { nodes, covered } = buildTrie(alphabet)
141
+
142
+ // Regex terminals, with their first-char gate. A terminal whose first set is
143
+ // empty can never start a token; one that is effectively unbounded is only
144
+ // consulted on the default arm.
145
+ const regexes: Array<{ id: number; src: string; flags: string; first: Set<number> | null }> = []
146
+ for (const t of alphabet.terminals) {
147
+ if (t.kind !== 'regex') continue
148
+ regexes.push({ id: t.id, src: t.source, flags: t.flags, first: firstSetOfRegexSource(t.source, t.flags) })
149
+ }
150
+
151
+ const usable = covered.size > 0 || regexes.length > 0
152
+ if (!usable) {
153
+ return { decls: [], nextFn: '', candidateIndexOf: () => -1, usable: false }
154
+ }
155
+
156
+ // Flatten the trie.
157
+ const START: number[] = [0]
158
+ const CHAR: number[] = []
159
+ const NEXT: number[] = []
160
+ const ACCEPT: number[] = []
161
+ for (const nd of nodes) {
162
+ for (const [c, t] of [...nd.edges.entries()].sort((a, b) => a[0] - b[0])) { CHAR.push(c); NEXT.push(t) }
163
+ START.push(CHAR.length)
164
+ ACCEPT.push(nd.accept)
165
+ }
166
+
167
+ // Candidate-set table, filled as decision points register.
168
+ const candOffsets: number[] = [0]
169
+ const candIds: number[] = []
170
+ const candKeyToIndex = new Map<string, number>()
171
+ const candidateIndexOf = (ids: readonly number[]): number => {
172
+ const sorted = [...new Set(ids)].sort((a, b) => a - b)
173
+ const key = sorted.join(',')
174
+ const hit = candKeyToIndex.get(key)
175
+ if (hit !== undefined) return hit
176
+ const idx = candOffsets.length - 1
177
+ for (const id of sorted) candIds.push(id)
178
+ candOffsets.push(candIds.length)
179
+ candKeyToIndex.set(key, idx)
180
+ return idx
181
+ }
182
+
183
+ const p = (n: string): string => `${ns}${n}`
184
+ const decls: string[] = []
185
+
186
+ decls.push(`const ${p('_tkChar')} = ${JSON.stringify(CHAR.map(c => String.fromCharCode(c)).join(''))}`)
187
+ // The DECODER for `packInts`, taken from the one shared helper rather than
188
+ // re-spelled here. `packInts` itself was deduplicated after a second copy shipped
189
+ // without the 12-bit bound check; the matching decoder was left copied, so the
190
+ // two halves of one encoding still lived in two files. They agree today — the
191
+ // point is that widening the encoding (which the `RangeError` tells you to do)
192
+ // must not be able to change one half and miss the other.
193
+ decls.push(sharedHelperDecl('unpack', () => p('_tkUnpack')))
194
+ decls.push(`const ${p('_tkNext_')} = ${p('_tkUnpack')}(${packInts(NEXT)})`)
195
+ decls.push(`const ${p('_tkStart')} = ${p('_tkUnpack')}(${packInts(START)})`)
196
+ decls.push(`const ${p('_tkAccept')} = ${p('_tkUnpack')}(${packInts(ACCEPT)})`)
197
+
198
+ for (const [i, r] of regexes.entries()) {
199
+ const flags = r.flags.includes('y') ? r.flags : `${r.flags.replace(/[g]/g, '')}y`
200
+ decls.push(`const ${p(`_tkRx${i}`)} = /${r.src}/${flags}`)
201
+ }
202
+ decls.push(`const ${p('_tkRx')} = [${regexes.map((_, i) => p(`_tkRx${i}`)).join(', ')}]`)
203
+ decls.push(`const ${p('_tkRxId')} = new Int32Array([${regexes.map(r => r.id).join(', ')}])`)
204
+
205
+ // Per-regex first-char gate as a bitset over 0..127 plus an "also non-ASCII" flag.
206
+ const gateWords: number[] = []
207
+ const gateNonAscii: number[] = []
208
+ for (const r of regexes) {
209
+ const w = new Array<number>(4).fill(0)
210
+ let nonAscii = 0
211
+ if (r.first === null) { w.fill(-1); nonAscii = 1 } else {
212
+ for (const c of r.first) {
213
+ if (c >= 128) { nonAscii = 1; continue }
214
+ w[c >> 5] = (w[c >> 5]! | (1 << (c & 31))) | 0
215
+ }
216
+ }
217
+ gateWords.push(...w)
218
+ gateNonAscii.push(nonAscii)
219
+ }
220
+ decls.push(`const ${p('_tkGate')} = new Int32Array([${gateWords.join(', ')}])`)
221
+ decls.push(`const ${p('_tkGateHi')} = new Uint8Array([${gateNonAscii.join(', ')}])`)
222
+
223
+ // Candidate table is emitted lazily by the caller after all sites registered;
224
+ // placeholders are patched in `finalizeScanner`.
225
+ decls.push(`__TK_CAND_OFFSETS__`)
226
+ decls.push(`__TK_CAND_IDS__`)
227
+
228
+ const nextFn = p('_tkScan')
229
+ decls.push([
230
+ `let ${p('_tkId')} = ${TOK_EOF}, ${p('_tkStartPos')} = 0, ${p('_tkEnd')} = 0, ${p('_tkTight')} = 0`,
231
+ // The memo is module state, so INPUT IDENTITY is part of the key. Without it a
232
+ // second parse of a different string hits the first string's cached token at the
233
+ // same (pos, mode, set) and returns it — a wrong token from a warm cache, with no
234
+ // error. The reference check is O(1) on the common path: the same parse passes the
235
+ // same string object every call.
236
+ `let ${p('_tkMemoInput')} = null, ${p('_tkMemoPos')} = -1, ${p('_tkMemoMode')} = -1, ${p('_tkMemoSet')} = -1`,
237
+ `function ${nextFn}(input, pos, mode, set) {`,
238
+ ` if (input === ${p('_tkMemoInput')} && pos === ${p('_tkMemoPos')} && mode === ${p('_tkMemoMode')} && set === ${p('_tkMemoSet')}) return ${p('_tkId')}`,
239
+ ` ${p('_tkMemoInput')} = input; ${p('_tkMemoPos')} = pos; ${p('_tkMemoMode')} = mode; ${p('_tkMemoSet')} = set`,
240
+ ` const n = input.length`,
241
+ ` let q = pos`,
242
+ // Comments skip in both modes; whitespace only outside selector mode.
243
+ ` for (;;) {`,
244
+ ` const before = q`,
245
+ ` if (mode !== 1) { while (q < n) { const c = input.charCodeAt(q); if (c === 32 || c === 9 || c === 10 || c === 13 || c === 12) q++; else break } }`,
246
+ ` if (q + 1 < n && input.charCodeAt(q) === 47 && input.charCodeAt(q + 1) === 42) { const e = input.indexOf('*/', q + 2); q = e < 0 ? n : e + 2 }`,
247
+ ` if (q === before) break`,
248
+ ` }`,
249
+ ` ${p('_tkTight')} = q === pos ? 1 : 0`,
250
+ ` ${p('_tkStartPos')} = q`,
251
+ ` if (mode === 1) {`,
252
+ ` let w = q`,
253
+ ` while (w < n) { const c = input.charCodeAt(w); if (c === 32 || c === 9 || c === 10 || c === 13 || c === 12) w++; else break }`,
254
+ ` if (w > q) { ${p('_tkEnd')} = w; ${p('_tkId')} = ${TOK_WS}; return ${TOK_WS} }`,
255
+ ` }`,
256
+ ` if (q >= n) { ${p('_tkEnd')} = q; ${p('_tkId')} = ${TOK_EOF}; return ${TOK_EOF} }`,
257
+ ` const lo = ${p('_tkCandOff')}[set], hi = ${p('_tkCandOff')}[set + 1]`,
258
+ ` let bestId = ${TOK_UNKNOWN}, bestEnd = q`,
259
+ // Trie walk: longest accepting prefix wins, which is maximal munch over the
260
+ // literal/keyword part of the candidate set.
261
+ ` {`,
262
+ ` let node = 0, i = q`,
263
+ ` while (i < n) {`,
264
+ // The emitted walker must fold exactly as the trie above did, or a lookup
265
+ // misses the node it built.
266
+ ` const c = ${foldExpr(`input.charCodeAt(i)`)}`,
267
+ ` let nx = -1`,
268
+ ` for (let e = ${p('_tkStart')}[node], z = ${p('_tkStart')}[node + 1]; e < z; e++) if (${p('_tkChar')}.charCodeAt(e) === c) { nx = ${p('_tkNext_')}[e]; break }`,
269
+ ` if (nx < 0) break`,
270
+ ` node = nx; i++`,
271
+ ` const acc = ${p('_tkAccept')}[node]`,
272
+ ` if (acc !== 0 && i > bestEnd) { for (let k = lo; k < hi; k++) if (${p('_tkCandIds')}[k] === acc) { bestId = acc; bestEnd = i; break } }`,
273
+ ` }`,
274
+ ` }`,
275
+ // Regex candidates, first-char gated.
276
+ ` {`,
277
+ ` const c0 = input.charCodeAt(q)`,
278
+ ` const wi = c0 < 128 ? (c0 >> 5) : -1, bit = c0 < 128 ? (1 << (c0 & 31)) : 0`,
279
+ ` for (let k = lo; k < hi; k++) {`,
280
+ ` const want = ${p('_tkCandIds')}[k]`,
281
+ ` for (let r = 0; r < ${p('_tkRxId')}.length; r++) {`,
282
+ ` if (${p('_tkRxId')}[r] !== want) continue`,
283
+ ` if (wi < 0 ? !${p('_tkGateHi')}[r] : !(${p('_tkGate')}[r * 4 + wi] & bit)) break`,
284
+ ` const rx = ${p('_tkRx')}[r]`,
285
+ ` rx.lastIndex = q`,
286
+ ` const m = rx.exec(input)`,
287
+ ` if (m !== null && m[0].length > 0 && q + m[0].length > bestEnd) { bestId = want; bestEnd = q + m[0].length }`,
288
+ ` break`,
289
+ ` }`,
290
+ ` }`,
291
+ ` }`,
292
+ ` ${p('_tkEnd')} = bestEnd > q ? bestEnd : q`,
293
+ ` ${p('_tkId')} = bestId`,
294
+ ` return bestId`,
295
+ `}`,
296
+ ].join('\n'))
297
+
298
+ return {
299
+ decls,
300
+ nextFn,
301
+ candidateIndexOf,
302
+ usable: true,
303
+ // Consumed by `finalizeScanner`.
304
+ ...({ _cand: { offsets: candOffsets, ids: candIds, ns } } as object),
305
+ } as ScannerEmission
306
+ }
307
+
308
+ /** Patch the candidate table placeholders once every decision point registered. */
309
+ export function finalizeScanner(emission: ScannerEmission): string[] {
310
+ const cand = (emission as unknown as { _cand?: { offsets: number[]; ids: number[]; ns: string } })._cand
311
+ if (cand === undefined) return emission.decls
312
+ const p = (n: string): string => `${cand.ns}${n}`
313
+ return emission.decls.map(d => {
314
+ if (d === '__TK_CAND_OFFSETS__') return `const ${p('_tkCandOff')} = new Int32Array([${cand.offsets.join(', ')}])`
315
+ if (d === '__TK_CAND_IDS__') return `const ${p('_tkCandIds')} = new Int32Array([${cand.ids.join(', ')}])`
316
+ return d
317
+ })
318
+ }
@@ -0,0 +1,147 @@
1
+ import type { Combinator, ParserDef } from '../types.ts'
2
+
3
+ /**
4
+ * Dead-value analysis (shared by the interpreter and codegen). A container
5
+ * combinator — `many` / `oneOrMore` / `optional` / `sequence` — builds an
6
+ * aggregate value (array / tuple / nullable). Under a `node()`, the node builds
7
+ * from the *captured children*, never from that aggregate, so the aggregate is
8
+ * built and thrown away. This pass marks those containers `valueUnused`; the
9
+ * interpreter and the emitter then skip building the aggregate entirely (the
10
+ * elements still parse and self-capture, so trees are identical).
11
+ *
12
+ * Walks ONE combinator tree (a single rule body, or a standalone root), stopping
13
+ * at `lazy`/ref boundaries — other rules are analyzed on their own root. Starts
14
+ * `consumed = true` (a rule's own value is assumed read — conservative); `node()`
15
+ * flips it to false for its inner parser; `transform` forces it true (its map fn
16
+ * reads the value). Sharing-safe: any `consumed = true` visit wins and is sticky,
17
+ * so a container reachable from BOTH a consuming and a non-consuming site keeps
18
+ * its aggregate (never wrongly elided).
19
+ *
20
+ * Soundness: a container is marked ONLY when every path to it passes through a
21
+ * `node()` (value-discarding) without an intervening value-reader — i.e. its
22
+ * aggregate provably isn't observed. Idempotent; safe to re-run.
23
+ */
24
+ export function markUnusedValues(root: Combinator<unknown>): void {
25
+ const seenConsumed = new WeakSet<Combinator<unknown>>()
26
+ const seenUnconsumed = new WeakSet<Combinator<unknown>>()
27
+
28
+ const mark = (def: { valueUnused?: boolean }, consumed: boolean): void => {
29
+ if (consumed) def.valueUnused = false
30
+ else if (def.valueUnused !== false) def.valueUnused = true
31
+ }
32
+
33
+ const visit = (c: Combinator<unknown>, consumed: boolean): void => {
34
+ const seen = consumed ? seenConsumed : seenUnconsumed
35
+ if (seen.has(c)) return
36
+ seen.add(c)
37
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
38
+ const def = c._def as ParserDef & { valueUnused?: boolean }
39
+
40
+ switch (def.tag) {
41
+ case 'many':
42
+ case 'oneOrMore':
43
+ mark(def, consumed)
44
+ visit(def.parser, consumed)
45
+ return
46
+ case 'optional':
47
+ // `optional` returns `inner | null` — no aggregate array/tuple to elide, so
48
+ // it carries no valueUnused flag. Still descend so an inner many/sequence
49
+ // under a node() is analyzed (it benefits even when its optional wrapper
50
+ // doesn't).
51
+ visit(def.parser, consumed)
52
+ return
53
+ case 'sequence':
54
+ mark(def, consumed)
55
+ for (const p of def.parsers) visit(p, consumed)
56
+ return
57
+ case 'choice':
58
+ // value is the winning arm's value — propagate consumed to each arm.
59
+ for (const p of def.parsers) visit(p, consumed)
60
+ return
61
+ case 'dispatch':
62
+ visit(def.selector, consumed)
63
+ for (const entry of def.cases) visit(entry.parser, consumed)
64
+ if (def.matchers) for (const entry of def.matchers) visit(entry.parser, consumed)
65
+ if (def.otherwise) visit(def.otherwise, consumed)
66
+ return
67
+ case 'node':
68
+ // builds from captured children, NOT the inner parser value.
69
+ visit(def.parser, false)
70
+ return
71
+ case 'transform':
72
+ // the map fn reads the value → always consumed.
73
+ visit(def.parser, true)
74
+ return
75
+ case 'field':
76
+ // the field capture records the parsed value → always consumed.
77
+ visit(def.parser, true)
78
+ return
79
+ case 'sepBy':
80
+ // sepBy is NOT elided (no valueUnused), so it always builds its array and
81
+ // therefore always reads each item's value — regardless of whether sepBy's
82
+ // OWN value is observed. Item parser is unconditionally consumed.
83
+ visit(def.parser, true)
84
+ visit(def.separator, false)
85
+ return
86
+ case 'not':
87
+ case 'peek':
88
+ visit(def.parser, false) // lookahead — value never observed
89
+ return
90
+ case 'grammar':
91
+ visit(def.parser, consumed)
92
+ if (def.triviaParser) visit(def.triviaParser, false)
93
+ return
94
+ case 'recover':
95
+ visit(def.parser, consumed)
96
+ visit(def.sentinel, false)
97
+ return
98
+ case 'token':
99
+ // token() returns the matched source slice, not the wrapped parser value.
100
+ // Its child still parses and captures one outer token leaf; inner aggregate
101
+ // values such as sequence tuples are unobserved and can be elided.
102
+ visit(def.parser, false)
103
+ return
104
+ case 'label':
105
+ case 'trivia':
106
+ case 'attempt':
107
+ case 'withCtx':
108
+ case 'expect':
109
+ visit(def.parser, consumed) // single-child pass-through wrappers
110
+ return
111
+ case 'leaf':
112
+ // The reducer observes the complete inner value even when its own result
113
+ // is immediately captured by a node().
114
+ visit(def.parser, true)
115
+ return
116
+ case 'lazy':
117
+ // An INNER ref boundary — a distinct rule, analyzed on its own root. Don't
118
+ // descend (and never call thunk(): an external ref would throw). A ref at
119
+ // the ROOT of a rule is different — it wraps this rule's own body — and is
120
+ // resolved by resolveRoot() before the walk starts.
121
+ return
122
+ default:
123
+ // literal / regex / keywords / guard / scanTo / unknown — no container child.
124
+ return
125
+ }
126
+ }
127
+
128
+ // A rule's root is often a `ref()`/`lazy` wrapping the real body — that is how
129
+ // rules() and compose() store forward-referenced and composed rules. Walking
130
+ // that lazy directly hits the boundary case and marks nothing, so resolve a
131
+ // ROOT lazy to its defined body first. Inner lazies (references to OTHER rules)
132
+ // stay boundaries — each rule is analyzed from its own resolved root.
133
+ const resolveRoot = (c: Combinator<unknown>): Combinator<unknown> => {
134
+ const guard = new Set<Combinator<unknown>>()
135
+ let cur = c
136
+ while (!guard.has(cur)) {
137
+ const d = cur._def as ParserDef
138
+ if (d.tag === 'grammar') { guard.add(cur); cur = d.parser; continue }
139
+ if (d.tag !== 'lazy') return cur
140
+ guard.add(cur)
141
+ try { cur = d.thunk() } catch { return c } // undefined ref → leave as-is
142
+ }
143
+ return cur
144
+ }
145
+
146
+ visit(resolveRoot(root), true)
147
+ }