Skip to content

Commit

Permalink
Add pretty-print to the Debug formatting to Value to Table.
Browse files Browse the repository at this point in the history
This would allow dumping any Lua variable in human readable form.
  • Loading branch information
khvzak committed Apr 11, 2023
1 parent ba324b4 commit cdbf04f
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 3 deletions.
40 changes: 39 additions & 1 deletion src/table.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashSet;
use std::fmt;
use std::marker::PhantomData;
use std::os::raw::c_void;

Expand All @@ -20,7 +22,7 @@ use crate::value::{FromLua, FromLuaMulti, IntoLua, IntoLuaMulti, Nil, Value};
use {futures_core::future::LocalBoxFuture, futures_util::future};

/// Handle to an internal Lua table.
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct Table<'lua>(pub(crate) LuaRef<'lua>);

/// Owned handle to an internal Lua table.
Expand Down Expand Up @@ -738,6 +740,42 @@ impl<'lua> Table<'lua> {
}
Ok(())
}

pub(crate) fn fmt_pretty(
&self,
fmt: &mut fmt::Formatter,
ident: usize,
visited: &mut HashSet<*const c_void>,
) -> fmt::Result {
visited.insert(self.to_pointer());

let t = self.clone();
// Collect key/value pairs into a vector so we can sort them
let mut pairs = t.pairs::<Value, Value>().flatten().collect::<Vec<_>>();
// Sort keys
pairs.sort_by(|(a, _), (b, _)| a.cmp(b));
if pairs.is_empty() {
return write!(fmt, "{{}}");
}
writeln!(fmt, "{{")?;
for (key, value) in pairs {
write!(fmt, "{}[", " ".repeat(ident + 2))?;
key.fmt_pretty(fmt, false, ident + 2, visited)?;
write!(fmt, "] = ")?;
value.fmt_pretty(fmt, true, ident + 2, visited)?;
writeln!(fmt, ",")?;
}
write!(fmt, "{}}}", " ".repeat(ident))
}
}

impl fmt::Debug for Table<'_> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if fmt.alternate() {
return self.fmt_pretty(fmt, 0, &mut HashSet::new());
}
fmt.write_fmt(format_args!("Table({:?})", self.0))
}
}

impl<'lua> PartialEq for Table<'lua> {
Expand Down
99 changes: 97 additions & 2 deletions src/value.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use std::cmp::Ordering;
use std::collections::HashSet;
use std::iter::{self, FromIterator};
use std::ops::Index;
use std::os::raw::c_void;
use std::sync::Arc;
use std::{ptr, slice, str, vec};
use std::{fmt, ptr, slice, str, vec};

#[cfg(feature = "serialize")]
use {
Expand All @@ -24,7 +26,7 @@ use crate::userdata::AnyUserData;
/// A dynamically typed Lua value. The `String`, `Table`, `Function`, `Thread`, and `UserData`
/// variants contain handle types into the internal Lua state. It is a logic error to mix handle
/// types between separate `Lua` instances, and doing so will result in a panic.
#[derive(Debug, Clone)]
#[derive(Clone)]
pub enum Value<'lua> {
/// The Lua value `nil`.
Nil,
Expand Down Expand Up @@ -121,6 +123,99 @@ impl<'lua> Value<'lua> {
}
}
}

