Skip to main content

antlr4_runtime/
char_stream.rs

1// SPDX-License-Identifier: BSD-3-Clause
2// Copyright (c) 2026 Konstantin Vyatkin
3use crate::int_stream::{EOF, IntStream, UNKNOWN_SOURCE_NAME};
4use std::io;
5use std::rc::Rc;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub struct TextInterval {
9    pub start: usize,
10    pub stop: usize,
11}
12
13impl TextInterval {
14    pub const fn new(start: usize, stop: usize) -> Self {
15        Self { start, stop }
16    }
17
18    pub const fn empty() -> Self {
19        Self { start: 1, stop: 0 }
20    }
21
22    pub const fn is_empty(self) -> bool {
23        self.start > self.stop
24    }
25}
26
27/// Line/column effect of consuming a half-open character span.
28#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
29pub struct PositionSummary {
30    /// Number of newline characters in the span.
31    pub line_breaks: usize,
32    /// Number of characters after the final newline, or the complete span
33    /// length when no newline is present.
34    pub trailing_columns: usize,
35}
36
37impl PositionSummary {
38    /// Applies this summary to an existing one-based line and zero-based column.
39    pub const fn apply(self, line: usize, column: usize) -> (usize, usize) {
40        let line = line.saturating_add(self.line_breaks);
41        let column = if self.line_breaks == 0 {
42            column.saturating_add(self.trailing_columns)
43        } else {
44            self.trailing_columns
45        };
46        (line, column)
47    }
48}
49
50pub trait CharStream: IntStream {
51    fn text(&self, interval: TextInterval) -> String;
52
53    /// Reads one Unicode scalar at an absolute character index without moving
54    /// the stream cursor.
55    ///
56    /// Returning `None` leaves callers on the compatible `seek` + `la`
57    /// fallback. Implementations that support immutable access return
58    /// [`EOF`] when `index` is outside the input.
59    fn symbol_at(&self, _index: usize) -> Option<i32> {
60        None
61    }
62
63    /// Returns the complete input as ASCII bytes when character and byte
64    /// indexes are identical.
65    fn contiguous_ascii(&self) -> Option<&[u8]> {
66        None
67    }
68
69    /// Summarizes source-position changes for the half-open character interval
70    /// `[start, end)` without moving the stream cursor.
71    ///
72    /// Implementations may clamp `end` to the input size. Returning `None`
73    /// leaves callers on scalar replay.
74    fn position_summary(&self, _start: usize, _end: usize) -> Option<PositionSummary> {
75        None
76    }
77
78    /// Returns the complete backing UTF-8 source when it can be shared with a
79    /// token store.
80    ///
81    /// Implementations that return `None` remain supported, but their token
82    /// text is copied into the store's sparse explicit-text pool.
83    fn source_text(&self) -> Option<Rc<str>> {
84        None
85    }
86
87    fn byte_interval(&self, interval: TextInterval) -> Option<(usize, usize)> {
88        self.text_source_interval(interval)
89            .map(|(_, start, stop)| (start, stop))
90    }
91
92    fn text_source_interval(&self, _interval: TextInterval) -> Option<(Rc<str>, usize, usize)> {
93        None
94    }
95}
96
97#[derive(Clone, Debug)]
98pub struct InputStream {
99    source: Rc<str>,
100    data: InputData,
101    cursor: usize,
102    source_name: String,
103}
104
105#[derive(Clone, Debug)]
106enum InputData {
107    Ascii,
108    Unicode {
109        chars: Vec<char>,
110        byte_offsets: Vec<usize>,
111    },
112}
113
114impl InputData {
115    fn new(input: &str) -> Self {
116        if input.is_ascii() {
117            Self::Ascii
118        } else {
119            Self::Unicode {
120                chars: input.chars().collect(),
121                byte_offsets: input.char_indices().map(|(index, _)| index).collect(),
122            }
123        }
124    }
125
126    const fn len(&self, source: &str) -> usize {
127        match self {
128            Self::Ascii => source.len(),
129            Self::Unicode { chars, .. } => chars.len(),
130        }
131    }
132
133    fn get(&self, source: &str, index: usize) -> Option<char> {
134        match self {
135            Self::Ascii => source.as_bytes().get(index).map(|byte| char::from(*byte)),
136            Self::Unicode { chars, .. } => chars.get(index).copied(),
137        }
138    }
139
140    fn byte_bounds(&self, source: &str, start: usize, stop: usize) -> Option<(usize, usize)> {
141        match self {
142            Self::Ascii => Some((start, stop + 1)),
143            Self::Unicode { byte_offsets, .. } => {
144                let start_byte = *byte_offsets.get(start)?;
145                let stop_byte = byte_offsets.get(stop + 1).copied().unwrap_or(source.len());
146                Some((start_byte, stop_byte))
147            }
148        }
149    }
150}
151
152impl InputStream {
153    /// Creates a character stream by draining UTF-8 text from a
154    /// [`std::io::Read`], using ANTLR's unknown source name placeholder.
155    ///
156    /// # Errors
157    ///
158    /// Returns any I/O error produced while reading, including
159    /// [`io::ErrorKind::InvalidData`] when the input is not valid UTF-8.
160    pub fn from_reader(reader: impl io::Read) -> io::Result<Self> {
161        Self::from_reader_with_source_name(reader, UNKNOWN_SOURCE_NAME)
162    }
163
164    /// Creates a named character stream by draining UTF-8 text from a
165    /// [`std::io::Read`].
166    ///
167    /// # Errors
168    ///
169    /// Returns any I/O error produced while reading, including
170    /// [`io::ErrorKind::InvalidData`] when the input is not valid UTF-8.
171    pub fn from_reader_with_source_name(
172        mut reader: impl io::Read,
173        source_name: impl Into<String>,
174    ) -> io::Result<Self> {
175        let mut input = String::new();
176        reader.read_to_string(&mut input)?;
177        Ok(Self::with_source_name(input, source_name))
178    }
179
180    /// Creates a character stream from UTF-8 text using ANTLR's unknown source
181    /// name placeholder.
182    pub fn new(input: impl AsRef<str>) -> Self {
183        Self::with_source_name(input, UNKNOWN_SOURCE_NAME)
184    }
185
186    /// Creates a character stream with an explicit source name for tokens and
187    /// diagnostics.
188    pub fn with_source_name(input: impl AsRef<str>, source_name: impl Into<String>) -> Self {
189        let input = input.as_ref();
190        Self {
191            source: Rc::from(input),
192            data: InputData::new(input),
193            cursor: 0,
194            source_name: source_name.into(),
195        }
196    }
197
198    /// Returns true when the cursor has reached or passed the end of input.
199    pub fn is_eof(&self) -> bool {
200        self.cursor >= self.data.len(&self.source)
201    }
202}
203
204impl IntStream for InputStream {
205    fn consume(&mut self) {
206        if !self.is_eof() {
207            self.cursor += 1;
208        }
209    }
210
211    fn la(&mut self, offset: isize) -> i32 {
212        if offset == 0 {
213            return 0;
214        }
215
216        let absolute = if offset > 0 {
217            self.cursor.checked_add((offset - 1).cast_unsigned())
218        } else {
219            offset
220                .checked_neg()
221                .and_then(|distance| usize::try_from(distance).ok())
222                .and_then(|distance| self.cursor.checked_sub(distance))
223        };
224
225        absolute
226            .and_then(|index| self.data.get(&self.source, index))
227            .map_or(EOF, |ch| ch as i32)
228    }
229
230    fn index(&self) -> usize {
231        self.cursor
232    }
233
234    fn seek(&mut self, index: usize) {
235        self.cursor = index.min(self.data.len(&self.source));
236    }
237
238    fn size(&self) -> usize {
239        self.data.len(&self.source)
240    }
241
242    fn source_name(&self) -> &str {
243        &self.source_name
244    }
245}
246
247impl CharStream for InputStream {
248    /// Returns text for an inclusive interval of Unicode scalar indices.
249    fn text(&self, interval: TextInterval) -> String {
250        if let Some((source, start, stop)) = self.text_source_interval(interval) {
251            return source[start..stop].to_owned();
252        }
253        String::new()
254    }
255
256    fn symbol_at(&self, index: usize) -> Option<i32> {
257        Some(
258            self.data
259                .get(&self.source, index)
260                .map_or(EOF, |ch| u32::from(ch).cast_signed()),
261        )
262    }
263
264    fn contiguous_ascii(&self) -> Option<&[u8]> {
265        matches!(self.data, InputData::Ascii).then(|| self.source.as_bytes())
266    }
267
268    fn position_summary(&self, start: usize, end: usize) -> Option<PositionSummary> {
269        if start > end {
270            return None;
271        }
272        let len = self.data.len(&self.source);
273        let start = start.min(len);
274        let end = end.min(len);
275
276        let mut summary = PositionSummary::default();
277        let mut note = |is_newline| {
278            if is_newline {
279                summary.line_breaks += 1;
280                summary.trailing_columns = 0;
281            } else {
282                summary.trailing_columns += 1;
283            }
284        };
285        match &self.data {
286            InputData::Ascii => {
287                for &byte in &self.source.as_bytes()[start..end] {
288                    note(byte == b'\n');
289                }
290            }
291            InputData::Unicode { chars, .. } => {
292                for &ch in &chars[start..end] {
293                    note(ch == '\n');
294                }
295            }
296        }
297        Some(summary)
298    }
299
300    fn text_source_interval(&self, interval: TextInterval) -> Option<(Rc<str>, usize, usize)> {
301        let len = self.data.len(&self.source);
302        if interval.is_empty() || len == 0 {
303            return None;
304        }
305
306        let start = interval.start.min(len);
307        let stop = interval.stop.min(len.saturating_sub(1));
308        if start > stop {
309            return None;
310        }
311
312        let (start_byte, stop_byte) = self.data.byte_bounds(&self.source, start, stop)?;
313        Some((Rc::clone(&self.source), start_byte, stop_byte))
314    }
315
316    fn source_text(&self) -> Option<Rc<str>> {
317        Some(Rc::clone(&self.source))
318    }
319
320    fn byte_interval(&self, interval: TextInterval) -> Option<(usize, usize)> {
321        let len = self.data.len(&self.source);
322        if interval.is_empty() || len == 0 {
323            return None;
324        }
325        let start = interval.start.min(len);
326        let stop = interval.stop.min(len.saturating_sub(1));
327        (start <= stop)
328            .then(|| self.data.byte_bounds(&self.source, start, stop))
329            .flatten()
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn lookahead_and_text_are_codepoint_indexed() {
339        let mut input = InputStream::with_source_name("aβ\n", "sample");
340        assert_eq!(input.source_name(), "sample");
341        assert_eq!(input.size(), 3);
342        assert_eq!(input.la(1), 'a' as i32);
343        assert_eq!(input.la(2), 'β' as i32);
344        assert_eq!(input.text(TextInterval::new(0, 1)), "aβ");
345        input.consume();
346        assert_eq!(input.index(), 1);
347        assert_eq!(input.la(-1), 'a' as i32);
348        assert_eq!(input.la(isize::MIN), EOF);
349        input.seek(99);
350        assert_eq!(input.la(1), EOF);
351    }
352
353    #[test]
354    fn optional_fast_paths_preserve_scalar_indexes_and_positions() {
355        let ascii = InputStream::new("ab\ncd");
356        assert_eq!(ascii.contiguous_ascii(), Some(&b"ab\ncd"[..]));
357        assert_eq!(ascii.symbol_at(2), Some('\n' as i32));
358        assert_eq!(ascii.symbol_at(5), Some(EOF));
359        assert_eq!(
360            ascii.position_summary(1, 5),
361            Some(PositionSummary {
362                line_breaks: 1,
363                trailing_columns: 2,
364            })
365        );
366        assert_eq!(
367            ascii.position_summary(5, 99),
368            Some(PositionSummary::default())
369        );
370        assert_eq!(ascii.position_summary(4, 2), None);
371        assert_eq!(ascii.position_summary(7, 6), None);
372
373        let unicode = InputStream::new("aβ\nγ");
374        assert_eq!(unicode.contiguous_ascii(), None);
375        assert_eq!(unicode.symbol_at(1), Some('β' as i32));
376        assert_eq!(unicode.symbol_at(4), Some(EOF));
377        assert_eq!(
378            unicode.position_summary(1, 4),
379            Some(PositionSummary {
380                line_breaks: 1,
381                trailing_columns: 1,
382            })
383        );
384    }
385
386    #[test]
387    fn position_summary_applies_to_existing_coordinates() {
388        assert_eq!(
389            PositionSummary {
390                line_breaks: 0,
391                trailing_columns: 3,
392            }
393            .apply(4, 7),
394            (4, 10)
395        );
396        assert_eq!(
397            PositionSummary {
398                line_breaks: 2,
399                trailing_columns: 3,
400            }
401            .apply(4, 7),
402            (6, 3)
403        );
404    }
405
406    #[test]
407    fn reader_constructors_decode_utf8_and_preserve_source_names() {
408        let mut named = InputStream::from_reader_with_source_name(
409            io::Cursor::new("aβ\n".as_bytes()),
410            "sample.txt",
411        )
412        .expect("in-memory UTF-8 should be readable");
413        assert_eq!(named.source_name(), "sample.txt");
414        assert_eq!(named.size(), 3);
415        assert_eq!(named.la(2), 'β' as i32);
416
417        let unnamed = InputStream::from_reader(io::Cursor::new(b"text"))
418            .expect("in-memory UTF-8 should be readable");
419        assert_eq!(unnamed.source_name(), UNKNOWN_SOURCE_NAME);
420        assert_eq!(unnamed.text(TextInterval::new(0, 3)), "text");
421    }
422
423    #[test]
424    fn reader_constructor_rejects_invalid_utf8() {
425        let error = InputStream::from_reader(io::Cursor::new([0xFF]))
426            .expect_err("invalid UTF-8 must not produce a character stream");
427        assert_eq!(error.kind(), io::ErrorKind::InvalidData);
428    }
429}