serpent-serializer 0.1.0

Serialize Lua values to round-trippable Lua source with cycle and shared-reference handling
Documentation
//! The Lua value model.
//!
//! [`Value`] mirrors the Lua types serpent can serialize: nil, booleans,
//! numbers, strings, tables, functions, and named globals. Tables use
//! reference-counted interior mutability so shared and cyclic graphs keep their
//! identity across a serialize and deserialize round trip.

use std::cell::RefCell;
use std::rc::Rc;

/// A Lua value.
///
/// Numbers are `f64`, matching Lua's default number type. Strings hold raw
/// bytes because Lua strings are byte sequences and may contain NUL or other
/// control characters. Tables are shared handles so two references to the same
/// table compare equal by identity.
///
/// Equality follows Lua. Scalars compare by value. `NaN` is not equal to itself.
/// Tables, functions, and globals compare by identity, not by contents.
#[derive(Clone, Debug, PartialEq)]
pub enum Value {
    /// Lua `nil`.
    Nil,
    /// Lua boolean.
    Bool(bool),
    /// Lua number. Serialized with `numformat`, or as `1/0`, `-1/0`, `0/0` for
    /// the special values.
    Number(f64),
    /// Lua string, stored as raw bytes.
    Str(Vec<u8>),
    /// Lua table with an array part and a hash part.
    Table(Table),
    /// A Lua function. Held opaquely by an identity id. The serializer emits a
    /// bytecode-reload stub or, under `nocode`, an empty body.
    Function(Func),
    /// A value known by a global name, such as `print` or `io.stdin`. The
    /// serializer emits the name verbatim.
    Global(Global),
}

/// A shared table handle.
///
/// Cloning a `Table` shares the same underlying storage, so identity is
/// preserved. Use [`Table::new`] to build one and the insertion helpers to fill
/// it.
///
/// Equality is by identity. Two `Table` handles are equal when they point at the
/// same storage, matching Lua reference semantics. Contents are not compared.
///
/// The storage is private. Read contents through [`Table::get`],
/// [`Table::entries`], or [`Table::with_entries`], and write through [`push`],
/// [`set`], and the meta setters.
///
/// [`push`]: Table::push
/// [`set`]: Table::set
#[derive(Clone, Debug)]
pub struct Table(Rc<RefCell<TableData>>);

impl PartialEq for Table {
    fn eq(&self, other: &Self) -> bool {
        self.id() == other.id()
    }
}

/// A table key. Only hashable Lua values can be keys.
///
/// Numbers, booleans, and strings hash by value. Tables and functions hash by
/// identity, matching Lua reference semantics.
///
/// Equality follows the same split. Scalar keys compare by value, reference keys
/// by identity. [`Key::same`] is the same relation expressed as a method.
#[derive(Clone, Debug, PartialEq)]
pub enum Key {
    /// Boolean key.
    Bool(bool),
    /// Numeric key.
    Number(f64),
    /// String key.
    Str(Vec<u8>),
    /// Table key, tracked by identity.
    Table(Table),
    /// Function key, tracked by identity.
    Function(Func),
    /// Global-named key such as `io.stdin`.
    Global(Global),
}

/// A metamethod. Returns the replacement value, or a [`MetaError`] to model a
/// metamethod that raises. The serializer treats an error as "the metamethod did
/// not apply" and serializes the table as itself.
pub type MetaFn = Rc<dyn Fn() -> Result<Value, MetaError>>;

/// An error raised by a `__serialize` or `__tostring` metamethod.
///
/// Carries the message the metamethod would raise with. The serializer ignores
/// the error and falls back to serializing the table directly, but the message
/// is available to callers that inspect it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MetaError {
    /// The error message.
    pub message: String,
}

impl MetaError {
    /// Build a metamethod error with the given message.
    #[must_use]
    pub fn new(message: impl Into<String>) -> Self {
        MetaError {
            message: message.into(),
        }
    }
}

impl std::fmt::Display for MetaError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "metamethod error: {}", self.message)
    }
}

impl std::error::Error for MetaError {}

/// The backing store for a table.
///
/// `entries` keeps insertion order for hash keys, which stands in for Lua's raw
/// `pairs` order. The optional metamethods model `__serialize` and `__tostring`.
#[derive(Default)]
pub struct TableData {
    /// Ordered key/value pairs, including integer keys.
    pub entries: Vec<(Key, Value)>,
    /// `__serialize` metamethod. When present and successful, its result
    /// replaces the table during serialization.
    pub serialize_meta: Option<MetaFn>,
    /// `__tostring` metamethod. Used when `__serialize` is absent, gated by the
    /// `metatostring` option.
    pub tostring_meta: Option<MetaFn>,
}

impl std::fmt::Debug for TableData {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("TableData")
            .field("entries", &self.entries)
            .field("has_serialize_meta", &self.serialize_meta.is_some())
            .field("has_tostring_meta", &self.tostring_meta.is_some())
            .finish()
    }
}

/// An opaque Lua function.
///
/// `id` gives identity for shared-reference tracking. `bytecode`, when present,
/// is the `string.dump` output the serializer quotes into a reload stub.
///
/// Equality is by `id`. Two functions with the same identity are equal, matching
/// Lua reference semantics. The bytecode is not compared.
#[derive(Clone, Debug)]
pub struct Func {
    /// Identity of this function.
    pub id: usize,
    /// Optional dumped bytecode. `None` means the function cannot be dumped and
    /// falls back to a global name or placeholder.
    pub bytecode: Option<Vec<u8>>,
}

