mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 08:51:25 +00:00
JavaScript: hexadecimal and octal numbers round once to the nearest value
ToNumber on a hexadecimal string folded its digits into a double one at a time, so a string past 2^53 could come out one unit off the literal with the same digits. The lexer kept digits exactly in a u128 but rounded per digit once the value passed 128 bits. Both now call one helper that keeps the digits exactly while they fit, records whether any later digit is set, and rounds once.
This commit is contained in:
@@ -90,6 +90,39 @@ pub fn hex_value(c: u32) -> Option<u32> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<Item = u32>, radix: u32) -> Option<f64> {
|
||||
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)
|
||||
}
|
||||
|
||||
+3
-19
@@ -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<f64> {
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user