Skip to main content

serpent_serializer/
serialize.rs

1//! The serializer core.
2//!
3//! Translates a [`Value`] graph to Lua source. This follows serpent's `val2str`
4//! recursion and top-level assembly: array-first key ordering, short vs bracket
5//! key notation, shared and cyclic reference wiring through a self-reference
6//! section, and the `do ... return name end` wrapper when a name is set.
7
8use crate::numfmt;
9use crate::options::Options;
10use crate::quote::quote;
11use crate::value::{Ident, Key, Table, Value};
12use std::collections::HashMap;
13
14/// The 22 Lua reserved words. String keys that are keywords never use short
15/// notation.
16const KEYWORDS: &[&str] = &[
17    "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "goto", "if", "in",
18    "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while",
19];
20
21fn is_keyword(s: &str) -> bool {
22    KEYWORDS.contains(&s)
23}
24
25/// Serializer state carried through the recursion.
26struct Serializer<'o> {
27    opts: &'o Options,
28    space: &'static str,
29    huge: bool,
30    maxl: usize,
31    maxlen: Option<i64>,
32    numformat: String,
33    iname: String,
34    comm: Option<usize>,
35    /// Map from a reference identity to the path expression where it was placed.
36    seen: HashMap<Ident, String>,
37    /// Self-reference section statements. Index 0 is the helper-table decl.
38    sref: Vec<String>,
39    /// Symbol table for gensym: matched substring to stable counter.
40    syms: HashMap<String, usize>,
41    symn: usize,
42    /// tostring of the first non-serializable value reached under `fatal`.
43    /// serpent raises `error("Can't serialize "..tostring(s))` at that point.
44    /// Recording the text lets the top level build the same message.
45    fatal_value: Option<String>,
46}
47
48/// The recursion context passed to [`Serializer::val2str`].
49///
50/// Grouping these together keeps the call sites readable. Transposing two
51/// borrowed strings in a positional list would compile and emit wrong output,
52/// so the names carry the meaning here instead of the argument order.
53#[derive(Clone, Copy)]
54struct Frame<'a> {
55    /// The key this value sits under, if any.
56    name: Option<&'a Key>,
57    /// The indentation unit, when output is multi-line.
58    indent: Option<&'a str>,
59    /// The path to register in `seen` for this value, overriding the computed
60    /// path. serpent's `insref`.
61    insref: Option<&'a str>,
62    /// The access path prefix this value hangs off, such as `_.a`.
63    path: Option<&'a str>,
64    /// Whether this element uses array notation with no `key =` prefix.
65    plainindex: bool,
66    /// The nesting depth, starting at 0.
67    level: usize,
68}
69
70impl<'o> Serializer<'o> {
71    fn new(opts: &'o Options) -> Self {
72        let name = opts.name.clone().unwrap_or_default();
73        let iname = format!("_{name}");
74        let space = if opts.compact { "" } else { " " };
75        Serializer {
76            space,
77            huge: !opts.nohuge,
78            maxl: opts.maxlevel.unwrap_or(usize::MAX),
79            maxlen: opts.maxlength,
80            numformat: opts
81                .numformat
82                .clone()
83                .unwrap_or_else(|| "%.17g".to_string()),
84            sref: vec![format!("local {iname}={{}}")],
85            iname,
86            comm: opts.comment,
87            seen: HashMap::new(),
88            syms: HashMap::new(),
89            symn: 0,
90            fatal_value: None,
91            opts,
92        }
93    }
94
95    /// serpent's `gensym`: build a stable identifier for a reference used as a
96    /// key. Strip non-alphanumerics from the identity text, then remap each
97    /// `[0-9][A-Za-z0-9]+` run to a stable counter, and prefix `_`.
98    fn gensym(&mut self, raw: &str) -> String {
99        let stripped: String = raw.chars().filter(|c| c.is_ascii_alphanumeric()).collect();
100        let remapped = self.remap_runs(&stripped);
101        format!("_{remapped}")
102    }
103
104    /// Replace each maximal `[0-9][A-Za-z0-9]+` run with a stable 1-based
105    /// counter assigned in first-seen order.
106    fn remap_runs(&mut self, s: &str) -> String {
107        let b = s.as_bytes();
108        let mut out = String::new();
109        let mut i = 0;
110        while i < b.len() {
111            if b[i].is_ascii_digit() {
112                // A run must be a digit followed by one or more word chars.
113                let start = i;
114                i += 1;
115                let mut count = 0;
116                while i < b.len() && b[i].is_ascii_alphanumeric() {
117                    i += 1;
118                    count += 1;
119                }
120                let run = &s[start..i];
121                if count >= 1 {
122                    let n = *self.syms.entry(run.to_string()).or_insert_with(|| {
123                        self.symn += 1;
124                        self.symn
125                    });
126                    out.push_str(&n.to_string());
127                } else {
128                    out.push_str(run);
129                }
130            } else {
131                out.push(b[i] as char);
132                i += 1;
133            }
134        }
135        out
136    }
137
138    /// serpent's `safestr`: a scalar value as a Lua literal.
139    fn safestr(&self, v: &Value) -> String {
140        match v {
141            Value::Number(n) => {
142                if self.huge {
143                    if n.is_infinite() {
144                        return if *n > 0.0 {
145                            "1/0 --[[math.huge]]".to_string()
146                        } else {
147                            "-1/0 --[[-math.huge]]".to_string()
148                        };
149                    }
150                    if n.is_nan() {
151                        return "0/0".to_string();
152                    }
153                }
154                numfmt::format(&self.numformat, *n)
155            }
156            Value::Str(s) => quote(s),
157            Value::Bool(b) => b.to_string(),
158            Value::Nil => "nil".to_string(),
159            // Non-scalar reaching safestr means a fallback tostring; use a
160            // placeholder path text.
161            other => quote(tostring(other).as_bytes()),
162        }
163    }
164
165    /// serpent's `comment`: an inline `--[[...]]` comment, when enabled and the
166    /// level is under the threshold.
167    fn comment(&self, text: &str, level: usize) -> String {
168        match self.comm {
169            Some(threshold) if level < threshold => format!(" --[[{text}]]"),
170            _ => String::new(),
171        }
172    }
173
174    /// serpent's `safename`: build an access path and return `(path, safe)`.
175    fn safename(&self, path: Option<&str>, name: Option<&Key>) -> (String, String) {
176        let plain_name = match name {
177            Some(Key::Str(s)) => std::str::from_utf8(s)
178                .ok()
179                .filter(|n| is_plain_ident(n) && !is_keyword(n))
180                .map(str::to_string),
181            _ => None,
182        };
183        let (safe, is_plain) = match &plain_name {
184            Some(n) => (n.clone(), true),
185            None => {
186                let key_val = name.map_or(Value::Str(Vec::new()), Key::to_value);
187                (format!("[{}]", self.safestr(&key_val)), false)
188            }
189        };
190        let joiner = if is_plain && path.is_some() { "." } else { "" };
191        let full = format!("{}{}{}", path.unwrap_or(""), joiner, safe);
192        (full, safe)
193    }
194
195    /// serpent's `val2str`. Returns the element text and appends any needed
196    /// self-reference statements.
197    fn val2str(&mut self, t: &Value, frame: Frame) -> String {
198        let level = frame.level;
199        let (spath, sname) = self.safename(frame.path, frame.name);
200        let tag = self.build_tag(frame.name, &sname, frame.plainindex);
201
202        // Already seen: emit nil and wire the reference in the self-ref section.
203        if let Some(id) = t.ident() {
204            if let Some(prev) = self.seen.get(&id).cloned() {
205                self.sref
206                    .push(format!("{spath}{}={}{prev}", self.space, self.space));
207                return format!("{tag}nil{}", self.comment("ref", level));
208            }
209        }
210
211        // Metamethods. When a table has __serialize or __tostring and
212        // metatostring is not false, the successful result replaces the table.
213        // __serialize wins over __tostring. A raising metamethod is ignored.
214        let replaced = self.apply_metamethods(t, frame.insref.unwrap_or(&spath));
215        let t = replaced.as_ref().unwrap_or(t);
216
217        match t {
218            Value::Table(tab) => self.table_to_str(tab, &tag, &spath, frame),
219            Value::Global(_) => {
220                if let Some(id) = t.ident() {
221                    self.seen
222                        .insert(id, frame.insref.unwrap_or(&spath).to_string());
223                }
224                format!("{tag}{}", self.globerr(t, level))
225            }
226            Value::Function(f) => {
227                if let Some(id) = t.ident() {
228                    self.seen
229                        .insert(id, frame.insref.unwrap_or(&spath).to_string());
230                }
231                if self.opts.nocode {
232                    return format!(
233                        "{tag}function() --[[..skipped..]] end{}",
234                        self.comment("", level)
235                    );
236                }
237                match &f.bytecode {
238                    Some(code) => {
239                        let quoted = quote(code);
240                        format!(
241                            "{tag}((loadstring or load)({quoted},'@serialized')){}",
242                            self.comment("", level)
243                        )
244                    }
245                    None => format!("{tag}{}", self.globerr(t, level)),
246                }
247            }
248            _ => format!("{tag}{}", self.safestr(t)),
249        }
250    }
251
252    /// Build the `key =` prefix for an element.
253    fn build_tag(&self, name: Option<&Key>, sname: &str, plainindex: bool) -> String {
254        if plainindex {
255            match name {
256                Some(Key::Number(_)) => String::new(),
257                Some(k) => {
258                    let raw = tostring(&k.to_value());
259                    format!("{raw}{}={}", self.space, self.space)
260                }
261                None => String::new(),
262            }
263        } else if name.is_some() {
264            format!("{sname}{}={}", self.space, self.space)
265        } else {
266            String::new()
267        }
268    }
269
270    /// Try the table's metamethods. Returns the replacement value when one
271    /// succeeds. `__serialize` is preferred over `__tostring`. A metamethod that
272    /// errors is skipped. The table is marked seen at `seen_path` when a
273    /// metamethod applies, matching serpent's `seen[t] = insref or spath`.
274    fn apply_metamethods(&mut self, t: &Value, seen_path: &str) -> Option<Value> {
275        let Value::Table(tab) = t else {
276            return None;
277        };
278        if self.opts.metatostring == Some(false) {
279            return None;
280        }
281        let ser = tab.serialize_meta();
282        let tos = tab.tostring_meta();
283        if ser.is_none() && tos.is_none() {
284            return None;
285        }
286        // __serialize wins when it succeeds, else __tostring.
287        let result = ser
288            .and_then(|f| f().ok())
289            .or_else(|| tos.and_then(|f| f().ok()))?;
290        if let Some(id) = t.ident() {
291            self.seen.insert(id, seen_path.to_string());
292        }
293        Some(result)
294    }
295
296    /// serpent's `globerr`: a global name, a fallback string, or an error.
297    ///
298    /// A `Global` with a non-empty name is a known global and emits its name.
299    /// An empty name means the value has no global mapping, matching an unknown
300    /// userdata: it falls back to a quoted string, or records a fatal value when
301    /// `fatal` is set so the top level can raise an error.
302    fn globerr(&mut self, v: &Value, level: usize) -> String {
303        if let Value::Global(g) = v {
304            if !g.name.is_empty() {
305                return format!("{}{}", g.name, self.comment(&g.name, level));
306            }
307        }
308        // A value with no global mapping stands in for unknown userdata, whose
309        // Lua tostring is `userdata: 0x...`. Build that address form for both
310        // the fallback string and the fatal message.
311        let text = match v {
312            Value::Global(g) if g.name.is_empty() => format!("userdata: 0x{:012x}", g.id),
313            _ => tostring(v),
314        };
315        if !self.opts.fatal {
316            return self.safestr(&Value::Str(text.into_bytes()));
317        }
318        // fatal: record the value text for the error message. Keep the first one.
319        if self.fatal_value.is_none() {
320            self.fatal_value = Some(text.clone());
321        }
322        // The output is discarded once a fatal value is recorded, so the return
323        // value here does not reach the caller.
324        self.safestr(&Value::Str(text.into_bytes()))
325    }
326
327    fn table_to_str(&mut self, tab: &Table, tag: &str, spath: &str, frame: Frame) -> String {
328        let Frame {
329            indent,
330            insref,
331            level,
332            ..
333        } = frame;
334        if level >= self.maxl {
335            return format!("{tag}{{}}{}", self.comment("maxlvl", level));
336        }
337        let id = Ident::Table(tab.id());
338        self.seen.insert(id, insref.unwrap_or(spath).to_string());
339        if tab.is_empty() {
340            let text = tab_tostring(tab);
341            return format!("{tag}{{}}{}", self.comment(&text, level));
342        }
343        if let Some(ml) = self.maxlen {
344            if ml < 0 {
345                return format!("{tag}{{}}{}", self.comment("maxlen", level));
346            }
347        }
348
349        let entries = tab.entries();
350        let border = tab.border();
351        let maxn = match self.opts.maxnum {
352            Some(m) => border.min(m),
353            None => border,
354        };
355
356        // Index the entries by key once for O(1) value lookup. Keys are unique
357        // under `Key::same`, so each projection maps to a single slot.
358        let index: HashMap<KeyId, usize> = entries
359            .iter()
360            .enumerate()
361            .filter_map(|(i, (k, _))| key_id(k).map(|id| (id, i)))
362            .collect();
363        let lookup = |key: &Key| -> Option<Value> {
364            key_id(key)
365                .and_then(|id| index.get(&id))
366                .map(|&i| entries[i].1.clone())
367        };
368
369        // Ordered key list: array indices 1..maxn first, then remaining keys.
370        let mut o: Vec<Key> = (1..=maxn).map(|i| Key::Number(i as f64)).collect();
371        if self.opts.maxnum.is_none() || o.len() < self.opts.maxnum.unwrap() {
372            for (k, _) in &entries {
373                if !is_array_index(k, maxn) {
374                    o.push(k.clone());
375                }
376            }
377        }
378        if let Some(m) = self.opts.maxnum {
379            if o.len() > m {
380                o.truncate(m);
381            }
382        }
383        if (self.opts.sortkeys || self.opts.sortfn.is_some()) && o.len() > maxn {
384            self.sort_keys(&mut o, &entries);
385        }
386        let sparse = self.opts.sparse && o.len() > maxn;
387
388        // Compute the table's seen-path once, not per element.
389        let seen_t = self.seen.get(&Ident::Table(tab.id())).cloned();
390
391        let mut out: Vec<String> = Vec::new();
392        for (n, key) in o.iter().enumerate() {
393            let idx = n + 1;
394            let value = lookup(key);
395            let plainindex = idx <= maxn && !sparse;
396
397            if self.should_skip(key, value.as_ref(), sparse) {
398                continue;
399            }
400
401            let is_complex = matches!(key, Key::Table(_) | Key::Function(_) | Key::Global(_));
402            if is_complex {
403                self.emit_complex_key(tab, key, value, indent);
404            } else {
405                let v = value.unwrap_or(Value::Nil);
406                let text = self.val2str(
407                    &v,
408                    Frame {
409                        name: Some(key),
410                        indent,
411                        insref: None,
412                        path: seen_t.as_deref(),
413                        plainindex,
414                        level: level + 1,
415                    },
416                );
417                let len = text.len() as i64;
418                out.push(text);
419                if let Some(ml) = self.maxlen.as_mut() {
420                    *ml -= len;
421                    if *ml < 0 {
422                        break;
423                    }
424                }
425            }
426        }
427
428        self.assemble_table(tab, tag, indent, &out, level)
429    }
430
431    /// A complex key (table/function/global). Emit its assignment into the
432    /// self-reference section, declaring a helper local first if needed.
433    fn emit_complex_key(
434        &mut self,
435        tab: &Table,
436        key: &Key,
437        value: Option<Value>,
438        indent: Option<&str>,
439    ) {
440        let key_val = key.to_value();
441        let key_id = key_val.ident();
442        let key_seen = key_id.as_ref().and_then(|i| self.seen.get(i)).cloned();
443        let key_global = match key {
444            Key::Global(g) => Some(g.name.clone()),
445            _ => None,
446        };
447
448        if key_seen.is_none() && key_global.is_none() {
449            self.sref.push("placeholder".to_string());
450            let idx = self.sref.len() - 1;
451            let sym = self.gensym(&tostring(&key_val));
452            let iname = self.iname.clone();
453            let (sname, _) = self.safename(Some(&iname), Some(&Key::Str(sym.into_bytes())));
454            let stmt = self.val2str(
455                &key_val,
456                Frame {
457                    name: Some(&Key::Str(sname.clone().into_bytes())),
458                    indent,
459                    insref: Some(&sname),
460                    path: Some(&iname),
461                    plainindex: true,
462                    level: 0,
463                },
464            );
465            self.sref[idx] = stmt;
466        }
467
468        self.sref.push("placeholder".to_string());
469        let idx = self.sref.len() - 1;
470        let seen_t = self
471            .seen
472            .get(&Ident::Table(tab.id()))
473            .cloned()
474            .unwrap_or_default();
475        // Recompute seen[key] here: the helper serialization above may have just
476        // registered the key at its helper-table path, matching serpent's
477        // `seen[key] or globals[key] or gensym(key)` at LHS-building time.
478        let key_seen_now = key_id.as_ref().and_then(|i| self.seen.get(i)).cloned();
479        let key_ref = key_seen_now
480            .or(key_global)
481            .unwrap_or_else(|| self.gensym(&tostring(&key_val)));
482        let path = format!("{seen_t}[{key_ref}]");
483        let value = value.unwrap_or(Value::Nil);
484        let val_seen = value.ident().and_then(|i| self.seen.get(&i)).cloned();
485        let rhs = match val_seen {
486            Some(p) => p,
487            // serpent calls val2str(value, nil, indent, path) so `insref` is the
488            // path and the fifth path argument is nil. That makes the value
489            // register at `path`, not at `path[""]`.
490            None => self.val2str(
491                &value,
492                Frame {
493                    name: None,
494                    indent,
495                    insref: Some(&path),
496                    path: None,
497                    plainindex: false,
498                    level: 0,
499                },
500            ),
501        };
502        self.sref[idx] = format!("{path}{}={}{rhs}", self.space, self.space);
503    }
504
505    /// Assemble the final table text from the emitted element list.
506    fn assemble_table(
507        &self,
508        tab: &Table,
509        tag: &str,
510        indent: Option<&str>,
511        out: &[String],
512        level: usize,
513    ) -> String {
514        let prefix = indent.map_or(String::new(), |ind| ind.repeat(level));
515        let head = match indent {
516            Some(ind) => format!("{{\n{prefix}{ind}"),
517            None => "{".to_string(),
518        };
519        let sep = match indent {
520            Some(ind) => format!(",\n{prefix}{ind}"),
521            None => format!(",{}", self.space),
522        };
523        let body = out.join(&sep);
524        let tail = match indent {
525            Some(_) => format!("\n{prefix}}}"),
526            None => "}".to_string(),
527        };
528        let table_text = match &self.opts.custom {
529            Some(f) => f(tag, &head, &body, &tail, level),
530            None => format!("{tag}{head}{body}{tail}"),
531        };
532        let text = tab_tostring(tab);
533        format!("{table_text}{}", self.comment(&text, level))
534    }
535
536    /// Whether an element is skipped by the ignore filters or sparse-nil rule.
537    fn should_skip(&self, key: &Key, value: Option<&Value>, sparse: bool) -> bool {
538        if let Some(v) = value {
539            if let Some(ig) = &self.opts.valignore {
540                if let Some(id) = v.ident() {
541                    if ig.contains(&id) {
542                        return true;
543                    }
544                }
545            }
546            if let Some(ig) = &self.opts.valignore_scalar {
547                if ig.iter().any(|x| x == v) {
548                    return true;
549                }
550            }
551        }
552        if let Some(allow) = &self.opts.keyallow {
553            if !allow.iter().any(|k| k == key) {
554                return true;
555            }
556        }
557        if let Some(ignore) = &self.opts.keyignore {
558            if ignore.iter().any(|k| k == key) {
559                return true;
560            }
561        }
562        if let Some(tignore) = &self.opts.valtypeignore {
563            let tn = value.map_or("nil", type_name);
564            if tignore.contains(tn) {
565                return true;
566            }
567        }
568        if sparse && value.is_none() {
569            return true;
570        }
571        false
572    }
573
574    /// serpent's `alphanumsort`, or the custom sort when set.
575    fn sort_keys(&self, o: &mut Vec<Key>, entries: &[(Key, Value)]) {
576        if let Some(f) = &self.opts.sortfn {
577            // The closure reorders the keys directly and can read values from
578            // `entries`, so no value round-trip and no key remap is needed.
579            f(o, entries);
580            return;
581        }
582        // Default: sort by a computed string. A numeric key whose value is a
583        // positive integer landing on a filled slot of the ordered list ranks
584        // first. serpent tests `o[key] ~= nil`, which holds for any integer key
585        // in 1..#o, not only the array indices 1..maxn.
586        let len = o.len();
587        o.sort_by_key(|a| sortkey(a, len));
588    }
589}
590
591/// Format a table like Lua's default `tostring`: `table: 0x...`.
592fn tab_tostring(tab: &Table) -> String {
593    format!("table: 0x{:012x}", tab.id())
594}
595
596/// Lua-style default `tostring` for non-string values used in comments and
597/// fallbacks.
598fn tostring(v: &Value) -> String {
599    match v {
600        Value::Nil => "nil".to_string(),
601        Value::Bool(b) => b.to_string(),
602        Value::Number(n) => {
603            if n.is_nan() {
604                "nan".to_string()
605            } else if n.is_infinite() {
606                if *n > 0.0 {
607                    "inf".to_string()
608                } else {
609                    "-inf".to_string()
610                }
611            } else {
612                numfmt::format("%.14g", *n)
613            }
614        }
615        Value::Str(s) => String::from_utf8_lossy(s).into_owned(),
616        Value::Table(t) => tab_tostring(t),
617        Value::Function(f) => format!("function: 0x{:012x}", f.id),
618        Value::Global(g) => g.name.clone(),
619    }
620}
621
622/// The Lua type name of a value.
623fn type_name(v: &Value) -> &'static str {
624    match v {
625        Value::Nil => "nil",
626        Value::Bool(_) => "boolean",
627        Value::Number(_) => "number",
628        Value::Str(_) => "string",
629        Value::Table(_) => "table",
630        Value::Function(_) => "function",
631        Value::Global(_) => "userdata",
632    }
633}
634
635/// A valid Lua identifier: starts with a letter or underscore, then word chars.
636fn is_plain_ident(s: &str) -> bool {
637    let mut chars = s.chars();
638    match chars.next() {
639        Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
640        _ => return false,
641    }
642    chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
643}
644
645/// Whether a key is one of the array indices 1..maxn.
646fn is_array_index(k: &Key, maxn: usize) -> bool {
647    match k {
648        Key::Number(x) => {
649            let f = *x;
650            f.fract() == 0.0 && f >= 1.0 && f <= maxn as f64
651        }
652        _ => false,
653    }
654}
655
656/// A hashable projection of a key, used to index a table's entries for O(1)
657/// value lookup.
658///
659/// Numbers use their bit pattern after normalizing `-0.0` to `0.0`, which keeps
660/// the projection consistent with [`Key::same`] (where `-0.0 == 0.0`). A `NaN`
661/// key returns `None`, since Lua forbids `NaN` keys and [`Key::same`] never
662/// matches two `NaN` values.
663#[derive(PartialEq, Eq, Hash)]
664enum KeyId {
665    Bool(bool),
666    Number(u64),
667    Str(Vec<u8>),
668    Table(usize),
669    Function(usize),
670    Global(usize),
671}
672
673fn key_id(k: &Key) -> Option<KeyId> {
674    Some(match k {
675        Key::Bool(b) => KeyId::Bool(*b),
676        Key::Number(n) if n.is_nan() => return None,
677        Key::Number(n) => KeyId::Number(if *n == 0.0 { 0.0_f64 } else { *n }.to_bits()),
678        Key::Str(s) => KeyId::Str(s.clone()),
679        Key::Table(t) => KeyId::Table(t.id()),
680        Key::Function(f) => KeyId::Function(f.id),
681        Key::Global(g) => KeyId::Global(g.id),
682    })
683}
684
685/// Build serpent's sort string for a key.
686///
687/// `list_len` is the length of the ordered key list. A numeric key whose value
688/// is a positive integer at or below `list_len` gets the `"0"` prefix, matching
689/// serpent's `o[key] ~= nil` test over the ordered list. Every other key falls
690/// back to the type rank.
691fn sortkey(k: &Key, list_len: usize) -> String {
692    let on_filled_slot = match k {
693        Key::Number(x) => x.fract() == 0.0 && *x >= 1.0 && *x <= list_len as f64,
694        _ => false,
695    };
696    let prefix = if on_filled_slot {
697        "0"
698    } else {
699        match k {
700            Key::Number(_) => "a",
701            Key::Str(_) => "b",
702            _ => "z",
703        }
704    };
705    let body = pad_numbers(&key_tostring(k));
706    format!("{prefix}{body}")
707}
708
709/// serpent's `padnum`: zero-pad each digit run to 12 places.
710fn pad_numbers(s: &str) -> String {
711    let mut out = String::new();
712    let b = s.as_bytes();
713    let mut i = 0;
714    while i < b.len() {
715        if b[i].is_ascii_digit() {
716            let start = i;
717            while i < b.len() && b[i].is_ascii_digit() {
718                i += 1;
719            }
720            let num = &s[start..i];
721            let val: u64 = num.parse().unwrap_or(0);
722            out.push_str(&format!("{val:012}"));
723        } else {
724            out.push(b[i] as char);
725            i += 1;
726        }
727    }
728    out
729}
730
731/// `tostring` of a key for sort-string building.
732fn key_tostring(k: &Key) -> String {
733    tostring(&k.to_value())
734}
735
736/// Serialize a value graph with the given options.
737///
738/// This is the raw entry point. Returns the Lua source, or an error when
739/// `fatal` is set and a non-serializable value is reached.
740///
741/// # Errors
742///
743/// Returns [`SerError`] when `opts.fatal` is set and a value cannot be
744/// serialized as data.
745pub fn serialize(t: &Value, opts: &Options) -> Result<String, SerError> {
746    let mut s = Serializer::new(opts);
747    let name = opts.name.clone();
748    let indent = opts.indent.clone();
749    let sepr = match &indent {
750        Some(_) => "\n".to_string(),
751        None => format!(";{}", s.space),
752    };
753    let top_name = name.as_ref().map(|n| Key::Str(n.clone().into_bytes()));
754    let body = s.val2str(
755        t,
756        Frame {
757            name: top_name.as_ref(),
758            indent: indent.as_deref(),
759            insref: None,
760            path: None,
761            plainindex: false,
762            level: 0,
763        },
764    );
765
766    if let Some(value) = s.fatal_value.take() {
767        return Err(SerError::NotSerializable(value));
768    }
769
770    let tail = if s.sref.len() > 1 {
771        format!("{}{sepr}", s.sref.join(&sepr))
772    } else {
773        String::new()
774    };
775    let warn = if opts.comment.is_some() && s.sref.len() > 1 {
776        format!(
777            "{}--[[incomplete output with shared/self-references skipped]]",
778            s.space
779        )
780    } else {
781        String::new()
782    };
783
784    let result = match name {
785        None => format!("{body}{warn}"),
786        Some(n) => format!("do local {body}{sepr}{tail}return {n}{sepr}end"),
787    };
788    Ok(result)
789}
790
791/// A serialization error.
792#[derive(Debug, PartialEq, Eq)]
793pub enum SerError {
794    /// A value could not be serialized and `fatal` was set. Carries the value's
795    /// `tostring` text, so the message names the value that failed.
796    NotSerializable(String),
797}
798
799impl std::fmt::Display for SerError {
800    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
801        match self {
802            SerError::NotSerializable(value) => write!(f, "Can't serialize {value}"),
803        }
804    }
805}
806
807impl std::error::Error for SerError {}