1use 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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
29pub struct PositionSummary {
30 pub line_breaks: usize,
32 pub trailing_columns: usize,
35}
36
37impl PositionSummary {
38 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 fn symbol_at(&self, _index: usize) -> Option<i32> {
60 None
61 }
62
63 fn contiguous_ascii(&self) -> Option<&[u8]> {
66 None
67 }
68
69 fn position_summary(&self, _start: usize, _end: usize) -> Option<PositionSummary> {
75 None
76 }
77
78 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 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 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 pub fn new(input: impl AsRef<str>) -> Self {
183 Self::with_source_name(input, UNKNOWN_SOURCE_NAME)
184 }
185
186 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 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 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}