m2-syn 0.6.1

Parser, typed CST, traversal and procedural macros for Macaulay2
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
//! Retained native parsing over independently cached source cells.
//!
//! [`NativeRealtimeParser`] treats a confirmed cell boundary as a restart
//! point. Edits invalidate the cell containing the edit and only the suffix
//! whose framing changes. The final EOF boundary remains speculative, so an
//! append reopens that cell while leaving every confirmed predecessor intact.

use std::error::Error;
use std::fmt::{Display, Formatter};
use std::mem;
use std::ops::Range;
use std::sync::Arc;

use crate::cell_stream::{end_span, ends_with_cell_semicolon, frame_recovering};
use crate::native::{
    NativeCellState, NativeParseError, incomplete_lex_error, incomplete_parse_error, parse_raw_cell,
};
use crate::{
    Cell, CellStream, CellTokenStream, LexError, Source, Span, Spanned, TextEdit,
    lex_str_recovering,
};

/// The structural boundary following one retained native cell.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NativeCellBoundary {
    /// A newline or semicolon confirmed the end of this cell.
    EOC,
    /// The current end of input provisionally ends otherwise complete syntax.
    EOF,
    /// The cell still requires more input and therefore has no closing boundary.
    Unbounded,
}

/// A source edit that does not fit the current realtime buffer.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NativeRealtimeEditError;

impl Display for NativeRealtimeEditError {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        formatter.write_str("source edit is outside the current realtime buffer")
    }
}

impl Error for NativeRealtimeEditError {}

/// One independently retained cell in a [`NativeRealtimeParser`].
///
/// Spans inside the token and typed representations are local to the exact
/// source returned by [`Self::source`]. [`Self::document_range`] locates that
/// immutable cell source in the current document. This separation permits an
/// unchanged cell to survive edits above it without rewriting every leaf span.
#[derive(Debug)]
pub struct NativeRealtimeCell {
    /// The document range.
    document_range: Range<usize>,
    /// The source.
    source: Span,
    /// The tokens.
    tokens: CellTokenStream,
    /// The syntax.
    syntax: Option<Cell>,
    /// The lexer diagnostics.
    lexer_diagnostics: Vec<LexError>,
    /// The parse error.
    parse_error: Option<NativeParseError>,
    /// The state.
    state: NativeCellState,
    /// The boundary.
    boundary: NativeCellBoundary,
}

impl NativeRealtimeCell {
    /// Returns this cell's exact source spelling.
    ///
    /// # Panics
    ///
    /// Panics if the internally retained source span cannot resolve its text.
    pub fn source(&self) -> &str {
        self.source
            .text()
            .expect("realtime cell source spans must resolve to text")
    }

    /// Returns this cell's half-open byte range in the current document.
    pub fn document_range(&self) -> Range<usize> {
        self.document_range.clone()
    }

    /// Returns the tokenized cell, including inferred adjacency punctuation.
    pub fn tokens(&self) -> &CellTokenStream {
        &self.tokens
    }

    /// Returns recovered typed syntax when the native parser could construct it.
    pub fn syntax(&self) -> Option<&Cell> {
        self.syntax.as_ref()
    }

    /// Returns recoverable lexer diagnostics associated with this cell.
    pub fn lexer_diagnostics(&self) -> &[LexError] {
        &self.lexer_diagnostics
    }

    /// Returns the native parser failure associated with this cell.
    pub fn parse_error(&self) -> Option<&NativeParseError> {
        self.parse_error.as_ref()
    }

    /// Returns whether this cell is complete, incomplete, or invalid.
    pub fn state(&self) -> NativeCellState {
        self.state
    }

    /// Returns the structural boundary currently following this cell.
    pub fn boundary(&self) -> NativeCellBoundary {
        self.boundary
    }
}

impl Spanned for NativeRealtimeCell {
    fn try_span(&self) -> Option<Span> {
        Some(self.source.clone())
    }
}

/// A pseudo-incremental native parser retaining independently framed cells.
///
/// Appending to an EOF-complete cell reparses only that speculative cell.
/// Confirmed cells above it retain their token and typed allocations. An edit
/// inside an earlier cell reparses strictly forwards; after cell framing
/// stabilizes, an unchanged suffix is reused in its entirety. Adjacency and
/// cell boundaries are regenerated together for every reparsed region.
#[derive(Debug)]
pub struct NativeRealtimeParser {
    /// The complete current source snapshot.
    source: String,
    /// Independently framed cells covering the current source.
    cells: Vec<NativeRealtimeCell>,
    /// The cell range rebuilt by the most recent operation.
    reparsed_cells: Range<usize>,
}

impl Default for NativeRealtimeParser {
    fn default() -> Self {
        Self::new("")
    }
}

impl NativeRealtimeParser {
    /// Parses and retains an initial source snapshot.
    pub fn new(source: impl Into<String>) -> Self {
        let source = source.into();
        let cells = analyze_region(&source, 0);
        let reparsed_cells = 0..cells.len();
        Self {
            source,
            cells,
            reparsed_cells,
        }
    }

