Skip to main content

lua_vm/
zio.rs

1//! Buffered streams — Rust port of `lzio.c` + `lzio.h`.
2//!
3//! Provides two public types:
4//! - [`ZIO`]: a read cursor wrapping an external chunk-supplier callback.
5//! - [`LexBuffer`]: a growable `Vec<u8>` byte buffer with the named interface
6//!   that C code accessed through the `luaZ_*buffer*` macro family.
7//!
8//! `lzio.h`'s macros are translated at their call sites and collected as
9//! methods or constants in this module.
10
11use crate::state::LuaState;
12use lua_types::error::LuaError;
13
14// ── Constants ──────────────────────────────────────────────────────────────────
15
16/// End-of-stream sentinel returned by [`ZIO::getc`] and [`ZIO::fill`].
17pub(crate) const EOZ: i32 = -1;
18
19/// Reentrant chunk supplier for a [`ZIO`].
20///
21/// Mirrors C's `lua_Reader`: the reader is invoked with the live `lua_State`
22/// each time more bytes are needed, so a `load` reader written in Lua can call
23/// back into the interpreter mid-parse. `Ok(None)` signals end-of-stream; an
24/// `Err` aborts the parse with that error (the C reader equivalent is a
25/// longjmp). Bytes are owned (`Vec<u8>`) rather than borrowed because a `dyn`
26/// trait object cannot name the reader's internal-buffer lifetime.
27pub type ChunkReader = Box<dyn FnMut(&mut LuaState) -> Result<Option<Vec<u8>>, LuaError>>;
28
29// ── LexBuffer (was Mbuffer in C) ───────────────────────────────────────────────
30
31/// Growable byte buffer used by the lexer for token text accumulation.
32///
33/// Corresponds to `Mbuffer` in `lzio.h`.  The C struct tracked `buffer`,
34/// `n` (used length), and `buffsize` (allocated capacity) as three separate
35/// fields with manual realloc.  In Rust all three are implicit in `Vec<u8>`.
36pub struct LexBuffer {
37    buffer: Vec<u8>,
38}
39
40impl LexBuffer {
41    /// Construct an empty `LexBuffer`.  Corresponds to the `luaZ_initbuffer` macro.
42    pub fn new() -> Self {
43        LexBuffer { buffer: Vec::new() }
44    }
45
46    /// Return the buffer contents as a mutable byte slice.
47    pub fn as_mut_slice(&mut self) -> &mut [u8] {
48        &mut self.buffer
49    }
50
51    /// Return the buffer's current allocation capacity in bytes.
52    pub fn capacity(&self) -> usize {
53        self.buffer.capacity()
54    }
55
56    /// Return the number of valid bytes currently stored in the buffer.
57    pub fn len(&self) -> usize {
58        self.buffer.len()
59    }
60
61    /// Shorten the live contents by `i` bytes without releasing capacity.
62    pub fn truncate_by(&mut self, i: usize) {
63        let new_len = self.buffer.len().saturating_sub(i);
64        self.buffer.truncate(new_len);
65    }
66
67    /// Reset the live length to zero without releasing capacity.
68    pub fn clear(&mut self) {
69        self.buffer.clear();
70    }
71
72    /// Resize the buffer to exactly `size` bytes, filling new bytes with `0`.
73    ///
74    /// Returns `Err(LuaError::Memory)` on allocation failure.
75    ///
76    /// C's `luaZ_resizebuffer` macro routes through `luaM_reallocvchar` and
77    /// Lua's custom allocator; here `Vec::resize` uses Rust's global allocator,
78    /// and OOM propagation via a custom allocator is not implemented.
79    pub fn resize(&mut self, _state: &mut LuaState, size: usize) -> Result<(), LuaError> {
80        self.buffer.resize(size, 0u8);
81        Ok(())
82    }
83
84    // `Drop for Vec` releases the heap allocation automatically. Call sites
85    // that use `luaZ_freebuffer` can simply let the `LexBuffer` drop.
86}
87
88impl Default for LexBuffer {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94// ── ZIO (buffered input stream) ────────────────────────────────────────────────
95
96/// Buffered input stream wrapping an external chunk-reader callback.
97///
98/// Corresponds to `struct Zio` / `ZIO` in `lzio.h`.  The C struct stored a
99/// `lua_State *L` back-pointer and a `void *data` opaque pointer alongside a
100/// raw `lua_Reader` function pointer.  In Rust:
101///
102/// - `lua_State *L` is removed from the struct; callers hold `&mut LuaState`
103///   directly and pass it to fallible methods.
104/// - `void *data` is folded into the reader closure.
105/// - `const char *p` (raw pointer into the reader's internal buffer) becomes a
106///   `usize` index into the owned `current_chunk` field.
107pub struct ZIO {
108    n: usize,
109    // Raw pointer replaced by index into `current_chunk`.
110    p: usize,
111    // C's reader function pointer + void *data collapsed into one closure. C
112    // stored `lua_State *L` in the ZIO; here it's threaded through
113    // fill/getc/read instead so the borrow checker sees the access.
114    reader: ChunkReader,
115    // Owned current chunk returned by the reader.  Not present as a separate
116    // field in C (C held a raw pointer into the reader's own internal buffer).
117    current_chunk: Vec<u8>,
118}
119
120impl ZIO {
121    /// Initialise a `ZIO` with the given reentrant reader callback.
122    ///
123    /// Corresponds to `luaZ_init` in `lzio.c`.  The C parameters `reader` and
124    /// `data` are combined into a single closure; `L` is threaded through the
125    /// fallible methods rather than stored on the struct.
126    pub fn new(reader: ChunkReader) -> Self {
127        ZIO {
128            n: 0,
129            p: 0,
130            current_chunk: Vec::new(),
131            reader,
132        }
133    }
134
135    /// Construct a `ZIO` that yields the supplied bytes once and then EOZ.
136    ///
137    /// Used for in-memory sources (a string chunk, or the lexer's own unit
138    /// tests) where there is no reader to call back into Lua. The state passed
139    /// to `getc`/`fill` is ignored by this reader.
140    pub fn from_bytes(bytes: Vec<u8>) -> Self {
141        let mut once = Some(bytes);
142        ZIO::new(Box::new(move |_state| Ok(once.take())))
143    }
144
145    /// Move this stream out, leaving an exhausted (empty) `ZIO` in its place.
146    ///
147    /// The parser owns the lexer's `LexState`, which owns its `ZIO`; the loader
148    /// only holds a `&mut ZIO`. This hands the live stream — its reader and any
149    /// bytes already buffered by [`getc`] — to the parser by value so the lexer
150    /// can keep pulling from the same reader (and the same `&mut LuaState`)
151    /// on demand. The original slot becomes an immediately-EOZ stream.
152    pub fn take(&mut self) -> ZIO {
153        std::mem::replace(self, ZIO::from_bytes(Vec::new()))
154    }
155
156    /// Refill the internal buffer by invoking the reader callback; return the
157    /// first byte of the new chunk as an `i32`, or [`EOZ`] if no more data is
158    /// available.
159    ///
160    /// `lua_unlock`/`lua_lock` are no-ops in the default C build and have no
161    /// equivalent call here. A reader error propagates as `Err` (C longjmps
162    /// out of the reader).
163    pub(crate) fn fill(&mut self, state: &mut LuaState) -> Result<i32, LuaError> {
164        let chunk_opt = (self.reader)(state)?;
165
166        match chunk_opt {
167            None => Ok(EOZ),
168            Some(chunk) if chunk.is_empty() => Ok(EOZ),
169            Some(chunk) => {
170                self.n = chunk.len() - 1;
171                self.current_chunk = chunk;
172                self.p = 0;
173                let byte = self.current_chunk[self.p] as u8;
174                self.p += 1;
175                Ok(byte as i32)
176            }
177        }
178    }
179
180    /// Return the next byte from the stream as an `i32`, or [`EOZ`] at
181    /// end-of-stream.
182    ///
183    /// This is the hot-path inline method corresponding to the `zgetc` macro.
184    /// When bytes remain in the current chunk no allocation occurs.
185    ///
186    /// C's macro uses `(z)->n-- > 0`, which reads n, tests it, then
187    /// decrements; when n == 0 the test is false (0 > 0) so fill is called
188    /// without decrementing. This preserves that: `if self.n > 0` followed by
189    /// an explicit `self.n -= 1`.
190    #[inline]
191    pub fn getc(&mut self, state: &mut LuaState) -> Result<i32, LuaError> {
192        if self.n > 0 {
193            self.n -= 1;
194            let byte = self.current_chunk[self.p] as u8;
195            self.p += 1;
196            Ok(byte as i32)
197        } else {
198            self.fill(state)
199        }
200    }
201
202    /// Read exactly `buf.len()` bytes into `buf`.
203    ///
204    /// Returns the number of bytes that could **not** be read: `0` means
205    /// complete success; a non-zero value means end-of-stream was reached with
206    /// that many bytes still outstanding.
207    ///
208    /// C's `void *b` + explicit `n` become Rust's `&mut [u8]`, whose
209    /// length encodes the requested byte count.  `memcpy` becomes
210    /// `copy_from_slice`.  The advancing pointer `b = (char *)b + m` is
211    /// replaced by a `dst` index into `buf`.
212    pub(crate) fn read(&mut self, state: &mut LuaState, buf: &mut [u8]) -> Result<usize, LuaError> {
213        let mut remaining = buf.len();
214        let mut dst: usize = 0;
215
216        while remaining > 0 {
217            if self.n == 0 {
218                if self.fill(state)? == EOZ {
219                    return Ok(remaining);
220                } else {
221                    // fill() advanced p by 1 and set n = chunk.len() - 1.
222                    // Undoing that makes the whole chunk available to the
223                    // copy loop below.
224                    self.n += 1;
225                    self.p -= 1;
226                }
227            }
228
229            let m = remaining.min(self.n);
230
231            buf[dst..dst + m].copy_from_slice(&self.current_chunk[self.p..self.p + m]);
232
233            self.n -= m;
234            self.p += m;
235
236            dst += m;
237            remaining -= m;
238        }
239
240        Ok(0)
241    }
242}