parseman 0.46.0 → 0.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +10339 -7787
- package/dist/analysis/diagnostics.cjs.map +3 -4
- package/dist/analysis/diagnostics.d.ts +2 -2
- package/dist/analysis/diagnostics.js +10339 -7787
- 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 +10924 -8506
- package/dist/cli/index.cjs.map +3 -4
- package/dist/cli/index.js +10924 -8506
- 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 +45 -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 +9290 -7181
- 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 +9300 -7191
- package/dist/index.js.map +3 -4
- package/dist/language-service/index.cjs +313 -121
- package/dist/language-service/index.cjs.map +3 -4
- package/dist/language-service/index.js +313 -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 +11175 -9534
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11175 -9534
- 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 +2934 -1611
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2934 -1611
- 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 +11463 -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 +11440 -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 +453 -0
- package/dist/table/ops.d.ts.map +1 -0
- package/dist/table/program.d.ts +496 -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 +26 -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 +348 -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 +237 -0
- package/src/combinators/token.ts +120 -0
- package/src/combinators/trivia-boundary.ts +48 -0
- package/src/combinators/trivia-skip.ts +852 -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 +3006 -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 +1777 -0
- package/src/table/emit.ts +429 -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 +1680 -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 +467 -0
- package/src/table/program.ts +876 -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,959 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental parse document — incremental re-parse over a rules() registry.
|
|
3
|
+
*/
|
|
4
|
+
import type { Combinator, ParseContext, ParseError, ParseResult, ParserDef, Span } from '../types.ts'
|
|
5
|
+
import type { NodeLike, CSTLeaf, CSTError } from '../cst/types.ts'
|
|
6
|
+
import { relativizeCST, absoluteSpanCST } from '../cst/relative-spans.ts'
|
|
7
|
+
import { REC } from '../recovery/scan.ts'
|
|
8
|
+
import { createParseContext } from '../parse-context.ts'
|
|
9
|
+
import { fusedHostModeOf, fusedHostElidedOf } from '../compiler/linker.ts'
|
|
10
|
+
import { assertHostModeCompatible } from '../cst/host-mode.ts'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build the parse ctx for a (re)parse. In `tolerant` mode the same recovery bundle
|
|
14
|
+
* the interpreter/`run` use is installed (`_tolerant` + `_rec`), so a broken edit
|
|
15
|
+
* keeps producing a tree (with recovered `ParseError`s embedded) instead of
|
|
16
|
+
* collapsing to `null`. Strict mode is byte-identical to before. Every reparse site
|
|
17
|
+
* — root, localized rule reparse, list-splice middle, and the soundness probes —
|
|
18
|
+
* threads the SAME flag, so a probe's tolerance matches the parse it is checking
|
|
19
|
+
* (a mismatch would spuriously diverge and forgo reuse, never miscompare).
|
|
20
|
+
*/
|
|
21
|
+
function mkCtx(rawState: unknown, build: ParseContext['build'], tolerant: boolean): ParseContext {
|
|
22
|
+
// A CST node stores `state ?? null`, so "unset" comes back as `null`. Feeding that
|
|
23
|
+
// null straight back in makes a REPARSE diverge from a fresh parse: the state-clone
|
|
24
|
+
// guard tests `!== undefined`, and `Object.assign({}, null)` is `{}` — so a reused
|
|
25
|
+
// subtree carries `state: {}` where a fresh parse carries `state: null`. Normalize the
|
|
26
|
+
// round-trip back to "unset" at this single seeding site.
|
|
27
|
+
const state = rawState ?? undefined
|
|
28
|
+
return tolerant
|
|
29
|
+
? { trackLines: false, state, build, _tolerant: true, _rec: REC, _errors: [] }
|
|
30
|
+
: { trackLines: false, state, build }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A single compiled (or interpreted) rule: parse from `pos`, producing node `N`. */
|
|
34
|
+
export type RuleFn<N> = (input: string, pos: number, ctx: ParseContext) => ParseResult<N>
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Rule name → parser. Each entry is either a bare parse function or a
|
|
38
|
+
* `Combinator` (what `rules()` returns). Passing the `rules()` combinators
|
|
39
|
+
* directly lets `.edit()` inspect the grammar — required for **sound** structural
|
|
40
|
+
* list-reuse (see `structuralReuse`): only a rule the grammar proves is a genuine
|
|
41
|
+
* repetition is ever spliced. A bare-function registry still parses correctly; it
|
|
42
|
+
* just can't be structurally reused (the splice is skipped, never guessed).
|
|
43
|
+
*/
|
|
44
|
+
export type Registry<N> = Record<string, RuleFn<N> | Combinator<N>>
|
|
45
|
+
|
|
46
|
+
/** Normalize a registry entry to a callable parse function. */
|
|
47
|
+
function asRuleFn<N>(entry: RuleFn<N> | Combinator<N>): RuleFn<N> {
|
|
48
|
+
return typeof entry === 'function' ? entry : (input, pos, ctx) => entry.parse(input, pos, ctx)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The combinator def behind a registry entry, if it carries one (bare functions don't). */
|
|
52
|
+
function defOf(entry: unknown): ParserDef | undefined {
|
|
53
|
+
return typeof entry === 'object' && entry !== null && '_def' in entry
|
|
54
|
+
? (entry as { _def: ParserDef })._def
|
|
55
|
+
: undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* What a rule's grammar proves about how it produces its element children.
|
|
60
|
+
* `{ kind: 'sep', text }` — a `sepBy`/`oneOrMoreSep` whose separator is a plain
|
|
61
|
+
* literal, so the exact delimiter bytes are recoverable FROM THE GRAMMAR.
|
|
62
|
+
* `{ kind: 'bare' }` — a `many`/`oneOrMore` with no separator at all.
|
|
63
|
+
*/
|
|
64
|
+
type Repetition = { kind: 'sep'; text: string } | { kind: 'bare' }
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The literal TEXT a separator parser is fixed to, or `null` when it isn't fixed.
|
|
68
|
+
* Only a case-sensitive `literal()` (behind transparent wrappers) qualifies: its
|
|
69
|
+
* construction pins the exact bytes. A `choice`, a `regex`, a `keywords`, or a
|
|
70
|
+
* rule reference could have matched more than one thing, so its text is NOT
|
|
71
|
+
* recoverable from the grammar and callers must decline reuse rather than guess.
|
|
72
|
+
*/
|
|
73
|
+
function literalTextOf(parser: unknown, depth = 0): string | null {
|
|
74
|
+
const def = defOf(parser)
|
|
75
|
+
if (!def || depth > 24) return null
|
|
76
|
+
const d = def as ParserDef & Record<string, unknown>
|
|
77
|
+
switch (d.tag) {
|
|
78
|
+
case 'literal':
|
|
79
|
+
return d.caseInsensitive ? null : (d.value as string)
|
|
80
|
+
case 'lazy': {
|
|
81
|
+
const thunk = d.thunk as (() => unknown) | undefined
|
|
82
|
+
try { return typeof thunk === 'function' ? literalTextOf(thunk(), depth + 1) : null } catch { return null }
|
|
83
|
+
}
|
|
84
|
+
case 'node':
|
|
85
|
+
case 'transform':
|
|
86
|
+
case 'attempt':
|
|
87
|
+
case 'token':
|
|
88
|
+
case 'leaf':
|
|
89
|
+
case 'label':
|
|
90
|
+
case 'field':
|
|
91
|
+
case 'expect':
|
|
92
|
+
case 'withCtx':
|
|
93
|
+
case 'grammar':
|
|
94
|
+
return literalTextOf(d.parser, depth + 1)
|
|
95
|
+
default:
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Does this rule's grammar produce its element children via a genuine, unbounded
|
|
102
|
+
* **repetition** (`sepBy` / `many` / `oneOrMore`) — as opposed to a fixed-arity
|
|
103
|
+
* sequence of same-typed tokens (e.g. `Triple = Num ',' Num ',' Num`)? Only the
|
|
104
|
+
* former is sound to structurally reuse: a full reparse accepts any element count,
|
|
105
|
+
* so splicing one in or out matches it; a fixed-arity rule would not. We walk the
|
|
106
|
+
* def, transparently unwrapping semantic wrappers (`node`, `transform`, …) and
|
|
107
|
+
* looking through a top-level `sequence` / `optional` (the `[ (sepBy)? ]` shape),
|
|
108
|
+
* and return the repetition's shape iff one is reachable that way. Structurally
|
|
109
|
+
* indistinguishable-from-the-CST cases (fixed sequences) return `null` and fall
|
|
110
|
+
* back to a full, correct reparse. This is what makes `structuralReuse` sound
|
|
111
|
+
* rather than a promise the caller has to keep.
|
|
112
|
+
*
|
|
113
|
+
* The SEPARATOR comes from here, not from the CST: a `sepBy` contributes only its
|
|
114
|
+
* ITEMS to `children`, so there is no delimiter leaf between two elements to sniff.
|
|
115
|
+
* A combinator may collapse only what its construction makes recoverable — so a
|
|
116
|
+
* `sepBy` whose separator is a literal yields its text, and a `sepBy` whose
|
|
117
|
+
* separator is anything else (or one the author opted out of collapsing with
|
|
118
|
+
* `keepSeparator`) yields `null`: no structural reuse, full correct reparse.
|
|
119
|
+
*/
|
|
120
|
+
function producesRepetition(def: ParserDef | undefined, depth = 0): Repetition | null {
|
|
121
|
+
if (!def || depth > 24) return null
|
|
122
|
+
const d = def as ParserDef & Record<string, unknown>
|
|
123
|
+
switch (d.tag) {
|
|
124
|
+
case 'sepBy': {
|
|
125
|
+
if (d.keepSeparators) return null // separators ride `children`; not the items-only shape we splice
|
|
126
|
+
const text = literalTextOf(d.separator)
|
|
127
|
+
return text === null || text.length === 0 ? null : { kind: 'sep', text }
|
|
128
|
+
}
|
|
129
|
+
case 'many':
|
|
130
|
+
case 'oneOrMore':
|
|
131
|
+
return { kind: 'bare' }
|
|
132
|
+
// Rule entries and `ref`s wrap their body in a `lazy` thunk — resolve it. The
|
|
133
|
+
// `depth` cap bounds any self-referential cycle (a repetition, if present, is
|
|
134
|
+
// found shallowly before recursion goes deep).
|
|
135
|
+
case 'lazy': {
|
|
136
|
+
const thunk = d.thunk as (() => { _def?: ParserDef }) | undefined
|
|
137
|
+
let inner: { _def?: ParserDef } | undefined
|
|
138
|
+
try { inner = typeof thunk === 'function' ? thunk() : undefined } catch { return null }
|
|
139
|
+
return producesRepetition(inner?._def, depth + 1)
|
|
140
|
+
}
|
|
141
|
+
// Transparent wrappers — look through to the inner parser.
|
|
142
|
+
case 'node':
|
|
143
|
+
case 'transform':
|
|
144
|
+
case 'attempt':
|
|
145
|
+
case 'token':
|
|
146
|
+
case 'leaf':
|
|
147
|
+
case 'label':
|
|
148
|
+
case 'field':
|
|
149
|
+
case 'expect':
|
|
150
|
+
case 'withCtx':
|
|
151
|
+
case 'grammar':
|
|
152
|
+
case 'optional':
|
|
153
|
+
return producesRepetition(defOf(d.parser), depth + 1)
|
|
154
|
+
// A bracketed/anchored list is a sequence whose element run is a repetition.
|
|
155
|
+
case 'sequence': {
|
|
156
|
+
for (const p of d.parsers as unknown[]) {
|
|
157
|
+
const rep = producesRepetition(defOf(p), depth + 1)
|
|
158
|
+
if (rep) return rep
|
|
159
|
+
}
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
default:
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export type ParseDocOptions<N extends NodeLike> = {
|
|
168
|
+
/** Initial grammar state threaded into ctx.state for the root parse. */
|
|
169
|
+
state?: unknown
|
|
170
|
+
/**
|
|
171
|
+
* Reconstruct a parent node with one child replaced (used when grafting a
|
|
172
|
+
* re-parsed subtree into its ancestors). Defaults to a shallow spread, which
|
|
173
|
+
* works for plain-object nodes; class-instance ASTs should supply their own.
|
|
174
|
+
*/
|
|
175
|
+
rebuild?: (node: N, children: ReadonlyArray<N | CSTLeaf | CSTError>) => N
|
|
176
|
+
/**
|
|
177
|
+
* Mode host for a linkable/fused grammar (RULE_ABI_PLAN §7): threaded into
|
|
178
|
+
* `ctx.build` on every (re)parse so `node()` rules build a positioned CST /
|
|
179
|
+
* language-service tree instead of their own eval-AST. Unset → the grammar's
|
|
180
|
+
* own builders (eval mode).
|
|
181
|
+
*/
|
|
182
|
+
build?: ParseContext['build']
|
|
183
|
+
/**
|
|
184
|
+
* Enable structural list-reuse: on a length-changing *structural* edit (adding
|
|
185
|
+
* or removing a whole element in a collection) that would otherwise force a full
|
|
186
|
+
* reparse, reparse only the disturbed span and reuse the collection's untouched
|
|
187
|
+
* tail elements by identity — turning an insert near the top of a large list from
|
|
188
|
+
* O(list) into O(edit + trailing siblings).
|
|
189
|
+
*
|
|
190
|
+
* OFF by default because it is sound only when a rule whose CST children form a
|
|
191
|
+
* homogeneous, separator-delimited element list is a genuine REPETITION
|
|
192
|
+
* (`many` / `sepBy` / `oneOrMore`), not a fixed-arity sequence of same-typed
|
|
193
|
+
* tokens (e.g. `Triple = Num ',' Num ',' Num`) — the two are structurally
|
|
194
|
+
* indistinguishable without the grammar, and splicing the latter would accept an
|
|
195
|
+
* element count it shouldn't. Turn it on when your list rules are true
|
|
196
|
+
* repetitions (the common case: JSON arrays/objects, CSS value lists, argument
|
|
197
|
+
* lists). Every splice is still guarded (exact tiling + lookahead probe +
|
|
198
|
+
* stateless-tail check) and falls back to a full, correct reparse when unproven;
|
|
199
|
+
* the flag only authorises *attempting* the reuse.
|
|
200
|
+
*/
|
|
201
|
+
structuralReuse?: boolean
|
|
202
|
+
/**
|
|
203
|
+
* Parse tolerantly: `many`/`sepBy`/`oneOrMore` recover from a failed element
|
|
204
|
+
* (skip to an inferred sync point, embed a `ParseError` over the skipped span),
|
|
205
|
+
* so a broken edit keeps producing a tree instead of collapsing to `null` — the
|
|
206
|
+
* editor-backend path. Off by default (strict, byte-identical). Recovery is a cold
|
|
207
|
+
* path: well-formed input never triggers it. Every reparse the doc does — root,
|
|
208
|
+
* localized rule, and list-splice — inherits this flag, and the reuse-soundness
|
|
209
|
+
* probes run at the same tolerance so incremental reuse stays valid under recovery
|
|
210
|
+
* (a scan that would cross a splice boundary just falls back to a full reparse).
|
|
211
|
+
*/
|
|
212
|
+
tolerant?: boolean
|
|
213
|
+
/**
|
|
214
|
+
* The grammar's trivia rule, used ONLY to compute `unconsumedFrom`: a root rule
|
|
215
|
+
* consumes trivia BETWEEN terms but not after the last, so trailing
|
|
216
|
+
* whitespace/comments would otherwise read as leftover input. Given the trivia
|
|
217
|
+
* rule, the tail is skipped before reporting the first unconsumed offset —
|
|
218
|
+
* matching `run()`'s semantics. Defaults to the root rule's own
|
|
219
|
+
* `_meta.grammarTrivia` (from `rules({ trivia })`); set it only to override.
|
|
220
|
+
*/
|
|
221
|
+
trivia?: Combinator<unknown>
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface ParseDoc<N extends NodeLike> {
|
|
225
|
+
/**
|
|
226
|
+
* The parse tree with PARENT-RELATIVE spans — each node's `span` is relative to
|
|
227
|
+
* its parent's start (root base 0). This is the shareable representation: a
|
|
228
|
+
* length-changing `.edit()` keeps every untouched subtree shared by identity,
|
|
229
|
+
* and reading the tree is O(1) (no offset rewrite). For absolute positions use
|
|
230
|
+
* the O(depth) cursor `spanAt(path)`, or `absolutizeCST(doc.tree)` to
|
|
231
|
+
* materialize the whole absolute tree. A fresh non-incremental `node().parse()`
|
|
232
|
+
* result is unchanged — still absolute.
|
|
233
|
+
*/
|
|
234
|
+
readonly tree: N | null
|
|
235
|
+
/**
|
|
236
|
+
* Recovery diagnostics collected during the (re)parse — the missing-token
|
|
237
|
+
* `expect()` errors and tolerant-list recovery errors that ride the tree as
|
|
238
|
+
* `parseError` nodes, surfaced here as a flat list too (spans ABSOLUTE). Empty
|
|
239
|
+
* in strict mode. On a hard (non-recovered) parse failure this holds the single
|
|
240
|
+
* top-level failure. This is what makes an editor document able to see syntax
|
|
241
|
+
* errors — a blank `errors: []` (the prior behaviour) hid every recovery.
|
|
242
|
+
*/
|
|
243
|
+
readonly errors: ParseError[]
|
|
244
|
+
/**
|
|
245
|
+
* Offset where unparsed input begins — the first non-trivia character the parse
|
|
246
|
+
* left unconsumed (trailing trivia skipped when a trivia rule is available), or
|
|
247
|
+
* `null` if the whole input was consumed. This is how a document detects "the
|
|
248
|
+
* grammar stopped short, there's junk here"; computed exactly as `run()` does.
|
|
249
|
+
*/
|
|
250
|
+
readonly unconsumedFrom: number | null
|
|
251
|
+
readonly input: string
|
|
252
|
+
/**
|
|
253
|
+
* Absolute span of the node at `path` (child indices from the root) — O(depth),
|
|
254
|
+
* without materializing the absolute tree. The projection cursor for the
|
|
255
|
+
* relative representation; use it for spot queries on a large incremental doc.
|
|
256
|
+
*/
|
|
257
|
+
spanAt(path: readonly number[]): { start: number; end: number }
|
|
258
|
+
/**
|
|
259
|
+
* Incrementally re-parse after a text change. `from`/`to` are byte offsets in
|
|
260
|
+
* the OLD input; `replacement` fills that range (editor change-event shape).
|
|
261
|
+
* Sound: the result tree is always structurally identical to a fresh
|
|
262
|
+
* `parseDoc` of the edited text (the Stage-2 guard falls back to a full
|
|
263
|
+
* reparse whenever reuse can't be proven safe). Reuse/strategy is intentionally
|
|
264
|
+
* NOT reported here — an observer derives it by diffing this tree against the
|
|
265
|
+
* previous one (see the incremental tests); the runtime's job is to be fast,
|
|
266
|
+
* not to measure itself.
|
|
267
|
+
*/
|
|
268
|
+
edit(from: number, to: number, replacement: string): ParseDoc<N>
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// Tree navigation (generic over NodeLike — no class, no CST assumptions)
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* A reentry candidate is only worth reparsing if it's meaningfully smaller than
|
|
277
|
+
* the whole document — reparsing a rule that spans (say) >half the input costs
|
|
278
|
+
* about as much as a full reparse with none of the reuse. Above this fraction of
|
|
279
|
+
* the input length, `.edit()` skips straight to a full reparse. Keeps the worst
|
|
280
|
+
* case (a structural edit near the front) at ~1× full reparse, never several.
|
|
281
|
+
*/
|
|
282
|
+
const REENTRY_MAX_SPAN_FRACTION = 0.5
|
|
283
|
+
|
|
284
|
+
type FoundNode<N extends NodeLike> = { node: N; path: number[] }
|
|
285
|
+
|
|
286
|
+
function isNode(x: unknown): x is NodeLike {
|
|
287
|
+
return typeof x === 'object' && x !== null && (x as { _tag?: string })._tag === 'node'
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Locate the deepest node containing absolute offset `pos`. The tree stores
|
|
292
|
+
* PARENT-RELATIVE spans (see relative-spans), so we thread each node's absolute
|
|
293
|
+
* start down the descent: a child's absolute start is `nodeAbsStart +
|
|
294
|
+
* child.span.start`.
|
|
295
|
+
*/
|
|
296
|
+
function findContaining<N extends NodeLike>(
|
|
297
|
+
node: N,
|
|
298
|
+
nodeAbsStart: number,
|
|
299
|
+
pos: number,
|
|
300
|
+
path: number[] = [],
|
|
301
|
+
): FoundNode<N> | null {
|
|
302
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
303
|
+
const child = node.children[i]!
|
|
304
|
+
if (!isNode(child)) continue
|
|
305
|
+
const childAbsStart = nodeAbsStart + child.span.start
|
|
306
|
+
const childAbsEnd = nodeAbsStart + child.span.end
|
|
307
|
+
if (childAbsStart <= pos && pos < childAbsEnd) {
|
|
308
|
+
const inner = findContaining(child as N, childAbsStart, pos, [...path, i])
|
|
309
|
+
return inner ?? { node: child as N, path: [...path, i] }
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return null
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function ancestorsAt<N extends NodeLike>(root: N, path: number[]): N[] {
|
|
316
|
+
const ancestors: N[] = [root]
|
|
317
|
+
let cur: N = root
|
|
318
|
+
for (const idx of path.slice(0, -1)) {
|
|
319
|
+
const child = cur.children[idx]
|
|
320
|
+
if (!child || !isNode(child)) break
|
|
321
|
+
ancestors.push(child as N)
|
|
322
|
+
cur = child as N
|
|
323
|
+
}
|
|
324
|
+
return ancestors
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function replaceAtPath<N extends NodeLike>(
|
|
328
|
+
rebuild: NonNullable<ParseDocOptions<N>['rebuild']>,
|
|
329
|
+
root: N,
|
|
330
|
+
path: number[],
|
|
331
|
+
newNode: N,
|
|
332
|
+
): N {
|
|
333
|
+
if (path.length === 0) return newNode
|
|
334
|
+
const [idx, ...rest] = path as [number, ...number[]]
|
|
335
|
+
const newChildren = [...root.children] as Array<N | CSTLeaf | CSTError>
|
|
336
|
+
newChildren[idx] = rest.length === 0
|
|
337
|
+
? newNode
|
|
338
|
+
: replaceAtPath(rebuild, root.children[idx] as N, rest, newNode)
|
|
339
|
+
return rebuild(root, newChildren)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function defaultRebuild<N extends NodeLike>(node: N, children: ReadonlyArray<N | CSTLeaf | CSTError>): N {
|
|
343
|
+
return { ...node, children } as N
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
type SpanChild = { span: { start: number; end: number }; children?: readonly unknown[] }
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Shift a node's PARENT-RELATIVE span by `delta`. Used for siblings that sit
|
|
350
|
+
* *after* a length-changing edit inside the same parent: the node moved as a
|
|
351
|
+
* unit, so both endpoints slide by `delta`, but its own children are relative to
|
|
352
|
+
* *its* start and are unchanged — so `children` is shared by identity. That's the
|
|
353
|
+
* relative-span win: O(1) per trailing sibling, not O(subtree). (Contrast the old
|
|
354
|
+
* absolute model, which had to deep-rewrite every descendant.)
|
|
355
|
+
*/
|
|
356
|
+
function shiftRelStart<T>(child: T, delta: number): T {
|
|
357
|
+
const c = child as unknown as SpanChild
|
|
358
|
+
return { ...(c as object), span: { start: c.span.start + delta, end: c.span.end + delta } } as unknown as T
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Graft `newNode` at `path` into a parent-relative tree for a length-changing
|
|
363
|
+
* edit (`delta !== 0`): children before the edit are shared by reference, the
|
|
364
|
+
* edited child is replaced, children *after* it have their relative start slid by
|
|
365
|
+
* `delta` (one shallow alloc each — their subtrees are shared by identity), and
|
|
366
|
+
* each ancestor's relative `span.end` grows by `delta` while its start is
|
|
367
|
+
* untouched. Cost is O(depth + trailing siblings along the spine), independent of
|
|
368
|
+
* how many nodes sit inside those trailing siblings.
|
|
369
|
+
*/
|
|
370
|
+
function graftRelative<N extends NodeLike>(root: N, path: number[], newNode: N, delta: number): N {
|
|
371
|
+
if (path.length === 0) return newNode
|
|
372
|
+
const [idx, ...rest] = path as [number, ...number[]]
|
|
373
|
+
const oldChildren = root.children as ReadonlyArray<N | CSTLeaf | CSTError>
|
|
374
|
+
const newChildren = oldChildren.map((child, i) => {
|
|
375
|
+
if (i < idx) return child
|
|
376
|
+
if (i === idx) {
|
|
377
|
+
return rest.length === 0 ? newNode : graftRelative(child as N, rest, newNode, delta)
|
|
378
|
+
}
|
|
379
|
+
return shiftRelStart(child, delta)
|
|
380
|
+
})
|
|
381
|
+
return {
|
|
382
|
+
...root,
|
|
383
|
+
span: { start: root.span.start, end: root.span.end + delta },
|
|
384
|
+
children: newChildren,
|
|
385
|
+
} as N
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// List-splice reuse (structural edits in a collection)
|
|
390
|
+
//
|
|
391
|
+
// A structural edit — inserting or deleting a whole element in a list — makes the
|
|
392
|
+
// innermost-rule reparse fail to converge, and the containing collection is often
|
|
393
|
+
// too big to reparse (that's the "insert at the top of a large array" case where
|
|
394
|
+
// a whole-rule reparse costs ~a full reparse). Instead of reparsing the whole
|
|
395
|
+
// collection, reparse ONLY the disturbed span between the last untouched element
|
|
396
|
+
// before the edit and the first untouched element after it, then splice: the head
|
|
397
|
+
// children are shared as-is, the freshly-parsed middle replaces the changed run,
|
|
398
|
+
// and the tail children are reused with their relative start slid by `delta`
|
|
399
|
+
// (O(1) each — their subtrees are shared by identity, the relative-span win).
|
|
400
|
+
//
|
|
401
|
+
// Soundness rests on: (1) the reparsed middle exactly fills [reStart, reEnd) and
|
|
402
|
+
// lands on element/separator boundaries; (2) a lookahead probe proving the middle
|
|
403
|
+
// read nothing at/after the splice point (else it peeked into the reused tail);
|
|
404
|
+
// (3) a stateless-reentry precondition on the reused tail (its saved `state` is
|
|
405
|
+
// null), so a forward, context-free grammar reproduces those subtrees unchanged.
|
|
406
|
+
// Any failure ⇒ `null` ⇒ the caller falls back to a full, correct reparse. The
|
|
407
|
+
// incremental oracle fuzz (edit() ≡ full reparse, 400 seeds × 3 list grammars +
|
|
408
|
+
// edge cases) is the end-to-end correctness net.
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
type AnyChild = { readonly _tag: string; readonly span: { start: number; end: number }; readonly value?: string; readonly type?: string; readonly state?: unknown; readonly children?: readonly unknown[] }
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Parse the collection's disturbed middle `[reStart, reEnd)`, C-relative, and
|
|
415
|
+
* return ONLY its element children — a `sepBy` contributes items and nothing else
|
|
416
|
+
* to `children`, so the spliced middle must match what a full reparse produces:
|
|
417
|
+
* separators are matched and CONSUMED here, never emitted.
|
|
418
|
+
*
|
|
419
|
+
* The region has a uniform shape. `reStart` is the end of the last reused head
|
|
420
|
+
* child and `reEnd` the start of the first reused tail element, which is always an
|
|
421
|
+
* element. So when the head ended on an element NODE the region is `(sep elem)* sep`,
|
|
422
|
+
* and when it ended on a LEAF (the collection's open delimiter — a `sequence` term,
|
|
423
|
+
* still a child) or at the collection's own start the region is `elem (sep elem)* sep`.
|
|
424
|
+
* Either way it ENDS with a separator, which `startsAtElement` seeds and the final
|
|
425
|
+
* `expectElement` assertion enforces: the region must leave us expecting an element,
|
|
426
|
+
* i.e. a separator was just eaten. That is the junction guarantee — a splice can
|
|
427
|
+
* never fuse the middle's last element onto the tail's first with no delimiter
|
|
428
|
+
* between them, which a full reparse could never produce. (An EMPTY region is
|
|
429
|
+
* accepted exactly when the head already ended on a separator or the open
|
|
430
|
+
* delimiter, i.e. `startsAtElement` — the same invariant, vacuously.)
|
|
431
|
+
*
|
|
432
|
+
* Alternation is STRICT: element/element juxtaposition is rejected. When the
|
|
433
|
+
* grammar is separator-free (`separator === null`, a bare `many`/`oneOrMore`)
|
|
434
|
+
* consecutive elements are allowed and no junction constraint applies. Returns the
|
|
435
|
+
* C-relative element children, or `null` if the run doesn't tile `[reStart, reEnd)`
|
|
436
|
+
* exactly on token boundaries.
|
|
437
|
+
*/
|
|
438
|
+
function parseMiddle(
|
|
439
|
+
input: string,
|
|
440
|
+
reStart: number,
|
|
441
|
+
reEnd: number,
|
|
442
|
+
cStart: number,
|
|
443
|
+
ruleFn: RuleFn<NodeLike>,
|
|
444
|
+
separator: string | null,
|
|
445
|
+
startsAtElement: boolean,
|
|
446
|
+
build: ParseContext['build'],
|
|
447
|
+
tolerant: boolean,
|
|
448
|
+
): AnyChild[] | null {
|
|
449
|
+
const ctx: ParseContext = mkCtx(null, build, tolerant)
|
|
450
|
+
const out: AnyChild[] = []
|
|
451
|
+
let pos = reStart
|
|
452
|
+
let guard = 0
|
|
453
|
+
// For a separator-free repetition every position expects an element.
|
|
454
|
+
let expectElement = separator === null ? true : startsAtElement
|
|
455
|
+
while (pos < reEnd) {
|
|
456
|
+
if (++guard > reEnd - reStart + 2) return null // no-progress backstop
|
|
457
|
+
if (separator !== null && !expectElement) {
|
|
458
|
+
if (pos + separator.length > reEnd || !input.startsWith(separator, pos)) return null
|
|
459
|
+
pos += separator.length
|
|
460
|
+
expectElement = true
|
|
461
|
+
continue
|
|
462
|
+
}
|
|
463
|
+
let r: ParseResult<NodeLike>
|
|
464
|
+
try {
|
|
465
|
+
r = ruleFn(input, pos, ctx)
|
|
466
|
+
} catch {
|
|
467
|
+
return null
|
|
468
|
+
}
|
|
469
|
+
if (!r.ok || r.span.end <= pos || r.span.end > reEnd) return null
|
|
470
|
+
out.push(relativizeCST(r.value as unknown as AnyChild, cStart))
|
|
471
|
+
pos = r.span.end
|
|
472
|
+
if (separator !== null) expectElement = false
|
|
473
|
+
}
|
|
474
|
+
// Exact tiling + the junction guarantee (see above).
|
|
475
|
+
return pos === reEnd && expectElement ? out : null
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Try to reuse the untouched tail of collection `C` (at `path`, absolute start
|
|
480
|
+
* `cStart`) around a length-changing edit, reparsing only the disturbed middle.
|
|
481
|
+
* Returns the new relative root, or `null` to fall back to a full reparse.
|
|
482
|
+
*/
|
|
483
|
+
function tryListSplice<N extends NodeLike>(
|
|
484
|
+
root: N,
|
|
485
|
+
path: number[],
|
|
486
|
+
C: N,
|
|
487
|
+
cStart: number,
|
|
488
|
+
newInput: string,
|
|
489
|
+
from: number,
|
|
490
|
+
to: number,
|
|
491
|
+
delta: number,
|
|
492
|
+
rep: Repetition,
|
|
493
|
+
registry: Record<string, RuleFn<N>>,
|
|
494
|
+
build: ParseContext['build'],
|
|
495
|
+
tolerant: boolean,
|
|
496
|
+
): N | null {
|
|
497
|
+
const kids = C.children as ReadonlyArray<AnyChild>
|
|
498
|
+
if (kids.length === 0) return null
|
|
499
|
+
|
|
500
|
+
// head = maximal prefix ending before the edit. Its last child is either an
|
|
501
|
+
// element node (the region then opens on a separator) or a leaf / nothing (the
|
|
502
|
+
// region then opens on an element) — `parseMiddle` handles both, so no back-off
|
|
503
|
+
// is needed and the head stays as long as the edit allows.
|
|
504
|
+
let h = 0
|
|
505
|
+
while (h < kids.length && cStart + kids[h]!.span.end <= from) h++
|
|
506
|
+
// tail = maximal suffix starting after the edit, advanced so it BEGINS at an
|
|
507
|
+
// element (node) boundary — a self-contained element run to reuse.
|
|
508
|
+
let t = kids.length
|
|
509
|
+
while (t > h && cStart + kids[t - 1]!.span.start >= to) t--
|
|
510
|
+
while (t < kids.length && kids[t]!._tag !== 'node') t++
|
|
511
|
+
if (t >= kids.length) return null // nothing reusable after the edit
|
|
512
|
+
|
|
513
|
+
// Stateless-reentry precondition on the reused tail (see soundness note).
|
|
514
|
+
for (let i = t; i < kids.length; i++) {
|
|
515
|
+
if (kids[i]!._tag === 'node' && kids[i]!.state != null) return null
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// C must look like a genuine homogeneous COLLECTION — a repetition of one
|
|
519
|
+
// element rule joined by one separator — not a fixed heterogeneous sequence
|
|
520
|
+
// (e.g. `Pair = Key ':' Val`, whose `:` is not a list separator). Require: all
|
|
521
|
+
// element (node) children share a type, and there are ≥2 of them. This rejects
|
|
522
|
+
// fixed sequences (their nodes differ in type, or there's only one) so we never
|
|
523
|
+
// treat them as splice-able lists.
|
|
524
|
+
let elemType: string | undefined
|
|
525
|
+
let elemCount = 0
|
|
526
|
+
for (const k of kids) {
|
|
527
|
+
if (k._tag !== 'node') continue
|
|
528
|
+
elemCount++
|
|
529
|
+
if (elemType === undefined) elemType = k.type
|
|
530
|
+
else if (k.type !== elemType) return null // heterogeneous → not a collection
|
|
531
|
+
}
|
|
532
|
+
if (elemType === undefined || elemCount < 2) return null
|
|
533
|
+
const ruleFn = registry[elemType]
|
|
534
|
+
if (!ruleFn) return null
|
|
535
|
+
// The separator comes from the GRAMMAR, not from `children`: a `sepBy` contributes
|
|
536
|
+
// only its items, so there is no delimiter leaf between two elements to sniff.
|
|
537
|
+
// `producesRepetition` already refused any list whose separator its construction
|
|
538
|
+
// doesn't pin (a choice, a regex, a rule ref), so what arrives here is either a
|
|
539
|
+
// fixed literal or a genuinely separator-free repetition.
|
|
540
|
+
const separator = rep.kind === 'sep' ? rep.text : null
|
|
541
|
+
|
|
542
|
+
// The disturbed middle must be pure elements + separators. If the collection is
|
|
543
|
+
// bracketed, its OPENING / CLOSING delimiter (a leaf whose value isn't the
|
|
544
|
+
// separator, sitting before the first / after the last element) must lie OUTSIDE
|
|
545
|
+
// the edit — otherwise the edit changed the collection's own framing and a
|
|
546
|
+
// whole-rule reparse (not a splice) is required.
|
|
547
|
+
const first = kids[0]!
|
|
548
|
+
const last = kids[kids.length - 1]!
|
|
549
|
+
if (first._tag === 'leaf' && first.value !== separator && from < cStart + first.span.end) return null
|
|
550
|
+
if (last._tag === 'leaf' && last.value !== separator && to > cStart + last.span.start) return null
|
|
551
|
+
|
|
552
|
+
const reStart = h > 0 ? cStart + kids[h - 1]!.span.end : cStart
|
|
553
|
+
const reEnd = cStart + kids[t]!.span.start + delta
|
|
554
|
+
if (reEnd < reStart) return null
|
|
555
|
+
// The region opens on an element unless the head ended on one (then it opens on
|
|
556
|
+
// the separator that joined it to what follows). `parseMiddle` carries this
|
|
557
|
+
// through to the junction guarantee.
|
|
558
|
+
const startsAtElement = !(h > 0 && kids[h - 1]!._tag === 'node')
|
|
559
|
+
|
|
560
|
+
const middle = parseMiddle(newInput, reStart, reEnd, cStart, ruleFn as RuleFn<NodeLike>, separator, startsAtElement, build, tolerant)
|
|
561
|
+
if (!middle) return null
|
|
562
|
+
|
|
563
|
+
// Lookahead guard: the middle must have read nothing at/after `reEnd`, else it
|
|
564
|
+
// peeked into the reused tail. Re-run over an input whose tail is overwritten
|
|
565
|
+
// with a sentinel and require an identical middle. Two sentinels so the real
|
|
566
|
+
// byte at `reEnd` can't accidentally match the probe.
|
|
567
|
+
if (reEnd < newInput.length) {
|
|
568
|
+
for (const sentinel of [' ', '']) {
|
|
569
|
+
if (newInput[reEnd] === sentinel) continue
|
|
570
|
+
const probed = newInput.slice(0, reEnd) + sentinel.repeat(newInput.length - reEnd)
|
|
571
|
+
const probe = parseMiddle(probed, reStart, reEnd, cStart, ruleFn as RuleFn<NodeLike>, separator, startsAtElement, build, tolerant)
|
|
572
|
+
if (!probe || probe.length !== middle.length) return null
|
|
573
|
+
for (let i = 0; i < middle.length; i++) if (!structurallyEqual(probe[i], middle[i])) return null
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const head = kids.slice(0, h)
|
|
578
|
+
const tail = kids.slice(t).map((k) => shiftRelStart(k, delta))
|
|
579
|
+
const newC = {
|
|
580
|
+
...C,
|
|
581
|
+
span: { start: C.span.start, end: C.span.end + delta },
|
|
582
|
+
children: [...head, ...middle, ...tail],
|
|
583
|
+
} as N
|
|
584
|
+
|
|
585
|
+
// Replace C at `path` and slide C's own trailing siblings / ancestor ends.
|
|
586
|
+
return graftRelative(root, path, newC, delta)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ---------------------------------------------------------------------------
|
|
590
|
+
// Stage-2 soundness guard
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Deep structural equality on parse trees: `_tag`, `span`, node `type`, leaf
|
|
595
|
+
* `value`, and children pairwise. This is the oracle relation `.edit()` must
|
|
596
|
+
* preserve against a full reparse; it's also what the Stage-2 guard compares
|
|
597
|
+
* probe results with.
|
|
598
|
+
*/
|
|
599
|
+
export function structurallyEqual(a: unknown, b: unknown): boolean {
|
|
600
|
+
if (a === b) return true
|
|
601
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false
|
|
602
|
+
const at = (a as { _tag?: string })._tag
|
|
603
|
+
const bt = (b as { _tag?: string })._tag
|
|
604
|
+
if (at !== bt) return false
|
|
605
|
+
const as = (a as { span?: { start: number; end: number } }).span
|
|
606
|
+
const bs = (b as { span?: { start: number; end: number } }).span
|
|
607
|
+
if (as || bs) {
|
|
608
|
+
if (!as || !bs || as.start !== bs.start || as.end !== bs.end) return false
|
|
609
|
+
}
|
|
610
|
+
if (at === 'leaf' || at === 'trivia') {
|
|
611
|
+
return (a as { value: string }).value === (b as { value: string }).value
|
|
612
|
+
}
|
|
613
|
+
if (at === 'parseError') {
|
|
614
|
+
// Embedded recovered error: span already compared above; also compare the
|
|
615
|
+
// expected-token set so two errors at the same span but different expectations
|
|
616
|
+
// are not conflated (the oracle must distinguish them).
|
|
617
|
+
const ae = (a as { expected?: readonly string[] }).expected ?? []
|
|
618
|
+
const be = (b as { expected?: readonly string[] }).expected ?? []
|
|
619
|
+
return ae.length === be.length && ae.every((x, i) => x === be[i])
|
|
620
|
+
}
|
|
621
|
+
if ((a as { type?: string }).type !== (b as { type?: string }).type) return false
|
|
622
|
+
const ac = (a as { children?: readonly unknown[] }).children
|
|
623
|
+
const bc = (b as { children?: readonly unknown[] }).children
|
|
624
|
+
if (ac || bc) {
|
|
625
|
+
if (!ac || !bc || ac.length !== bc.length) return false
|
|
626
|
+
for (let i = 0; i < ac.length; i++) if (!structurallyEqual(ac[i], bc[i])) return false
|
|
627
|
+
}
|
|
628
|
+
return true
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* A reused suffix spliced at `boundary` (new-input coords) is sound only if the
|
|
633
|
+
* re-parse of the containing rule did NOT read any input at or after `boundary`
|
|
634
|
+
* — otherwise a lookahead or backtrack peeked across the splice and the reused
|
|
635
|
+
* tail could be wrong. We prove independence by re-running the same rule at the
|
|
636
|
+
* same start on an input whose entire tail from `boundary` is overwritten with a
|
|
637
|
+
* sentinel: if the produced node is byte-for-byte structurally identical,
|
|
638
|
+
* nothing past `boundary` was inspected. Two distinct sentinels are tried so the
|
|
639
|
+
* real char at `boundary` can't accidentally equal the probe. Conservative by
|
|
640
|
+
* construction — any probe difference, failure, or throw ⇒ not safe ⇒ the caller
|
|
641
|
+
* widens toward a full reparse (correctness over reuse fraction).
|
|
642
|
+
*/
|
|
643
|
+
function boundaryIsSafe<N extends NodeLike>(
|
|
644
|
+
ruleFn: RuleFn<N>,
|
|
645
|
+
newInput: string,
|
|
646
|
+
start: number,
|
|
647
|
+
boundary: number,
|
|
648
|
+
state: unknown,
|
|
649
|
+
build: ParseContext['build'],
|
|
650
|
+
produced: ParseResult<N>,
|
|
651
|
+
tolerant: boolean,
|
|
652
|
+
): boolean {
|
|
653
|
+
if (!produced.ok) return false
|
|
654
|
+
if (boundary >= newInput.length) return true // nothing after the node to peek at
|
|
655
|
+
for (const sentinel of [' ', '']) {
|
|
656
|
+
if (newInput[boundary] === sentinel) continue
|
|
657
|
+
const probed = newInput.slice(0, boundary) + sentinel.repeat(newInput.length - boundary)
|
|
658
|
+
const ctx: ParseContext = mkCtx(state, build, tolerant)
|
|
659
|
+
let r: ParseResult<N>
|
|
660
|
+
try {
|
|
661
|
+
r = ruleFn(probed, start, ctx)
|
|
662
|
+
} catch {
|
|
663
|
+
return false
|
|
664
|
+
}
|
|
665
|
+
if (!r.ok || r.span.end !== produced.span.end) return false
|
|
666
|
+
if (!structurallyEqual(r.value, produced.value)) return false
|
|
667
|
+
}
|
|
668
|
+
return true
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ---------------------------------------------------------------------------
|
|
672
|
+
// Document
|
|
673
|
+
// ---------------------------------------------------------------------------
|
|
674
|
+
|
|
675
|
+
class ParseDocImpl<N extends NodeLike> implements ParseDoc<N> {
|
|
676
|
+
private readonly _registry: Registry<N>
|
|
677
|
+
/** Registry entries normalized to callable parse functions (memoized once). */
|
|
678
|
+
private readonly _fns: Record<string, RuleFn<N>>
|
|
679
|
+
/**
|
|
680
|
+
* Rule names the grammar proves are genuine repetitions — the ONLY splice-safe
|
|
681
|
+
* types — mapped to the repetition shape (and, for a `sepBy`, the separator text
|
|
682
|
+
* its construction pins). A rule whose separator isn't recoverable is absent.
|
|
683
|
+
*/
|
|
684
|
+
private readonly _reps: Map<string, Repetition>
|
|
685
|
+
private readonly _rootRule: string
|
|
686
|
+
private readonly _opts: ParseDocOptions<N>
|
|
687
|
+
/**
|
|
688
|
+
* The tree, held in whichever coordinate system it arrived in. A fresh parse
|
|
689
|
+
* arrives ABSOLUTE (`_abs` set); an incremental graft arrives PARENT-RELATIVE
|
|
690
|
+
* (`_rel` set). The public `tree` is always the RELATIVE form (so untouched
|
|
691
|
+
* subtrees stay shared by identity across edits and reads are O(1)); it's
|
|
692
|
+
* materialized from `_abs` once, on demand, and memoized. `undefined` = not yet
|
|
693
|
+
* computed; `null` = failed parse. A fresh parse that's never edited nor
|
|
694
|
+
* tree-read pays no conversion — important for the full-reparse fallback path.
|
|
695
|
+
*/
|
|
696
|
+
private _abs: N | null | undefined
|
|
697
|
+
private _rel: N | null | undefined
|
|
698
|
+
readonly errors: ParseError[]
|
|
699
|
+
readonly unconsumedFrom: number | null
|
|
700
|
+
readonly input: string
|
|
701
|
+
|
|
702
|
+
constructor(
|
|
703
|
+
registry: Registry<N>,
|
|
704
|
+
rootRule: string,
|
|
705
|
+
opts: ParseDocOptions<N>,
|
|
706
|
+
trees: { abs?: N | null; rel?: N | null },
|
|
707
|
+
errors: ParseError[],
|
|
708
|
+
unconsumedFrom: number | null,
|
|
709
|
+
input: string,
|
|
710
|
+
) {
|
|
711
|
+
this._registry = registry
|
|
712
|
+
this._fns = {}
|
|
713
|
+
this._reps = new Map()
|
|
714
|
+
for (const [name, entry] of Object.entries(registry)) {
|
|
715
|
+
this._fns[name] = asRuleFn(entry)
|
|
716
|
+
const rep = producesRepetition(defOf(entry))
|
|
717
|
+
if (rep) this._reps.set(name, rep)
|
|
718
|
+
}
|
|
719
|
+
this._rootRule = rootRule
|
|
720
|
+
this._opts = opts
|
|
721
|
+
this._abs = trees.abs
|
|
722
|
+
this._rel = trees.rel
|
|
723
|
+
this.errors = errors
|
|
724
|
+
this.unconsumedFrom = unconsumedFrom
|
|
725
|
+
this.input = input
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* The parse tree with PARENT-RELATIVE spans — a node's `span` is relative to
|
|
730
|
+
* its parent's start (root base 0). This is the shareable representation:
|
|
731
|
+
* untouched subtrees keep the same identity across `.edit()`s, and reading the
|
|
732
|
+
* tree after a length-changing edit is O(1) (no offset rewrite). For absolute
|
|
733
|
+
* positions use the O(depth) cursor `spanAt(path)`, or `absolutizeCST(tree)` to
|
|
734
|
+
* materialize the whole absolute tree. (A fresh non-incremental `node().parse()`
|
|
735
|
+
* result is unchanged — still absolute.)
|
|
736
|
+
*/
|
|
737
|
+
get tree(): N | null {
|
|
738
|
+
if (this._rel === undefined) {
|
|
739
|
+
this._rel = this._abs ? (relativizeCST(this._abs as unknown as N & { span: Span }, 0) as unknown as N) : null
|
|
740
|
+
}
|
|
741
|
+
return this._rel
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
spanAt(path: readonly number[]): { start: number; end: number } {
|
|
745
|
+
const rel = this.tree
|
|
746
|
+
if (!rel) throw new Error('spanAt on a failed parse (tree is null)')
|
|
747
|
+
return absoluteSpanCST(rel as unknown as { span: Span; children?: readonly unknown[] }, path)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Wrap a reused (grafted/spliced) RELATIVE tree, recomputing `errors` and
|
|
752
|
+
* `unconsumedFrom` from it so both stay consistent with the tree across the
|
|
753
|
+
* edit (the flat errors are recovered errors embedded in the reused subtrees;
|
|
754
|
+
* unconsumedFrom re-derives trailing junk over the new input).
|
|
755
|
+
*/
|
|
756
|
+
private wrapReuse(newTree: N, newInput: string): ParseDoc<N> {
|
|
757
|
+
const trivia = triviaOf(this._registry[this._rootRule], this._opts)
|
|
758
|
+
const end = (newTree as unknown as { span: { end: number } }).span.end
|
|
759
|
+
const errors = collectEmbeddedErrors(newTree, 0, [])
|
|
760
|
+
const unconsumedFrom = unconsumedAfter(end, newInput, trivia)
|
|
761
|
+
return new ParseDocImpl(this._registry, this._rootRule, this._opts, { rel: newTree }, errors, unconsumedFrom, newInput)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
edit(from: number, to: number, replacement: string): ParseDoc<N> {
|
|
765
|
+
const newInput = this.input.slice(0, from) + replacement + this.input.slice(to)
|
|
766
|
+
const reparse = () => parseDoc(this._registry, this._rootRule, newInput, this._opts)
|
|
767
|
+
|
|
768
|
+
const root = this.tree
|
|
769
|
+
if (!root) return reparse()
|
|
770
|
+
|
|
771
|
+
const delta = replacement.length - (to - from)
|
|
772
|
+
// When `from` sits on the root's own boundary (e.g. right after a top-level
|
|
773
|
+
// element, before its separator), no child node contains it and
|
|
774
|
+
// `findContaining` returns null — but the root still does. Fall back to the
|
|
775
|
+
// root as the container so the structural splice below still gets a shot
|
|
776
|
+
// (this is exactly the "insert/delete a top-level list element" position).
|
|
777
|
+
const found = findContaining(root, 0, from) ?? { node: root, path: [] as number[] }
|
|
778
|
+
|
|
779
|
+
// Try the innermost containing rule first, then widen outward.
|
|
780
|
+
const ancestors = ancestorsAt(root, found.path)
|
|
781
|
+
const candidates: FoundNode<N>[] = [found]
|
|
782
|
+
const pathCopy = [...found.path]
|
|
783
|
+
for (let i = ancestors.length - 2; i >= 0; i--) {
|
|
784
|
+
pathCopy.pop()
|
|
785
|
+
candidates.push({ node: ancestors[i + 1]!, path: [...pathCopy] })
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const rebuild = this._opts.rebuild ?? defaultRebuild
|
|
789
|
+
for (const { node, path } of candidates) {
|
|
790
|
+
// Absolute span of this candidate, projected from the relative root (O(depth)).
|
|
791
|
+
const { start: absStart, end: absEnd } = absoluteSpanCST(root as unknown as { span: Span; children?: readonly unknown[] }, path)
|
|
792
|
+
// Reentry only pays off when the re-parsed rule is substantially smaller
|
|
793
|
+
// than the whole document. Candidates widen deepest→root (monotonically
|
|
794
|
+
// growing span), so once one covers most of the input, reparsing it — and
|
|
795
|
+
// every larger ancestor after it — can't beat a full reparse and would just
|
|
796
|
+
// be wasted work before the fallback. Bail to the full reparse NOW. This
|
|
797
|
+
// caps `.edit()` at ~one full reparse in the worst case (e.g. a structural
|
|
798
|
+
// insert near the front) instead of stacking several near-full reparses.
|
|
799
|
+
if (absEnd - absStart > this.input.length * REENTRY_MAX_SPAN_FRACTION) break
|
|
800
|
+
const ruleFn = this._fns[node.type]
|
|
801
|
+
if (!ruleFn) continue
|
|
802
|
+
// The reused subtree must FULLY CONTAIN the edited range (old coords).
|
|
803
|
+
// `findContaining` only locates `from`; if the edit's end `to` spills past
|
|
804
|
+
// this node's end, the edit also changed a sibling/separator after it, and
|
|
805
|
+
// reusing the untouched suffix would be unsound — widen to an ancestor
|
|
806
|
+
// that does span the whole edit (ultimately a full reparse).
|
|
807
|
+
if (!(absStart <= from && to <= absEnd)) continue
|
|
808
|
+
const ctx: ParseContext = mkCtx(node.state, this._opts.build, !!this._opts.tolerant)
|
|
809
|
+
const r = ruleFn(newInput, absStart, ctx)
|
|
810
|
+
if (!r.ok) continue
|
|
811
|
+
if (r.span.end !== absEnd + delta) continue
|
|
812
|
+
|
|
813
|
+
// Stage-2 soundness guard: only reuse the untouched suffix if the re-parse
|
|
814
|
+
// provably read no input past its own end (else a lookahead/backtrack
|
|
815
|
+
// crossed the splice). Widen to the next candidate — ultimately a full
|
|
816
|
+
// reparse — when it can't be proven.
|
|
817
|
+
if (!boundaryIsSafe(ruleFn, newInput, absStart, absEnd + delta, node.state, this._opts.build, r, !!this._opts.tolerant)) {
|
|
818
|
+
continue
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// The re-parse produced an ABSOLUTE subtree; rebase it to parent-relative
|
|
822
|
+
// for splicing into the relative tree. The parent's absolute start is
|
|
823
|
+
// `absStart - node.span.start` (node.span is already relative to it).
|
|
824
|
+
const parentBase = absStart - node.span.start
|
|
825
|
+
const newRel = relativizeCST(r.value as unknown as N & { span: Span }, parentBase) as unknown as N
|
|
826
|
+
|
|
827
|
+
// delta === 0: relative spans are unchanged, so the spine graft (sharing
|
|
828
|
+
// every untouched sibling by reference) is already correct.
|
|
829
|
+
if (delta === 0) {
|
|
830
|
+
const newTree = replaceAtPath(rebuild, root, path, newRel)
|
|
831
|
+
return this.wrapReuse(newTree, newInput)
|
|
832
|
+
}
|
|
833
|
+
// Length-changing edit: trailing siblings' relative starts slide by `delta`.
|
|
834
|
+
// A custom `rebuild` (possibly a class instance) can't have its span slid
|
|
835
|
+
// safely, so fall back to a full, correct reparse.
|
|
836
|
+
if (this._opts.rebuild) return reparse()
|
|
837
|
+
const newTree = graftRelative(root, path, newRel, delta)
|
|
838
|
+
return this.wrapReuse(newTree, newInput)
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// No localized rule reparse converged — the edit is structural. Before paying
|
|
842
|
+
// a full reparse, try reusing the untouched tail of a containing collection
|
|
843
|
+
// (add/remove an element in a list). We only ever splice a rule the GRAMMAR
|
|
844
|
+
// proves is a genuine repetition (`this._reps`, from its combinator def) — a
|
|
845
|
+
// fixed-arity same-typed sequence is structurally indistinguishable from a list
|
|
846
|
+
// by its CST alone, so splicing it could accept a wrong element count; it's
|
|
847
|
+
// excluded here and falls back to a full, correct reparse. Innermost containing
|
|
848
|
+
// collection first; a candidate whose disturbed middle doesn't tile cleanly
|
|
849
|
+
// returns null and we widen. Correctness net is the incremental oracle fuzz.
|
|
850
|
+
if (this._opts.structuralReuse && delta !== 0 && !this._opts.rebuild) {
|
|
851
|
+
// Candidates run innermost→ancestor but never include the root itself; the
|
|
852
|
+
// splice-able collection can BE the root (e.g. a top-level `sepBy` list), so
|
|
853
|
+
// consider it last.
|
|
854
|
+
const spliceCandidates: FoundNode<N>[] = [...candidates, { node: root, path: [] }]
|
|
855
|
+
for (const { node, path } of spliceCandidates) {
|
|
856
|
+
const rep = this._reps.get(node.type)
|
|
857
|
+
if (!rep) continue // grammar didn't prove this rule a splice-able repetition
|
|
858
|
+
const { start: cStart, end: cEnd } = absoluteSpanCST(root as unknown as { span: Span; children?: readonly unknown[] }, path)
|
|
859
|
+
if (!(cStart <= from && to <= cEnd)) continue
|
|
860
|
+
const spliced = tryListSplice(root, path, node, cStart, newInput, from, to, delta, rep, this._fns, this._opts.build, !!this._opts.tolerant)
|
|
861
|
+
if (spliced) return this.wrapReuse(spliced, newInput)
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return reparse()
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* The relative (parent-offset) tree backing a doc. Currently identical to the
|
|
871
|
+
* public `doc.tree` (which is relative); kept as a named internal handle for the
|
|
872
|
+
* reuse-metric tests, which assert on the shareable representation explicitly.
|
|
873
|
+
*/
|
|
874
|
+
export function relTreeOf<N extends NodeLike>(doc: ParseDoc<N>): N | null {
|
|
875
|
+
return doc.tree
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* The grammar trivia rule to skip when computing `unconsumedFrom`: an explicit
|
|
880
|
+
* `trivia` option wins; otherwise the root entry's ambient `grammarTrivia` (the
|
|
881
|
+
* same source `run()` derives it from). A bare-function registry carries no meta,
|
|
882
|
+
* so trailing trivia can't be inferred and the parse must reach the exact end.
|
|
883
|
+
*/
|
|
884
|
+
function triviaOf<N extends NodeLike>(
|
|
885
|
+
entry: RuleFn<N> | Combinator<N> | undefined,
|
|
886
|
+
opts: ParseDocOptions<N>,
|
|
887
|
+
): Combinator<unknown> | undefined {
|
|
888
|
+
if (opts.trivia) return opts.trivia
|
|
889
|
+
// Only an interpreter Combinator carries `_meta`; a bare parse function or a
|
|
890
|
+
// compiled-grammar object (which bakes its ambient trivia into codegen) does
|
|
891
|
+
// not, so there's nothing to derive and trailing trivia isn't skipped.
|
|
892
|
+
if (entry === undefined || typeof entry === 'function') return undefined
|
|
893
|
+
const meta = (entry as { _meta?: { grammarTrivia?: Combinator<unknown> } })._meta
|
|
894
|
+
return meta ? meta.grammarTrivia : undefined
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* First unconsumed non-trivia offset after a parse that ended at `end` (trailing
|
|
899
|
+
* trivia skipped when a trivia rule is available), or `null` when the whole input
|
|
900
|
+
* was consumed — byte-for-byte the computation in `run()` (run.ts:117-129).
|
|
901
|
+
*/
|
|
902
|
+
function unconsumedAfter(end: number, input: string, trivia: Combinator<unknown> | undefined): number | null {
|
|
903
|
+
let pos = end
|
|
904
|
+
if (trivia && pos < input.length) {
|
|
905
|
+
const t = trivia.parse(input, pos, createParseContext())
|
|
906
|
+
if (t.ok && t.span.end > pos) pos = t.span.end
|
|
907
|
+
}
|
|
908
|
+
return pos < input.length ? pos : null
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Collect embedded `parseError` recovery nodes from a PARENT-RELATIVE tree,
|
|
913
|
+
* projecting each to an ABSOLUTE span (root base 0) — the flat mirror of the
|
|
914
|
+
* errors that ride the tree, recomputed for a reuse-path (grafted/spliced) result
|
|
915
|
+
* so `doc.errors` stays consistent with the tree across incremental edits.
|
|
916
|
+
*/
|
|
917
|
+
function collectEmbeddedErrors(node: unknown, base: number, out: ParseError[]): ParseError[] {
|
|
918
|
+
const c = node as { _tag?: string; span?: { start: number; end: number }; expected?: string[]; children?: readonly unknown[] }
|
|
919
|
+
if (!c || typeof c !== 'object' || !c.span) return out
|
|
920
|
+
const start = base + c.span.start
|
|
921
|
+
if (c._tag === 'parseError') {
|
|
922
|
+
out.push({ _tag: 'parseError', span: { start, end: base + c.span.end }, expected: c.expected ?? [] })
|
|
923
|
+
return out
|
|
924
|
+
}
|
|
925
|
+
if (Array.isArray(c.children)) for (const k of c.children) collectEmbeddedErrors(k, start, out)
|
|
926
|
+
return out
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Parse `input` from `rootRule` and wrap the result in a ParseDoc that can
|
|
931
|
+
* be incrementally re-parsed via `.edit()`.
|
|
932
|
+
*/
|
|
933
|
+
export function parseDoc<N extends NodeLike>(
|
|
934
|
+
registry: Registry<N>,
|
|
935
|
+
rootRule: string,
|
|
936
|
+
input: string,
|
|
937
|
+
opts: ParseDocOptions<N> = {},
|
|
938
|
+
): ParseDoc<N> {
|
|
939
|
+
const entry = registry[rootRule]
|
|
940
|
+
if (!entry) throw new Error(`No rule '${rootRule}' in registry`)
|
|
941
|
+
// Once per document, not once per node: the fused artifact records the host mode it
|
|
942
|
+
// was lowered for, so a mismatched driver is an error here rather than a wrong-shaped
|
|
943
|
+
// tree later. Every reparse this doc performs reuses the same registry and host.
|
|
944
|
+
assertHostModeCompatible(fusedHostModeOf(registry), opts.build, fusedHostElidedOf(registry))
|
|
945
|
+
const ctx: ParseContext = mkCtx(opts.state, opts.build, !!opts.tolerant)
|
|
946
|
+
const r: ParseResult<N> = asRuleFn(entry)(input, 0, ctx)
|
|
947
|
+
if (r.ok) {
|
|
948
|
+
// A fresh parse is ABSOLUTE; the relative form is materialized lazily on the
|
|
949
|
+
// first edit/spanAt, so a parse that's never edited pays no conversion. The
|
|
950
|
+
// recovery errors the tolerant parse collected into ctx._errors are surfaced
|
|
951
|
+
// flat too (like run()), and unconsumedFrom reports any trailing junk.
|
|
952
|
+
const errors = ctx._errors ? [...ctx._errors] : []
|
|
953
|
+
const unconsumedFrom = unconsumedAfter(r.span.end, input, triviaOf(entry, opts))
|
|
954
|
+
return new ParseDocImpl(registry, rootRule, opts, { abs: r.value }, errors, unconsumedFrom, input)
|
|
955
|
+
}
|
|
956
|
+
// Hard (non-recovered) failure: the single top-level failure, as a parseError.
|
|
957
|
+
const fail: ParseError = { _tag: 'parseError', span: r.span, expected: r.expected }
|
|
958
|
+
return new ParseDocImpl(registry, rootRule, opts, { abs: null }, [fail], null, input)
|
|
959
|
+
}
|