Skip to main content

lua_vm/
dump.rs

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