parseman 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +34 -19
  2. package/dist/analysis/duplication.d.ts +421 -0
  3. package/dist/analysis/duplication.d.ts.map +1 -0
  4. package/dist/analysis/gating.d.ts +25 -0
  5. package/dist/analysis/gating.d.ts.map +1 -1
  6. package/dist/analysis/grammar.d.ts +12 -0
  7. package/dist/analysis/grammar.d.ts.map +1 -0
  8. package/dist/combinators/parser.d.ts +26 -0
  9. package/dist/combinators/parser.d.ts.map +1 -1
  10. package/dist/compiler/build-arity.d.ts +12 -0
  11. package/dist/compiler/build-arity.d.ts.map +1 -1
  12. package/dist/compiler/codegen.d.ts +43 -1
  13. package/dist/compiler/codegen.d.ts.map +1 -1
  14. package/dist/compiler/linker.d.ts +64 -7
  15. package/dist/compiler/linker.d.ts.map +1 -1
  16. package/dist/cst/host-mode.d.ts +43 -0
  17. package/dist/cst/host-mode.d.ts.map +1 -0
  18. package/dist/functional/doc.d.ts.map +1 -1
  19. package/dist/functional/run.d.ts.map +1 -1
  20. package/dist/index.cjs +1620 -54
  21. package/dist/index.cjs.map +4 -4
  22. package/dist/index.d.ts +6 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1620 -54
  25. package/dist/index.js.map +4 -4
  26. package/dist/language-service/index.cjs +37 -1
  27. package/dist/language-service/index.cjs.map +3 -3
  28. package/dist/language-service/index.js +37 -1
  29. package/dist/language-service/index.js.map +3 -3
  30. package/dist/oracle/corpus.d.ts +32 -0
  31. package/dist/oracle/corpus.d.ts.map +1 -0
  32. package/dist/oracle/digest.d.ts +21 -0
  33. package/dist/oracle/digest.d.ts.map +1 -0
  34. package/dist/oracle/identity.d.ts +117 -0
  35. package/dist/oracle/identity.d.ts.map +1 -0
  36. package/dist/oracle/index.cjs +423 -0
  37. package/dist/oracle/index.cjs.map +7 -0
  38. package/dist/oracle/index.d.ts +37 -0
  39. package/dist/oracle/index.d.ts.map +1 -0
  40. package/dist/oracle/index.js +400 -0
  41. package/dist/oracle/index.js.map +7 -0
  42. package/dist/plugin/index.cjs +1627 -61
  43. package/dist/plugin/index.cjs.map +4 -4
  44. package/dist/plugin/index.d.ts.map +1 -1
  45. package/dist/plugin/index.js +1627 -61
  46. package/dist/plugin/index.js.map +4 -4
  47. package/dist/run/index.cjs +23 -0
  48. package/dist/run/index.cjs.map +3 -3
  49. package/dist/run/index.js +23 -0
  50. package/dist/run/index.js.map +3 -3
  51. package/dist/spec/index.cjs +2977 -56
  52. package/dist/spec/index.cjs.map +4 -4
  53. package/dist/spec/index.js +2977 -56
  54. package/dist/spec/index.js.map +4 -4
  55. package/dist/spec/model.d.ts.map +1 -1
  56. package/dist/types.d.ts +7 -0
  57. package/dist/types.d.ts.map +1 -1
  58. package/dist/version.d.ts +1 -1
  59. package/package.json +10 -3
package/README.md CHANGED
@@ -4,16 +4,37 @@
4
4
 
5
5
  # Parséman (PAR-zə-mahn)
6
6
 
7
- Write parsers as TypeScript functions. Ship them like hand-written parsers.
7
+ Write parsers as ordinary functions. Ship them like hand-written parsers.
8
8
 
9
9
  Parser combinators are pleasant to write and usually slow. Parser generators are fast and
10
- usually mean grammar files, generated code, and extra tooling. Parséman is a TypeScript
11
- combinator library with an optional compiler: write ordinary TypeScript, and a build-time
12
- macro turns it into flat JavaScript that behaves like a parser you wrote by hand.
10
+ usually mean grammar files, generated code, and extra tooling. Parséman is a combinator
11
+ library with an optional compiler, and it gives you both.
13
12
 