    /// Applies an edit and reparses only the forward cell region it invalidates.
    ///
    /// The operation is transactional when the edit is outside the source or
    /// crosses a UTF-8 character boundary. Invalid Macaulay2 syntax is retained
    /// as [`NativeCellState::Invalid`] rather than failing the edit operation.
    ///
    /// # Errors
    ///
    /// Returns [`NativeRealtimeEditError`] when `edit` cannot be applied to the
    /// current source buffer.
    pub fn apply_edit(
        &mut self,
        edit: &TextEdit,
    ) -> Result<NativeCellState, NativeRealtimeEditError> {
        let source = edit.apply(&self.source).ok_or(NativeRealtimeEditError)?;
        let first_changed = self.first_affected_cell(edit);
        let region_start = self
            .cells
            .get(first_changed)
            .map_or(self.source.len(), |cell| cell.document_range.start);
        let reusable_suffix = self.reusable_suffix(edit, &source, first_changed, region_start);
        let rebuild_end = reusable_suffix
            .map(|(_, start)| start)
            .unwrap_or(source.len());
        let rebuilt = analyze_region(&source[region_start..rebuild_end], region_start);

        let mut previous = mem::take(&mut self.cells);
        let previous_suffix = previous.split_off(first_changed);
        let rebuilt_len = rebuilt.len();
        previous.extend(rebuilt);
        let reuse_from = reusable_suffix
            .map(|(index, _)| index.saturating_sub(first_changed))
            .unwrap_or(previous_suffix.len());
        for mut retained in previous_suffix.into_iter().skip(reuse_from) {
            retained.document_range = shifted_range(edit, retained.document_range);
            previous.push(retained);
        }

        self.source = source;
        self.cells = previous;
        self.reparsed_cells = first_changed..first_changed + rebuilt_len;
        Ok(self.state())
    }

    /// Appends source text at the current EOF.
    ///
    /// # Panics
    ///
    /// Panics if the parser's retained source length is not a valid insertion
    /// boundary, which would violate its internal UTF-8 source invariant.
    pub fn append(&mut self, source: impl Into<String>) -> NativeCellState {
        let edit = TextEdit::insertion(self.source.len(), source);
        self.apply_edit(&edit)
            .expect("an insertion at the current EOF is always valid")
    }

    /// Returns the complete current source buffer.
    pub fn source(&self) -> &str {
        &self.source
    }

    /// Returns every retained complete, incomplete, or invalid cell.
    pub fn cells(&self) -> &[NativeRealtimeCell] {
        &self.cells
    }

    /// Returns the range of newly analyzed cells after the most recent edit.
    ///
    /// Cells outside this range were retained from the preceding snapshot.
    pub fn reparsed_cells(&self) -> Range<usize> {
        self.reparsed_cells.clone()
    }

    /// Returns the aggregate state of the current source snapshot.
    pub fn state(&self) -> NativeCellState {
        if self
            .cells
            .iter()
            .any(|cell| cell.state == NativeCellState::Invalid)
        {
            NativeCellState::Invalid
        } else if self
            .cells
            .last()
            .is_some_and(|cell| cell.state == NativeCellState::Incomplete)
        {
            NativeCellState::Incomplete
        } else if self.cells.is_empty() {
            NativeCellState::Empty
        } else {
            NativeCellState::Complete
        }
    }

    /// Projects the retained cells as the parser-facing tokenized stream.
    ///
    /// The projection includes an incomplete or invalid tail so a parser or
    /// macro can inspect the exact current tokenization.
    pub fn cell_stream(&self) -> CellStream {
        CellStream::from_cells(self.cells.iter().map(|cell| cell.tokens.clone()))
    }

    /// Handles first affected cell.
    fn first_affected_cell(&self, edit: &TextEdit) -> usize {
        let start = match edit {
            TextEdit::Insertion { at, .. } => *at,
            TextEdit::Deletion { start, .. } => *start,
        };
        self.cells
            .iter()
            .position(|cell| {
                start < cell.document_range.end
                    || (start == cell.document_range.end
                        && cell.boundary != NativeCellBoundary::EOC)
            })
            .unwrap_or(self.cells.len())
    }

    /// Handles reusable suffix.
    fn reusable_suffix(
        &self,
        edit: &TextEdit,
        source: &str,
        first_changed: usize,
        region_start: usize,
    ) -> Option<(usize, usize)> {
        for index in first_changed.saturating_add(1)..self.cells.len() {
            let old_start = self.cells[index].document_range.start;
            let new_start = shifted_offset(edit, old_start);
            if new_start < region_start
                || self.source.get(old_start..) != source.get(new_start..)
                || !ends_at_confirmed_boundary(&source[region_start..new_start])
            {
                continue;
            }
            return Some((index, new_start));
        }
        None
    }
}

/// Handles shifted range.
fn shifted_range(edit: &TextEdit, range: Range<usize>) -> Range<usize> {
    shifted_offset(edit, range.start)..shifted_offset(edit, range.end)
}

