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}