pub(crate) fn exact_i64(number: f64) -> Option<i64> {
let minimum = i64::MIN as f64;
let maximum = f64::from_bits((i64::MAX as f64).to_bits() - 1);
let outside_i64 = number.total_cmp(&minimum).is_lt() || number.total_cmp(&maximum).is_gt();
if number.is_finite() && !outside_i64 && number.trunc().to_bits() == number.to_bits() {
Some(number as i64)
} else {
None
}
}
pub(crate) fn parse_lua_numeral(input: &[u8]) -> Option<f64> {
let bytes = trim_lua_whitespace(input);
let (negative, body) = match bytes {
[b'-', rest @ ..] => (true, rest),
[b'+', rest @ ..] => (false, rest),
_ => (false, bytes),
};
let value = if body.starts_with(b"0x") || body.starts_with(b"0X") {
parse_hex(&body[2..])?
} else {
parse_decimal(body)?
};
Some(if negative { -value } else { value })
}
fn trim_lua_whitespace(mut bytes: &[u8]) -> &[u8] {
while bytes.first().is_some_and(|byte| is_lua_whitespace(*byte)) {
bytes = &bytes[1..];
}
while bytes.last().is_some_and(|byte| is_lua_whitespace(*byte)) {
bytes = &bytes[..bytes.len() - 1];
}
bytes
}
pub(crate) const fn is_lua_whitespace(byte: u8) -> bool {
matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
}
fn parse_decimal(bytes: &[u8]) -> Option<f64> {
let (mantissa, exponent) = split_exponent(bytes, b'e', b'E')?;
let digits = validate_mantissa(mantissa, |byte| byte.is_ascii_digit())?;
if digits == 0 {
return None;
}
if let Some(exponent) = exponent {
validate_signed_digits(exponent)?;
}
let text = match std::str::from_utf8(bytes) {
Ok(text) => text,
Err(_) => return None,
};
let parsed = text.parse::<f64>();
if parsed.is_err() {
return None;
}
Some(parsed.unwrap_or(f64::NAN))
}
fn parse_hex(bytes: &[u8]) -> Option<f64> {
let (mantissa, exponent) = split_exponent(bytes, b'p', b'P')?;
let digits = validate_mantissa(mantissa, |byte| byte.is_ascii_hexdigit())?;
if digits == 0 {
return None;
}
let mantissa_len = i64::try_from(mantissa.len()).unwrap_or(i64::MAX);
let exponent_limit = mantissa_len.saturating_mul(4).saturating_add(2048);
let exponent = match exponent {
Some(exponent) => parse_signed_i64_saturating(exponent, exponent_limit)?,
None => 0,
};
let mut prefix = 0_u64;
let mut significant_digits = 0_i64;
let mut fractional_digits = 0_i64;
let mut dropped_digits = 0_i64;
let mut dropped_nonzero = false;
let mut fractional = false;
for &byte in mantissa {
if byte == b'.' {
fractional = true;
continue;
}
let digit = hex_value(byte)?;
if fractional {
fractional_digits = fractional_digits.saturating_add(1);
}
if significant_digits == 0 && digit == 0 {
continue;
}
if significant_digits < MAX_SIGNIFICANT_HEX_DIGITS {
prefix = prefix.saturating_mul(16).saturating_add(u64::from(digit));
significant_digits = significant_digits.saturating_add(1);
} else {
dropped_digits = dropped_digits.saturating_add(1);
dropped_nonzero |= digit != 0;
}
}
if significant_digits == 0 {
return Some(0.0);
}
let digit_adjustment = dropped_digits
.saturating_sub(fractional_digits)
.saturating_mul(4);
let scale = exponent.saturating_add(digit_adjustment);
Some(finish_hex(prefix, dropped_nonzero, scale))
}
const MAX_SIGNIFICANT_HEX_DIGITS: i64 = 16;
const F64_PRECISION_BITS: u32 = f64::MANTISSA_DIGITS;
const MIN_NORMAL_EXPONENT: i64 = -1022;
const HALF_MIN_SUBNORMAL_EXPONENT: i64 = -1075;
const MAX_NORMAL_EXPONENT: i64 = 1023;
fn parse_signed_i64_saturating(bytes: &[u8], limit: i64) -> Option<i64> {
validate_signed_digits(bytes)?;
let (negative, digits) = match bytes {
[b'-', rest @ ..] => (true, rest),
[b'+', rest @ ..] => (false, rest),
_ => (false, bytes),
};
let mut magnitude = 0_i64;
for &byte in digits {
magnitude = magnitude
.saturating_mul(10)
.saturating_add(i64::from(byte - b'0'))
.min(limit);
}
Some(if negative { -magnitude } else { magnitude })
}
fn finish_hex(prefix: u64, dropped_nonzero: bool, scale: i64) -> f64 {
let bit_count = u64::BITS - prefix.leading_zeros();
let top_exponent = scale.saturating_add(i64::from(bit_count.saturating_sub(1)));
if top_exponent > MAX_NORMAL_EXPONENT {
return f64::INFINITY;
}
if top_exponent < HALF_MIN_SUBNORMAL_EXPONENT {
return 0.0;
}
if top_exponent < MIN_NORMAL_EXPONENT {
return round_hex_subnormal(prefix, dropped_nonzero, scale);
}
let shift = bit_count.saturating_sub(F64_PRECISION_BITS);
let significand = round_shift_right(prefix, shift, dropped_nonzero);
let adjusted_scale = scale.saturating_add(i64::from(shift));
scale_finite_by_power_of_two(significand as f64, adjusted_scale)
}
fn round_shift_right(value: u64, shift: u32, sticky: bool) -> u64 {
if shift == 0 {
return value;
}
let kept = value >> shift;
let remainder_mask = (1_u64 << shift) - 1;
let remainder = value & remainder_mask;
let halfway = 1_u64 << (shift - 1);
let round_up = remainder > halfway || (remainder == halfway && (sticky || (kept & 1) != 0));
kept.saturating_add(u64::from(round_up))
}
fn round_hex_subnormal(prefix: u64, sticky: bool, scale: i64) -> f64 {
let shift = (-1074_i64).saturating_sub(scale);
let units = if shift <= 0 {
let left = shift.saturating_neg().clamp(0, 63) as u32;
prefix << left
} else if shift < 64 {
round_shift_right(prefix, shift.clamp(1, 63) as u32, sticky)
} else if shift == 64 {
let halfway = 1_u64 << 63;
u64::from(prefix > halfway || (prefix == halfway && sticky))
} else {
0
};
f64::from_bits(units)
}
fn scale_finite_by_power_of_two(value: f64, scale: i64) -> f64 {
const FRACTION_MASK: u64 = (1_u64 << 52) - 1;
const ONE_EXPONENT_BITS: u64 = 1023_u64 << 52;
let bits = value.to_bits();
let encoded_exponent = ((bits >> 52) & 0x7ff) as i64;
let value_exponent = encoded_exponent.saturating_sub(1023);
let normalized = f64::from_bits((bits & FRACTION_MASK) | ONE_EXPONENT_BITS);
let exponent = scale.saturating_add(value_exponent);
if exponent > MAX_NORMAL_EXPONENT {
return f64::INFINITY;
}
if exponent < HALF_MIN_SUBNORMAL_EXPONENT {
return 0.0;
}
if exponent < MIN_NORMAL_EXPONENT {
let tail_exponent = exponent.saturating_sub(MIN_NORMAL_EXPONENT).clamp(-53, -1) as i32;
return (normalized * f64::MIN_POSITIVE) * 2_f64.powi(tail_exponent);
}
let exponent = exponent.clamp(MIN_NORMAL_EXPONENT, MAX_NORMAL_EXPONENT) as i32;
normalized * 2_f64.powi(exponent)
}
fn split_exponent(bytes: &[u8], lower: u8, upper: u8) -> Option<(&[u8], Option<&[u8]>)> {
let index = bytes
.iter()
.position(|byte| *byte == lower || *byte == upper);
match index {
Some(index)
if bytes[index + 1..]
.iter()
.any(|byte| *byte == lower || *byte == upper) =>
{
None
}
Some(index) => Some((&bytes[..index], Some(&bytes[index + 1..]))),
None => Some((bytes, None)),
}
}
fn validate_mantissa(bytes: &[u8], valid_digit: impl Fn(u8) -> bool) -> Option<usize> {
let mut dots = 0;
let mut digits = 0;
for byte in bytes {
if *byte == b'.' {
dots += 1;
if dots > 1 {
return None;
}
} else if valid_digit(*byte) {
digits += 1;
} else {
return None;
}
}
Some(digits)
}
fn validate_signed_digits(bytes: &[u8]) -> Option<()> {
let bytes = match bytes {
[b'+' | b'-', rest @ ..] => rest,
_ => bytes,
};
(!bytes.is_empty() && bytes.iter().all(u8::is_ascii_digit)).then_some(())
}
fn hex_value(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::parse_lua_numeral;
#[test]
fn parse_hex_extremes() {
let long_f = format!("0x{}p-1000", "f".repeat(300));
let long_one = format!("0x1{}p-1000", "0".repeat(299));
let cases = [
("0x1.8p1", Some(3.0_f64.to_bits())),
("0xA", Some(10.0_f64.to_bits())),
("0x.8", Some(0.5_f64.to_bits())),
("0x1p4", Some(16.0_f64.to_bits())),
("0x00000000000000000001.8p1", Some(3.0_f64.to_bits())),
("0x1p2147483648", Some(f64::INFINITY.to_bits())),
("0x1p9999999999", Some(f64::INFINITY.to_bits())),
("0x1p-2147483648", Some(0.0_f64.to_bits())),
("0x1p-9999999999", Some(0.0_f64.to_bits())),
("0x0p9999999999", Some(0.0_f64.to_bits())),
("-0x0p9999999999", Some(0x8000_0000_0000_0000)),
("0x1p1023", Some(0x7fe0_0000_0000_0000)),
("0x1.fffffffffffffp1023", Some(f64::MAX.to_bits())),
("0x1p1024", Some(f64::INFINITY.to_bits())),
("0x1p-1022", Some(f64::MIN_POSITIVE.to_bits())),
("0x1p-1074", Some(0x0000_0000_0000_0001)),
("0x1p-1075", Some(0.0_f64.to_bits())),
("0x1.0000000000001p-1075", Some(0x0000_0000_0000_0001)),
("0x1p+", None),
("0x1p--1", None),
];
for (input, expected) in cases {
assert_eq!(
parse_lua_numeral(input.as_bytes()).map(f64::to_bits),
expected
);
}
assert_eq!(
parse_lua_numeral(long_f.as_bytes()).map(f64::to_bits),
Some(0x4c70_0000_0000_0000)
);
assert_eq!(
parse_lua_numeral(long_one.as_bytes()).map(f64::to_bits),
Some(0x4c30_0000_0000_0000)
);
}
}