1use crate::int_stream::{EOF, IntStream, UNKNOWN_SOURCE_NAME};
2use std::io;
3use std::rc::Rc;
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6pub struct TextInterval {
7 pub start: usize,
8 pub stop: usize,
9}
10
11impl TextInterval {
12 pub const fn new(start: usize, stop: usize) -> Self {
13 Self { start, stop }
14 }
15
16 pub const fn empty() -> Self {
17 Self { start: 1, stop: 0 }
18 }
19
20 pub const fn is_empty(self) -> bool {
21 self.start > self.stop
22 }
23}
24
25#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
27pub struct PositionSummary {
28 pub line_breaks: usize,
30 pub trailing_columns: usize,
33}
34
35impl PositionSummary {
36 pub const fn apply(self, line: usize, column: usize) -> (usize, usize) {
38 let line = line.saturating_add(self.line_breaks);
39 let column = if self.line_breaks == 0 {
40 column.saturating_add(self.trailing_columns)
41 } else {
42 self.trailing_columns
43 };
44 (line, column)
45 }
46}
47
48pub trait CharStream: IntStream {
49 fn text(&self, interval: TextInterval) -> String;
50
51 fn symbol_at(&self, _index: usize) -> Option<i32> {
58 None
59 }
60
61 fn contiguous_ascii(&self) -> Option<&[u8]> {
64 None
65 }
66
67 fn position_summary(&self, _start: usize, _end: usize) -> Option<PositionSummary> {
73 None
74 }
75
76 fn source_text(&self) -> Option<Rc<str>> {
82 None
83 }
84
85 fn byte_interval(&self, interval: TextInterval) -> Option<(usize, usize)> {
86 self.text_source_interval(interval)
87 .map(|(_, start, stop)| (start, stop))
88 }
89
90 fn text_source_interval(&self, _interval: TextInterval) -> Option<(Rc<str>, usize, usize)> {
91 None
92 }
93}
94
95#[derive(Clone, Debug)]
96pub struct InputStream {
97 source: Rc<str>,
98 data: InputData,
99 cursor: usize,
100 source_name: String,
101}
102
103#[derive(Clone, Debug)]
104enum InputData {
105 Ascii,
106 Unicode {
107 chars: Vec<char>,
108 byte_offsets: Vec<usize>,
109 },
110}
111
112impl InputData {
113 fn new(input: &str) -> Self {
114 if input.is_ascii() {
115 Self::Ascii
116 } else {
117 Self::Unicode {
118 chars: input.chars().collect(),
119 byte_offsets: input.char_indices().map(|(index, _)| index).collect(),
120 }
121 }
122 }
123
124 const fn len(&self, source: &str) -> usize {
125 match self {
126 Self::Ascii => source.len(),
127 Self::Unicode { chars, .. } => chars.len(),
128 }
129 }
130
131 fn get(&self, source: &str, index: usize) -> Option<char> {
132 match self {
133 Self::Ascii => source.as_bytes().get(index).map(|byte| char::from(*byte)),
134 Self::Unicode { chars, .. } => chars.get(index).copied(),
135 }
136 }
137
138 fn byte_bounds(&self, source: &str, start: usize, stop: usize) -> Option<(usize, usize)> {
139 match self {
140 Self::Ascii => Some((start, stop + 1)),
141 Self::Unicode { byte_offsets, .. } => {
142 let start_byte = *byte_offsets.get(start)?;
143 let stop_byte = byte_offsets.get(stop + 1).copied().unwrap_or(source.len());
144 Some((start_byte, stop_byte))
145 }
146 }
147 }
148}
149
150impl InputStream {
151 pub fn from_reader(reader: impl io::Read) -> io::Result<Self> {
159 Self::from_reader_with_source_name(reader, UNKNOWN_SOURCE_NAME)
160 }
161
162 pub fn from_reader_with_source_name(
170 mut reader: impl io::Read,
171 source_name: impl Into<String>,
172 ) -> io::Result<Self> {
173 let mut input = String::new();
174 reader.read_to_string(&mut input)?;
175 Ok(Self::with_source_name(input, source_name))
176 }
177
178 pub fn new(input: impl AsRef<str>) -> Self {
181 Self::with_source_name(input, UNKNOWN_SOURCE_NAME)
182 }
183
184 pub fn with_source_name(input: impl AsRef<str>, source_name: impl Into<String>) -> Self {
187 let input = input.as_ref();
188 Self {
189 source: Rc::from(input),
190 data: InputData::new(input),
191 cursor: 0,
192 source_name: source_name.into(),
193 }
194 }
195
196 pub fn is_eof(&self) -> bool {
198 self.cursor >= self.data.len(&self.source)
199 }
200}
201
202impl IntStream for InputStream {
203 fn consume(&mut self) {
204 if !self.is_eof() {
205 self.cursor += 1;
206 }
207 }
208
209 fn la(&mut self, offset: isize) -> i32 {
210 if offset == 0 {
211 return 0;
212 }
213
214 let absolute = if offset > 0 {
215 self.cursor.checked_add((offset - 1).cast_unsigned())
216 } else {
217 offset
218 .checked_neg()
219 .and_then(|distance| usize::try_from(distance).ok())
220 .and_then(|distance| self.cursor.checked_sub(distance))
221 };
222
223 absolute
224 .and_then(|index| self.data.get(&self.source, index))
225 .map_or(EOF, |ch| ch as i32)
226 }
227
228 fn index(&self) -> usize {
229 self.cursor
230 }
231
232 fn seek(&mut self, index: usize) {
233 self.cursor = index.min(self.data.len(&self.source));
234 }
235
236 fn size(&self) -> usize {
237 self.data.len(&self.source)
238 }
239
240 fn source_name(&self) -> &str {
241 &self.source_name
242 }
243}
244
245impl CharStream for InputStream {
246 fn text(&self, interval: TextInterval) -> String {
248 if let Some((source, start, stop)) = self.text_source_interval(interval) {
249 return source[start..stop].to_owned();
250 }
251 String::new()
252 }
253
254 fn symbol_at(&self, index: usize) -> Option<i32> {
255 Some(
256 self.data
257 .get(&self.source, index)
258 .map_or(EOF, |ch| u32::from(ch).cast_signed()),
259 )
260 }
261
262 fn contiguous_ascii(&self) -> Option<&[u8]> {
263 matches!(self.data, InputData::Ascii).then(|| self.source.as_bytes())
264 }
265
266 fn position_summary(&self, start: usize, end: usize) -> Option<PositionSummary> {
267 if start > end {
268 return None;
269 }
270 let len = self.data.len(&self.source);
271 let start = start.min(len);
272 let end = end.min(len);
273
274 let mut summary = PositionSummary::default();
275 let mut note = |is_newline| {
276 if is_newline {
277 summary.line_breaks += 1;
278 summary.trailing_columns = 0;
279 } else {
280 summary.trailing_columns += 1;
281 }
282 };
283 match &self.data {
284 InputData::Ascii => {
285 for &byte in &self.source.as_bytes()[start..end] {
286 note(byte == b'\n');
287 }
288 }
289 InputData::Unicode { chars, .. } => {
290 for &ch in &chars[start..end] {
291 note(ch == '\n');
292 }
293 }
294 }
295 Some(summary)
296 }
297
298 fn text_source_interval(&self, interval: TextInterval) -> Option<(Rc<str>, usize, usize)> {
299 let len = self.data.len(&self.source);
300 if interval.is_empty() || len == 0 {
301 return None;
302 }
303
304 let start = interval.start.min(len);
305 let stop = interval.stop.min(len.saturating_sub(1));
306 if start > stop {
307 return None;
308 }
309
310 let (start_byte, stop_byte) = self.data.byte_bounds(&self.source, start, stop)?;
311 Some((Rc::clone(&self.source), start_byte, stop_byte))
312 }
313
314 fn source_text(&self) -> Option<Rc<str>> {
315 Some(Rc::clone(&self.source))
316 }
317
318 fn byte_interval(&self, interval: TextInterval) -> Option<(usize, usize)> {
319 let len = self.data.len(&self.source);
320 if interval.is_empty() || len == 0 {
321 return None;
322 }
323 let start = interval.start.min(len);
324 let stop = interval.stop.min(len.saturating_sub(1));
325 (start <= stop)
326 .then(|| self.data.byte_bounds(&self.source, start, stop))
327 .flatten()
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 #[test]
336 fn lookahead_and_text_are_codepoint_indexed() {
337 let mut input = InputStream::with_source_name("aβ\n", "sample");
338 assert_eq!(input.source_name(), "sample");
339 assert_eq!(input.size(), 3);
340 assert_eq!(input.la(1), 'a' as i32);
341 assert_eq!(input.la(2), 'β' as i32);
342 assert_eq!(input.text(TextInterval::new(0, 1)), "aβ");
343 input.consume();
344 assert_eq!(input.index(), 1);
345 assert_eq!(input.la(-1), 'a' as i32);
346 assert_eq!(input.la(isize::MIN), EOF);
347 input.seek(99);
348 assert_eq!(input.la(1), EOF);
349 }
350
351 #[test]
352 fn optional_fast_paths_preserve_scalar_indexes_and_positions() {
353 let ascii = InputStream::new("ab\ncd");
354 assert_eq!(ascii.contiguous_ascii(), Some(&b"ab\ncd"[..]));
355 assert_eq!(ascii.symbol_at(2), Some('\n' as i32));
356 assert_eq!(ascii.symbol_at(5), Some(EOF));
357 assert_eq!(
358 ascii.position_summary(1, 5),
359 Some(PositionSummary {
360 line_breaks: 1,
361 trailing_columns: 2,
362 })
363 );
364 assert_eq!(
365 ascii.position_summary(5, 99),
366 Some(PositionSummary::default())
367 );
368 assert_eq!(ascii.position_summary(4, 2), None);
369 assert_eq!(ascii.position_summary(7, 6), None);
370
371 let unicode = InputStream::new("aβ\nγ");
372 assert_eq!(unicode.contiguous_ascii(), None);
373 assert_eq!(unicode.symbol_at(1), Some('β' as i32));
374 assert_eq!(unicode.symbol_at(4), Some(EOF));
375 assert_eq!(
376 unicode.position_summary(1, 4),
377 Some(PositionSummary {
378 line_breaks: 1,
379 trailing_columns: 1,
380 })
381 );
382 }
383
384 #[test]
385 fn position_summary_applies_to_existing_coordinates() {
386 assert_eq!(
387 PositionSummary {
388 line_breaks: 0,
389 trailing_columns: 3,
390 }
391 .apply(4, 7),
392 (4, 10)
393 );
394 assert_eq!(
395 PositionSummary {
396 line_breaks: 2,
397 trailing_columns: 3,
398 }
399 .apply(4, 7),
400 (6, 3)
401 );
402 }
403
404 #[test]
405 fn reader_constructors_decode_utf8_and_preserve_source_names() {
406 let mut named = InputStream::from_reader_with_source_name(
407 io::Cursor::new("aβ\n".as_bytes()),
408 "sample.txt",
409 )
410 .expect("in-memory UTF-8 should be readable");
411 assert_eq!(named.source_name(), "sample.txt");
412 assert_eq!(named.size(), 3);
413 assert_eq!(named.la(2), 'β' as i32);
414
415 let unnamed = InputStream::from_reader(io::Cursor::new(b"text"))
416 .expect("in-memory UTF-8 should be readable");
417 assert_eq!(unnamed.source_name(), UNKNOWN_SOURCE_NAME);
418 assert_eq!(unnamed.text(TextInterval::new(0, 3)), "text");
419 }
420
421 #[test]
422 fn reader_constructor_rejects_invalid_utf8() {
423 let error = InputStream::from_reader(io::Cursor::new([0xFF]))
424 .expect_err("invalid UTF-8 must not produce a character stream");
425 assert_eq!(error.kind(), io::ErrorKind::InvalidData);
426 }
427}