mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 12:21:54 +00:00
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.
398 lines
12 KiB
Rust
398 lines
12 KiB
Rust
//! Values (ECMA-262 5.1 section 8) and the conversions between primitives (section 9).
|
||
//!
|
||
//! A string is a rope: concatenation makes a node that points at its two halves, and the first
|
||
//! read of its characters flattens it, with a loop and an explicit stack, into one buffer that
|
||
//! replaces the node. Strings hold no objects, so the reference counts that share them cannot form
|
||
//! a cycle; dropping a long rope also uses a loop, not recursion.
|
||
|
||
use std::cell::RefCell;
|
||
use std::rc::Rc;
|
||
|
||
use crate::heap::ObjId;
|
||
|
||
/// A JavaScript value. Objects are handles into the interpreter's heap.
|
||
#[derive(Clone, Debug)]
|
||
pub(crate) enum Value {
|
||
Undefined,
|
||
Null,
|
||
Bool(bool),
|
||
Number(f64),
|
||
String(Str),
|
||
Object(ObjId),
|
||
}
|
||
|
||
impl Value {
|
||
pub(crate) fn str(s: &str) -> Value {
|
||
Value::String(Str::from(s))
|
||
}
|
||
|
||
pub(crate) fn as_object(&self) -> Option<ObjId> {
|
||
match self {
|
||
Value::Object(id) => Some(*id),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
pub(crate) fn is_nullish(&self) -> bool {
|
||
matches!(self, Value::Undefined | Value::Null)
|
||
}
|
||
|
||
/// ToBoolean (section 9.2).
|
||
pub(crate) fn truthy(&self) -> bool {
|
||
match self {
|
||
Value::Undefined | Value::Null => false,
|
||
Value::Bool(b) => *b,
|
||
Value::Number(n) => !(n.is_nan() || *n == 0.0),
|
||
Value::String(s) => s.len() > 0,
|
||
Value::Object(_) => true,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The text of a string value, in UTF-16 code units: a flat buffer or a concatenation.
|
||
#[derive(Clone)]
|
||
pub(crate) struct Str(Rc<RefCell<Node>>);
|
||
|
||
enum Node {
|
||
Flat(Rc<[u16]>),
|
||
Concat {
|
||
left: Str,
|
||
right: Str,
|
||
len: usize,
|
||
},
|
||
/// A node whose halves were moved out while it was dropped.
|
||
Taken,
|
||
}
|
||
|
||
impl std::fmt::Debug for Str {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
write!(f, "{:?}", self.to_rust_string())
|
||
}
|
||
}
|
||
|
||
impl From<&str> for Str {
|
||
fn from(s: &str) -> Self {
|
||
Str::from_units(s.encode_utf16().collect())
|
||
}
|
||
}
|
||
|
||
/// Strings at most this long are copied when concatenated instead of becoming a rope node.
|
||
const FLAT_CONCAT: usize = 32;
|
||
|
||
impl Str {
|
||
pub(crate) fn from_units(units: Vec<u16>) -> Self {
|
||
Str::from_rc(units.into())
|
||
}
|
||
|
||
pub(crate) fn from_rc(units: Rc<[u16]>) -> Self {
|
||
Str(Rc::new(RefCell::new(Node::Flat(units))))
|
||
}
|
||
|
||
pub(crate) fn len(&self) -> usize {
|
||
match &*self.0.borrow() {
|
||
Node::Flat(f) => f.len(),
|
||
Node::Concat { len, .. } => *len,
|
||
Node::Taken => 0,
|
||
}
|
||
}
|
||
|
||
/// The node is not yet flattened.
|
||
pub(crate) fn is_rope(&self) -> bool {
|
||
matches!(&*self.0.borrow(), Node::Concat { .. })
|
||
}
|
||
|
||
/// An address that identifies the node, for counting shared nodes once.
|
||
pub(crate) fn node_addr(&self) -> usize {
|
||
Rc::as_ptr(&self.0) as *const () as usize
|
||
}
|
||
|
||
/// The address of the code-unit buffer of a flat node, which a property name can share.
|
||
pub(crate) fn buffer_addr(&self) -> Option<usize> {
|
||
match &*self.0.borrow() {
|
||
Node::Flat(f) => Some(Rc::as_ptr(f) as *const () as usize),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// The two halves of a concatenation node.
|
||
pub(crate) fn halves(&self) -> Option<(Str, Str)> {
|
||
match &*self.0.borrow() {
|
||
Node::Concat { left, right, .. } => Some((left.clone(), right.clone())),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// `a + b`. The caller checks the length limit first.
|
||
pub(crate) fn concat(a: &Str, b: &Str) -> Str {
|
||
let (la, lb) = (a.len(), b.len());
|
||
if la == 0 {
|
||
return b.clone();
|
||
}
|
||
if lb == 0 {
|
||
return a.clone();
|
||
}
|
||
if la + lb <= FLAT_CONCAT {
|
||
let mut units = Vec::with_capacity(la + lb);
|
||
units.extend_from_slice(&a.flat());
|
||
units.extend_from_slice(&b.flat());
|
||
return Str::from_units(units);
|
||
}
|
||
Str(Rc::new(RefCell::new(Node::Concat { left: a.clone(), right: b.clone(), len: la + lb })))
|
||
}
|
||
|
||
/// The code units, flattening the rope first if it is one.
|
||
pub(crate) fn flat(&self) -> Rc<[u16]> {
|
||
if let Node::Flat(f) = &*self.0.borrow() {
|
||
return f.clone();
|
||
}
|
||
let mut units = Vec::with_capacity(self.len());
|
||
let mut stack = vec![self.clone()];
|
||
while let Some(s) = stack.pop() {
|
||
match &*s.0.borrow() {
|
||
Node::Flat(f) => units.extend_from_slice(f),
|
||
Node::Concat { left, right, .. } => {
|
||
stack.push(right.clone());
|
||
stack.push(left.clone());
|
||
}
|
||
Node::Taken => {}
|
||
}
|
||
}
|
||
let flat: Rc<[u16]> = units.into();
|
||
let old = std::mem::replace(&mut *self.0.borrow_mut(), Node::Flat(flat.clone()));
|
||
drop_node(old);
|
||
flat
|
||
}
|
||
|
||
pub(crate) fn to_rust_string(&self) -> String {
|
||
String::from_utf16_lossy(&self.flat())
|
||
}
|
||
}
|
||
|
||
impl PartialEq for Str {
|
||
fn eq(&self, other: &Self) -> bool {
|
||
Rc::ptr_eq(&self.0, &other.0) || (self.len() == other.len() && self.flat() == other.flat())
|
||
}
|
||
}
|
||
|
||
/// Drops a node's halves with a loop, so a rope of a million appends does not recurse a million
|
||
/// levels deep.
|
||
fn drop_node(node: Node) {
|
||
let mut stack = Vec::new();
|
||
if let Node::Concat { left, right, .. } = node {
|
||
stack.push(left);
|
||
stack.push(right);
|
||
}
|
||
while let Some(s) = stack.pop() {
|
||
if Rc::strong_count(&s.0) == 1 {
|
||
let taken = std::mem::replace(&mut *s.0.borrow_mut(), Node::Taken);
|
||
if let Node::Concat { left, right, .. } = taken {
|
||
stack.push(left);
|
||
stack.push(right);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Drop for Str {
|
||
fn drop(&mut self) {
|
||
if Rc::strong_count(&self.0) == 1 && self.is_rope() {
|
||
let node = std::mem::replace(&mut *self.0.borrow_mut(), Node::Taken);
|
||
drop_node(node);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// White space and line terminators as StrWhiteSpaceChar (section 9.3.1) counts them: the same
|
||
/// classes the lexer uses between tokens.
|
||
pub(crate) fn is_str_space(u: u16) -> bool {
|
||
crate::chars::is_whitespace(u32::from(u)) || crate::chars::is_line_terminator(u32::from(u))
|
||
}
|
||
|
||
/// ToNumber applied to a string (section 9.3.1): the StringNumericLiteral grammar, NaN for
|
||
/// anything else.
|
||
pub(crate) fn string_to_number(units: &[u16]) -> f64 {
|
||
let start = units.iter().position(|&u| !is_str_space(u)).unwrap_or(units.len());
|
||
let end = units.iter().rposition(|&u| !is_str_space(u)).map_or(start, |i| i + 1);
|
||
let Some(text) = units.get(start..end) else { return f64::NAN };
|
||
if text.is_empty() {
|
||
return 0.0;
|
||
}
|
||
let Ok(s) = String::from_utf16(text) else { return f64::NAN };
|
||
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
|
||
if hex.is_empty() || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
|
||
return f64::NAN;
|
||
}
|
||
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..]),
|
||
Some(b'-') => (-1.0, &s[1..]),
|
||
_ => (1.0, s.as_str()),
|
||
};
|
||
if body == "Infinity" {
|
||
return sign * f64::INFINITY;
|
||
}
|
||
if !is_decimal_literal(body) {
|
||
return f64::NAN;
|
||
}
|
||
body.parse::<f64>().map_or(f64::NAN, |n| sign * n)
|
||
}
|
||
|
||
/// `body` is a StrUnsignedDecimalLiteral without `Infinity`: digits with an optional fraction and
|
||
/// exponent, at least one digit in the mantissa.
|
||
fn is_decimal_literal(body: &str) -> bool {
|
||
let b = body.as_bytes();
|
||
let mut i = 0;
|
||
let digits = |i: &mut usize| {
|
||
let s = *i;
|
||
while b.get(*i).is_some_and(u8::is_ascii_digit) {
|
||
*i += 1;
|
||
}
|
||
*i - s
|
||
};
|
||
let mut mantissa = digits(&mut i);
|
||
if b.get(i) == Some(&b'.') {
|
||
i += 1;
|
||
mantissa += digits(&mut i);
|
||
}
|
||
if mantissa == 0 {
|
||
return false;
|
||
}
|
||
if matches!(b.get(i), Some(b'e' | b'E')) {
|
||
i += 1;
|
||
if matches!(b.get(i), Some(b'+' | b'-')) {
|
||
i += 1;
|
||
}
|
||
if digits(&mut i) == 0 {
|
||
return false;
|
||
}
|
||
}
|
||
i == b.len()
|
||
}
|
||
|
||
/// ToString applied to a number (section 9.8.1).
|
||
pub(crate) fn number_to_str(n: f64) -> Str {
|
||
Str::from(crate::number::to_string(n).as_str())
|
||
}
|
||
|
||
/// ToInt32 (section 9.5).
|
||
pub(crate) fn to_int32(n: f64) -> i32 {
|
||
to_uint32(n) as i32
|
||
}
|
||
|
||
/// ToUint32 (section 9.6).
|
||
pub(crate) fn to_uint32(n: f64) -> u32 {
|
||
if !n.is_finite() || n == 0.0 {
|
||
return 0;
|
||
}
|
||
let int = n.trunc();
|
||
let m = int.rem_euclid(4_294_967_296.0);
|
||
m as u32
|
||
}
|
||
|
||
/// The array index a property name stands for (section 15.4: a canonical numeric string below
|
||
/// 2^32 − 1).
|
||
pub(crate) fn array_index(key: &[u16]) -> Option<u32> {
|
||
if key.is_empty() || key.len() > 10 || (key.len() > 1 && key.first() == Some(&u16::from(b'0'))) {
|
||
return None;
|
||
}
|
||
let mut n: u64 = 0;
|
||
for &u in key {
|
||
let d = u.checked_sub(u16::from(b'0')).filter(|d| *d <= 9)?;
|
||
n = n * 10 + u64::from(d);
|
||
}
|
||
u32::try_from(n).ok().filter(|&n| n != u32::MAX)
|
||
}
|
||
|
||
/// The decimal digits of an array index.
|
||
pub(crate) fn index_key(i: u32) -> Rc<[u16]> {
|
||
i.to_string().encode_utf16().collect::<Vec<u16>>().into()
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::{Str, array_index, string_to_number, to_int32, to_uint32};
|
||
|
||
fn u(s: &str) -> Vec<u16> {
|
||
s.encode_utf16().collect()
|
||
}
|
||
|
||
#[test]
|
||
fn strings_convert_to_numbers_by_the_string_numeric_literal_grammar() {
|
||
let cases: &[(&str, f64)] = &[
|
||
("", 0.0),
|
||
(" \n ", 0.0),
|
||
("12", 12.0),
|
||
(" -1.5e2 ", -150.0),
|
||
("0x1F", 31.0),
|
||
(".5", 0.5),
|
||
("5.", 5.0),
|
||
("+Infinity", f64::INFINITY),
|
||
("-Infinity", f64::NEG_INFINITY),
|
||
];
|
||
for (s, want) in cases {
|
||
assert_eq!(string_to_number(&u(s)), *want, "{s:?}");
|
||
}
|
||
for s in ["inf", "nan", "1_0", "0x", "1e", ".", "--1", "0x-1", "infinity", "1 2"] {
|
||
assert!(string_to_number(&u(s)).is_nan(), "{s:?}");
|
||
}
|
||
}
|
||
|
||
#[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);
|
||
assert_eq!(to_int32(2_147_483_648.0), i32::MIN);
|
||
assert_eq!(to_uint32(-1.0), u32::MAX);
|
||
assert_eq!(to_int32(f64::NAN), 0);
|
||
assert_eq!(to_int32(-1.9), -1);
|
||
}
|
||
|
||
#[test]
|
||
fn array_indices_are_canonical_and_below_the_largest_u32() {
|
||
assert_eq!(array_index(&u("0")), Some(0));
|
||
assert_eq!(array_index(&u("4294967294")), Some(4_294_967_294));
|
||
assert_eq!(array_index(&u("4294967295")), None);
|
||
assert_eq!(array_index(&u("01")), None);
|
||
assert_eq!(array_index(&u("1.0")), None);
|
||
assert_eq!(array_index(&u("")), None);
|
||
}
|
||
|
||
#[test]
|
||
fn a_million_appends_flatten_and_drop_without_recursion() {
|
||
let handle = std::thread::Builder::new()
|
||
.stack_size(256 << 10)
|
||
.spawn(|| {
|
||
let x = Str::from("abcdefghijklmnopqrstuvwxyz0123456789");
|
||
let mut s = Str::from("");
|
||
for _ in 0..1_000_000 {
|
||
s = Str::concat(&s, &x);
|
||
}
|
||
let len = s.len();
|
||
let flat = s.flat();
|
||
(len, flat.len())
|
||
})
|
||
.unwrap();
|
||
assert_eq!(handle.join().unwrap(), (36_000_000, 36_000_000));
|
||
let handle = std::thread::Builder::new()
|
||
.stack_size(256 << 10)
|
||
.spawn(|| {
|
||
let x = Str::from("abcdefghijklmnopqrstuvwxyz0123456789");
|
||
let mut s = Str::from("");
|
||
for _ in 0..1_000_000 {
|
||
s = Str::concat(&x, &s);
|
||
}
|
||
s.len()
|
||
})
|
||
.unwrap();
|
||
assert_eq!(handle.join().unwrap(), 36_000_000);
|
||
}
|
||
}
|