Skip to main content

lua_vm/
undump.rs

1//! Load precompiled Lua chunks.
2//!
3//! The binary chunk format matches the reference C implementation
4//! (`lundump.c`/`lundump.h`) byte-for-byte, so `string.dump` output and
5//! precompiled chunks stay interchangeable with stock Lua.
6//!
7//! The public entry point is [`undump`], which reads a binary Lua chunk from
8//! a [`ZIO`] stream and returns a Lua closure ready to call.
9
10#[allow(unused_imports)]
11use crate::prelude::*;
12use crate::state::LuaState;
13use crate::zio::ZIO;
14use lua_types::error::LuaError;
15use lua_types::value::LuaValue;
16
17use lua_types::closure::LuaLClosure;
18use lua_types::gc::GcRef;
19use lua_types::opcode::Instruction;
20use lua_types::proto::{AbsLineInfo, LocalVar, LuaProto, UpvalDesc};
21use lua_types::string::LuaString;
22use lua_types::LuaVersion;
23
24// ── Constants (from lundump.h) ─────────────────────────────────────────────
25
26/// Six-byte data marker in the chunk header used to catch conversion errors.
27const LUAC_DATA: &[u8] = b"\x19\x93\r\n\x1a\n";
28
29/// Reference integer written in the header to detect integer endianness/size
30/// mismatches.
31const LUAC_INT: i64 = 0x5678;
32
33/// Reference float written in the header to detect float format mismatches.
34const LUAC_NUM: f64 = 370.5;
35
36const LUAC_INT_55: i64 = -0x5678;
37
38const LUAC_INST_55: u32 = 0x12345678;
39
40const LUAC_NUM_55: f64 = -370.5;
41
42// LUA_VERSION_NUM = 504 → ((5 * 16) + 4) = 0x54 = 84
43/// One-byte version tag: upper nibble = major, lower nibble = minor.
44const LUAC_VERSION_51: u8 = 0x51;
45const LUAC_VERSION_52: u8 = 0x52;
46const LUAC_VERSION_53: u8 = 0x53;
47const LUAC_VERSION_54: u8 = 0x54;
48const LUAC_VERSION_55: u8 = 0x55;
49
50const LUAC_FORMAT: u8 = 0;
51
52const LUA_SIGNATURE: &[u8] = b"\x1bLua";
53
54const MAX_SHORT_LEN: usize = 40;
55
56// ── Constant-pool type tags (from lobject.h makevariant) ───────────────────
57//
58// These are the byte values written by ldump.c into the constants array.
59// makevariant(t, v) = t | (v << 4).
60//
61// The byte values used in the binary format are the raw tag integers from
62// lobject.h, distinct from LuaValue's variant tags. Defined here as u8
63// constants so the match in load_constants is self-documenting.
64
65const TAG_NIL: u8 = 0x00;
66const TAG_FALSE: u8 = 0x01;
67const TAG_TRUE: u8 = 0x11;
68const TAG_INT: u8 = 0x03;
69const TAG_FLOAT: u8 = 0x13;
70const TAG_SHORT_STR: u8 = 0x04;
71const TAG_LONG_STR: u8 = 0x14;
72
73// ── LoadState ──────────────────────────────────────────────────────────────
74
75/// Loader state bundled for convenience: Lua state, input stream, and the
76/// chunk name used in error messages.
77///
78/// Always stack-allocated inside [`undump`] and never escapes the call.
79struct LoadState<'a> {
80    state: &'a mut LuaState,
81    z: &'a mut ZIO,
82}
83
84// ── Error helper ───────────────────────────────────────────────────────────
85
86/// Build a syntax error for a malformed binary chunk.
87///
88/// Returns a `LuaError` for the caller to propagate with `?`, rather than
89/// throwing via `longjmp` as the C reference does.
90fn load_error(_s: &LoadState<'_>, why: &'static str) -> LuaError {
91    LuaError::syntax(format_args!("bad binary format ({})", why))
92}
93
94// ── Low-level I/O ──────────────────────────────────────────────────────────
95
96/// Read exactly `buf.len()` bytes from the stream into `buf`.
97///
98/// `ZIO::read` returns the number of bytes NOT read (0 = success).
99fn load_block(s: &mut LoadState<'_>, buf: &mut [u8]) -> Result<(), LuaError> {
100    if s.z.read(s.state, buf)? != 0 {
101        return Err(load_error(s, "truncated chunk"));
102    }
103    Ok(())
104}
105
106/// Read a single byte from the stream.
107fn load_byte(s: &mut LoadState<'_>) -> Result<u8, LuaError> {
108    let b = s.z.getc(s.state)?;
109    if b == crate::zio::EOZ {
110        return Err(load_error(s, "truncated chunk"));
111    }
112    Ok(b as u8)
113}
114
115/// Read a variable-length unsigned integer (7 bits per byte, big-endian,
116/// MSB-first continuation flag).
117///
118/// The encoding terminates when a byte with the high bit set is seen (the
119/// *last* byte has bit 7 = 1) — the opposite of the more common LEB128, where
120/// the continuation bit means "more follows".
121fn load_unsigned(s: &mut LoadState<'_>, limit: usize) -> Result<usize, LuaError> {
122    let mut x: usize = 0;
123    let limit = limit >> 7;
124    loop {
125        let b = load_byte(s)? as usize;
126        if x >= limit {
127            return Err(load_error(s, "integer overflow"));
128        }
129        x = (x << 7) | (b & 0x7f);
130        if (b & 0x80) != 0 {
131            break;
132        }
133    }
134    Ok(x)
135}
136
137/// Read a `size_t`-sized unsigned value.
138fn load_size(s: &mut LoadState<'_>) -> Result<usize, LuaError> {
139    load_unsigned(s, usize::MAX)
140}
141
142/// Read a signed `int`-sized value.
143fn load_int(s: &mut LoadState<'_>) -> Result<i32, LuaError> {
144    let v = load_unsigned(s, i32::MAX as usize)?;
145    Ok(v as i32)
146}
147
148/// Read a `lua_Number` (f64) as eight raw native-endian bytes.
149///
150/// The binary format is host-endian for these fields; the header check
151/// verifies endianness compatibility via the `LUAC_INT` and `LUAC_NUM`
152/// sentinels.
153fn load_number(s: &mut LoadState<'_>) -> Result<f64, LuaError> {
154    let mut buf = [0u8; 8];
155    load_block(s, &mut buf)?;
156    Ok(f64::from_ne_bytes(buf))
157}
158
159/// Read a `lua_Integer` (i64) as eight raw native-endian bytes. Same
160/// endianness reasoning as [`load_number`].
161fn load_integer(s: &mut LoadState<'_>) -> Result<i64, LuaError> {
162    let mut buf = [0u8; 8];
163    load_block(s, &mut buf)?;
164    Ok(i64::from_ne_bytes(buf))
165}
166
167fn load_raw_i32(s: &mut LoadState<'_>) -> Result<i32, LuaError> {
168    let mut buf = [0u8; 4];
169    load_block(s, &mut buf)?;
170    Ok(i32::from_ne_bytes(buf))
171}
172
173fn load_raw_u32(s: &mut LoadState<'_>) -> Result<u32, LuaError> {
174    let mut buf = [0u8; 4];
175    load_block(s, &mut buf)?;
176    Ok(u32::from_ne_bytes(buf))
177}
178
179// ── String loading ─────────────────────────────────────────────────────────
180
181/// Load a nullable string.  Returns `None` if the stored size is zero.
182///
183/// The Lua binary format stores `actual_length + 1` so that size=0 is the
184/// null-string sentinel. After reading `raw_size`, the actual byte count is
185/// `raw_size - 1`.
186///
187/// Long strings are interned through the same `intern_str` path as short
188/// strings; C creates long strings directly via `luaS_createlngstrobj`
189/// without interning them.
190///
191/// The `_proto` parameter corresponds to C's `Proto *p`, used there only for
192/// the `luaC_objbarrier(L, p, ts)` write barrier. That barrier is not invoked
193/// here.
194fn load_string_n(
195    s: &mut LoadState<'_>,
196    _proto: &LuaProto,
197) -> Result<Option<GcRef<LuaString>>, LuaError> {
198    let raw_size = load_size(s)?;
199    if raw_size == 0 {
200        return Ok(None);
201    }
202    let size = raw_size - 1;
203
204    // Read the raw bytes regardless of short/long distinction.
205    let mut buf = vec![0u8; size];
206
207    if size <= MAX_SHORT_LEN {
208        load_block(s, &mut buf)?;
209    } else {
210        load_block(s, &mut buf)?;
211    }
212
213    let ts = s.state.intern_str(&buf)?;
214
215    Ok(Some(ts))
216}
217
218/// Load a non-nullable string; error if the stream encodes a null string.
219fn load_string(s: &mut LoadState<'_>, proto: &LuaProto) -> Result<GcRef<LuaString>, LuaError> {
220    match load_string_n(s, proto)? {
221        Some(ts) => Ok(ts),
222        None => Err(load_error(s, "bad format for constant string")),
223    }
224}
225
226// ── Proto-field loaders ────────────────────────────────────────────────────
227
228/// Load the bytecode instruction array into a prototype.
229///
230/// Reads `n` raw 4-byte words in native-endian order, consistent with how
231/// [`load_number`] and [`load_integer`] work.
232fn load_code(s: &mut LoadState<'_>, f: &mut LuaProto) -> Result<(), LuaError> {
233    let n = load_int(s)? as usize;
234    let mut code = Vec::with_capacity(n);
235    for _ in 0..n {
236        let mut buf = [0u8; 4];
237        load_block(s, &mut buf)?;
238        code.push(Instruction(u32::from_ne_bytes(buf)));
239    }
240    f.code = code;
241    Ok(())
242}
243
244/// Load the constant pool into a prototype.
245///
246/// Reads the tag byte for each constant, then its payload if any.
247fn load_constants(s: &mut LoadState<'_>, f: &mut LuaProto) -> Result<(), LuaError> {
248    let n = load_int(s)? as usize;
249    let mut k = Vec::with_capacity(n);
250
251    for _ in 0..n {
252        let t = load_byte(s)?;
253        let val = match t {
254            TAG_NIL => LuaValue::Nil,
255            TAG_FALSE => LuaValue::Bool(false),
256            TAG_TRUE => LuaValue::Bool(true),
257            TAG_FLOAT => LuaValue::Float(load_number(s)?),
258            TAG_INT => LuaValue::Int(load_integer(s)?),
259
260            TAG_SHORT_STR | TAG_LONG_STR => {
261                let ts = load_string(s, f)?;
262                LuaValue::Str(ts)
263            }
264
265            _ => {
266                debug_assert!(false, "unknown constant type tag {:#04x}", t);
267                LuaValue::Nil
268            }
269        };
270        k.push(val);
271    }
272
273    f.k = k;
274    Ok(())
275}
276
277/// Load nested function prototypes into a prototype.
278///
279/// C creates the proto first, as a GC anchor, then fills it. Here a default
280/// `LuaProto` is built, filled, then wrapped in a `GcRef`.
281fn load_protos(s: &mut LoadState<'_>, f: &mut LuaProto) -> Result<(), LuaError> {
282    let n = load_int(s)? as usize;
283    let mut protos = Vec::with_capacity(n);
284
285    for _ in 0..n {
286        let mut sub = LuaProto::placeholder();
287
288        // Pass parent source as fallback.
289        let parent_source = f.source.clone();
290        load_function(s, &mut sub, parent_source)?;
291
292        // Wrap in GcRef after loading.
293        let sub_ref = GcRef::new(sub);
294        sub_ref.account_buffer(sub_ref.buffer_bytes() as isize);
295        protos.push(sub_ref);
296    }
297
298    f.p = protos;
299    Ok(())
300}
301
302/// Load upvalue descriptors into a prototype.
303///
304/// C fills upvalue names first (`NULL`) for GC safety, then names are
305/// attached separately. Here `UpvalDesc` values are built with `name: None`
306/// and filled in later by [`load_debug`], which is why `UpvalDesc.name` is
307/// `Option<GcRef<LuaString>>` rather than a bare `GcRef<LuaString>`.
308fn load_upvalues(s: &mut LoadState<'_>, f: &mut LuaProto) -> Result<(), LuaError> {
309    let n = load_int(s)? as usize;
310
311    let mut upvalues = Vec::with_capacity(n);
312    for _ in 0..n {
313        let instack_raw = load_byte(s)?;
314        let idx = load_byte(s)?;
315        let kind = load_byte(s)?;
316
317        upvalues.push(UpvalDesc {
318            name: None, // filled by load_debug
319            instack: instack_raw != 0,
320            idx,
321            kind,
322        });
323    }
324
325    f.upvalues = upvalues;
326    Ok(())
327}
328
329/// Load debug information into a prototype.
330///
331/// `lineinfo` is `ls_byte` (a signed byte) in C; each byte is read as `u8`
332/// then cast to `i8`, which is safe since the two share the same in-memory
333/// representation. `LocalVar.varname` and `UpvalDesc.name` are both
334/// `Option<GcRef<LuaString>>` here because `loadStringN` can return `None`;
335/// see also the note on [`load_upvalues`].
336fn load_debug(s: &mut LoadState<'_>, f: &mut LuaProto) -> Result<(), LuaError> {
337    let n = load_int(s)? as usize;
338    let mut lineinfo = vec![0i8; n];
339    for item in lineinfo.iter_mut() {
340        *item = load_byte(s)? as i8;
341    }
342    f.lineinfo = lineinfo;
343
344    let n = load_int(s)? as usize;
345    let mut abslineinfo = Vec::with_capacity(n);
346    for _ in 0..n {
347        abslineinfo.push(AbsLineInfo {
348            pc: load_int(s)?,
349            line: load_int(s)?,
350        });
351    }
352    f.abslineinfo = abslineinfo;
353
354    let n = load_int(s)? as usize;
355
356    let mut locvars = Vec::with_capacity(n);
357    for _ in 0..n {
358        let varname = load_string_n(s, f)?;
359        let startpc = load_int(s)?;
360        let endpc = load_int(s)?;
361        let varname = match varname {
362            Some(v) => v,
363            None => s.state.new_string(b"")?,
364        };
365        locvars.push(LocalVar {
366            varname,
367            startpc,
368            endpc,
369        });
370    }
371    f.locvars = locvars;
372
373    // If n == 0 there is no upvalue name info (stripped).
374    let has_names = load_int(s)?;
375    if has_names != 0 {
376        let n_upvals = f.upvalues.len();
377        for i in 0..n_upvals {
378            let name = load_string_n(s, f)?;
379            f.upvalues[i].name = name;
380        }
381    }
382
383    Ok(())
384}
385
386// ── Function loader ────────────────────────────────────────────────────────
387
388/// Load a complete function prototype from the stream.
389///
390/// `psource` is `None` at the top level; a nested prototype with no source of
391/// its own inherits the parent's, expressed here by falling back to
392/// `psource` when `loadStringN` returns `None`.
393fn load_function(
394    s: &mut LoadState<'_>,
395    f: &mut LuaProto,
396    psource: Option<GcRef<LuaString>>,
397) -> Result<(), LuaError> {
398    let source = load_string_n(s, f)?;
399    f.source = source.or(psource);
400
401    f.linedefined = load_int(s)?;
402    f.lastlinedefined = load_int(s)?;
403    f.numparams = load_byte(s)?;
404    f.is_vararg = load_byte(s)? != 0;
405    f.maxstacksize = load_byte(s)?;
406    load_code(s, f)?;
407    reconstruct_vararg_table_reg(f);
408    load_constants(s, f)?;
409    load_upvalues(s, f)?;
410    load_protos(s, f)?;
411    load_debug(s, f)?;
412
413    Ok(())
414}
415
416/// Recover `LuaProto.vararg_table_reg` from the loaded bytecode instead of from
417/// the wire format, so a precompiled chunk keeps Lua 5.5 named-vararg aliasing
418/// (`function f(...t)`) without lua-rs's `string.dump` output diverging from
419/// C's bytecode layout (which the structural oracle compares).
420///
421/// A named-vararg function emits exactly one `OP_VARARGPACK` (opcode 84) at
422/// entry; its A operand is the register holding the shared vararg table. Its
423/// k bit records whether the table must be materialized.
424fn reconstruct_vararg_table_reg(f: &mut LuaProto) {
425    const OP_VARARGPACK: u32 = 84;
426    const OPCODE_MASK: u32 = 0x7F;
427    const POS_K: u32 = 15;
428    if let Some((reg, needed)) = f.code.iter().find_map(|inst| {
429        let raw = inst.raw();
430        (raw & OPCODE_MASK == OP_VARARGPACK).then(|| {
431            let reg = ((raw >> 7) & 0xFF) as u8;
432            let needed = ((raw >> POS_K) & 1) != 0;
433            (reg, needed)
434        })
435    }) {
436        f.vararg_table_reg = Some(reg);
437        f.vararg_table_needed = needed;
438    }
439}
440
441// ── Header validation ──────────────────────────────────────────────────────
442
443/// Verify that the next `expected.len()` bytes in the stream match `expected`.
444fn check_literal(
445    s: &mut LoadState<'_>,
446    expected: &[u8],
447    msg: &'static str,
448) -> Result<(), LuaError> {
449    let mut buf = vec![0u8; expected.len()];
450    load_block(s, &mut buf)?;
451    if buf != expected {
452        return Err(load_error(s, msg));
453    }
454    Ok(())
455}
456
457/// Verify that the next byte in the stream equals `expected_size`. `tname` is
458/// always a Rust type-name string literal (ASCII) from the call sites.
459fn fcheck_size(
460    s: &mut LoadState<'_>,
461    expected_size: usize,
462    tname: &'static str,
463) -> Result<(), LuaError> {
464    let b = load_byte(s)? as usize;
465    if b != expected_size {
466        return Err(LuaError::syntax(format_args!("{} size mismatch", tname)));
467    }
468    Ok(())
469}
470
471/// Validate the binary chunk header.
472///
473/// The three fixed-size checks below cover `Instruction` (4 bytes, u32),
474/// `lua_Integer` (8 bytes, i64), and `lua_Number` (8 bytes, f64).
475///
476/// The first byte of `LUA_SIGNATURE` (`\x1b`) is already consumed by the
477/// caller before `check_header` is invoked, so only bytes 1.. of the
478/// signature (`"Lua"`) are checked here.
479fn check_header(s: &mut LoadState<'_>) -> Result<(), LuaError> {
480    // Skip LUA_SIGNATURE[0] (\x1b) — already consumed by the caller.
481    check_literal(s, &LUA_SIGNATURE[1..], "not a binary chunk")?;
482
483    let version = s.state.global().lua_version;
484    let expected_version = match version {
485        LuaVersion::V51 => LUAC_VERSION_51,
486        LuaVersion::V52 => LUAC_VERSION_52,
487        LuaVersion::V53 => LUAC_VERSION_53,
488        LuaVersion::V55 => LUAC_VERSION_55,
489        _ => LUAC_VERSION_54,
490    };
491    let ver = load_byte(s)?;
492    if ver != expected_version {
493        return Err(load_error(s, "version mismatch"));
494    }
495
496    let fmt = load_byte(s)?;
497    if fmt != LUAC_FORMAT {
498        return Err(load_error(s, "format mismatch"));
499    }
500
501    match version {
502        LuaVersion::V51 => {
503            check_legacy_sizes(s)?;
504        }
505        LuaVersion::V52 => {
506            check_legacy_sizes(s)?;
507            check_literal(s, LUAC_DATA, "corrupted chunk")?;
508        }
509        LuaVersion::V53 => {
510            check_literal(s, LUAC_DATA, "corrupted chunk")?;
511            fcheck_size(s, size_of::<i32>(), "int")?;
512            fcheck_size(s, size_of::<usize>(), "size_t")?;
513            fcheck_size(s, 4, "Instruction")?;
514            fcheck_size(s, 8, "lua_Integer")?;
515            fcheck_size(s, 8, "lua_Number")?;
516            if load_integer(s)? != LUAC_INT {
517                return Err(load_error(s, "integer format mismatch"));
518            }
519            if load_number(s)? != LUAC_NUM {
520                return Err(load_error(s, "float format mismatch"));
521            }
522        }
523        LuaVersion::V55 => {
524            check_literal(s, LUAC_DATA, "corrupted chunk")?;
525            fcheck_size(s, 4, "int")?;
526            if load_raw_i32(s)? != LUAC_INT_55 as i32 {
527                return Err(load_error(s, "int format mismatch"));
528            }
529
530            fcheck_size(s, 4, "instruction")?;
531            if load_raw_u32(s)? != LUAC_INST_55 {
532                return Err(load_error(s, "instruction format mismatch"));
533            }
534
535            fcheck_size(s, 8, "Lua integer")?;
536            if load_integer(s)? != LUAC_INT_55 {
537                return Err(load_error(s, "Lua integer format mismatch"));
538            }
539
540            fcheck_size(s, 8, "Lua number")?;
541            if load_number(s)? != LUAC_NUM_55 {
542                return Err(load_error(s, "Lua number format mismatch"));
543            }
544        }
545        _ => {
546            check_literal(s, LUAC_DATA, "corrupted chunk")?;
547            fcheck_size(s, 4, "Instruction")?;
548
549            fcheck_size(s, 8, "lua_Integer")?;
550
551            fcheck_size(s, 8, "lua_Number")?;
552
553            let int_check = load_integer(s)?;
554            if int_check != LUAC_INT {
555                return Err(load_error(s, "integer format mismatch"));
556            }
557
558            let num_check = load_number(s)?;
559            if num_check != LUAC_NUM {
560                return Err(load_error(s, "float format mismatch"));
561            }
562        }
563    }
564
565    Ok(())
566}
567
568/// Validate the 5.1/5.2 endianness + size + integral-flag block: endian = 1
569/// (little), `sizeof(int)` = 4, `sizeof(size_t)`, `sizeof(Instruction)` = 4,
570/// `sizeof(lua_Number)` = 8, integral = 0. These versions have no integer
571/// subtype, so there is no `lua_Integer` size byte and no `LUAC_INT`/`LUAC_NUM`
572/// sentinel.
573fn check_legacy_sizes(s: &mut LoadState<'_>) -> Result<(), LuaError> {
574    if load_byte(s)? != 1 {
575        return Err(load_error(s, "endianness mismatch"));
576    }
577    fcheck_size(s, size_of::<i32>(), "int")?;
578    fcheck_size(s, size_of::<usize>(), "size_t")?;
579    fcheck_size(s, 4, "Instruction")?;
580    fcheck_size(s, 8, "lua_Number")?;
581    if load_byte(s)? != 0 {
582        return Err(load_error(s, "number format mismatch"));
583    }
584    Ok(())
585}
586
587// ── Public entry point ─────────────────────────────────────────────────────
588
589/// Load a precompiled Lua chunk and return the top-level Lua closure.
590///
591/// This is the Rust equivalent of `luaU_undump` — the single public function
592/// exported by `lundump.c`.
593///
594/// # Parameters
595/// - `state` — the Lua thread state.
596/// - `z` — input stream positioned at the start of the binary chunk
597///   (the first byte `\x1b` of `LUA_SIGNATURE` must still be present).
598/// - `name` — chunk name for error messages.  Stripped per Lua convention:
599///   - `@…` → filename (strip `@`)
600///   - `=…` → literal name (strip `=`)
601///   - starts with `\x1b` → `"binary string"`
602///   - otherwise used as-is.
603///
604/// The closure is pushed onto the stack for GC anchoring before its proto is
605/// fully loaded, mirroring the C reference's discipline of anchoring the
606/// half-built closure while parsing continues; the caller is responsible for
607/// popping it when done. `luai_verifycode`, a no-op in the default C build,
608/// has no equivalent call here.
609pub(crate) fn undump(
610    state: &mut LuaState,
611    z: &mut ZIO,
612    _name: &[u8],
613) -> Result<GcRef<LuaLClosure>, LuaError> {
614    let mut s = LoadState { state, z };
615
616    check_header(&mut s)?;
617
618    // Reads the number of upvalues for the top-level closure.
619    let nupvalues = load_byte(&mut s)?;
620    let mut cl = LuaLClosure::placeholder();
621    let mut upvals_vec = Vec::with_capacity(nupvalues as usize);
622    for _ in 0..nupvalues as usize {
623        upvals_vec.push(std::cell::Cell::new(
624            s.state.new_upval_closed(LuaValue::Nil),
625        ));
626    }
627    cl.upvals = upvals_vec.into_boxed_slice();
628
629    // Push a placeholder Nil first; the real closure value is set after the
630    // proto is loaded.
631    s.state.push(LuaValue::Nil); // placeholder; replaced below
632
633    let mut proto = LuaProto::placeholder();
634
635    load_function(&mut s, &mut proto, None)?;
636
637    // Wrap the proto in a GcRef and attach it to the closure.
638    let proto_ref = GcRef::new(proto);
639    proto_ref.account_buffer(proto_ref.buffer_bytes() as isize);
640
641    debug_assert_eq!(
642        nupvalues as usize,
643        proto_ref.upvalues.len(),
644        "upvalue count mismatch between closure header and prototype"
645    );
646
647    // Attach the loaded proto to the closure.
648    cl.proto = proto_ref;
649
650    // Wrap the closure in GcRef.
651    let cl_ref = GcRef::new(cl);
652    cl_ref.account_buffer(cl_ref.buffer_bytes() as isize);
653
654    Ok(cl_ref)
655}