parseman 0.46.0 → 0.47.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.
- package/README.md +8 -3
- package/dist/analysis/choice-cost-render.d.ts +12 -4
- package/dist/analysis/choice-cost-render.d.ts.map +1 -1
- package/dist/analysis/choice-cost.d.ts +2 -2
- package/dist/analysis/choice-cost.d.ts.map +1 -1
- package/dist/analysis/commitment.d.ts +131 -0
- package/dist/analysis/commitment.d.ts.map +1 -0
- package/dist/analysis/diagnostics.cjs +9929 -7465
- package/dist/analysis/diagnostics.cjs.map +3 -4
- package/dist/analysis/diagnostics.d.ts +2 -2
- package/dist/analysis/diagnostics.js +9929 -7465
- package/dist/analysis/diagnostics.js.map +3 -4
- package/dist/analysis/duplication.d.ts.map +1 -1
- package/dist/analysis/fix.d.ts +3 -3
- package/dist/analysis/fix.d.ts.map +1 -1
- package/dist/analysis/format-number.d.ts +23 -0
- package/dist/analysis/format-number.d.ts.map +1 -0
- package/dist/analysis/gating.d.ts +39 -2
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/analysis/rebuild.d.ts.map +1 -1
- package/dist/analysis/terminal.d.ts +5 -2
- package/dist/analysis/terminal.d.ts.map +1 -1
- package/dist/cli/index.cjs +10865 -8535
- package/dist/cli/index.cjs.map +3 -4
- package/dist/cli/index.js +10865 -8535
- package/dist/cli/index.js.map +3 -4
- package/dist/combinators/adjacency.d.ts +100 -0
- package/dist/combinators/adjacency.d.ts.map +1 -0
- package/dist/combinators/choice.d.ts.map +1 -1
- package/dist/combinators/completions.d.ts.map +1 -1
- package/dist/combinators/dispatch.d.ts +8 -1
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/gate.d.ts.map +1 -1
- package/dist/combinators/grammar.d.ts +22 -0
- package/dist/combinators/grammar.d.ts.map +1 -1
- package/dist/combinators/literal.d.ts +8 -0
- package/dist/combinators/literal.d.ts.map +1 -1
- package/dist/combinators/map.d.ts +0 -1
- package/dist/combinators/map.d.ts.map +1 -1
- package/dist/combinators/node.d.ts +13 -3
- package/dist/combinators/node.d.ts.map +1 -1
- package/dist/combinators/parser.d.ts +1 -1
- package/dist/combinators/parser.d.ts.map +1 -1
- package/dist/combinators/ref.d.ts.map +1 -1
- package/dist/combinators/regex.d.ts +1 -16
- package/dist/combinators/regex.d.ts.map +1 -1
- package/dist/combinators/repeat.d.ts +30 -2
- package/dist/combinators/repeat.d.ts.map +1 -1
- package/dist/combinators/scanTo.d.ts +71 -1
- package/dist/combinators/scanTo.d.ts.map +1 -1
- package/dist/combinators/sequence.d.ts.map +1 -1
- package/dist/combinators/token.d.ts.map +1 -1
- package/dist/combinators/trivia-boundary.d.ts +26 -0
- package/dist/combinators/trivia-boundary.d.ts.map +1 -0
- package/dist/combinators/trivia-skip.d.ts +40 -1
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/combinators/withCtx.d.ts.map +1 -1
- package/dist/compiler/compile-linkable-table.d.ts +104 -0
- package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
- package/dist/compiler/degradation.d.ts +0 -2
- package/dist/compiler/degradation.d.ts.map +1 -1
- package/dist/compiler/fields.d.ts +13 -0
- package/dist/compiler/fields.d.ts.map +1 -1
- package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
- package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts +47 -53
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +81 -0
- package/dist/compiler/token-alphabet.d.ts.map +1 -0
- package/dist/compiler/token-dispatch.d.ts +95 -0
- package/dist/compiler/token-dispatch.d.ts.map +1 -0
- package/dist/compiler/token-scanner.d.ts +53 -0
- package/dist/compiler/token-scanner.d.ts.map +1 -0
- package/dist/compiler/value-usage.d.ts.map +1 -1
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.d.ts.map +1 -1
- package/dist/cst/capture-buffer.d.ts +35 -0
- package/dist/cst/capture-buffer.d.ts.map +1 -1
- package/dist/cst/reflection-attach.d.ts +8 -0
- package/dist/cst/reflection-attach.d.ts.map +1 -0
- package/dist/cst/reflection-symbols.d.ts +10 -0
- package/dist/cst/reflection-symbols.d.ts.map +1 -0
- package/dist/cst/reflection.d.ts +2 -4
- package/dist/cst/reflection.d.ts.map +1 -1
- package/dist/cst/root-trivia-scope.d.ts +26 -0
- package/dist/cst/root-trivia-scope.d.ts.map +1 -0
- package/dist/cst/trivia-charscan.d.ts +18 -0
- package/dist/cst/trivia-charscan.d.ts.map +1 -0
- package/dist/cst/trivia-kinds.d.ts +19 -0
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/functional/doc.d.ts.map +1 -1
- package/dist/functional/run-tabled.d.ts +12 -0
- package/dist/functional/run-tabled.d.ts.map +1 -0
- package/dist/functional/run.d.ts +25 -42
- package/dist/functional/run.d.ts.map +1 -1
- package/dist/grammar-metadata.d.ts +3 -0
- package/dist/grammar-metadata.d.ts.map +1 -0
- package/dist/index.cjs +9224 -7203
- package/dist/index.cjs.map +3 -4
- package/dist/index.d.ts +18 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9219 -7198
- package/dist/index.js.map +3 -4
- package/dist/language-service/index.cjs +311 -121
- package/dist/language-service/index.cjs.map +3 -4
- package/dist/language-service/index.js +311 -121
- package/dist/language-service/index.js.map +3 -4
- package/dist/oracle/index.cjs.map +0 -1
- package/dist/oracle/index.js.map +0 -1
- package/dist/parse-context.d.ts +49 -0
- package/dist/parse-context.d.ts.map +1 -0
- package/dist/plugin/evaluator.d.ts +40 -1
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.cjs +11146 -9593
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11146 -9593
- package/dist/plugin/index.js.map +3 -4
- package/dist/regex/classes.d.ts +28 -0
- package/dist/regex/classes.d.ts.map +1 -1
- package/dist/regex/first-set.d.ts +14 -0
- package/dist/regex/first-set.d.ts.map +1 -1
- package/dist/run/index.cjs +136 -51
- package/dist/run/index.cjs.map +2 -3
- package/dist/run/index.js +136 -51
- package/dist/run/index.js.map +2 -3
- package/dist/spec/index.cjs +2898 -1623
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2898 -1623
- package/dist/spec/index.js.map +3 -4
- package/dist/spec/model.d.ts +8 -0
- package/dist/spec/model.d.ts.map +1 -1
- package/dist/table/assemble.d.ts +307 -0
- package/dist/table/assemble.d.ts.map +1 -0
- package/dist/table/cell.d.ts +42 -0
- package/dist/table/cell.d.ts.map +1 -0
- package/dist/table/child-slots.d.ts +30 -0
- package/dist/table/child-slots.d.ts.map +1 -0
- package/dist/table/compile-rule-map.d.ts +136 -0
- package/dist/table/compile-rule-map.d.ts.map +1 -0
- package/dist/table/compile.d.ts +56 -0
- package/dist/table/compile.d.ts.map +1 -0
- package/dist/table/duplication-hook.d.ts +32 -0
- package/dist/table/duplication-hook.d.ts.map +1 -0
- package/dist/table/emit-assembly.d.ts +139 -0
- package/dist/table/emit-assembly.d.ts.map +1 -0
- package/dist/table/emit.d.ts +113 -0
- package/dist/table/emit.d.ts.map +1 -0
- package/dist/table/encode-baseline.d.ts +16 -0
- package/dist/table/encode-baseline.d.ts.map +1 -0
- package/dist/table/encode.d.ts +71 -0
- package/dist/table/encode.d.ts.map +1 -0
- package/dist/table/exec-baseline.d.ts +9 -0
- package/dist/table/exec-baseline.d.ts.map +1 -0
- package/dist/table/exec.d.ts +72 -0
- package/dist/table/exec.d.ts.map +1 -0
- package/dist/table/fold.d.ts +25 -0
- package/dist/table/fold.d.ts.map +1 -0
- package/dist/table/index.cjs +11375 -0
- package/dist/table/index.cjs.map +6 -0
- package/dist/table/index.d.ts +100 -0
- package/dist/table/index.d.ts.map +1 -0
- package/dist/table/index.js +11352 -0
- package/dist/table/index.js.map +6 -0
- package/dist/table/inspect.d.ts +17 -0
- package/dist/table/inspect.d.ts.map +1 -0
- package/dist/table/ops.d.ts +446 -0
- package/dist/table/ops.d.ts.map +1 -0
- package/dist/table/program.d.ts +493 -0
- package/dist/table/program.d.ts.map +1 -0
- package/dist/table/run-support.d.ts +38 -0
- package/dist/table/run-support.d.ts.map +1 -0
- package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
- package/dist/table/scan-shapes.d.ts.map +1 -0
- package/dist/table/site-labels.d.ts +58 -0
- package/dist/table/site-labels.d.ts.map +1 -0
- package/dist/table/stamp.d.ts +70 -0
- package/dist/table/stamp.d.ts.map +1 -0
- package/dist/types.d.ts +80 -24
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +25 -2
- package/src/analysis/choice-cost-gate.ts +359 -0
- package/src/analysis/choice-cost-render.ts +293 -0
- package/src/analysis/choice-cost.ts +1008 -0
- package/src/analysis/commitment.ts +557 -0
- package/src/analysis/corpus.ts +130 -0
- package/src/analysis/diagnose-render.ts +625 -0
- package/src/analysis/diagnose.ts +317 -0
- package/src/analysis/diagnostics.ts +33 -0
- package/src/analysis/duplication.ts +1955 -0
- package/src/analysis/fix-render.ts +217 -0
- package/src/analysis/fix.ts +669 -0
- package/src/analysis/format-number.ts +31 -0
- package/src/analysis/gating.ts +792 -0
- package/src/analysis/grammar.ts +72 -0
- package/src/analysis/rebuild.ts +287 -0
- package/src/analysis/terminal.ts +336 -0
- package/src/cli/index.ts +413 -0
- package/src/combinators/adjacency.ts +171 -0
- package/src/combinators/attempt.ts +41 -0
- package/src/combinators/case-fold.ts +90 -0
- package/src/combinators/choice.ts +466 -0
- package/src/combinators/completions.ts +64 -0
- package/src/combinators/dispatch.ts +358 -0
- package/src/combinators/expect.ts +159 -0
- package/src/combinators/first-set.ts +322 -0
- package/src/combinators/gate.ts +50 -0
- package/src/combinators/grammar.ts +272 -0
- package/src/combinators/keywords.ts +145 -0
- package/src/combinators/literal.ts +102 -0
- package/src/combinators/map.ts +106 -0
- package/src/combinators/node.ts +345 -0
- package/src/combinators/not.ts +55 -0
- package/src/combinators/parser.ts +266 -0
- package/src/combinators/peek.ts +63 -0
- package/src/combinators/probe.ts +25 -0
- package/src/combinators/ref.ts +109 -0
- package/src/combinators/regex.ts +170 -0
- package/src/combinators/repeat.ts +561 -0
- package/src/combinators/scanTo.ts +393 -0
- package/src/combinators/sequence.ts +221 -0
- package/src/combinators/token.ts +120 -0
- package/src/combinators/trivia-boundary.ts +48 -0
- package/src/combinators/trivia-skip.ts +821 -0
- package/src/combinators/withCtx.ts +45 -0
- package/src/compiler/build-arity.ts +330 -0
- package/src/compiler/compile-linkable-table.ts +209 -0
- package/src/compiler/degradation.ts +266 -0
- package/src/compiler/fields.ts +191 -0
- package/src/compiler/grammar-coverage-ids.ts +129 -0
- package/src/compiler/ir-serialize.ts +615 -0
- package/src/compiler/linker.ts +954 -0
- package/src/compiler/token-alphabet.ts +209 -0
- package/src/compiler/token-dispatch.ts +353 -0
- package/src/compiler/token-scanner.ts +318 -0
- package/src/compiler/value-usage.ts +147 -0
- package/src/coverage.ts +383 -0
- package/src/cst/capture-buffer.ts +297 -0
- package/src/cst/host-mode.ts +69 -0
- package/src/cst/reflection-attach.ts +11 -0
- package/src/cst/reflection-symbols.ts +9 -0
- package/src/cst/reflection.ts +149 -0
- package/src/cst/relative-spans.ts +207 -0
- package/src/cst/root-trivia-scope.ts +30 -0
- package/src/cst/trivia-charscan.ts +312 -0
- package/src/cst/trivia-entries.ts +285 -0
- package/src/cst/trivia-index.ts +131 -0
- package/src/cst/trivia-kinds.ts +358 -0
- package/src/cst/types.ts +76 -0
- package/src/cst/walk.ts +101 -0
- package/src/functional/doc.ts +959 -0
- package/src/functional/run-tabled.ts +153 -0
- package/src/functional/run.ts +558 -0
- package/src/grammar-metadata.ts +2 -0
- package/src/index.ts +189 -0
- package/src/language-service/index.ts +221 -0
- package/src/line-index.ts +131 -0
- package/src/oracle/digest.ts +620 -0
- package/src/oracle/index.ts +55 -0
- package/src/parse-context.ts +91 -0
- package/src/plugin/direct-builder-static.ts +102 -0
- package/src/plugin/evaluator.ts +1505 -0
- package/src/plugin/free-identifiers.ts +338 -0
- package/src/plugin/index.ts +2281 -0
- package/src/plugin/reducer-resolver.ts +606 -0
- package/src/recovery/scan.ts +155 -0
- package/src/regex/classes.ts +183 -0
- package/src/regex/first-set.ts +316 -0
- package/src/run/index.ts +31 -0
- package/src/spec/ebnf.ts +128 -0
- package/src/spec/index.ts +54 -0
- package/src/spec/model.ts +636 -0
- package/src/spec/railroad-lib.ts +559 -0
- package/src/spec/railroad.ts +256 -0
- package/src/table/assemble.ts +2994 -0
- package/src/table/cell.ts +42 -0
- package/src/table/child-slots.ts +113 -0
- package/src/table/compile-rule-map.ts +336 -0
- package/src/table/compile.ts +244 -0
- package/src/table/duplication-hook.ts +89 -0
- package/src/table/emit-assembly.ts +1766 -0
- package/src/table/emit.ts +418 -0
- package/src/table/encode-baseline.ts +340 -0
- package/src/table/encode.ts +1569 -0
- package/src/table/exec-baseline.ts +537 -0
- package/src/table/exec.ts +1672 -0
- package/src/table/fold.ts +38 -0
- package/src/table/index.ts +108 -0
- package/src/table/inspect.ts +52 -0
- package/src/table/ops.ts +459 -0
- package/src/table/program.ts +855 -0
- package/src/table/run-support.ts +66 -0
- package/src/table/scan-shapes.ts +1578 -0
- package/src/table/site-labels.ts +228 -0
- package/src/table/stamp.ts +229 -0
- package/src/types.ts +563 -0
- package/src/version.ts +16 -0
- package/dist/compiler/codegen.d.ts +0 -334
- package/dist/compiler/codegen.d.ts.map +0 -1
- package/dist/compiler/inline-build.d.ts +0 -11
- package/dist/compiler/inline-build.d.ts.map +0 -1
- package/dist/compiler/inline-callback.d.ts +0 -21
- package/dist/compiler/inline-callback.d.ts.map +0 -1
- package/dist/compiler/line-index.d.ts +0 -2
- package/dist/compiler/line-index.d.ts.map +0 -1
- package/dist/compiler/module-hoist.d.ts +0 -106
- package/dist/compiler/module-hoist.d.ts.map +0 -1
- package/dist/compiler/scannable-run.d.ts.map +0 -1
- package/dist/compiler/scannable-terminal.d.ts +0 -23
- package/dist/compiler/scannable-terminal.d.ts.map +0 -1
- package/dist/compiler/trivia-fast-path.d.ts +0 -38
- 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
|
+
}
|