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        if let Some((key, _)) = entries
351            .iter()
352            .find(|(key, _)| matches!(key, Key::Number(n) if n.is_nan()))
353        {
354            self.fatal_value = Some(tostring(&key.to_value()));
355            return String::new();
356        }
357        let border = tab.border();
358        let maxn = match self.opts.maxnum {
359            Some(m) => border.min(m),
360            None => border,
361        };
362
363        // Index the entries by key once for O(1) value lookup. Keys are unique
364        // under `Key::same`, so each projection maps to a single slot.
365        let index: HashMap<KeyId, usize> = entries
366            .iter()
367            .enumerate()
368            .filter_map(|(i, (k, _))| key_id(k).map(|id| (id, i)))
369            .collect();
370        let lookup = |key: &Key| -> Option<Value> {
371            key_id(key)
372                .and_then(|id| index.get(&id))
373                .map(|&i| entries[i].1.clone())
374        };
375
376        // Ordered key list: array indices 1..maxn first, then remaining keys.
377        let mut o: Vec<Key> = (1..=maxn).map(|i| Key::Number(i as f64)).collect();
378        if self.opts.maxnum.is_none() || o.len() < self.opts.maxnum.unwrap() {
379            for (k, _) in &entries {
380                if !is_array_index(k, maxn) {
381                    o.push(k.clone());
382                }
383            }
384        }
385        if let Some(m) = self.opts.maxnum {
386            if o.len() > m {
387                o.truncate(m);
388            }
389        }
390        if (self.opts.sortkeys || self.opts.sortfn.is_some()) && o.len() > maxn {
391            self.sort_keys(&mut o, &entries);
392        }
393        let sparse = self.opts.sparse && o.len() > maxn;
394
395        // Compute the table's seen-path once, not per element.
396        let seen_t = self.seen.get(&Ident::Table(tab.id())).cloned();
397
398        let mut out: Vec<String> = Vec::new();
399        for (n, key) in o.iter().enumerate() {
400            let idx = n + 1;
401            let value = lookup(key);
402            let plainindex = idx <= maxn && !sparse;
403
404            if self.should_skip(key, value.as_ref(), sparse) {
405                continue;
406            }
407
408            let is_complex = matches!(key, Key::Table(_) | Key::Function(_) | Key::Global(_));
409            if is_complex {
410                self.emit_complex_key(tab, key, value, indent);
411            } else {
412                let v = value.unwrap_or(Value::Nil);
413                let text = self.val2str(
414                    &v,
415                    Frame {
416                        name: Some(key),
417                        indent,
418                        insref: None,
419                        path: seen_t.as_deref(),
420                        plainindex,
421                        level: level + 1,
422                    },
423                );
424                let len = text.len() as i64;
425                out.push(text);
426                if let Some(ml) = self.maxlen.as_mut() {
427                    *ml -= len;
428                    if *ml < 0 {
429                        break;
430                    }
431                }
432            }
433        }
434
435        self.assemble_table(tab, tag, indent, &out, level)
436    }
437
438    /// A complex key (table/function/global). Emit its assignment into the
439    /// self-reference section, declaring a helper local first if needed.
440    fn emit_complex_key(
441        &mut self,
442        tab: &Table,
443        key: &Key,
444        value: Option<Value>,
445        indent: Option<&str>,
446    ) {
447        let key_val = key.to_value();
448        let key_id = key_val.ident();
449        let key_seen = key_id.as_ref().and_then(|i| self.seen.get(i)).cloned();
450        let key_global = match key {
451            Key::Global(g) => Some(g.name.clone()),
452            _ => None,
453        };
454
455        if key_seen.is_none() && key_global.is_none() {
456            self.sref.push("placeholder".to_string());
457            let idx = self.sref.len() - 1;
458            let sym = self.gensym(&tostring(&key_val));
459            let iname = self.iname.clone();
460            let (sname, _) = self.safename(Some(&iname), Some(&Key::Str(sym.into_bytes())));
461            let stmt = self.val2str(
462                &key_val,
463                Frame {
464                    name: Some(&Key::Str(sname.clone().into_bytes())),
465                    indent,
466                    insref: Some(&sname),
467                    path: Some(&iname),
468                    plainindex: true,
469                    level: 0,
470                },
471            );
472            self.sref[idx] = stmt;
473        }
474
475        self.sref.push("placeholder".to_string());
476        let idx = self.sref.len() - 1;
477        let seen_t = self
478            .seen
479            .get(&Ident::Table(tab.id()))
480            .cloned()
481            .unwrap_or_default();
482        // Recompute seen[key] here: the helper serialization above may have just
483        // registered the key at its helper-table path, matching serpent's
484        // `seen[key] or globals[key] or gensym(key)` at LHS-building time.
485        let key_seen_now = key_id.as_ref().and_then(|i| self.seen.get(i)).cloned();
486        let key_ref = key_seen_now
487            .or(key_global)
488            .unwrap_or_else(|| self.gensym(&tostring(&key_val)));
489        let path = format!("{seen_t}[{key_ref}]");
490        let value = value.unwrap_or(Value::Nil);
491        let val_seen = value.ident().and_then(|i| self.seen.get(&i)).cloned();
492        let rhs = match val_seen {
493            Some(p) => p,
494            // serpent calls val2str(value, nil, indent, path) so `insref` is the
495            // path and the fifth path argument is nil. That makes the value
496            // register at `path`, not at `path[""]`.
497            None => self.val2str(
498                &value,
499                Frame {
500                    name: None,
501                    indent,
502                    insref: Some(&path),
503                    path: None,
504                    plainindex: false,
505                    level: 0,
506                },
507            ),
508        };
509        self.sref[idx] = format!("{path}{}={}{rhs}", self.space, self.space);
510    }
511
512    /// Assemble the final table text from the emitted element list.
513    fn assemble_table(
514        &self,
515        tab: &Table,
516        tag: &str,
517        indent: Option<&str>,
518        out: &[String],
519        level: usize,
520    ) -> String {
521        let prefix = indent.map_or(String::new(), |ind| ind.repeat(level));
522        let head = match indent {
523            Some(ind) => format!("{{\n{prefix}{ind}"),
524            None => "{".to_string(),
525        };
526        let sep = match indent {
527            Some(ind) => format!(",\n{prefix}{ind}"),
528            None => format!(",{}", self.space),
529        };
530        let body = out.join(&sep);
531        let tail = match indent {
532            Some(_) => format!("\n{prefix}}}"),
533            None => "}".to_string(),
534        };
535        let table_text = match &self.opts.custom {
536            Some(f) => f(tag, &head, &body, &tail, level),
537            None => format!("{tag}{head}{body}{tail}"),
538        };
539        let text = tab_tostring(tab);
540        format!("{table_text}{}", self.comment(&text, level))
541    }
542
543    /// Whether an element is skipped by the ignore filters or sparse-nil rule.
544    fn should_skip(&self, key: &Key, value: Option<&Value>, sparse: bool) -> bool {
545        if let Some(v) = value {
546            if let Some(ig) = &self.opts.valignore {
547                if let Some(id) = v.ident() {
548                    if ig.contains(&id) {
549                        return true;
550                    }
551                }
552            }
553            if let Some(ig) = &self.opts.valignore_scalar {
554                if ig.iter().any(|x| x == v) {
555                    return true;
556                }
557            }
558        }
559        if let Some(allow) = &self.opts.keyallow {
560            if !allow.iter().any(|k| k == key) {
561                return true;
562            }
563        }
564        if let Some(ignore) = &self.opts.keyignore {
565            if ignore.iter().any(|k| k == key) {
566                return true;
567            }
568        }
569        if let Some(tignore) = &self.opts.valtypeignore {
570            let tn = value.map_or("nil", type_name);
571            if tignore.contains(tn) {
572                return true;
573            }
574        }
575        if sparse && value.is_none() {
576            return true;
577        }
578        false
579    }
580
581    /// serpent's `alphanumsort`, or the custom sort when set.
582    fn sort_keys(&self, o: &mut Vec<Key>, entries: &[(Key, Value)]) {
583        if let Some(f) = &self.opts.sortfn {
584            // The closure reorders the keys directly and can read values from
585            // `entries`, so no value round-trip and no key remap is needed.
586            f(o, entries);
587            return;
588        }
589        // Default: sort by a computed string. A numeric key whose value is a
590        // positive integer landing on a filled slot of the ordered list ranks
591        // first. serpent tests `o[key] ~= nil`, which holds for any integer key
592        // in 1..#o, not only the array indices 1..maxn.
593        let len = o.len();
594        o.sort_by_key(|a| sortkey(a, len));
595    }
596}
597
598/// Format a table like Lua's default `tostring`: `table: 0x...`.
599fn tab_tostring(tab: &Table) -> String {
600    format!("table: 0x{:012x}", tab.id())
601}
602
603/// Lua-style default `tostring` for non-string values used in comments and
604/// fallbacks.
605fn tostring(v: &Value) -> String {
606    match v {
607        Value::Nil => "nil".to_string(),
608        Value::Bool(b) => b.to_string(),
609        Value::Number(n) => {
610            if n.is_nan() {
611                "nan".to_string()
612            } else if n.is_infinite() {
613                if *n > 0.0 {
614                    "inf".to_string()
615                } else {
616                    "-inf".to_string()
617                }
618            } else {
619                numfmt::format("%.14g", *n)
620            }
621        }
622        Value::Str(s) => String::from_utf8_lossy(s).into_owned(),
623        Value::Table(t) => tab_tostring(t),
624        Value::Function(f) => format!("function: 0x{:012x}", f.id),
625        Value::Global(g) => g.name.clone(),
626    }
627}
628
629/// The Lua type name of a value.
630fn type_name(v: &Value) -> &'static str {
631    match v {
632        Value::Nil => "nil",
633        Value::Bool(_) => "boolean",
634        Value::Number(_) => "number",
635        Value::Str(_) => "string",
636        Value::Table(_) => "table",
637        Value::Function(_) => "function",
638        Value::Global(_) => "userdata",
639    }
640}
641
642/// A valid Lua identifier: starts with a letter or underscore, then word chars.
643fn is_plain_ident(s: &str) -> bool {
644    let mut chars = s.chars();
645    match chars.next() {
646        Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
647        _ => return false,
648    }
649    chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
650}
651
652/// Whether a key is one of the array indices 1..maxn.
653fn is_array_index(k: &Key, maxn: usize) -> bool {
654    match k {
655        Key::Number(x) => {
656            let f = *x;
657            f.fract() == 0.0 && f >= 1.0 && f <= maxn as f64
658        }
659        _ => false,
660    }
661}
662
663/// A hashable projection of a key, used to index a table's entries for O(1)
664/// value lookup.
665///
666/// Numbers use their bit pattern after normalizing `-0.0` to `0.0`, which keeps
667/// the projection consistent with [`Key::same`] (where `-0.0 == 0.0`). A `NaN`
668/// key returns `None`, since Lua forbids `NaN` keys and [`Key::same`] never
669/// matches two `NaN` values.
670#[derive(PartialEq, Eq, Hash)]
671enum KeyId {
672    Bool(bool),
673    Number(u64),
674    Str(Vec<u8>),
675    Table(usize),
676    Function(usize),
677    Global(usize),
678}
679
680fn key_id(k: &Key) -> Option<KeyId> {
681    Some(match k {
682        Key::Bool(b) => KeyId::Bool(*b),
683        Key::Number(n) if n.is_nan() => return None,
684        Key::Number(n) => KeyId::Number(if *n == 0.0 { 0.0_f64 } else { *n }.to_bits()),
685        Key::Str(s) => KeyId::Str(s.clone()),
686        Key::Table(t) => KeyId::Table(t.id()),
687        Key::Function(f) => KeyId::Function(f.id),
688        Key::Global(g) => KeyId::Global(g.id),
689    })
690}
691
692/// Build serpent's sort string for a key.
693///
694/// `list_len` is the length of the ordered key list. A numeric key whose value
695/// is a positive integer at or below `list_len` gets the `"0"` prefix, matching
696/// serpent's `o[key] ~= nil` test over the ordered list. Every other key falls
697/// back to the type rank.
698fn sortkey(k: &Key, list_len: usize) -> String {
699    let on_filled_slot = match k {
700        Key::Number(x) => x.fract() == 0.0 && *x >= 1.0 && *x <= list_len as f64,
701        _ => false,
702    };
703    let prefix = if on_filled_slot {
704        "0"
705    } else {
706        match k {
707            Key::Number(_) => "a",
708            Key::Str(_) => "b",
709            _ => "z",
710        }
711    };
712    let body = pad_numbers(&key_tostring(k));
713    format!("{prefix}{body}")
714}
715
716/// serpent's `padnum`: zero-pad each digit run to 12 places.
717fn pad_numbers(s: &str) -> String {
718    let mut out = String::new();
719    let b = s.as_bytes();
720    let mut i = 0;
721    while i < b.len() {
722        if b[i].is_ascii_digit() {
723            let start = i;
724            while i < b.len() && b[i].is_ascii_digit() {
725                i += 1;
726            }
727            let num = &s[start..i];
728            let val: u64 = num.parse().unwrap_or(0);
729            out.push_str(&format!("{val:012}"));
730        } else {
731            out.push(b[i] as char);
732            i += 1;
733        }
734    }
735    out
736}
737
738/// `tostring` of a key for sort-string building.
739fn key_tostring(k: &Key) -> String {
740    tostring(&k.to_value())
741}
742
743/// Serialize a value graph with the given options.
744///
745/// This is the raw entry point. Returns the Lua source, or an error when
746/// `fatal` is set and a non-serializable value is reached.
747///
748/// # Errors
749///
750/// Returns [`SerError`] when `opts.fatal` is set and a value cannot be
751/// serialized as data.
752pub fn serialize(t: &Value, opts: &Options) -> Result<String, SerError> {
753    let mut s = Serializer::new(opts);
754    let name = opts.name.clone();
755    let indent = opts.indent.clone();
756    let sepr = match &indent {
757        Some(_) => "\n".to_string(),
758        None => format!(";{}", s.space),
759    };
760    let top_name = name.as_ref().map(|n| Key::Str(n.clone().into_bytes()));
761    let body = s.val2str(
762        t,
763        Frame {
764            name: top_name.as_ref(),
765            indent: indent.as_deref(),
766            insref: None,
767            path: None,
768            plainindex: false,
769            level: 0,
770        },
771    );
772
773    if let Some(value) = s.fatal_value.take() {
774        return Err(SerError::NotSerializable(value));
775    }
776
777    let tail = if s.sref.len() > 1 {
778        format!("{}{sepr}", s.sref.join(&sepr))
779    } else {
780        String::new()
781    };
782    let warn = if opts.comment.is_some() && s.sref.len() > 1 {
783        format!(
784            "{}--[[incomplete output with shared/self-references skipped]]",
785            s.space
786        )
787    } else {
788        String::new()
789    };
790
791    let result = match name {
792        None => format!("{body}{warn}"),
793        Some(n) => format!("do local {body}{sepr}{tail}return {n}{sepr}end"),
794    };
795    Ok(result)
796}
797
798/// A serialization error.
799#[derive(Debug, PartialEq, Eq)]
800pub enum SerError {
801    /// A value could not be serialized and `fatal` was set. Carries the value's
802    /// `tostring` text, so the message names the value that failed.
803    NotSerializable(String),
804}
805
806impl std::fmt::Display for SerError {
807    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
808        match self {
809            SerError::NotSerializable(value) => write!(f, "Can't serialize {value}"),
810        }
811    }
812}
813
814impl std::error::Error for SerError {}
815
816#[cfg(test)]
817mod tests {
818    use super::SerError;
819    use crate::value::{Key, Table, Value};
820    use crate::{dump, line};
821
822    #[test]
823    fn nan_table_keys_return_error() {
824        let table = Table::new();
825        table.set(Key::Number(f64::NAN), Value::Number(7.0));
826
827        assert_eq!(
828            line(&Value::Table(table.clone())).unwrap_err(),
829            SerError::NotSerializable("nan".to_string())
830        );
831        assert_eq!(
832            dump(&Value::Table(table)).unwrap_err(),
833            SerError::NotSerializable("nan".to_string())
834        );
835    }
836}