antlr4_runtime/byte_stream.rs
1// SPDX-License-Identifier: BSD-3-Clause
2// Copyright (c) 2026 Konstantin Vyatkin
3//! A byte-oriented [`CharStream`] for parsing binary formats.
4//!
5//! ANTLR grammars normally consume Unicode text, but many real-world formats
6//! are raw bytes: chunk containers (RIFF/WAV), fixed-width records (tar), and
7//! self-describing tag streams (CBOR, Standard MIDI). The reference runtimes
8//! parse these by treating each byte as a codepoint in `U+0000..=U+00FF` (a
9//! "Latin-1" view) and writing lexer rules over `' '..'ÿ'`.
10//!
11//! [`InputStream`](crate::InputStream) can do this too, but only after decoding
12//! the bytes into a `String`: any byte `>= 0x80` is not valid UTF-8 on its own,
13//! so the whole input takes the non-ASCII path and is materialized into a
14//! `Vec<char>` plus a byte-offset table — roughly 12 bytes of heap per input
15//! byte, and the compiled-DFA ASCII scanner is disabled.
16//!
17//! `ByteStream` avoids all of that. It is generic over any `AsRef<[u8]>`
18//! backing store, so stream index equals byte offset, lookahead is a single
19//! array read, and there is no transcoding or auxiliary allocation.
20//!
21//! # Mapping to Rust IO primitives
22//!
23//! ANTLR parsing needs random access — the lexer and parser `seek`, look
24//! behind with `la(-1)`, and `mark`/`release` for prediction — so the bytes
25//! must live fully in memory; `ByteStream` cannot lazily pull from a socket
26//! mid-parse. The design instead meets the two IO shapes that matter:
27//!
28//! - **Bytes you already hold** (a network read buffer, an `mmap`, a slice of a
29//! larger frame): borrow them zero-copy with `ByteStream::new(&buf[..])`.
30//! Nothing is copied; the stream lives as long as the borrow.
31//! - **A reader** (`File`, `TcpStream`, `Stdin`, `Cursor`): drain it into an
32//! owned buffer with [`ByteStream::from_reader`], which is just a thin
33//! wrapper over [`std::io::Read::read_to_end`].
34//! - **An owned `Vec<u8>`**: hand it over with `ByteStream::new(vec)` and the
35//! stream takes ownership without copying.
36//!
37//! ```ignore
38//! // From a file:
39//! let stream = ByteStream::from_reader(std::fs::File::open(path)?)?;
40//! // Zero-copy from an in-memory buffer (e.g. bytes read off a socket):
41//! let stream = ByteStream::new(&packet[..]);
42//!
43//! // Feed it to any generated parser built from a byte-oriented grammar.
44//! let parsed =
45//! foo_parser::parse_stream(stream, FooLexer::new, FooParser::entry_rule)?;
46//! ```
47//!
48//! Write lexer rules against the byte range, e.g. `BYTE : ' ' .. 'ÿ';`. A
49//! complete worked example — a Standard MIDI File grammar parsed over a
50//! `ByteStream` — lives under
51//! `crates/antlr-rust-codegen/tests/fixtures/antlr4-rust-gen/midi-binary/`.
52//!
53//! # Token text is hex
54//!
55//! Because the bytes are not text, [`CharStream::text`] renders the matched
56//! span as a lowercase hex string with no separators (`[0xDE, 0xAD]` becomes
57//! `"dead"`). Token *positions* are still exact byte offsets; use
58//! [`crate::IntStream::index`] or a token's byte span when you need to slice
59//! the original bytes.
60
61use std::io;
62
63use crate::char_stream::{CharStream, PositionSummary, TextInterval};
64use crate::int_stream::{EOF, IntStream, UNKNOWN_SOURCE_NAME};
65
66/// A [`CharStream`] backed by raw bytes, where each byte is one symbol in
67/// `0..=255` and the stream index is the byte offset.
68///
69/// Generic over the backing store `B: AsRef<[u8]>`: use `Vec<u8>` for owned
70/// bytes or `&[u8]` to borrow an existing buffer zero-copy. See the
71/// [module documentation](self) for how this maps onto Rust IO primitives.
72#[derive(Clone, Debug)]
73pub struct ByteStream<B = Vec<u8>> {
74 bytes: B,
75 cursor: usize,
76 source_name: String,
77}
78
79impl ByteStream<Vec<u8>> {
80 /// Creates a byte stream by draining a [`std::io::Read`] into an owned
81 /// buffer — the bridge for files, sockets, stdin, and [`std::io::Cursor`].
82 ///
83 /// # Errors
84 ///
85 /// Returns any error produced while reading `reader` to end.
86 pub fn from_reader(mut reader: impl io::Read) -> io::Result<Self> {
87 let mut bytes = Vec::new();
88 reader.read_to_end(&mut bytes)?;
89 Ok(Self::new(bytes))
90 }
91}
92
93impl<B: AsRef<[u8]>> ByteStream<B> {
94 /// Creates a byte stream over `bytes`, using ANTLR's unknown source-name
95 /// placeholder.
96 ///
97 /// `bytes` may be an owned `Vec<u8>` or a borrowed `&[u8]` (zero-copy).
98 pub fn new(bytes: B) -> Self {
99 Self::with_source_name(bytes, UNKNOWN_SOURCE_NAME)
100 }
101
102 /// Creates a byte stream with an explicit source name for tokens and
103 /// diagnostics.
104 pub fn with_source_name(bytes: B, source_name: impl Into<String>) -> Self {
105 Self {
106 bytes,
107 cursor: 0,
108 source_name: source_name.into(),
109 }
110 }
111
112 /// Returns the backing bytes.
113 #[must_use]
114 pub fn bytes(&self) -> &[u8] {
115 self.bytes.as_ref()
116 }
117
118 /// Returns true when the cursor has reached or passed the end of input.
119 #[must_use]
120 pub fn is_eof(&self) -> bool {
121 self.cursor >= self.bytes.as_ref().len()
122 }
123}
124
125impl<B: AsRef<[u8]>> IntStream for ByteStream<B> {
126 fn consume(&mut self) {
127 if !self.is_eof() {
128 self.cursor += 1;
129 }
130 }
131
132 fn la(&mut self, offset: isize) -> i32 {
133 if offset == 0 {
134 return 0;
135 }
136
137 // Mirror `InputStream::la`: `+1` is the symbol under the cursor, and
138 // negative offsets look behind. `checked_*` keeps `isize::MIN` and
139 // out-of-range lookahead on the EOF path instead of panicking.
140 let absolute = if offset > 0 {
141 self.cursor.checked_add((offset - 1).cast_unsigned())
142 } else {
143 offset
144 .checked_neg()
145 .and_then(|distance| usize::try_from(distance).ok())
146 .and_then(|distance| self.cursor.checked_sub(distance))
147 };
148
149 absolute.map_or(EOF, |index| self.symbol_at(index).unwrap_or(EOF))
150 }
151
152 fn index(&self) -> usize {
153 self.cursor
154 }
155
156 fn seek(&mut self, index: usize) {
157 self.cursor = index.min(self.bytes.as_ref().len());
158 }
159
160 fn size(&self) -> usize {
161 self.bytes.as_ref().len()
162 }
163
164 fn source_name(&self) -> &str {
165 &self.source_name
166 }
167}
168
169impl<B: AsRef<[u8]>> CharStream for ByteStream<B> {
170 /// Renders the inclusive byte interval as a lowercase, separator-free hex
171 /// string. See the [module documentation](self) for the rationale.
172 fn text(&self, interval: TextInterval) -> String {
173 // Clamp `stop` before any `+1`, mirroring `InputStream`: a caller
174 // passing `TextInterval::new(_, usize::MAX)` (e.g. an EOF token span)
175 // must not overflow.
176 let bytes = self.bytes.as_ref();
177 let len = bytes.len();
178 if interval.is_empty() || len == 0 {
179 return String::new();
180 }
181 let start = interval.start.min(len);
182 let stop = interval.stop.min(len - 1);
183 if start > stop {
184 return String::new();
185 }
186 use std::fmt::Write as _;
187 bytes[start..=stop].iter().fold(
188 String::with_capacity((stop - start + 1) * 2),
189 |mut acc, byte| {
190 // Writing to a String is infallible.
191 let _ = write!(acc, "{byte:02x}");
192 acc
193 },
194 )
195 }
196
197 fn symbol_at(&self, index: usize) -> Option<i32> {
198 Some(
199 self.bytes
200 .as_ref()
201 .get(index)
202 .map_or(EOF, |&byte| i32::from(byte)),
203 )
204 }
205
206 // NOTE: `contiguous_ascii` is deliberately NOT implemented. That fast path
207 // feeds bytes into a 128-wide ASCII DFA row (`ascii_target`), which is only
208 // valid for 7-bit input; bytes `>= 0x80` route correctly through the
209 // generic path's `wide_rows` instead.
210
211 /// Summarizes line/column movement over `[start, end)` by scanning the raw
212 /// bytes.
213 ///
214 /// Without this, [`BaseLexer`](crate::lexer::BaseLexer) would fall back to
215 /// iterating [`Self::text`], which is hex — so a span of N bytes would count
216 /// as 2N columns and `0x0A` newline bytes would be invisible, corrupting the
217 /// line/column of split or synthesized tokens.
218 fn position_summary(&self, start: usize, end: usize) -> Option<PositionSummary> {
219 let bytes = self.bytes.as_ref();
220 let len = bytes.len();
221 if start > end {
222 return None;
223 }
224 let start = start.min(len);
225 let end = end.min(len);
226 let mut summary = PositionSummary::default();
227 for &byte in &bytes[start..end] {
228 if byte == b'\n' {
229 summary.line_breaks += 1;
230 summary.trailing_columns = 0;
231 } else {
232 summary.trailing_columns += 1;
233 }
234 }
235 Some(summary)
236 }
237
238 fn byte_interval(&self, interval: TextInterval) -> Option<(usize, usize)> {
239 // Index == byte offset, so the byte span is exact. Clamp `stop` before
240 // the `+1` for the same overflow reason as `text`.
241 let len = self.bytes.as_ref().len();
242 if interval.is_empty() || len == 0 {
243 return None;
244 }
245 let start = interval.start.min(len);
246 let stop = interval.stop.min(len - 1);
247 (start <= stop).then_some((start, stop + 1))
248 }
249}
250
251#[cfg(test)]
252#[allow(clippy::disallowed_methods)] // insta assertion macros unwrap internal I/O.
253mod tests {
254 use super::*;
255
256 #[test]
257 fn lookahead_reads_bytes_including_high_bytes() {
258 let mut stream = ByteStream::new(vec![0x00, 0x7F, 0x80, 0xFF]);
259 assert_eq!(stream.la(0), 0, "la(0) is the ANTLR sentinel, not EOF");
260 assert_eq!(stream.la(1), 0x00);
261 assert_eq!(stream.la(2), 0x7F);
262 assert_eq!(stream.la(3), 0x80, "high byte is 128, not sign-extended");
263 assert_eq!(stream.la(4), 0xFF);
264 assert_eq!(stream.la(5), EOF);
265 stream.consume();
266 assert_eq!(stream.index(), 1);
267 assert_eq!(stream.la(-1), 0x00);
268 assert_eq!(stream.la(isize::MIN), EOF, "no panic on extreme offset");
269 }
270
271 #[test]
272 fn consume_stops_at_eof_and_seek_clamps() {
273 let mut stream = ByteStream::new(vec![0x01, 0x02]);
274 assert_eq!(stream.size(), 2);
275 stream.consume();
276 stream.consume();
277 stream.consume(); // past EOF is a no-op
278 assert_eq!(stream.index(), 2);
279 assert!(stream.is_eof());
280 stream.seek(99);
281 assert_eq!(stream.index(), 2, "seek clamps to size");
282 stream.seek(1);
283 assert_eq!(stream.la(1), 0x02);
284 }
285
286 #[test]
287 fn text_is_lowercase_hex_and_byte_interval_is_exact() {
288 let stream = ByteStream::new(vec![0xDE, 0xAD, 0xBE, 0xEF]);
289 assert_eq!(stream.text(TextInterval::new(0, 3)), "deadbeef");
290 assert_eq!(stream.text(TextInterval::new(1, 2)), "adbe");
291 assert_eq!(stream.text(TextInterval::empty()), "");
292 // Inclusive char interval [1, 2] -> half-open byte span [1, 3).
293 assert_eq!(stream.byte_interval(TextInterval::new(1, 2)), Some((1, 3)));
294 assert_eq!(stream.byte_interval(TextInterval::empty()), None);
295 assert_eq!(stream.symbol_at(0), Some(0xDE));
296 assert_eq!(stream.symbol_at(4), Some(EOF));
297 }
298
299 #[test]
300 fn text_and_byte_interval_clamp_usize_max_without_overflow() {
301 // An EOF-token span can carry `stop == usize::MAX`; clamping before the
302 // `+1` must not overflow (debug panic / release wrap).
303 let stream = ByteStream::new(vec![0xDE, 0xAD]);
304 assert_eq!(stream.text(TextInterval::new(0, usize::MAX)), "dead");
305 assert_eq!(
306 stream.byte_interval(TextInterval::new(0, usize::MAX)),
307 Some((0, 2)),
308 );
309 // Out-of-range start clamps to empty rather than panicking.
310 assert_eq!(stream.text(TextInterval::new(5, usize::MAX)), "");
311 }
312
313 #[test]
314 fn position_summary_scans_raw_bytes_not_hex() {
315 // Bytes, not hex: a newline byte (0x0A) is one line break, and each
316 // other byte is one column — never doubled as the hex rendering would.
317 let stream = ByteStream::new(vec![0x41, 0x0A, 0x42, 0x43]);
318 assert_eq!(
319 stream.position_summary(0, 4),
320 Some(PositionSummary {
321 line_breaks: 1,
322 trailing_columns: 2,
323 }),
324 );
325 // No newline in [2, 4): two raw bytes are two columns (hex would say
326 // four).
327 assert_eq!(
328 stream.position_summary(2, 4),
329 Some(PositionSummary {
330 line_breaks: 0,
331 trailing_columns: 2,
332 }),
333 );
334 assert_eq!(stream.position_summary(4, 2), None);
335 }
336
337 #[test]
338 fn borrows_bytes_zero_copy() {
339 // The network-buffer case: parse a slice we already hold without
340 // handing ownership to the stream.
341 let buffer: [u8; 4] = [0xCA, 0xFE, 0xBA, 0xBE];
342 let mut stream = ByteStream::new(&buffer[..]);
343 assert_eq!(stream.la(1), 0xCA);
344 assert_eq!(stream.size(), 4);
345 // `buffer` is still ours afterwards.
346 assert_eq!(buffer[0], 0xCA);
347 }
348
349 #[test]
350 fn from_reader_drains_any_read() {
351 // The file/socket case, exercised here with an in-memory Cursor that
352 // implements the same `io::Read` contract as `File`/`TcpStream`.
353 let source = io::Cursor::new(vec![0x4D, 0x54, 0x68, 0x64]); // "MThd"
354 let mut stream = ByteStream::from_reader(source).expect("cursor read is infallible");
355 assert_eq!(stream.size(), 4);
356 assert_eq!(stream.la(1), 0x4D);
357 assert_eq!(stream.text(TextInterval::new(0, 3)), "4d546864");
358 }
359}