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,636 @@
1
+ /**
2
+ * Grammar spec model — a small, notation-agnostic tree that both the EBNF
3
+ * emitter and the railroad-diagram emitter consume.
4
+ *
5
+ * The model is produced by walking the SAME `_def` structure the interpreter
6
+ * and macro compiler consume, so a generated spec cannot drift from what
7
+ * actually parses. See `docs/proposals/grammar-spec-generation.md`.
8
+ */
9
+ import type { Combinator, ParserDef } from '../types.ts'
10
+ import type { BalancedMarked } from '../combinators/scanTo.ts'
11
+ import { RULE_ORDER } from '../combinators/parser.ts'
12
+ import { recoverComposedRules } from '../compiler/linker.ts'
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Spec node tree
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /** A node in the grammar spec tree — one per syntactic construct. */
19
+ export type SpecNode =
20
+ | { kind: 'seq'; items: SpecNode[] }
21
+ | { kind: 'choice'; items: SpecNode[] }
22
+ /** `item*`, or `item{0,max}` when `many(…, { max })` bounded it. */
23
+ | { kind: 'star'; item: SpecNode; max?: number }
24
+ /** `item+`, or `item{min,max}` when `many(…, { min, max })` bounded it. `min`
25
+ * defaults to 1 and is always >= 1 (a min-0 repeat is a `star`). */
26
+ | { kind: 'plus'; item: SpecNode; min?: number; max?: number }
27
+ | { kind: 'opt'; item: SpecNode }
28
+ /**
29
+ * Separated repetition. `min`/`max` count ITEMS, not separators: `min: 0` is
30
+ * `(item (sep item)*)?` — NULLABLE — and any `min >= 1` requires that many
31
+ * items, so it is not. `trailing` mirrors `sepBy`'s option of the same name.
32
+ */
33
+ | { kind: 'sepBy'; item: SpecNode; sep: SpecNode; min: number; max?: number; trailing?: 'allow' }
34
+ /** Reference to a named production (non-terminal). */
35
+ | { kind: 'ref'; name: string }
36
+ /**
37
+ * A terminal. `literal` terminals are exact strings (rendered quoted);
38
+ * non-literal terminals are patterns/prose (rendered as-is, e.g. a regex
39
+ * or a caller-supplied display name).
40
+ */
41
+ | { kind: 'terminal'; text: string; literal: boolean }
42
+ /** Negative lookahead (`not`). Rendered as an annotation, not consumed input. */
43
+ | { kind: 'not'; item: SpecNode }
44
+ /** Positive lookahead (`ahead`). Zero-width, like `not`. */
45
+ | { kind: 'peek'; item: SpecNode }
46
+ /** An out-of-band annotation (guards, error-recovery, unknowns). */
47
+ | { kind: 'annotation'; text: string }
48
+ /** Matches nothing (elided trivia / semantic-only wrappers). */
49
+ | { kind: 'empty' }
50
+
51
+ /** One named production: `name ::= expr`. */
52
+ export type Production = {
53
+ name: string
54
+ expr: SpecNode
55
+ /** True when the rule is trivia (whitespace/comment). Elided by default. */
56
+ trivia: boolean
57
+ }
58
+
59
+ export type SpecModel = {
60
+ productions: Production[]
61
+ }
62
+
63
+ export type SpecOptions = {
64
+ /**
65
+ * How to order the emitted productions when neither `order` nor `root` is given:
66
+ * - `'source'` (default) — the order the rules were **declared** in the
67
+ * `rules()` factory. Predictable, includes every rule, and leads with the
68
+ * entry rule (authors write it first). Internal-but-referenced helper rules
69
+ * follow, in first-reference order.
70
+ * - `'reachable'` — BFS from the entry rule: the first-declared rule leads,
71
+ * then each rule appears the first time it is referenced; any rules not
72
+ * reachable from the entry are appended in declaration order.
73
+ * Ignored when `order` or `root` is set.
74
+ */
75
+ sort?: 'source' | 'reachable'
76
+ /**
77
+ * Explicit rule order (and subset). When given, only these rules are emitted,
78
+ * in this order, plus any rules they reach. Overrides `sort`.
79
+ */
80
+ order?: string[]
81
+ /**
82
+ * Start rule(s). When given, only these and the rules they reach are emitted
83
+ * (a pruned, reachability-ordered spec). Overrides `sort`.
84
+ */
85
+ root?: string | string[]
86
+ /** Include trivia rules (whitespace/comment) in the output. Default: false. */
87
+ includeTrivia?: boolean
88
+ /**
89
+ * Display names for terminals, keyed by rule name. When a rule whose body is a
90
+ * single terminal (regex/literal/keywords) has an entry here, that whole rule
91
+ * renders as the given terminal name instead of expanding — e.g.
92
+ * `{ Ident: 'identifier' }`.
93
+ */
94
+ terminals?: Record<string, string>
95
+ /**
96
+ * Best-effort rendering of a regex terminal to a readable form. Return
97
+ * `undefined` to fall back to the default `/source/` rendering.
98
+ */
99
+ regexDisplay?: (source: string, flags: string) => string | undefined
100
+ }
101
+
102
+ export type GrammarInput =
103
+ | Record<string, Combinator<unknown>>
104
+ | Combinator<unknown>
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Walk
108
+ // ---------------------------------------------------------------------------
109
+
110
+ type Tagged = Combinator<unknown> & { _ruleName?: string }
111
+
112
+ function ruleNameOf(c: Combinator<unknown>): string | undefined {
113
+ return (c as Tagged)._ruleName
114
+ }
115
+
116
+ /** Resolve a `lazy`/`ref` combinator one hop to its defined body, if resolvable. */
117
+ function resolveLazy(c: Combinator<unknown>): Combinator<unknown> | undefined {
118
+ const def = c._def
119
+ if (def.tag !== 'lazy') return undefined
120
+ try {
121
+ return def.thunk() as Combinator<unknown>
122
+ } catch {
123
+ return undefined
124
+ }
125
+ }
126
+
127
+ function defaultRegexDisplay(source: string): string {
128
+ return `/${source}/`
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Authored terminals behind a regex
133
+ // ---------------------------------------------------------------------------
134
+ //
135
+ // A spec reader's vocabulary is the LANGUAGE's: MDN and the CSS specs draw
136
+ // `@import`, not `/@import(?![-_a-zA-Z0-9\u0080-\uFFFF])/`. The trailing
137
+ // lookahead is a word-boundary assertion — an implementation detail of how a
138
+ // keyword is recognised, not part of the language — and printing it drowns the
139
+ // grammar in emitter noise. It also broke the diagrams as a complexity metric:
140
+ // a rule made of keywords scored worse than a genuinely tangled one.
141
+ //
142
+ // So when a `regex()` terminal IS a fixed string (optionally guarded by a word
143
+ // boundary, optionally a set of fixed strings), render the string(s). This is
144
+ // not simplification — nothing is collapsed, hidden, or elided; the terminal is
145
+ // shown as the text it matches, which is strictly MORE information than its
146
+ // compiled pattern. Anything with real regex structure still prints raw.
147
+
148
+ /**
149
+ * Start index of the top-level group that closes at the very end of `src`, or
150
+ * `-1`. Character classes and escapes are skipped so `[)]` and `\)` don't lie.
151
+ */
152
+ function trailingGroupStart(src: string): number {
153
+ let depth = 0
154
+ let open = -1
155
+ let lastStart = -1
156
+ let lastEnd = -1
157
+ for (let i = 0; i < src.length; i++) {
158
+ const ch = src[i]!
159
+ if (ch === '\\') { i++; continue }
160
+ if (ch === '[') {
161
+ // Skip the class body.
162
+ for (i++; i < src.length; i++) {
163
+ if (src[i] === '\\') i++
164
+ else if (src[i] === ']') break
165
+ }
166
+ continue
167
+ }
168
+ if (ch === '(') { if (depth === 0) open = i; depth++ }
169
+ else if (ch === ')') { depth--; if (depth === 0) { lastStart = open; lastEnd = i } }
170
+ }
171
+ return lastEnd === src.length - 1 ? lastStart : -1
172
+ }
173
+
174
+ /** `(?![…])` — a negative lookahead over a single character class. */
175
+ const BOUNDARY_GUARD = /^\(\?!\[(?:[^\\\]]|\\[\s\S])*\]\)$/
176
+
177
+ /** Split on top-level `|`, skipping escapes, classes and groups. */
178
+ function topLevelAlternatives(src: string): string[] {
179
+ const parts: string[] = []
180
+ let depth = 0
181
+ let start = 0
182
+ for (let i = 0; i < src.length; i++) {
183
+ const ch = src[i]!
184
+ if (ch === '\\') { i++; continue }
185
+ if (ch === '[') {
186
+ for (i++; i < src.length; i++) {
187
+ if (src[i] === '\\') i++
188
+ else if (src[i] === ']') break
189
+ }
190
+ continue
191
+ }
192
+ if (ch === '(') depth++
193
+ else if (ch === ')') depth--
194
+ else if (ch === '|' && depth === 0) { parts.push(src.slice(start, i)); start = i + 1 }
195
+ }
196
+ parts.push(src.slice(start))
197
+ return parts
198
+ }
199
+
200
+ /** Regex metacharacters an escape may legitimately spell as a literal. */
201
+ const ESCAPABLE = '\\^$.|?*+()[]{}/-'
202
+
203
+ /**
204
+ * Decode one alternative to the exact string it matches, or `undefined` when it
205
+ * is not a fixed string. Deliberately strict: any quantifier, class, group,
206
+ * anchor, class shorthand or control escape disqualifies it, because rendering
207
+ * those as text would state something the grammar does not say.
208
+ */
209
+ function fixedString(alt: string): string | undefined {
210
+ if (alt.length === 0) return undefined
211
+ let out = ''
212
+ for (let i = 0; i < alt.length; i++) {
213
+ const ch = alt[i]!
214
+ if (ch === '\\') {
215
+ const next = alt[i + 1]
216
+ if (next === undefined) return undefined
217
+ if (next === 'u' || next === 'x') {
218
+ const hex = next === 'u'
219
+ ? (alt[i + 2] === '{' ? /^\\u\{([0-9a-fA-F]+)\}/.exec(alt.slice(i)) : /^\\u([0-9a-fA-F]{4})/.exec(alt.slice(i)))
220
+ : /^\\x([0-9a-fA-F]{2})/.exec(alt.slice(i))
221
+ if (hex === null) return undefined
222
+ const cp = parseInt(hex[1]!, 16)
223
+ // Control and non-printable code points have no honest text form.
224
+ if (cp < 0x20 || cp === 0x7f) return undefined
225
+ out += String.fromCodePoint(cp)
226
+ i += hex[0]!.length - 1
227
+ continue
228
+ }
229
+ if (!ESCAPABLE.includes(next)) return undefined
230
+ out += next
231
+ i++
232
+ continue
233
+ }
234
+ // Unescaped metacharacter → real structure, not a fixed string.
235
+ if ('^$.|?*+()[]{}'.includes(ch)) return undefined
236
+ // Literal control characters likewise have no honest text form.
237
+ if (ch.codePointAt(0)! < 0x20) return undefined
238
+ out += ch
239
+ }
240
+ return out.length > 0 ? out : undefined
241
+ }
242
+
243
+ /**
244
+ * The fixed string(s) a regex matches — the terminal its author actually wrote —
245
+ * or `undefined` when the pattern has real structure. Handles the three shapes
246
+ * that spell a keyword: a bare literal, a literal guarded by a trailing word
247
+ * boundary (`word()` / `keywords()` and every hand-written copy of them), and a
248
+ * `\b…\b`-anchored word.
249
+ */
250
+ export function fixedStringsOfRegex(source: string): string[] | undefined {
251
+ let src = source
252
+ // `\b` anchors assert a boundary, exactly like the `(?![…])` guard.
253
+ if (src.startsWith('\\b')) src = src.slice(2)
254
+ if (src.endsWith('\\b') && !src.endsWith('\\\\b')) src = src.slice(0, -2)
255
+
256
+ const g = trailingGroupStart(src)
257
+ if (g >= 0 && BOUNDARY_GUARD.test(src.slice(g))) src = src.slice(0, g)
258
+
259
+ // `keywords()` compiles to `(?:a|b|c)` plus the guard; unwrap that hull.
260
+ const outer = trailingGroupStart(src)
261
+ if (outer === 0 && src.startsWith('(?:')) src = src.slice(3, -1)
262
+
263
+ const alts = topLevelAlternatives(src)
264
+ const out: string[] = []
265
+ for (const alt of alts) {
266
+ const fixed = fixedString(alt)
267
+ if (fixed === undefined) return undefined
268
+ out.push(fixed)
269
+ }
270
+ return out.length > 0 ? out : undefined
271
+ }
272
+
273
+ class Builder {
274
+ private seen = new Set<string>()
275
+ /** Combinators on the CURRENT walk path — the anonymous-cycle guard. */
276
+ private walking = new Set<Combinator<unknown>>()
277
+ private pending: Array<{ name: string; comb: Combinator<unknown> }> = []
278
+ private pendingNames = new Set<string>()
279
+ private record: Record<string, Combinator<unknown>>
280
+ private opts: SpecOptions
281
+ productions: Production[] = []
282
+
283
+ constructor(record: Record<string, Combinator<unknown>>, opts: SpecOptions) {
284
+ this.record = record
285
+ this.opts = opts
286
+ }
287
+
288
+ enqueue(name: string, comb: Combinator<unknown>): void {
289
+ if (this.seen.has(name)) return
290
+ if (this.pendingNames.has(name)) return
291
+ this.pendingNames.add(name)
292
+ this.pending.push({ name, comb })
293
+ }
294
+
295
+ /**
296
+ * Seed and drain in phases. Each phase seeds its names, then BFS-drains the
297
+ * queue (following references) before the next phase seeds. This lets callers
298
+ * express "entry first, then its reachable closure, then any leftovers" by
299
+ * splitting the seed across phases; a single phase with every rule yields plain
300
+ * declaration order (references, already queued, keep their declared slot).
301
+ */
302
+ run(phases: string[][]): SpecModel {
303
+ for (const phase of phases) {
304
+ for (const name of phase) {
305
+ const comb = this.record[name]
306
+ if (comb) this.enqueue(name, comb)
307
+ }
308
+ this.drain()
309
+ }
310
+ return { productions: this.productions }
311
+ }
312
+
313
+ private drain(): void {
314
+ while (this.pending.length > 0) {
315
+ const { name, comb } = this.pending.shift()!
316
+ this.pendingNames.delete(name)
317
+ if (this.seen.has(name)) continue
318
+ this.seen.add(name)
319
+ const expr = this.ruleBody(comb, name)
320
+ this.productions.push({ name, expr, trivia: comb._meta.isTrivia === true })
321
+ }
322
+ }
323
+
324
+ /** Walk a rule's top-level combinator, transparently unwrapping its own self-ref. */
325
+ private ruleBody(comb: Combinator<unknown>, name: string): SpecNode {
326
+ // A rule that IS a single terminal can be pinned to a caller display name.
327
+ const pinned = this.opts.terminals?.[name]
328
+ if (pinned !== undefined) return { kind: 'terminal', text: pinned, literal: false }
329
+
330
+ let c = comb
331
+ // Unwrap the rule's own placeholder ref(s) so we expand its body, not a
332
+ // self-reference. Both the placeholder and the resolved body carry the same
333
+ // `_ruleName`, so guard against cycles.
334
+ const guard = new Set<Combinator<unknown>>()
335
+ while (c._def.tag === 'lazy' && ruleNameOf(c) === name && !guard.has(c)) {
336
+ guard.add(c)
337
+ const next = resolveLazy(c)
338
+ if (!next) break
339
+ c = next
340
+ }
341
+ return this.walk(c, true)
342
+ }
343
+
344
+ /** Walk any combinator. `isRuleRoot` suppresses treating it as a self-reference. */
345
+ private walk(c: Combinator<unknown>, isRuleRoot = false): SpecNode {
346
+ // A reference to a named rule → non-terminal (unless this IS that rule's root).
347
+ const rn = ruleNameOf(c)
348
+ if (rn !== undefined && !isRuleRoot) {
349
+ // Register the target so it gets its own production, even if it wasn't in
350
+ // the returned record (internal-but-referenced helper rules).
351
+ if (!this.record[rn]) this.record[rn] = c
352
+ this.enqueue(rn, c)
353
+ return { kind: 'ref', name: rn }
354
+ }
355
+
356
+ // A balanced() is a delimiter scan, so render the delimiters and mark the
357
+ // interior opaque — see `BalancedMarked`. Checked before the cycle guard
358
+ // because it is the honest rendering, not merely a way to terminate.
359
+ const bal = (c as BalancedMarked)._balanced
360
+ if (bal !== undefined) {
361
+ return {
362
+ kind: 'seq',
363
+ items: [
364
+ { kind: 'terminal', text: bal.open, literal: true },
365
+ { kind: 'annotation', text: 'balanced …' },
366
+ { kind: 'terminal', text: bal.close, literal: true },
367
+ ],
368
+ }
369
+ }
370
+
371
+ // CYCLE CUT ON OBJECT IDENTITY.
372
+ //
373
+ // The walk used to cut only at `_ruleName`, which assumes every cycle passes
374
+ // through a named rule. A combinator whose interior refers back to ITSELF
375
+ // through an anonymous `ref()` — what `balanced()` builds, and what any caller
376
+ // of the public `ref()` can build — closes a cycle carrying no name, and the
377
+ // walk went around it until the process died (RangeError at the default stack,
378
+ // SIGSEGV at a raised one: a true cycle, not deep-but-finite recursion).
379
+ //
380
+ // `walking` is a PATH set, added on entry and removed on exit — not a global
381
+ // visited set. A combinator shared by two sibling positions is real structure
382
+ // that must be drawn twice; only a combinator that contains ITSELF is a cycle.
383
+ if (this.walking.has(c)) return { kind: 'annotation', text: '(recursive)' }
384
+ this.walking.add(c)
385
+ try {
386
+ return this.walkDef(c._def, c)
387
+ } finally {
388
+ this.walking.delete(c)
389
+ }
390
+ }
391
+
392
+ private walkDef(def: ParserDef, self: Combinator<unknown>): SpecNode {
393
+ switch (def.tag) {
394
+ case 'literal':
395
+ return { kind: 'terminal', text: def.value, literal: true }
396
+
397
+ case 'regex': {
398
+ // A caller-supplied display name always wins — it is the most specific
399
+ // statement of intent available.
400
+ const shown = this.opts.regexDisplay?.(def.source, def.flags)
401
+ if (shown !== undefined) return { kind: 'terminal', text: shown, literal: false }
402
+ const fixed = fixedStringsOfRegex(def.source)
403
+ if (fixed !== undefined) return literalTerminals(fixed)
404
+ return { kind: 'terminal', text: defaultRegexDisplay(def.source), literal: false }
405
+ }
406
+
407
+ case 'keywords':
408
+ return literalTerminals(def.words)
409
+
410
+ case 'sequence': {
411
+ const items = def.parsers.map(p => this.walk(p)).filter(nonEmpty)
412
+ return flattenSeq(items)
413
+ }
414
+
415
+ case 'choice': {
416
+ const items = def.parsers.map(p => this.walk(p)).filter(nonEmpty)
417
+ return flattenChoice(items)
418
+ }
419
+
420
+ case 'dispatch': {
421
+ const selector = this.walk(def.selector)
422
+ const tails = [
423
+ ...def.cases.map(entry => this.walk(entry.parser)),
424
+ ...(def.matchers ? def.matchers.map(entry => this.walk(entry.parser)) : []),
425
+ ...(def.otherwise ? [this.walk(def.otherwise)] : []),
426
+ ]
427
+ return flattenSeq([selector, flattenChoice(tails)])
428
+ }
429
+
430
+ // `max`/`min` are carried through so a BOUNDED repeat does not render as an
431
+ // unbounded one — the spec is generated to be read as the truth about what
432
+ // parses, and `many(x, { min: 3, max: 8 })` is not `x+`.
433
+ case 'many':
434
+ return { kind: 'star', item: this.walk(def.parser), ...(def.max === undefined ? {} : { max: def.max }) }
435
+ case 'oneOrMore':
436
+ return {
437
+ kind: 'plus', item: this.walk(def.parser),
438
+ ...(def.min === 1 ? {} : { min: def.min }),
439
+ ...(def.max === undefined ? {} : { max: def.max }),
440
+ }
441
+ case 'optional':
442
+ return { kind: 'opt', item: this.walk(def.parser) }
443
+ case 'attempt':
444
+ return this.walk(def.parser)
445
+ case 'sepBy':
446
+ return {
447
+ kind: 'sepBy', item: this.walk(def.parser), sep: this.walk(def.separator), min: def.min,
448
+ ...(def.max === undefined ? {} : { max: def.max }),
449
+ ...(def.trailing === undefined ? {} : { trailing: def.trailing }),
450
+ }
451
+
452
+ case 'not':
453
+ return { kind: 'not', item: this.walk(def.parser) }
454
+ case 'peek':
455
+ return { kind: 'peek', item: this.walk(def.parser) }
456
+ case 'routed':
457
+ // The routed token is spelled by the dispatch selector, so it contributes
458
+ // nothing here; a fallback IS spelled in place, so render it.
459
+ return def.fallback === undefined ? { kind: 'empty' } : this.walk(def.fallback)
460
+
461
+ // Transparent semantic wrappers — render the inner syntax.
462
+ case 'transform':
463
+ case 'token':
464
+ case 'leaf':
465
+ case 'label':
466
+ case 'field':
467
+ case 'expect':
468
+ case 'withCtx':
469
+ case 'node':
470
+ return this.walk((def as { parser: Combinator<unknown> }).parser)
471
+
472
+ case 'grammar':
473
+ return this.walk(def.parser)
474
+
475
+ // Error recovery — render the intended sub-parser, drop the sentinel.
476
+ case 'recover':
477
+ return this.walk(def.parser)
478
+ case 'scanTo':
479
+ return { kind: 'annotation', text: '…' }
480
+
481
+ // Trivia — elided by default; included as an annotation when asked.
482
+ case 'trivia':
483
+ return this.opts.includeTrivia ? this.walk(def.parser) : { kind: 'empty' }
484
+
485
+ case 'guard':
486
+ return { kind: 'empty' }
487
+
488
+ // Zero-width, but — unlike a state guard — it is a statement ABOUT THE SOURCE
489
+ // TEXT, so a grammar spec that elided it would document a different language.
490
+ case 'adjacency':
491
+ return { kind: 'annotation', text: def.polarity === 'adjacent' ? '(adjacent)' : def.kinds ? `(separated: ${def.kinds.join('|')})` : '(separated)' }
492
+
493
+ case 'lazy': {
494
+ const inner = resolveLazy(self)
495
+ return inner ? this.walk(inner) : { kind: 'annotation', text: '?' }
496
+ }
497
+
498
+ case 'unknown':
499
+ default:
500
+ return { kind: 'annotation', text: '?' }
501
+ }
502
+ }
503
+ }
504
+
505
+ /**
506
+ * One or more fixed strings as terminal node(s). A ONE-word set is a terminal,
507
+ * not a one-arm alternation: `word('@import')` used to render `("@import")`,
508
+ * and drew a branch box in the diagram, purely because a keyword set is
509
+ * internally a set. That paren and that box are emitter artifacts — there is no
510
+ * alternative to choose between.
511
+ */
512
+ function literalTerminals(words: readonly string[]): SpecNode {
513
+ const items = words.map(w => ({ kind: 'terminal', text: w, literal: true }) as SpecNode)
514
+ return flattenChoice(items)
515
+ }
516
+
517
+ function nonEmpty(n: SpecNode): boolean {
518
+ return n.kind !== 'empty'
519
+ }
520
+
521
+ function flattenSeq(items: SpecNode[]): SpecNode {
522
+ const flat: SpecNode[] = []
523
+ for (const it of items) {
524
+ if (it.kind === 'seq') flat.push(...it.items)
525
+ else flat.push(it)
526
+ }
527
+ if (flat.length === 0) return { kind: 'empty' }
528
+ if (flat.length === 1) return flat[0]!
529
+ return { kind: 'seq', items: flat }
530
+ }
531
+
532
+ function flattenChoice(items: SpecNode[]): SpecNode {
533
+ const flat: SpecNode[] = []
534
+ for (const it of items) {
535
+ if (it.kind === 'choice') flat.push(...it.items)
536
+ else flat.push(it)
537
+ }
538
+ if (flat.length === 0) return { kind: 'empty' }
539
+ if (flat.length === 1) return flat[0]!
540
+ return { kind: 'choice', items: flat }
541
+ }
542
+
543
+ function toRecord(grammar: GrammarInput): Record<string, Combinator<unknown>> {
544
+ // A single combinator → a one-rule grammar keyed by its rule name or "start".
545
+ if (isCombinator(grammar)) {
546
+ const name = ruleNameOf(grammar) ?? 'start'
547
+ return { [name]: grammar }
548
+ }
549
+ // A `compose()` result is a map of FUSED rule FUNCTIONS, not combinators — walking
550
+ // it read `_def` off a function and threw a bare `TypeError: Cannot read properties
551
+ // of undefined (reading 'tag')`. The combinator graph is recoverable from the
552
+ // carried IR, so recover it rather than fail. (Same recovery the gating analysis
553
+ // uses — see `recoverComposedRules`.)
554
+ const recovered = recoverComposedRules(grammar as Record<string, unknown>)
555
+ if (recovered !== undefined) {
556
+ if (recovered.opaque.length > 0) {
557
+ // Fail LOUDLY and specifically. A partial spec is worse than none: it renders a
558
+ // grammar that looks complete while silently omitting whole artifacts.
559
+ const named = recovered.opaque.map(o => `"${o.ns}"${o.ruleNames.length > 0 ? ` (${o.ruleNames.length} rule(s))` : ''}`).join(', ')
560
+ throw new TypeError(
561
+ `parseman spec: this composed grammar includes opaque precompiled artifact(s) ${named} that carry compiled rule `
562
+ + 'functions rather than re-lowerable IR. Their rules cannot be rendered, and emitting a spec without them would '
563
+ + 'silently understate the grammar. Recompile the contributing grammar(s) so they carry IR.',
564
+ )
565
+ }
566
+ return Object.fromEntries(recovered.rules)
567
+ }
568
+ return grammar
569
+ }
570
+
571
+ function isCombinator(x: unknown): x is Combinator<unknown> {
572
+ return (
573
+ typeof x === 'object' &&
574
+ x !== null &&
575
+ typeof (x as { parse?: unknown }).parse === 'function' &&
576
+ '_def' in (x as object)
577
+ )
578
+ }
579
+
580
+ /** Declaration order recorded by `rules()`; falls back to the record's key order. */
581
+ function declarationOrder(record: Record<string, Combinator<unknown>>): string[] {
582
+ const declared = (record as Record<string, unknown>)[RULE_ORDER]
583
+ if (Array.isArray(declared)) return declared.filter(k => k in record)
584
+ return Object.keys(record)
585
+ }
586
+
587
+ /**
588
+ * Build the notation-agnostic spec model from a `rules()` grammar (or a single
589
+ * combinator). Following every referenced named rule keeps the closure complete;
590
+ * ordering is controlled by `order` / `root` / `sort` (see {@link SpecOptions}).
591
+ */
592
+ export function buildSpecModel(grammar: GrammarInput, options: SpecOptions = {}): SpecModel {
593
+ const record = toRecord(grammar)
594
+ const decl = declarationOrder(record)
595
+
596
+ // Validate rule names in `order` / `root` up front. Left unchecked, an unknown
597
+ // name (or a stray string like `order: 'source'` where a `string[]` is meant)
598
+ // seeds a phase that reaches nothing and silently yields an EMPTY model — a
599
+ // confusing footgun. Fail loudly with the offending names and the known rules.
600
+ const known = new Set(decl)
601
+ const asList = (v: string | string[]): string[] => (Array.isArray(v) ? v : [v])
602
+ const requireKnown = (names: string[], opt: string): void => {
603
+ const bad = names.filter(n => !known.has(n))
604
+ if (bad.length > 0) {
605
+ throw new Error(
606
+ `buildSpecModel: unknown rule name(s) in \`${opt}\`: ${bad.map(n => JSON.stringify(n)).join(', ')}. ` +
607
+ `Known rules: ${decl.map(n => JSON.stringify(n)).join(', ')}.`,
608
+ )
609
+ }
610
+ }
611
+
612
+ // Seed phases (see Builder.run). Priority: explicit `order` > `root` (pruned) >
613
+ // `sort`. Default `sort` = 'source' (declaration order, every rule).
614
+ let phases: string[][]
615
+ if (options.order !== undefined) {
616
+ const order = asList(options.order as string | string[])
617
+ requireKnown(order, 'order')
618
+ phases = [order]
619
+ } else if (options.root !== undefined) {
620
+ const root = asList(options.root)
621
+ requireKnown(root, 'root')
622
+ phases = [root]
623
+ } else if (options.sort === 'reachable') {
624
+ // Entry (first-declared) leads; BFS discovers the rest in first-reference
625
+ // order; unreachable rules trail in declaration order.
626
+ phases = decl.length > 0 ? [[decl[0]!], decl.slice(1)] : [decl]
627
+ } else {
628
+ phases = [decl]
629
+ }
630
+
631
+ const model = new Builder({ ...record }, options).run(phases)
632
+ if (!options.includeTrivia) {
633
+ model.productions = model.productions.filter(p => !p.trivia)
634
+ }
635
+ return model
636
+ }