Skip to main content

badness_parser/parser/
reparse.rs

1//! Incremental reparse: splice a small edit into the previous green tree instead
2//! of re-parsing the whole text.
3//!
4//! # Contract
5//!
6//! A successful reparse must produce the same green tree and [`SyntaxError`]
7//! vector as a full parse of the edited text. Incremental reparse is only a
8//! performance optimization; a failed proof falls back to a full parse.
9//!
10//! Guards return [`None`] when they cannot prove equivalence. Extend them by
11//! adding supported cases or conservative bailouts, never by weakening the oracle.
12//!
13//! The previous-parse cache cannot affect the query result. A cold, stale, or
14//! evicted cache only forces a full parse.
15//!
16//! # Design
17//!
18//! The tiers sit strictly **on top of** [`parse_with_declarations_resolved`] and
19//! [`lex_with`]. There is no incremental lexer, no token-stream reuse, no restarting
20//! the grammar at an offset:
21//!
22//! - the token tier relexes one leaf in isolation, proves the relex is a
23//!   single token of the same kind that joins to its neighbours the same way, and
24//!   splices with rowan's [`SyntaxToken::replace_with`], sharing every green node
25//!   off the leaf-to-root path — `O(depth)`, not `O(file)`;
26//! - the protected-body tier splices the same way, but proves it differently: a raw
27//!   capture cannot be relexed alone, so it relexes the leaf's whole enclosing node
28//!   with its delimiters and requires that to reproduce the tree's own tokens;
29//! - the region tier re-runs the *ordinary* parser over a substring and splices the
30//!   resulting children under `ROOT`, using neighbour-sized boundary parses purely
31//!   as proofs that the substring is decoupled from its context.
32//!
33//! This avoids checkpointing lexer state, prescan indices, or forward shape-gate
34//! scans. The region tier declines edits whose effects may escape the fragment.
35
36mod leaf;
37mod protected;
38mod region;
39mod token;
40
41use rowan::GreenNode;
42
43use crate::declarations::ResolvedDeclarations;
44use crate::parser::core::{Parse, SyntaxError, parse_with_declarations_resolved};
45use crate::parser::lexer::{LexConfig, ParseCtx, dtx_has_expl_signal};
46use crate::syntax::SyntaxNode;
47
48pub use crate::parser::edit::{Edit, apply_edits, diff_edit, try_apply_edits};
49
50/// Which tier produced a [`Reparsed`]. Surfaced for tests and benchmarks, which
51/// assert the tier a scenario reaches — a grammar change that silently downgrades
52/// one should fail loudly rather than quietly show up as a slower number.
53///
54/// Ordered cheapest-first, so a chain can report the most expensive tier any of its
55/// steps needed with `max`.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
57pub enum ReparseTier {
58    /// One leaf token was relexed in isolation and spliced in place.
59    Token,
60    /// A protected body (`VERBATIM_BODY`, `VERB`) was relexed with its enclosing
61    /// node's delimiters and spliced in place.
62    Verbatim,
63    /// A run of top-level children was reparsed and spliced under `ROOT`.
64    Region,
65}
66
67/// A successful incremental reparse: the new whole-file green tree and its errors,
68/// both in the *new* text's offsets.
69#[derive(Debug, Clone)]
70pub struct Reparsed {
71    pub green: GreenNode,
72    pub errors: Vec<SyntaxError>,
73    pub tier: ReparseTier,
74}
75
76/// The previous parse a reparse splices against.
77///
78/// `ctx` is the context the tree was parsed under, from
79/// [`parse_with_declarations_resolved`] — a tier that relexes a fragment must use
80/// the same one, or a `\newcommand` the definition scan found makes the fragment's
81/// tokens disagree with the tree's. `config` and `declared` are the parse's other
82/// two inputs, needed to reproduce it exactly.
83#[derive(Debug, Clone, Copy)]
84pub struct ReparseBase<'a> {
85    pub text: &'a str,
86    pub green: &'a GreenNode,
87    pub errors: &'a [SyntaxError],
88    pub ctx: &'a ParseCtx,
89    pub config: LexConfig,
90    /// The file-level `.dtx` implicit-expl signal (`%<@@=...>` / `\ProvidesExpl*`)
91    /// computed from the full base text.
92    ///
93    /// The lexer derives this before tokenizing; fragment relexes need the same
94    /// regime to be faithful.
95    pub implicit_expl: bool,
96    pub declared: &'a ResolvedDeclarations,
97}
98
99impl<'a> ReparseBase<'a> {
100    pub fn from_parts(
101        text: &'a str,
102        green: &'a GreenNode,
103        errors: &'a [SyntaxError],
104        ctx: &'a ParseCtx,
105        config: LexConfig,
106        declared: &'a ResolvedDeclarations,
107    ) -> Self {
108        Self {
109            text,
110            green,
111            errors,
112            ctx,
113            config,
114            implicit_expl: implicit_expl_for(text, config),
115            declared,
116        }
117    }
118
119    /// Materialize a red-tree cursor over the base. Cheap (an atomic clone).
120    pub fn syntax(&self) -> SyntaxNode {
121        SyntaxNode::new_root(self.green.clone())
122    }
123}
124
125fn implicit_expl_for(text: &str, config: LexConfig) -> bool {
126    config.dtx && dtx_has_expl_signal(text)
127}
128
129/// Attempt an incremental reparse of `base` under `edit`, which transforms
130/// `base.text` into `new_text`. [`None`] means no tier applied and the caller must
131/// do a full parse.
132pub fn reparse(base: &ReparseBase<'_>, edit: &Edit, new_text: &str) -> Option<Reparsed> {
133    // The edit is untrusted: a chain staged against a buffer that has since moved
134    // slices out of bounds, and a panic here takes down an analysis query where a
135    // bail would have cost one parse.
136    if !edit.fits(base.text) {
137        return None;
138    }
139    reparse_one(base, edit, new_text)
140}
141
142/// [`reparse`] for a chain of edits, each expressed against the text its
143/// predecessors produced — the shape an LSP `didChange` batch arrives in.
144///
145/// Replaying the chain is not the same as collapsing it: a diff of scattered edits
146/// spans everything between them, which a cost guard declines outright, while the
147/// chain splices each edit on its own.
148pub fn reparse_edits(base: &ReparseBase<'_>, edits: &[Edit], new_text: &str) -> Option<Reparsed> {
149    if edits.is_empty() {
150        return None;
151    }
152
153    // Verify the chain describes exactly the transform claimed, then replay it. The
154    // fold is deliberately *not* hoisted ahead of the splices as a pre-check: it
155    // costs the same order as the work it would guard, and each step below already
156    // validates against the text its predecessors produced.
157    let mut text = base.text.to_string();
158    let mut green = base.green.clone();
159    let mut errors = base.errors.to_vec();
160    let mut tier: Option<ReparseTier> = None;
161
162    for edit in edits {
163        if !edit.fits(&text) {
164            return None;
165        }
166        let next = edit.apply(&text);
167        let step = {
168            let step_base = ReparseBase::from_parts(
169                &text,
170                &green,
171                &errors,
172                base.ctx,
173                base.config,
174                base.declared,
175            );
176            reparse_one(&step_base, edit, &next)?
177        };
178        text = next;
179        green = step.green;
180        errors = step.errors;
181        tier = Some(tier.map_or(step.tier, |t| t.max(step.tier)));
182    }
183
184    // A stale chain can apply cleanly and still land somewhere other than the
185    // buffer the caller is asking about. Reject it rather than answer for the wrong
186    // text.
187    if text != new_text {
188        return None;
189    }
190
191    Some(Reparsed {
192        green,
193        errors,
194        tier: tier?,
195    })
196}
197
198/// The tier ladder for one already-validated edit, cheapest first.
199///
200/// Each tier lands here as an `.or_else` and returns through [`finish`], so none
201/// can skip the length check or the oracle.
202fn reparse_one(base: &ReparseBase<'_>, edit: &Edit, new_text: &str) -> Option<Reparsed> {
203    token::reparse_token(base, edit, new_text)
204        .or_else(|| protected::reparse_protected(base, edit, new_text))
205        .or_else(|| region::reparse_region(base, edit, new_text))
206}
207
208/// The single exit for every tier.
209///
210/// Routing all of them through one function is deliberate: a tier cannot return a
211/// result without paying the every-build length check and the debug oracle, so
212/// "did the new tier remember to verify?" is not a question a reviewer has to ask.
213///
214/// The length check is the release-build backstop. The oracle below is
215/// `debug_assertions`-only because it costs a full parse, which would defeat the
216/// point in the build that ships — but that is also the build whose formatter
217/// rewrites the user's file, so *something* must hold there. A tree that does not
218/// span exactly its text is the cheap, `O(1)`, always-affordable half of the
219/// invariant, and it catches the whole class of offset-arithmetic bugs a splice can
220/// have. It *falls back* rather than panicking, per the refusal-first contract.
221fn finish(
222    green: GreenNode,
223    errors: Vec<SyntaxError>,
224    tier: ReparseTier,
225    base: &ReparseBase<'_>,
226    new_text: &str,
227) -> Option<Reparsed> {
228    if !spans_its_text(&green, new_text) {
229        return None;
230    }
231    let out = Reparsed {
232        green,
233        errors,
234        tier,
235    };
236    assert_matches_full_parse(&out, base, new_text);
237    Some(out)
238}
239
240/// Whether `green` spans exactly `text`. `O(1)` — rowan stores the width.
241fn spans_its_text(green: &GreenNode, text: &str) -> bool {
242    usize::from(green.text_len()) == text.len()
243}
244
245/// Render every node and token in preorder as `KIND@range "text"`.
246///
247/// Equal fingerprints mean byte-identical trees, and an unequal pair names the
248/// first place they diverge, which a `GreenNode` inequality does not. Public (and
249/// hidden) so the in-crate assert and the external harness share one definition of
250/// "identical" and can never drift apart.
251#[doc(hidden)]
252pub fn fingerprint(node: &SyntaxNode) -> String {
253    use std::fmt::Write as _;
254
255    let mut out = String::new();
256    for element in node.descendants_with_tokens() {
257        match element {
258            rowan::NodeOrToken::Node(n) => {
259                let _ = writeln!(out, "{:?}@{:?}", n.kind(), n.text_range());
260            }
261            rowan::NodeOrToken::Token(t) => {
262                let _ = writeln!(out, "{:?}@{:?} {:?}", t.kind(), t.text_range(), t.text());
263            }
264        }
265    }
266    out
267}
268
269/// Assert the governing invariant on a result about to be returned.
270///
271/// **Every failure here is an incremental-parser bug whose fix is a new
272/// bail-to-full-parse condition, never a relaxation of this assert.** If a tier
273/// produces a tree a full parse would not, the tier does not understand the
274/// construct it just spliced, and the honest repair is to stop claiming it does.
275#[cfg(debug_assertions)]
276fn assert_matches_full_parse(result: &Reparsed, base: &ReparseBase<'_>, new_text: &str) {
277    let full = full_parse(base, new_text);
278    debug_assert_eq!(
279        fingerprint(&SyntaxNode::new_root(result.green.clone())),
280        fingerprint(&full.syntax()),
281        "reparse ({:?}) produced a different tree than a full parse",
282        result.tier,
283    );
284    debug_assert_eq!(
285        result.errors, full.errors,
286        "reparse ({:?}) produced different errors than a full parse",
287        result.tier,
288    );
289}
290
291#[cfg(not(debug_assertions))]
292fn assert_matches_full_parse(_: &Reparsed, _: &ReparseBase<'_>, _: &str) {}
293
294/// The full parse a reparse must agree with, under the base's own inputs.
295#[cfg_attr(not(debug_assertions), allow(dead_code))]
296fn full_parse(base: &ReparseBase<'_>, text: &str) -> Parse {
297    parse_with_declarations_resolved(text, base.config, base.declared).0
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303    use crate::parser::lexer::LatexFlavor;
304
305    /// Build a base by fully parsing `text`, the way the host does.
306    fn base_of(text: &str) -> (Parse, ParseCtx, ResolvedDeclarations) {
307        let declared = ResolvedDeclarations::default();
308        let (parse, ctx) = parse_with_declarations_resolved(text, LatexFlavor::Document, &declared);
309        (parse, ctx, declared)
310    }
311
312    fn with_base<R>(text: &str, f: impl FnOnce(&ReparseBase<'_>) -> R) -> R {
313        let (parse, ctx, declared) = base_of(text);
314        f(&ReparseBase::from_parts(
315            text,
316            &parse.green,
317            &parse.errors,
318            &ctx,
319            LatexFlavor::Document.into(),
320            &declared,
321        ))
322    }
323
324    fn edit(range: std::ops::Range<usize>, insert: &str) -> Edit {
325        Edit {
326            range,
327            insert: insert.to_string(),
328        }
329    }
330
331    /// An edit that lands on no spliceable leaf falls back. The ladder is
332    /// refusal-first, so "no tier claimed it" is the ordinary outcome and has to
333    /// stay cheap and silent rather than becoming an error.
334    #[test]
335    fn an_edit_outside_a_plain_leaf_falls_back() {
336        with_base("\\section{Hi}\n\nbody text\n", |base| {
337            // On the `{`: a structural token, not a leaf this tier relexes.
338            let e = edit(8..8, "x");
339            assert!(reparse(base, &e, &e.apply(base.text)).is_none());
340            // Straddling two tokens: the covering element is a node.
341            let e = edit(7..10, "zz");
342            assert!(reparse(base, &e, &e.apply(base.text)).is_none());
343        });
344    }
345
346    /// An edit that does not fit the base is refused before anything slices it. A
347    /// language server can stage a chain against a buffer that has since moved.
348    #[test]
349    fn an_edit_that_does_not_fit_the_base_is_refused() {
350        with_base("abc\n", |base| {
351            assert!(reparse(base, &edit(90..99, "x"), "abc\n").is_none());
352            // Offset 1 is inside a two-byte char.
353            assert!(reparse(base, &edit(1..1, "x"), "abc\n").is_none());
354        });
355        with_base("α\n", |base| {
356            assert!(reparse(base, &edit(1..1, "x"), "αx\n").is_none());
357        });
358    }
359
360    #[test]
361    fn an_empty_chain_is_refused() {
362        with_base("abc\n", |base| {
363            assert!(reparse_edits(base, &[], "abc\n").is_none());
364        });
365    }
366
367    /// A chain that applies cleanly but composes to some *other* text is stale, and
368    /// answering for it would answer for the wrong buffer.
369    #[test]
370    fn a_chain_that_lands_elsewhere_is_refused() {
371        with_base("abc\n", |base| {
372            assert!(reparse_edits(base, &[edit(0..0, "x")], "totally different").is_none());
373        });
374    }
375
376    #[test]
377    fn spans_its_text_measures_the_green_width() {
378        with_base("\\section{Hi}\n", |base| {
379            assert!(spans_its_text(base.green, base.text));
380            assert!(!spans_its_text(base.green, "\\section{Hi}"));
381            assert!(!spans_its_text(base.green, "\\section{Hi}\n\n"));
382        });
383    }
384
385    /// The release-build backstop: `finish` refuses a tree that does not span its
386    /// text rather than handing back a splice with bad offsets. It must *bail*, not
387    /// panic — the contract is refusal-first even for a bug.
388    #[test]
389    fn finish_refuses_a_tree_that_does_not_span_its_text() {
390        with_base("\\section{Hi}\n", |base| {
391            let out = finish(
392                base.green.clone(),
393                base.errors.to_vec(),
394                ReparseTier::Token,
395                base,
396                // One byte longer than the tree.
397                "\\section{Hi}\n\n",
398            );
399            assert!(out.is_none());
400        });
401    }
402
403    /// The happy path through the single exit, so the length check and the oracle
404    /// are both exercised on a result that should pass. An identity splice is the
405    /// only result Phase 1 can construct honestly.
406    #[test]
407    fn finish_accepts_an_identity_splice() {
408        with_base("\\section{Hi}\n\nbody\n", |base| {
409            let out = finish(
410                base.green.clone(),
411                base.errors.to_vec(),
412                ReparseTier::Token,
413                base,
414                base.text,
415            );
416            let out = out.expect("an identity splice matches a full parse");
417            assert_eq!(out.tier, ReparseTier::Token);
418            assert_eq!(&out.green, base.green);
419        });
420    }
421
422    #[test]
423    fn tiers_order_cheapest_first() {
424        assert!(ReparseTier::Token < ReparseTier::Verbatim);
425        assert!(ReparseTier::Verbatim < ReparseTier::Region);
426    }
427
428    #[test]
429    fn fingerprint_separates_trees_that_differ_only_in_token_text() {
430        let a = crate::parser::parse("\\a{b}");
431        let b = crate::parser::parse("\\a{c}");
432        assert_ne!(fingerprint(&a.syntax()), fingerprint(&b.syntax()));
433    }
434
435    #[test]
436    fn fingerprint_agrees_with_itself_across_equal_parses() {
437        let a = crate::parser::parse("\\section{Hi}\n\nbody $x^2$ % c\n");
438        let b = crate::parser::parse("\\section{Hi}\n\nbody $x^2$ % c\n");
439        assert_eq!(fingerprint(&a.syntax()), fingerprint(&b.syntax()));
440    }
441
442    /// # Oracle self-tests
443    ///
444    /// The oracle is the whole safety story, so it has to be shown capable of
445    /// failing. Without these, Phase 1 ships a net with no proof it catches
446    /// anything — and it would keep passing if `assert_matches_full_parse` were
447    /// accidentally compiled out.
448    ///
449    /// `debug_assertions`-gated because that is what the assert itself is gated on;
450    /// under `--release` there is deliberately nothing here to trip.
451    #[cfg(debug_assertions)]
452    mod oracle_self_tests {
453        use super::*;
454
455        #[test]
456        #[should_panic(expected = "different tree")]
457        fn the_oracle_rejects_a_wrong_tree() {
458            with_base("\\section{Hi}\n", |base| {
459                // A tree of the right *width* but the wrong content: the length
460                // backstop cannot see this one, which is exactly why the oracle exists.
461                let wrong = crate::parser::parse("\\section{Ho}\n");
462                let _ = finish(
463                    wrong.green,
464                    base.errors.to_vec(),
465                    ReparseTier::Token,
466                    base,
467                    base.text,
468                );
469            });
470        }
471
472        #[test]
473        #[should_panic(expected = "different errors")]
474        fn the_oracle_rejects_a_perturbed_error_vector() {
475            with_base("\\section{Hi}\n", |base| {
476                let mut errors = base.errors.to_vec();
477                errors.push(SyntaxError {
478                    message: "invented".to_string(),
479                    start: 0,
480                    end: 1,
481                });
482                let _ = finish(
483                    base.green.clone(),
484                    errors,
485                    ReparseTier::Token,
486                    base,
487                    base.text,
488                );
489            });
490        }
491
492        /// The error check is not satisfied by a mere count match — a diagnostic
493        /// that moved is as wrong as one that appeared.
494        #[test]
495        #[should_panic(expected = "different errors")]
496        fn the_oracle_rejects_an_error_that_moved() {
497            let text = "\\begin{itemize}\n";
498            with_base(text, |base| {
499                assert!(
500                    !base.errors.is_empty(),
501                    "this fixture exists to carry an error"
502                );
503                let mut errors = base.errors.to_vec();
504                errors[0].start += 1;
505                let _ = finish(
506                    base.green.clone(),
507                    errors,
508                    ReparseTier::Token,
509                    base,
510                    base.text,
511                );
512            });
513        }
514    }
515}