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