luallaby 0.1.0

**Work in progress** A pure-Rust Lua interpreter/compiler
Documentation
use std::{
    fmt,
    hash::{Hash, Hasher},
};

use crate::{error::LuaErrorSrc, util::SliceExt, LuaError, Result};

pub type LuaInt = i64;
pub type LuaFloat = f64;

#[derive(Clone, Debug)]
pub enum Numeric {
    Integer(LuaInt),
    Float(LuaFloat),
}

impl Numeric {
    pub fn from_str(str: &[u8]) -> Result<Self> {
        Self::from_str_radix(str, None)
    }

    pub fn from_str_radix(str: &[u8], base: Option<u32>) -> Result<Self> {
        let str = str.trim();
        Ok(match base {
            // Base provided, so must be int without any prefix (0x)
            Some(base) => {
                Numeric::Integer(i64::from_str_radix(&String::from_utf8_lossy(str), base)?)
            }
            // Figure out base from prefix (0x), then try int first, otherwise float
            None => {
                let hex = if !str.is_empty() && matches!(&str[0..1], b"+" | b"-") {
                    str.len() >= 3 && matches!(&str[1..3], b"0x" | b"0X")
                } else {
                    str.len() >= 2 && matches!(&str[0..2], b"0x" | b"0X")
                };
                if hex {
                    match from_str_radix_wrapping(str, 16) {
                        Some(n) => Numeric::Integer(n),
                        None => match parse_f64_hex(str) {
                            Some(f) => Numeric::Float(f),
                            None => {
                                return err!(LuaError::CustomString(
                                    "invalid hex float".to_string()
                                ))
                            }
                        },
                    }
                } else {
                    match String::from_utf8_lossy(str).parse() {
                        Ok(n) => Numeric::Integer(n),
                        Err(..) => Numeric::Float(parse_f64_dec(str)?),
                    }
                }
            }
        })
    }

    pub(crate) fn from_usize_try_int(u: usize) -> Numeric {
        if u <= i64::MAX as usize {
            Numeric::Integer(u as i64)
        } else {
            Numeric::Float(u as f64)
        }
    }

    pub(crate) fn from_f64_try_int(f: f64) -> Numeric {
        if f > i128::MIN as f64
            && f < i128::MAX as f64
            && f as i128 >= i64::MIN as i128
            && f as i128 <= i64::MAX as i128
        {
            Numeric::Integer(f as i64)
        } else {
            Numeric::Float(f)
        }
    }

    pub(crate) fn to_int(&self) -> Result<i64> {
        match self {
            Numeric::Integer(n) => Ok(*n),
            Numeric::Float(..) => err!(LuaError::FloatToInt(LuaErrorSrc::None)),
        }
    }

    pub(crate) fn coerce_int(&self) -> Result<i64> {
        match self {
            Numeric::Integer(n) => Ok(*n),
            Numeric::Float(n) => {
                if n.fract().abs() <= f64::EPSILON
                    && *n > i128::MIN as f64
                    && *n < i128::MAX as f64
                    && *n as i128 >= i64::MIN as i128
                    && *n as i128 <= i64::MAX as i128
                {
                    Ok(*n as i64)
                } else {
                    err!(LuaError::FloatToInt(LuaErrorSrc::None))
                }
            }
        }
    }

    pub(crate) fn to_float(&self) -> f64 {
        match self {
            Numeric::Integer(n) => *n as f64,
            Numeric::Float(n) => *n,
        }
    }

    pub(super) fn coerce_int_failover(&self) -> Numeric {
        match self {
            Numeric::Integer(n) => Numeric::Integer(*n),
            Numeric::Float(n) => {
                if n.fract().abs() <= f64::EPSILON {
                    Numeric::Integer(*n as i64)
                } else {
                    Numeric::Float(*n)
                }
            }
        }
    }
}

impl fmt::Display for Numeric {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Numeric::Integer(n) => write!(f, "{}", n),
            Numeric::Float(n) => write!(f, "{:?}", n),
        }
    }
}

impl Hash for Numeric {
    fn hash<H: Hasher>(&self, state: &mut H) {
        match self {
            Numeric::Integer(n) => {
                state.write_u8(0);
                state.write_i64(*n);
            }
            Numeric::Float(f) => {
                state.write_u8(1);
                for byte in f.to_be_bytes() {
                    state.write_u8(byte);
                }
            }
        }
    }
}

