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//! The lzio header is merged here per PORTING.md §1 ("Headers merge into the
9//! consuming `.rs`"). All macros defined in `lzio.h` are translated at their
10//! call sites and collected as methods or constants in this module.
11//!
12//! # C source files
13//! - `reference/lua-5.4.7/src/lzio.c` (68 lines, 3 functions)
14//! - `reference/lua-5.4.7/src/lzio.h` (66 lines, struct + macros; merged)
15
16// TODO(port): import path for LuaState will need adjustment once the
17// crate-internal module graph is settled in Phase B. Using a local path
18// for now; may become `use lua_types::state::LuaState` or similar.
19use crate::state::LuaState;
20use lua_types::error::LuaError;
21
22// ── Constants ──────────────────────────────────────────────────────────────────
23
24// macros.tsv: EOZ → const EOZ: i32 = -1
25/// End-of-stream sentinel returned by [`ZIO::getc`] and [`ZIO::fill`].
26pub(crate) const EOZ: i32 = -1;
27
28/// Reentrant chunk supplier for a [`ZIO`].
29///
30/// Mirrors C's `lua_Reader`: the reader is invoked with the live `lua_State`
31/// each time more bytes are needed, so a `load` reader written in Lua can call
32/// back into the interpreter mid-parse. `Ok(None)` signals end-of-stream; an
33/// `Err` aborts the parse with that error (the C reader equivalent is a
34/// longjmp). Bytes are owned (`Vec<u8>`) rather than borrowed because a `dyn`
35/// trait object cannot name the reader's internal-buffer lifetime.
36pub type ChunkReader = Box<dyn FnMut(&mut LuaState) -> Result<Option<Vec<u8>>, LuaError>>;
37
38// ── LexBuffer (was Mbuffer in C) ───────────────────────────────────────────────
39
40/// Growable byte buffer used by the lexer for token text accumulation.
41///
42/// Corresponds to `Mbuffer` in `lzio.h`. The C struct tracked `buffer`,
43/// `n` (used length), and `buffsize` (allocated capacity) as three separate
44/// fields with manual realloc. In Rust all three are implicit in `Vec<u8>`.
45///
46/// # C mapping (types.tsv)
47/// ```text
48/// Mbuffer → LexBuffer
49/// .buffer → Vec<u8> (heap storage)
50/// .n → Vec::len()
51/// .buffsize → Vec::capacity()
52/// ```
53pub struct LexBuffer {
54 buffer: Vec<u8>,
55}
56
57impl LexBuffer {
58 // macros.tsv: luaZ_initbuffer → buf.init() (most call sites just construct)
59 /// Construct an empty `LexBuffer`. Corresponds to the `luaZ_initbuffer` macro.
60 pub fn new() -> Self {
61 LexBuffer { buffer: Vec::new() }
62 }
63
64 // macros.tsv: luaZ_buffer → buf.as_mut_slice()
65 /// Return the buffer contents as a mutable byte slice.
66 pub fn as_mut_slice(&mut self) -> &mut [u8] {
67 &mut self.buffer
68 }
69
70 // macros.tsv: luaZ_sizebuffer → buf.capacity()
71 /// Return the buffer's current allocation capacity in bytes.
72 pub fn capacity(&self) -> usize {
73 self.buffer.capacity()
74 }
75
76 // macros.tsv: luaZ_bufflen → buf.len()
77 /// Return the number of valid bytes currently stored in the buffer.
78 pub fn len(&self) -> usize {
79 self.buffer.len()
80 }
81
82 // macros.tsv: luaZ_buffremove → buf.truncate_by(i)
83 /// Shorten the live contents by `i` bytes without releasing capacity.
84 pub fn truncate_by(&mut self, i: usize) {
85 let new_len = self.buffer.len().saturating_sub(i);
86 self.buffer.truncate(new_len);
87 }
88
89 // macros.tsv: luaZ_resetbuffer → buf.clear()
90 /// Reset the live length to zero without releasing capacity.
91 pub fn clear(&mut self) {
92 self.buffer.clear();
93 }
94
95 // ((buff)->buffer = luaM_reallocvchar(L, (buff)->buffer, \
96 // (buff)->buffsize, size), \
97 // (buff)->buffsize = size)
98 // macros.tsv: luaZ_resizebuffer → buf.resize(state, size)?
99 /// Resize the buffer to exactly `size` bytes, filling new bytes with `0`.
100 ///
101 /// Returns `Err(LuaError::Memory)` on allocation failure.
102 ///
103 /// PORT NOTE: the C macro routes through `luaM_reallocvchar` and Lua's
104 /// custom allocator. Phase A uses `Vec::resize` with Rust's global
105 /// allocator; OOM propagation via the custom allocator is a Phase D concern.
106 // PERF(port): luaM_reallocvchar — Vec::resize may over-allocate relative
107 // to the exact-fit C behaviour; profile in Phase B.
108 pub fn resize(&mut self, _state: &mut LuaState, size: usize) -> Result<(), LuaError> {
109 self.buffer.resize(size, 0u8);
110 Ok(())
111 }
112
113 // macros.tsv: luaZ_freebuffer → (Rust Drop handles deallocation; drop the call)
114 // PORT NOTE: `Drop for Vec` releases the heap allocation automatically.
115 // Call sites that use `luaZ_freebuffer` can simply let the `LexBuffer` drop.
116}
117
118impl Default for LexBuffer {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124// ── ZIO (buffered input stream) ────────────────────────────────────────────────
125
126/// Buffered input stream wrapping an external chunk-reader callback.
127///
128/// Corresponds to `struct Zio` / `ZIO` in `lzio.h`. The C struct stored a
129/// `lua_State *L` back-pointer and a `void *data` opaque pointer alongside a
130/// raw `lua_Reader` function pointer. In Rust:
131///
132/// - `lua_State *L` is removed from the struct; callers hold `&mut LuaState`
133/// directly and pass it to fallible methods (per types.tsv).
134/// - `void *data` is folded into the reader closure (per types.tsv).
135/// - `const char *p` (raw pointer into the reader's internal buffer) becomes a
136/// `usize` index into the owned `current_chunk` field.
137///
138/// # C mapping (types.tsv)
139/// ```text
140/// Zio → ZIO
141/// .n → usize (bytes still unread in current_chunk)
142/// .p → usize (cursor index; was const char *)
143/// .reader+.data → ChunkReader (combined)
144/// .L → re-threaded as &mut LuaState parameters (the reader needs
145/// it to call back into Lua), matching C's stored z->L.
146/// ```
147pub struct ZIO {
148 n: usize,
149 // PORT NOTE: raw pointer replaced by index into `current_chunk`.
150 p: usize,
151 // PORT NOTE: C reader function pointer + void *data collapsed into one
152 // closure. C stored `lua_State *L` in the ZIO; the Rust port threads it
153 // through fill/getc/read instead so the borrow checker sees the access.
154 reader: ChunkReader,
155 // Owned current chunk returned by the reader. Not present as a separate
156 // field in C (C held a raw pointer into the reader's own internal buffer).
157 current_chunk: Vec<u8>,
158}
159
160impl ZIO {
161 // macros.tsv: LUAI_FUNC → pub(crate)
162 /// Initialise a `ZIO` with the given reentrant reader callback.
163 ///
164 /// Corresponds to `luaZ_init` in `lzio.c`. The C parameters `reader` and
165 /// `data` are combined into a single closure; `L` is threaded through the
166 /// fallible methods rather than stored on the struct.
167 pub fn new(reader: ChunkReader) -> Self {
168 ZIO {
169 n: 0,
170 p: 0,
171 current_chunk: Vec::new(),
172 reader,
173 }
174 }
175
176 /// Construct a `ZIO` that yields the supplied bytes once and then EOZ.
177 ///
178 /// Used for in-memory sources (a string chunk, or the lexer's own unit
179 /// tests) where there is no reader to call back into Lua. The state passed
180 /// to `getc`/`fill` is ignored by this reader.
181 pub fn from_bytes(bytes: Vec<u8>) -> Self {
182 let mut once = Some(bytes);
183 ZIO::new(Box::new(move |_state| Ok(once.take())))
184 }
185
186 /// Move this stream out, leaving an exhausted (empty) `ZIO` in its place.
187 ///
188 /// The parser owns the lexer's `LexState`, which owns its `ZIO`; the loader
189 /// only holds a `&mut ZIO`. This hands the live stream — its reader and any
190 /// bytes already buffered by [`getc`] — to the parser by value so the lexer
191 /// can keep pulling from the same reader (and the same `&mut LuaState`)
192 /// on demand. The original slot becomes an immediately-EOZ stream.
193 pub fn take(&mut self) -> ZIO {
194 std::mem::replace(self, ZIO::from_bytes(Vec::new()))
195 }
196
197 // macros.tsv: LUAI_FUNC → pub(crate)
198 /// Refill the internal buffer by invoking the reader callback; return the
199 /// first byte of the new chunk as an `i32`, or [`EOZ`] if no more data is
200 /// available.
201 ///
202 /// # C source
203 /// ```c
204 ///
205 /// // size_t size;
206 /// // lua_State *L = z->L;
207 /// // const char *buff;
208 /// // lua_unlock(L);
209 /// // buff = z->reader(L, z->data, &size);
210 /// // lua_lock(L);
211 /// // if (buff == NULL || size == 0)
212 /// // return EOZ;
213 /// // z->n = size - 1; /* discount char being returned */
214 /// // z->p = buff;
215 /// // return cast_uchar(*(z->p++));
216 /// // }
217 /// ```
218 ///
219 /// PORT NOTE: `lua_unlock`/`lua_lock` are no-ops in the default build and
220 /// are dropped per macros.tsv. `cast_uchar` → `as u8` per macros.tsv.
221 /// A reader error propagates as `Err` (C longjmps out of the reader).
222 pub(crate) fn fill(&mut self, state: &mut LuaState) -> Result<i32, LuaError> {
223 let chunk_opt = (self.reader)(state)?;
224
225 match chunk_opt {
226 None => Ok(EOZ),
227 Some(chunk) if chunk.is_empty() => Ok(EOZ),
228 Some(chunk) => {
229 self.n = chunk.len() - 1;
230 self.current_chunk = chunk;
231 self.p = 0;
232 // cast_uchar → as u8 per macros.tsv
233 let byte = self.current_chunk[self.p] as u8;
234 self.p += 1;
235 Ok(byte as i32)
236 }
237 }
238 }
239
240 // macros.tsv: zgetc → z.getc() returning i32 (next byte or EOZ)
241 /// Return the next byte from the stream as an `i32`, or [`EOZ`] at
242 /// end-of-stream.
243 ///
244 /// This is the hot-path inline method corresponding to the `zgetc` macro.
245 /// When bytes remain in the current chunk no allocation occurs.
246 ///
247 /// # C source (macro)
248 /// ```c
249 ///
250 /// ```
251 ///
252 /// PORT NOTE: The C macro uses `(z)->n-- > 0` which reads n, tests it, then
253 /// decrements. When n == 0 the test is false (0 > 0) so fill is called
254 /// without decrementing. The Rust translation preserves this: `if self.n > 0`
255 /// followed by an explicit `self.n -= 1`.
256 #[inline]
257 pub fn getc(&mut self, state: &mut LuaState) -> Result<i32, LuaError> {
258 if self.n > 0 {
259 self.n -= 1;
260 let byte = self.current_chunk[self.p] as u8;
261 self.p += 1;
262 Ok(byte as i32)
263 } else {
264 self.fill(state)
265 }
266 }
267
268 // macros.tsv: LUAI_FUNC → pub(crate)
269 /// Read exactly `buf.len()` bytes into `buf`.
270 ///
271 /// Returns the number of bytes that could **not** be read: `0` means
272 /// complete success; a non-zero value means end-of-stream was reached with
273 /// that many bytes still outstanding.
274 ///
275 /// # C source
276 /// ```c
277 ///
278 /// // while (n) {
279 /// // size_t m;
280 /// // if (z->n == 0) { /* no bytes in buffer? */
281 /// // if (luaZ_fill(z) == EOZ) /* try to read more */
282 /// // return n; /* no more input; return number of missing bytes */
283 /// // else {
284 /// // z->n++; /* luaZ_fill consumed first byte; put it back */
285 /// // z->p--;
286 /// // }
287 /// // }
288 /// // m = (n <= z->n) ? n : z->n; /* min. between n and z->n */
289 /// // memcpy(b, z->p, m);
290 /// // z->n -= m;
291 /// // z->p += m;
292 /// // b = (char *)b + m;
293 /// // n -= m;
294 /// // }
295 /// // return 0;
296 /// // }
297 /// ```
298 ///
299 /// PORT NOTE: C's `void *b` + explicit `n` become Rust's `&mut [u8]`, whose
300 /// length encodes the requested byte count. `memcpy` becomes
301 /// `copy_from_slice`. The advancing pointer `b = (char *)b + m` is
302 /// replaced by a `dst` index into `buf`.
303 pub(crate) fn read(&mut self, state: &mut LuaState, buf: &mut [u8]) -> Result<usize, LuaError> {
304 let mut remaining = buf.len();
305 let mut dst: usize = 0;
306
307 while remaining > 0 {
308 if self.n == 0 {
309 if self.fill(state)? == EOZ {
310 return Ok(remaining);
311 } else {
312 // fill() advanced p by 1 and set n = chunk.len() - 1.
313 // Undoing that makes the whole chunk available to the
314 // copy loop below.
315 self.n += 1;
316 self.p -= 1;
317 }
318 }
319
320 let m = remaining.min(self.n);
321
322 buf[dst..dst + m].copy_from_slice(&self.current_chunk[self.p..self.p + m]);
323
324 self.n -= m;
325 self.p += m;
326
327 dst += m;
328 remaining -= m;
329 }
330
331 Ok(0)
332 }
333}
334
335// ──────────────────────────────────────────────────────────────────────────────
336// PORT STATUS
337// source: src/lzio.c (68 lines, 3 functions)
338// src/lzio.h (66 lines, merged)
339// target_crate: lua-vm
340// confidence: medium
341// todos: 1
342// port_notes: 4
343// unsafe_blocks: 0 (must be 0 outside explicit unsafe-budget crates)
344// notes: Logic is faithful. The one open question (TODO) is whether
345// concrete reader callbacks will need `&mut LuaState` as a
346// parameter when load/dofile lands in Phase B. If so,
347// `ZIO::reader`, `fill`, `getc`, and `read` all need a
348// threading change. `LexBuffer::resize` stubs OOM handling
349// (real allocator wiring is Phase D). Import paths for
350// `LuaState` and `LuaError` will require crate-graph fixes
351// in Phase B.
352// ──────────────────────────────────────────────────────────────────────────────