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,72 @@
1
+ /**
2
+ * Gating analysis for a WHOLE grammar value — including a `compose()` result.
3
+ *
4
+ * `analyzeGatingRules` walks combinators. A `compose()` result holds none: fusion
5
+ * lowers every rule to an executable function and the combinator graph is gone from
6
+ * the returned map. Walking one therefore saw nothing — and, because the default-on
7
+ * fuse-time diagnostic swallowed the resulting error, saw nothing SILENTLY. That is
8
+ * the shape every real composed grammar has (`compose([importedArtifact, rules(…)])`),
9
+ * so the diagnostic was blind on exactly the grammars it exists to serve.
10
+ *
11
+ * The graph is recoverable: `compose()` retains its inputs as carried IR, and that IR
12
+ * re-lowers to real combinators. This module does that recovery, and — where a piece
13
+ * is a genuinely opaque precompiled artifact carrying rule FUNCTIONS rather than IR —
14
+ * reports it as `unanalysable` instead of quietly dropping it.
15
+ */
16
+ import type { Combinator } from '../types.ts'
17
+ import { analyzeGatingRules, type AnalyzeGatingOptions, type GatingReport, type Unanalysable } from './gating.ts'
18
+ import { recoverComposedRules } from '../compiler/linker.ts'
19
+ import { recordDegradation } from '../compiler/degradation.ts'
20
+
21
+ /** A `rules()` map, a `compose()` result, or any rule-name → value record. */
22
+ export type AnalysableGrammar = Record<string, unknown>
23
+
24
+ /**
25
+ * Analyze a grammar's choices, resolving a composed grammar back to its carried IR
26
+ * first. Accepts a `rules()` map (walked directly) or a `compose()` result (recovered).
27
+ *
28
+ * The returned report's `unanalysable` is authoritative: non-empty means part of the
29
+ * grammar was never examined, and a clean `ungated` must NOT be read as a pass.
30
+ */
31
+ export function analyzeGrammarGating(
32
+ grammar: AnalysableGrammar,
33
+ opts?: AnalyzeGatingOptions,
34
+ ): GatingReport {
35
+ const recovered = recoverComposedRules(grammar)
36
+ if (recovered === undefined) {
37
+ // Not a composed grammar — a plain `rules()` map of combinators. Walk it as-is;
38
+ // any non-combinator value is reported by the walk itself.
39
+ return analyzeGatingRules(Object.entries(grammar) as Array<[string, Combinator<unknown>]>, opts)
40
+ }
41
+ const { rules: winners, opaque } = recovered
42
+ const entries = [...winners]
43
+ const report = analyzeGatingRules(entries, { ...opts, resolveRef: name => winners.get(name) })
44
+ const fromOpaque: Unanalysable[] = opaque.map(o => ({
45
+ rule: o.ruleNames.length > 0 ? o.ruleNames.join(', ') : `<artifact ${o.ns}>`,
46
+ kind: 'opaque-artifact' as const,
47
+ reason: `precompiled artifact "${o.ns}" carries compiled rule functions, not re-lowerable IR — `
48
+ + 'its choices cannot be examined. Recompile the contributing grammar so it carries IR '
49
+ + '(the macro emits IR by default) to bring it back into analysis.',
50
+ }))
51
+ // Also announce it on the degradation channel. `unanalysable` is structured and correct,
52
+ // but it is only seen by a caller that reads the report and knows to check that field;
53
+ // the degradation channel is the greppable, countable one a build gate asserts zero of.
54
+ // `'opaque-artifact'` was a DECLARED degradation code with no record site anywhere — a
55
+ // published diagnostic vocabulary that could never fire. This is its real trigger.
56
+ for (const o of opaque) {
57
+ recordDegradation({
58
+ code: 'opaque-artifact',
59
+ // `info`, not `warn`: the artifact is usually someone else's package, so the author
60
+ // of THIS grammar has nothing to act on — but the blind spot still has to be counted.
61
+ severity: 'info',
62
+ where: o.ruleNames.length > 0 ? o.ruleNames.join(', ') : `<artifact ${o.ns}>`,
63
+ subject: `precompiled artifact "${o.ns}"`,
64
+ fellBackTo: `it carries compiled rule functions rather than re-lowerable IR, so its `
65
+ + `${o.ruleNames.length > 0 ? `${o.ruleNames.length} rule(s)` : 'rules'} were NOT analysed `
66
+ + 'and a clean gating report must not be read as a pass',
67
+ otherwise: 'its choices would be walked like any other rule. Recompile the contributing '
68
+ + 'grammar so it carries IR (the macro emits IR by default)',
69
+ })
70
+ }
71
+ return fromOpaque.length === 0 ? report : { ...report, unanalysable: [...report.unanalysable, ...fromOpaque] }
72
+ }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Rebuild a combinator graph THROUGH THE PUBLIC FACTORIES, substituting selected nodes.
3
+ *
4
+ * This exists so a proposed grammar fix can be APPLIED and then MEASURED, rather than
5
+ * printed and hoped for. Everything downstream — `proposeFixes()` — depends on one
6
+ * property: a rewritten grammar must be a real grammar, built the same way the author's
7
+ * was, so that compiling it and parsing with it means the same thing.
8
+ *
9
+ * WHY THE FACTORIES AND NOT A `_def` DEEP-COPY
10
+ * --------------------------------------------
11
+ * A combinator is two things at once: a `_def` descriptor the COMPILER reads, and a
12
+ * `parse` closure the INTERPRETER runs, which captured its children when the factory
13
+ * ran. Patching `_def.parser` moves the compiled engine and leaves the interpreted one
14
+ * on the old graph — the two engines would then disagree, and a verification that runs
15
+ * either one would certify a rewrite that does not exist in the other. Re-running the
16
+ * factory produces both halves from the same inputs, and also recomputes everything
17
+ * DERIVED that the fix is trying to move in the first place: `_meta.firstSet`,
18
+ * `choice._def.disjoint`, and the choice strategy.
19
+ *
20
+ * FROZEN SUBTREES, NOT A FAILED REBUILD
21
+ * -------------------------------------
22
+ * Some node kinds have no faithful public reconstruction — `dispatch` (its arms carry
23
+ * matcher objects), `recover`, `scanTo`, `guard`, `withCtx`, `trivia` (classified
24
+ * trivia carries labels derived at construction). Refusing to rebuild a whole grammar
25
+ * because it contains one of them would make this useless on every real grammar. So
26
+ * such a node is REUSED VERBATIM: nothing inside it changed, so reusing it is exact.
27
+ * The cost is precise and reported — a rewrite target that sits inside a frozen subtree
28
+ * cannot be applied, and comes back in `unapplied` so the caller can say WHY rather
29
+ * than silently dropping it.
30
+ *
31
+ * THE HONESTY BACKSTOP
32
+ * --------------------
33
+ * This module does not claim to be faithful; it is CHECKED. `proposeFixes()` first
34
+ * rebuilds with NO substitutions and requires that identity rebuild to produce
35
+ * byte-identical parse output over the caller's corpus. If any option threading below
36
+ * is wrong, that check fails and NO fix is offered for that grammar. A rebuilder that
37
+ * is wrong makes the tool silent, never wrong.
38
+ */
39
+ import type { Combinator, ParserDef } from '../types.ts'
40
+ import { literal } from '../combinators/literal.ts'
41
+ import { regex } from '../combinators/regex.ts'
42
+ import { keywords } from '../combinators/keywords.ts'
43
+ import { sequence } from '../combinators/sequence.ts'
44
+ import { choice } from '../combinators/choice.ts'
45
+ import { attempt } from '../combinators/attempt.ts'
46
+ import { many, oneOrMore, optional, sepBy } from '../combinators/repeat.ts'
47
+ import { ref } from '../combinators/ref.ts'
48
+ import { not } from '../combinators/not.ts'
49
+ import { peek } from '../combinators/peek.ts'
50
+ import { node } from '../combinators/node.ts'
51
+ import { transform, label, field } from '../combinators/map.ts'
52
+ import { token, leaf } from '../combinators/token.ts'
53
+ import { expect } from '../combinators/expect.ts'
54
+ import { adjacent, notAdjacent } from '../combinators/adjacency.ts'
55
+ import { parser } from '../combinators/grammar.ts'
56
+
57
+ /** A node kind reused verbatim: no public factory reconstructs it without loss. */
58
+ const FROZEN_TAGS: ReadonlySet<string> = new Set([
59
+ 'dispatch', 'recover', 'scanTo', 'guard', 'withCtx', 'routed', 'trivia', 'unknown',
60
+ ])
61
+
62
+ /** A subtree that was reused verbatim instead of rebuilt. */
63
+ export type FrozenSubtree = {
64
+ tag: string
65
+ /** Nearest enclosing rule name, so the report can point at it. */
66
+ rule: string
67
+ }
68
+
69
+ export type RebuildResult = {
70
+ root: Combinator<unknown>
71
+ /** Reused-verbatim subtrees, deduplicated by (rule, tag) and sorted. */
72
+ frozen: FrozenSubtree[]
73
+ /**
74
+ * Substitutions that could NOT be applied because their target sits inside a frozen
75
+ * subtree. Non-empty means the returned graph is NOT the graph the caller asked for.
76
+ */
77
+ unapplied: Combinator<unknown>[]
78
+ }
79
+
80
+ /** `_meta` fields stamped by `rules()` / `parser()` that a factory does not recompute. */
81
+ const CARRIED_META = [
82
+ 'grammarTrivia', 'grammarScanSkip', 'grammarHostMode', 'grammarTrackLines',
83
+ 'rootTriviaClassified',
84
+ ] as const
85
+
86
+ function carryStamps(from: Combinator<unknown>, to: Combinator<unknown>): void {
87
+ const fromRule = (from as { _ruleName?: string })._ruleName
88
+ if (fromRule !== undefined) (to as { _ruleName?: string })._ruleName = fromRule
89
+ const fm = from._meta as Record<string, unknown>
90
+ const tm = to._meta as Record<string, unknown>
91
+ for (const k of CARRIED_META) if (fm[k] !== undefined && tm[k] === undefined) tm[k] = fm[k]
92
+ }
93
+
94
+ /** Every combinator reachable from `p`, without resolving refs (used to test containment). */
95
+ function subtreeContains(p: Combinator<unknown>, targets: ReadonlySet<Combinator<unknown>>): Combinator<unknown>[] {
96
+ const hit: Combinator<unknown>[] = []
97
+ const seen = new Set<Combinator<unknown>>()
98
+ const walk = (c: Combinator<unknown>): void => {
99
+ if (seen.has(c)) return
100
+ seen.add(c)
101
+ if (targets.has(c)) hit.push(c)
102
+ const d = (c as { _def?: unknown })._def as ParserDef | undefined
103
+ if (d === undefined || typeof d !== 'object') return
104
+ const rec = d as unknown as Record<string, unknown>
105
+ const kids: Combinator<unknown>[] = []
106
+ if (Array.isArray(rec.parsers)) kids.push(...(rec.parsers as Combinator<unknown>[]))
107
+ if (Array.isArray(rec.skip)) kids.push(...(rec.skip as Combinator<unknown>[]))
108
+ for (const k of ['parser', 'main', 'skipped', 'separator', 'sentinel', 'selector', 'otherwise', 'fallback', 'triviaParser'])
109
+ if (rec[k]) kids.push(rec[k] as Combinator<unknown>)
110
+ if (d.tag === 'dispatch') {
111
+ for (const c2 of d.cases) kids.push(c2.parser)
112
+ if (d.matchers) for (const c2 of d.matchers) kids.push(c2.parser)
113
+ }
114
+ for (const k of kids) walk(k)
115
+ }
116
+ walk(p)
117
+ return hit
118
+ }
119
+
120
+ /**
121
+ * Rebuild `root`, replacing every combinator that is a key of `replacements` with its
122
+ * value. The replacement is spliced WITHOUT descending into the original target.
123
+ */
124
+ export function rebuildCombinator(
125
+ root: Combinator<unknown>,
126
+ replacements: ReadonlyMap<Combinator<unknown>, Combinator<unknown>>,
127
+ ): RebuildResult {
128
+ const memo = new Map<Combinator<unknown>, Combinator<unknown>>()
129
+ const frozenKeys = new Set<string>()
130
+ const frozen: FrozenSubtree[] = []
131
+ const unapplied: Combinator<unknown>[] = []
132
+ const targets = new Set(replacements.keys())
133
+
134
+ const go = (p: Combinator<unknown>, rule: string): Combinator<unknown> => {
135
+ const already = memo.get(p)
136
+ if (already !== undefined) return already
137
+ const sub = replacements.get(p)
138
+ if (sub !== undefined) { memo.set(p, sub); return sub }
139
+
140
+ const d = (p as { _def?: unknown })._def as ParserDef | undefined
141
+ if (d === undefined || typeof d !== 'object') { memo.set(p, p); return p }
142
+ const here = (p as { _ruleName?: string })._ruleName ?? rule
143
+
144
+ if (FROZEN_TAGS.has(d.tag)) {
145
+ const key = JSON.stringify([here, d.tag])
146
+ if (!frozenKeys.has(key)) { frozenKeys.add(key); frozen.push({ tag: d.tag, rule: here }) }
147
+ for (const t of subtreeContains(p, targets)) if (!unapplied.includes(t)) unapplied.push(t)
148
+ memo.set(p, p)
149
+ return p
150
+ }
151
+
152
+ // A ref/lazy is the only place a cycle can close. Publish an EMPTY slot into the
153
+ // memo before descending, so a body that refers back to this rule binds to the
154
+ // new slot rather than re-entering (which would not terminate).
155
+ if (d.tag === 'lazy') {
156
+ let target: Combinator<unknown>
157
+ try { target = (d as { thunk(): Combinator<unknown> }).thunk() }
158
+ catch {
159
+ // An undefined ref: nothing to rebuild through, and nothing inside it to miss.
160
+ memo.set(p, p)
161
+ return p
162
+ }
163
+ const slot = ref<unknown>()
164
+ memo.set(p, slot)
165
+ slot.define(go(target, here))
166
+ carryStamps(p, slot)
167
+ return slot
168
+ }
169
+
170
+ const one = (c: Combinator<unknown>): Combinator<unknown> => go(c, here)
171
+ let out: Combinator<unknown>
172
+ switch (d.tag) {
173
+ case 'literal':
174
+ out = literal(d.value, d.caseInsensitive ? { caseInsensitive: true } : {}); break
175
+ case 'regex':
176
+ out = regex(d.source, d.flags); break
177
+ case 'keywords':
178
+ out = keywords(d.words, {
179
+ ...(d.caseInsensitive ? { caseInsensitive: true } : {}),
180
+ ...(d.boundary === undefined ? {} : { boundary: d.boundary }),
181
+ }); break
182
+ case 'sequence': {
183
+ const kids = d.parsers.map(one) as [Combinator<unknown>, ...Combinator<unknown>[]]
184
+ out = sequence(...kids); break
185
+ }
186
+ case 'choice': {
187
+ // A per-arm gate predicate cannot be re-wrapped through the public `choice()`
188
+ // arity (it takes GatedArm objects the grammar built), and the whole point of
189
+ // rebuilding a choice is that its strategy is recomputed — which the gated path
190
+ // suppresses anyway. Freeze it rather than rebuild it wrong.
191
+ if (d.gates.some(g => g !== null)) {
192
+ const key = JSON.stringify([here, "choice(gated)"])
193
+ if (!frozenKeys.has(key)) { frozenKeys.add(key); frozen.push({ tag: 'choice(gated)', rule: here }) }
194
+ for (const t of subtreeContains(p, targets)) if (!unapplied.includes(t)) unapplied.push(t)
195
+ memo.set(p, p)
196
+ return p
197
+ }
198
+ const kids = d.parsers.map(one) as [Combinator<unknown>, ...Combinator<unknown>[]]
199
+ out = choice(...kids); break
200
+ }
201
+ // Reconstructible from public factories with no loss, so NOT frozen.
202
+ case 'adjacency':
203
+ out = d.polarity === 'adjacent'
204
+ ? adjacent()
205
+ : d.kinds === undefined ? notAdjacent() : notAdjacent({ kinds: d.kinds })
206
+ break
207
+ case 'attempt': out = attempt(one(d.parser)); break
208
+ case 'not': out = not(one(d.parser)); break
209
+ case 'peek': out = peek(one(d.parser)); break
210
+ case 'optional': out = optional(one(d.parser)); break
211
+ case 'many':
212
+ out = many(one(d.parser), d.max === undefined ? {} : { max: d.max }); break
213
+ case 'oneOrMore':
214
+ out = oneOrMore(one(d.parser), { min: d.min, ...(d.max === undefined ? {} : { max: d.max }) }); break
215
+ case 'sepBy':
216
+ out = sepBy(one(d.parser), one(d.separator), {
217
+ min: d.min,
218
+ ...(d.max === undefined ? {} : { max: d.max }),
219
+ ...(d.trailing === undefined ? {} : { trailing: d.trailing }),
220
+ }); break
221
+ case 'transform': out = transform(one(d.parser), d.fn); break
222
+ case 'label': out = label(d.label, one(d.parser)); break
223
+ case 'field': out = field(d.name, one(d.parser)); break
224
+ case 'token': out = token(one(d.parser)); break
225
+ case 'leaf': out = leaf(one(d.parser), d.fn); break
226
+ // A LABELLED expect reproduces `d.expected` exactly (`expect` sets it to `[label]`).
227
+ // An unlabelled one RE-DERIVES it from the rebuilt inner parser, so it can differ
228
+ // from `d.expected` when derivation hit an undefined `lazy` thunk in a different
229
+ // order than the original construction did. That is diagnostic text, not parse
230
+ // output: `fix.ts` — the only consumer of this rebuild — compares failures by
231
+ // POSITION and excludes `expected` labels by design (see its module header).
232
+ // Preserving `d.expected` verbatim would need an expected-set override on the
233
+ // public `expect()` signature, which is not worth widening for a label.
234
+ case 'expect': out = expect(one(d.parser), d.label) as Combinator<unknown>; break
235
+ case 'node': {
236
+ const opts = {
237
+ ...(d.unwrap ? { unwrap: true } : {}),
238
+ ...(d.collapse ? { collapse: true } : {}),
239
+ ...(d.project === undefined ? {} : { project: d.project }),
240
+ ...(d.captureTrivia ? { captureTrivia: true } : {}),
241
+ ...(d.trailingTrivia ? { trailingTrivia: true } : {}),
242
+ ...(d.buildArity === undefined ? {} : { buildArity: d.buildArity }),
243
+ ...(d.tags === undefined ? {} : { tags: d.tags }),
244
+ }
245
+ // `node(combinator, build, opts)` — the UNTYPED-with-build arity — drops `opts`
246
+ // (src/combinators/node.ts:159-160 reads the 4th positional argument, which that
247
+ // arity never receives). Reconstructing through it would silently lose
248
+ // unwrap/collapse/project/tags, so freeze that one shape instead.
249
+ if (d.type === undefined && d.build !== undefined && Object.keys(opts).length > 0) {
250
+ const key = `${here} node(untyped+build+opts)`
251
+ if (!frozenKeys.has(key)) { frozenKeys.add(key); frozen.push({ tag: 'node(untyped+build+opts)', rule: here }) }
252
+ for (const t of subtreeContains(p, targets)) if (!unapplied.includes(t)) unapplied.push(t)
253
+ memo.set(p, p)
254
+ return p
255
+ }
256
+ out = d.type === undefined
257
+ ? (d.build === undefined ? node(one(d.parser), opts) : node(one(d.parser), d.build))
258
+ : node(d.type, one(d.parser), d.build, opts)
259
+ break
260
+ }
261
+ case 'grammar': {
262
+ const inner = one(d.parser)
263
+ out = parser({
264
+ ...(d.clearTrivia ? { trivia: null } : d.triviaParser === undefined ? {} : { trivia: d.triviaParser }),
265
+ ...(d.rootCapture === undefined ? {} : { rootCapture: d.rootCapture }),
266
+ ...(d.captureTrivia ? { captureTrivia: true } : {}),
267
+ trackLines: d.trackLines,
268
+ }, inner) as unknown as Combinator<unknown>
269
+ break
270
+ }
271
+ default: {
272
+ const key = JSON.stringify([here, (d as { tag: string }).tag])
273
+ if (!frozenKeys.has(key)) { frozenKeys.add(key); frozen.push({ tag: (d as { tag: string }).tag, rule: here }) }
274
+ for (const t of subtreeContains(p, targets)) if (!unapplied.includes(t)) unapplied.push(t)
275
+ memo.set(p, p)
276
+ return p
277
+ }
278
+ }
279
+ carryStamps(p, out)
280
+ memo.set(p, out)
281
+ return out
282
+ }
283
+
284
+ const rebuilt = go(root, (root as { _ruleName?: string })._ruleName ?? '<entry>')
285
+ frozen.sort((a, b) => (a.rule < b.rule ? -1 : a.rule > b.rule ? 1 : a.tag < b.tag ? -1 : a.tag > b.tag ? 1 : 0))
286
+ return { root: rebuilt, frozen, unapplied }
287
+ }