Skip to main content

lua_vm/
dump.rs

1//! Pre-compiled Lua chunk serializer.
2//!
3//! Translates `reference/lua-5.4.7/src/ldump.c` (230 lines, 9 functions + 1 public entry point).
4//! Writes a `LuaProto` to a byte sink in the standard Lua 5.4 bytecode format.
5
6// C: #define ldump_c / #define LUA_CORE — build guards; not needed in Rust.
7
8// TODO(port): Adjust import paths once crate boundaries stabilise in Phase B.
9// The types below are expected to resolve as follows:
10//   GcRef        — lua_types (or lua-gc Phase D)
11//   LuaError     — lua_types
12//   LuaProto     — lua-vm (this crate) or lua-types
13//   LuaString    — lua-vm / lua-types
14//   LuaValue     — lua_types
15//   LuaState     — lua-vm (this crate)
16use std::mem::size_of;
17#[allow(unused_imports)] use crate::prelude::*;
18
19use crate::state::LuaState;
20use lua_types::{GcRef, LuaError, LuaString, LuaValue};
21use lua_types::proto::LuaProto;
22
23// ── Constants from lundump.h ─────────────────────────────────────────────────
24
25// C: LUA_SIGNATURE "\x1bLua"  (lua.h; also in macros.tsv)
26// dumpLiteral expands to dumpBlock(D, s, sizeof(s) - sizeof(char)).
27// sizeof("\x1bLua") = 5; minus 1 = 4 bytes, no NUL terminator.
28// b"\x1bLua" is &[u8; 4] in Rust — no NUL — so direct use is correct.
29const LUA_SIGNATURE: &[u8] = b"\x1bLua";
30
31// C: #define LUAC_VERSION (((LUA_VERSION_NUM / 100) * 16) + LUA_VERSION_NUM % 100)
32// With LUA_VERSION_NUM = 504 (macros.tsv):
33//   (504 / 100) * 16 + 504 % 100 = 5 * 16 + 4 = 84 = 0x54
34const LUA_VERSION_NUM_DUMP: i32 = 504;
35const LUAC_VERSION: u8 =
36    ((LUA_VERSION_NUM_DUMP / 100) * 16 + LUA_VERSION_NUM_DUMP % 100) as u8;
37
38// C: #define LUAC_FORMAT 0  /* this is the official format */
39const LUAC_FORMAT: u8 = 0;
40
41// C: #define LUAC_DATA "\x19\x93\r\n\x1a\n"
42// sizeof("\x19\x93\r\n\x1a\n") = 7; minus 1 = 6 bytes written.
43// b"\x19\x93\r\n\x1a\n" is &[u8; 6].
44const LUAC_DATA: &[u8] = b"\x19\x93\r\n\x1a\n";
45
46// C: #define LUAC_INT 0x5678
47const LUAC_INT: i64 = 0x5678;
48
49// C: #define LUAC_NUM cast_num(370.5)   cast_num → `as f64` (macros.tsv)
50const LUAC_NUM: f64 = 370.5;
51
52// C: sizeof(Instruction); Instruction is a u32 newtype (types.tsv).
53const INSTRUCTION_SIZE: u8 = size_of::<u32>() as u8;
54
55// C: sizeof(lua_Integer) = 8; lua_Integer → i64 (types.tsv).
56const LUA_INTEGER_SIZE: u8 = size_of::<i64>() as u8;
57
58// C: sizeof(lua_Number) = 8; lua_Number → f64 (types.tsv).
59const LUA_NUMBER_SIZE: u8 = size_of::<f64>() as u8;
60
61// ── DumpState ────────────────────────────────────────────────────────────────
62
63/// Internal state threaded through every dump operation.
64///
65/// C: `typedef struct { lua_State *L; lua_Writer writer; void *data; int strip; int status; } DumpState;`
66///
67/// PORT NOTE: `lua_State *L` removed — it was used only for `lua_lock`/`lua_unlock`, which are
68/// no-ops in the default Lua build and dropped here (macros.tsv). `void *data` is folded into
69/// the writer closure. `int status` is replaced by `Result<(), LuaError>` propagated with `?`.
70struct DumpState<'a> {
71    /// Byte-sink callback. C original: `lua_Writer writer` + `void *data` (combined).
72    /// lua_Writer type is TBD in types.tsv; for dump we use a bare byte-slice callback.
73    writer: &'a mut dyn FnMut(&[u8]) -> Result<(), LuaError>,
74    /// When true, strip all debug information from the output.
75    strip: bool,
76}
77
78impl<'a> DumpState<'a> {
79    // ── Low-level write primitives ────────────────────────────────────────────
80
81    /// Write raw bytes to the output stream.
82    ///
83    /// C: `static void dumpBlock(DumpState *D, const void *b, size_t size)`
84    ///
85    /// PORT NOTE: C accumulates errors in `D->status` and skips subsequent writes once
86    /// non-zero; Rust returns `Result<(), LuaError>` and short-circuits via `?`.
87    /// `lua_lock`/`lua_unlock` are no-ops in the default build and are dropped (macros.tsv).
88    fn dump_block(&mut self, data: &[u8]) -> Result<(), LuaError> {
89        // C: if (D->status == 0 && size > 0)
90        if !data.is_empty() {
91            // C: lua_unlock(D->L);
92            // C: D->status = (*D->writer)(D->L, b, size, D->data);
93            // C: lua_lock(D->L);
94            (self.writer)(data)?;
95        }
96        Ok(())
97    }
98
99    /// Write one byte.
100    ///
101    /// C: `static void dumpByte(DumpState *D, int y)`
102    /// C body: `lu_byte x = (lu_byte)y; dumpVar(D, x);`
103    /// (`dumpVar(D,x)` expands to `dumpVector(D,&x,1)` expands to `dumpBlock(D,&x,sizeof(x))`)
104    fn dump_byte(&mut self, y: u8) -> Result<(), LuaError> {
105        // C: lu_byte x = (lu_byte)y; dumpVar(D, x)
106        self.dump_block(&[y])
107    }
108
109    /// Write a `size_t` using Lua's variable-length encoding.
110    ///
111    /// C: `static void dumpSize(DumpState *D, size_t x)`
112    ///
113    /// Encoding (big-endian 7-bit groups, **last** byte marked with MSB = 1):
114    /// - Each byte holds 7 payload bits.
115    /// - Bytes are written most-significant group first.
116    /// - The final byte (least-significant group) has its MSB set as an end marker.
117    ///
118    /// This differs from standard LEB128, which marks the *continuation* bytes rather than
119    /// the terminating byte.
120    ///
121    /// C: `#define DIBS ((sizeof(size_t) * CHAR_BIT + 6) / 7)` — 10 on 64-bit.
122    fn dump_size(&mut self, mut x: usize) -> Result<(), LuaError> {
123        // C: lu_byte buff[DIBS]; int n = 0;
124        // DIBS = (usize::BITS + 6) / 7; on 64-bit = (64+6)/7 = 10.
125        const DIBS: usize = (usize::BITS as usize + 6) / 7;
126        let mut buff = [0u8; DIBS];
127        let mut n: usize = 0;
128
129        // C: do { buff[DIBS - (++n)] = x & 0x7f; x >>= 7; } while (x != 0);
130        loop {
131            n += 1;
132            buff[DIBS - n] = (x & 0x7f) as u8; // fill buffer in reverse order
133            x >>= 7;
134            if x == 0 {
135                break;
136            }
137        }
138
139        // C: buff[DIBS - 1] |= 0x80; /* mark last byte */
140        // The byte at buff[DIBS-1] is the first byte placed (least-significant group).
141        // Setting its MSB marks it as the terminal byte of the encoding.
142        buff[DIBS - 1] |= 0x80;
143
144        // C: dumpVector(D, buff + DIBS - n, n);
145        self.dump_block(&buff[DIBS - n..])
146    }
147
148    /// Write an `int` as a variable-length size.
149    ///
150    /// C: `static void dumpInt(DumpState *D, int x)` → `dumpSize(D, x);`
151    ///
152    /// PORT NOTE: C implicitly casts `int` → `size_t`. All call sites pass non-negative values
153    /// (line numbers, instruction counts, vector lengths); a debug assertion guards this.
154    fn dump_int(&mut self, x: i32) -> Result<(), LuaError> {
155        // C: dumpSize(D, x)
156        debug_assert!(
157            x >= 0,
158            "dump_int: negative value {} cast to usize would wrap",
159            x
160        );
161        self.dump_size(x as usize)
162    }
163
164    /// Write a `lua_Number` (f64) in the platform's native byte order.
165    ///
166    /// C: `static void dumpNumber(DumpState *D, lua_Number x)` → `dumpVar(D, x);`
167    ///
168    /// `dumpVar(D,x)` expands to `dumpBlock(D, &x, sizeof(lua_Number))` — 8 bytes, native order.
169    /// `to_ne_bytes()` replicates native-endian serialisation. The bytecode header's `LUAC_NUM`
170    /// sentinel (370.5) lets `lundump` detect byte-order mismatches at load time.
171    fn dump_number(&mut self, x: f64) -> Result<(), LuaError> {
172        // C: dumpVar(D, x) → dumpBlock(D, &x, sizeof(lua_Number))
173        self.dump_block(&x.to_ne_bytes())
174    }
175
176    /// Write a `lua_Integer` (i64) in the platform's native byte order.
177    ///
178    /// C: `static void dumpInteger(DumpState *D, lua_Integer x)` → `dumpVar(D, x);`
179    fn dump_integer(&mut self, x: i64) -> Result<(), LuaError> {
180        // C: dumpVar(D, x) → dumpBlock(D, &x, sizeof(lua_Integer))
181        self.dump_block(&x.to_ne_bytes())
182    }
183
184    // ── Mid-level serialisers ─────────────────────────────────────────────────
185
186    /// Write an interned or long string, or a null sentinel (encoded size = 0).
187    ///
188    /// C: `static void dumpString(DumpState *D, const TString *s)`
189    ///
190    /// Encoding: `dumpSize(len + 1)` followed by `len` raw bytes; size 0 means null/absent.
191    /// `tsslen(s)` → `s.len()` and `getstr(s)` → `s.as_bytes()` (macros.tsv).
192    fn dump_string(&mut self, s: Option<&GcRef<LuaString>>) -> Result<(), LuaError> {
193        match s {
194            // C: if (s == NULL) dumpSize(D, 0);
195            None => self.dump_size(0),
196
197            Some(s) => {
198                // C: size_t size = tsslen(s); const char *str = getstr(s);
199                let bytes = s.as_bytes(); // tsslen → .len(); getstr → .as_bytes()
200                // C: dumpSize(D, size + 1); dumpVector(D, str, size);
201                self.dump_size(bytes.len() + 1)?;
202                self.dump_block(bytes)
203            }
204        }
205    }
206
207    /// Write the bytecode instruction array.
208    ///
209    /// C: `static void dumpCode(DumpState *D, const Proto *f)`
210    ///
211    /// PORT NOTE: `f->sizecode` is covered by `Vec::len()` (types.tsv).
212    fn dump_code(&mut self, proto: &LuaProto) -> Result<(), LuaError> {
213        // C: dumpInt(D, f->sizecode);
214        self.dump_int(proto.code.len() as i32)?;
215
216        // C: dumpVector(D, f->code, f->sizecode)
217        // dumpVector writes n * sizeof(Instruction) = n * 4 bytes in native byte order.
218        for instr in &proto.code {
219            // TODO(port): `Instruction` is a u32 newtype (types.tsv). Accessing the inner u32
220            // via `.0` assumes a tuple-struct layout. If the Instruction API differs (e.g.,
221            // exposes `.raw()` or `u32::from(*instr)`), adjust accordingly in Phase B.
222            self.dump_block(&instr.0.to_ne_bytes())?;
223        }
224        Ok(())
225    }
226
227    /// Write the constant pool.
228    ///
229    /// C: `static void dumpConstants(DumpState *D, const Proto *f)`
230    ///
231    /// Each constant is written as: one tag byte (`ttypetag`), followed by the payload
232    /// (float: 8 bytes; integer: 8 bytes; string: variable-length; nil/bool: nothing).
233    ///
234    /// PORT NOTE: `f->sizek` is covered by `Vec::len()` (types.tsv).
235    fn dump_constants(&mut self, proto: &LuaProto) -> Result<(), LuaError> {
236        // C: int n = f->sizek; dumpInt(D, n);
237        let n = proto.k.len();
238        self.dump_int(n as i32)?;
239
240        for constant in &proto.k {
241            // C: int tt = ttypetag(o); dumpByte(D, tt);
242            // ttypetag(o) → o.full_type_tag() (macros.tsv)
243            // Returns the C-side tag byte: bits 0-3 base type, bits 4-5 variant, bit 6 collectable.
244            let tag = constant.full_type_tag();
245            self.dump_byte(tag)?;
246
247            // C: switch (tt) { case LUA_VNUMFLT / LUA_VNUMINT / LUA_VSHRSTR / LUA_VLNGSTR / default }
248            match constant {
249                LuaValue::Float(f) => {
250                    // C: case LUA_VNUMFLT: dumpNumber(D, fltvalue(o));
251                    // fltvalue(o) → o.as_float().expect("not float") or `if let` (macros.tsv)
252                    self.dump_number(*f)?;
253                }
254                LuaValue::Int(i) => {
255                    // C: case LUA_VNUMINT: dumpInteger(D, ivalue(o));
256                    self.dump_integer(*i)?;
257                }
258                LuaValue::Str(s) => {
259                    // C: case LUA_VSHRSTR: case LUA_VLNGSTR: dumpString(D, tsvalue(o));
260                    // tsvalue(o) → o.as_string().expect("not string") (macros.tsv)
261                    self.dump_string(Some(s))?;
262                }
263                LuaValue::Nil | LuaValue::Bool(_) => {
264                    // C: default: lua_assert(tt == LUA_VNIL || tt == LUA_VFALSE || tt == LUA_VTRUE)
265                    // Only the tag byte is written; nil and booleans carry no additional payload.
266                    // lua_assert → debug_assert! (macros.tsv)
267                    debug_assert!(
268                        matches!(constant, LuaValue::Nil | LuaValue::Bool(_)),
269                        "dump_constants: default branch reached for unexpected variant"
270                    );
271                }
272                _ => {
273                    // TODO(port): LuaValue variant not valid as a constant-pool entry.
274                    // In C the default branch asserts nil/false/true only. Any other variant
275                    // here indicates a malformed proto; flag for Phase B investigation.
276                    debug_assert!(false, "dump_constants: unexpected LuaValue variant in constant pool");
277                }
278            }
279        }
280        Ok(())
281    }
282
283    /// Write nested function prototypes (sub-functions defined inside `proto`).
284    ///
285    /// C: `static void dumpProtos(DumpState *D, const Proto *f)`
286    ///
287    /// PORT NOTE: `f->sizep` is covered by `Vec::len()` (types.tsv).
288    /// The parent's source string is passed down so that children with identical source
289    /// origins can omit the redundant source name (see `dump_function`).
290    fn dump_protos(&mut self, proto: &LuaProto) -> Result<(), LuaError> {
291        // C: int n = f->sizep; dumpInt(D, n);
292        let n = proto.p.len();
293        self.dump_int(n as i32)?;
294
295        for sub in &proto.p {
296            // C: dumpFunction(D, f->p[i], f->source);
297            // sub: &GcRef<LuaProto>; deref coercion (&GcRef<LuaProto> → &LuaProto) expected
298            // when GcRef<T>: Deref<Target=T> (true for Rc<T> in Phase A).
299            self.dump_function(sub, proto.source.as_ref())?;
300        }
301        Ok(())
302    }
303
304    /// Write upvalue descriptors (instack / idx / kind for each upvalue slot).
305    ///
306    /// C: `static void dumpUpvalues(DumpState *D, const Proto *f)`
307    ///
308    /// PORT NOTE: `f->sizeupvalues` is covered by `Vec::len()` (types.tsv).
309    /// `Upvaldesc.instack` is `bool` in Rust (types.tsv); cast to `u8` for the wire format.
310    fn dump_upvalues(&mut self, proto: &LuaProto) -> Result<(), LuaError> {
311        // C: int i, n = f->sizeupvalues; dumpInt(D, n);
312        let n = proto.upvalues.len();
313        self.dump_int(n as i32)?;
314
315        for upval in &proto.upvalues {
316            // C: dumpByte(D, f->upvalues[i].instack);
317            // PORT NOTE: instack is bool in Rust (types.tsv); cast to u8: true→1, false→0.
318            self.dump_byte(upval.instack as u8)?;
319            // C: dumpByte(D, f->upvalues[i].idx);
320            self.dump_byte(upval.idx)?;
321            // C: dumpByte(D, f->upvalues[i].kind);
322            self.dump_byte(upval.kind)?;
323        }
324        Ok(())
325    }
326
327    /// Write debug information: per-instruction line deltas, absolute line records,
328    /// local-variable lifetimes, and upvalue names.
329    ///
330    /// All counts are written as zero when `self.strip` is true.
331    ///
332    /// C: `static void dumpDebug(DumpState *D, const Proto *f)`
333    ///
334    /// PORT NOTE: all `f->size*` fields are covered by `Vec::len()` (types.tsv).
335    fn dump_debug(&mut self, proto: &LuaProto) -> Result<(), LuaError> {
336        // C: n = (D->strip) ? 0 : f->sizelineinfo; dumpInt(D, n);
337        let n_lineinfo = if self.strip { 0 } else { proto.lineinfo.len() };
338        self.dump_int(n_lineinfo as i32)?;
339
340        // C: dumpVector(D, f->lineinfo, n)
341        // lineinfo is Vec<i8> (ls_byte per types.tsv). C writes them as raw bytes (sizeof(i8)=1).
342        // Cast each i8 to u8 (same bit pattern) before writing.
343        // PERF(port): iterating one byte at a time vs. bulk write — profile in Phase B.
344        // (A bulk write would require bytemuck::cast_slice or similar to avoid unsafe.)
345        let lineinfo_bytes: Vec<u8> = proto.lineinfo[..n_lineinfo]
346            .iter()
347            .map(|&b| b as u8)
348            .collect();
349        self.dump_block(&lineinfo_bytes)?;
350
351        // C: n = (D->strip) ? 0 : f->sizeabslineinfo; dumpInt(D, n);
352        let n_absline = if self.strip { 0 } else { proto.abslineinfo.len() };
353        self.dump_int(n_absline as i32)?;
354
355        for abs in proto.abslineinfo.iter().take(n_absline) {
356            // C: dumpInt(D, f->abslineinfo[i].pc); dumpInt(D, f->abslineinfo[i].line);
357            // AbsLineInfo.pc and .line are i32 (types.tsv); non-negative in valid bytecode.
358            self.dump_int(abs.pc)?;
359            self.dump_int(abs.line)?;
360        }
361
362        // C: n = (D->strip) ? 0 : f->sizelocvars; dumpInt(D, n);
363        let n_locvars = if self.strip { 0 } else { proto.locvars.len() };
364        self.dump_int(n_locvars as i32)?;
365
366        for locvar in proto.locvars.iter().take(n_locvars) {
367            // C: dumpString(D, f->locvars[i].varname);
368            // LocVar.varname is GcRef<LuaString> (types.tsv).
369            self.dump_string(Some(&locvar.varname))?;
370            // C: dumpInt(D, f->locvars[i].startpc);
371            self.dump_int(locvar.startpc)?;
372            // C: dumpInt(D, f->locvars[i].endpc);
373            self.dump_int(locvar.endpc)?;
374        }
375
376        // C: n = (D->strip) ? 0 : f->sizeupvalues; dumpInt(D, n);
377        // (Re-uses upvalues.len() for the name-writing pass — separate from dumpUpvalues
378        //  which wrote structural descriptors; here we write debug names.)
379        let n_upval_names = if self.strip { 0 } else { proto.upvalues.len() };
380        self.dump_int(n_upval_names as i32)?;
381
382        for upval in proto.upvalues.iter().take(n_upval_names) {
383            // C: dumpString(D, f->upvalues[i].name);
384            // PORT NOTE: UpvalDesc.name is GcRef<LuaString> per types.tsv (non-optional).
385            // TODO(port): In C, `TString *name` can be NULL when an upvalue is unnamed (e.g.,
386            // in bytecode compiled without debug info). Verify whether UpvalDesc.name should be
387            // `Option<GcRef<LuaString>>` in the Rust model; if so, change call to pass the Option
388            // directly instead of wrapping in Some.
389            self.dump_string(upval.name.as_ref())?;
390        }
391        Ok(())
392    }
393
394    /// Write a complete function prototype: source name, header bytes, code, constants,
395    /// upvalue descriptors, nested prototypes, and debug information.
396    ///
397    /// `psource` is the parent function's source string. When `f->source == psource` (pointer
398    /// equality — Lua interns short strings so identical source names share an object), the
399    /// source is written as null (size 0) to avoid duplication. The top-level call passes
400    /// `None` to force writing the source.
401    ///
402    /// C: `static void dumpFunction(DumpState *D, const Proto *f, TString *psource)`
403    ///
404    /// PORT NOTE: `f->source == psource` is a C pointer comparison exploiting string interning.
405    /// In Rust we use `GcRef::ptr_eq` (equivalent to `Rc::ptr_eq` in Phase A) for identity.
406    /// `is_vararg` is `bool` in Rust (types.tsv); cast to `u8` for the wire format.
407    fn dump_function(
408        &mut self,
409        proto: &LuaProto,
410        psource: Option<&GcRef<LuaString>>,
411    ) -> Result<(), LuaError> {
412        // C: if (D->strip || f->source == psource) dumpString(D, NULL); else dumpString(D, f->source);
413        // Pointer-equality check: same interned string object means same source file.
414        let same_source = match (psource, proto.source.as_ref()) {
415            (Some(ps), Some(src)) => GcRef::ptr_eq(src, ps),
416            _ => false,
417        };
418
419        if self.strip || same_source {
420            self.dump_string(None)?;
421        } else {
422            self.dump_string(proto.source.as_ref())?;
423        }
424
425        // C: dumpInt(D, f->linedefined);
426        self.dump_int(proto.linedefined)?;
427        // C: dumpInt(D, f->lastlinedefined);
428        self.dump_int(proto.lastlinedefined)?;
429        // C: dumpByte(D, f->numparams);
430        self.dump_byte(proto.numparams)?;
431        // C: dumpByte(D, f->is_vararg);
432        // PORT NOTE: is_vararg is bool in Rust (types.tsv); true → 1u8, false → 0u8.
433        self.dump_byte(proto.is_vararg as u8)?;
434        // C: dumpByte(D, f->maxstacksize);
435        self.dump_byte(proto.maxstacksize)?;
436
437        self.dump_code(proto)?;
438        self.dump_constants(proto)?;
439        self.dump_upvalues(proto)?;
440        self.dump_protos(proto)?;
441        self.dump_debug(proto)?;
442        Ok(())
443    }
444
445    /// Write the binary chunk header.
446    ///
447    /// The header allows `lundump` (and external tools) to verify the bytecode format,
448    /// platform word sizes, and byte order before attempting to load the chunk.
449    ///
450    /// C: `static void dumpHeader(DumpState *D)`
451    fn dump_header(&mut self) -> Result<(), LuaError> {
452        // C: dumpLiteral(D, LUA_SIGNATURE)
453        // dumpLiteral(D,s) = dumpBlock(D, s, sizeof(s) - sizeof(char))
454        // b"\x1bLua" is &[u8; 4] (no NUL terminator in Rust byte literals), matching the
455        // C expansion of sizeof("\x1bLua")-1 = 4 bytes.
456        self.dump_block(LUA_SIGNATURE)?;
457
458        // C: dumpByte(D, LUAC_VERSION)
459        self.dump_byte(LUAC_VERSION)?;
460
461        // C: dumpByte(D, LUAC_FORMAT)
462        self.dump_byte(LUAC_FORMAT)?;
463
464        // C: dumpLiteral(D, LUAC_DATA)
465        // b"\x19\x93\r\n\x1a\n" is &[u8; 6], matching sizeof(LUAC_DATA)-1 = 6 bytes.
466        self.dump_block(LUAC_DATA)?;
467
468        // C: dumpByte(D, sizeof(Instruction))
469        self.dump_byte(INSTRUCTION_SIZE)?;
470
471        // C: dumpByte(D, sizeof(lua_Integer))
472        self.dump_byte(LUA_INTEGER_SIZE)?;
473
474        // C: dumpByte(D, sizeof(lua_Number))
475        self.dump_byte(LUA_NUMBER_SIZE)?;
476
477        // C: dumpInteger(D, LUAC_INT)   — 0x5678 as i64, native byte order
478        self.dump_integer(LUAC_INT)?;
479
480        // C: dumpNumber(D, LUAC_NUM)    — 370.5 as f64, native byte order
481        self.dump_number(LUAC_NUM)?;
482
483        Ok(())
484    }
485}
486
487// ── Public entry point ───────────────────────────────────────────────────────
488
489/// Serialize a compiled Lua function prototype as a precompiled bytecode chunk.
490///
491/// The `writer` callback receives successive slices of the serialised bytes and returns
492/// `Err(LuaError)` to abort. `strip` omits debug info (line numbers, local names, etc.)
493/// from the output.
494///
495/// C: `int luaU_dump(lua_State *L, const Proto *f, lua_Writer w, void *data, int strip)`
496///
497/// PORT NOTE: `lua_Writer w` (fn pointer) + `void *data` (userdata) are collapsed into a
498/// single `impl FnMut(&[u8]) -> Result<(), LuaError>` closure — the Rust idiom for the
499/// callback + context pair. `_state` is retained in the signature for API parity but unused
500/// in the body: the C code needed it only for `lua_lock`/`lua_unlock`, which are no-ops per
501/// macros.tsv. Return type changes from `int` (0 = ok, non-zero = writer error) to
502/// `Result<(), LuaError>`.
503pub(crate) fn dump(
504    _state: &LuaState,
505    proto: &GcRef<LuaProto>,
506    writer: &mut dyn FnMut(&[u8]) -> Result<(), LuaError>,
507    strip: bool,
508) -> Result<(), LuaError> {
509    // C: DumpState D; D.L = L; D.writer = w; D.data = data; D.strip = strip; D.status = 0;
510    let mut d = DumpState {
511        writer,
512        strip,
513    };
514
515    // C: dumpHeader(&D);
516    d.dump_header()?;
517
518    // C: dumpByte(&D, f->sizeupvalues);
519    // PORT NOTE: f->sizeupvalues is covered by Vec::len(). Bounded by MAXUPVAL = 255
520    // (macros.tsv), so truncation via `as u8` is safe for well-formed prototypes.
521    d.dump_byte(proto.upvalues.len() as u8)?;
522
523    // C: dumpFunction(&D, f, NULL);
524    // psource = None forces the top-level function to always write its source name.
525    // Deref coercion: &GcRef<LuaProto> → &LuaProto (via Deref<Target=LuaProto> on GcRef/Rc).
526    d.dump_function(proto, None)?;
527
528    // C: return D.status;
529    Ok(())
530}
531
532// ────────────────────────────────────────────────────────────────────────────
533// PORT STATUS
534//   source:        src/ldump.c  (230 lines, 10 functions)
535//   target_crate:  lua-vm
536//   confidence:    medium
537//   todos:         4
538//   port_notes:    12
539//   unsafe_blocks: 0
540//   notes:         Types/imports need Phase B wiring; logic should be faithful.
541//                  Key uncertainties: (1) Instruction newtype inner-field access (.0 vs
542//                  method); (2) UpvalDesc.name optionality; (3) GcRef::ptr_eq method
543//                  existence. Lineinfo bulk-write is done via collect()+dump_block to
544//                  avoid unsafe transmute of &[i8] → &[u8]; revisit with bytemuck in
545//                  Phase B for performance. Native-endian serialisation via to_ne_bytes()
546//                  matches C's raw-memory dumpVector behaviour.
547// ────────────────────────────────────────────────────────────────────────────