Skip to main content

links_notation/
stream_parser.rs

1//! Incremental parsing for long-lived Links Notation streams.
2
3use crate::quotes::{DelimitedReferences, QUOTES};
4use crate::reference_literal::reference_literal_end;
5use crate::{
6    LiNo, ParseError, ParserConfig, SyntaxReference, parse_lino_to_links_with_config,
7    parse_lino_to_syntax_links_with_config,
8};
9use std::collections::VecDeque;
10use std::fmt;
11
12const DEFAULT_MAX_BUFFER_SIZE: usize = 10 * 1024 * 1024;
13/// What ends a bare reference, so that a token starts after it.
14const SEPARATORS: &[u8] = b" \t\n\r(:)";
15const BEFORE_COMMENT: &[u8] = b" \t\n\r";
16
17/// The point immediately after the last character written to a stream parser.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct StreamPosition {
20    /// One-based line number.
21    pub line: usize,
22    /// One-based character column.
23    pub column: usize,
24    /// Absolute byte offset from the start of the stream.
25    pub offset: usize,
26    /// Bytes held for the one unresolved top-level record.
27    pub buffered: usize,
28}
29
30/// Backward-compatible name for stream position information.
31pub type ErrorLocation = StreamPosition;
32
33impl fmt::Display for StreamPosition {
34    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35        write!(
36            formatter,
37            "line {}, column {} (offset {})",
38            self.line, self.column, self.offset
39        )
40    }
41}
42
43/// A canonical parser or stream-limit error with absolute stream coordinates.
44#[derive(Debug)]
45pub struct StreamParseError {
46    /// Human-readable description of the error.
47    pub message: String,
48    /// Position in the complete stream, when available.
49    pub location: Option<StreamPosition>,
50    /// The canonical parser error, when parsing a completed record failed.
51    pub parse_error: Option<Box<ParseError>>,
52}
53
54impl fmt::Display for StreamParseError {
55    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match &self.location {
57            Some(location) => write!(
58                formatter,
59                "stream parse error at {location}: {}",
60                self.message
61            ),
62            None => write!(formatter, "stream parse error: {}", self.message),
63        }
64    }
65}
66
67impl std::error::Error for StreamParseError {
68    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
69        self.parse_error
70            .as_deref()
71            .map(|error| error as &(dyn std::error::Error + 'static))
72    }
73}
74
75impl From<ParseError> for StreamParseError {
76    fn from(error: ParseError) -> Self {
77        Self {
78            message: error.to_string(),
79            location: None,
80            parse_error: Some(Box::new(error)),
81        }
82    }
83}
84
85/// Callback invoked for every completed link.
86pub type LinkCallback<T = String> = Box<dyn FnMut(&LiNo<T>) + Send>;
87
88/// Callback invoked when a write or finish operation fails.
89pub type ErrorCallback = Box<dyn FnMut(&StreamParseError) + Send>;
90
91mod sealed {
92    pub trait Sealed {}
93    impl Sealed for String {}
94    impl Sealed for crate::SyntaxReference {}
95}
96
97/// The reference type a [`StreamParser`] emits: `String` for the plain links
98/// (the AST mode), or [`SyntaxReference`] for links that record how each ID
99/// was written. The trait is sealed.
100pub trait StreamReference: sealed::Sealed + Clone + Send + 'static {
101    /// Parses one completed record.
102    #[doc(hidden)]
103    fn parse_record(document: &str, config: &ParserConfig) -> Result<Vec<LiNo<Self>>, ParseError>;
104}
105
106impl StreamReference for String {
107    fn parse_record(document: &str, config: &ParserConfig) -> Result<Vec<LiNo<Self>>, ParseError> {
108        parse_lino_to_links_with_config(document, config)
109    }
110}
111
112impl StreamReference for SyntaxReference {
113    fn parse_record(document: &str, config: &ParserConfig) -> Result<Vec<LiNo<Self>>, ParseError> {
114        // Each record is parsed on its own, so raw IDs are sliced from the
115        // record, which holds the exact text the stream received.
116        parse_lino_to_syntax_links_with_config(document, config)
117    }
118}
119
120/// Incrementally emits complete top-level Links Notation records.
121///
122/// A record can acquire indented children, so a terminating newline is not by
123/// itself a safe boundary. A record is committed when the next non-indented
124/// content line begins, and the canonical parser validates every committed
125/// segment. Set collection off for bounded-memory callback or iterator use.
126///
127/// `StreamParser::new()` emits `LiNo<String>`, like
128/// [`parse_lino_to_links`](crate::parse_lino_to_links).
129/// [`StreamParser::syntax`] emits `LiNo<SyntaxReference>`, like
130/// [`parse_lino_to_syntax_links`](crate::parse_lino_to_syntax_links), so every
131/// link remembers how its ID was written:
132///
133/// ```
134/// use links_notation::StreamParser;
135///
136/// let mut stream = StreamParser::syntax();
137/// stream.write("(t 1 '1')\n").unwrap();
138/// let links = stream.finish().unwrap();
139/// assert_eq!(links[0].to_string(), "(t 1 1)");
140/// let links_notation::LiNo::Link { values, .. } = &links[0] else { unreachable!() };
141/// assert_eq!(values[2].raw_id(), Some("'1'"));
142/// assert_eq!(values[2].quote(), Some('\''));
143/// ```
144pub struct StreamParser<T: StreamReference = String> {
145    config: ParserConfig,
146    buffer: String,
147    current_line: String,
148    base_indentation: Option<usize>,
149    line_classified: bool,
150    link_callback: Option<LinkCallback<T>>,
151    error_callback: Option<ErrorCallback>,
152    collect: bool,
153    max_buffer_size: usize,
154    links: Vec<LiNo<T>>,
155    offset: usize,
156    line: usize,
157    column: usize,
158    segment_offset: usize,
159    segment_line: usize,
160    ended: bool,
161}
162
163impl Default for StreamParser {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169impl StreamParser {
170    /// Create a parser with the canonical default options.
171    pub fn new() -> Self {
172        Self::with_config(ParserConfig::default())
173    }
174
175    /// Create a parser with explicit canonical parser options.
176    pub fn with_config(config: ParserConfig) -> Self {
177        Self::create(config)
178    }
179
180    /// Lazily parse any iterator of string-like chunks.
181    pub fn parse_chunks<I, S>(chunks: I) -> StreamIterator<I::IntoIter>
182    where
183        I: IntoIterator<Item = S>,
184        S: AsRef<str>,
185    {
186        StreamIterator::new(chunks.into_iter(), Self::new())
187    }
188}
189
190impl StreamParser<SyntaxReference> {
191    /// Create a parser whose links record how each ID was written.
192    pub fn syntax() -> Self {
193        Self::syntax_with_config(ParserConfig::default())
194    }
195
196    /// Create a parser with explicit options whose links record how each ID
197    /// was written.
198    pub fn syntax_with_config(config: ParserConfig) -> Self {
199        Self::create(config)
200    }
201
202    /// Lazily parse any iterator of string-like chunks into links that record
203    /// how each ID was written.
204    pub fn parse_syntax_chunks<I, S>(chunks: I) -> StreamIterator<I::IntoIter, SyntaxReference>
205    where
206        I: IntoIterator<Item = S>,
207        S: AsRef<str>,
208    {
209        StreamIterator::new(chunks.into_iter(), Self::syntax())
210    }
211}
212
213impl<T: StreamReference> StreamParser<T> {
214    fn create(config: ParserConfig) -> Self {
215        Self {
216            config,
217            buffer: String::new(),
218            current_line: String::new(),
219            base_indentation: None,
220            line_classified: false,
221            link_callback: None,
222            error_callback: None,
223            collect: true,
224            max_buffer_size: DEFAULT_MAX_BUFFER_SIZE,
225            links: Vec::new(),
226            offset: 0,
227            line: 1,
228            column: 1,
229            segment_offset: 0,
230            segment_line: 1,
231            ended: false,
232        }
233    }
234
235    /// Register a callback invoked once for every completed link.
236    pub fn on_link<F>(&mut self, callback: F) -> &mut Self
237    where
238        F: FnMut(&LiNo<T>) + Send + 'static,
239    {
240        self.link_callback = Some(Box::new(callback));
241        self
242    }
243
244    /// Register a callback invoked when parsing fails.
245    pub fn on_error<F>(&mut self, callback: F) -> &mut Self
246    where
247        F: FnMut(&StreamParseError) + Send + 'static,
248    {
249        self.error_callback = Some(Box::new(callback));
250        self
251    }
252
253    /// Enable or disable retaining links for [`finish`](Self::finish) and [`drain`](Self::drain).
254    pub fn set_collect(&mut self, collect: bool) -> &mut Self {
255        self.collect = collect;
256        self
257    }
258
259    /// Set the largest unresolved record accepted by the stream.
260    pub fn set_max_buffer_size(
261        &mut self,
262        max_buffer_size: usize,
263    ) -> Result<&mut Self, StreamParseError> {
264        if max_buffer_size == 0 {
265            return Err(StreamParseError {
266                message: "maximum buffer size must be positive".to_string(),
267                location: Some(self.position()),
268                parse_error: None,
269            });
270        }
271        self.max_buffer_size = max_buffer_size;
272        Ok(self)
273    }
274
275    /// Consume a string chunk and return links made complete by it.
276    pub fn write(&mut self, chunk: &str) -> Result<Vec<LiNo<T>>, StreamParseError> {
277        if self.ended {
278            return self.fail(StreamParseError {
279                message: "cannot write after finish()".to_string(),
280                location: Some(self.position()),
281                parse_error: None,
282            });
283        }
284
285        let mut emitted = Vec::new();
286        for character in chunk.chars() {
287            self.current_line.push(character);
288            self.offset += character.len_utf8();
289
290            if character == '\n' {
291                self.buffer.push_str(&self.current_line);
292                self.current_line.clear();
293                self.line_classified = false;
294                self.line += 1;
295                self.column = 1;
296            } else {
297                if !self.line_classified && !matches!(character, ' ' | '\t' | '\r') {
298                    self.line_classified = true;
299                    if !(self.config.comments && character == '#') {
300                        let indentation = leading_spaces(&self.current_line);
301                        self.start_content_line(indentation, &mut emitted);
302                    }
303                }
304                self.column += 1;
305            }
306
307            if self.buffer.len() + self.current_line.len() > self.max_buffer_size {
308                return self.fail(StreamParseError {
309                    message: format!(
310                        "buffered record exceeds maximum size of {} bytes",
311                        self.max_buffer_size
312                    ),
313                    location: Some(self.position()),
314                    parse_error: None,
315                });
316            }
317        }
318        Ok(emitted)
319    }
320
321    /// Finish the stream and return all undrained or newly emitted links.
322    pub fn finish(&mut self) -> Result<Vec<LiNo<T>>, StreamParseError> {
323        if self.ended {
324            return Ok(if self.collect {
325                self.links.clone()
326            } else {
327                Vec::new()
328            });
329        }
330
331        let mut emitted = Vec::new();
332        let document = format!("{}{}", self.buffer, self.current_line);
333        if !document.is_empty() {
334            let parsed = match T::parse_record(&document, &self.config) {
335                Ok(links) => links,
336                Err(error) => {
337                    let stream_error = self.stream_error(error);
338                    return self.fail(stream_error);
339                }
340            };
341            self.publish(parsed, &mut emitted);
342            self.advance_segment(&document);
343        }
344
345        self.buffer.clear();
346        self.current_line.clear();
347        self.base_indentation = None;
348        self.ended = true;
349        Ok(if self.collect {
350            self.links.clone()
351        } else {
352            emitted
353        })
354    }
355
356    /// Return and forget retained links.
357    pub fn drain(&mut self) -> Vec<LiNo<T>> {
358        std::mem::take(&mut self.links)
359    }
360
361    /// Reuse this parser while preserving its configuration and callbacks.
362    pub fn reset(&mut self) -> &mut Self {
363        self.buffer.clear();
364        self.current_line.clear();
365        self.base_indentation = None;
366        self.line_classified = false;
367        self.links.clear();
368        self.offset = 0;
369        self.line = 1;
370        self.column = 1;
371        self.segment_offset = 0;
372        self.segment_line = 1;
373        self.ended = false;
374        self
375    }
376
377    /// Return the absolute stream position and unresolved buffer size.
378    pub fn position(&self) -> StreamPosition {
379        StreamPosition {
380            line: self.line,
381            column: self.column,
382            offset: self.offset,
383            buffered: self.buffer.len() + self.current_line.len(),
384        }
385    }
386
387    fn start_content_line(&mut self, indentation: usize, emitted: &mut Vec<LiNo<T>>) {
388        if !self.buffer.is_empty()
389            && self
390                .base_indentation
391                .is_some_and(|base| indentation <= base)
392            && structurally_complete(&self.buffer, self.config.comments)
393        {
394            if let Ok(parsed) = T::parse_record(&self.buffer, &self.config) {
395                let document = std::mem::take(&mut self.buffer);
396                self.publish(parsed, emitted);
397                self.advance_segment(&document);
398                self.base_indentation = None;
399            }
400        }
401        self.base_indentation.get_or_insert(indentation);
402    }
403
404    fn publish(&mut self, parsed: Vec<LiNo<T>>, emitted: &mut Vec<LiNo<T>>) {
405        for link in parsed {
406            emitted.push(link.clone());
407            if self.collect {
408                self.links.push(link.clone());
409            }
410            if let Some(callback) = &mut self.link_callback {
411                callback(&link);
412            }
413        }
414    }
415
416    fn advance_segment(&mut self, document: &str) {
417        self.segment_offset += document.len();
418        self.segment_line += document.matches('\n').count();
419    }
420
421    fn stream_error(&self, error: ParseError) -> StreamParseError {
422        let location = match &error {
423            ParseError::SyntaxError(syntax) => StreamPosition {
424                line: self.segment_line + syntax.line - 1,
425                column: syntax.column,
426                offset: self.segment_offset + syntax.offset,
427                buffered: self.buffer.len() + self.current_line.len(),
428            },
429            ParseError::NestingTooDeep(nesting) => StreamPosition {
430                line: self.segment_line + nesting.line - 1,
431                column: nesting.column,
432                offset: self.segment_offset + nesting.offset,
433                buffered: self.buffer.len() + self.current_line.len(),
434            },
435            _ => StreamPosition {
436                line: self.segment_line,
437                column: 1,
438                offset: self.segment_offset,
439                buffered: self.buffer.len() + self.current_line.len(),
440            },
441        };
442        StreamParseError {
443            message: error.to_string(),
444            location: Some(location),
445            parse_error: Some(Box::new(error)),
446        }
447    }
448
449    fn fail<R>(&mut self, error: StreamParseError) -> Result<R, StreamParseError> {
450        if let Some(callback) = &mut self.error_callback {
451            callback(&error);
452        }
453        Err(error)
454    }
455}
456
457/// Lazy iterator returned by [`StreamParser::parse_chunks`] and
458/// [`StreamParser::parse_syntax_chunks`].
459pub struct StreamIterator<I, T: StreamReference = String> {
460    chunks: I,
461    parser: StreamParser<T>,
462    ready: VecDeque<LiNo<T>>,
463    finished: bool,
464}
465
466impl<I, T: StreamReference> StreamIterator<I, T> {
467    fn new(chunks: I, mut parser: StreamParser<T>) -> Self {
468        parser.set_collect(false);
469        Self {
470            chunks,
471            parser,
472            ready: VecDeque::new(),
473            finished: false,
474        }
475    }
476}
477
478impl<I, S, T> Iterator for StreamIterator<I, T>
479where
480    I: Iterator<Item = S>,
481    S: AsRef<str>,
482    T: StreamReference,
483{
484    type Item = Result<LiNo<T>, StreamParseError>;
485
486    fn next(&mut self) -> Option<Self::Item> {
487        loop {
488            if let Some(link) = self.ready.pop_front() {
489                return Some(Ok(link));
490            }
491            if self.finished {
492                return None;
493            }
494            match self.chunks.next() {
495                Some(chunk) => match self.parser.write(chunk.as_ref()) {
496                    Ok(links) => self.ready.extend(links),
497                    Err(error) => {
498                        self.finished = true;
499                        return Some(Err(error));
500                    }
501                },
502                None => {
503                    self.finished = true;
504                    match self.parser.finish() {
505                        Ok(links) => self.ready.extend(links),
506                        Err(error) => return Some(Err(error)),
507                    }
508                }
509            }
510        }
511    }
512}
513
514fn leading_spaces(line: &str) -> usize {
515    line.bytes().take_while(|byte| *byte == b' ').count()
516}
517
518fn follows(document: &[u8], position: usize, allowed: &[u8]) -> bool {
519    position == 0 || allowed.contains(&document[position - 1])
520}
521
522fn structurally_complete(document: &str, comments: bool) -> bool {
523    // Tokens are read as the comment reader and the parser read them: a
524    // reference may start after a separator, and right after a delimited
525    // reference or a literal (issue #377).
526    let bytes = document.as_bytes();
527    let references = DelimitedReferences::new(document);
528    let mut position = 0;
529    let mut depth = 0_isize;
530    let mut token_start = true;
531
532    while position < bytes.len() {
533        let byte = bytes[position];
534        if comments && byte == b'#' && follows(bytes, position, BEFORE_COMMENT) {
535            match document[position..].find('\n') {
536                Some(newline) => position += newline,
537                None => break,
538            }
539            continue;
540        }
541        if SEPARATORS.contains(&byte) {
542            match byte {
543                b'(' => depth += 1,
544                b')' => depth -= 1,
545                _ => {}
546            }
547            token_start = true;
548            position += 1;
549            continue;
550        }
551        if token_start && QUOTES.contains(&byte) {
552            // A reference nothing closes yet may be closed by a later line,
553            // which changes what it holds: an even run is the empty reference
554            // only when no run in the whole document closes it.
555            match references.read(&document[position..]) {
556                Some(reading) if reading.closed => position += reading.length,
557                _ => return false,
558            }
559            continue;
560        }
561        if token_start {
562            if let Some(end) = reference_literal_end(document, position) {
563                position = end;
564                continue;
565            }
566        }
567        token_start = false;
568        position += 1;
569    }
570    depth == 0
571}