impl PartialEq for Func {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

/// A value referenced by a global name.
///
/// `name` is the Lua expression that reaches it, such as `print` or `io.stdin`.
/// `id` gives identity so two references to the same global stay shared.
///
/// Equality is by `id`. Two globals with the same identity are equal, matching
/// Lua reference semantics. The name is not compared.
#[derive(Clone, Debug)]
pub struct Global {
    /// The global name to emit.
    pub name: String,
    /// Identity of this global object.
    pub id: usize,
}

impl PartialEq for Global {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
    }
}

impl Table {
    /// Create an empty table.
    #[must_use]
    pub fn new() -> Self {
        Table(Rc::new(RefCell::new(TableData::default())))
    }

    /// Append a value to the array part at the next integer index.
    pub fn push(&self, v: Value) {
        let n = self.array_len_raw() + 1;
        self.set(Key::Number(n as f64), v);
    }

    /// Set `key` to `value`. Replaces an existing entry with an equal key.
    pub fn set(&self, key: Key, value: Value) {
        let mut d = self.0.borrow_mut();
        if let Some(slot) = d.entries.iter_mut().find(|(k, _)| k.same(&key)) {
            slot.1 = value;
        } else {
            d.entries.push((key, value));
        }
    }

    /// Number of contiguous integer keys starting at 1.
    fn array_len_raw(&self) -> usize {
        let d = self.0.borrow();
        let mut n = 0usize;
        loop {
            let want = (n + 1) as f64;
            if d.entries.iter().any(|(k, _)| match k {
                Key::Number(x) => *x == want,
                _ => false,
            }) {
                n += 1;
            } else {
                return n;
            }
        }
    }

    /// The Lua `#` length: a border of the array part. This model uses the
    /// contiguous integer prefix from 1.
    #[must_use]
    pub fn border(&self) -> usize {
        self.array_len_raw()
    }

    /// True when the table has no entries.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.0.borrow().entries.is_empty()
    }

    /// Raw pointer used for identity comparison and hashing.
    #[must_use]
    pub fn id(&self) -> usize {
        Rc::as_ptr(&self.0) as usize
    }

    /// Set the `__serialize` metamethod.
    pub fn set_serialize_meta(&self, f: MetaFn) {
        self.0.borrow_mut().serialize_meta = Some(f);
    }

    /// Set the `__tostring` metamethod.
    pub fn set_tostring_meta(&self, f: MetaFn) {
        self.0.borrow_mut().tostring_meta = Some(f);
    }

    /// The `__serialize` metamethod, if set.
    #[must_use]
    pub fn serialize_meta(&self) -> Option<MetaFn> {
        self.0.borrow().serialize_meta.clone()
    }

    /// The `__tostring` metamethod, if set.
    #[must_use]
    pub fn tostring_meta(&self) -> Option<MetaFn> {
        self.0.borrow().tostring_meta.clone()
    }

    /// The value for `key`, if present. Compares keys with [`Key::same`].
    #[must_use]
    pub fn get(&self, key: &Key) -> Option<Value> {
        self.0
            .borrow()
            .entries
            .iter()
            .find(|(k, _)| k.same(key))
            .map(|(_, v)| v.clone())
    }

    /// A snapshot of the key/value pairs in insertion order.
    ///
    /// This clones the entries. For a read-only pass without a clone, use
    /// [`Table::with_entries`].
    #[must_use]
    pub fn entries(&self) -> Vec<(Key, Value)> {
        self.0.borrow().entries.clone()
    }

    /// Run `f` over a borrow of the key/value pairs and return its result.
    ///
    /// This avoids cloning the entries when a read pass is enough. The borrow is
    /// held for the duration of `f`, so `f` must not call back into methods that
    /// borrow the same table mutably.
    pub fn with_entries<R>(&self, f: impl FnOnce(&[(Key, Value)]) -> R) -> R {
        f(&self.0.borrow().entries)
    }
}

impl Default for Table {
    fn default() -> Self {
        Table::new()
    }
}

impl Key {
    /// Identity or value equality used to dedupe keys within one table.
    ///
    /// Equivalent to `self == other`. Kept as a named method for call sites that
    /// read better with it.
    #[must_use]
    pub fn same(&self, other: &Key) -> bool {
        self == other
    }

    /// Convert this key back to a value for serialization.
    #[must_use]
    pub fn to_value(&self) -> Value {
        match self {
            Key::Bool(b) => Value::Bool(*b),
            Key::Number(n) => Value::Number(*n),
            Key::Str(s) => Value::Str(s.clone()),
            Key::Table(t) => Value::Table(t.clone()),
            Key::Function(f) => Value::Function(f.clone()),
            Key::Global(g) => Value::Global(g.clone()),
        }
    }
}

/// A hashable identity for any value, used to track shared references.
///
/// Scalars hash by value. Tables, functions, and globals hash by identity.
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub enum Ident {
    /// Table identity.
    Table(usize),
    /// Function identity.
    Function(usize),
    /// Global identity.
    Global(usize),
}

impl Value {
    /// The identity of a reference value, if it has one.
    ///
    /// Only tables, functions, and globals participate in shared-reference
    /// tracking. Scalars return `None`.
    #[must_use]
    pub fn ident(&self) -> Option<Ident> {
        match self {
            Value::Table(t) => Some(Ident::Table(t.id())),
            Value::Function(f) => Some(Ident::Function(f.id)),
            Value::Global(g) => Some(Ident::Global(g.id)),
            _ => None,
        }
    }
}