Skip to main content

lua_vm/
undump.rs

1//! Load precompiled Lua chunks.
2//!
3//! Direct port of `reference/lua-5.4.7/src/lundump.c` (335 lines, 20 items).
4//! Declarations from `lundump.h` are merged here per PORTING.md §1.
5//!
6//! The public entry point is [`undump`], which reads a binary Lua chunk from
7//! a [`ZIO`] stream and returns a Lua closure ready to call.
8
9// TODO(port): resolve import paths once the crate module graph is settled
10// in Phase B.  These are best-guess paths based on other translated files.
11#[allow(unused_imports)]
12use crate::prelude::*;
13use crate::state::LuaState;
14use crate::zio::ZIO;
15use lua_types::error::LuaError;
16use lua_types::value::LuaValue;
17
18// PORT NOTE: GcRef<T>, LuaProto, LuaClosure, LuaString, UpvalDesc, LocalVar,
19// AbsLineInfo, and Instruction are expected to live in lua_types or lua_vm
20// crates.  All paths below are provisional for Phase A.
21use lua_types::closure::LuaLClosure;
22use lua_types::gc::GcRef;
23use lua_types::opcode::Instruction;
24use lua_types::proto::{AbsLineInfo, LocalVar, LuaProto, UpvalDesc};
25use lua_types::string::LuaString;
26use lua_types::LuaVersion;
27
28// ── Constants (from lundump.h) ─────────────────────────────────────────────
29
30/// Six-byte data marker in the chunk header used to catch conversion errors.
31const LUAC_DATA: &[u8] = b"\x19\x93\r\n\x1a\n";
32
33/// Reference integer written in the header to detect integer endianness/size
34/// mismatches.
35const LUAC_INT: i64 = 0x5678;
36
37// macros.tsv: cast_num → x as f64
38/// Reference float written in the header to detect float format mismatches.
39const LUAC_NUM: f64 = 370.5;
40
41const LUAC_INT_55: i64 = -0x5678;
42
43const LUAC_INST_55: u32 = 0x12345678;
44
45const LUAC_NUM_55: f64 = -370.5;
46
47// LUA_VERSION_NUM = 504 → ((5 * 16) + 4) = 0x54 = 84
48/// One-byte version tag: upper nibble = major, lower nibble = minor.
49const LUAC_VERSION_51: u8 = 0x51;
50const LUAC_VERSION_52: u8 = 0x52;
51const LUAC_VERSION_53: u8 = 0x53;
52const LUAC_VERSION_54: u8 = 0x54;
53const LUAC_VERSION_55: u8 = 0x55;
54
55const LUAC_FORMAT: u8 = 0;
56
57const LUA_SIGNATURE: &[u8] = b"\x1bLua";
58
59// macros.tsv: LUAI_MAXSHORTLEN → const MAX_SHORT_LEN: usize = 40
60const MAX_SHORT_LEN: usize = 40;
61
62// ── Constant-pool type tags (from lobject.h makevariant) ───────────────────
63//
64// These are the byte values written by ldump.c into the constants array.
65// makevariant(t, v) = t | (v << 4).
66//
67// PORT NOTE: types.tsv maps LUA_VNIL → LuaValue::Nil etc. but the *byte
68// values* used in the binary format are the raw tag integers from lobject.h.
69// We define them here as u8 constants so the match in load_constants is
70// self-documenting.
71
72const TAG_NIL: u8 = 0x00;
73const TAG_FALSE: u8 = 0x01;
74const TAG_TRUE: u8 = 0x11;
75const TAG_INT: u8 = 0x03;
76const TAG_FLOAT: u8 = 0x13;
77const TAG_SHORT_STR: u8 = 0x04;
78const TAG_LONG_STR: u8 = 0x14;
79
80// ── LoadState ──────────────────────────────────────────────────────────────
81
82/// Loader state bundled for convenience: Lua state, input stream, and the
83/// chunk name used in error messages.
84///
85/// # C mapping
86/// ```c
87///
88/// ```
89///
90/// PORT NOTE: In C, `LoadState` holds raw pointers to `lua_State` and `ZIO`.
91/// In Rust these become references with a shared lifetime `'a`.  The struct is
92/// always stack-allocated inside [`undump`] and never escapes the call.
93struct LoadState<'a> {
94    state: &'a mut LuaState,
95    z: &'a mut ZIO,
96}
97
98// ── Error helper ───────────────────────────────────────────────────────────
99
100/// Build a syntax error for a malformed binary chunk.
101///
102/// # C source
103/// ```c
104///
105/// //   luaO_pushfstring(S->L, "%s: bad binary format (%s)", S->name, why);
106/// //   luaD_throw(S->L, LUA_ERRSYNTAX);
107/// // }
108/// ```
109///
110/// PORT NOTE: `l_noret` in C (diverges via `longjmp`).  In Rust we return
111/// `LuaError` and the caller does `return Err(load_error(...))`.  The C
112/// pattern `luaO_pushfstring + luaD_throw(LUA_ERRSYNTAX)` collapses to a
113/// single `LuaError::syntax` per error_sites.tsv.
114///
115/// TODO(port): `s.name` is `Vec<u8>`; `LuaError::syntax` takes `format_args!`
116/// which requires an `std::fmt::Display` implementor.  `Vec<u8>` does not
117/// implement `Display`.  Phase B should add a byte-string formatting path to
118/// `LuaError::syntax_bytes` or similar, so the chunk name is included verbatim
119/// in the message.
120fn load_error(_s: &LoadState<'_>, why: &'static str) -> LuaError {
121    LuaError::syntax(format_args!("bad binary format ({})", why))
122}
123
124// ── Low-level I/O ──────────────────────────────────────────────────────────
125
126/// Read exactly `buf.len()` bytes from the stream into `buf`.
127///
128/// # C source
129/// ```c
130///
131/// //   if (luaZ_read(S->Z, b, size) != 0)
132/// //     error(S, "truncated chunk");
133/// // }
134/// ```
135///
136/// PORT NOTE: C takes `void *b` + explicit `size`.  In Rust we use `&mut [u8]`
137/// whose length encodes the byte count.  `luaZ_read` returns the number of
138/// bytes NOT read (0 = success), matching `ZIO::read`'s contract.
139fn load_block(s: &mut LoadState<'_>, buf: &mut [u8]) -> Result<(), LuaError> {
140    // macros.tsv: luaZ_read → z.read(buf)  (returns usize unread)
141    if s.z.read(s.state, buf)? != 0 {
142        return Err(load_error(s, "truncated chunk"));
143    }
144    Ok(())
145}
146
147/// Read a single byte from the stream.
148///
149/// # C source
150/// ```c
151///
152/// //   int b = zgetc(S->Z);
153/// //   if (b == EOZ)
154/// //     error(S, "truncated chunk");
155/// //   return cast_byte(b);
156/// // }
157/// ```
158///
159/// PORT NOTE: `cast_byte` → `as u8` per macros.tsv; `zgetc` → `z.getc()`.
160fn load_byte(s: &mut LoadState<'_>) -> Result<u8, LuaError> {
161    // macros.tsv: zgetc → z.getc()  returning i32
162    let b = s.z.getc(s.state)?;
163    if b == crate::zio::EOZ {
164        return Err(load_error(s, "truncated chunk"));
165    }
166    // macros.tsv: cast_byte → x as u8
167    Ok(b as u8)
168}
169
170/// Read a variable-length unsigned integer (7 bits per byte, big-endian,
171/// MSB-first continuation flag).
172///
173/// # C source
174/// ```c
175///
176/// //   size_t x = 0;
177/// //   int b;
178/// //   limit >>= 7;
179/// //   do {
180/// //     b = loadByte(S);
181/// //     if (x >= limit)
182/// //       error(S, "integer overflow");
183/// //     x = (x << 7) | (b & 0x7f);
184/// //   } while ((b & 0x80) == 0);
185/// //   return x;
186/// // }
187/// ```
188///
189/// PORT NOTE: The encoding terminates when a byte with the high bit set is
190/// seen (the *last* byte has bit 7 = 1).  That is the opposite of the more
191/// common LEB128 where the continuation bit means "more follows".
192fn load_unsigned(s: &mut LoadState<'_>, limit: usize) -> Result<usize, LuaError> {
193    let mut x: usize = 0;
194    let limit = limit >> 7;
195    loop {
196        let b = load_byte(s)? as usize;
197        if x >= limit {
198            return Err(load_error(s, "integer overflow"));
199        }
200        x = (x << 7) | (b & 0x7f);
201        if (b & 0x80) != 0 {
202            break;
203        }
204    }
205    Ok(x)
206}
207
208/// Read a `size_t`-sized unsigned value.
209///
210/// # C source
211/// ```c
212///
213/// //   return loadUnsigned(S, MAX_SIZET);
214/// // }
215/// ```
216///
217/// PORT NOTE: `MAX_SIZET` → `usize::MAX` per macros.tsv.
218fn load_size(s: &mut LoadState<'_>) -> Result<usize, LuaError> {
219    // macros.tsv: MAX_SIZET → usize::MAX
220    load_unsigned(s, usize::MAX)
221}
222
223/// Read a signed `int`-sized value.
224///
225/// # C source
226/// ```c
227///
228/// //   return cast_int(loadUnsigned(S, INT_MAX));
229/// // }
230/// ```
231///
232/// PORT NOTE: `cast_int` → `x as i32` per macros.tsv.  `INT_MAX` → `i32::MAX
233/// as usize`.
234fn load_int(s: &mut LoadState<'_>) -> Result<i32, LuaError> {
235    // macros.tsv: cast_int → x as i32
236    let v = load_unsigned(s, i32::MAX as usize)?;
237    Ok(v as i32)
238}
239
240/// Read a `lua_Number` (f64) as eight raw native-endian bytes.
241///
242/// # C source
243/// ```c
244///
245/// //   lua_Number x;
246/// //   loadVar(S, x);   /* expands to loadBlock(S, &x, sizeof(x)) */
247/// //   return x;
248/// // }
249/// ```
250///
251/// PORT NOTE: `loadVar` reads `sizeof(lua_Number) = 8` raw bytes directly
252/// into the value.  In Rust we use `f64::from_ne_bytes` (native endian) to
253/// reconstruct the value from the eight bytes.  The binary format is host-
254/// endian for these fields; the header check verifies endianness compatibility
255/// via `LUAC_INT` and `LUAC_NUM` sentinels.
256fn load_number(s: &mut LoadState<'_>) -> Result<f64, LuaError> {
257    let mut buf = [0u8; 8];
258    load_block(s, &mut buf)?;
259    // PERF(port): f64::from_ne_bytes is zero-cost — same as C's union cast
260    Ok(f64::from_ne_bytes(buf))
261}
262
263/// Read a `lua_Integer` (i64) as eight raw native-endian bytes.
264///
265/// # C source
266/// ```c
267///
268/// //   lua_Integer x;
269/// //   loadVar(S, x);   /* expands to loadBlock(S, &x, sizeof(x)) */
270/// //   return x;
271/// // }
272/// ```
273///
274/// PORT NOTE: Same reasoning as [`load_number`] — uses `i64::from_ne_bytes`.
275fn load_integer(s: &mut LoadState<'_>) -> Result<i64, LuaError> {
276    let mut buf = [0u8; 8];
277    load_block(s, &mut buf)?;
278    Ok(i64::from_ne_bytes(buf))
279}
280
281fn load_raw_i32(s: &mut LoadState<'_>) -> Result<i32, LuaError> {
282    let mut buf = [0u8; 4];
283    load_block(s, &mut buf)?;
284    Ok(i32::from_ne_bytes(buf))
285}
286
287fn load_raw_u32(s: &mut LoadState<'_>) -> Result<u32, LuaError> {
288    let mut buf = [0u8; 4];
289    load_block(s, &mut buf)?;
290    Ok(u32::from_ne_bytes(buf))
291}
292
293// ── String loading ─────────────────────────────────────────────────────────
294
295/// Load a nullable string.  Returns `None` if the stored size is zero.
296///
297/// # C source
298/// ```c
299///
300/// //   lua_State *L = S->L;
301/// //   TString *ts;
302/// //   size_t size = loadSize(S);
303/// //   if (size == 0) return NULL;
304/// //   else if (--size <= LUAI_MAXSHORTLEN) {  /* short string? */
305/// //     char buff[LUAI_MAXSHORTLEN];
306/// //     loadVector(S, buff, size);
307/// //     ts = luaS_newlstr(L, buff, size);
308/// //   } else {  /* long string */
309/// //     ts = luaS_createlngstrobj(L, size);
310/// //     setsvalue2s(L, L->top.p, ts);  /* anchor it (loadVector can GC) */
311/// //     luaD_inctop(L);
312/// //     loadVector(S, getlngstr(ts), size);
313/// //     L->top.p--;
314/// //   }
315/// //   luaC_objbarrier(L, p, ts);
316/// //   return ts;
317/// // }
318/// ```
319///
320/// PORT NOTE: The Lua binary format stores `actual_length + 1` so that size=0
321/// is the null-string sentinel.  After reading `raw_size`, the actual byte
322/// count is `raw_size - 1`.
323///
324/// PORT NOTE: In C, long strings are created first (to anchor them from GC)
325/// and then filled in-place via `getlngstr`.  In Rust, GC anchoring is not
326/// needed in Phase A–C (Rc keeps objects alive); we read into a buffer and
327/// then create the string.
328///
329/// TODO(port): `luaS_newlstr` interns the string (short strings only);
330/// `luaS_createlngstrobj` does NOT intern.  Phase A uses `state.intern_str()`
331/// for both.  Phase B should add a `state.create_long_str()` path that skips
332/// the intern table, matching C semantics.
333///
334/// PORT NOTE: The `_proto` parameter corresponds to C's `Proto *p` used only
335/// for `luaC_objbarrier(L, p, ts)`.  The barrier is a no-op in Phase A–C
336/// (macros.tsv: `luaC_objbarrier → state.gc().obj_barrier(p, o)` no-op).
337fn load_string_n(
338    s: &mut LoadState<'_>,
339    _proto: &LuaProto,
340) -> Result<Option<GcRef<LuaString>>, LuaError> {
341    let raw_size = load_size(s)?;
342    if raw_size == 0 {
343        return Ok(None);
344    }
345    let size = raw_size - 1;
346
347    // Read the raw bytes regardless of short/long distinction.
348    let mut buf = vec![0u8; size];
349
350    if size <= MAX_SHORT_LEN {
351        load_block(s, &mut buf)?;
352    } else {
353        load_block(s, &mut buf)?;
354    }
355
356    // macros.tsv: luaS_newlstr → state.intern_str(&s[..n])
357    // TODO(port): long strings should not be interned; see doc-comment above.
358    let ts = s.state.intern_str(&buf)?;
359
360    // macros.tsv: luaC_objbarrier → state.gc().obj_barrier(p, o)  no-op Phase A
361    // (dropped — Phase A GC is Rc, no barrier needed)
362
363    Ok(Some(ts))
364}
365
366/// Load a non-nullable string; error if the stream encodes a null string.
367///
368/// # C source
369/// ```c
370///
371/// //   TString *st = loadStringN(S, p);
372/// //   if (st == NULL)
373/// //     error(S, "bad format for constant string");
374/// //   return st;
375/// // }
376/// ```
377fn load_string(s: &mut LoadState<'_>, proto: &LuaProto) -> Result<GcRef<LuaString>, LuaError> {
378    match load_string_n(s, proto)? {
379        Some(ts) => Ok(ts),
380        None => Err(load_error(s, "bad format for constant string")),
381    }
382}
383
384// ── Proto-field loaders ────────────────────────────────────────────────────
385
386/// Load the bytecode instruction array into a prototype.
387///
388/// # C source
389/// ```c
390///
391/// //   int n = loadInt(S);
392/// //   f->code = luaM_newvectorchecked(S->L, n, Instruction);
393/// //   f->sizecode = n;
394/// //   loadVector(S, f->code, n);
395/// // }
396/// ```
397///
398/// PORT NOTE: `loadVector(S, f->code, n)` expands to
399/// `loadBlock(S, f->code, n * sizeof(Instruction))` — `n` raw 4-byte words.
400/// We read each `u32` in native-endian order, consistent with how
401/// [`load_number`] and [`load_integer`] work.
402///
403/// PORT NOTE: `f->sizecode` is removed in Rust — `Vec::len()` covers it
404/// (types.tsv: `Proto.sizecode → removed`).
405fn load_code(s: &mut LoadState<'_>, f: &mut LuaProto) -> Result<(), LuaError> {
406    let n = load_int(s)? as usize;
407    // macros.tsv: luaM_newvectorchecked → vec_checked::<T>(n)?
408    // PORT NOTE: Phase A uses Vec directly; overflow check omitted for brevity.
409    // TODO(port): add overflow / OOM check matching luaM_newvectorchecked.
410    let mut code = Vec::with_capacity(n);
411    for _ in 0..n {
412        let mut buf = [0u8; 4];
413        load_block(s, &mut buf)?;
414        // Instruction is a u32 newtype per types.tsv
415        code.push(Instruction(u32::from_ne_bytes(buf)));
416    }
417    f.code = code;
418    Ok(())
419}
420
421/// Load the constant pool into a prototype.
422///
423/// # C source
424/// ```c
425///
426/// //   int i; int n = loadInt(S);
427/// //   f->k = luaM_newvectorchecked(S->L, n, TValue);
428/// //   f->sizek = n;
429/// //   for (i = 0; i < n; i++) setnilvalue(&f->k[i]);
430/// //   for (i = 0; i < n; i++) {
431/// //     TValue *o = &f->k[i];
432/// //     int t = loadByte(S);
433/// //     switch (t) {
434/// //       case LUA_VNIL:    setnilvalue(o); break;
435/// //       case LUA_VFALSE:  setbfvalue(o); break;
436/// //       case LUA_VTRUE:   setbtvalue(o); break;
437/// //       case LUA_VNUMFLT: setfltvalue(o, loadNumber(S)); break;
438/// //       case LUA_VNUMINT: setivalue(o, loadInteger(S)); break;
439/// //       case LUA_VSHRSTR:
440/// //       case LUA_VLNGSTR: setsvalue2n(S->L, o, loadString(S, f)); break;
441/// //       default: lua_assert(0);
442/// //     }
443/// //   }
444/// // }
445/// ```
446///
447/// PORT NOTE: The initial `setnilvalue` loop initialises the vector for GC
448/// safety in C.  In Rust, `Vec` is always in a valid state; we skip it.
449fn load_constants(s: &mut LoadState<'_>, f: &mut LuaProto) -> Result<(), LuaError> {
450    let n = load_int(s)? as usize;
451    // TODO(port): add overflow / OOM check.
452    let mut k = Vec::with_capacity(n);
453
454    // Dropped — Rust Vec elements are never uninitialized.
455
456    for _ in 0..n {
457        let t = load_byte(s)?;
458        let val = match t {
459            // macros.tsv: setnilvalue → *o = LuaValue::Nil
460            TAG_NIL => LuaValue::Nil,
461
462            // macros.tsv: setbfvalue → *o = LuaValue::Bool(false)
463            TAG_FALSE => LuaValue::Bool(false),
464
465            // macros.tsv: setbtvalue → *o = LuaValue::Bool(true)
466            TAG_TRUE => LuaValue::Bool(true),
467
468            // macros.tsv: setfltvalue → *o = LuaValue::Float(x)
469            TAG_FLOAT => LuaValue::Float(load_number(s)?),
470
471            // macros.tsv: setivalue → *o = LuaValue::Int(x)
472            TAG_INT => LuaValue::Int(load_integer(s)?),
473
474            // macros.tsv: setsvalue2n → *dst = LuaValue::Str(s.clone())
475            TAG_SHORT_STR | TAG_LONG_STR => {
476                let ts = load_string(s, f)?;
477                LuaValue::Str(ts)
478            }
479
480            // macros.tsv: lua_assert → debug_assert!
481            _ => {
482                debug_assert!(false, "unknown constant type tag {:#04x}", t);
483                LuaValue::Nil
484            }
485        };
486        k.push(val);
487    }
488
489    f.k = k;
490    Ok(())
491}
492
493/// Load nested function prototypes into a prototype.
494///
495/// # C source
496/// ```c
497///
498/// //   int i; int n = loadInt(S);
499/// //   f->p = luaM_newvectorchecked(S->L, n, Proto *);
500/// //   f->sizep = n;
501/// //   for (i = 0; i < n; i++) f->p[i] = NULL;
502/// //   for (i = 0; i < n; i++) {
503/// //     f->p[i] = luaF_newproto(S->L);
504/// //     luaC_objbarrier(S->L, f, f->p[i]);
505/// //     loadFunction(S, f->p[i], f->source);
506/// //   }
507/// // }
508/// ```
509///
510/// PORT NOTE: C creates the proto first (for GC anchor) then fills it.  In
511/// Rust we create a default `LuaProto`, fill it, then wrap in `GcRef`.
512/// `f->sizep` is removed per types.tsv (`Proto.sizep → removed`).
513fn load_protos(s: &mut LoadState<'_>, f: &mut LuaProto) -> Result<(), LuaError> {
514    let n = load_int(s)? as usize;
515    // TODO(port): add overflow / OOM check.
516    let mut protos = Vec::with_capacity(n);
517
518    for _ in 0..n {
519        let mut sub = LuaProto::placeholder();
520
521        // macros.tsv: luaC_objbarrier → state.gc().obj_barrier(p, o)  no-op Phase A
522
523        // Pass parent source as fallback.
524        let parent_source = f.source.clone();
525        load_function(s, &mut sub, parent_source)?;
526
527        // Wrap in GcRef after loading.
528        // PORT NOTE: In C f->p[i] is a Proto * held by the proto's GC roots.
529        // In Rust Phase A it becomes Rc<LuaProto>.
530        // TODO(D-1c-bridge): wraps fully-populated LuaProto value; state.new_proto produces a placeholder
531        let sub_ref = GcRef::new(sub);
532        sub_ref.account_buffer(sub_ref.buffer_bytes() as isize);
533        protos.push(sub_ref);
534    }
535
536    f.p = protos;
537    Ok(())
538}
539
540/// Load upvalue descriptors into a prototype.
541///
542/// # C source
543/// ```c
544///
545/// //   int i, n;
546/// //   n = loadInt(S);
547/// //   f->upvalues = luaM_newvectorchecked(S->L, n, Upvaldesc);
548/// //   f->sizeupvalues = n;
549/// //   for (i = 0; i < n; i++)
550/// //     f->upvalues[i].name = NULL;  /* make array valid for GC */
551/// //   for (i = 0; i < n; i++) {
552/// //     f->upvalues[i].instack = loadByte(S);
553/// //     f->upvalues[i].idx    = loadByte(S);
554/// //     f->upvalues[i].kind   = loadByte(S);
555/// //   }
556/// // }
557/// ```
558///
559/// PORT NOTE: The C comment says names must be filled first for GC safety.
560/// In Rust we build `UpvalDesc` values with `name: None` and fill names later
561/// in [`load_debug`].  This requires `UpvalDesc.name` to be
562/// `Option<GcRef<LuaString>>` rather than `GcRef<LuaString>` as listed in
563/// types.tsv.  Phase B should reconcile the types.tsv entry.
564///
565/// PORT NOTE: `f->sizeupvalues` is removed per types.tsv.
566fn load_upvalues(s: &mut LoadState<'_>, f: &mut LuaProto) -> Result<(), LuaError> {
567    let n = load_int(s)? as usize;
568    // TODO(port): add overflow / OOM check.
569
570    // In Rust: construct with name = None.
571
572    let mut upvalues = Vec::with_capacity(n);
573    for _ in 0..n {
574        let instack_raw = load_byte(s)?;
575        let idx = load_byte(s)?;
576        let kind = load_byte(s)?;
577
578        // types.tsv: Upvaldesc.instack → bool (stored as lu_byte in C)
579        upvalues.push(UpvalDesc {
580            name: None, // filled by load_debug
581            instack: instack_raw != 0,
582            idx,
583            kind,
584        });
585    }
586
587    f.upvalues = upvalues;
588    Ok(())
589}
590
591/// Load debug information into a prototype.
592///
593/// # C source
594/// ```c
595///
596/// //   int i, n;
597/// //   n = loadInt(S);
598/// //   f->lineinfo = luaM_newvectorchecked(S->L, n, ls_byte);
599/// //   f->sizelineinfo = n;
600/// //   loadVector(S, f->lineinfo, n);
601/// //   n = loadInt(S);
602/// //   f->abslineinfo = luaM_newvectorchecked(S->L, n, AbsLineInfo);
603/// //   f->sizeabslineinfo = n;
604/// //   for (i = 0; i < n; i++) {
605/// //     f->abslineinfo[i].pc   = loadInt(S);
606/// //     f->abslineinfo[i].line = loadInt(S);
607/// //   }
608/// //   n = loadInt(S);
609/// //   f->locvars = luaM_newvectorchecked(S->L, n, LocVar);
610/// //   f->sizelocvars = n;
611/// //   for (i = 0; i < n; i++) f->locvars[i].varname = NULL;
612/// //   for (i = 0; i < n; i++) {
613/// //     f->locvars[i].varname = loadStringN(S, f);
614/// //     f->locvars[i].startpc = loadInt(S);
615/// //     f->locvars[i].endpc   = loadInt(S);
616/// //   }
617/// //   n = loadInt(S);
618/// //   if (n != 0)  /* does it have debug information? */
619/// //     n = f->sizeupvalues;  /* must be this many */
620/// //   for (i = 0; i < n; i++)
621/// //     f->upvalues[i].name = loadStringN(S, f);
622/// // }
623/// ```
624///
625/// PORT NOTE: `ls_byte` (signed byte) maps to `i8` per types.tsv.
626/// `loadVector(S, f->lineinfo, n)` reads `n * sizeof(ls_byte) = n` bytes.
627/// We read them as `u8` then reinterpret as `i8` via cast.
628///
629/// PORT NOTE: Size companion fields (`sizelineinfo`, `sizeabslineinfo`,
630/// `sizelocvars`) are all removed per types.tsv — `Vec::len()` covers them.
631///
632/// PORT NOTE: `LocalVar.varname` and `UpvalDesc.name` are both
633/// `Option<GcRef<LuaString>>` here because `loadStringN` can return `None`.
634/// See also the note on [`load_upvalues`].
635fn load_debug(s: &mut LoadState<'_>, f: &mut LuaProto) -> Result<(), LuaError> {
636    let n = load_int(s)? as usize;
637    let mut lineinfo = vec![0i8; n];
638    // Read as u8 slice then cast — safe because i8 and u8 have the same
639    // in-memory representation and we're casting a byte from the binary stream.
640    // SAFETY(port): this would need `unsafe` for the slice transmute in real
641    // code; for Phase A we read byte-by-byte.
642    // TODO(port): replace the loop with a single load_block into a u8 buffer
643    //             followed by an i8 transmute in Phase B (or use bytemuck).
644    for item in lineinfo.iter_mut() {
645        *item = load_byte(s)? as i8;
646    }
647    f.lineinfo = lineinfo;
648
649    let n = load_int(s)? as usize;
650    let mut abslineinfo = Vec::with_capacity(n);
651    for _ in 0..n {
652        abslineinfo.push(AbsLineInfo {
653            pc: load_int(s)?,
654            line: load_int(s)?,
655        });
656    }
657    f.abslineinfo = abslineinfo;
658
659    let n = load_int(s)? as usize;
660
661    let mut locvars = Vec::with_capacity(n);
662    for _ in 0..n {
663        let varname = load_string_n(s, f)?;
664        let startpc = load_int(s)?;
665        let endpc = load_int(s)?;
666        let varname = match varname {
667            Some(v) => v,
668            None => s.state.new_string(b"")?,
669        };
670        locvars.push(LocalVar {
671            varname,
672            startpc,
673            endpc,
674        });
675    }
676    f.locvars = locvars;
677
678    // PORT NOTE: if n == 0 then there is no upvalue name info (stripped).
679    let has_names = load_int(s)?;
680    if has_names != 0 {
681        let n_upvals = f.upvalues.len();
682        for i in 0..n_upvals {
683            let name = load_string_n(s, f)?;
684            f.upvalues[i].name = name;
685        }
686    }
687
688    Ok(())
689}
690
691// ── Function loader ────────────────────────────────────────────────────────
692
693/// Load a complete function prototype from the stream.
694///
695/// # C source
696/// ```c
697///
698/// //   f->source = loadStringN(S, f);
699/// //   if (f->source == NULL) f->source = psource;
700/// //   f->linedefined    = loadInt(S);
701/// //   f->lastlinedefined = loadInt(S);
702/// //   f->numparams   = loadByte(S);
703/// //   f->is_vararg   = loadByte(S);
704/// //   f->maxstacksize = loadByte(S);
705/// //   loadCode(S, f);
706/// //   loadConstants(S, f);
707/// //   loadUpvalues(S, f);
708/// //   loadProtos(S, f);
709/// //   loadDebug(S, f);
710/// // }
711/// ```
712///
713/// PORT NOTE: `TString *psource` becomes `Option<GcRef<LuaString>>` because
714/// the top-level call passes `NULL` (mapped to `None`).  `f->source` in `LuaProto`
715/// is typed `GcRef<LuaString>` in types.tsv, but the undump path needs
716/// `Option<GcRef<LuaString>>` to express "inherited from parent".  Phase B
717/// should align types.tsv or add a dedicated `Option` wrapper there.
718///
719/// PORT NOTE: `f->is_vararg` is stored as `lu_byte` in C but `bool` in
720/// types.tsv.  We read the raw byte and convert to `bool` via `!= 0`.
721fn load_function(
722    s: &mut LoadState<'_>,
723    f: &mut LuaProto,
724    psource: Option<GcRef<LuaString>>,
725) -> Result<(), LuaError> {
726    let source = load_string_n(s, f)?;
727    f.source = source.or(psource);
728
729    f.linedefined = load_int(s)?;
730    f.lastlinedefined = load_int(s)?;
731    f.numparams = load_byte(s)?;
732    // types.tsv: Proto.is_vararg → bool (stored as lu_byte in C)
733    f.is_vararg = load_byte(s)? != 0;
734    f.maxstacksize = load_byte(s)?;
735    load_code(s, f)?;
736    reconstruct_vararg_table_reg(f);
737    load_constants(s, f)?;
738    load_upvalues(s, f)?;
739    load_protos(s, f)?;
740    load_debug(s, f)?;
741
742    Ok(())
743}
744
745/// Recover `LuaProto.vararg_table_reg` from the loaded bytecode instead of from
746/// the wire format, so a precompiled chunk keeps Lua 5.5 named-vararg aliasing
747/// (`function f(...t)`) without lua-rs's `string.dump` output diverging from
748/// C's bytecode layout (which the structural oracle compares).
749///
750/// A named-vararg function emits exactly one `OP_VARARGPACK` (opcode 84) at
751/// entry; its A operand is the register holding the shared vararg table. Its
752/// k bit records whether the table must be materialized.
753fn reconstruct_vararg_table_reg(f: &mut LuaProto) {
754    const OP_VARARGPACK: u32 = 84;
755    const OPCODE_MASK: u32 = 0x7F;
756    const POS_K: u32 = 15;
757    if let Some((reg, needed)) = f.code.iter().find_map(|inst| {
758        let raw = inst.raw();
759        (raw & OPCODE_MASK == OP_VARARGPACK).then(|| {
760            let reg = ((raw >> 7) & 0xFF) as u8;
761            let needed = ((raw >> POS_K) & 1) != 0;
762            (reg, needed)
763        })
764    }) {
765        f.vararg_table_reg = Some(reg);
766        f.vararg_table_needed = needed;
767    }
768}
769
770// ── Header validation ──────────────────────────────────────────────────────
771
772/// Verify that the next `expected.len()` bytes in the stream match `expected`.
773///
774/// # C source
775/// ```c
776///
777/// //   char buff[sizeof(LUA_SIGNATURE) + sizeof(LUAC_DATA)];
778/// //   size_t len = strlen(s);
779/// //   loadVector(S, buff, len);
780/// //   if (memcmp(s, buff, len) != 0)
781/// //     error(S, msg);
782/// // }
783/// ```
784///
785/// PORT NOTE: `strlen` on a `const char *` becomes `.len()` on a `&[u8]`.
786/// `memcmp` becomes slice equality.
787fn check_literal(
788    s: &mut LoadState<'_>,
789    expected: &[u8],
790    msg: &'static str,
791) -> Result<(), LuaError> {
792    let mut buf = vec![0u8; expected.len()];
793    load_block(s, &mut buf)?;
794    if buf != expected {
795        return Err(load_error(s, msg));
796    }
797    Ok(())
798}
799
800/// Verify that the next byte in the stream equals `expected_size`.
801///
802/// # C source
803/// ```c
804///
805/// //   if (loadByte(S) != size)
806/// //     error(S, luaO_pushfstring(S->L, "%s size mismatch", tname));
807/// // }
808/// ```
809///
810/// PORT NOTE: `luaO_pushfstring` is used here as a message formatter, not as
811/// a throw site.  We inline the message directly.  `tname` is always a Rust
812/// type-name string literal (ASCII) from the call sites; using `&'static str`
813/// is appropriate here (not Lua data).
814fn fcheck_size(
815    s: &mut LoadState<'_>,
816    expected_size: usize,
817    tname: &'static str,
818) -> Result<(), LuaError> {
819    let b = load_byte(s)? as usize;
820    if b != expected_size {
821        // PORT NOTE: We build the error message inline rather than using
822        // luaO_pushfstring to avoid a stack push just for error formatting.
823        // TODO(port): include `tname` in the error message once LuaError::syntax
824        // supports composing byte-string and &str fragments.
825        return Err(LuaError::syntax(format_args!("{} size mismatch", tname)));
826    }
827    Ok(())
828}
829
830/// Validate the binary chunk header.
831///
832/// # C source
833/// ```c
834///
835/// //   checkliteral(S, &LUA_SIGNATURE[1], "not a binary chunk");
836/// //   if (loadByte(S) != LUAC_VERSION) error(S, "version mismatch");
837/// //   if (loadByte(S) != LUAC_FORMAT)  error(S, "format mismatch");
838/// //   checkliteral(S, LUAC_DATA, "corrupted chunk");
839/// //   checksize(S, Instruction);
840/// //   checksize(S, lua_Integer);
841/// //   checksize(S, lua_Number);
842/// //   if (loadInteger(S) != LUAC_INT) error(S, "integer format mismatch");
843/// //   if (loadNumber(S)  != LUAC_NUM) error(S, "float format mismatch");
844/// // }
845/// ```
846///
847/// PORT NOTE: `checksize(S, T)` expands to `fchecksize(S, sizeof(T), #T)`.
848/// We emit the three concrete sizes inline.
849/// - `sizeof(Instruction)` = 4 (u32)
850/// - `sizeof(lua_Integer)` = 8 (i64)
851/// - `sizeof(lua_Number)` = 8 (f64)
852///
853/// PORT NOTE: The first byte of `LUA_SIGNATURE` (`\x1b`) is already consumed
854/// by the caller before `checkHeader` is invoked, so we check only bytes 1..
855/// of the signature (`"Lua"`).
856fn check_header(s: &mut LoadState<'_>) -> Result<(), LuaError> {
857    // Skip LUA_SIGNATURE[0] (\x1b) — already consumed by the caller.
858    check_literal(s, &LUA_SIGNATURE[1..], "not a binary chunk")?;
859
860    let version = s.state.global().lua_version;
861    let expected_version = match version {
862        LuaVersion::V51 => LUAC_VERSION_51,
863        LuaVersion::V52 => LUAC_VERSION_52,
864        LuaVersion::V53 => LUAC_VERSION_53,
865        LuaVersion::V55 => LUAC_VERSION_55,
866        _ => LUAC_VERSION_54,
867    };
868    let ver = load_byte(s)?;
869    if ver != expected_version {
870        return Err(load_error(s, "version mismatch"));
871    }
872
873    let fmt = load_byte(s)?;
874    if fmt != LUAC_FORMAT {
875        return Err(load_error(s, "format mismatch"));
876    }
877
878    match version {
879        LuaVersion::V51 => {
880            check_legacy_sizes(s)?;
881        }
882        LuaVersion::V52 => {
883            check_legacy_sizes(s)?;
884            check_literal(s, LUAC_DATA, "corrupted chunk")?;
885        }
886        LuaVersion::V53 => {
887            check_literal(s, LUAC_DATA, "corrupted chunk")?;
888            fcheck_size(s, size_of::<i32>(), "int")?;
889            fcheck_size(s, size_of::<usize>(), "size_t")?;
890            fcheck_size(s, 4, "Instruction")?;
891            fcheck_size(s, 8, "lua_Integer")?;
892            fcheck_size(s, 8, "lua_Number")?;
893            if load_integer(s)? != LUAC_INT {
894                return Err(load_error(s, "integer format mismatch"));
895            }
896            if load_number(s)? != LUAC_NUM {
897                return Err(load_error(s, "float format mismatch"));
898            }
899        }
900        LuaVersion::V55 => {
901            check_literal(s, LUAC_DATA, "corrupted chunk")?;
902            fcheck_size(s, 4, "int")?;
903            if load_raw_i32(s)? != LUAC_INT_55 as i32 {
904                return Err(load_error(s, "int format mismatch"));
905            }
906
907            fcheck_size(s, 4, "instruction")?;
908            if load_raw_u32(s)? != LUAC_INST_55 {
909                return Err(load_error(s, "instruction format mismatch"));
910            }
911
912            fcheck_size(s, 8, "Lua integer")?;
913            if load_integer(s)? != LUAC_INT_55 {
914                return Err(load_error(s, "Lua integer format mismatch"));
915            }
916
917            fcheck_size(s, 8, "Lua number")?;
918            if load_number(s)? != LUAC_NUM_55 {
919                return Err(load_error(s, "Lua number format mismatch"));
920            }
921        }
922        _ => {
923            check_literal(s, LUAC_DATA, "corrupted chunk")?;
924            fcheck_size(s, 4, "Instruction")?;
925
926            fcheck_size(s, 8, "lua_Integer")?;
927
928            fcheck_size(s, 8, "lua_Number")?;
929
930            let int_check = load_integer(s)?;
931            if int_check != LUAC_INT {
932                return Err(load_error(s, "integer format mismatch"));
933            }
934
935            let num_check = load_number(s)?;
936            if num_check != LUAC_NUM {
937                return Err(load_error(s, "float format mismatch"));
938            }
939        }
940    }
941
942    Ok(())
943}
944
945/// Validate the 5.1/5.2 endianness + size + integral-flag block: endian = 1
946/// (little), `sizeof(int)` = 4, `sizeof(size_t)`, `sizeof(Instruction)` = 4,
947/// `sizeof(lua_Number)` = 8, integral = 0. These versions have no integer
948/// subtype, so there is no `lua_Integer` size byte and no `LUAC_INT`/`LUAC_NUM`
949/// sentinel.
950fn check_legacy_sizes(s: &mut LoadState<'_>) -> Result<(), LuaError> {
951    if load_byte(s)? != 1 {
952        return Err(load_error(s, "endianness mismatch"));
953    }
954    fcheck_size(s, size_of::<i32>(), "int")?;
955    fcheck_size(s, size_of::<usize>(), "size_t")?;
956    fcheck_size(s, 4, "Instruction")?;
957    fcheck_size(s, 8, "lua_Number")?;
958    if load_byte(s)? != 0 {
959        return Err(load_error(s, "number format mismatch"));
960    }
961    Ok(())
962}
963
964// ── Public entry point ─────────────────────────────────────────────────────
965
966/// Load a precompiled Lua chunk and return the top-level Lua closure.
967///
968/// This is the Rust equivalent of `luaU_undump` — the single public function
969/// exported by `lundump.c`.
970///
971/// # C source
972/// ```c
973///
974/// //   LoadState S;
975/// //   LClosure *cl;
976/// //   if (*name == '@' || *name == '=')
977/// //     S.name = name + 1;
978/// //   else if (*name == LUA_SIGNATURE[0])
979/// //     S.name = "binary string";
980/// //   else
981/// //     S.name = name;
982/// //   S.L = L; S.Z = Z;
983/// //   checkHeader(&S);
984/// //   cl = luaF_newLclosure(L, loadByte(&S));
985/// //   setclLvalue2s(L, L->top.p, cl);
986/// //   luaD_inctop(L);
987/// //   cl->p = luaF_newproto(L);
988/// //   luaC_objbarrier(L, cl, cl->p);
989/// //   loadFunction(&S, cl->p, NULL);
990/// //   lua_assert(cl->nupvalues == cl->p->sizeupvalues);
991/// //   luai_verifycode(L, cl->p);
992/// //   return cl;
993/// // }
994/// ```
995///
996/// # Parameters
997/// - `state` — the Lua thread state.
998/// - `z` — input stream positioned at the start of the binary chunk
999///   (the first byte `\x1b` of `LUA_SIGNATURE` must still be present).
1000/// - `name` — chunk name for error messages.  Stripped per Lua convention:
1001///   - `@…` → filename (strip `@`)
1002///   - `=…` → literal name (strip `=`)
1003///   - starts with `\x1b` → `"binary string"`
1004///   - otherwise used as-is.
1005///
1006/// PORT NOTE: The C function returns `LClosure *`.  In Rust we return
1007/// `GcRef<LuaLClosure>` (the Lua-closure variant of `LuaClosure`).  The
1008/// closure is also pushed onto the stack for GC anchoring, matching the C
1009/// behaviour (`setclLvalue2s + luaD_inctop`).  The caller is responsible for
1010/// popping it when done (consistent with C).
1011///
1012/// PORT NOTE: `luai_verifycode` is a no-op in the default build
1013/// (`#define luai_verifycode(L,f)  /* empty */`); dropped here.
1014///
1015/// PORT NOTE: `cl->nupvalues == cl->p->sizeupvalues` — in Rust the nupvalues
1016/// count is implicit in `cl.upvals.len()` and `f.upvalues.len()`; the
1017/// assertion becomes `debug_assert_eq!`.
1018pub(crate) fn undump(
1019    state: &mut LuaState,
1020    z: &mut ZIO,
1021    _name: &[u8],
1022) -> Result<GcRef<LuaLClosure>, LuaError> {
1023    let mut s = LoadState { state, z };
1024
1025    check_header(&mut s)?;
1026
1027    // loadByte(&S) reads the number of upvalues for the top-level closure.
1028    let nupvalues = load_byte(&mut s)?;
1029    // PORT NOTE: `luaF_newLclosure` allocates a closure with `nupvalues`
1030    // upvalue slots.  In Rust Phase A we construct the struct directly; the
1031    // GcRef wrapping happens after the proto is loaded.
1032    // TODO(port): use the proper lfunc::new_lua_closure(state, nupvalues) API
1033    // once lfunc.rs is translated and the API is settled.
1034    let mut cl = LuaLClosure::placeholder();
1035    let mut upvals_vec = Vec::with_capacity(nupvalues as usize);
1036    for _ in 0..nupvalues as usize {
1037        upvals_vec.push(std::cell::Cell::new(
1038            s.state.new_upval_closed(LuaValue::Nil),
1039        ));
1040    }
1041    cl.upvals = upvals_vec.into_boxed_slice();
1042
1043    // macros.tsv: setclLvalue2s → state.set_at(o, LuaValue::Function(LuaClosure::Lua(cl)))
1044    // macros.tsv: luaD_inctop → (state.push already increments; use state.push)
1045    // PORT NOTE: We push a placeholder Nil first; the real closure value is
1046    // set after the proto is loaded.  This mirrors the C "anchor for GC"
1047    // pattern.  In Phase A-C GC anchoring via the stack is not strictly
1048    // necessary (Rc keeps things alive) but we preserve the stack discipline
1049    // for behavioural parity.
1050    // TODO(port): once GcRef<LuaLClosure> is cloneable into LuaValue, push
1051    // the real value here instead of a placeholder.
1052    s.state.push(LuaValue::Nil); // placeholder; replaced below
1053
1054    let mut proto = LuaProto::placeholder();
1055
1056    // macros.tsv: luaC_objbarrier → state.gc().obj_barrier(p, o)  no-op Phase A
1057
1058    load_function(&mut s, &mut proto, None)?;
1059
1060    // Wrap the proto in a GcRef and attach it to the closure.
1061    // TODO(D-1c-bridge): wraps fully-populated LuaProto value; state.new_proto produces a placeholder
1062    let proto_ref = GcRef::new(proto);
1063    proto_ref.account_buffer(proto_ref.buffer_bytes() as isize);
1064
1065    // macros.tsv: lua_assert → debug_assert!
1066    // nupvalues is the byte we read; sizeupvalues = proto_ref.upvalues.len()
1067    debug_assert_eq!(
1068        nupvalues as usize,
1069        proto_ref.upvalues.len(),
1070        "upvalue count mismatch between closure header and prototype"
1071    );
1072
1073    // The macro is defined as `/* empty */` in the default build; dropped.
1074
1075    // Attach the loaded proto to the closure.
1076    cl.proto = proto_ref;
1077
1078    // Wrap the closure in GcRef.
1079    // TODO(D-1c-bridge): wraps fully-populated LuaLClosure value; state.new_lclosure makes Nil-filled upvals
1080    let cl_ref = GcRef::new(cl);
1081    cl_ref.account_buffer(cl_ref.buffer_bytes() as isize);
1082
1083    // Replace the stack placeholder with the real closure value.
1084    // macros.tsv: setclLvalue2s → state.set_at(o, LuaValue::Function(LuaClosure::Lua(...)))
1085    // TODO(port): replace the placeholder at the correct stack slot.
1086    // For now the top slot holds Nil; Phase B must fix this once
1087    // GcRef<LuaLClosure> → LuaValue conversion is defined.
1088    // TODO(port): update the stack slot pushed above with the real cl_ref value.
1089
1090    Ok(cl_ref)
1091}
1092
1093// ──────────────────────────────────────────────────────────────────────────
1094// PORT STATUS
1095//   source:        src/lundump.c  (335 lines, 20 functions/items)
1096//                  src/lundump.h  (35 lines, merged)
1097//   target_crate:  lua-vm
1098//   confidence:    medium
1099//   todos:         15
1100//   port_notes:    39
1101//   unsafe_blocks: 0   (must be 0 outside explicit unsafe-budget crates)
1102//   notes:         Logic is faithful to the C.  The main open items for Phase B
1103//                  are: (1) import paths for GcRef/LuaProto/LuaClosure/etc.;
1104//                  (2) LuaError::syntax byte-string formatting for the chunk
1105//                  name in load_error; (3) long-string vs short-string intern
1106//                  distinction in load_string_n; (4) the stack placeholder in
1107//                  undump must be replaced with the real GcRef<LuaLClosure>
1108//                  value once LuaValue conversion is defined; (5) UpvalDesc.name
1109//                  and LocalVar.varname need Option<GcRef<LuaString>> in the
1110//                  proto type to match the two-pass load order here.
1111// ──────────────────────────────────────────────────────────────────────────