#[derive(Clone, Copy, PartialEq, Eq)]
pub struct NanBox(u64);
const QNAN: u64 = 0x7ffc_0000_0000_0000;
const SIGN: u64 = 0x8000_0000_0000_0000;
const TAG_UNDEFINED: u64 = QNAN | 0x01;
const TAG_NULL: u64 = QNAN | 0x02;
const TAG_FALSE: u64 = QNAN | 0x03;
const TAG_TRUE: u64 = QNAN | 0x04;
const CANONICAL_NAN: u64 = 0x7ff8_0000_0000_0000;
const HANDLE_MASK: u64 = 0x0000_ffff_ffff_ffff;
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Unpacked {
Undefined,
Null,
Bool(bool),
Number(f64),
Handle(u64),
}
impl NanBox {
#[must_use]
pub const fn undefined() -> Self {
Self(TAG_UNDEFINED)
}
#[must_use]
pub const fn null() -> Self {
Self(TAG_NULL)
}
#[must_use]
pub const fn boolean(b: bool) -> Self {
Self(if b { TAG_TRUE } else { TAG_FALSE })
}
#[must_use]
pub fn number(n: f64) -> Self {
if n.is_nan() {
Self(CANONICAL_NAN)
} else {
Self(n.to_bits())
}
}
#[must_use]
pub const fn handle(index: u64) -> Self {
Self(SIGN | QNAN | (index & HANDLE_MASK))
}
#[must_use]
pub const fn to_bits(self) -> u64 {
self.0
}
#[must_use]
pub const fn from_bits(bits: u64) -> Self {
Self(bits)
}
#[must_use]
pub const fn is_number(self) -> bool {
(self.0 & QNAN) != QNAN
}
#[must_use]
pub const fn is_handle(self) -> bool {
!self.is_number() && (self.0 & SIGN) == SIGN
}
#[must_use]
pub const fn is_undefined(self) -> bool {
self.0 == TAG_UNDEFINED
}
#[must_use]
pub const fn is_null(self) -> bool {
self.0 == TAG_NULL
}
#[must_use]
pub const fn is_boolean(self) -> bool {
self.0 == TAG_TRUE || self.0 == TAG_FALSE
}
#[must_use]
pub fn as_number(self) -> Option<f64> {
if self.is_number() {
Some(f64::from_bits(self.0))
} else {
None
}
}
#[must_use]
pub const fn as_boolean(self) -> Option<bool> {
match self.0 {
TAG_TRUE => Some(true),
TAG_FALSE => Some(false),
_ => None,
}
}
#[must_use]
pub const fn as_handle(self) -> Option<u64> {
if self.is_handle() {
Some(self.0 & HANDLE_MASK)
} else {
None
}
}
#[must_use]
pub fn unpack(self) -> Unpacked {
if self.is_number() {
return Unpacked::Number(f64::from_bits(self.0));
}
match self.0 {
TAG_UNDEFINED => Unpacked::Undefined,
TAG_NULL => Unpacked::Null,
TAG_TRUE => Unpacked::Bool(true),
TAG_FALSE => Unpacked::Bool(false),
_ => Unpacked::Handle(self.0 & HANDLE_MASK),
}
}
#[must_use]
pub fn to_boolean(self) -> bool {
match self.unpack() {
Unpacked::Undefined | Unpacked::Null => false,
Unpacked::Bool(b) => b,
Unpacked::Number(n) => n != 0.0 && !n.is_nan(),
Unpacked::Handle(_) => true,
}
}
#[must_use]
pub fn strict_equals(self, other: Self) -> bool {
if self.is_number() && other.is_number() {
f64::from_bits(self.0) == f64::from_bits(other.0)
} else {
self.0 == other.0
}
}
#[must_use]
pub fn same_value(self, other: Self) -> bool {
if self.is_number() && other.is_number() {
let a = f64::from_bits(self.0);
let b = f64::from_bits(other.0);
if a.is_nan() && b.is_nan() {
return true;
}
if a == 0.0 && b == 0.0 {
return a.is_sign_negative() == b.is_sign_negative();
}
a == b
} else {
self.0 == other.0
}
}
#[must_use]
pub fn type_of(self) -> &'static str {
match self.unpack() {
Unpacked::Undefined => "undefined",
Unpacked::Null => "object",
Unpacked::Bool(_) => "boolean",
Unpacked::Number(_) => "number",
Unpacked::Handle(_) => "object",
}
}
}
impl core::fmt::Debug for NanBox {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self.unpack() {
Unpacked::Undefined => write!(f, "undefined"),
Unpacked::Null => write!(f, "null"),
Unpacked::Bool(b) => write!(f, "{b}"),
Unpacked::Number(n) => write!(f, "{n}"),
Unpacked::Handle(h) => write!(f, "handle({h})"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn singletons_are_distinct_and_typed() {
assert!(NanBox::undefined().is_undefined());
assert!(NanBox::null().is_null());
assert!(NanBox::boolean(true).is_boolean());
assert!(NanBox::boolean(false).is_boolean());
assert!(!NanBox::undefined().is_null());
assert!(!NanBox::null().is_undefined());
assert!(!NanBox::undefined().is_number());
assert!(!NanBox::undefined().is_handle());
assert_ne!(NanBox::undefined(), NanBox::null());
assert_ne!(NanBox::boolean(true), NanBox::boolean(false));
assert_eq!(NanBox::boolean(true).as_boolean(), Some(true));
assert_eq!(NanBox::boolean(false).as_boolean(), Some(false));
assert_eq!(NanBox::undefined().as_boolean(), None);
}
#[test]
fn numbers_round_trip() {
for &n in &[
0.0_f64,
-0.0,
1.0,
-1.0,
123.456,
42.0,
-273.15,
f64::MIN,
f64::MAX,
f64::MIN_POSITIVE,
f64::EPSILON,
f64::INFINITY,
f64::NEG_INFINITY,
1e308,
-1e-308,
] {
let b = NanBox::number(n);
assert!(b.is_number(), "{n} should be a number");
assert!(!b.is_handle());
assert!(!b.is_undefined());
assert_eq!(b.as_number(), Some(n));
assert_eq!(b.unpack(), Unpacked::Number(n));
}
assert!(NanBox::number(-0.0).as_number().unwrap().is_sign_negative());
}
#[test]
fn nan_is_a_number_not_a_box() {
let b = NanBox::number(f64::NAN);
assert!(b.is_number());
assert!(!b.is_handle() && !b.is_undefined() && !b.is_null() && !b.is_boolean());
assert!(b.as_number().unwrap().is_nan());
let arith_nan = NanBox::number(f64::INFINITY - f64::INFINITY);
assert!(arith_nan.is_number());
assert!(arith_nan.as_number().unwrap().is_nan());
}
#[test]
fn handles_round_trip() {
for &h in &[
0_u64,
1,
2,
1000,
0xffff,
0x1_0000,
HANDLE_MASK,
HANDLE_MASK - 1,
] {
let b = NanBox::handle(h);
assert!(b.is_handle(), "handle {h} should be a handle");
assert!(!b.is_number());
assert!(!b.is_undefined() && !b.is_null() && !b.is_boolean());
assert_eq!(b.as_handle(), Some(h));
assert_eq!(b.unpack(), Unpacked::Handle(h));
}
}
#[test]
fn bits_round_trip() {
for b in [
NanBox::undefined(),
NanBox::null(),
NanBox::boolean(true),
NanBox::boolean(false),
NanBox::number(123.456),
NanBox::number(f64::NAN),
NanBox::handle(0x1234_5678),
] {
assert_eq!(NanBox::from_bits(b.to_bits()), b);
assert_eq!(NanBox::from_bits(b.to_bits()).to_bits(), b.to_bits());
}
}
#[test]
fn to_boolean_follows_spec() {
assert!(!NanBox::undefined().to_boolean());
assert!(!NanBox::null().to_boolean());
assert!(NanBox::boolean(true).to_boolean());
assert!(!NanBox::boolean(false).to_boolean());
assert!(!NanBox::number(0.0).to_boolean());
assert!(!NanBox::number(-0.0).to_boolean());
assert!(!NanBox::number(f64::NAN).to_boolean());
assert!(NanBox::number(1.0).to_boolean());
assert!(NanBox::number(-1.0).to_boolean());
assert!(NanBox::number(f64::INFINITY).to_boolean());
assert!(NanBox::handle(0).to_boolean()); }
#[test]
fn strict_equals_follows_spec() {
assert!(NanBox::number(1.0).strict_equals(NanBox::number(1.0)));
assert!(!NanBox::number(1.0).strict_equals(NanBox::number(2.0)));
assert!(!NanBox::number(f64::NAN).strict_equals(NanBox::number(f64::NAN)));
assert!(NanBox::number(0.0).strict_equals(NanBox::number(-0.0)));
assert!(NanBox::undefined().strict_equals(NanBox::undefined()));
assert!(NanBox::null().strict_equals(NanBox::null()));
assert!(!NanBox::undefined().strict_equals(NanBox::null()));
assert!(NanBox::boolean(true).strict_equals(NanBox::boolean(true)));
assert!(!NanBox::boolean(true).strict_equals(NanBox::boolean(false)));
assert!(NanBox::handle(7).strict_equals(NanBox::handle(7)));
assert!(!NanBox::handle(7).strict_equals(NanBox::handle(8)));
assert!(!NanBox::number(0.0).strict_equals(NanBox::null()));
assert!(!NanBox::handle(0).strict_equals(NanBox::number(0.0)));
}
#[test]
fn same_value_differs_from_strict_on_nan_and_zero() {
assert!(NanBox::number(f64::NAN).same_value(NanBox::number(f64::NAN)));
assert!(!NanBox::number(0.0).same_value(NanBox::number(-0.0)));
assert!(NanBox::number(0.0).same_value(NanBox::number(0.0)));
assert!(NanBox::number(-0.0).same_value(NanBox::number(-0.0)));
assert!(NanBox::number(3.0).same_value(NanBox::number(3.0)));
assert!(NanBox::handle(1).same_value(NanBox::handle(1)));
assert!(!NanBox::handle(1).same_value(NanBox::handle(2)));
}
#[test]
fn type_of_follows_spec() {
assert_eq!(NanBox::undefined().type_of(), "undefined");
assert_eq!(NanBox::null().type_of(), "object"); assert_eq!(NanBox::boolean(true).type_of(), "boolean");
assert_eq!(NanBox::number(1.0).type_of(), "number");
assert_eq!(NanBox::number(f64::NAN).type_of(), "number");
assert_eq!(NanBox::handle(0).type_of(), "object");
}
#[test]
fn unpack_matches_predicates() {
let cases = [
(NanBox::undefined(), Unpacked::Undefined),
(NanBox::null(), Unpacked::Null),
(NanBox::boolean(true), Unpacked::Bool(true)),
(NanBox::boolean(false), Unpacked::Bool(false)),
(NanBox::number(2.5), Unpacked::Number(2.5)),
(NanBox::handle(7), Unpacked::Handle(7)),
];
for (box_, expected) in cases {
assert_eq!(box_.unpack(), expected);
}
}
}