Skip to main content

rune/
dump.rs

1//! Heap dump module for rune.
2//! After a successful bootstrap in which elisp code is converted to bytecode,
3//! the VM heap memory can be serialized to a rune.pdmp file, and
4//! subsequent runs can avoid bootstrapping and just load rune.pdmp instead.
5//!
6//! The idea is based on emacs' portable dumper (pdump) which replaced the older unexec.
7//! However, this pdmp format is a somewhat human-readable text format and makes no attempt to
8//! have any compatibility with unexec or pdump.
9//! <https://lwn.net/Articles/707619/>
10//! <https://github.com/emacs-mirror/emacs/blob/master/src/pdumper.h>
11use crate::core::{
12    env::{Env, INTERNED_SYMBOLS},
13    gc::{Context, Rt},
14    object::{NIL, Object, ObjectType},
15};
16use std::collections::HashMap;
17use std::fmt::{self, Display, Write};
18use std::path::Path;
19
20/// Unique id for each object pointer, used to handle cycles and sharing.
21type ObjId = u32;
22
23/// A serialized representation of a single GC object, decoupled from live
24/// pointers. Children are referenced by ObjId rather than raw addresses.
25enum DumpedObject {
26    Nil,
27    Int(i64),
28    Float(f64),
29    String(std::string::String),
30    ByteString(Vec<u8>),
31    Symbol {
32        name: std::string::String,
33        interned: bool,
34    },
35    Cons {
36        car: ObjId,
37        cdr: ObjId,
38    },
39    Vec(Vec<ObjId>),
40    ByteFn {
41        args: u64,
42        depth: usize,
43        codes: Vec<u8>,
44        consts: Vec<ObjId>,
45    },
46    /// SubrFn contains a Rust fn pointer which can't be serialized.
47    /// We store only the name during serialization; the loader will
48    /// recover the function pointer by name lookup.
49    Subr(std::string::String),
50    Record(Vec<ObjId>),
51    HashTable(Vec<(ObjId, ObjId)>),
52    Buffer,
53    CharTable,
54    BigInt(std::string::String),
55    ChannelSender,
56    ChannelReceiver,
57}
58
59/// An entry in the symbol table section: name -> (symbol object id, optional function object id)
60struct DumpedSymbol {
61    name: std::string::String,
62    sym_id: ObjId,
63    func_id: Option<ObjId>,
64}
65
66/// An entry in the env section: a variable binding from symbol to value.
67struct DumpedBinding {
68    sym_id: ObjId,
69    val_id: ObjId,
70}
71
72struct DumpState {
73    /// Maps live pointer address -> assigned object id (handles cycles + dedup)
74    seen: HashMap<usize, ObjId>,
75    objects: Vec<DumpedObject>,
76    symbols: Vec<DumpedSymbol>,
77    env: Vec<DumpedBinding>,
78}
79
80impl Display for DumpedObject {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        match self {
83            Self::Nil => write!(f, "nil"),
84            Self::Int(x) => write!(f, "int {x}"),
85            Self::Float(x) => write!(f, "float {x}"),
86            Self::String(s) => write!(f, "string \"{}\"", escape_str(s)),
87            Self::ByteString(bytes) => {
88                write!(f, "bytestring #")?;
89                for b in bytes {
90                    write!(f, "{b:02x}")?;
91                }
92                Ok(())
93            }
94            Self::Symbol { name, interned } => {
95                write!(f, "symbol \"{}\" interned={interned}", escape_str(name))
96            }
97            Self::Cons { car, cdr } => write!(f, "cons car=@{car} cdr=@{cdr}"),
98            Self::Vec(elems) => {
99                write!(f, "vec [")?;
100                fmt_id_list(f, elems)?;
101                write!(f, "]")
102            }
103            Self::ByteFn { args, depth, codes, consts } => {
104                write!(f, "bytefn args={args} depth={depth} codes=#")?;
105                for b in codes {
106                    write!(f, "{b:02x}")?;
107                }
108                write!(f, " consts=[")?;
109                fmt_id_list(f, consts)?;
110                write!(f, "]")
111            }
112            Self::Subr(name) => write!(f, "subr \"{}\"", escape_str(name)),
113            Self::Record(elems) => {
114                write!(f, "record [")?;
115                fmt_id_list(f, elems)?;
116                write!(f, "]")
117            }
118            Self::HashTable(entries) => {
119                write!(f, "hashtable [")?;
120                for (i, (k, v)) in entries.iter().enumerate() {
121                    if i > 0 {
122                        write!(f, " ")?;
123                    }
124                    write!(f, "{k}:{v}")?;
125                }
126                write!(f, "]")
127            }
128            Self::Buffer => write!(f, "buffer <opaque>"),
129            Self::CharTable => write!(f, "chartable <opaque>"),
130            Self::BigInt(s) => write!(f, "bigint {s}"),
131            Self::ChannelSender => write!(f, "channel-sender <opaque>"),
132            Self::ChannelReceiver => write!(f, "channel-receiver <opaque>"),
133        }
134    }
135}
136
137fn fmt_id_list(f: &mut fmt::Formatter<'_>, ids: &[ObjId]) -> fmt::Result {
138    for (i, id) in ids.iter().enumerate() {
139        if i > 0 {
140            write!(f, " ")?;
141        }
142        write!(f, "@{id}")?;
143    }
144    Ok(())
145}
146
147impl DumpState {
148    fn new() -> Self {
149        Self { seen: HashMap::new(), objects: Vec::new(), symbols: Vec::new(), env: Vec::new() }
150    }
151
152    /// Serialize an object and return its id. If already visited (cycle or
153    /// shared reference), returns the cached id immediately.
154    fn dump_object(&mut self, obj: Object) -> ObjId {
155        let addr = obj.into_raw().addr();
156
157        // nil is special: always id 0
158        if obj.ptr_eq(NIL) {
159            if !self.seen.contains_key(&addr) {
160                let id = self.alloc_id(DumpedObject::Nil);
161                self.seen.insert(addr, id);
162            }
163            return self.seen[&addr];
164        }
165
166        // If already visited -> return cached id (handles cycles + sharing)
167        if let Some(&id) = self.seen.get(&addr) {
168            return id;
169        }
170
171        // Pre-insert before recursing into children to break cycles.
172        // If a child points back to this object, dump_object will find
173        // it in `seen` and return the id without infinite recursion.
174        let id = self.alloc_id(DumpedObject::Nil);
175        self.seen.insert(addr, id);
176
177        let dumped = match obj.untag() {
178            ObjectType::Int(x) => DumpedObject::Int(x),
179            ObjectType::Float(x) => DumpedObject::Float(**x),
180            ObjectType::String(s) => DumpedObject::String(s.inner().to_owned()),
181            ObjectType::ByteString(s) => DumpedObject::ByteString(s.to_vec()),
182            ObjectType::Symbol(sym) => {
183                DumpedObject::Symbol { name: sym.name().to_owned(), interned: sym.interned() }
184            }
185            ObjectType::Cons(cons) => {
186                let car = self.dump_object(cons.car());
187                let cdr = self.dump_object(cons.cdr());
188                DumpedObject::Cons { car, cdr }
189            }
190            ObjectType::Vec(vec) => {
191                let elems = vec.iter().map(|cell| self.dump_object(cell.get())).collect();
192                DumpedObject::Vec(elems)
193            }
194            ObjectType::ByteFn(bf) => {
195                // Bytecode is trivially serializable; the constants
196                // vector contains arbitrary Objects so we recurse into each.
197                let consts = bf.consts().iter().map(|c| self.dump_object(*c)).collect();
198                DumpedObject::ByteFn {
199                    args: bf.args.into_arg_spec(),
200                    depth: bf.depth,
201                    codes: bf.codes().to_vec(),
202                    consts,
203                }
204            }
205            ObjectType::SubrFn(subr) => DumpedObject::Subr(subr.name.to_owned()),
206            ObjectType::HashTable(ht) => {
207                let entries: Vec<(ObjId, ObjId)> = (0..ht.len())
208                    .filter_map(|i| ht.get_index(i))
209                    .map(|(k, v)| (self.dump_object(k), self.dump_object(v)))
210                    .collect();
211                DumpedObject::HashTable(entries)
212            }
213            ObjectType::Record(rec) => {
214                let elems = rec.iter().map(|cell| self.dump_object(cell.get())).collect();
215                DumpedObject::Record(elems)
216            }
217            ObjectType::Buffer(_) => DumpedObject::Buffer,
218            ObjectType::CharTable(_) => DumpedObject::CharTable,
219            ObjectType::BigInt(n) => DumpedObject::BigInt(n.to_string()),
220            ObjectType::ChannelSender(_) => DumpedObject::ChannelSender,
221            ObjectType::ChannelReceiver(_) => DumpedObject::ChannelReceiver,
222        };
223
224        // Overwrite the Nil placeholder with the real object
225        self.objects[id as usize] = dumped;
226        id
227    }
228
229    fn alloc_id(&mut self, placeholder: DumpedObject) -> ObjId {
230        let id = self.objects.len() as ObjId;
231        self.objects.push(placeholder);
232        id
233    }
234
235    /// Walk the global symbol table and serialize each symbol + its function cell.
236    fn dump_symbols(&mut self, cx: &Context) {
237        let map = INTERNED_SYMBOLS.lock().unwrap();
238        for (name, sym) in map.iter() {
239            let sym_obj: Object = sym.into();
240            let sym_id = self.dump_object(sym_obj);
241            let func_id = if sym.has_func() {
242                let func = sym.func(cx).unwrap();
243                Some(self.dump_object(func.as_obj()))
244            } else {
245                None
246            };
247            self.symbols.push(DumpedSymbol { name: name.to_owned(), sym_id, func_id });
248        }
249    }
250
251    /// Serialize the Env variable bindings (symbol -> value mappings).
252    fn dump_env(&mut self, env: &Rt<Env>, _cx: &Context) {
253        for (sym_slot, val_slot) in env.vars.iter() {
254            // Slot<Symbol> derefs to Symbol, Slot<Object> derefs to Object.
255            // Double-deref: &Slot<T> -> &T -> T (Copy).
256            let sym_obj: Object = (**sym_slot).into();
257            let val: Object = **val_slot;
258            let sym_id = self.dump_object(sym_obj);
259            let val_id = self.dump_object(val);
260            self.env.push(DumpedBinding { sym_id, val_id });
261        }
262    }
263
264    fn into_output(self) -> std::string::String {
265        let mut out = std::string::String::new();
266        writeln!(out, ".HEADER").unwrap();
267        writeln!(out, "  version 1").unwrap();
268        writeln!(out, "  objects {}", self.objects.len()).unwrap();
269        writeln!(out).unwrap();
270
271        writeln!(out, ".OBJECTS").unwrap();
272        for (id, obj) in self.objects.iter().enumerate() {
273            writeln!(out, "  @{id} {obj}").unwrap();
274        }
275        writeln!(out).unwrap();
276
277        writeln!(out, ".SYMBOLS").unwrap();
278        for sym in &self.symbols {
279            let escaped = escape_str(&sym.name);
280            match sym.func_id {
281                Some(fid) => writeln!(out, "  \"{escaped}\" -> @{} func=@{fid}", sym.sym_id),
282                None => writeln!(out, "  \"{escaped}\" -> @{}", sym.sym_id),
283            }
284            .unwrap();
285        }
286        writeln!(out).unwrap();
287
288        writeln!(out, ".ENV").unwrap();
289        for b in &self.env {
290            writeln!(out, "  @{} = @{}", b.sym_id, b.val_id).unwrap();
291        }
292        out
293    }
294}
295
296fn escape_str(s: &str) -> std::string::String {
297    let mut out = std::string::String::with_capacity(s.len());
298    for c in s.chars() {
299        match c {
300            '"' => out.push_str("\\\""),
301            '\\' => out.push_str("\\\\"),
302            '\n' => out.push_str("\\n"),
303            '\r' => out.push_str("\\r"),
304            '\t' => out.push_str("\\t"),
305            c => out.push(c),
306        }
307    }
308    out
309}
310
311// ── Serialization ───────────────────────────────────────────────────────────
312pub(crate) fn dump_to_file(path: &Path, env: &Rt<Env>, cx: &Context) -> Result<(), std::io::Error> {
313    let mut state = DumpState::new();
314    state.dump_symbols(cx);
315    state.dump_env(env, cx);
316    let output = state.into_output();
317    std::fs::write(path, output)
318}
319
320/// Registry mapping SubrFn names to their static references.
321/// Used by the loader to reconstruct function pointers from dump files.
322/// We serialize the name since Rust fn pointer names can't be serialized,
323/// and look it up here on load.
324fn subr_registry() -> HashMap<&'static str, &'static crate::core::object::SubrFn> {
325    use crate::core::env::sym;
326    let mut map = HashMap::new();
327    for subr in sym::SUBR_DEFS.iter() {
328        map.insert(subr.name, *subr);
329    }
330    map
331}
332
333// ── Deserialization ─────────────────────────────────────────────────────────
334
335/// Deserialized contents of a .pdmp file.
336struct DumpFile {
337    objects: Vec<DumpedObject>,
338    symbols: Vec<DumpedSymbol>,
339    env: Vec<DumpedBinding>,
340}
341
342fn parse_dump(input: &str) -> Result<DumpFile, String> {
343    let mut objects: Vec<Option<DumpedObject>> = Vec::new();
344    let mut symbols = Vec::new();
345    let mut env = Vec::new();
346    let mut section = "";
347
348    for line in input.lines() {
349        let line = line.trim();
350        if line.is_empty() {
351            continue;
352        }
353        if line.starts_with('.') {
354            section = match line {
355                ".HEADER" => "header",
356                ".OBJECTS" => "objects",
357                ".SYMBOLS" => "symbols",
358                ".ENV" => "env",
359                _ => return Err(format!("unknown section: {line}")),
360            };
361            continue;
362        }
363        match section {
364            "header" => {} // skip version/count lines
365            "objects" => {
366                let (id, obj) = parse_object_line(line)?;
367                // Grow vec to fit, filling gaps with None
368                if id as usize >= objects.len() {
369                    objects.resize_with(id as usize + 1, || None);
370                }
371                objects[id as usize] = Some(obj);
372            }
373            "symbols" => symbols.push(parse_symbol_line(line)?),
374            "env" => env.push(parse_binding_line(line)?),
375            _ => {}
376        }
377    }
378
379    let objects = objects
380        .into_iter()
381        .enumerate()
382        .map(|(i, o)| o.ok_or_else(|| format!("missing object @{i}")))
383        .collect::<Result<Vec<_>, _>>()?;
384
385    Ok(DumpFile { objects, symbols, env })
386}
387
388/// Parse `@{id} {type} {fields...}` into (ObjId, DumpedObject).
389fn parse_object_line(line: &str) -> Result<(ObjId, DumpedObject), String> {
390    let line = line.strip_prefix('@').ok_or("expected @")?;
391    let (id_str, rest) = line.split_once(' ').ok_or("expected space after id")?;
392    let id: ObjId = id_str.parse().map_err(|e| format!("bad id: {e}"))?;
393
394    let obj = if rest == "nil" {
395        DumpedObject::Nil
396    } else if let Some(val) = rest.strip_prefix("int ") {
397        DumpedObject::Int(val.parse().map_err(|e| format!("bad int: {e}"))?)
398    } else if let Some(val) = rest.strip_prefix("float ") {
399        DumpedObject::Float(val.parse().map_err(|e| format!("bad float: {e}"))?)
400    } else if let Some(val) = rest.strip_prefix("string ") {
401        DumpedObject::String(parse_quoted_string(val)?)
402    } else if let Some(val) = rest.strip_prefix("bytestring #") {
403        DumpedObject::ByteString(parse_hex_bytes(val)?)
404    } else if let Some(val) = rest.strip_prefix("symbol ") {
405        let (name, rest) = parse_quoted_string_rest(val)?;
406        let interned = rest
407            .strip_prefix(" interned=")
408            .ok_or("expected interned=")?
409            .parse()
410            .map_err(|e| format!("bad bool: {e}"))?;
411        DumpedObject::Symbol { name, interned }
412    } else if let Some(val) = rest.strip_prefix("cons ") {
413        let car = parse_field_ref(val, "car=@")?;
414        let cdr_start = val.find("cdr=@").ok_or("expected cdr=")?;
415        let cdr = parse_ref(&val[cdr_start + "cdr=".len()..])?;
416        DumpedObject::Cons { car, cdr }
417    } else if let Some(val) = rest.strip_prefix("vec [") {
418        let inner = val.strip_suffix(']').ok_or("expected ]")?;
419        DumpedObject::Vec(parse_ref_list(inner)?)
420    } else if let Some(val) = rest.strip_prefix("bytefn ") {
421        parse_bytefn(val)?
422    } else if let Some(val) = rest.strip_prefix("subr ") {
423        DumpedObject::Subr(parse_quoted_string(val)?)
424    } else if let Some(val) = rest.strip_prefix("record [") {
425        let inner = val.strip_suffix(']').ok_or("expected ]")?;
426        DumpedObject::Record(parse_ref_list(inner)?)
427    } else if let Some(val) = rest.strip_prefix("bigint ") {
428        DumpedObject::BigInt(val.to_owned())
429    } else if let Some(val) = rest.strip_prefix("hashtable [") {
430        let inner = val.strip_suffix(']').ok_or("expected ]")?;
431        let entries = if inner.trim().is_empty() {
432            Vec::new()
433        } else {
434            inner
435                .split_whitespace()
436                .map(|pair| {
437                    let (k, v) = pair.split_once(':').ok_or("expected k:v in hashtable")?;
438                    Ok((
439                        k.trim_start_matches('@').parse::<ObjId>().map_err(|e| e.to_string())?,
440                        v.trim_start_matches('@').parse::<ObjId>().map_err(|e| e.to_string())?,
441                    ))
442                })
443                .collect::<Result<Vec<_>, String>>()?
444        };
445        DumpedObject::HashTable(entries)
446    } else if rest.starts_with("buffer") {
447        DumpedObject::Buffer
448    } else if rest.starts_with("chartable") {
449        DumpedObject::CharTable
450    } else if rest.starts_with("channel-sender") {
451        DumpedObject::ChannelSender
452    } else if rest.starts_with("channel-receiver") {
453        DumpedObject::ChannelReceiver
454    } else {
455        return Err(format!("unknown object type: {rest}"));
456    };
457
458    Ok((id, obj))
459}
460
461fn parse_bytefn(val: &str) -> Result<DumpedObject, String> {
462    // args={n} depth={n} codes=#{hex} consts=[...]
463    let args_str = extract_field(val, "args=", ' ')?;
464    let args: u64 = args_str.parse().map_err(|e| format!("bad args: {e}"))?;
465    let depth_str = extract_field(val, "depth=", ' ')?;
466    let depth: usize = depth_str.parse().map_err(|e| format!("bad depth: {e}"))?;
467    let codes_start = val.find("codes=#").ok_or("expected codes=#")? + 7;
468    let codes_end = val[codes_start..].find(' ').map_or(val.len(), |i| codes_start + i);
469    let codes = parse_hex_bytes(&val[codes_start..codes_end])?;
470    let consts_start = val.find("consts=[").ok_or("expected consts=[")? + 8;
471    let consts_end = val[consts_start..].find(']').ok_or("expected ]")? + consts_start;
472    let consts = parse_ref_list(&val[consts_start..consts_end])?;
473    Ok(DumpedObject::ByteFn { args, depth, codes, consts })
474}
475
476/// Parse `"escaped string"` and return the unescaped content.
477fn parse_quoted_string(s: &str) -> Result<std::string::String, String> {
478    parse_quoted_string_rest(s).map(|(s, _)| s)
479}
480
481/// Parse `"escaped string" rest...` returning (unescaped, rest).
482fn parse_quoted_string_rest(s: &str) -> Result<(std::string::String, &str), String> {
483    let s = s.strip_prefix('"').ok_or("expected opening quote")?;
484    let mut out = std::string::String::new();
485    let mut chars = s.char_indices();
486    while let Some((i, c)) = chars.next() {
487        match c {
488            '"' => return Ok((out, &s[i + 1..])),
489            '\\' => match chars.next() {
490                Some((_, 'n')) => out.push('\n'),
491                Some((_, 'r')) => out.push('\r'),
492                Some((_, 't')) => out.push('\t'),
493                Some((_, '\\')) => out.push('\\'),
494                Some((_, '"')) => out.push('"'),
495                Some((_, c)) => {
496                    out.push('\\');
497                    out.push(c);
498                }
499                None => return Err("unexpected end of string".into()),
500            },
501            c => out.push(c),
502        }
503    }
504    Err("unterminated string".into())
505}
506
507fn parse_hex_bytes(s: &str) -> Result<Vec<u8>, String> {
508    (0..s.len())
509        .step_by(2)
510        .map(|i| {
511            u8::from_str_radix(s.get(i..i + 2).ok_or("odd hex length")?, 16)
512                .map_err(|e| format!("bad hex: {e}"))
513        })
514        .collect()
515}
516
517/// Parse `@{id}` returning the id.
518fn parse_ref(s: &str) -> Result<ObjId, String> {
519    let s = s.trim();
520    let digits = s.strip_prefix('@').ok_or_else(|| format!("expected @, got: {s}"))?;
521    // Take only digits (stop at space or end)
522    let end = digits.find(|c: char| !c.is_ascii_digit()).unwrap_or(digits.len());
523    digits[..end].parse().map_err(|e| format!("bad ref: {e}"))
524}
525
526/// Parse `prefix{id}` from the start of s.
527fn parse_field_ref(s: &str, prefix: &str) -> Result<ObjId, String> {
528    let start = s.find(prefix).ok_or_else(|| format!("expected {prefix}"))?;
529    parse_ref(&s[start + prefix.len() - 1..]) // -1 to keep the @
530}
531
532/// Parse space-separated `@id @id ...` list.
533fn parse_ref_list(s: &str) -> Result<Vec<ObjId>, String> {
534    let s = s.trim();
535    if s.is_empty() {
536        return Ok(Vec::new());
537    }
538    s.split_whitespace().map(parse_ref).collect()
539}
540
541/// Extract value between `key` and `delim` (or end of string).
542fn extract_field<'a>(s: &'a str, key: &str, delim: char) -> Result<&'a str, String> {
543    let start = s.find(key).ok_or_else(|| format!("expected {key}"))? + key.len();
544    let end = s[start..].find(delim).map_or(s.len(), |i| start + i);
545    Ok(&s[start..end])
546}
547
548/// Parse `"name" -> @{id}` or `"name" -> @{id} func=@{fid}`.
549fn parse_symbol_line(line: &str) -> Result<DumpedSymbol, String> {
550    let (name, rest) = parse_quoted_string_rest(line)?;
551    let rest = rest.strip_prefix(" -> ").ok_or("expected -> ")?;
552    let sym_id = parse_ref(rest)?;
553    let func_id = if let Some(fpos) = rest.find("func=@") {
554        Some(parse_ref(&rest[fpos + "func=".len()..])?)
555    } else {
556        None
557    };
558    Ok(DumpedSymbol { name, sym_id, func_id })
559}
560
561/// Parse `@{sym_id} = @{val_id}`.
562fn parse_binding_line(line: &str) -> Result<DumpedBinding, String> {
563    let (left, right) = line.split_once(" = ").ok_or("expected = ")?;
564    Ok(DumpedBinding { sym_id: parse_ref(left)?, val_id: parse_ref(right)? })
565}
566
567// ── Loader ──────────────────────────────────────────────────────────────────
568// Two-pass reconstruction of the GC heap from a parsed DumpFile.
569//
570// Pass 1: Allocate a placeholder GC object for every DumpedObject entry,
571//         building an id->Object lookup table.
572//         Leaf types (int, float, string, symbol, subr) are fully constructed here.
573//         Compound types (cons, vec, bytefn) get placeholders with nil/empty contents.
574//         SubrFn is a special case: only the name of the function pointer is
575//         serialized, so we look up the static reference in `sym::SUBR_DEFS`
576//
577// Pass 2: Fix up pointer fields in compound objects: patch cons car/cdr,
578//         vec elements, and bytefn constants with the real Objects from the
579//         lookup table.
580//
581// After both passes, we rebuild the symbol function cells and env var bindings and inject
582// it into the runtime.
583
584use crate::core::{
585    cons::Cons,
586    env::intern,
587    object::{FnArgs, HashTable, IntoObject, Symbol},
588};
589use rune_core::hashmap::IndexMap;
590
591pub(crate) fn load_dump(path: &Path, env: &mut Rt<Env>, cx: &mut Context) -> Result<(), String> {
592    let input = std::fs::read_to_string(path).map_err(|e| format!("read error: {e}"))?;
593    let dump = parse_dump(&input)?;
594    let subrs = subr_registry();
595
596    // Pass 1: allocate placeholder GC objects and build a id->Object lookup table
597    let mut table: Vec<Object> = Vec::with_capacity(dump.objects.len());
598
599    for obj in &dump.objects {
600        let live: Object = match obj {
601            DumpedObject::Nil => NIL,
602            DumpedObject::Int(x) => cx.add(*x),
603            DumpedObject::Float(x) => cx.add(*x),
604            DumpedObject::String(s) => cx.add(s.as_str()),
605            DumpedObject::ByteString(b) => cx.add(b.clone()),
606            DumpedObject::Symbol { name, interned } => {
607                if *interned {
608                    // Interned symbols already exist in the global table;
609                    // look them up rather than creating duplicates.
610                    let sym = intern(name, cx);
611                    let obj: Object = sym.into();
612                    obj
613                } else {
614                    let sym = Symbol::new_uninterned(name, cx);
615                    let obj: Object = sym.into();
616                    obj
617                }
618            }
619            // Cons: allocate with nil/nil, will be patched in pass 2
620            DumpedObject::Cons { .. } => {
621                let cons = Cons::new(NIL, NIL, cx);
622                let obj: Object = cons.into();
623                obj
624            }
625            // Vec: allocate with correct length, filled with nil, patched in pass 2
626            DumpedObject::Vec(elems) => {
627                let nils: Vec<Object> = vec![NIL; elems.len()];
628                cx.add(nils)
629            }
630            DumpedObject::ByteFn { args, depth, codes, consts } => {
631                // Allocate constants vector with nil placeholders, patched in pass 2
632                let nils: Vec<Object> = vec![NIL; consts.len()];
633                let const_vec: crate::core::object::Gc<&crate::core::object::LispVec> =
634                    nils.into_obj(cx);
635                let fn_args = FnArgs::from_arg_spec(*args as i64)
636                    .map_err(|e| format!("bad arg spec: {e}"))?;
637                let bytefn = unsafe {
638                    crate::core::object::ByteFn::make(codes, const_vec.untag(), fn_args, *depth)
639                };
640                let obj: Object = bytefn.into_obj(cx).into();
641                obj
642            }
643            DumpedObject::Subr(name) => {
644                // Look up the Rust function pointer by name
645                let subr =
646                    subrs.get(name.as_str()).ok_or_else(|| format!("unknown subr: {name}"))?;
647                let obj: Object = (*subr).into();
648                obj
649            }
650            DumpedObject::Record(elems) => {
651                let nils: Vec<Object> = vec![NIL; elems.len()];
652                let mut gvec = cx.vec_with_capacity(nils.len());
653                gvec.extend_from_slice(&nils);
654                let builder = crate::core::object::RecordBuilder(gvec);
655                let obj: Object = builder.into_obj(cx).into();
656                obj
657            }
658            DumpedObject::BigInt(s) => {
659                let n: num_bigint::BigInt = s.parse().map_err(|e| format!("bad bigint: {e}"))?;
660                cx.add(n)
661            }
662            // Hash tables: allocate empty, patched in pass 2
663            DumpedObject::HashTable(_entries) => {
664                let ht: HashTable = IndexMap::default();
665                let obj: Object = ht.into_obj(cx).into();
666                obj
667            }
668            // Opaque types - we can't reconstruct these, use nil as placeholder
669            DumpedObject::Buffer
670            | DumpedObject::CharTable
671            | DumpedObject::ChannelSender
672            | DumpedObject::ChannelReceiver => NIL,
673        };
674        table.push(live);
675    }
676
677    // Pass 2: fix up compound object pointers
678    for (id, obj) in dump.objects.iter().enumerate() {
679        match obj {
680            DumpedObject::Cons { car, cdr } => {
681                let cons = table[id].untag();
682                if let ObjectType::Cons(c) = cons {
683                    // Cons cells are allocated mutable, so set_car/set_cdr work
684                    c.set_car(table[*car as usize]).map_err(|e| e.to_string())?;
685                    c.set_cdr(table[*cdr as usize]).map_err(|e| e.to_string())?;
686                }
687            }
688            DumpedObject::Vec(elems) => {
689                if let ObjectType::Vec(v) = table[id].untag() {
690                    let cells = v.try_mut().map_err(|e| e.to_string())?;
691                    for (i, elem_id) in elems.iter().enumerate() {
692                        cells[i].set(table[*elem_id as usize]);
693                    }
694                }
695            }
696            DumpedObject::ByteFn { consts, .. } => {
697                if let ObjectType::ByteFn(bf) = table[id].untag() {
698                    // The constants vector was allocated with nil placeholders;
699                    // patch each element with the real object.
700                    let cells = bf.consts_mut().map_err(|e| e.to_string())?;
701                    for (i, c_id) in consts.iter().enumerate() {
702                        cells[i].set(table[*c_id as usize]);
703                    }
704                }
705            }
706            DumpedObject::Record(elems) => {
707                if let ObjectType::Record(r) = table[id].untag() {
708                    let cells = r.try_mut().map_err(|e| e.to_string())?;
709                    for (i, elem_id) in elems.iter().enumerate() {
710                        cells[i].set(table[*elem_id as usize]);
711                    }
712                }
713            }
714            DumpedObject::HashTable(entries) => {
715                if let ObjectType::HashTable(ht) = table[id].untag() {
716                    for (k_id, v_id) in entries {
717                        ht.insert(table[*k_id as usize], table[*v_id as usize]);
718                    }
719                }
720            }
721
722            // Leaf types are already fully constructed in pass 1, skip
723            DumpedObject::Nil
724            | DumpedObject::Int(_)
725            | DumpedObject::Float(_)
726            | DumpedObject::String(_)
727            | DumpedObject::ByteString(_)
728            | DumpedObject::Symbol { .. }
729            | DumpedObject::Subr(_)
730            | DumpedObject::BigInt(_) => {}
731
732            // skip opaque types as well
733            // TODO these will be implemented in the future
734            DumpedObject::Buffer
735            | DumpedObject::CharTable
736            | DumpedObject::ChannelSender
737            | DumpedObject::ChannelReceiver => {}
738        }
739    }
740
741    // Rebuild symbol function cells
742    // For symbols that had a function binding at dump time, re-bind it.
743    // We go through INTERNED_SYMBOLS.set_func which clones the function
744    // into the global block and marks it immutable.
745    {
746        let map = INTERNED_SYMBOLS.lock().unwrap();
747        for sym_entry in &dump.symbols {
748            if let Some(func_id) = sym_entry.func_id {
749                let func_obj = table[func_id as usize];
750                // Only re-bind non-subr functions - subrs are already bound
751                // by init_symbols(). Elisp-defined functions (ByteFn, Cons
752                // closures) from bootstrap are what we need to restore.
753                if let ObjectType::SubrFn(_) = func_obj.untag() {
754                    continue;
755                }
756                if let Some(sym) = map.get(&sym_entry.name) {
757                    let func: crate::core::object::Function =
758                        unsafe { crate::core::object::Gc::from_raw(func_obj.into_raw()) };
759                    // set_func clones into the global block and marks immutable
760                    map.set_func(sym, func).map_err(|e| e.to_string())?;
761                }
762            }
763        }
764    }
765
766    // Rebuild env variable bindings
767    for binding in &dump.env {
768        let sym_obj = table[binding.sym_id as usize];
769        let val_obj = table[binding.val_id as usize];
770        if let ObjectType::Symbol(sym) = sym_obj.untag() {
771            env.vars.insert(sym, val_obj);
772        }
773    }
774
775    Ok(())
776}
777
778#[cfg(test)]
779mod tests {
780    use super::*;
781
782    // escape_str / parse_quoted_string round-trip
783    #[test]
784    fn test_escape_roundtrip_plain() {
785        let s = "hello world";
786        assert_eq!(parse_quoted_string(&format!("\"{}\"", escape_str(s))).unwrap(), s);
787    }
788
789    #[test]
790    fn test_escape_roundtrip_empty() {
791        assert_eq!(parse_quoted_string(&format!("\"{}\"", escape_str(""))).unwrap(), "");
792    }
793
794    #[test]
795    fn test_parse_quoted_string_no_opening_quote() {
796        assert!(parse_quoted_string("no quote").is_err());
797    }
798
799    #[test]
800    fn test_parse_quoted_string_unterminated() {
801        assert!(parse_quoted_string("\"unterminated").is_err());
802    }
803
804    // parse_hex_bytes
805    #[test]
806    fn test_parse_hex_bytes_valid() {
807        assert_eq!(parse_hex_bytes("deadbeef").unwrap(), vec![0xde, 0xad, 0xbe, 0xef]);
808    }
809
810    #[test]
811    fn test_parse_hex_bytes_empty() {
812        assert_eq!(parse_hex_bytes("").unwrap(), Vec::<u8>::new());
813    }
814
815    #[test]
816    fn test_parse_hex_bytes_odd_length() {
817        assert!(parse_hex_bytes("abc").is_err());
818    }
819
820    // parse_ref
821    #[test]
822    fn test_parse_ref_valid() {
823        assert_eq!(parse_ref("@42").unwrap(), 42);
824        assert_eq!(parse_ref("@0").unwrap(), 0);
825        assert_eq!(parse_ref("  @7  ").unwrap(), 7);
826    }
827
828    #[test]
829    fn test_parse_ref_no_at() {
830        assert!(parse_ref("42").is_err());
831    }
832
833    // parse_ref_list
834    #[test]
835    fn test_parse_ref_list() {
836        assert_eq!(parse_ref_list("@1 @2 @3").unwrap(), vec![1, 2, 3]);
837        assert_eq!(parse_ref_list("").unwrap(), Vec::<ObjId>::new());
838        assert_eq!(parse_ref_list("  ").unwrap(), Vec::<ObjId>::new());
839    }
840
841    // parse_object_line
842    #[test]
843    fn test_parse_nil() {
844        let (id, obj) = parse_object_line("@0 nil").unwrap();
845        assert_eq!(id, 0);
846        assert!(matches!(obj, DumpedObject::Nil));
847    }
848
849    #[test]
850    fn test_parse_int() {
851        let (id, obj) = parse_object_line("@1 int 42").unwrap();
852        assert_eq!(id, 1);
853        assert!(matches!(obj, DumpedObject::Int(42)));
854    }
855
856    #[test]
857    fn test_parse_negative_int() {
858        let (_, obj) = parse_object_line("@2 int -7").unwrap();
859        assert!(matches!(obj, DumpedObject::Int(-7)));
860    }
861
862    #[test]
863    #[allow(clippy::approx_constant)]
864    fn test_parse_float() {
865        let (_, obj) = parse_object_line("@3 float 3.14").unwrap();
866        if let DumpedObject::Float(v) = obj {
867            assert!((v - 3.14).abs() < f64::EPSILON);
868        } else {
869            panic!("expected Float");
870        }
871    }
872
873    #[test]
874    fn test_parse_string() {
875        let (_, obj) = parse_object_line(r#"@4 string "hello""#).unwrap();
876        assert!(matches!(obj, DumpedObject::String(s) if s == "hello"));
877    }
878
879    #[test]
880    fn test_parse_string_with_escapes() {
881        let (_, obj) = parse_object_line(r#"@5 string "line\none""#).unwrap();
882        assert!(matches!(obj, DumpedObject::String(s) if s == "line\none"));
883    }
884
885    #[test]
886    fn test_parse_bytestring() {
887        let (_, obj) = parse_object_line("@6 bytestring #ff00ab").unwrap();
888        assert!(matches!(obj, DumpedObject::ByteString(b) if b == vec![0xff, 0x00, 0xab]));
889    }
890
891    #[test]
892    fn test_parse_symbol() {
893        let (_, obj) = parse_object_line(r#"@7 symbol "foo" interned=true"#).unwrap();
894        assert!(
895            matches!(obj, DumpedObject::Symbol { name, interned } if name == "foo" && interned)
896        );
897    }
898
899    #[test]
900    fn test_parse_cons() {
901        let (_, obj) = parse_object_line("@8 cons car=@1 cdr=@2").unwrap();
902        assert!(matches!(obj, DumpedObject::Cons { car: 1, cdr: 2 }));
903    }
904
905    #[test]
906    fn test_parse_vec() {
907        let (_, obj) = parse_object_line("@9 vec [@1 @2 @3]").unwrap();
908        assert!(matches!(obj, DumpedObject::Vec(v) if v == vec![1, 2, 3]));
909    }
910
911    #[test]
912    fn test_parse_vec_empty() {
913        let (_, obj) = parse_object_line("@10 vec []").unwrap();
914        assert!(matches!(obj, DumpedObject::Vec(v) if v.is_empty()));
915    }
916
917    #[test]
918    fn test_parse_subr() {
919        let (_, obj) = parse_object_line(r#"@11 subr "car""#).unwrap();
920        assert!(matches!(obj, DumpedObject::Subr(s) if s == "car"));
921    }
922
923    #[test]
924    fn test_parse_record() {
925        let (_, obj) = parse_object_line("@12 record [@5 @6]").unwrap();
926        assert!(matches!(obj, DumpedObject::Record(v) if v == vec![5, 6]));
927    }
928
929    #[test]
930    fn test_parse_hashtable() {
931        let (_, obj) = parse_object_line("@13 hashtable [1:2 3:4]").unwrap();
932        assert!(matches!(obj, DumpedObject::HashTable(e) if e == vec![(1,2),(3,4)]));
933    }
934
935    #[test]
936    fn test_parse_hashtable_empty() {
937        let (_, obj) = parse_object_line("@14 hashtable []").unwrap();
938        assert!(matches!(obj, DumpedObject::HashTable(e) if e.is_empty()));
939    }
940
941    #[test]
942    fn test_parse_bigint() {
943        let (_, obj) = parse_object_line("@15 bigint 99999999999999999999").unwrap();
944        assert!(matches!(obj, DumpedObject::BigInt(s) if s == "99999999999999999999"));
945    }
946
947    #[test]
948    fn test_parse_opaque_types() {
949        assert!(matches!(
950            parse_object_line("@16 buffer <opaque>").unwrap().1,
951            DumpedObject::Buffer
952        ));
953        assert!(matches!(
954            parse_object_line("@17 chartable <opaque>").unwrap().1,
955            DumpedObject::CharTable
956        ));
957        assert!(matches!(
958            parse_object_line("@18 channel-sender <opaque>").unwrap().1,
959            DumpedObject::ChannelSender
960        ));
961        assert!(matches!(
962            parse_object_line("@19 channel-receiver <opaque>").unwrap().1,
963            DumpedObject::ChannelReceiver
964        ));
965    }
966
967    #[test]
968    fn test_parse_bytefn() {
969        let (_, obj) =
970            parse_object_line("@20 bytefn args=0 depth=5 codes=#0102 consts=[@1 @2]").unwrap();
971        if let DumpedObject::ByteFn { args, depth, codes, consts } = obj {
972            assert_eq!(args, 0);
973            assert_eq!(depth, 5);
974            assert_eq!(codes, vec![0x01, 0x02]);
975            assert_eq!(consts, vec![1, 2]);
976        } else {
977            panic!("expected ByteFn");
978        }
979    }
980
981    #[test]
982    fn test_parse_object_line_unknown_type() {
983        assert!(parse_object_line("@0 foobar xyz").is_err());
984    }
985
986    // Display round-trip (format then parse)
987
988    fn display_roundtrip(id: ObjId, obj: &DumpedObject) -> DumpedObject {
989        let line = format!("@{id} {obj}");
990        parse_object_line(&line).unwrap().1
991    }
992
993    #[test]
994    #[allow(clippy::approx_constant)]
995    fn test_display_roundtrip_leaf_types() {
996        let cases: Vec<(ObjId, DumpedObject)> = vec![
997            (0, DumpedObject::Nil),
998            (1, DumpedObject::Int(-42)),
999            (2, DumpedObject::Float(2.718)),
1000            (3, DumpedObject::String("hello \"world\"\nnewline".into())),
1001            (4, DumpedObject::ByteString(vec![0xca, 0xfe])),
1002            (5, DumpedObject::Symbol { name: "my-sym".into(), interned: false }),
1003            (6, DumpedObject::Subr("car".into())),
1004            (7, DumpedObject::BigInt("123456789012345678901234567890".into())),
1005        ];
1006        for (id, original) in &cases {
1007            let recovered = display_roundtrip(*id, original);
1008            assert_eq!(format!("{recovered}"), format!("{original}"), "mismatch for @{id}");
1009        }
1010    }
1011
1012    #[test]
1013    fn test_display_roundtrip_compound_types() {
1014        let cases: Vec<(ObjId, DumpedObject)> = vec![
1015            (0, DumpedObject::Cons { car: 1, cdr: 2 }),
1016            (1, DumpedObject::Vec(vec![3, 4, 5])),
1017            (2, DumpedObject::Record(vec![6, 7])),
1018            (3, DumpedObject::HashTable(vec![(8, 9), (10, 11)])),
1019            (
1020                4,
1021                DumpedObject::ByteFn {
1022                    args: 257,
1023                    depth: 3,
1024                    codes: vec![0xab, 0xcd],
1025                    consts: vec![0, 1],
1026                },
1027            ),
1028        ];
1029        for (id, original) in &cases {
1030            let recovered = display_roundtrip(*id, original);
1031            assert_eq!(format!("{recovered}"), format!("{original}"), "mismatch for @{id}");
1032        }
1033    }
1034
1035    #[test]
1036    fn test_parse_symbol_line_no_func() {
1037        let sym = parse_symbol_line(r#""my-var" -> @5"#).unwrap();
1038        assert_eq!(sym.name, "my-var");
1039        assert_eq!(sym.sym_id, 5);
1040        assert_eq!(sym.func_id, None);
1041    }
1042
1043    #[test]
1044    fn test_parse_symbol_line_with_func() {
1045        let sym = parse_symbol_line(r#""my-fn" -> @3 func=@7"#).unwrap();
1046        assert_eq!(sym.name, "my-fn");
1047        assert_eq!(sym.sym_id, 3);
1048        assert_eq!(sym.func_id, Some(7));
1049    }
1050
1051    #[test]
1052    fn test_parse_binding_line() {
1053        let b = parse_binding_line("@10 = @20").unwrap();
1054        assert_eq!(b.sym_id, 10);
1055        assert_eq!(b.val_id, 20);
1056    }
1057
1058    // parse_dump (full file)
1059
1060    #[test]
1061    fn test_parse_dump_minimal() {
1062        let input = "\
1063.HEADER
1064  version 1
1065  objects 2
1066
1067.OBJECTS
1068  @0 nil
1069  @1 int 42
1070
1071.SYMBOLS
1072  \"nil\" -> @0
1073
1074.ENV
1075  @0 = @1
1076";
1077        let dump = parse_dump(input).unwrap();
1078        assert_eq!(dump.objects.len(), 2);
1079        assert!(matches!(dump.objects[0], DumpedObject::Nil));
1080        assert!(matches!(dump.objects[1], DumpedObject::Int(42)));
1081        assert_eq!(dump.symbols.len(), 1);
1082        assert_eq!(dump.symbols[0].name, "nil");
1083        assert_eq!(dump.env.len(), 1);
1084        assert_eq!(dump.env[0].sym_id, 0);
1085        assert_eq!(dump.env[0].val_id, 1);
1086    }
1087
1088    #[test]
1089    fn test_parse_dump_unknown_section() {
1090        let input = ".UNKNOWN\n";
1091        assert!(parse_dump(input).is_err());
1092    }
1093
1094    #[test]
1095    fn test_parse_dump_missing_object() {
1096        // Gap: @0 present, @1 missing, @2 present
1097        let input = "\
1098.HEADER
1099  version 1
1100  objects 3
1101
1102.OBJECTS
1103  @0 nil
1104  @2 int 1
1105
1106.SYMBOLS
1107
1108.ENV
1109";
1110        assert!(parse_dump(input).is_err());
1111    }
1112
1113    // into_output / parse_dump round-trip
1114
1115    #[test]
1116    fn test_dump_state_roundtrip() {
1117        let mut state = DumpState::new();
1118        state.objects.push(DumpedObject::Nil);
1119        state.objects.push(DumpedObject::Int(99));
1120        state.objects.push(DumpedObject::String("test".into()));
1121        state.objects.push(DumpedObject::Cons { car: 1, cdr: 2 });
1122        state.symbols.push(DumpedSymbol { name: "x".into(), sym_id: 1, func_id: None });
1123        state
1124            .symbols
1125            .push(DumpedSymbol { name: "f".into(), sym_id: 2, func_id: Some(3) });
1126        state.env.push(DumpedBinding { sym_id: 1, val_id: 0 });
1127
1128        let output = state.into_output();
1129        let dump = parse_dump(&output).unwrap();
1130
1131        assert_eq!(dump.objects.len(), 4);
1132        assert!(matches!(dump.objects[0], DumpedObject::Nil));
1133        assert!(matches!(dump.objects[1], DumpedObject::Int(99)));
1134        assert!(matches!(dump.objects[2], DumpedObject::String(ref s) if s == "test"));
1135        assert!(matches!(dump.objects[3], DumpedObject::Cons { car: 1, cdr: 2 }));
1136        assert_eq!(dump.symbols.len(), 2);
1137        assert_eq!(dump.symbols[0].name, "x");
1138        assert_eq!(dump.symbols[0].func_id, None);
1139        assert_eq!(dump.symbols[1].name, "f");
1140        assert_eq!(dump.symbols[1].func_id, Some(3));
1141        assert_eq!(dump.env.len(), 1);
1142        assert_eq!(dump.env[0].sym_id, 1);
1143        assert_eq!(dump.env[0].val_id, 0);
1144    }
1145
1146    #[test]
1147    #[allow(clippy::approx_constant)]
1148    fn test_parse_sample_dump_file() {
1149        let input = r#".HEADER
1150  version 1
1151  objects 7
1152
1153.OBJECTS
1154  @0 nil
1155  @1 subr "elt"
1156  @2 symbol "bare-symbol" interned=true
1157  @3 subr "bare-symbol"
1158  @4 int 42
1159  @5 float 3.14
1160  @6 string "hello world"
1161
1162.SYMBOLS
1163  "nil" -> @0
1164  "elt" -> @0 func=@1
1165  "bare-symbol" -> @2 func=@3
1166
1167.ENV
1168  @2 = @4
1169"#;
1170        let dump = parse_dump(input).unwrap();
1171
1172        assert_eq!(dump.objects.len(), 7);
1173        assert!(matches!(dump.objects[0], DumpedObject::Nil));
1174        assert!(matches!(dump.objects[1], DumpedObject::Subr(ref s) if s == "elt"));
1175        assert!(
1176            matches!(dump.objects[2], DumpedObject::Symbol { ref name, interned: true } if name == "bare-symbol")
1177        );
1178        assert!(matches!(dump.objects[3], DumpedObject::Subr(ref s) if s == "bare-symbol"));
1179        assert!(matches!(dump.objects[4], DumpedObject::Int(42)));
1180        if let DumpedObject::Float(v) = dump.objects[5] {
1181            assert!((v - 3.14).abs() < f64::EPSILON);
1182        } else {
1183            panic!("expected Float");
1184        }
1185        assert!(matches!(dump.objects[6], DumpedObject::String(ref s) if s == "hello world"));
1186
1187        assert_eq!(dump.symbols.len(), 3);
1188        assert_eq!(dump.symbols[0].name, "nil");
1189        assert_eq!(dump.symbols[0].sym_id, 0);
1190        assert_eq!(dump.symbols[0].func_id, None);
1191        assert_eq!(dump.symbols[1].name, "elt");
1192        assert_eq!(dump.symbols[1].sym_id, 0);
1193        assert_eq!(dump.symbols[1].func_id, Some(1));
1194        assert_eq!(dump.symbols[2].name, "bare-symbol");
1195        assert_eq!(dump.symbols[2].sym_id, 2);
1196        assert_eq!(dump.symbols[2].func_id, Some(3));
1197
1198        assert_eq!(dump.env.len(), 1);
1199        assert_eq!(dump.env[0].sym_id, 2);
1200        assert_eq!(dump.env[0].val_id, 4);
1201    }
1202}