use std::any::Any;
use std::fmt;
use std::hash::{Hash, Hasher};
use super::closure::Closure;
use super::gc::arena::GcRef;
use super::gc::trace::Trace;
use super::state::LuaThread;
use super::string::LuaString;
use super::table::Table;
#[cfg(not(feature = "send"))]
pub type UserDataBox = Box<dyn Any>;
#[cfg(feature = "send")]
pub type UserDataBox = Box<dyn Any + Send>;
pub struct Userdata {
data: UserDataBox,
metatable: Option<GcRef<Table>>,
env: Option<GcRef<Table>>,
finalized: bool,
alloc_seq: u64,
}
impl Userdata {
pub fn new(data: UserDataBox) -> Self {
Self {
data,
metatable: None,
env: None,
finalized: false,
alloc_seq: 0,
}
}
pub fn with_metatable(data: UserDataBox, mt: GcRef<Table>) -> Self {
Self {
data,
metatable: Some(mt),
env: None,
finalized: false,
alloc_seq: 0,
}
}
pub fn alloc_seq(&self) -> u64 {
self.alloc_seq
}
pub fn set_alloc_seq(&mut self, seq: u64) {
self.alloc_seq = seq;
}
pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
self.data.downcast_ref::<T>()
}
pub fn downcast_mut<T: Any>(&mut self) -> Option<&mut T> {
self.data.downcast_mut::<T>()
}
pub fn metatable(&self) -> Option<GcRef<Table>> {
self.metatable
}
pub fn set_metatable(&mut self, mt: Option<GcRef<Table>>) {
self.metatable = mt;
}
pub fn env(&self) -> Option<GcRef<Table>> {
self.env
}
pub fn set_env(&mut self, env: Option<GcRef<Table>>) {
self.env = env;
}
pub fn finalized(&self) -> bool {
self.finalized
}
pub fn set_finalized(&mut self, finalized: bool) {
self.finalized = finalized;
}
}
impl Trace for Userdata {
fn trace(&self) {
}
}
#[derive(Clone, Copy)]
pub enum Val {
Nil,
Bool(bool),
Num(f64),
Str(GcRef<LuaString>),
Table(GcRef<Table>),
Function(GcRef<Closure>),
Userdata(GcRef<Userdata>),
Thread(GcRef<LuaThread>),
LightUserdata(usize),
}
impl Val {
#[inline]
pub fn is_truthy(self) -> bool {
!matches!(self, Self::Nil | Self::Bool(false))
}
#[inline]
pub fn is_nil(self) -> bool {
matches!(self, Self::Nil)
}
pub fn type_name(self) -> &'static str {
match self {
Self::Nil => "nil",
Self::Bool(_) => "boolean",
Self::Num(_) => "number",
Self::Str(_) => "string",
Self::Table(_) => "table",
Self::Function(_) => "function",
Self::Userdata(_) | Self::LightUserdata(_) => "userdata",
Self::Thread(_) => "thread",
}
}
#[inline]
pub fn as_number(self) -> Option<f64> {
match self {
Self::Num(n) => Some(n),
_ => None,
}
}
#[inline]
pub fn as_bool(self) -> Option<bool> {
match self {
Self::Bool(b) => Some(b),
_ => None,
}
}
}
impl PartialEq for Val {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Nil, Self::Nil) => true,
(Self::Bool(a), Self::Bool(b)) => a == b,
(Self::Num(a), Self::Num(b)) => a == b,
(Self::Str(a), Self::Str(b)) => a == b,
(Self::Table(a), Self::Table(b)) => a == b,
(Self::Function(a), Self::Function(b)) => a == b,
(Self::Userdata(a), Self::Userdata(b)) => a == b,
(Self::Thread(a), Self::Thread(b)) => a == b,
(Self::LightUserdata(a), Self::LightUserdata(b)) => a == b,
_ => false,
}
}
}
impl Hash for Val {
fn hash<H: Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
match self {
Self::Nil => {}
Self::Bool(b) => b.hash(state),
Self::Num(n) => {
if *n == 0.0 {
0.0_f64.to_bits().hash(state);
} else {
n.to_bits().hash(state);
}
}
Self::Str(r) => r.hash(state),
Self::Table(r) => r.hash(state),
Self::Function(r) => r.hash(state),
Self::Userdata(r) => r.hash(state),
Self::Thread(r) => r.hash(state),
Self::LightUserdata(p) => p.hash(state),
}
}
}
impl fmt::Debug for Val {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Nil => write!(f, "Nil"),
Self::Bool(b) => write!(f, "Bool({b})"),
Self::Num(n) => write!(f, "Num({n})"),
Self::Str(r) => write!(f, "Str({r:?})"),
Self::Table(r) => write!(f, "Table({r:?})"),
Self::Function(r) => write!(f, "Function({r:?})"),
Self::Userdata(r) => write!(f, "Userdata({r:?})"),
Self::Thread(r) => write!(f, "Thread({r:?})"),
Self::LightUserdata(p) => write!(f, "LightUserdata(0x{p:x})"),
}
}
}
impl fmt::Display for Val {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Nil => write!(f, "nil"),
Self::Bool(b) => write!(f, "{b}"),
Self::Num(n) => fmt_lua_number(f, *n),
Self::Str(r) => write!(f, "string: 0x{:08x}", r.index()),
Self::Table(r) => write!(f, "table: 0x{:08x}", r.index()),
Self::Function(r) => write!(f, "function: 0x{:08x}", r.index()),
Self::Userdata(r) => write!(f, "userdata: 0x{:08x}", r.index()),
Self::Thread(r) => write!(f, "thread: 0x{:08x}", r.index()),
Self::LightUserdata(p) => write!(f, "userdata: 0x{p:08x}"),
}
}
}
fn fmt_lua_number(f: &mut fmt::Formatter<'_>, n: f64) -> fmt::Result {
if n.is_nan() {
return write!(f, "-nan");
}
if n.is_infinite() {
return if n.is_sign_positive() {
write!(f, "inf")
} else {
write!(f, "-inf")
};
}
if n == 0.0 {
return if n.is_sign_negative() {
write!(f, "-0")
} else {
write!(f, "0")
};
}
let abs = n.abs();
let exp10 = abs.log10().floor() as i32;
if (-4..14).contains(&exp10) {
let dec = i32::max(13 - exp10, 0) as usize;
let s = format!("{n:.dec$}");
write!(f, "{}", strip_trailing_zeros(&s))
} else {
let s = format!("{n:.13e}");
fmt_scientific_stripped(f, &s)
}
}
fn strip_trailing_zeros(s: &str) -> &str {
if !s.contains('.') {
return s;
}
let trimmed = s.trim_end_matches('0');
trimmed.trim_end_matches('.')
}
fn fmt_scientific_stripped(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
let Some(e_pos) = s.find('e') else {
return write!(f, "{s}");
};
let mantissa = &s[..e_pos];
let exp_str = &s[e_pos + 1..];
let mantissa = strip_trailing_zeros(mantissa);
let Ok(exp) = exp_str.parse::<i32>() else {
return write!(f, "{s}");
};
if exp >= 0 {
write!(f, "{mantissa}e+{exp:02}")
} else {
write!(f, "{mantissa}e-{:02}", exp.unsigned_abs())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vm::gc::Color;
use crate::vm::gc::arena::Arena;
use crate::vm::string::lua_hash;
use std::collections::hash_map::DefaultHasher;
fn hash_val(v: &Val) -> u64 {
let mut hasher = DefaultHasher::new();
v.hash(&mut hasher);
hasher.finish()
}
#[test]
fn nil_is_falsy() {
assert!(!Val::Nil.is_truthy());
}
#[test]
fn false_is_falsy() {
assert!(!Val::Bool(false).is_truthy());
}
#[test]
fn true_is_truthy() {
assert!(Val::Bool(true).is_truthy());
}
#[test]
fn zero_is_truthy() {
assert!(Val::Num(0.0).is_truthy());
}
#[test]
fn number_is_truthy() {
assert!(Val::Num(42.0).is_truthy());
}
#[test]
fn string_ref_is_truthy() {
let mut arena: Arena<LuaString> = Arena::new();
let r = arena.alloc(LuaString::new(b"test", lua_hash(b"test")), Color::White0);
assert!(Val::Str(r).is_truthy());
}
#[test]
fn table_ref_is_truthy() {
let mut arena: Arena<Table> = Arena::new();
let r = arena.alloc(Table::new(), Color::White0);
assert!(Val::Table(r).is_truthy());
}
#[test]
fn nil_equals_nil() {
assert_eq!(Val::Nil, Val::Nil);
}
#[test]
fn bool_equality() {
assert_eq!(Val::Bool(true), Val::Bool(true));
assert_eq!(Val::Bool(false), Val::Bool(false));
assert_ne!(Val::Bool(true), Val::Bool(false));
}
#[test]
fn number_equality() {
assert_eq!(Val::Num(1.0), Val::Num(1.0));
assert_ne!(Val::Num(1.0), Val::Num(2.0));
}
#[test]
fn nan_not_equal_to_nan() {
let nan = Val::Num(f64::NAN);
assert_ne!(nan, nan);
}
#[test]
fn negative_zero_equals_positive_zero() {
assert_eq!(Val::Num(-0.0), Val::Num(0.0));
}
#[test]
fn different_types_not_equal() {
assert_ne!(Val::Nil, Val::Bool(false));
assert_ne!(Val::Num(0.0), Val::Bool(false));
assert_ne!(Val::Num(1.0), Val::Bool(true));
}
#[test]
fn string_identity_equality() {
let mut arena: Arena<LuaString> = Arena::new();
let r1 = arena.alloc(LuaString::new(b"test", lua_hash(b"test")), Color::White0);
let r2 = arena.alloc(LuaString::new(b"test", lua_hash(b"test")), Color::White0);
assert_eq!(Val::Str(r1), Val::Str(r1));
assert_ne!(Val::Str(r1), Val::Str(r2));
}
#[test]
fn table_identity_equality() {
let mut arena: Arena<Table> = Arena::new();
let r1 = arena.alloc(Table::new(), Color::White0);
let r2 = arena.alloc(Table::new(), Color::White0);
assert_eq!(Val::Table(r1), Val::Table(r1));
assert_ne!(Val::Table(r1), Val::Table(r2));
}
#[test]
fn equal_values_have_equal_hashes() {
assert_eq!(hash_val(&Val::Nil), hash_val(&Val::Nil));
assert_eq!(hash_val(&Val::Bool(true)), hash_val(&Val::Bool(true)));
assert_eq!(hash_val(&Val::Num(42.0)), hash_val(&Val::Num(42.0)));
}
#[test]
fn negative_zero_same_hash_as_positive_zero() {
assert_eq!(hash_val(&Val::Num(-0.0)), hash_val(&Val::Num(0.0)));
}
#[test]
fn different_types_different_hashes() {
assert_ne!(hash_val(&Val::Nil), hash_val(&Val::Bool(false)));
}
#[test]
fn type_names() {
assert_eq!(Val::Nil.type_name(), "nil");
assert_eq!(Val::Bool(true).type_name(), "boolean");
assert_eq!(Val::Num(1.0).type_name(), "number");
assert_eq!(Val::LightUserdata(0).type_name(), "userdata");
}
#[test]
fn type_name_for_refs() {
use crate::vm::closure::{Closure, RustClosure};
use crate::vm::state::LuaState;
#[allow(clippy::unnecessary_wraps)]
fn dummy(_: &mut LuaState) -> crate::error::LuaResult<u32> {
Ok(0)
}
let mut strings: Arena<LuaString> = Arena::new();
let mut tables: Arena<Table> = Arena::new();
let mut closures: Arena<Closure> = Arena::new();
let s = strings.alloc(LuaString::new(b"test", lua_hash(b"test")), Color::White0);
let t = tables.alloc(Table::new(), Color::White0);
let c = closures.alloc(
Closure::Rust(RustClosure::new(dummy, "test")),
Color::White0,
);
assert_eq!(Val::Str(s).type_name(), "string");
assert_eq!(Val::Table(t).type_name(), "table");
assert_eq!(Val::Function(c).type_name(), "function");
}
#[test]
fn display_nil() {
assert_eq!(format!("{}", Val::Nil), "nil");
}
#[test]
fn display_bool() {
assert_eq!(format!("{}", Val::Bool(true)), "true");
assert_eq!(format!("{}", Val::Bool(false)), "false");
}
#[test]
fn display_integers() {
assert_eq!(format!("{}", Val::Num(0.0)), "0");
assert_eq!(format!("{}", Val::Num(1.0)), "1");
assert_eq!(format!("{}", Val::Num(-1.0)), "-1");
assert_eq!(format!("{}", Val::Num(42.0)), "42");
assert_eq!(format!("{}", Val::Num(100.0)), "100");
}
#[test]
fn display_fractions() {
assert_eq!(format!("{}", Val::Num(1.5)), "1.5");
assert_eq!(format!("{}", Val::Num(0.1)), "0.1");
}
#[test]
fn display_large_number_scientific() {
assert_eq!(format!("{}", Val::Num(1e15)), "1e+15");
assert_eq!(format!("{}", Val::Num(1e20)), "1e+20");
}
#[test]
fn display_small_number_scientific() {
assert_eq!(format!("{}", Val::Num(1e-5)), "1e-05");
}
#[test]
fn display_small_number_fixed() {
assert_eq!(format!("{}", Val::Num(0.0001)), "0.0001");
}
#[test]
fn display_infinity() {
assert_eq!(format!("{}", Val::Num(f64::INFINITY)), "inf");
assert_eq!(format!("{}", Val::Num(f64::NEG_INFINITY)), "-inf");
}
#[test]
fn display_nan() {
assert_eq!(format!("{}", Val::Num(f64::NAN)), "-nan");
}
#[test]
fn display_pi() {
assert_eq!(
format!("{}", Val::Num(std::f64::consts::PI)),
"3.1415926535898"
);
}
#[test]
fn display_one_third() {
assert_eq!(format!("{}", Val::Num(1.0 / 3.0)), "0.33333333333333");
}
#[test]
fn as_number() {
assert_eq!(Val::Num(42.0).as_number(), Some(42.0));
assert_eq!(Val::Nil.as_number(), None);
assert_eq!(Val::Bool(true).as_number(), None);
}
#[test]
fn as_bool() {
assert_eq!(Val::Bool(true).as_bool(), Some(true));
assert_eq!(Val::Bool(false).as_bool(), Some(false));
assert_eq!(Val::Nil.as_bool(), None);
}
#[test]
fn is_nil() {
assert!(Val::Nil.is_nil());
assert!(!Val::Bool(false).is_nil());
assert!(!Val::Num(0.0).is_nil());
}
#[test]
fn userdata_new_unit() {
let ud = Userdata::new(Box::new(()));
assert!(ud.downcast_ref::<()>().is_some());
assert!(ud.metatable().is_none());
assert!(ud.env().is_none());
}
#[test]
fn userdata_new_i32() {
let ud = Userdata::new(Box::new(42_i32));
assert_eq!(ud.downcast_ref::<i32>(), Some(&42));
assert!(ud.downcast_ref::<String>().is_none());
}
#[test]
fn userdata_new_string() {
let ud = Userdata::new(Box::new(String::from("hello")));
assert_eq!(
ud.downcast_ref::<String>().map(String::as_str),
Some("hello")
);
assert!(ud.downcast_ref::<i32>().is_none());
}
#[test]
fn userdata_downcast_mut() {
let mut ud = Userdata::new(Box::new(10_i32));
if let Some(v) = ud.downcast_mut::<i32>() {
*v = 20;
}
assert_eq!(ud.downcast_ref::<i32>(), Some(&20));
}
#[test]
fn userdata_metatable() {
let mut table_arena: Arena<Table> = Arena::new();
let mt = table_arena.alloc(Table::new(), Color::White0);
let ud = Userdata::with_metatable(Box::new(()), mt);
assert_eq!(ud.metatable(), Some(mt));
}
#[test]
fn userdata_set_metatable() {
let mut table_arena: Arena<Table> = Arena::new();
let mt = table_arena.alloc(Table::new(), Color::White0);
let mut ud = Userdata::new(Box::new(()));
assert!(ud.metatable().is_none());
ud.set_metatable(Some(mt));
assert_eq!(ud.metatable(), Some(mt));
ud.set_metatable(None);
assert!(ud.metatable().is_none());
}
#[test]
fn userdata_env() {
let mut table_arena: Arena<Table> = Arena::new();
let env = table_arena.alloc(Table::new(), Color::White0);
let mut ud = Userdata::new(Box::new(()));
assert!(ud.env().is_none());
ud.set_env(Some(env));
assert_eq!(ud.env(), Some(env));
ud.set_env(None);
assert!(ud.env().is_none());
}
#[test]
fn userdata_type_name() {
let mut arena: Arena<Userdata> = Arena::new();
let r = arena.alloc(Userdata::new(Box::new(())), Color::White0);
assert_eq!(Val::Userdata(r).type_name(), "userdata");
}
#[test]
fn userdata_is_truthy() {
let mut arena: Arena<Userdata> = Arena::new();
let r = arena.alloc(Userdata::new(Box::new(())), Color::White0);
assert!(Val::Userdata(r).is_truthy());
}
#[test]
fn userdata_identity_equality() {
let mut arena: Arena<Userdata> = Arena::new();
let r1 = arena.alloc(Userdata::new(Box::new(1_i32)), Color::White0);
let r2 = arena.alloc(Userdata::new(Box::new(1_i32)), Color::White0);
assert_eq!(Val::Userdata(r1), Val::Userdata(r1));
assert_ne!(Val::Userdata(r1), Val::Userdata(r2));
}
}