parseman 0.45.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-gate.d.ts +158 -0
- package/dist/analysis/choice-cost-gate.d.ts.map +1 -0
- package/dist/analysis/choice-cost-render.d.ts +96 -0
- package/dist/analysis/choice-cost-render.d.ts.map +1 -0
- package/dist/analysis/choice-cost.d.ts +383 -0
- package/dist/analysis/choice-cost.d.ts.map +1 -0
- package/dist/analysis/commitment.d.ts +131 -0
- package/dist/analysis/commitment.d.ts.map +1 -0
- package/dist/analysis/corpus.d.ts +73 -0
- package/dist/analysis/corpus.d.ts.map +1 -0
- package/dist/analysis/diagnose-render.d.ts +65 -0
- package/dist/analysis/diagnose-render.d.ts.map +1 -0
- package/dist/analysis/diagnose.d.ts +15 -0
- package/dist/analysis/diagnose.d.ts.map +1 -1
- package/dist/analysis/diagnostics.cjs +13736 -0
- package/dist/analysis/diagnostics.cjs.map +6 -0
- package/dist/analysis/diagnostics.d.ts +27 -0
- package/dist/analysis/diagnostics.d.ts.map +1 -0
- package/dist/analysis/diagnostics.js +13713 -0
- package/dist/analysis/diagnostics.js.map +6 -0
- package/dist/analysis/duplication.d.ts.map +1 -1
- package/dist/analysis/fix-render.d.ts +43 -0
- package/dist/analysis/fix-render.d.ts.map +1 -0
- package/dist/analysis/fix.d.ts +172 -0
- package/dist/analysis/fix.d.ts.map +1 -0
- 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 +47 -2
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/analysis/rebuild.d.ts +61 -0
- package/dist/analysis/rebuild.d.ts.map +1 -0
- package/dist/analysis/terminal.d.ts +145 -0
- package/dist/analysis/terminal.d.ts.map +1 -0
- package/dist/cli/index.cjs +14837 -0
- package/dist/cli/index.cjs.map +6 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +14814 -0
- package/dist/cli/index.js.map +6 -0
- 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/build-arity.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 +79 -47
- 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 +10499 -7478
- package/dist/index.cjs.map +3 -4
- package/dist/index.d.ts +24 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10499 -7478
- 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 +41 -2
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/free-identifiers.d.ts +23 -0
- package/dist/plugin/free-identifiers.d.ts.map +1 -0
- package/dist/plugin/index.cjs +11381 -9442
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11381 -9442
- package/dist/plugin/index.js.map +3 -4
- package/dist/plugin/reducer-resolver.d.ts +17 -1
- package/dist/plugin/reducer-resolver.d.ts.map +1 -1
- 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 -1622
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2898 -1622
- 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 +41 -3
- 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/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,131 @@
|
|
|
1
|
+
import type { Span } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
/** One captured trivia token (a whitespace run or a comment) with its source span. */
|
|
4
|
+
export type TriviaToken = { readonly value: string; readonly span: Span }
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Before/after offset index of captured trivia. Built from a tree whose nodes
|
|
8
|
+
* carry a `triviaLog` property (the flat `[start, end, insertIdx, ...]` array
|
|
9
|
+
* produced by parseman when `parser({ captureTrivia: true })` wraps a `node()` grammar).
|
|
10
|
+
*
|
|
11
|
+
* index.before.get(node.span.start) // trivia immediately before a node
|
|
12
|
+
* index.after.get(node.span.end) // trivia immediately after a node
|
|
13
|
+
*
|
|
14
|
+
* A given trivia entry is registered under BOTH the following item's start
|
|
15
|
+
* (`before`) and the preceding item's end (`after`), so either lookup finds it.
|
|
16
|
+
*/
|
|
17
|
+
export type TriviaIndex = {
|
|
18
|
+
readonly before: Map<number, TriviaToken[]>
|
|
19
|
+
readonly after: Map<number, TriviaToken[]>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type Spanned = { readonly span: Span }
|
|
23
|
+
type NodeWithTrivia = {
|
|
24
|
+
readonly span?: Span
|
|
25
|
+
readonly children?: ReadonlyArray<unknown>
|
|
26
|
+
readonly rawChildren?: ReadonlyArray<Spanned>
|
|
27
|
+
readonly triviaLog?: readonly number[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function merge(map: Map<number, TriviaToken[]>, key: number, run: TriviaToken[]): void {
|
|
31
|
+
if (run.length === 0) return
|
|
32
|
+
const existing = map.get(key)
|
|
33
|
+
if (existing) existing.push(...run)
|
|
34
|
+
else map.set(key, [...run])
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Options for trailing/leading trivia at the document boundary. A repeating root
|
|
39
|
+
* (e.g. `many()`) rolls back the trivia after its last item — it's "trailing" and
|
|
40
|
+
* belongs to the enclosing context, which at the document root is the document
|
|
41
|
+
* itself. Pass `{ trivia }` to re-scan and register that boundary trivia,
|
|
42
|
+
* so a round-trippable trivia map isn't missing the run before EOF.
|
|
43
|
+
*/
|
|
44
|
+
export type TriviaIndexOptions = {
|
|
45
|
+
/**
|
|
46
|
+
* A regex matching ONE trivia token (a whitespace run or a comment). Applied
|
|
47
|
+
* repeatedly to tokenize the boundary trivia. Each match becomes one token, so
|
|
48
|
+
* write it to match a maximal run, e.g.
|
|
49
|
+
* `/[ \t\n\r\f]+|\/\*(?:[^*]|\*(?!\/))*\*\//`.
|
|
50
|
+
*/
|
|
51
|
+
trivia: RegExp
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Walk a tree whose nodes carry `triviaLog` (from `node()` + `captureTrivia`)
|
|
56
|
+
* and build a before/after trivia index. Each node must have
|
|
57
|
+
* `triviaLog: readonly number[]` (flat `[start, end, insertIdx, …]` —
|
|
58
|
+
* three numbers per entry) and `rawChildren` (structural items with `.span`).
|
|
59
|
+
* `input` is the source string used to materialize trivia values.
|
|
60
|
+
*
|
|
61
|
+
* With `opts`, also captures leading trivia (before the root's content) and
|
|
62
|
+
* trailing trivia (after it, up to EOF) — the document boundaries a repeating
|
|
63
|
+
* root rolls back.
|
|
64
|
+
*/
|
|
65
|
+
export function buildTriviaIndex(root: unknown, input = '', opts?: TriviaIndexOptions): TriviaIndex {
|
|
66
|
+
const before = new Map<number, TriviaToken[]>()
|
|
67
|
+
const after = new Map<number, TriviaToken[]>()
|
|
68
|
+
|
|
69
|
+
const visit = (node: NodeWithTrivia): void => {
|
|
70
|
+
const log = node.triviaLog
|
|
71
|
+
const raw = node.rawChildren
|
|
72
|
+
|
|
73
|
+
if (log && log.length && raw) {
|
|
74
|
+
for (let i = 0; i < log.length; i += 3) {
|
|
75
|
+
const tStart = log[i]!, tEnd = log[i + 1]!, insertIdx = log[i + 2]!
|
|
76
|
+
if (tEnd <= tStart) continue
|
|
77
|
+
const run: TriviaToken[] = [{ value: input.slice(tStart, tEnd), span: { start: tStart, end: tEnd } }]
|
|
78
|
+
const next = raw[insertIdx]
|
|
79
|
+
if (next) merge(before, next.span.start, run)
|
|
80
|
+
const prev = insertIdx > 0 ? raw[insertIdx - 1] : undefined
|
|
81
|
+
if (prev) merge(after, prev.span.end, run)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const ch = node.children
|
|
86
|
+
if (ch) {
|
|
87
|
+
for (const c of ch) {
|
|
88
|
+
if (typeof c === 'object' && c !== null && (c as NodeWithTrivia).children !== undefined) {
|
|
89
|
+
visit(c as NodeWithTrivia)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (root) visit(root as NodeWithTrivia)
|
|
96
|
+
|
|
97
|
+
// Document-boundary trivia: re-scan leading (offset 0) and trailing (root end
|
|
98
|
+
// → EOF) trivia that a repeating root rolled back.
|
|
99
|
+
if (opts && input) {
|
|
100
|
+
const rootSpan = (root as NodeWithTrivia | null)?.span
|
|
101
|
+
const re = new RegExp(opts.trivia.source, opts.trivia.flags.replace(/[gy]/g, '') + 'y')
|
|
102
|
+
const scanFrom = (from: number): TriviaToken[] => {
|
|
103
|
+
const run: TriviaToken[] = []
|
|
104
|
+
let pos = from
|
|
105
|
+
re.lastIndex = pos
|
|
106
|
+
let m = re.exec(input)
|
|
107
|
+
while (m && m.index === pos && m[0].length > 0) {
|
|
108
|
+
run.push({ value: m[0], span: { start: pos, end: pos + m[0].length } })
|
|
109
|
+
pos += m[0].length
|
|
110
|
+
re.lastIndex = pos
|
|
111
|
+
m = re.exec(input)
|
|
112
|
+
}
|
|
113
|
+
return run
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (rootSpan && rootSpan.start > 0) {
|
|
117
|
+
const lead = scanFrom(0)
|
|
118
|
+
if (lead.length) merge(before, rootSpan.start, lead)
|
|
119
|
+
}
|
|
120
|
+
const end = rootSpan ? rootSpan.end : 0
|
|
121
|
+
if (end < input.length) {
|
|
122
|
+
const trail = scanFrom(end)
|
|
123
|
+
if (trail.length) {
|
|
124
|
+
merge(after, end, trail)
|
|
125
|
+
merge(before, end + trail.reduce((n, t) => n + (t.span.end - t.span.start), 0), trail)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { before, after }
|
|
131
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import type { Combinator, ParseContext } from '../types.ts'
|
|
2
|
+
import { pushCstTriviaEntry, pushTriviaLogEntry } from './capture-buffer.ts'
|
|
3
|
+
import { startsFirstSet } from '../combinators/first-set.ts'
|
|
4
|
+
import { createDetachedParseContext } from '../parse-context.ts'
|
|
5
|
+
import { charArmsFor, charTriviaEnd, charTriviaKindMask, charTriviaVisit } from './trivia-charscan.ts'
|
|
6
|
+
|
|
7
|
+
export type TriviaChunk = { start: number; end: number; kindIndex: number }
|
|
8
|
+
|
|
9
|
+
export type LabeledTriviaSpec = {
|
|
10
|
+
readonly labels: readonly string[]
|
|
11
|
+
readonly arms: ReadonlyArray<{ label: string; kindIndex: number; parser: Combinator<unknown> }>
|
|
12
|
+
readonly minRepeats: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** See `trivia-skip.ts`: bounded scalar memoisation avoids a WeakMap in the
|
|
16
|
+
* shipped table runtime while retaining the common trivia grammars. */
|
|
17
|
+
let labeledTrivia0: Combinator<unknown> | undefined
|
|
18
|
+
let labeledTrivia1: Combinator<unknown> | undefined
|
|
19
|
+
let labeledTrivia2: Combinator<unknown> | undefined
|
|
20
|
+
let labeledTrivia3: Combinator<unknown> | undefined
|
|
21
|
+
let labeledSpec0: LabeledTriviaSpec | null | undefined
|
|
22
|
+
let labeledSpec1: LabeledTriviaSpec | null | undefined
|
|
23
|
+
let labeledSpec2: LabeledTriviaSpec | null | undefined
|
|
24
|
+
let labeledSpec3: LabeledTriviaSpec | null | undefined
|
|
25
|
+
|
|
26
|
+
function cacheLabeledTrivia(trivia: Combinator<unknown>, spec: LabeledTriviaSpec | null): LabeledTriviaSpec | null {
|
|
27
|
+
labeledTrivia3 = labeledTrivia2
|
|
28
|
+
labeledSpec3 = labeledSpec2
|
|
29
|
+
labeledTrivia2 = labeledTrivia1
|
|
30
|
+
labeledSpec2 = labeledSpec1
|
|
31
|
+
labeledTrivia1 = labeledTrivia0
|
|
32
|
+
labeledSpec1 = labeledSpec0
|
|
33
|
+
labeledTrivia0 = trivia
|
|
34
|
+
labeledSpec0 = spec
|
|
35
|
+
return spec
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function unwrapTrivia(p: Combinator<unknown>): Combinator<unknown> {
|
|
39
|
+
let cur = p
|
|
40
|
+
while (cur._def.tag === 'trivia') cur = cur._def.parser
|
|
41
|
+
return cur
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Strip `label()` wrapper; returns inner parser for matching. */
|
|
45
|
+
export function peelLabel(p: Combinator<unknown>): { label: string; parser: Combinator<unknown> } | null {
|
|
46
|
+
if (p._def.tag === 'label') return { label: p._def.label, parser: p._def.parser }
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* When every trivia arm is `label(name, parser)` inside `oneOrMore(choice(…))`
|
|
52
|
+
* (or a single labeled arm), return the label table and matchers.
|
|
53
|
+
*/
|
|
54
|
+
export function analyzeLabeledTrivia(trivia: Combinator<unknown>): LabeledTriviaSpec | null {
|
|
55
|
+
if (trivia === labeledTrivia0) return labeledSpec0!
|
|
56
|
+
if (trivia === labeledTrivia1) return labeledSpec1!
|
|
57
|
+
if (trivia === labeledTrivia2) return labeledSpec2!
|
|
58
|
+
if (trivia === labeledTrivia3) return labeledSpec3!
|
|
59
|
+
|
|
60
|
+
let core = unwrapTrivia(trivia)
|
|
61
|
+
let minRepeats = 1
|
|
62
|
+
|
|
63
|
+
if (core._def.tag === 'oneOrMore') {
|
|
64
|
+
const min = core._def.min
|
|
65
|
+
core = core._def.parser
|
|
66
|
+
minRepeats = min
|
|
67
|
+
} else if (core._def.tag === 'many') {
|
|
68
|
+
core = core._def.parser
|
|
69
|
+
minRepeats = 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const arms: LabeledTriviaSpec['arms'][number][] = []
|
|
73
|
+
|
|
74
|
+
if (core._def.tag === 'choice') {
|
|
75
|
+
for (let i = 0; i < core._def.parsers.length; i++) {
|
|
76
|
+
const peeled = peelLabel(core._def.parsers[i]!)
|
|
77
|
+
if (!peeled) {
|
|
78
|
+
return cacheLabeledTrivia(trivia, null)
|
|
79
|
+
}
|
|
80
|
+
arms.push({ label: peeled.label, kindIndex: i, parser: peeled.parser })
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
const peeled = peelLabel(core)
|
|
84
|
+
if (!peeled) {
|
|
85
|
+
return cacheLabeledTrivia(trivia, null)
|
|
86
|
+
}
|
|
87
|
+
arms.push({ label: peeled.label, kindIndex: 0, parser: peeled.parser })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const spec = {
|
|
91
|
+
labels: arms.map(a => a.label),
|
|
92
|
+
arms,
|
|
93
|
+
minRepeats,
|
|
94
|
+
}
|
|
95
|
+
return cacheLabeledTrivia(trivia, spec)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Label table on a `trivia()` combinator, if all arms are labeled. */
|
|
99
|
+
export function triviaKindLabels(trivia: Combinator<unknown> | undefined): readonly string[] | undefined {
|
|
100
|
+
if (!trivia) return undefined
|
|
101
|
+
const fromMeta = trivia._meta.triviaKindLabels
|
|
102
|
+
if (fromMeta) return fromMeta
|
|
103
|
+
return analyzeLabeledTrivia(trivia)?.labels
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function matchArmAt(
|
|
107
|
+
input: string,
|
|
108
|
+
pos: number,
|
|
109
|
+
arm: Combinator<unknown>,
|
|
110
|
+
state: unknown,
|
|
111
|
+
): { end: number } | null {
|
|
112
|
+
// The first-set check is structural Parseman metadata. It neither assigns
|
|
113
|
+
// meaning to a label nor recognizes a particular comment form; it avoids
|
|
114
|
+
// entering arms whose own grammar proves they cannot start at this offset.
|
|
115
|
+
if (!startsFirstSet(arm, input, pos)) return null
|
|
116
|
+
const r = arm.parse(input, pos, createDetachedParseContext(false, state))
|
|
117
|
+
if (!r.ok || r.span.end <= pos) return null
|
|
118
|
+
return { end: r.span.end }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Scan maximal labeled trivia chunks (PEG `oneOrMore(choice(…))` semantics).
|
|
123
|
+
* Each successful arm match becomes one entry with that arm's kind index.
|
|
124
|
+
*/
|
|
125
|
+
export function scanLabeledTriviaChunks(
|
|
126
|
+
input: string,
|
|
127
|
+
cur: number,
|
|
128
|
+
spec: LabeledTriviaSpec,
|
|
129
|
+
state?: unknown,
|
|
130
|
+
): { end: number; chunks: TriviaChunk[] } {
|
|
131
|
+
const chunks: TriviaChunk[] = []
|
|
132
|
+
let pos = cur
|
|
133
|
+
|
|
134
|
+
while (pos < input.length) {
|
|
135
|
+
let matched: { end: number; kindIndex: number } | null = null
|
|
136
|
+
for (const arm of spec.arms) {
|
|
137
|
+
const m = matchArmAt(input, pos, arm.parser, state)
|
|
138
|
+
if (m) {
|
|
139
|
+
matched = { end: m.end, kindIndex: arm.kindIndex }
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!matched) break
|
|
144
|
+
chunks.push({ start: pos, end: matched.end, kindIndex: matched.kindIndex })
|
|
145
|
+
pos = matched.end
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (chunks.length < spec.minRepeats) {
|
|
149
|
+
return { end: cur, chunks: [] }
|
|
150
|
+
}
|
|
151
|
+
return { end: pos, chunks }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Scan labeled trivia when no caller will retain individual chunks. This is the
|
|
156
|
+
* ordinary skip path for a classified grammar: labels remain available for an
|
|
157
|
+
* explicit capture request, but merely having labels must not allocate one
|
|
158
|
+
* `{ start, end, kindIndex }` object per whitespace/comment run.
|
|
159
|
+
*/
|
|
160
|
+
export function scanLabeledTriviaEnd(
|
|
161
|
+
input: string,
|
|
162
|
+
cur: number,
|
|
163
|
+
spec: LabeledTriviaSpec,
|
|
164
|
+
state?: unknown,
|
|
165
|
+
): number {
|
|
166
|
+
// `charArmsFor` only lowers `minRepeats <= 1`, where "no chunk matched" and
|
|
167
|
+
// "the minimum was not met" are the same position — so an end IS the answer.
|
|
168
|
+
const chars = charArmsFor(spec)
|
|
169
|
+
if (chars !== null) return charTriviaEnd(input, cur, chars)
|
|
170
|
+
|
|
171
|
+
let pos = cur
|
|
172
|
+
let count = 0
|
|
173
|
+
|
|
174
|
+
while (pos < input.length) {
|
|
175
|
+
let matched: { end: number } | null = null
|
|
176
|
+
for (const arm of spec.arms) {
|
|
177
|
+
matched = matchArmAt(input, pos, arm.parser, state)
|
|
178
|
+
if (matched) break
|
|
179
|
+
}
|
|
180
|
+
if (!matched) break
|
|
181
|
+
pos = matched.end
|
|
182
|
+
count++
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return count < spec.minRepeats ? cur : pos
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Visit classified trivia matches without constructing per-match objects. The
|
|
190
|
+
* visitor is only invoked after one or more arms matched, which is the shape
|
|
191
|
+
* used by `classifiedTrivia()`; callers with a stronger repeat minimum keep
|
|
192
|
+
* the buffered scanner so an unsuccessful minimum cannot leak a partial run.
|
|
193
|
+
*/
|
|
194
|
+
export function visitLabeledTrivia(
|
|
195
|
+
input: string,
|
|
196
|
+
cur: number,
|
|
197
|
+
spec: LabeledTriviaSpec,
|
|
198
|
+
state: unknown | undefined,
|
|
199
|
+
visit: (start: number, end: number, kindIndex: number) => void,
|
|
200
|
+
): number | undefined {
|
|
201
|
+
if (spec.minRepeats > 1) return undefined
|
|
202
|
+
const chars = charArmsFor(spec)
|
|
203
|
+
if (chars !== null) return charTriviaVisit(input, cur, chars, visit)
|
|
204
|
+
|
|
205
|
+
let pos = cur
|
|
206
|
+
let count = 0
|
|
207
|
+
|
|
208
|
+
while (pos < input.length) {
|
|
209
|
+
let matched: { end: number; kindIndex: number } | null = null
|
|
210
|
+
for (const arm of spec.arms) {
|
|
211
|
+
const match = matchArmAt(input, pos, arm.parser, state)
|
|
212
|
+
if (match) {
|
|
213
|
+
matched = { end: match.end, kindIndex: arm.kindIndex }
|
|
214
|
+
break
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (!matched) break
|
|
218
|
+
visit(pos, matched.end, matched.kindIndex)
|
|
219
|
+
pos = matched.end
|
|
220
|
+
count++
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return count < spec.minRepeats ? cur : pos
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function recordTriviaChunks(ctx: ParseContext, chunks: readonly TriviaChunk[]): void {
|
|
227
|
+
const kinds = ctx.triviaKindLabels
|
|
228
|
+
const mask = ctx._triviaCaptureMask
|
|
229
|
+
const rootLog = ctx._rootTriviaLog
|
|
230
|
+
const rootKinds = ctx._rootTriviaKindIndex
|
|
231
|
+
const rootMark = rootLog?.length ?? 0
|
|
232
|
+
for (const ch of chunks) {
|
|
233
|
+
// Global trivia log: always complete (never kind-filtered).
|
|
234
|
+
pushTriviaLogEntry(ctx, ch.start, ch.end, kinds ? ch.kindIndex : undefined)
|
|
235
|
+
const rootKindIndex = ctx._rootTriviaCapture === false || rootLog === undefined || rootKinds === undefined || kinds === undefined
|
|
236
|
+
? -1
|
|
237
|
+
: (rootKinds[kinds[ch.kindIndex] ?? ''] ?? -1)
|
|
238
|
+
if (rootKindIndex >= 0) {
|
|
239
|
+
// Fill the enclosing committed gap after the scanner has consumed every
|
|
240
|
+
// chunk. A selected marker therefore carries its exact authored context
|
|
241
|
+
// without recording any whitespace-only chunk.
|
|
242
|
+
rootLog!.push(0, 0, ch.start, ch.end, rootKindIndex)
|
|
243
|
+
}
|
|
244
|
+
// Per-node CST log: honour the kind mask when both a mask and labels are
|
|
245
|
+
// present, so a host can capture (e.g.) comments only without logging every
|
|
246
|
+
// whitespace run. No mask / no labels → capture everything, as before.
|
|
247
|
+
if (ctx.captureTrivia && (ctx._cstBuf !== undefined || ctx._cstTriviaLog !== undefined)) {
|
|
248
|
+
if (mask === undefined || kinds === undefined || (mask & (1 << ch.kindIndex)) !== 0) {
|
|
249
|
+
pushCstTriviaEntry(ctx, ch.start, ch.end, kinds ? ch.kindIndex : undefined)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (rootLog !== undefined && rootLog.length !== rootMark && chunks.length > 0) {
|
|
254
|
+
const start = chunks[0]!.start
|
|
255
|
+
const end = chunks[chunks.length - 1]!.end
|
|
256
|
+
for (let offset = rootMark; offset < rootLog.length; offset += 5) {
|
|
257
|
+
rootLog[offset] = start
|
|
258
|
+
rootLog[offset + 1] = end
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Resolve a per-node trivia capture mask (`ctx._triviaCaptureMask`) from a trivia
|
|
265
|
+
* label table and the kind names a host wants to keep. Returns `undefined` when
|
|
266
|
+
* `labels` is absent (nothing to key on → capture everything).
|
|
267
|
+
*
|
|
268
|
+
* Names in `keep` that aren't in `labels` are **ignored by design**: `keep` is
|
|
269
|
+
* often a dialect-independent list (e.g. `['blockComment', 'lineComment']`) applied
|
|
270
|
+
* to a grammar whose trivia only defines some of them (CSS has no `lineComment`) —
|
|
271
|
+
* so an unknown name is a normal cross-dialect no-op, not necessarily a typo, and
|
|
272
|
+
* is not worth a warning that would cry wolf on legitimate superset lists. An empty
|
|
273
|
+
* `keep` (or one where nothing matches) yields `0`, which means "capture NO trivia
|
|
274
|
+
* into per-node logs" — an intentional, valid state (the global `_triviaLog` is
|
|
275
|
+
* unaffected either way). If you expect comments and get an empty per-node log,
|
|
276
|
+
* check the name against the grammar's `triviaKindLabels`.
|
|
277
|
+
*/
|
|
278
|
+
/**
|
|
279
|
+
* Resolve an ADJACENCY kind filter against the ACTIVE trivia table — the strict
|
|
280
|
+
* counterpart of `triviaKindMask` below.
|
|
281
|
+
*
|
|
282
|
+
* Deliberately NOT lenient. `triviaKindMask` is a capture preference, where an
|
|
283
|
+
* unknown name is a legitimate cross-dialect no-op; this is a RECOGNITION
|
|
284
|
+
* constraint, and a silently-dropped name turns `notAdjacent({kinds:['whitespace']})`
|
|
285
|
+
* into a bare `notAdjacent()` — i.e. `calc()` quietly accepting a comment where
|
|
286
|
+
* css-values-4 10.1 demands real whitespace, the exact defect the filter exists to
|
|
287
|
+
* prevent. Both failure modes are hard errors.
|
|
288
|
+
*/
|
|
289
|
+
export function resolveAdjacencyKindMask(
|
|
290
|
+
trivia: Combinator<unknown> | undefined,
|
|
291
|
+
kinds: readonly string[],
|
|
292
|
+
): number {
|
|
293
|
+
const spec = trivia ? analyzeLabeledTrivia(trivia) : null
|
|
294
|
+
if (!spec) {
|
|
295
|
+
throw new TypeError(
|
|
296
|
+
`notAdjacent({ kinds: [${kinds.map(k => JSON.stringify(k)).join(', ')}] }) requires classified trivia: `
|
|
297
|
+
+ 'the active trivia table has no category labels. Build it with classifiedTrivia({ whitespace: …, comment: … }).',
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
let mask = 0
|
|
301
|
+
for (const name of kinds) {
|
|
302
|
+
const idx = spec.labels.indexOf(name)
|
|
303
|
+
if (idx < 0) {
|
|
304
|
+
throw new TypeError(
|
|
305
|
+
`notAdjacent(): unknown trivia kind ${JSON.stringify(name)}. `
|
|
306
|
+
+ `The active trivia table declares: ${spec.labels.map(l => JSON.stringify(l)).join(', ')}.`,
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
mask |= 1 << idx
|
|
310
|
+
}
|
|
311
|
+
return mask
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Bitmask of the trivia CATEGORIES occurring in the run starting at `pos`
|
|
316
|
+
* (`1 << kindIndex` per matched chunk); `0` when no trivia is there at all.
|
|
317
|
+
* The single source of truth for the interpreter's adjacency kind test — the
|
|
318
|
+
* compiled probe (`_akN`) mirrors this loop arm for arm.
|
|
319
|
+
*/
|
|
320
|
+
export function triviaKindMaskAt(
|
|
321
|
+
input: string,
|
|
322
|
+
pos: number,
|
|
323
|
+
spec: LabeledTriviaSpec,
|
|
324
|
+
state?: unknown,
|
|
325
|
+
): number {
|
|
326
|
+
const chars = charArmsFor(spec)
|
|
327
|
+
if (chars !== null) return charTriviaKindMask(input, pos, chars)
|
|
328
|
+
|
|
329
|
+
let cur = pos
|
|
330
|
+
let mask = 0
|
|
331
|
+
while (cur < input.length) {
|
|
332
|
+
let matched: { end: number; kindIndex: number } | null = null
|
|
333
|
+
for (const arm of spec.arms) {
|
|
334
|
+
const m = matchArmAt(input, cur, arm.parser, state)
|
|
335
|
+
if (m) {
|
|
336
|
+
matched = { end: m.end, kindIndex: arm.kindIndex }
|
|
337
|
+
break
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (!matched) break
|
|
341
|
+
mask |= 1 << matched.kindIndex
|
|
342
|
+
cur = matched.end
|
|
343
|
+
}
|
|
344
|
+
return mask
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function triviaKindMask(
|
|
348
|
+
labels: readonly string[] | undefined,
|
|
349
|
+
keep: readonly string[],
|
|
350
|
+
): number | undefined {
|
|
351
|
+
if (!labels) return undefined
|
|
352
|
+
let mask = 0
|
|
353
|
+
for (const name of keep) {
|
|
354
|
+
const idx = labels.indexOf(name)
|
|
355
|
+
if (idx >= 0) mask |= 1 << idx
|
|
356
|
+
}
|
|
357
|
+
return mask
|
|
358
|
+
}
|
package/src/cst/types.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Span } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal interface a node must satisfy for incremental re-parse to navigate and
|
|
5
|
+
* re-parse the tree. Any object with `_tag: 'node'`, `type`, `span`, `state`,
|
|
6
|
+
* and `children` works — whether from `node()` build callbacks or custom ASTs.
|
|
7
|
+
*
|
|
8
|
+
* `children` only needs items with `_tag` so traversal can distinguish sub-nodes
|
|
9
|
+
* from leaves. `parseDoc` uses an optional `rebuild` callback (or a
|
|
10
|
+
* shallow spread) when grafting a re-parsed subtree.
|
|
11
|
+
*/
|
|
12
|
+
export type NodeLike = {
|
|
13
|
+
readonly _tag: 'node'
|
|
14
|
+
readonly type: string
|
|
15
|
+
readonly span: Span
|
|
16
|
+
readonly state: unknown
|
|
17
|
+
readonly children: ReadonlyArray<{ readonly _tag: string }>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A named CST node from a `node()` rule (or equivalent build callback).
|
|
22
|
+
* Children are in parse order: sub-nodes interspersed with CSTLeaf terminals.
|
|
23
|
+
*/
|
|
24
|
+
export type CSTNode = {
|
|
25
|
+
readonly _tag: 'node'
|
|
26
|
+
/** Rule name — the method name that produced this node. */
|
|
27
|
+
readonly type: string
|
|
28
|
+
/** Optional grammar categories materialized by `cstBuildHost({ tags: true })`. */
|
|
29
|
+
readonly tags?: readonly string[]
|
|
30
|
+
readonly span: Span
|
|
31
|
+
readonly children: CSTChild[]
|
|
32
|
+
/**
|
|
33
|
+
* Shallow clone of ctx.state at the moment this node's parse began.
|
|
34
|
+
* Used as the re-entry point for incremental re-parsing.
|
|
35
|
+
* Only meaningful when ctx.state is primitives-only (no mutable objects).
|
|
36
|
+
*/
|
|
37
|
+
readonly state: unknown
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A terminal token — the result of a literal() or regex() match inside a node rule.
|
|
42
|
+
*/
|
|
43
|
+
export type CSTLeaf = {
|
|
44
|
+
readonly _tag: 'leaf'
|
|
45
|
+
readonly value: string
|
|
46
|
+
readonly span: Span
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A recovered parse error embedded in the tree at a recovery point (tolerant
|
|
51
|
+
* parsing). Spans exactly the skipped text; `expected` is what the grammar wanted
|
|
52
|
+
* there. Identical shape to the runtime `ParseError`, so recovery emits ONE value
|
|
53
|
+
* into both the CST `children` and the flat `ctx._errors` channel. A tree walk
|
|
54
|
+
* finds every diagnostic, and — because it rides in the tree — an incremental
|
|
55
|
+
* reuse keeps it attached to the untouched subtree it belongs to.
|
|
56
|
+
*/
|
|
57
|
+
export type CSTError = {
|
|
58
|
+
readonly _tag: 'parseError'
|
|
59
|
+
readonly span: Span
|
|
60
|
+
readonly expected: string[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type CSTChild = CSTNode | CSTLeaf | CSTError
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A trivia token — whitespace or comment consumed between terms during parsing.
|
|
67
|
+
* Only present in `rawChildren`; never in the structural `children` array.
|
|
68
|
+
*/
|
|
69
|
+
export type CSTTrivia = {
|
|
70
|
+
readonly _tag: 'trivia'
|
|
71
|
+
readonly value: string
|
|
72
|
+
readonly span: Span
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Full child union including trivia — used in the `rawChildren` arg of `buildNode`. */
|
|
76
|
+
export type CSTRawChild = CSTNode | CSTLeaf | CSTTrivia | CSTError
|
package/src/cst/walk.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grammar-aware CST traversal. `createVisitor(grammar, handlers)` consumes the
|
|
3
|
+
* reflection stamped on interpreted `rules()` maps and macro/compiled grammar
|
|
4
|
+
* maps, precomputes tag dispatch once, and returns a reusable `visit(root)` fn.
|
|
5
|
+
*/
|
|
6
|
+
import type { CSTChild, CSTNode } from './types.ts'
|
|
7
|
+
import type { Combinator } from '../types.ts'
|
|
8
|
+
import {
|
|
9
|
+
collectGrammarReflection,
|
|
10
|
+
grammarReflectionOf,
|
|
11
|
+
type GrammarWithReflection,
|
|
12
|
+
} from './reflection.ts'
|
|
13
|
+
|
|
14
|
+
export type Walkable = {
|
|
15
|
+
readonly _tag: string
|
|
16
|
+
readonly type?: string
|
|
17
|
+
readonly children?: ReadonlyArray<Walkable>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type GrammarNodeType<G> =
|
|
21
|
+
G extends GrammarWithReflection<infer NodeType, string>
|
|
22
|
+
? NodeType
|
|
23
|
+
: G extends Record<string, unknown>
|
|
24
|
+
? Extract<keyof G, string>
|
|
25
|
+
: string
|
|
26
|
+
|
|
27
|
+
type GrammarNodeTag<G> =
|
|
28
|
+
G extends GrammarWithReflection<string, infer NodeTag> ? NodeTag : never
|
|
29
|
+
|
|
30
|
+
type NodeForType<N extends Walkable, Type extends string> =
|
|
31
|
+
Extract<N, { readonly _tag: 'node' }> extends never
|
|
32
|
+
? CSTNode & { readonly type: Type }
|
|
33
|
+
: Extract<N, { readonly _tag: 'node' }> & { readonly type: Type }
|
|
34
|
+
|
|
35
|
+
export type VisitorHandler<N extends Walkable, Root extends Walkable = CSTChild, C = undefined> =
|
|
36
|
+
(node: N, parent: Root | null, ctx: C) => void
|
|
37
|
+
|
|
38
|
+
export type VisitorSpec<G, N extends Walkable = CSTChild, C = undefined> = {
|
|
39
|
+
enter?(node: N, parent: N | null, ctx: C): boolean | void
|
|
40
|
+
leave?(node: N, parent: N | null, ctx: C): void
|
|
41
|
+
type?: {
|
|
42
|
+
[K in GrammarNodeType<G>]?: VisitorHandler<NodeForType<N, K>, N, C>
|
|
43
|
+
}
|
|
44
|
+
tag?: {
|
|
45
|
+
[K in GrammarNodeTag<G>]?: VisitorHandler<NodeForType<N, GrammarNodeType<G>>, N, C>
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isCombinator(value: unknown): value is Combinator<unknown> {
|
|
50
|
+
return !!value && typeof value === 'object'
|
|
51
|
+
&& '_def' in value
|
|
52
|
+
&& '_meta' in value
|
|
53
|
+
&& typeof (value as { parse?: unknown }).parse === 'function'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function reflectionFor(grammar: object) {
|
|
57
|
+
const stamped = grammarReflectionOf(grammar)
|
|
58
|
+
if (stamped) return stamped
|
|
59
|
+
const entries = Object.entries(grammar).filter((entry): entry is [string, Combinator<unknown>] => isCombinator(entry[1]))
|
|
60
|
+
return collectGrammarReflection(entries)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createVisitor<G extends object, N extends Walkable = CSTChild, C = undefined>(
|
|
64
|
+
grammar: G,
|
|
65
|
+
spec: VisitorSpec<G, N, C>,
|
|
66
|
+
): (root: N, ctx?: C) => void {
|
|
67
|
+
const reflection = reflectionFor(grammar)
|
|
68
|
+
const typeHandlers = spec.type as Record<string, VisitorHandler<N, N, C> | undefined> | undefined
|
|
69
|
+
const tagSpec = spec.tag as Record<string, VisitorHandler<N, N, C> | undefined> | undefined
|
|
70
|
+
const tagHandlersByType = new Map<string, Array<VisitorHandler<N, N, C>>>()
|
|
71
|
+
|
|
72
|
+
if (tagSpec !== undefined) {
|
|
73
|
+
for (const node of reflection.nodes) {
|
|
74
|
+
const handlers: Array<VisitorHandler<N, N, C>> = []
|
|
75
|
+
for (const tag of node.tags) {
|
|
76
|
+
const handler = tagSpec[tag]
|
|
77
|
+
if (handler) handlers.push(handler)
|
|
78
|
+
}
|
|
79
|
+
if (handlers.length > 0) tagHandlersByType.set(node.type, handlers)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (root: N, ctx: C = undefined as C): void => {
|
|
84
|
+
const go = (node: N, parent: N | null): void => {
|
|
85
|
+
const descend = spec.enter ? spec.enter(node, parent, ctx) : undefined
|
|
86
|
+
if (node._tag === 'node' && node.type !== undefined) {
|
|
87
|
+
typeHandlers?.[node.type]?.(node, parent, ctx)
|
|
88
|
+
const tagHandlers = tagHandlersByType.get(node.type)
|
|
89
|
+
if (tagHandlers !== undefined) {
|
|
90
|
+
for (const handler of tagHandlers) handler(node, parent, ctx)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const children = node.children as ReadonlyArray<N> | undefined
|
|
94
|
+
if (descend !== false && Array.isArray(children)) {
|
|
95
|
+
for (const child of children) go(child, node)
|
|
96
|
+
}
|
|
97
|
+
spec.leave?.(node, parent, ctx)
|
|
98
|
+
}
|
|
99
|
+
go(root, null)
|
|
100
|
+
}
|
|
101
|
+
}
|