// Compares two values.
// Used to sort values for Debug printing.
pub(crate) fn cmp(&self, other: &Self) -> Ordering {
fn cmp_num(a: Number, b: Number) -> Ordering {
match (a, b) {
_ if a < b => Ordering::Less,
_ if a > b => Ordering::Greater,
_ => Ordering::Equal,
}
}

match (self, other) {
// Nil
(Value::Nil, Value::Nil) => Ordering::Equal,
(Value::Nil, _) => Ordering::Less,
(_, Value::Nil) => Ordering::Greater,
// Null (a special case)
(Value::LightUserData(ud1), Value::LightUserData(ud2)) if ud1 == ud2 => Ordering::Equal,
(Value::LightUserData(ud1), _) if ud1.0.is_null() => Ordering::Less,
(_, Value::LightUserData(ud2)) if ud2.0.is_null() => Ordering::Greater,
// Boolean
(Value::Boolean(a), Value::Boolean(b)) => a.cmp(b),
(Value::Boolean(_), _) => Ordering::Less,
(_, Value::Boolean(_)) => Ordering::Greater,
// Integer && Number
(Value::Integer(a), Value::Integer(b)) => a.cmp(b),
(&Value::Integer(a), &Value::Number(b)) => cmp_num(a as Number, b),
(&Value::Number(a), &Value::Integer(b)) => cmp_num(a, b as Number),
(&Value::Number(a), &Value::Number(b)) => cmp_num(a, b),
(Value::Integer(_) | Value::Number(_), _) => Ordering::Less,
(_, Value::Integer(_) | Value::Number(_)) => Ordering::Greater,
// String
(Value::String(a), Value::String(b)) => a.as_bytes().cmp(b.as_bytes()),
(Value::String(_), _) => Ordering::Less,
(_, Value::String(_)) => Ordering::Greater,
// Other variants can be randomly ordered
(a, b) => a.to_pointer().cmp(&b.to_pointer()),
}
}

pub(crate) fn fmt_pretty(
&self,
fmt: &mut fmt::Formatter,
recursive: bool,
ident: usize,
visited: &mut HashSet<*const c_void>,
) -> fmt::Result {
match self {
Value::Nil => write!(fmt, "nil"),
Value::Boolean(b) => write!(fmt, "{b}"),
Value::LightUserData(ud) if ud.0.is_null() => write!(fmt, "null"),
Value::LightUserData(ud) => write!(fmt, "<lightuserdata {:?}>", ud.0),
Value::Integer(i) => write!(fmt, "{i}"),
Value::Number(n) => write!(fmt, "{n}"),
#[cfg(feature = "luau")]
Value::Vector(x, y, z) => write!(fmt, "vector({x}, {y}, {z})"),
Value::String(s) => write!(fmt, "{s:?}"),
Value::Table(t) if recursive && !visited.contains(&t.to_pointer()) => {
t.fmt_pretty(fmt, ident, visited)
}
t @ Value::Table(_) => write!(fmt, "<table {:?}>", t.to_pointer()),
f @ Value::Function(_) => write!(fmt, "<function {:?}>", f.to_pointer()),
t @ Value::Thread(_) => write!(fmt, "<thread {:?}>", t.to_pointer()),
// TODO: Show type name for registered userdata
u @ Value::UserData(_) => write!(fmt, "<userdata {:?}>", u.to_pointer()),
Value::Error(e) if recursive => write!(fmt, "{e:?}"),
Value::Error(_) => write!(fmt, "<error>"),
}
}
}

impl fmt::Debug for Value<'_> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if fmt.alternate() {
return self.fmt_pretty(fmt, true, 0, &mut HashSet::new());
}
match self {
Value::Nil => write!(fmt, "Nil"),
Value::Boolean(b) => write!(fmt, "Boolean({b})"),
Value::LightUserData(ud) => write!(fmt, "{ud:?}"),
Value::Integer(i) => write!(fmt, "Integer({i})"),
Value::Number(n) => write!(fmt, "Number({n})"),
#[cfg(feature = "luau")]
Value::Vector(x, y, z) => write!(fmt, "Vector({x}, {y}, {z})"),
Value::String(s) => write!(fmt, "String({s:?})"),
Value::Table(t) => write!(fmt, "{t:?}"),
Value::Function(f) => write!(fmt, "{f:?}"),
Value::Thread(t) => write!(fmt, "{t:?}"),
Value::UserData(ud) => write!(fmt, "{ud:?}"),
Value::Error(e) => write!(fmt, "Error({e:?})"),
}
}
}

impl<'lua> PartialEq for Value<'lua> {
Expand Down
15 changes: 15 additions & 0 deletions tests/debug.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use mlua::{Lua, Result};

#[test]
fn test_debug_format() -> Result<()> {
let lua = Lua::new();

// Globals
let globals = lua.globals();
let dump = format!("{globals:#?}");
assert!(dump.starts_with("{\n [\"_G\"] = <table"));

// TODO: Other cases

Ok(())
}

0 comments on commit cdbf04f

Please sign in to comment.