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