impl PartialEq for Numeric {
    fn eq(&self, other: &Self) -> bool {
        match (self.coerce_int(), other.coerce_int()) {
            (Ok(n1), Ok(n2)) => n1 == n2,
            (Ok(..), ..) | (.., Ok(..)) => false,
            _ => {
                let n1 = self.to_float();
                let n2 = other.to_float();
                if n1.is_finite() && n2.is_finite() {
                    (n1 - n2).abs() < f64::EPSILON
                } else {
                    n1 == n2
                }
            }
        }
    }
}

impl Eq for Numeric {}

pub fn from_str_radix_wrapping(str: &[u8], radix: u32) -> Option<i64> {
    assert!((2..=36).contains(&radix), "radix must be in range [2, 36]");
    let mut out: i64 = 0;

    let (positive, digits) = match str[0] {
        b'-' => (false, &str[1..]),
        b'+' => (true, &str[1..]),
        _ => (true, str),
    };
    let digits = if digits.len() >= 2 && matches!(&digits[0..2], b"0x" | b"0X") {
        &digits[2..]
    } else {
        digits
    };
    if digits.is_empty() {
        return None;
    }

    for &d in digits {
        let d = match (d as char).to_digit(radix) {
            Some(d) => d,
            None => return None,
        };
        out = out.wrapping_mul(radix as i64);
        out = if positive {
            out.wrapping_add(d as i64)
        } else {
            out.wrapping_sub(d as i64)
        };
    }

    Some(out)
}

// Inf and NaN are invalid numeric literals in Lua, thus filter them out
pub fn parse_f64_dec(str: &[u8]) -> Result<f64> {
    let f: f64 = String::from_utf8_lossy(str).parse()?;
    if f.is_nan() || f.is_infinite() {
        err!(LuaError::CustomString(
            "inf and nan are invalid numeric literals".to_string()
        ))
    } else {
        Ok(f)
    }
}

// maximum number of significant digits to read
// (to avoid overflows even with single floats)
const MAX_SIG_DIG: usize = 30;

// Convert a string containing a hexadecimal float literal into f64,
// this code is derived from the hexadecimal float parsing code of the Lua project compiler
pub fn parse_f64_hex(str: &[u8]) -> Option<f64> {
    let str = str.trim();

    let mut acc: f64 = 0.0; // accumulator/result
    let mut sig_dig = 0; // significant digits
    let mut non_sig_dig = 0; // non-significant digits
    let mut exp: i64 = 0; // exponent correction
    let mut has_dot = false;

    let (str, neg) = match str[0] {
        b'-' => (&str[1..], true),
        b'+' => (&str[1..], false),
        _ => (str, false),
    };
    // Must contain 0x
    if !matches!(&str[..2], b"0x" | b"0X") {
        return None;
    }

    // Read numeral part
    let mut str = str[2..].iter().peekable();
    while let Some(b) = str.peek() {
        if matches!(b, b'.') {
            if has_dot {
                return None;
            }
            has_dot = true;
        } else if let Some(d) = (**b as char).to_digit(16) {
            if sig_dig == 0 && d == 0 {
                // No significant digits yet and current is neither
                non_sig_dig += 1;
            } else if sig_dig < MAX_SIG_DIG {
                // Can be read without overflow
                sig_dig += 1;
                acc = acc.mul_add(16.0, if neg { -(d as f64) } else { d as f64 });
            } else {
                // Too many digits, but still correct exponent
                exp += 4; // Each digit increments exponent by 4
            }
            if has_dot {
                // If decimal digit, correct exponent
                exp -= 4; // Each digit decrements exponent by 4
            }
        } else {
            break;
        }
        str.next();
    }
    // Check if read anything
    if non_sig_dig + sig_dig == 0 {
        return None;
    }

    // Read exponent
    if matches!(str.peek(), Some(b'p' | b'P')) {
        str.next();
        let mut exp_lit: i64 = 0;

        let exp_neg = match str.peek() {
            Some(b'-') => {
                str.next();
                true
            }
            Some(b'+') => {
                str.next();
                false
            }
            _ => false,
        };
        // Must have at least one digit
        str.peek()?;

        for b in &mut str {
            match (*b as char).to_digit(10) {
                Some(d) => {
                    exp_lit *= 10;
                    if exp_neg {
                        exp_lit -= d as i64;
                    } else {
                        exp_lit += d as i64;
                    }
                }
                None => return None,
            }
        }

        exp += exp_lit;
    }

    // Consumed everything, but still stuff left, invalid
    if str.next().is_some() {
        return None;
    }

    Some(acc * (exp as f64).exp2())
}