diff --git a/crates/js/src/chars.rs b/crates/js/src/chars.rs index fb6da4f36..707d14471 100644 --- a/crates/js/src/chars.rs +++ b/crates/js/src/chars.rs @@ -90,6 +90,39 @@ pub fn hex_value(c: u32) -> Option { } } +/// The value of the digit values `digits` in `radix` (8 or 16), rounded once to the nearest number +/// with ties to even (sections 7.8.3 and 9.3.1), or `None` when there are no digits. Digits are +/// kept exactly in a `u128` while they fit; past that, each later digit counts only for its +/// position and for whether any of the dropped bits is set. +pub fn radix_value(digits: impl IntoIterator, radix: u32) -> Option { + let bits = radix.trailing_zeros(); + let mut kept: u128 = 0; + let mut dropped_bits: u32 = 0; + let mut sticky = false; + let mut any = false; + for d in digits { + any = true; + if kept.leading_zeros() >= bits { + kept = kept << bits | u128::from(d); + } else { + dropped_bits = dropped_bits.saturating_add(bits); + sticky |= d != 0; + } + } + if !any { + return None; + } + // `kept` holds at least 125 bits once a digit is dropped, so its lowest bit lies below the + // rounding bit and stands in for every dropped bit. + let mut value = (kept | u128::from(sticky)) as f64; + while dropped_bits > 0 && value.is_finite() { + let step = dropped_bits.min(64); + value *= (1_u128 << step) as f64; + dropped_bits -= step; + } + Some(value) +} + pub fn is_decimal_digit(c: u32) -> bool { (0x30..=0x39).contains(&c) } diff --git a/crates/js/src/lexer.rs b/crates/js/src/lexer.rs index 9b16b16c7..8b82570a7 100644 --- a/crates/js/src/lexer.rs +++ b/crates/js/src/lexer.rs @@ -13,7 +13,7 @@ //! //! The lexer moves forward through the source one character at a time and never recurses. -use crate::chars::{hex_value, is_decimal_digit, is_id_part, is_id_start, is_line_terminator, is_octal_digit, is_whitespace}; +use crate::chars::{hex_value, is_decimal_digit, is_id_part, is_id_start, is_line_terminator, is_octal_digit, is_whitespace, radix_value}; use crate::error::SyntaxError; use crate::regexp; use crate::token::{Directive, DirectiveName, Keyword, Punct, Span, Token, TokenKind}; @@ -352,26 +352,10 @@ impl<'s> Lexer<'s> { /// Reads digits in `radix` (8 or 16) and returns their value, or `None` when there are none. fn radix_digits(&mut self, radix: u32) -> Option { let begin = self.pos; - let mut exact: u128 = 0; - let mut value = 0.0_f64; - let mut overflow = false; - while let Some(d) = self.peek().and_then(hex_value).filter(|&d| d < radix) { - match exact.checked_mul(u128::from(radix)).and_then(|v| v.checked_add(u128::from(d))) { - Some(v) if !overflow => exact = v, - _ => { - if !overflow { - value = exact as f64; - overflow = true; - } - value = value * f64::from(radix) + f64::from(d); - } - } + while self.peek().and_then(hex_value).is_some_and(|d| d < radix) { self.pos += 1; } - if self.pos == begin { - return None; - } - Some(if overflow { value } else { exact as f64 }) + radix_value(self.src[begin..self.pos].iter().filter_map(|&u| hex_value(u32::from(u))), radix) } /// After `0` and a digit: an octal literal when every digit is octal, else a decimal literal diff --git a/crates/js/src/tests.rs b/crates/js/src/tests.rs index 548b20ab0..17f9a4cca 100644 --- a/crates/js/src/tests.rs +++ b/crates/js/src/tests.rs @@ -321,6 +321,9 @@ fn numeric_literals() { assert_eq!(number("0x1F;"), 31.0); assert_eq!(number("0XfF;"), 255.0); assert_eq!(number("0x20000000000001;"), 9007199254740992.0); + // 2^136 + 2^83 + 1 and 2^150 + 2^97 + 1: a set bit past 128 bits rounds the half-way case up. + assert_eq!(number(&format!("0x1{}8{}1;", "0".repeat(13), "0".repeat(19))), 2f64.powi(136) + 2f64.powi(84)); + assert_eq!(number(&format!("01{}2{}1;", "0".repeat(17), "0".repeat(31))), 2f64.powi(150) + 2f64.powi(98)); assert_eq!(number("017;"), 15.0); assert_eq!(number("08;"), 8.0); assert_eq!(number("09.5;"), 9.5); diff --git a/crates/js/src/value.rs b/crates/js/src/value.rs index 6d2e34f9e..e164001ad 100644 --- a/crates/js/src/value.rs +++ b/crates/js/src/value.rs @@ -222,7 +222,7 @@ pub(crate) fn string_to_number(units: &[u16]) -> f64 { if hex.is_empty() || !hex.bytes().all(|b| b.is_ascii_hexdigit()) { return f64::NAN; } - return hex.bytes().fold(0.0, |acc, b| acc * 16.0 + f64::from((b as char).to_digit(16).unwrap_or(0))); + return crate::chars::radix_value(hex.bytes().filter_map(|b| crate::chars::hex_value(u32::from(b))), 16).unwrap_or(f64::NAN); } let (sign, body) = match s.as_bytes().first() { Some(b'+') => (1.0, &s[1..]), @@ -338,6 +338,14 @@ mod tests { } } + #[test] + fn hex_strings_round_once_to_the_nearest_number() { + assert_eq!(string_to_number(&u("0xc83ce80553ee648")), 0xc83_ce80_553e_e648_u64 as f64); + // 2^136 + 2^83 + 1: a set bit past 128 bits rounds the half-way case up. + let digits = format!("0x1{}8{}1", "0".repeat(13), "0".repeat(19)); + assert_eq!(string_to_number(&u(&digits)), 2f64.powi(136) + 2f64.powi(84)); + } + #[test] fn int32_and_uint32_wrap() { assert_eq!(to_int32(4_294_967_296.0 + 5.0), 5);