Skip to main content

serpent_serializer/
value.rs

1//! The Lua value model.
2//!
3//! [`Value`] mirrors the Lua types serpent can serialize: nil, booleans,
4//! numbers, strings, tables, functions, and named globals. Tables use
5//! reference-counted interior mutability so shared and cyclic graphs keep their
6//! identity across a serialize and deserialize round trip.
7
8use std::cell::RefCell;
9use std::fmt;
10use std::rc::Rc;
11
12/// A Lua value.
13///
14/// Numbers are `f64`, matching Lua's default number type. Strings hold raw
15/// bytes because Lua strings are byte sequences and may contain NUL or other
16/// control characters. Tables are shared handles so two references to the same
17/// table compare equal by identity.
18///
19/// Equality follows Lua. Scalars compare by value. `NaN` is not equal to itself.
20/// Tables, functions, and globals compare by identity, not by contents.
21#[derive(Clone, Debug, PartialEq)]
22pub enum Value {
23    /// Lua `nil`.
24    Nil,
25    /// Lua boolean.
26    Bool(bool),
27    /// Lua number. Serialized with `numformat`, or as `1/0`, `-1/0`, `0/0` for
28    /// the special values.
29    Number(f64),
30    /// Lua string, stored as raw bytes.
31    Str(Vec<u8>),
32    /// Lua table with an array part and a hash part.
33    Table(Table),
34    /// A Lua function. Held opaquely by an identity id. The serializer emits a
35    /// bytecode-reload stub or, under `nocode`, an empty body.
36    Function(Func),
37    /// A value known by a global name, such as `print` or `io.stdin`. The
38    /// serializer emits the name verbatim.
39    Global(Global),
40}
41
42/// A shared table handle.
43///
44/// Cloning a `Table` shares the same underlying storage, so identity is
45/// preserved. Use [`Table::new`] to build one and the insertion helpers to fill
46/// it.
47///
48/// Equality is by identity. Two `Table` handles are equal when they point at the
49/// same storage, matching Lua reference semantics. Contents are not compared.
50///
51/// The storage is private. Read contents through [`Table::get`],
52/// [`Table::entries`], or [`Table::with_entries`], and write through [`push`],
53/// [`set`], and the meta setters.
54///
55/// [`push`]: Table::push
56/// [`set`]: Table::set
57#[derive(Clone)]
58pub struct Table(Rc<RefCell<TableData>>);
59
60impl PartialEq for Table {
61    fn eq(&self, other: &Self) -> bool {
62        self.id() == other.id()
63    }
64}
65
66thread_local! {
67    static DEBUG_TABLES: RefCell<Vec<usize>> = const { RefCell::new(Vec::new()) };
68}
69
70struct CycleDebug;
71
72impl fmt::Debug for CycleDebug {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        f.write_str("<cycle>")
75    }
76}
77
78impl fmt::Debug for Table {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        let id = self.id();
81        DEBUG_TABLES.with(|tables| {
82            {
83                let tables = tables.borrow();
84                if tables.contains(&id) {
85                    return f.debug_tuple("Table").field(&CycleDebug).finish();
86                }
87            }
88
89            tables.borrow_mut().push(id);
90            let result = f.debug_tuple("Table").field(&self.0).finish();
91            tables.borrow_mut().pop();
92            result
93        })
94    }
95}
96
97/// A table key. Only hashable Lua values can be keys.
98///
99/// Numbers, booleans, and strings hash by value. Tables and functions hash by
100/// identity, matching Lua reference semantics.
101///
102/// Equality follows the same split. Scalar keys compare by value, reference keys
103/// by identity. [`Key::same`] is the same relation expressed as a method.
104#[derive(Clone, Debug, PartialEq)]
105pub enum Key {
106    /// Boolean key.
107    Bool(bool),
108    /// Numeric key.
109    Number(f64),
110    /// String key.
111    Str(Vec<u8>),
112    /// Table key, tracked by identity.
113    Table(Table),
114    /// Function key, tracked by identity.
115    Function(Func),
116    /// Global-named key such as `io.stdin`.
117    Global(Global),
118}
119
120/// A metamethod. Returns the replacement value, or a [`MetaError`] to model a
121/// metamethod that raises. The serializer treats an error as "the metamethod did
122/// not apply" and serializes the table as itself.
123pub type MetaFn = Rc<dyn Fn() -> Result<Value, MetaError>>;
124
125/// An error raised by a `__serialize` or `__tostring` metamethod.
126///
127/// Carries the message the metamethod would raise with. The serializer ignores
128/// the error and falls back to serializing the table directly, but the message
129/// is available to callers that inspect it.
130#[derive(Clone, Debug, PartialEq, Eq)]
131pub struct MetaError {
132    /// The error message.
133    pub message: String,
134}
135
136impl MetaError {
137    /// Build a metamethod error with the given message.
138    #[must_use]
139    pub fn new(message: impl Into<String>) -> Self {
140        MetaError {
141            message: message.into(),
142        }
143    }
144}
145
146impl std::fmt::Display for MetaError {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        write!(f, "metamethod error: {}", self.message)
149    }
150}
151
152impl std::error::Error for MetaError {}
153
154/// The backing store for a table.
155///
156/// `entries` keeps insertion order for hash keys, which stands in for Lua's raw
157/// `pairs` order. The optional metamethods model `__serialize` and `__tostring`.
158#[derive(Default)]
159pub struct TableData {
160    /// Ordered key/value pairs, including integer keys.
161    pub entries: Vec<(Key, Value)>,
162    /// `__serialize` metamethod. When present and successful, its result
163    /// replaces the table during serialization.
164    pub serialize_meta: Option<MetaFn>,
165    /// `__tostring` metamethod. Used when `__serialize` is absent, gated by the
166    /// `metatostring` option.
167    pub tostring_meta: Option<MetaFn>,
168}
169
170impl std::fmt::Debug for TableData {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        f.debug_struct("TableData")
173            .field("entries", &self.entries)
174            .field("has_serialize_meta", &self.serialize_meta.is_some())
175            .field("has_tostring_meta", &self.tostring_meta.is_some())
176            .finish()
177    }
178}
179
180/// An opaque Lua function.
181///
182/// `id` gives identity for shared-reference tracking. `bytecode`, when present,
183/// is the `string.dump` output the serializer quotes into a reload stub.
184///
185/// Equality is by `id`. Two functions with the same identity are equal, matching
186/// Lua reference semantics. The bytecode is not compared.
187#[derive(Clone, Debug)]
188pub struct Func {
189    /// Identity of this function.
190    pub id: usize,
191    /// Optional dumped bytecode. `None` means the function cannot be dumped and
192    /// falls back to a global name or placeholder.
193    pub bytecode: Option<Vec<u8>>,
194}
195
196impl PartialEq for Func {
197    fn eq(&self, other: &Self) -> bool {
198        self.id == other.id
199    }
200}
201
202/// A value referenced by a global name.
203///
204/// `name` is the Lua expression that reaches it, such as `print` or `io.stdin`.
205/// `id` gives identity so two references to the same global stay shared.
206///
207/// Equality is by `id`. Two globals with the same identity are equal, matching
208/// Lua reference semantics. The name is not compared.
209#[derive(Clone, Debug)]
210pub struct Global {
211    /// The global name to emit.
212    pub name: String,
213    /// Identity of this global object.
214    pub id: usize,
215}
216
217impl PartialEq for Global {
218    fn eq(&self, other: &Self) -> bool {
219        self.id == other.id
220    }
221}
222
223impl Table {
224    /// Create an empty table.
225    #[must_use]
226    pub fn new() -> Self {
227        Table(Rc::new(RefCell::new(TableData::default())))
228    }
229
230    /// Append a value to the array part at the next integer index.
231    pub fn push(&self, v: Value) {
232        let n = self.array_len_raw() + 1;
233        self.set(Key::Number(n as f64), v);
234    }
235
236    /// Set `key` to `value`. Replaces an existing entry with an equal key.
237    pub fn set(&self, key: Key, value: Value) {
238        let mut d = self.0.borrow_mut();
239        if let Some(slot) = d.entries.iter_mut().find(|(k, _)| k.same(&key)) {
240            slot.1 = value;
241        } else {
242            d.entries.push((key, value));
243        }
244    }
245
246    /// Number of contiguous integer keys starting at 1.
247    fn array_len_raw(&self) -> usize {
248        let d = self.0.borrow();
249        let mut n = 0usize;
250        loop {
251            let want = (n + 1) as f64;
252            if d.entries.iter().any(|(k, _)| match k {
253                Key::Number(x) => *x == want,
254                _ => false,
255            }) {
256                n += 1;
257            } else {
258                return n;
259            }
260        }
261    }
262
263    /// The Lua `#` length: a border of the array part. This model uses the
264    /// contiguous integer prefix from 1.
265    #[must_use]
266    pub fn border(&self) -> usize {
267        self.array_len_raw()
268    }
269
270    /// True when the table has no entries.
271    #[must_use]
272    pub fn is_empty(&self) -> bool {
273        self.0.borrow().entries.is_empty()
274    }
275
276    /// Raw pointer used for identity comparison and hashing.
277    #[must_use]
278    pub fn id(&self) -> usize {
279        Rc::as_ptr(&self.0) as usize
280    }
281
282    /// Set the `__serialize` metamethod.
283    pub fn set_serialize_meta(&self, f: MetaFn) {
284        self.0.borrow_mut().serialize_meta = Some(f);
285    }
286
287    /// Set the `__tostring` metamethod.
288    pub fn set_tostring_meta(&self, f: MetaFn) {
289        self.0.borrow_mut().tostring_meta = Some(f);
290    }
291
292    /// The `__serialize` metamethod, if set.
293    #[must_use]
294    pub fn serialize_meta(&self) -> Option<MetaFn> {
295        self.0.borrow().serialize_meta.clone()
296    }
297
298    /// The `__tostring` metamethod, if set.
299    #[must_use]
300    pub fn tostring_meta(&self) -> Option<MetaFn> {
301        self.0.borrow().tostring_meta.clone()
302    }
303
304    /// The value for `key`, if present. Compares keys with [`Key::same`].
305    #[must_use]
306    pub fn get(&self, key: &Key) -> Option<Value> {
307        self.0
308            .borrow()
309            .entries
310            .iter()
311            .find(|(k, _)| k.same(key))
312            .map(|(_, v)| v.clone())
313    }
314
315    /// A snapshot of the key/value pairs in insertion order.
316    ///
317    /// This clones the entries. For a read-only pass without a clone, use
318    /// [`Table::with_entries`].
319    #[must_use]
320    pub fn entries(&self) -> Vec<(Key, Value)> {
321        self.0.borrow().entries.clone()
322    }
323
324    /// Run `f` over a borrow of the key/value pairs and return its result.
325    ///
326    /// This avoids cloning the entries when a read pass is enough. The borrow is
327    /// held for the duration of `f`, so `f` must not call back into methods that
328    /// borrow the same table mutably.
329    pub fn with_entries<R>(&self, f: impl FnOnce(&[(Key, Value)]) -> R) -> R {
330        f(&self.0.borrow().entries)
331    }
332}
333
334impl Default for Table {
335    fn default() -> Self {
336        Table::new()
337    }
338}
339
340impl Key {
341    /// Identity or value equality used to dedupe keys within one table.
342    ///
343    /// Equivalent to `self == other`. Kept as a named method for call sites that
344    /// read better with it.
345    #[must_use]
346    pub fn same(&self, other: &Key) -> bool {
347        self == other
348    }
349
350    /// Convert this key back to a value for serialization.
351    #[must_use]
352    pub fn to_value(&self) -> Value {
353        match self {
354            Key::Bool(b) => Value::Bool(*b),
355            Key::Number(n) => Value::Number(*n),
356            Key::Str(s) => Value::Str(s.clone()),
357            Key::Table(t) => Value::Table(t.clone()),
358            Key::Function(f) => Value::Function(f.clone()),
359            Key::Global(g) => Value::Global(g.clone()),
360        }
361    }
362}
363
364/// A hashable identity for any value, used to track shared references.
365///
366/// Scalars hash by value. Tables, functions, and globals hash by identity.
367#[derive(Clone, PartialEq, Eq, Hash, Debug)]
368pub enum Ident {
369    /// Table identity.
370    Table(usize),
371    /// Function identity.
372    Function(usize),
373    /// Global identity.
374    Global(usize),
375}
376
377impl Value {
378    /// The identity of a reference value, if it has one.
379    ///
380    /// Only tables, functions, and globals participate in shared-reference
381    /// tracking. Scalars return `None`.
382    #[must_use]
383    pub fn ident(&self) -> Option<Ident> {
384        match self {
385            Value::Table(t) => Some(Ident::Table(t.id())),
386            Value::Function(f) => Some(Ident::Function(f.id)),
387            Value::Global(g) => Some(Ident::Global(g.id)),
388            _ => None,
389        }
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::{Key, Table, Value};
396    use std::process::Command;
397
398    #[test]
399    fn table_debug_handles_cycles() {
400        if std::env::var_os("SERPENT_DEBUG_CYCLE_CHILD").is_some() {
401            let table = Table::new();
402            table.set(Key::Str(b"self".to_vec()), Value::Table(table.clone()));
403            let output = format!("{:?}", Value::Table(table));
404            assert!(output.contains("<cycle>"), "{output}");
405            assert_eq!(format!("{:?}", Value::Number(7.0)), "Number(7.0)");
406            return;
407        }
408
409        let exe = std::env::current_exe().unwrap();
410        let status = Command::new(exe)
411            .env("SERPENT_DEBUG_CYCLE_CHILD", "1")
412            .arg("--exact")
413            .arg("value::tests::table_debug_handles_cycles")
414            .arg("--nocapture")
415            .status()
416            .unwrap();
417
418        assert!(status.success(), "debug child failed with {status}");
419    }
420}