14
- Reach for it when you want normal TypeScript instead of grammar files, CST/AST nodes with
15
- spans and trivia, error recovery and incremental re-parsing for editor tooling, or simply
16
- a fast parser for a DSL, config language, formatter, or linter.
13
+ **Parsing to JS values, the macro build is the fastest general-purpose JS parser in the
14
+ suite** — ahead of every other library measured, at every grammar and every input size.
15
+ Every parser in the suite builds real output: objects, row arrays, AST nodes. On a 7.7 kB
16
+ GraphQL document Parséman takes **131 µs**; Peggy takes 328 µs. Only a purpose-built native
17
+ edges it out, `JSON.parse` on JSON.
18
+
19
+ ![GraphQL parsing benchmarks](https://raw.githubusercontent.com/matthew-dean/parseman/main/assets/bench-graphql.svg)
20
+
21
+ Two more results. The compiled CST path beats Lezer on the JSON CST fixture — **174 µs** vs
22
+ 594 µs at 11.9 kB — while producing a richer tree carrying spans and trivia. And `parseDoc`
23
+ stores parent-relative spans, so an in-place edit costs a fraction of a full reparse rather
24
+ than a multiple of it. Results move with grammar shape, input size and runtime — which is
25
+ why [the suite](#benchmarks) ships with the library rather than only its conclusions.
26
+
27
+ You get there by writing normal code. Add the bundler plugin, mark one import, and the
28
+ combinators you already wrote compile to flat JavaScript — the compiler computes the first
29
+ sets and left-factors the choices for you.
30
+
31
+ Grammars work the same in plain JavaScript: the macro compiles a `.js` grammar to the same
32
+ output as a `.ts` one, and the package ships both ESM and CJS. Write in TypeScript and result
33
+ types are inferred across the whole combinator chain — types you didn't have to write out.
34
+
35
+ Reach for it when you want CST/AST nodes with spans and trivia, error recovery and
36
+ incremental re-parsing for editor tooling, or simply a fast parser for a DSL, config
37
+ language, formatter, or linter.
17
38
 
18
39
  > **📖 Full documentation: [matthew-dean.github.io/parseman](https://matthew-dean.github.io/parseman/)**
19
40
 
@@ -123,24 +144,18 @@ hand-written parsers in
123
144
  Benchmarked against [Peggy](https://peggyjs.org/),
124
145
  [Parsimmon](https://github.com/jneen/parsimmon), [Chevrotain](https://chevrotain.io/),
125
146
  [Nearley](https://nearley.js.org/), [Jison](https://github.com/zaach/jison) and
126
- [Lezer](https://lezer.codemirror.net/) on JSON, CSV and GraphQL — each building real output
127
- (objects, row arrays, AST nodes), not validating syntax.
128
-
129
- **Parsing to JS values, the macro build is the fastest general-purpose JS parser in the
130
- suite** — ahead of every library above at every grammar and size (GraphQL large: **142 µs**
131
- vs Peggy's 339 µs). Only a purpose-built native edges it out, `JSON.parse` on JSON.
147
+ [Lezer](https://lezer.codemirror.net/) on JSON, CSV and GraphQL, at three input sizes each.
148
+ Each chart's legend names the libraries measured for that grammar.
132
149
 
133
- Two more results worth calling out: the compiled CST path beats Lezer on the JSON CST
134
- fixture while producing a richer tree carrying spans and trivia, and `parseDoc` stores
135
- parent-relative spans so an in-place edit costs a fraction of a full reparse rather than a
136
- multiple of it.
150
+ Largest fixture of each, macro build against the fastest other library on that chart:
151
+ GraphQL **131 µs** vs Peggy's 328 µs, JSON **133 µs** vs Chevrotain's 241 µs, CSV
152
+ **75.3 µs** vs Peggy's 420 µs. Native `JSON.parse` does JSON large in 51.6 µs. On the CST
153
+ chart, macro build runs 174 µs against Lezer's 594 µs parse-only.
137
154
 
138
155
  ![JSON parsing benchmarks](https://raw.githubusercontent.com/matthew-dean/parseman/main/assets/bench-json.svg)
139
156
 
140
157
  ![CSV parsing benchmarks](https://raw.githubusercontent.com/matthew-dean/parseman/main/assets/bench-csv.svg)
141
158
 
142
- ![GraphQL parsing benchmarks](https://raw.githubusercontent.com/matthew-dean/parseman/main/assets/bench-graphql.svg)
143
-
144
159
  ![JSON CST parsing benchmarks](https://raw.githubusercontent.com/matthew-dean/parseman/main/assets/bench-cst-json.svg)
145
160
 
146
161
  Per-fixture figures, initialization costs, hardware, grammar provenance and how to reproduce
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Static grammar-duplication / overlap / rewrite diagnostic.
3
+ *
4
+ * A parseman grammar is a COMBINATOR TREE, not source text — so the question
5
+ * "did I write this production twice?" is a structural one a machine answers
6
+ * exactly, and a reviewer answers badly. A few hundred productions is tens of
7
+ * thousands of pairs; nobody reads that. This module walks the same tree
8
+ * `analyzeGating` walks and reports nine families:
9
+ *
10
+ * 1. `duplicates` — subtrees that are structurally IDENTICAL, in ≥2 places.
11
+ * 2. `nearDuplicates` — subtrees identical except at ONE slot. This is the
12
+ * high-value case: a general rule cloned with one term
13
+ * swapped, where the fix is one production whose varying
14
+ * slot is a `choice`, not N copies of the scaffolding.
15
+ * 3. `regexFragments` — alternation runs re-spelled across several `regex()`
16
+ * terminals. Structural hashing cannot see inside a
17
+ * regex, so this is its own pass.
18
+ * 4. `regexClasses` — character classes re-spelled across terminals, and, more
19
+ * usefully, NEAR-identical ones with the drift shown side
20
+ * by side.
21
+ * 5. `overlaps` — `choice` arms whose first-sets intersect, reported as
22
+ * "these two arms, on these chars, sharing these leading
23
+ * terms" rather than gating's "dispatch failed".
24
+ * 6. `rewrites` — ALGEBRAIC simplifications: mechanically-derived exact
25
+ * rewrites (`choice(sequence(A,B), B)` → `sequence(
26
+ * optional(A), B)`), plus the dead-arm cases that are
27
+ * outright bugs.
28
+ * 7. `divergentNodes` — ONE `node()` type built by several structurally different
29
+ * productions that are nonetheless variants of one shape.
30
+ * 8. `structureLoss` — an earlier `choice` arm that FLATTENS what a later arm
31
+ * would have structured: same `node()` type, overlapping
32
+ * first-sets, and the earlier body contains no `node()`
33
+ * at all. The parse succeeds either way, so nothing else
34
+ * — not a test suite, not an output diff — reports it.
35
+ * The ordered, consequential half of `divergentNodes`.
36
+ * 9. `keywordRegexes` — hand-rolled keyword regexes (`regex(/not(?![-\w])/i)`)
37
+ * that should be `word()`/`keywords()`. Not a style note:
38
+ * `/i` without `/u` gets non-ASCII case folding wrong,
39
+ * and the fix that lives inside `keywords()` (see
40
+ * `combinators/case-fold.ts`) never reached the copies.
41
+ *
42
+ * ## What this CANNOT see
43
+ *
44
+ * Structural hashing is exactly that: STRUCTURAL. Two productions that accept the
45
+ * same language but are shaped differently — `many(x)` vs `optional(oneOrMore(x))`,
46
+ * a `regex` spelling of what another rule builds from combinators, a rule inlined
47
+ * once and referenced once — are invisible to `duplicates`/`nearDuplicates` unless
48
+ * they happen to fall into the `rewrites` algebra below. Equally, two subtrees that
49
+ * differ only in a `transform`/`node` CALLBACK are reported as distinct when the
50
+ * callbacks' source text differs, even if the functions are equivalent. This finds
51
+ * COPY-PASTE and mechanical redundancy. It does not find semantic redundancy, and a
52
+ * clean report is not a proof that a grammar has none.
53
+ *
54
+ * `analyzeDuplication()` / `analyzeDuplicationRules()` are the programmatic
55
+ * surface, mirroring `analyzeGating` / `analyzeGatingRules`. The compile-time
56
+ * wiring is OPT-IN (`compile(g, { duplication: 'warn' })` /
57
+ * `PARSEMAN_DUPLICATION=warn`) and runs on ALL THREE lowering paths — `compile`,
58
+ * `compileRuleMap` and `compileLinkable` — because the macro build takes the
59
+ * latter two and a diagnostic that only runs on the first is a diagnostic that
60
+ * reports zero findings forever.
61
+ */
62
+ import type { Combinator, FirstSet } from '../types.ts';
63
+ import { type RefResolver } from '../combinators/first-set.ts';
64
+ /** Where a combinator sits: its owning rule, and the accessor path within it. */
65
+ export type Site = {
66
+ /** Nearest enclosing `_ruleName`, or the seed name it was reached under. */
67
+ rule: string;
68
+ /** Structural path inside that rule, e.g. `choice[0] › node(Declaration) › seq[4]`. */
69
+ path: string;
70
+ };
71
+ export declare const siteToString: (s: Site) => string;
72
+ /** N structurally identical copies of one subtree. */
73
+ export type DuplicateFinding = {
74
+ kind: 'exact-duplicate';
75
+ id: string;
76
+ /** Node count of the repeated subtree. */
77
+ size: number;
78
+ /** How many DISTINCT combinator instances share the shape (shared-by-reference
79
+ * reuse is not duplication and is never counted here). */
80
+ count: number;
81
+ /** Nodes removed by hoisting the shape to one shared const: `(count - 1) * size`. */
82
+ savings: number;
83
+ /** One-line rendering of the repeated shape. */
84
+ shape: string;
85
+ sites: Site[];
86
+ suggestion: string;
87
+ };
88
+ /** N subtrees identical except at ONE slot. */
89
+ export type NearDuplicateFinding = {
90
+ kind: 'near-duplicate';
91
+ id: string;
92
+ /** Node count shared by every member (the scaffolding). */
93
+ sharedSize: number;
94
+ count: number;
95
+ savings: number;
96
+ /** The scaffolding with the varying slot rendered as `‹slot›`. */
97
+ shape: string;
98
+ /** Path to the varying slot, relative to the members' root. */
99
+ slotPath: string;
100
+ /** What each member puts in that slot, aligned with `sites`. */
101
+ variants: string[];
102
+ sites: Site[];
103
+ suggestion: string;
104
+ };
105
+ /** An alternation run re-spelled across several `regex()` terminals. */
106
+ export type RegexFragmentFinding = {
107
+ kind: 'regex-fragment';
108
+ id: string;
109
+ /** The shared run, as it appears in the sources (`>=|<=|=>|=<|=~|[<>=]`). */
110
+ fragment: string;
111
+ /** Number of alternation branches in the run. */
112
+ branches: number;
113
+ count: number;
114
+ /** Characters removed by hoisting: `(count - 1) * fragment.length`. */
115
+ savings: number;
116
+ /** The full source of each regex that carries it, aligned with `sites`. */
117
+ sources: string[];
118
+ sites: Site[];
119
+ suggestion: string;
120
+ };
121
+ /** One spelling of a character class, and where it appears. */
122
+ export type RegexClassVariant = {
123
+ /** The class as written, including any `^`: `-_a-zA-Z0-9€-￿`. */
124
+ source: string;
125
+ /** Members this spelling has that the cluster's most common spelling does not,
126
+ * and vice versa — the DRIFT, rendered `+a-f / -0-9`. */
127
+ delta: string;
128
+ count: number;
129
+ sites: Site[];
130
+ };
131
+ /**
132
+ * A character class (or boundary lookahead class) re-spelled across several
133
+ * `regex()` terminals — and, when the spellings are not identical, the drift
134
+ * between them.
135
+ */
136
+ export type RegexClassFinding = {
137
+ kind: 'regex-class';
138
+ id: string;
139
+ /** The cluster's most common spelling. */
140
+ canonical: string;
141
+ /** Every spelling in the cluster, most common first. Two variants side by side
142
+ * IS the finding: one of them is wrong and reading cannot tell you which. */
143
+ variants: RegexClassVariant[];
144
+ /** Distinct `regex()` terminals across the whole cluster. */
145
+ count: number;
146
+ /** True when the cluster holds more than one spelling. */
147
+ drifted: boolean;
148
+ /** The class's highest code point is U+FFFF and it was written as an explicit
149
+ * range — astral-plane characters fall outside it. */
150
+ bmpCeiling: boolean;
151
+ suggestion: string;
152
+ };
153
+ /** Two arms of one `choice` whose first-sets intersect. */
154
+ export type ArmOverlapFinding = {
155
+ kind: 'arm-overlap';
156
+ id: string;
157
+ site: Site;
158
+ a: number;
159
+ b: number;
160
+ /** The SHARED first characters (not the union). */
161
+ on: FirstSet;
162
+ /** How many leading terms the two arms spell identically. */
163
+ sharedLeadingTerms: number;
164
+ /** Rendering of those shared leading terms, when there are any. */
165
+ sharedPrefix: string | null;
166
+ /** True when parseman's `sharedPrefix` choice strategy already recognizes the
167
+ * common prefix once at runtime — the finding is then about READABILITY, not
168
+ * speed. */
169
+ handledByStrategy: boolean;
170
+ /** Both arms lead with a `regex()` whose character classes intersect — usually
171
+ * a sign the two arms want to be one terminal. */
172
+ regexPair: boolean;
173
+ suggestion: string;
174
+ };
175
+ export type RewriteKind = 'optional-prefix' | 'optional-suffix' | 'left-factor' | 'hand-rolled-sepby' | 'idempotent-nesting' | 'single-element' | 'duplicate-arm' | 'shadowed-arm';
176
+ /** A mechanically-derived rewrite. */
177
+ export type RewriteFinding = {
178
+ kind: 'rewrite';
179
+ id: string;
180
+ rewrite: RewriteKind;
181
+ site: Site;
182
+ /** What is there now. */
183
+ from: string;
184
+ /** What it is equal to. */
185
+ to: string;
186
+ /**
187
+ * `true` only when the rewrite provably cannot move the parse VALUE — which,
188
+ * here, means it only deletes an arm that can never be selected. Every other
189
+ * rewrite in this family changes the child arity or nesting of the value the
190
+ * site produces, so a `node()` build / `transform` / downstream consumer that
191
+ * reads positionally WILL see a different tree. Those are reported as
192
+ * CANDIDATES to verify, never as "fix this".
193
+ */
194
+ astNeutral: boolean;
195
+ /** Non-empty when the rewrite removes speculative work at parse time. */
196
+ perf: string;
197
+ /** Only on `hand-rolled-sepby`: whether this SITE can actually take `sepBy`. A
198
+ * count of matches is not a worklist — in a real grammar most matches are
199
+ * blocked, and reporting them all as convertible generates false work. */
200
+ sepByVerdict?: SepByVerdict;
201
+ /** True when this is a latent BUG (an unreachable arm), not a verbosity smell. */
202
+ bug: boolean;
203
+ suggestion: string;
204
+ };
205
+ /**
206
+ * One AST node type built by two or more STRUCTURALLY DIFFERENT productions that
207
+ * are nonetheless variants of a single shape (they spell several of the same
208
+ * terms). This is the clone family near-duplicate detection cannot see: the copies
209
+ * diverge in more than one slot — extra scaffolding, a hand-rolled whitespace run,
210
+ * a terminal guard — so no single hole explains them, yet every edit to "the
211
+ * declaration shape" still has to land in all of them, and nothing checks that it did.
212
+ */
213
+ export type DivergentNodeFinding = {
214
+ kind: 'divergent-node';
215
+ id: string;
216
+ /** The `node()` type every member produces. */
217
+ nodeType: string;
218
+ /** Number of structurally distinct productions building it. */
219
+ count: number;
220
+ /** Terms every member spells identically — the evidence they are one shape. */
221
+ sharedTerms: string[];
222
+ productions: {
223
+ shape: string;
224
+ site: Site;
225
+ /** Terms this production has that at least one sibling does not. */
226
+ distinctTerms: string[];
227
+ }[];
228
+ suggestion: string;
229
+ };
230
+ /**
231
+ * An earlier `choice` arm that FLATTENS what a later arm would have STRUCTURED.
232
+ *
233
+ * Both arms build the same `node()` type and can start on the same character, so
234
+ * on the inputs both accept the earlier one wins — and its body contains no
235
+ * `node()` at all, so the tree it produces is that node over bare leaves. The
236
+ * later arm, on those same inputs, would have produced child nodes. Nothing about
237
+ * this is visible in a parse that succeeds: `ok` is true, the span is right, the
238
+ * text round-trips. Only the SHAPE moved, and only on the subset of inputs the
239
+ * flattening arm happens to accept — which is why it survives a test suite that
240
+ * checks "does it parse" and a corpus diff that checks emitted output. It shows
241
+ * up in whatever consumes the tree: a language service that lints number tokens,
242
+ * a formatter, a refactor.
243
+ *
244
+ * This is the ordered, consequential half of `divergent-node`. That finding says
245
+ * two productions build one type and nothing keeps them in sync, and it expressly
246
+ * allows "the variants exist for a parse-order reason (a fast path tried first)".
247
+ * This one is the case where that excuse is the bug: the fast path is not
248
+ * tree-neutral, and here is the list of node types it deletes.
249
+ */
250
+ export type StructureLossFinding = {
251
+ kind: 'structure-loss';
252
+ id: string;
253
+ /** The `choice` that orders the two arms. */
254
+ site: Site;
255
+ /** The `node()` type BOTH arms build. */
256
+ nodeType: string;
257
+ /** Index of the flattening arm (the earlier one). */
258
+ earlier: number;
259
+ /** Index of the structuring arm it shadows. */
260
+ later: number;
261
+ /** The characters on which both arms can start — where the shadowing bites. */
262
+ on: FirstSet;
263
+ /** Node types the later arm builds under `nodeType` and the earlier cannot. */
264
+ lostNodeTypes: string[];
265
+ earlierShape: string;
266
+ laterShape: string;
267
+ suggestion: string;
268
+ };
269
+ /** A `regex()` that hand-rolls a keyword + word boundary. */
270
+ export type KeywordRegexFinding = {
271
+ kind: 'keyword-regex';
272
+ id: string;
273
+ site: Site;
274
+ source: string;
275
+ flags: string;
276
+ words: string[];
277
+ /** The boundary character class, as a `word()`/`keywords()` `boundary` argument.
278
+ * `null` when the regex enumerates a vocabulary with no guard at all. */
279
+ boundary: string | null;
280
+ /** `/i` without `/u`: the fold class is NOT `{c, upper(c), lower(c)}`. */
281
+ caseFoldRisk: boolean;
282
+ /** Sibling arm indices in the same `choice` that are also hand-rolled keywords. */
283
+ siblingArms: number[];
284
+ /**
285
+ * >= 3 literal alternatives: a fixed VOCABULARY enumerated by hand rather than a
286
+ * keyword with a guard. The interesting sub-case, because ordering starts to matter
287
+ * and the list is usually long enough that nobody re-reads it.
288
+ */
289
+ vocabulary: boolean;
290
+ /**
291
+ * Alternatives are in non-increasing length order. `keywords()` sorts longest-first
292
+ * by construction; a hand-written alternation does not, and nothing checks it.
293
+ */
294
+ longestFirst: boolean;
295
+ /**
296
+ * Earlier alternatives that are strict PREFIXES of later ones. Regex alternation is
297
+ * first-match, so the longer branch is reachable only when a trailing boundary guard
298
+ * rejects the following character and forces a backtrack (`rescuedByBoundary`).
299
+ */
300
+ hazards: {
301
+ shorter: string;
302
+ longer: string;
303
+ at: string;
304
+ rescuedByBoundary: boolean;
305
+ }[];
306
+ /**
307
+ * At least one hazard is NOT rescued by a boundary guard — a later alternative can
308
+ * never match. That is a live bug, not a cleanup.
309
+ */
310
+ bug: boolean;
311
+ suggestion: string;
312
+ };
313
+ export type DuplicationReport = {
314
+ duplicates: DuplicateFinding[];
315
+ nearDuplicates: NearDuplicateFinding[];
316
+ regexFragments: RegexFragmentFinding[];
317
+ regexClasses: RegexClassFinding[];
318
+ overlaps: ArmOverlapFinding[];
319
+ rewrites: RewriteFinding[];
320
+ divergentNodes: DivergentNodeFinding[];
321
+ structureLoss: StructureLossFinding[];
322
+ keywordRegexes: KeywordRegexFinding[];
323
+ /** Ids listed in `accept` that matched no finding — stale entries to prune. */
324
+ acceptedUnused: string[];
325
+ stats: {
326
+ rules: number;
327
+ /** Distinct combinator INSTANCES reached. */
328
+ nodes: number;
329
+ /** Distinct structural shapes among them. */
330
+ shapes: number;
331
+ };
332
+ };
333
+ export type AnalyzeDuplicationOptions = {
334
+ /** Smallest repeated subtree worth reporting, in nodes. Default 3 — which is
335
+ * what keeps `optional(ws)` (2 nodes) out of the ranking. */
336
+ minSize?: number;
337
+ /** Cap per category, applied AFTER ranking. Default 25. */
338
+ maxFindings?: number;
339
+ /** Finding `id`s to suppress — the single per-finding acknowledgement channel,
340
+ * mirroring the gating snapshot allowlist. */
341
+ accept?: Iterable<string>;
342
+ /** Bind cross-artifact `g.Foo` holes when computing first-sets for `overlaps`. */
343
+ resolveRef?: RefResolver;
344
+ /** Name to attribute an unnamed entry to, instead of `<entry>`. */
345
+ entryName?: string;
346
+ };
347
+ /**
348
+ * Every alternation branch list in a regex source, at ANY nesting depth. Depth
349
+ * matters: `/a|b/` and `/x(?:a|b)y/` re-spell the same run, and only one of them
350
+ * has it at top level.
351
+ */
352
+ export declare function alternationGroups(src: string): string[][];
353
+ /**
354
+ * Recognize a `regex()` that hand-rolls what `word()`/`keywords()` owns.
355
+ *
356
+ * TWO shapes, because they fail differently:
357
+ *
358
+ * - a word (or word alternation) plus a trailing word-boundary guard —
359
+ * `regex(/not(?![-\w])/i)`;
360
+ * - a bare alternation of >= 3 literal words with NO guard — a fixed vocabulary
361
+ * enumerated by hand. A regex enumerating a fixed vocabulary is a keyword set
362
+ * written the hard way: it loses first-set gating, it hand-maintains an ordering
363
+ * the combinator guarantees, and with `/i` and no `/u` it inherits the non-ASCII
364
+ * case-folding bug `keywords()` fixes internally. The no-guard form is also where
365
+ * ordering is load-bearing, since nothing backtracks past a shorter match.
366
+ *
367
+ * Returns the words and the boundary CLASS in the exact form `word(str, boundary)` /
368
+ * `keywords(words, { boundary })` take, so the suggestion names a real call rather
369
+ * than describing one.
370
+ */
371
+ export declare function keywordRegexShape(source: string): {
372
+ words: string[];
373
+ boundary: string | null;
374
+ } | null;
375
+ /**
376
+ * Ordering hazards in a hand-written alternation: an EARLIER alternative that is a
377
+ * strict prefix of a LATER one. Regex alternation is first-match, not longest-match,
378
+ * so `red|redish` matches only `red` — unless a trailing boundary guard rejects the
379
+ * character that follows, which makes the engine backtrack into the longer branch.
380
+ *
381
+ * That distinction is the whole point of reporting this: without a rescuing guard the
382
+ * longer word is UNREACHABLE, which is a live bug, not a style finding. `keywords()`
383
+ * sorts longest-first by construction and the hazard cannot exist there at all.
384
+ */
385
+ export declare function keywordAlternationHazards(words: readonly string[], boundary: string | null): {
386
+ shorter: string;
387
+ longer: string;
388
+ at: string;
389
+ rescuedByBoundary: boolean;
390
+ }[];
391
+ export declare function analyzeDuplication(entry: Combinator<unknown>, opts?: AnalyzeDuplicationOptions): DuplicationReport;
392
+ export declare function analyzeDuplicationRules(ruleMap: ReadonlyArray<readonly [string, Combinator<unknown>]>, opts?: AnalyzeDuplicationOptions): DuplicationReport;
393
+ /**
394
+ * Every `[...]` in a regex source, INCLUDING the ones inside boundary lookaheads
395
+ * (`(?![-\w])` yields `-\w`) — those are the same duplication in a different
396
+ * costume, and the same drift risk.
397
+ */
398
+ export declare function extractCharClasses(src: string): string[];
399
+ /**
400
+ * Split a class body into normalized MEMBERS (`a-z`, `\w`, `-`, `€-￿`).
401
+ * Non-ASCII literals are rewritten to `\uXXXX` so a class typed with a raw `￿`
402
+ * and one typed with `￿` compare equal — otherwise the diagnostic reports
403
+ * drift that is only an editor's.
404
+ */
405
+ export declare function charClassMembers(body: string): string[];
406
+ /**
407
+ * Whether a hand-rolled separated list can ACTUALLY become `sepBy` — a verdict per
408
+ * site, not a count. Reporting every match as convertible generates false work: on
409
+ * the reference Less grammar only a minority are, and the rest are blocked for two
410
+ * concrete, detectable reasons.
411
+ */
412
+ export type SepByVerdict = 'convertible' | 'blocked-by-capture' | 'reducer-stride-review';
413
+ export type DuplicationWarnLevel = 'off' | 'warn' | 'error';
414
+ /**
415
+ * Ranked, actionable lines — same tone and shape as `formatGatingWarnings`: what
416
+ * it is, where it is, and one concrete thing to do about it.
417
+ */
418
+ export declare function formatDuplicationFindings(report: DuplicationReport): string[];
419
+ /** Total findings across every category — the number the `'error'` gate keys on. */
420
+ export declare function duplicationFindingCount(report: DuplicationReport): number;
421
+ //# sourceMappingURL=duplication.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duplication.d.ts","sourceRoot":"","sources":["../../src/analysis/duplication.ts"],"names":[],"mappings":"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"}
@@ -134,10 +134,35 @@ export type AnalyzeGatingOptions = {
134
134
  */
135
135
  resolveRef?: RefResolver;
136
136
  };
137
+ /**
138
+ * A rule the walk could NOT introspect. Its choices were never examined, so no
139
+ * verdict about it — clean or otherwise — is available.
140
+ *
141
+ * This exists because the alternative is silence. A `compose()` result is a map of
142
+ * FUSED rule functions with no `_def` combinator graph; walking one used to throw a
143
+ * bare `TypeError: Cannot read properties of undefined (reading 'tag')`, and the
144
+ * default-on diagnostic swallowed that throw and returned `undefined` — a failed
145
+ * analysis and a clean grammar were indistinguishable. Every unanalysable input is
146
+ * now counted and named here, and `formatGatingWarnings` always reports it.
147
+ */
148
+ export type Unanalysable = {
149
+ /** The rule name (or seed name) whose walk stopped. */
150
+ rule: string;
151
+ /** Why it could not be walked, in terms the caller can act on. */
152
+ reason: string;
153
+ kind: 'fused-rule' | 'opaque-artifact' | 'not-a-combinator';
154
+ };
137
155
  export type GatingReport = {
138
156
  totalChoices: number;
139
157
  gated: number;
140
158
  recoverable: number;
159
+ /**
160
+ * Rules the walk could not introspect (see `Unanalysable`). NON-EMPTY MEANS THE
161
+ * REPORT IS PARTIAL: `totalChoices === 0` with a non-empty `unanalysable` is a
162
+ * blind walk, not a clean grammar. Callers that treat an empty `ungated` as a pass
163
+ * MUST also assert this is empty.
164
+ */
165
+ unanalysable: Unanalysable[];
141
166
  /** Genuinely-ungated choices NOT in the accepted allowlist — warned + gate-failing. */
142
167
  ungated: ChoiceGating[];
143
168
  /** Ungated choices whose id was in the accepted allowlist — silent, accepted with intent. */
@@ -1 +1 @@
1
- {"version":3,"file":"gating.d.ts","sourceRoot":"","sources":["../../src/analysis/gating.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAa,MAAM,aAAa,CAAA;AAClE,OAAO,EAA4B,KAAK,WAAW,EAAE,MAAM,6BAA6B,CAAA;AAExF,+EAA+E;AAC/E,MAAM,MAAM,aAAa,GACrB,aAAa,GACb,iBAAiB,GACjB,oBAAoB,GACpB,kBAAkB,GAClB,gBAAgB,GAChB,WAAW,CAAA;AAEf,yDAAyD;AACzD,MAAM,MAAM,MAAM,GAAG;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,aAAa,CAAA;IACpB,gGAAgG;IAChG,MAAM,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAA;IACvB;;;;;;;;OAQG;IACH,kBAAkB,EAAE,OAAO,CAAA;IAC3B,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,oEAAoE;AACpE,MAAM,MAAM,OAAO,GAAG;IACpB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,EAAE,EAAE,QAAQ,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,iFAAiF;AACjF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,YAAY,GAAG,aAAa,GAAG,eAAe,CAAA;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,gBAAgB,GAAG,sBAAsB,GAAG,cAAc,CAAA;AAEzG,MAAM,MAAM,YAAY,GAAG;IACzB;;;;;OAKG;IACH,EAAE,EAAE,MAAM,CAAA;IACV,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,iBAAiB,CAAA;IAC3B;;;;;;;OAOG;IACH,KAAK,EAAE,KAAK,GAAG,aAAa,GAAG,IAAI,CAAA;IACnC,kFAAkF;IAClF,QAAQ,EAAE,OAAO,CAAA;IACjB;;;;;;;;;OASG;IACH,QAAQ,EAAE,OAAO,CAAA;IACjB,gBAAgB,EAAE;QAAE,OAAO,EAAE,QAAQ,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAA;IACvD,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,QAAQ,EAAE,OAAO,EAAE,CAAA;CACpB,CAAA;AAED,qEAAqE;AACrE,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;IACzB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,WAAW,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,uFAAuF;IACvF,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,6FAA6F;IAC7F,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB;;;;OAIG;IACH,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB,qFAAqF;IACrF,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,wDAAwD;IACxD,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,YAAY,EAAE,WAAW,EAAE,CAAA;CAC5B,CAAA;AAsCD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,CAIrD;AAmKD,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,oBAAoB,GAAG,YAAY,CAEnG;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,EAC9D,IAAI,CAAC,EAAE,oBAAoB,GAC1B,YAAY,CA2Dd;AAuDD,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAA;AAEtD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,EAAE,CAanE"}
1
+ {"version":3,"file":"gating.d.ts","sourceRoot":"","sources":["../../src/analysis/gating.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { type AnalyzeGatingOptions, type GatingReport } from './gating.ts';
2
+ /** A `rules()` map, a `compose()` result, or any rule-name → value record. */
3
+ export type AnalysableGrammar = Record<string, unknown>;
4
+ /**
5
+ * Analyze a grammar's choices, resolving a composed grammar back to its carried IR
6
+ * first. Accepts a `rules()` map (walked directly) or a `compose()` result (recovered).
7
+ *
8
+ * The returned report's `unanalysable` is authoritative: non-empty means part of the
9
+ * grammar was never examined, and a clean `ungated` must NOT be read as a pass.
10
+ */
11
+ export declare function analyzeGrammarGating(grammar: AnalysableGrammar, opts?: AnalyzeGatingOptions): GatingReport;
12
+ //# sourceMappingURL=grammar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../../src/analysis/grammar.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAsB,KAAK,oBAAoB,EAAE,KAAK,YAAY,EAAqB,MAAM,aAAa,CAAA;AAGjH,8EAA8E;AAC9E,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEvD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,CAAC,EAAE,oBAAoB,GAC1B,YAAY,CAkBd"}
@@ -1,4 +1,5 @@
1
1
  import type { Combinator } from '../types.ts';
2
+ import type { HostMode } from '../compiler/codegen.ts';
2
3
  /**
3
4
  * Non-enumerable key on a `rules()` result holding the factory's declaration
4
5
  * order (the returned object's key order), which differs from the result's own
@@ -45,6 +46,31 @@ export type RulesOptions = {
45
46
  * and inherited everywhere, mirroring `trivia`. `null`/absent = none.
46
47
  */
47
48
  scanSkip?: Combinator<unknown>[] | null;
49
+ /**
50
+ * Compile-time host mode for this grammar, same meaning as `compile(g, { hostMode })`
51
+ * and `compose(items, { hostMode })`: `'ast'` (default) emits each direct builder's
52
+ * own result and NO positioned-CST branch; `'cst'` builds every node through the
53
+ * `ctx.build` host and captures unconditionally.
54
+ *
55
+ * Declaring it HERE is what lets ONE grammar source serve both consumers under the
56
+ * macro, which cannot take a compile option any other way:
57
+ *
58
+ * ```ts
59
+ * const factory = (g: any) => ({ … })
60
+ * export const grammar = rules({ trivia: rw }, factory)
61
+ * export const cstGrammar = rules({ trivia: rw, hostMode: 'cst' }, factory)
62
+ * ```
63
+ *
64
+ * Two call sites over one factory, so the macro emits two independent top-level
65
+ * artifacts and each bundle tree-shakes away the one it does not import. Neither
66
+ * pays the other's cost — which is the whole reason host mode is a compile-time
67
+ * decision rather than a per-node runtime read.
68
+ *
69
+ * The INTERPRETER routes dynamically off the host and does not need this; it is
70
+ * still recorded, so `run()` can refuse a mismatched host once per parse instead of
71
+ * quietly producing the wrong tree shape.
72
+ */
73
+ hostMode?: HostMode;
48
74
  };
49
75
  export declare function rules<T extends Record<string, Combinator<unknown>>>(factory: (self: any) => T): T;
50
76
  export declare function rules<T extends Record<string, Combinator<unknown>>>(options: RulesOptions, factory: (self: any) => T): T;
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/combinators/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAI7C;;;;GAIG;AACH,eAAO,MAAM,UAAU,wBAAwB,CAAA;AAO/C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB;8EAC0E;IAC1E,MAAM,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,GAAG,IAAI,CAAA;CACxC,CAAA;AAKD,wBAAgB,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,CAAA;AAClG,wBAAgB,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,CAAA"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/combinators/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAItD;;;;GAIG;AACH,eAAO,MAAM,UAAU,wBAAwB,CAAA;AAO/C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB;8EAC0E;IAC1E,MAAM,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,GAAG,IAAI,CAAA;IACvC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAA;CACpB,CAAA;AAKD,wBAAgB,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,CAAA;AAClG,wBAAgB,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,CAAA"}
@@ -13,6 +13,18 @@ import type { ParserDef } from '../types.ts';
13
13
  type NodeDef = Extract<ParserDef, {
14
14
  tag: 'node';
15
15
  }>;
16
+ /**
17
+ * Is a positioned-CST host installed for this parse?
18
+ *
19
+ * `cstBuildHost` (and the language-service hosts) mark themselves with
20
+ * `_parsemanCstOutput`. That flag is what re-routes a node with its OWN direct builder
21
+ * through the host — and therefore what makes the direct builder's formal arity the
22
+ * WRONG basis for eliding capture, since the consumer is no longer that builder.
23
+ *
24
+ * The COMPILED engine answers this at compile time (`hostMode`, 0.37.0). The interpreter
25
+ * has no compile step, so it asks per parse.
26
+ */
27
+ export declare function cstOutputHost(build: unknown): boolean;
16
28
  /**
17
29
  * Confirmed count of simple formal parameters the build declares, or `null` when the
18
30
  * source can't be parsed into a plain identifier list (→ caller keeps full capture).
@@ -1 +1 @@
1
- {"version":3,"file":"build-arity.d.ts","sourceRoot":"","sources":["../../src/compiler/build-arity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C;;;;;;;;;;GAUG;AAEH,KAAK,OAAO,GAAG,OAAO,CAAC,SAAS,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAkBlD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuB9D;AAED,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAMxD;AAED,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAMnD;AAED,sFAAsF;AACtF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAMtD;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAMrD"}
1
+ {"version":3,"file":"build-arity.d.ts","sourceRoot":"","sources":["../../src/compiler/build-arity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C;;;;;;;;;;GAUG;AAEH,KAAK,OAAO,GAAG,OAAO,CAAC,SAAS,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAElD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAErD;AAkBD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuB9D;AAED,uFAAuF;AACvF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAMxD;AAED,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAMnD;AAED,sFAAsF;AACtF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAMtD;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAMrD"}