Skip to main content

m2_syn/parser/
realtime.rs

1//! Retained native parsing over independently cached source cells.
2//!
3//! [`NativeRealtimeParser`] treats a confirmed cell boundary as a restart
4//! point. Edits invalidate the cell containing the edit and only the suffix
5//! whose framing changes. The final EOF boundary remains speculative, so an
6//! append reopens that cell while leaving every confirmed predecessor intact.
7
8use std::error::Error;
9use std::fmt::{Display, Formatter};
10use std::ops::Range;
11use std::sync::Arc;
12
13use crate::cell_stream::{end_span, ends_with_cell_semicolon, frame_recovering};
14use crate::native::{
15    NativeCellState, NativeParseError, incomplete_lex_error, incomplete_parse_error, parse_raw_cell,
16};
17use crate::{
18    Cell, CellStream, LexError, Source, Span, Spanned, TextEdit, TokenStream, lex_str_recovering,
19};
20
21/// The structural boundary following one retained native cell.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum NativeCellBoundary {
24    /// A newline or semicolon confirmed the end of this cell.
25    EOC,
26    /// The current end of input provisionally ends otherwise complete syntax.
27    EOF,
28    /// The cell still requires more input and therefore has no closing boundary.
29    Unbounded,
30}
31
32/// A source edit that does not fit the current realtime buffer.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub struct NativeRealtimeEditError;
35
36impl Display for NativeRealtimeEditError {
37    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
38        formatter.write_str("source edit is outside the current realtime buffer")
39    }
40}
41
42impl Error for NativeRealtimeEditError {}
43
44/// One independently retained cell in a [`NativeRealtimeParser`].
45///
46/// Spans inside the token and typed representations are local to the exact
47/// source returned by [`Self::source`]. [`Self::document_range`] locates that
48/// immutable cell source in the current document. This separation permits an
49/// unchanged cell to survive edits above it without rewriting every leaf span.
50#[derive(Debug)]
51pub struct NativeRealtimeCell {
52    document_range: Range<usize>,
53    source: Span,
54    tokens: TokenStream,
55    syntax: Option<Cell>,
56    lexer_diagnostics: Vec<LexError>,
57    parse_error: Option<NativeParseError>,
58    state: NativeCellState,
59    boundary: NativeCellBoundary,
60}
61
62impl NativeRealtimeCell {
63    /// Returns this cell's exact source spelling.
64    ///
65    /// # Panics
66    ///
67    /// Panics if the internally retained source span cannot resolve its text.
68    pub fn source(&self) -> &str {
69        self.source
70            .text()
71            .expect("realtime cell source spans must resolve to text")
72    }
73
74    /// Returns this cell's half-open byte range in the current document.
75    pub fn document_range(&self) -> Range<usize> {
76        self.document_range.clone()
77    }
78
79    /// Returns the tokenized cell, including inferred adjacency punctuation.
80    pub fn tokens(&self) -> &TokenStream {
81        &self.tokens
82    }
83
84    /// Returns recovered typed syntax when the native parser could construct it.
85    pub fn syntax(&self) -> Option<&Cell> {
86        self.syntax.as_ref()
87    }
88
89    /// Returns recoverable lexer diagnostics associated with this cell.
90    pub fn lexer_diagnostics(&self) -> &[LexError] {
91        &self.lexer_diagnostics
92    }
93
94    /// Returns the native parser failure associated with this cell.
95    pub fn parse_error(&self) -> Option<&NativeParseError> {
96        self.parse_error.as_ref()
97    }
98
99    /// Returns whether this cell is complete, incomplete, or invalid.
100    pub fn state(&self) -> NativeCellState {
101        self.state
102    }
103
104    /// Returns the structural boundary currently following this cell.
105    pub fn boundary(&self) -> NativeCellBoundary {
106        self.boundary
107    }
108}
109
110impl Spanned for NativeRealtimeCell {
111    fn try_span(&self) -> Option<Span> {
112        Some(self.source.clone())
113    }
114}
115
116/// A pseudo-incremental native parser retaining independently framed cells.
117///
118/// Appending to an EOF-complete cell reparses only that speculative cell.
119/// Confirmed cells above it retain their token and typed allocations. An edit
120/// inside an earlier cell reparses strictly forwards; after cell framing
121/// stabilizes, an unchanged suffix is reused in its entirety. Adjacency and
122/// cell boundaries are regenerated together for every reparsed region.
123#[derive(Debug)]
124pub struct NativeRealtimeParser {
125    source: String,
126    cells: Vec<NativeRealtimeCell>,
127    reparsed_cells: Range<usize>,
128}
129
130impl Default for NativeRealtimeParser {
131    fn default() -> Self {
132        Self::new("")
133    }
134}
135
136impl NativeRealtimeParser {
137    /// Parses and retains an initial source snapshot.
138    pub fn new(source: impl Into<String>) -> Self {
139        let source = source.into();
140        let cells = analyze_region(&source, 0);
141        let reparsed_cells = 0..cells.len();
142        Self {
143            source,
144            cells,
145            reparsed_cells,
146        }
147    }
148
149    /// Applies an edit and reparses only the forward cell region it invalidates.
150    ///
151    /// The operation is transactional when the edit is outside the source or
152    /// crosses a UTF-8 character boundary. Invalid Macaulay2 syntax is retained
153    /// as [`NativeCellState::Invalid`] rather than failing the edit operation.
154    ///
155    /// # Errors
156    ///
157    /// Returns [`NativeRealtimeEditError`] when `edit` cannot be applied to the
158    /// current source buffer.
159    pub fn apply_edit(
160        &mut self,
161        edit: &TextEdit,
162    ) -> Result<NativeCellState, NativeRealtimeEditError> {
163        let source = edit.apply(&self.source).ok_or(NativeRealtimeEditError)?;
164        let first_changed = self.first_affected_cell(edit);
165        let region_start = self
166            .cells
167            .get(first_changed)
168            .map_or(self.source.len(), |cell| cell.document_range.start);
169        let reusable_suffix = self.reusable_suffix(edit, &source, first_changed, region_start);
170        let rebuild_end = reusable_suffix
171            .map(|(_, start)| start)
172            .unwrap_or(source.len());
173        let rebuilt = analyze_region(&source[region_start..rebuild_end], region_start);
174
175        let mut previous = std::mem::take(&mut self.cells);
176        let previous_suffix = previous.split_off(first_changed);
177        let rebuilt_len = rebuilt.len();
178        previous.extend(rebuilt);
179        let reuse_from = reusable_suffix
180            .map(|(index, _)| index.saturating_sub(first_changed))
181            .unwrap_or(previous_suffix.len());
182        for mut retained in previous_suffix.into_iter().skip(reuse_from) {
183            retained.document_range = shifted_range(edit, retained.document_range);
184            previous.push(retained);
185        }
186
187        self.source = source;
188        self.cells = previous;
189        self.reparsed_cells = first_changed..first_changed + rebuilt_len;
190        Ok(self.state())
191    }
192
193    /// Appends source text at the current EOF.
194    ///
195    /// # Panics
196    ///
197    /// Panics if the parser's retained source length is not a valid insertion
198    /// boundary, which would violate its internal UTF-8 source invariant.
199    pub fn append(&mut self, source: impl Into<String>) -> NativeCellState {
200        let edit = TextEdit::insertion(self.source.len(), source);
201        self.apply_edit(&edit)
202            .expect("an insertion at the current EOF is always valid")
203    }
204
205    /// Returns the complete current source buffer.
206    pub fn source(&self) -> &str {
207        &self.source
208    }
209
210    /// Returns every retained complete, incomplete, or invalid cell.
211    pub fn cells(&self) -> &[NativeRealtimeCell] {
212        &self.cells
213    }
214
215    /// Returns the range of newly analyzed cells after the most recent edit.
216    ///
217    /// Cells outside this range were retained from the preceding snapshot.
218    pub fn reparsed_cells(&self) -> Range<usize> {
219        self.reparsed_cells.clone()
220    }
221
222    /// Returns the aggregate state of the current source snapshot.
223    pub fn state(&self) -> NativeCellState {
224        if self
225            .cells
226            .iter()
227            .any(|cell| cell.state == NativeCellState::Invalid)
228        {
229            NativeCellState::Invalid
230        } else if self
231            .cells
232            .last()
233            .is_some_and(|cell| cell.state == NativeCellState::Incomplete)
234        {
235            NativeCellState::Incomplete
236        } else if self.cells.is_empty() {
237            NativeCellState::Empty
238        } else {
239            NativeCellState::Complete
240        }
241    }
242
243    /// Projects the retained cells as the parser-facing tokenized stream.
244    ///
245    /// The projection includes an incomplete or invalid tail so a parser or
246    /// macro can inspect the exact current tokenization.
247    pub fn cell_stream(&self) -> CellStream {
248        CellStream::from_cells(self.cells.iter().map(|cell| cell.tokens.clone()))
249    }
250
251    fn first_affected_cell(&self, edit: &TextEdit) -> usize {
252        let start = match edit {
253            TextEdit::Insertion { at, .. } => *at,
254            TextEdit::Deletion { start, .. } => *start,
255        };
256        self.cells
257            .iter()
258            .position(|cell| {
259                start < cell.document_range.end
260                    || (start == cell.document_range.end
261                        && cell.boundary != NativeCellBoundary::EOC)
262            })
263            .unwrap_or(self.cells.len())
264    }
265
266    fn reusable_suffix(
267        &self,
268        edit: &TextEdit,
269        source: &str,
270        first_changed: usize,
271        region_start: usize,
272    ) -> Option<(usize, usize)> {
273        for index in first_changed.saturating_add(1)..self.cells.len() {
274            let old_start = self.cells[index].document_range.start;
275            let new_start = shifted_offset(edit, old_start);
276            if new_start < region_start
277                || self.source.get(old_start..) != source.get(new_start..)
278                || !ends_at_confirmed_boundary(&source[region_start..new_start])
279            {
280                continue;
281            }
282            return Some((index, new_start));
283        }
284        None
285    }
286}
287
288fn shifted_range(edit: &TextEdit, range: Range<usize>) -> Range<usize> {
289    shifted_offset(edit, range.start)..shifted_offset(edit, range.end)
290}
291
292fn shifted_offset(edit: &TextEdit, offset: usize) -> usize {
293    match edit {
294        TextEdit::Insertion { at, text } if offset >= *at => offset + text.len(),
295        TextEdit::Insertion { .. } => offset,
296        TextEdit::Deletion { start, .. } if offset <= *start => offset,
297        TextEdit::Deletion { start, end } if offset <= *end => *start,
298        TextEdit::Deletion { start, end } => offset - (end - start),
299    }
300}
301
302fn ends_at_confirmed_boundary(source: &str) -> bool {
303    if source.trim().is_empty() {
304        return true;
305    }
306    let Ok(recovered) = lex_str_recovering(source) else {
307        return false;
308    };
309    let framed = frame_recovering(recovered.value);
310    if framed.incomplete.is_some() {
311        return false;
312    }
313    let Some(last) = framed.cells.last() else {
314        return true;
315    };
316    ends_with_cell_semicolon(last) || source.ends_with('\n')
317}
318
319fn analyze_region(source: &str, document_start: usize) -> Vec<NativeRealtimeCell> {
320    let recovered = match lex_str_recovering(source) {
321        Ok(recovered) => recovered,
322        Err(error) => {
323            if source.is_empty() {
324                return Vec::new();
325            }
326            return vec![cell_from_lex_error(
327                source,
328                document_start..document_start + source.len(),
329                error,
330            )];
331        }
332    };
333    let framed = frame_recovering(recovered.value);
334    let complete_count = framed.cells.len();
335    let has_incomplete = framed.incomplete.is_some();
336    let mut start = 0;
337    let mut cells = Vec::with_capacity(complete_count + usize::from(has_incomplete));
338
339    for (index, tokens) in framed.cells.into_iter().enumerate() {
340        let has_following = index + 1 < complete_count || has_incomplete;
341        let mut end = end_span(&tokens)
342            .end()
343            .unwrap_or(source.len())
344            .min(source.len());
345        if !has_following && end < source.len() {
346            end = source.len();
347        }
348        let boundary = if has_following
349            || ends_with_cell_semicolon(&tokens)
350            || source[start..end].ends_with('\n')
351        {
352            NativeCellBoundary::EOC
353        } else {
354            NativeCellBoundary::EOF
355        };
356        cells.push(analyze_cell(
357            &source[start..end],
358            document_start + start..document_start + end,
359            boundary,
360        ));
361        start = end;
362    }
363
364    if framed.incomplete.is_some() {
365        cells.push(analyze_cell(
366            &source[start..],
367            document_start + start..document_start + source.len(),
368            NativeCellBoundary::Unbounded,
369        ));
370    }
371    cells
372}
373
374fn analyze_cell(
375    source: &str,
376    document_range: Range<usize>,
377    boundary: NativeCellBoundary,
378) -> NativeRealtimeCell {
379    let recovered = match lex_str_recovering(source) {
380        Ok(recovered) => recovered,
381        Err(error) => return cell_from_lex_error(source, document_range, error),
382    };
383    let diagnostics = recovered.diagnostics;
384    let framed = frame_recovering(recovered.value);
385    let mut streams = framed.cells;
386    streams.extend(framed.incomplete);
387    let tokens = match streams.len() {
388        0 => TokenStream::new(),
389        1 => streams.pop().expect("one framed stream is present"),
390        _ => CellStream::from_cells(streams).into_token_stream(),
391    };
392    let source_owner = tokens.source().unwrap_or_else(|| Source::new(source));
393    let source_span = Span::ranged(source_owner, 0..source.len());
394
395    let (syntax, parse_error, state) = match parse_raw_cell(tokens.clone()) {
396        Ok(syntax) => {
397            let state = if boundary == NativeCellBoundary::Unbounded || !diagnostics.is_empty() {
398                NativeCellState::Incomplete
399            } else {
400                NativeCellState::Complete
401            };
402            (syntax, None, state)
403        }
404        Err(error) => {
405            let state = if boundary != NativeCellBoundary::EOC && incomplete_parse_error(&error) {
406                NativeCellState::Incomplete
407            } else {
408                NativeCellState::Invalid
409            };
410            (None, Some(error), state)
411        }
412    };
413
414    NativeRealtimeCell {
415        document_range,
416        source: source_span,
417        tokens,
418        syntax,
419        lexer_diagnostics: diagnostics,
420        parse_error,
421        state,
422        boundary,
423    }
424}
425
426fn cell_from_lex_error(
427    source: &str,
428    document_range: Range<usize>,
429    error: LexError,
430) -> NativeRealtimeCell {
431    let state = if incomplete_lex_error(error.kind()) {
432        NativeCellState::Incomplete
433    } else {
434        NativeCellState::Invalid
435    };
436    let source_owner = Arc::clone(error.span().source());
437    NativeRealtimeCell {
438        document_range,
439        source: Span::ranged(source_owner, 0..source.len()),
440        tokens: TokenStream::new(),
441        syntax: None,
442        lexer_diagnostics: vec![error],
443        parse_error: None,
444        state,
445        boundary: NativeCellBoundary::Unbounded,
446    }
447}