/// Handles shifted offset.
fn shifted_offset(edit: &TextEdit, offset: usize) -> usize {
    match edit {
        TextEdit::Insertion { at, text } if offset >= *at => offset + text.len(),
        TextEdit::Insertion { .. } => offset,
        TextEdit::Deletion { start, .. } if offset <= *start => offset,
        TextEdit::Deletion { start, end } if offset <= *end => *start,
        TextEdit::Deletion { start, end } => offset - (end - start),
    }
}

/// Returns whether the value ends at confirmed boundary.
fn ends_at_confirmed_boundary(source: &str) -> bool {
    if source.trim().is_empty() {
        return true;
    }
    let Ok(recovered) = lex_str_recovering(source) else {
        return false;
    };
    if !recovered.diagnostics.is_empty() {
        return false;
    }
    let framed = frame_recovering(recovered.value);
    if framed.incomplete.is_some() {
        return false;
    }
    let Some(last) = framed.cells.last() else {
        return true;
    };
    ends_with_cell_semicolon(last) || source.ends_with('\n')
}

/// Handles analyze region.
fn analyze_region(source: &str, document_start: usize) -> Vec<NativeRealtimeCell> {
    let recovered = match lex_str_recovering(source) {
        Ok(recovered) => recovered,
        Err(error) => {
            if source.is_empty() {
                return Vec::new();
            }
            return vec![cell_from_lex_error(
                source,
                document_start..document_start + source.len(),
                error,
            )];
        }
    };
    let lexically_incomplete = !recovered.diagnostics.is_empty();
    let framed = frame_recovering(recovered.value);
    let complete_count = framed.cells.len();
    let has_incomplete = framed.incomplete.is_some();
    let mut start = 0;
    let mut cells = Vec::with_capacity(complete_count + usize::from(has_incomplete));

    for (index, tokens) in framed.cells.into_iter().enumerate() {
        let has_following = index + 1 < complete_count || has_incomplete;
        let mut end = end_span(&tokens)
            .end()
            .unwrap_or(source.len())
            .min(source.len());
        if !has_following && end < source.len() {
            end = source.len();
        }
        let boundary = if !has_following && lexically_incomplete {
            NativeCellBoundary::Unbounded
        } else if has_following
            || ends_with_cell_semicolon(&tokens)
            || source[start..end].ends_with('\n')
        {
            NativeCellBoundary::EOC
        } else {
            NativeCellBoundary::EOF
        };
        cells.push(analyze_cell(
            &source[start..end],
            document_start + start..document_start + end,
            boundary,
        ));
        start = end;
    }

    if framed.incomplete.is_some() {
        cells.push(analyze_cell(
            &source[start..],
            document_start + start..document_start + source.len(),
            NativeCellBoundary::Unbounded,
        ));
    }
    cells
}
/// Handles analyze cell
fn analyze_cell(
    source: &str,
    document_range: Range<usize>,
    boundary: NativeCellBoundary,
) -> NativeRealtimeCell {
    let recovered = match lex_str_recovering(source) {
        Ok(recovered) => recovered,
        Err(error) => return cell_from_lex_error(source, document_range, error),
    };
    let diagnostics = recovered.diagnostics;
    let framed = frame_recovering(recovered.value);
    let mut streams = framed.cells;
    streams.extend(framed.incomplete);
    let tokens = match streams.len() {
        0 => CellTokenStream::default(),
        1 => streams.pop().expect("one framed stream is present"),
        _ => CellTokenStream::from_framed_cells(streams),
    };
    let source_owner = tokens.source().unwrap_or_else(|| Source::new(source));
    let source_span = Span::ranged(source_owner, 0..source.len());

    let (syntax, parse_error, state) = match parse_raw_cell(tokens.clone()) {
        Ok(syntax) => {
            let state = if boundary == NativeCellBoundary::Unbounded || !diagnostics.is_empty() {
                NativeCellState::Incomplete
            } else {
                NativeCellState::Complete
            };
            (syntax, None, state)
        }
        Err(error) => {
            let state = if boundary != NativeCellBoundary::EOC && incomplete_parse_error(&error) {
                NativeCellState::Incomplete
            } else {
                NativeCellState::Invalid
            };
            (None, Some(error), state)
        }
    };

    NativeRealtimeCell {
        document_range,
        source: source_span,
        tokens,
        syntax,
        lexer_diagnostics: diagnostics,
        parse_error,
        state,
        boundary,
    }
}

/// Handles cell from lex error.
fn cell_from_lex_error(
    source: &str,
    document_range: Range<usize>,
    error: LexError,
) -> NativeRealtimeCell {
    let state = if incomplete_lex_error(error.kind()) {
        NativeCellState::Incomplete
    } else {
        NativeCellState::Invalid
    };
    let source_owner = Arc::clone(error.span().source());
    NativeRealtimeCell {
        document_range,
        source: Span::ranged(source_owner, 0..source.len()),
        tokens: CellTokenStream::default(),
        syntax: None,
        lexer_diagnostics: vec![error],
        parse_error: None,
        state,
        boundary: NativeCellBoundary::Unbounded,
    }
}