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}