mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 04:54:45 +00:00
JavaScript: String, Number, RegExp and Date built-ins
A realm now has String, Number, RegExp and Date as ECMAScript 5.1 sections 15.5, 15.7, 15.9 and 15.10 define them, with Annex B's substr, getYear, setYear and toGMTString. - String: every ES5 prototype method; split, replace, match and search take strings and RegExps. Each new string's length is checked against the heap limit before it is built, and scans take fuel. - Number: toString(radix), toFixed, toExponential and toPrecision format from the number's exact decimal value. - RegExp: a backtracking matcher keeps its choice points and capture state on the heap, within the heap room left, and takes fuel for every step, so a catastrophic pattern stops at the fuel limit. Patterns nested past Options::max_depth give a SyntaxError. Flags g, i and m, lastIndex, exec, test and toString. - Date: chrono converts days to calendar dates, shifting by whole 400-year cycles past its years. Realm::set_clock and Realm::set_time_zone set the clock and local time (UTC by default). Without a hint, a Date converts to a primitive as a string. cargo xtask test262 also runs the suite's built-ins tests for the four and prints their counts.
This commit is contained in:
Generated
+1
@@ -6337,6 +6337,7 @@ dependencies = [
|
||||
name = "vectorcraft-js"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"proptest",
|
||||
"unicode-general-category",
|
||||
]
|
||||
|
||||
@@ -10,6 +10,7 @@ rust-version.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true }
|
||||
unicode-general-category = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! The built-in objects the interpreter itself needs (ECMA-262 5.1 section 15): `Object` and the
|
||||
//! basic methods of `Object.prototype`, `Function` with `Function.prototype.call`, `apply` and
|
||||
//! `toString`, `eval`, the error constructors and `Error.prototype.toString`, and the prototypes
|
||||
//! primitive values look properties up on. The rest of the standard library is not here yet.
|
||||
//! primitive values look properties up on. `String`, `Number`, `RegExp` and `Date` are in their
|
||||
//! own modules; the rest of the standard library is not here yet.
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
@@ -16,24 +17,24 @@ fn alloc(interp: &mut Interp, class: Class, proto: Option<ObjId>) -> ObjId {
|
||||
}
|
||||
|
||||
/// A built-in function object.
|
||||
fn native(interp: &mut Interp, name: &'static str, f: NativeFn, length: u32, kind: NativeKind, constructor: bool) -> JsResult<ObjId> {
|
||||
pub(crate) fn native(interp: &mut Interp, name: &'static str, f: NativeFn, length: u32, kind: NativeKind, constructor: bool) -> JsResult<ObjId> {
|
||||
let proto = interp.intrinsics.function_proto;
|
||||
let o = alloc(interp, Class::Function(Func::Native { f, kind, constructor, name }), Some(proto));
|
||||
interp.define(o, key("length"), Prop::Data { value: Value::Number(f64::from(length)), flags: 0 })?;
|
||||
Ok(o)
|
||||
}
|
||||
|
||||
fn method(interp: &mut Interp, on: ObjId, name: &'static str, f: NativeFn, length: u32) -> JsResult<()> {
|
||||
pub(crate) fn method(interp: &mut Interp, on: ObjId, name: &'static str, f: NativeFn, length: u32) -> JsResult<()> {
|
||||
let m = native(interp, name, f, length, NativeKind::Plain, false)?;
|
||||
interp.define(on, key(name), Prop::Data { value: Value::Object(m), flags: HIDDEN })
|
||||
}
|
||||
|
||||
fn data(interp: &mut Interp, on: ObjId, name: &str, value: Value, flags: u8) -> JsResult<()> {
|
||||
pub(crate) fn data(interp: &mut Interp, on: ObjId, name: &str, value: Value, flags: u8) -> JsResult<()> {
|
||||
interp.define(on, key(name), Prop::Data { value, flags })
|
||||
}
|
||||
|
||||
/// Links a constructor and its prototype and makes the constructor a global.
|
||||
fn constructor(interp: &mut Interp, name: &'static str, ctor: ObjId, proto: ObjId) -> JsResult<()> {
|
||||
pub(crate) fn constructor(interp: &mut Interp, name: &'static str, ctor: ObjId, proto: ObjId) -> JsResult<()> {
|
||||
data(interp, ctor, "prototype", Value::Object(proto), 0)?;
|
||||
data(interp, proto, "constructor", Value::Object(ctor), HIDDEN)?;
|
||||
let global = interp.global;
|
||||
@@ -65,7 +66,7 @@ fn try_install(interp: &mut Interp) -> JsResult<()> {
|
||||
}
|
||||
interp.intrinsics.array_proto = alloc(interp, Class::Array { length: 0, length_writable: true }, Some(object_proto));
|
||||
interp.intrinsics.string_proto = alloc(interp, Class::String(Str::from("")), Some(object_proto));
|
||||
interp.intrinsics.number_proto = alloc(interp, Class::Number, Some(object_proto));
|
||||
interp.intrinsics.number_proto = alloc(interp, Class::Number(0.0), Some(object_proto));
|
||||
interp.intrinsics.boolean_proto = alloc(interp, Class::Boolean, Some(object_proto));
|
||||
interp.intrinsics.regexp_proto = alloc(interp, Class::Ordinary, Some(object_proto));
|
||||
let thrower = native(interp, "", throw_type_error, 0, NativeKind::Plain, false)?;
|
||||
@@ -98,7 +99,11 @@ fn try_install(interp: &mut Interp) -> JsResult<()> {
|
||||
data(interp, function_proto, "apply", Value::Object(apply), HIDDEN)?;
|
||||
method(interp, function_proto, "toString", function_to_string, 0)?;
|
||||
|
||||
install_errors(interp, object_proto)
|
||||
install_errors(interp, object_proto)?;
|
||||
crate::string_builtins::install(interp)?;
|
||||
crate::number_builtins::install(interp)?;
|
||||
crate::regexp_builtins::install(interp)?;
|
||||
crate::date_builtins::install(interp)
|
||||
}
|
||||
|
||||
fn install_errors(interp: &mut Interp, object_proto: ObjId) -> JsResult<()> {
|
||||
@@ -132,7 +137,7 @@ fn install_errors(interp: &mut Interp, object_proto: ObjId) -> JsResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn arg(args: &[Value], i: usize) -> Value {
|
||||
pub(crate) fn arg(args: &[Value], i: usize) -> Value {
|
||||
args.get(i).cloned().unwrap_or(Value::Undefined)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,842 @@
|
||||
//! `Date` (ECMA-262 5.1 section 15.9), with `getYear`, `setYear` and `toGMTString` from Annex B.
|
||||
//!
|
||||
//! Time values are milliseconds since 1970-01-01 UTC (section 15.9.1). `chrono` turns days into
|
||||
//! calendar dates and back; past the years it represents, dates are shifted by whole 400-year
|
||||
//! cycles. The current time comes from the host's clock and local time from the host's
|
||||
//! time zone ([`DateHost`]): by default the system clock (none in a wasm build, where `Date.now()`
|
||||
//! is NaN until the host sets one) and UTC. `Date.parse` reads the format of section 15.9.1.15
|
||||
//! and the formats `toString` and `toUTCString` write.
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use chrono::{Datelike, NaiveDate};
|
||||
|
||||
use crate::builtins::{arg, constructor, data, method, native};
|
||||
use crate::heap::{Class, NativeFn, NativeKind, Object, key};
|
||||
use crate::interp::{ErrorType, Hint, Interp, JsResult};
|
||||
use crate::string_builtins::{charge_units, reserve_units, to_integer};
|
||||
use crate::value::{Str, Value};
|
||||
|
||||
/// The clock and time zone a realm's `Date` reads.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DateHost {
|
||||
/// The current time value.
|
||||
pub(crate) clock: Rc<dyn Fn() -> f64>,
|
||||
/// The offset of local time from UTC, in milliseconds, at a UTC time value.
|
||||
pub(crate) offset: Rc<dyn Fn(f64) -> f64>,
|
||||
}
|
||||
|
||||
impl Default for DateHost {
|
||||
fn default() -> Self {
|
||||
Self { clock: Rc::new(system_clock), offset: Rc::new(|_| 0.0) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn system_clock() -> f64 {
|
||||
match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
|
||||
Ok(d) => d.as_millis() as f64,
|
||||
Err(e) => -(e.duration().as_millis() as f64),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn system_clock() -> f64 {
|
||||
f64::NAN
|
||||
}
|
||||
|
||||
const MS_PER_DAY: f64 = 86_400_000.0;
|
||||
const MS_PER_HOUR: f64 = 3_600_000.0;
|
||||
const MS_PER_MINUTE: f64 = 60_000.0;
|
||||
const MS_PER_SECOND: f64 = 1000.0;
|
||||
|
||||
const DAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
const MONTHS: [&str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||
|
||||
// ---- section 15.9.1 ----
|
||||
|
||||
fn day(t: f64) -> f64 {
|
||||
(t / MS_PER_DAY).floor()
|
||||
}
|
||||
|
||||
fn time_within_day(t: f64) -> f64 {
|
||||
t.rem_euclid(MS_PER_DAY)
|
||||
}
|
||||
|
||||
/// `chrono`'s day number (0001-01-01 is day 1) of 1970-01-01.
|
||||
const EPOCH_CE_DAYS: i64 = 719_163;
|
||||
/// The days in 400 Gregorian years, after which the calendar repeats.
|
||||
const CYCLE_DAYS: i64 = 146_097;
|
||||
/// The days from 1970-01-01 to 2000-01-01, where the 400-year cycle `chrono` is asked about starts.
|
||||
const DAYS_TO_2000: i64 = 10_957;
|
||||
|
||||
/// The year, the month (0 to 11) and the day of the month of a day (days since 1970-01-01).
|
||||
/// `chrono` computes them for the matching day of the 400-year cycle starting in 2000, and the
|
||||
/// whole cycles are added back to the year, so days past `chrono`'s range have dates too.
|
||||
fn civil(day: f64) -> Option<(f64, f64, f64)> {
|
||||
if !day.is_finite() || day.abs() > 1e9 {
|
||||
return None;
|
||||
}
|
||||
let d = day as i64;
|
||||
let cycles = (d - DAYS_TO_2000).div_euclid(CYCLE_DAYS);
|
||||
let date = NaiveDate::from_num_days_from_ce_opt(i32::try_from(d - cycles * CYCLE_DAYS + EPOCH_CE_DAYS).ok()?)?;
|
||||
Some((f64::from(date.year()) + 400.0 * cycles as f64, f64::from(date.month0()), f64::from(date.day())))
|
||||
}
|
||||
|
||||
/// The year of the 400-year cycle starting in 2000 that has `year`'s calendar, and the number of
|
||||
/// whole cycles between the two.
|
||||
fn cycle_year(year: f64) -> Option<(i32, i64)> {
|
||||
if !year.is_finite() || year.abs() > 1e7 {
|
||||
return None;
|
||||
}
|
||||
let y = year as i64;
|
||||
let cycles = (y - 2000).div_euclid(400);
|
||||
Some((i32::try_from(y - 400 * cycles).ok()?, cycles))
|
||||
}
|
||||
|
||||
/// The day (days since 1970-01-01) on which month `month0` (0 to 11) of `year` starts.
|
||||
fn first_day_of_month(year: f64, month0: u32) -> Option<f64> {
|
||||
let (y, cycles) = cycle_year(year)?;
|
||||
let date = NaiveDate::from_ymd_opt(y, month0 + 1, 1)?;
|
||||
Some((i64::from(date.num_days_from_ce()) - EPOCH_CE_DAYS + cycles * CYCLE_DAYS) as f64)
|
||||
}
|
||||
|
||||
/// Month `month` (1 to 12) of `year` has a day `date`.
|
||||
fn is_date(year: f64, month: f64, date: f64) -> bool {
|
||||
(1.0..=12.0).contains(&month)
|
||||
&& (1.0..=31.0).contains(&date)
|
||||
&& cycle_year(year).is_some_and(|(y, _)| NaiveDate::from_ymd_opt(y, month as u32, date as u32).is_some())
|
||||
}
|
||||
|
||||
fn year_from_time(t: f64) -> f64 {
|
||||
civil(day(t)).map_or(f64::NAN, |(y, _, _)| y)
|
||||
}
|
||||
|
||||
fn month_from_time(t: f64) -> f64 {
|
||||
civil(day(t)).map_or(f64::NAN, |(_, m, _)| m)
|
||||
}
|
||||
|
||||
fn date_from_time(t: f64) -> f64 {
|
||||
civil(day(t)).map_or(f64::NAN, |(_, _, d)| d)
|
||||
}
|
||||
|
||||
fn week_day(t: f64) -> f64 {
|
||||
(day(t) + 4.0).rem_euclid(7.0)
|
||||
}
|
||||
|
||||
fn hour_from_time(t: f64) -> f64 {
|
||||
(t / MS_PER_HOUR).floor().rem_euclid(24.0)
|
||||
}
|
||||
|
||||
fn min_from_time(t: f64) -> f64 {
|
||||
(t / MS_PER_MINUTE).floor().rem_euclid(60.0)
|
||||
}
|
||||
|
||||
fn sec_from_time(t: f64) -> f64 {
|
||||
(t / MS_PER_SECOND).floor().rem_euclid(60.0)
|
||||
}
|
||||
|
||||
fn ms_from_time(t: f64) -> f64 {
|
||||
t.rem_euclid(MS_PER_SECOND)
|
||||
}
|
||||
|
||||
fn make_time(h: f64, m: f64, s: f64, ms: f64) -> f64 {
|
||||
if !(h.is_finite() && m.is_finite() && s.is_finite() && ms.is_finite()) {
|
||||
return f64::NAN;
|
||||
}
|
||||
to_integer(h) * MS_PER_HOUR + to_integer(m) * MS_PER_MINUTE + to_integer(s) * MS_PER_SECOND + to_integer(ms)
|
||||
}
|
||||
|
||||
fn make_day(year: f64, month: f64, date: f64) -> f64 {
|
||||
if !(year.is_finite() && month.is_finite() && date.is_finite()) {
|
||||
return f64::NAN;
|
||||
}
|
||||
let (y, m, dt) = (to_integer(year), to_integer(month), to_integer(date));
|
||||
let ym = y + (m / 12.0).floor();
|
||||
// Years this far out are past every time value TimeClip keeps.
|
||||
if ym.abs() > 400_000.0 {
|
||||
return f64::NAN;
|
||||
}
|
||||
first_day_of_month(ym, m.rem_euclid(12.0) as u32).map_or(f64::NAN, |first| first + dt - 1.0)
|
||||
}
|
||||
|
||||
fn make_date(day: f64, time: f64) -> f64 {
|
||||
if !(day.is_finite() && time.is_finite()) {
|
||||
return f64::NAN;
|
||||
}
|
||||
day * MS_PER_DAY + time
|
||||
}
|
||||
|
||||
fn time_clip(t: f64) -> f64 {
|
||||
if !t.is_finite() || t.abs() > 8.64e15 { f64::NAN } else { to_integer(t) }
|
||||
}
|
||||
|
||||
impl Interp {
|
||||
/// The host's offset of local time from UTC at `utc`; an offset that is not finite or is a
|
||||
/// day or more counts as 0.
|
||||
fn local_offset(&self, utc: f64) -> f64 {
|
||||
let o = (self.date_host.offset)(utc);
|
||||
if o.is_finite() && o.abs() < MS_PER_DAY { o } else { 0.0 }
|
||||
}
|
||||
|
||||
/// LocalTime (section 15.9.1.9).
|
||||
fn local_time(&self, t: f64) -> f64 {
|
||||
t + self.local_offset(t)
|
||||
}
|
||||
|
||||
/// UTC (section 15.9.1.9): the time value of a local time.
|
||||
fn utc(&self, t: f64) -> f64 {
|
||||
if !t.is_finite() {
|
||||
return f64::NAN;
|
||||
}
|
||||
let guess = t - self.local_offset(t);
|
||||
t - self.local_offset(guess)
|
||||
}
|
||||
|
||||
fn now(&self) -> f64 {
|
||||
time_clip((self.date_host.clock)())
|
||||
}
|
||||
}
|
||||
|
||||
// ---- formatting ----
|
||||
|
||||
fn year_text(y: f64) -> String {
|
||||
if y < 0.0 { format!("-{:06}", -y as i64) } else { format!("{:04}", y as i64) }
|
||||
}
|
||||
|
||||
fn offset_text(offset_ms: f64) -> String {
|
||||
let minutes = (offset_ms / MS_PER_MINUTE).round() as i64;
|
||||
let sign = if minutes < 0 { '-' } else { '+' };
|
||||
format!("GMT{sign}{:02}{:02}", minutes.abs() / 60, minutes.abs() % 60)
|
||||
}
|
||||
|
||||
fn date_text(t: f64) -> String {
|
||||
let wd = DAYS.get(week_day(t) as usize).copied().unwrap_or("");
|
||||
let mon = MONTHS.get(month_from_time(t) as usize).copied().unwrap_or("");
|
||||
format!("{wd} {mon} {:02} {}", date_from_time(t) as i64, year_text(year_from_time(t)))
|
||||
}
|
||||
|
||||
fn time_text(t: f64) -> String {
|
||||
format!("{:02}:{:02}:{:02}", hour_from_time(t) as i64, min_from_time(t) as i64, sec_from_time(t) as i64)
|
||||
}
|
||||
|
||||
impl Interp {
|
||||
/// `toString`'s format: `Tue Nov 07 2026 12:00:00 GMT+0100`.
|
||||
fn date_to_string(&self, t: f64) -> String {
|
||||
if t.is_nan() {
|
||||
return "Invalid Date".into();
|
||||
}
|
||||
let local = self.local_time(t);
|
||||
format!("{} {} {}", date_text(local), time_text(local), offset_text(local - t))
|
||||
}
|
||||
}
|
||||
|
||||
fn utc_text(t: f64) -> String {
|
||||
if t.is_nan() {
|
||||
return "Invalid Date".into();
|
||||
}
|
||||
let wd = DAYS.get(week_day(t) as usize).copied().unwrap_or("");
|
||||
let mon = MONTHS.get(month_from_time(t) as usize).copied().unwrap_or("");
|
||||
format!("{wd}, {:02} {mon} {} {} GMT", date_from_time(t) as i64, year_text(year_from_time(t)), time_text(t))
|
||||
}
|
||||
|
||||
fn iso_text(t: f64) -> String {
|
||||
let y = year_from_time(t);
|
||||
let year = if (0.0..=9999.0).contains(&y) {
|
||||
format!("{:04}", y as i64)
|
||||
} else if y < 0.0 {
|
||||
format!("-{:06}", -y as i64)
|
||||
} else {
|
||||
format!("+{:06}", y as i64)
|
||||
};
|
||||
format!("{year}-{:02}-{:02}T{}.{:03}Z", month_from_time(t) as i64 + 1, date_from_time(t) as i64, time_text(t), ms_from_time(t) as i64)
|
||||
}
|
||||
|
||||
// ---- parsing ----
|
||||
|
||||
/// Reads the format of section 15.9.1.15: `YYYY[-MM[-DD]][THH:mm[:ss[.sss]][Z|±HH:mm]]`, with
|
||||
/// six-digit signed years. A missing offset means UTC.
|
||||
fn parse_iso(s: &[u8]) -> Option<f64> {
|
||||
let mut i = 0;
|
||||
let num = |i: &mut usize, n: usize| -> Option<f64> {
|
||||
let part = s.get(*i..*i + n)?;
|
||||
if !part.iter().all(u8::is_ascii_digit) {
|
||||
return None;
|
||||
}
|
||||
*i += n;
|
||||
Some(part.iter().fold(0.0, |v, d| v * 10.0 + f64::from(d - b'0')))
|
||||
};
|
||||
let year = match s.first() {
|
||||
Some(b'+') | Some(b'-') => {
|
||||
let neg = s.first() == Some(&b'-');
|
||||
i = 1;
|
||||
let y = num(&mut i, 6)?;
|
||||
if neg && y == 0.0 {
|
||||
return None;
|
||||
}
|
||||
if neg { -y } else { y }
|
||||
}
|
||||
_ => num(&mut i, 4)?,
|
||||
};
|
||||
let (mut month, mut date) = (1.0, 1.0);
|
||||
if s.get(i) == Some(&b'-') {
|
||||
i += 1;
|
||||
month = num(&mut i, 2)?;
|
||||
if s.get(i) == Some(&b'-') {
|
||||
i += 1;
|
||||
date = num(&mut i, 2)?;
|
||||
}
|
||||
}
|
||||
let (mut h, mut m, mut sec, mut ms) = (0.0, 0.0, 0.0, 0.0);
|
||||
let mut offset = 0.0;
|
||||
if s.get(i) == Some(&b'T') {
|
||||
i += 1;
|
||||
h = num(&mut i, 2)?;
|
||||
if s.get(i) != Some(&b':') {
|
||||
return None;
|
||||
}
|
||||
i += 1;
|
||||
m = num(&mut i, 2)?;
|
||||
if s.get(i) == Some(&b':') {
|
||||
i += 1;
|
||||
sec = num(&mut i, 2)?;
|
||||
if s.get(i) == Some(&b'.') {
|
||||
i += 1;
|
||||
let start = i;
|
||||
while s.get(i).is_some_and(u8::is_ascii_digit) {
|
||||
i += 1;
|
||||
}
|
||||
let digits = s.get(start..i)?;
|
||||
if digits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
ms = digits.iter().take(3).fold(0.0, |v, d| v * 10.0 + f64::from(d - b'0')) * 10f64.powi(3 - digits.len().min(3) as i32);
|
||||
}
|
||||
}
|
||||
match s.get(i) {
|
||||
Some(b'Z') => i += 1,
|
||||
Some(&c @ (b'+' | b'-')) => {
|
||||
i += 1;
|
||||
let oh = num(&mut i, 2)?;
|
||||
if s.get(i) != Some(&b':') {
|
||||
return None;
|
||||
}
|
||||
i += 1;
|
||||
let om = num(&mut i, 2)?;
|
||||
if oh > 23.0 || om > 59.0 {
|
||||
return None;
|
||||
}
|
||||
offset = (oh * 60.0 + om) * MS_PER_MINUTE * if c == b'-' { -1.0 } else { 1.0 };
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if i != s.len() {
|
||||
return None;
|
||||
}
|
||||
let valid = is_date(year, month, date) && (h < 24.0 || (h == 24.0 && m == 0.0 && sec == 0.0 && ms == 0.0)) && m < 60.0 && sec < 60.0;
|
||||
if !valid {
|
||||
return None;
|
||||
}
|
||||
let t = make_date(make_day(year, month - 1.0, date), make_time(h, m, sec, ms)) - offset;
|
||||
Some(time_clip(t))
|
||||
}
|
||||
|
||||
/// Reads the formats `toString` and `toUTCString` write: a weekday name, a month name, a day, a
|
||||
/// year, an optional `HH:mm[:ss]` time and an optional `GMT[±hhmm]` offset, in either order of
|
||||
/// day and month. Without an offset the time is local.
|
||||
fn parse_text(interp: &Interp, s: &str) -> Option<f64> {
|
||||
let mut words: Vec<&str> = Vec::new();
|
||||
for w in s.split(|c: char| c.is_whitespace() || c == ',') {
|
||||
if w.starts_with('(') {
|
||||
break;
|
||||
}
|
||||
if !w.is_empty() {
|
||||
words.push(w);
|
||||
}
|
||||
}
|
||||
let (mut month, mut date, mut year) = (None, None, None);
|
||||
let (mut h, mut m, mut sec) = (0.0, 0.0, 0.0);
|
||||
let mut offset: Option<f64> = None;
|
||||
for w in words {
|
||||
let lower = w.to_ascii_lowercase();
|
||||
if DAYS.iter().any(|d| lower.starts_with(&d.to_ascii_lowercase())) && month.is_none() && date.is_none() && w.len() >= 3 && !w.contains(':') {
|
||||
continue;
|
||||
}
|
||||
if let Some(mi) = MONTHS.iter().position(|mo| lower.starts_with(&mo.to_ascii_lowercase())) {
|
||||
month = Some(mi as f64);
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = lower.strip_prefix("gmt").or_else(|| lower.strip_prefix("utc")).or_else(|| (lower == "z").then_some("")) {
|
||||
if rest.is_empty() {
|
||||
offset = Some(0.0);
|
||||
continue;
|
||||
}
|
||||
let (sign, digits) = match rest.as_bytes().first() {
|
||||
Some(b'+') => (1.0, rest.get(1..)?),
|
||||
Some(b'-') => (-1.0, rest.get(1..)?),
|
||||
_ => return None,
|
||||
};
|
||||
let digits = digits.replace(':', "");
|
||||
if digits.len() != 4 || !digits.bytes().all(|b| b.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
let v: f64 = digits.parse().ok()?;
|
||||
offset = Some(sign * ((v / 100.0).floor() * 60.0 + v % 100.0) * MS_PER_MINUTE);
|
||||
continue;
|
||||
}
|
||||
if w.contains(':') {
|
||||
let parts: Vec<&str> = w.split(':').collect();
|
||||
let field = |i: usize| -> Option<f64> { parts.get(i).map(|p| p.parse::<f64>().ok()).unwrap_or(Some(0.0)) };
|
||||
if parts.len() < 2 || parts.len() > 3 || !parts.iter().all(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit())) {
|
||||
return None;
|
||||
}
|
||||
(h, m, sec) = (field(0)?, field(1)?, field(2)?);
|
||||
if h > 24.0 || m > 59.0 || sec > 59.0 {
|
||||
return None;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let (neg, digits) = match w.strip_prefix('-') {
|
||||
Some(d) => (true, d),
|
||||
None => (false, w),
|
||||
};
|
||||
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
let v: f64 = digits.parse().ok()?;
|
||||
if date.is_none() && !neg && digits.len() <= 2 {
|
||||
date = Some(v);
|
||||
} else if year.is_none() {
|
||||
year = Some(if neg { -v } else { v });
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let (month, date, year) = (month?, date?, year?);
|
||||
if !(1.0..=31.0).contains(&date) {
|
||||
return None;
|
||||
}
|
||||
let local = make_date(make_day(year, month, date), make_time(h, m, sec, 0.0));
|
||||
let t = match offset {
|
||||
Some(o) => local - o,
|
||||
None => interp.utc(local),
|
||||
};
|
||||
Some(time_clip(t))
|
||||
}
|
||||
|
||||
/// Takes fuel for the units of `s` the parsers read.
|
||||
fn parse_date(interp: &mut Interp, s: &Str) -> JsResult<f64> {
|
||||
charge_units(interp, s.len())?;
|
||||
let text = s.to_rust_string();
|
||||
let trimmed = text.trim();
|
||||
Ok(parse_iso(trimmed.as_bytes()).or_else(|| parse_text(interp, trimmed)).unwrap_or(f64::NAN))
|
||||
}
|
||||
|
||||
// ---- the constructor ----
|
||||
|
||||
pub(crate) fn install(interp: &mut Interp) -> JsResult<()> {
|
||||
let object_proto = interp.intrinsics.object_proto;
|
||||
let proto = interp.heap.alloc_object(Object::new(Class::Date(f64::NAN), Some(object_proto)));
|
||||
interp.intrinsics.date_proto = proto;
|
||||
let ctor = native(interp, "Date", date_ctor, 7, NativeKind::Plain, true)?;
|
||||
constructor(interp, "Date", ctor, proto)?;
|
||||
method(interp, ctor, "parse", date_parse, 1)?;
|
||||
method(interp, ctor, "UTC", date_utc, 7)?;
|
||||
method(interp, ctor, "now", date_now, 0)?;
|
||||
let methods: [(&'static str, NativeFn, u32); 44] = [
|
||||
("toString", to_string, 0),
|
||||
("toDateString", to_date_string, 0),
|
||||
("toTimeString", to_time_string, 0),
|
||||
("toLocaleString", to_string, 0),
|
||||
("toLocaleDateString", to_date_string, 0),
|
||||
("toLocaleTimeString", to_time_string, 0),
|
||||
("valueOf", get_time, 0),
|
||||
("getTime", get_time, 0),
|
||||
("getFullYear", get_full_year, 0),
|
||||
("getUTCFullYear", get_utc_full_year, 0),
|
||||
("getMonth", get_month, 0),
|
||||
("getUTCMonth", get_utc_month, 0),
|
||||
("getDate", get_date, 0),
|
||||
("getUTCDate", get_utc_date, 0),
|
||||
("getDay", get_day, 0),
|
||||
("getUTCDay", get_utc_day, 0),
|
||||
("getHours", get_hours, 0),
|
||||
("getUTCHours", get_utc_hours, 0),
|
||||
("getMinutes", get_minutes, 0),
|
||||
("getUTCMinutes", get_utc_minutes, 0),
|
||||
("getSeconds", get_seconds, 0),
|
||||
("getUTCSeconds", get_utc_seconds, 0),
|
||||
("getMilliseconds", get_milliseconds, 0),
|
||||
("getUTCMilliseconds", get_utc_milliseconds, 0),
|
||||
("getTimezoneOffset", get_timezone_offset, 0),
|
||||
("setTime", set_time, 1),
|
||||
("setMilliseconds", set_milliseconds, 1),
|
||||
("setUTCMilliseconds", set_utc_milliseconds, 1),
|
||||
("setSeconds", set_seconds, 2),
|
||||
("setUTCSeconds", set_utc_seconds, 2),
|
||||
("setMinutes", set_minutes, 3),
|
||||
("setUTCMinutes", set_utc_minutes, 3),
|
||||
("setHours", set_hours, 4),
|
||||
("setUTCHours", set_utc_hours, 4),
|
||||
("setDate", set_date, 1),
|
||||
("setUTCDate", set_utc_date, 1),
|
||||
("setMonth", set_month, 2),
|
||||
("setUTCMonth", set_utc_month, 2),
|
||||
("setFullYear", set_full_year, 3),
|
||||
("setUTCFullYear", set_utc_full_year, 3),
|
||||
("toISOString", to_iso_string, 0),
|
||||
("toJSON", to_json, 1),
|
||||
("getYear", get_year, 0),
|
||||
("setYear", set_year, 1),
|
||||
];
|
||||
for (name, f, len) in methods {
|
||||
method(interp, proto, name, f, len)?;
|
||||
}
|
||||
// Annex B.2.6: `toGMTString` is the same function object as `toUTCString`.
|
||||
method(interp, proto, "toUTCString", to_utc_string, 0)?;
|
||||
let utc = interp.get_value(&Value::Object(proto), &key("toUTCString"))?;
|
||||
data(interp, proto, "toGMTString", utc, crate::heap::HIDDEN)
|
||||
}
|
||||
|
||||
fn text(interp: &mut Interp, s: &str) -> JsResult<Value> {
|
||||
reserve_units(interp, s.len())?;
|
||||
Ok(Value::String(Str::from(s)))
|
||||
}
|
||||
|
||||
/// The numbers of the arguments from `year` on, as `new Date(y, m, …)` and `Date.UTC` read them
|
||||
/// (sections 15.9.3.1 and 15.9.4.3): the local or UTC date they describe, before TimeClip. An
|
||||
/// omitted month is January, which `Date.UTC(year)` uses (section 15.9.4.3 leaves a call with
|
||||
/// fewer than two arguments to the implementation).
|
||||
fn date_from_parts(interp: &mut Interp, args: &[Value]) -> JsResult<f64> {
|
||||
let mut n = [f64::NAN, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0];
|
||||
for (i, slot) in n.iter_mut().enumerate() {
|
||||
if let Some(v) = args.get(i) {
|
||||
*slot = interp.to_number(v)?;
|
||||
}
|
||||
}
|
||||
let [y, m, dt, h, min, s, ms] = n;
|
||||
let yi = to_integer(y);
|
||||
let yr = if !y.is_nan() && (0.0..=99.0).contains(&yi) { 1900.0 + yi } else { y };
|
||||
Ok(make_date(make_day(yr, m, dt), make_time(h, min, s, ms)))
|
||||
}
|
||||
|
||||
/// `Date(…)`, `new Date()`, `new Date(value)` and `new Date(y, m, …)` (sections 15.9.2 and
|
||||
/// 15.9.3).
|
||||
fn date_ctor(interp: &mut Interp, _: Value, args: &[Value], construct: bool) -> JsResult<Value> {
|
||||
if !construct {
|
||||
let s = interp.date_to_string(interp.now());
|
||||
return text(interp, &s);
|
||||
}
|
||||
let tv = match args.len() {
|
||||
0 => interp.now(),
|
||||
1 => {
|
||||
let v = interp.to_primitive(&args[0], Hint::Default)?;
|
||||
match &v {
|
||||
Value::String(s) => parse_date(interp, s)?,
|
||||
_ => time_clip(interp.to_number(&v)?),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let local = date_from_parts(interp, args)?;
|
||||
time_clip(interp.utc(local))
|
||||
}
|
||||
};
|
||||
let proto = interp.intrinsics.date_proto;
|
||||
Ok(Value::Object(interp.heap.alloc_object(Object::new(Class::Date(tv), Some(proto)))))
|
||||
}
|
||||
|
||||
fn date_parse(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let s = interp.to_str(&arg(args, 0))?;
|
||||
Ok(Value::Number(parse_date(interp, &s)?))
|
||||
}
|
||||
|
||||
fn date_utc(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let t = date_from_parts(interp, args)?;
|
||||
Ok(Value::Number(time_clip(t)))
|
||||
}
|
||||
|
||||
fn date_now(interp: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
Ok(Value::Number(interp.now()))
|
||||
}
|
||||
|
||||
// ---- Date.prototype ----
|
||||
|
||||
/// The time value of a Date object `this`.
|
||||
fn this_time(interp: &mut Interp, this: &Value) -> JsResult<f64> {
|
||||
if let Some(Class::Date(t)) = this.as_object().and_then(|o| interp.heap.object(o)).map(|o| &o.class) {
|
||||
return Ok(*t);
|
||||
}
|
||||
Err(interp.type_error("Date.prototype method called on a value that is not a Date object"))
|
||||
}
|
||||
|
||||
fn set_this_time(interp: &mut Interp, this: &Value, t: f64) -> JsResult<Value> {
|
||||
if let Some(o) = this.as_object().and_then(|o| interp.heap.object_mut(o))
|
||||
&& let Class::Date(slot) = &mut o.class
|
||||
{
|
||||
*slot = t;
|
||||
}
|
||||
Ok(Value::Number(t))
|
||||
}
|
||||
|
||||
fn to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let t = this_time(interp, &this)?;
|
||||
let s = interp.date_to_string(t);
|
||||
text(interp, &s)
|
||||
}
|
||||
|
||||
fn to_date_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let t = this_time(interp, &this)?;
|
||||
let s = if t.is_nan() { "Invalid Date".into() } else { date_text(interp.local_time(t)) };
|
||||
text(interp, &s)
|
||||
}
|
||||
|
||||
fn to_time_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let t = this_time(interp, &this)?;
|
||||
let s = if t.is_nan() {
|
||||
"Invalid Date".into()
|
||||
} else {
|
||||
let local = interp.local_time(t);
|
||||
format!("{} {}", time_text(local), offset_text(local - t))
|
||||
};
|
||||
text(interp, &s)
|
||||
}
|
||||
|
||||
fn to_utc_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let t = this_time(interp, &this)?;
|
||||
text(interp, &utc_text(t))
|
||||
}
|
||||
|
||||
/// `toISOString` (section 15.9.5.43).
|
||||
fn to_iso_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let t = this_time(interp, &this)?;
|
||||
if !t.is_finite() {
|
||||
return Err(interp.throw(ErrorType::Range, "invalid time value"));
|
||||
}
|
||||
text(interp, &iso_text(t))
|
||||
}
|
||||
|
||||
/// `toJSON` (section 15.9.5.44).
|
||||
fn to_json(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let o = interp.to_object(&this)?;
|
||||
let ov = Value::Object(o);
|
||||
interp.temp_roots.push(ov.clone());
|
||||
let r = (|| {
|
||||
let tv = interp.to_primitive(&ov, Hint::Number)?;
|
||||
if let Value::Number(n) = tv
|
||||
&& !n.is_finite()
|
||||
{
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
let f = interp.get_value(&ov, &key("toISOString"))?;
|
||||
if !interp.is_callable(&f) {
|
||||
return Err(interp.type_error("toISOString is not a function"));
|
||||
}
|
||||
interp.call(&f, &ov, &[])
|
||||
})();
|
||||
interp.temp_roots.pop();
|
||||
r
|
||||
}
|
||||
|
||||
fn get_time(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
this_time(interp, &this).map(Value::Number)
|
||||
}
|
||||
|
||||
/// A getter: `f` of the time value, in local time when `local`, NaN for an invalid date.
|
||||
fn getter(interp: &mut Interp, this: &Value, local: bool, f: fn(f64) -> f64) -> JsResult<Value> {
|
||||
let t = this_time(interp, this)?;
|
||||
if t.is_nan() {
|
||||
return Ok(Value::Number(f64::NAN));
|
||||
}
|
||||
let t = if local { interp.local_time(t) } else { t };
|
||||
Ok(Value::Number(f(t)))
|
||||
}
|
||||
|
||||
macro_rules! getters {
|
||||
($($name:ident, $local:expr, $f:expr;)*) => {
|
||||
$(fn $name(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
getter(interp, &this, $local, $f)
|
||||
})*
|
||||
};
|
||||
}
|
||||
|
||||
getters! {
|
||||
get_full_year, true, year_from_time;
|
||||
get_utc_full_year, false, year_from_time;
|
||||
get_month, true, month_from_time;
|
||||
get_utc_month, false, month_from_time;
|
||||
get_date, true, date_from_time;
|
||||
get_utc_date, false, date_from_time;
|
||||
get_day, true, week_day;
|
||||
get_utc_day, false, week_day;
|
||||
get_hours, true, hour_from_time;
|
||||
get_utc_hours, false, hour_from_time;
|
||||
get_minutes, true, min_from_time;
|
||||
get_utc_minutes, false, min_from_time;
|
||||
get_seconds, true, sec_from_time;
|
||||
get_utc_seconds, false, sec_from_time;
|
||||
get_milliseconds, true, ms_from_time;
|
||||
get_utc_milliseconds, false, ms_from_time;
|
||||
}
|
||||
|
||||
/// `getYear` (Annex B.2.4).
|
||||
fn get_year(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
getter(interp, &this, true, |t| year_from_time(t) - 1900.0)
|
||||
}
|
||||
|
||||
fn get_timezone_offset(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let t = this_time(interp, &this)?;
|
||||
if t.is_nan() {
|
||||
return Ok(Value::Number(f64::NAN));
|
||||
}
|
||||
Ok(Value::Number((t - interp.local_time(t)) / MS_PER_MINUTE))
|
||||
}
|
||||
|
||||
fn set_time(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
this_time(interp, &this)?;
|
||||
let t = interp.to_number(&arg(args, 0))?;
|
||||
set_this_time(interp, &this, time_clip(t))
|
||||
}
|
||||
|
||||
/// The time fields a setter starts at: the setter sets that field and the smaller ones after it
|
||||
/// from its arguments.
|
||||
#[derive(Clone, Copy)]
|
||||
enum TimeField {
|
||||
Hour = 0,
|
||||
Minute = 1,
|
||||
Second = 2,
|
||||
Ms = 3,
|
||||
}
|
||||
|
||||
/// `setHours`, `setMinutes`, `setSeconds` and `setMilliseconds`, local or UTC (sections
|
||||
/// 15.9.5.28 to 15.9.5.35).
|
||||
fn set_time_fields(interp: &mut Interp, this: &Value, args: &[Value], local: bool, first: TimeField, count: usize) -> JsResult<Value> {
|
||||
let tv = this_time(interp, this)?;
|
||||
let t = if local { interp.local_time(tv) } else { tv };
|
||||
let mut fields = [hour_from_time(t), min_from_time(t), sec_from_time(t), ms_from_time(t)];
|
||||
let first = first as usize;
|
||||
for k in 0..count {
|
||||
let given = k == 0 || args.get(k).is_some();
|
||||
if given {
|
||||
let v = interp.to_number(&arg(args, k))?;
|
||||
if let Some(slot) = fields.get_mut(first + k) {
|
||||
*slot = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
let [h, m, s, ms] = fields;
|
||||
let date = make_date(day(t), make_time(h, m, s, ms));
|
||||
let u = time_clip(if local { interp.utc(date) } else { date });
|
||||
set_this_time(interp, this, u)
|
||||
}
|
||||
|
||||
/// The date fields a setter starts at.
|
||||
#[derive(Clone, Copy)]
|
||||
enum DateField {
|
||||
Year = 0,
|
||||
Month = 1,
|
||||
Date = 2,
|
||||
}
|
||||
|
||||
/// `setFullYear`, `setMonth` and `setDate`, local or UTC (sections 15.9.5.36 to 15.9.5.41).
|
||||
fn set_date_fields(interp: &mut Interp, this: &Value, args: &[Value], local: bool, first: DateField, count: usize) -> JsResult<Value> {
|
||||
let tv = this_time(interp, this)?;
|
||||
let mut t = if local { interp.local_time(tv) } else { tv };
|
||||
// setFullYear starts from +0 on an invalid date.
|
||||
if matches!(first, DateField::Year) && t.is_nan() {
|
||||
t = 0.0;
|
||||
}
|
||||
let mut fields = [year_from_time(t), month_from_time(t), date_from_time(t)];
|
||||
let first = first as usize;
|
||||
for k in 0..count {
|
||||
let given = k == 0 || args.get(k).is_some();
|
||||
if given {
|
||||
let v = interp.to_number(&arg(args, k))?;
|
||||
if let Some(slot) = fields.get_mut(first + k) {
|
||||
*slot = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
let [y, m, d] = fields;
|
||||
let date = make_date(make_day(y, m, d), time_within_day(t));
|
||||
let u = time_clip(if local { interp.utc(date) } else { date });
|
||||
set_this_time(interp, this, u)
|
||||
}
|
||||
|
||||
macro_rules! setters {
|
||||
($($name:ident, $f:ident, $local:expr, $field:expr, $count:expr;)*) => {
|
||||
$(fn $name(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
$f(interp, &this, args, $local, $field, $count)
|
||||
})*
|
||||
};
|
||||
}
|
||||
|
||||
setters! {
|
||||
set_milliseconds, set_time_fields, true, TimeField::Ms, 1;
|
||||
set_utc_milliseconds, set_time_fields, false, TimeField::Ms, 1;
|
||||
set_seconds, set_time_fields, true, TimeField::Second, 2;
|
||||
set_utc_seconds, set_time_fields, false, TimeField::Second, 2;
|
||||
set_minutes, set_time_fields, true, TimeField::Minute, 3;
|
||||
set_utc_minutes, set_time_fields, false, TimeField::Minute, 3;
|
||||
set_hours, set_time_fields, true, TimeField::Hour, 4;
|
||||
set_utc_hours, set_time_fields, false, TimeField::Hour, 4;
|
||||
set_date, set_date_fields, true, DateField::Date, 1;
|
||||
set_utc_date, set_date_fields, false, DateField::Date, 1;
|
||||
set_month, set_date_fields, true, DateField::Month, 2;
|
||||
set_utc_month, set_date_fields, false, DateField::Month, 2;
|
||||
set_full_year, set_date_fields, true, DateField::Year, 3;
|
||||
set_utc_full_year, set_date_fields, false, DateField::Year, 3;
|
||||
}
|
||||
|
||||
/// `setYear` (Annex B.2.5).
|
||||
fn set_year(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let tv = this_time(interp, &this)?;
|
||||
let t = if tv.is_nan() { 0.0 } else { interp.local_time(tv) };
|
||||
let y = interp.to_number(&arg(args, 0))?;
|
||||
if y.is_nan() {
|
||||
return set_this_time(interp, &this, f64::NAN);
|
||||
}
|
||||
let yi = to_integer(y);
|
||||
let yr = if (0.0..=99.0).contains(&yi) { 1900.0 + yi } else { y };
|
||||
let d = make_day(yr, month_from_time(t), date_from_time(t));
|
||||
let u = time_clip(interp.utc(make_date(d, time_within_day(t))));
|
||||
set_this_time(interp, &this, u)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{date_from_time, first_day_of_month, iso_text, make_date, make_day, make_time, month_from_time, parse_iso, week_day, year_from_time};
|
||||
|
||||
#[test]
|
||||
fn time_values_split_into_fields_and_back() {
|
||||
// 2000-02-29 is a leap day; 1969-12-31 is before the epoch.
|
||||
let cases: &[(f64, f64, f64, f64)] = &[(2000.0, 1.0, 29.0, 2.0), (1969.0, 11.0, 31.0, 3.0), (1970.0, 0.0, 1.0, 4.0), (-1.0, 0.0, 1.0, 5.0)];
|
||||
for &(y, m, d, wd) in cases {
|
||||
let t = make_date(make_day(y, m, d), make_time(13.0, 5.0, 7.0, 9.0));
|
||||
assert_eq!((year_from_time(t), month_from_time(t), date_from_time(t), week_day(t)), (y, m, d, wd), "{y}-{m}-{d}");
|
||||
}
|
||||
assert_eq!(first_day_of_month(1970.0, 0), Some(0.0));
|
||||
assert_eq!(make_day(2001.0, -1.0, 1.0), make_day(2000.0, 11.0, 1.0));
|
||||
// The ends of the time value range lie past chrono's years; whole 400-year cycles reach them.
|
||||
assert_eq!(make_day(275_760.0, 8.0, 13.0), 1e8);
|
||||
assert_eq!(make_day(-271_821.0, 3.0, 20.0), -1e8);
|
||||
assert_eq!((year_from_time(8.64e15), month_from_time(8.64e15), date_from_time(8.64e15)), (275_760.0, 8.0, 13.0));
|
||||
assert_eq!((year_from_time(-8.64e15), month_from_time(-8.64e15), date_from_time(-8.64e15)), (-271_821.0, 3.0, 20.0));
|
||||
assert_eq!(make_day(1600.0, 1.0, 29.0) + 1.0, make_day(1600.0, 2.0, 1.0));
|
||||
assert_eq!(make_day(1900.0, 1.0, 29.0), make_day(1900.0, 2.0, 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_iso_format_reads_back_what_to_iso_string_writes() {
|
||||
for t in [0.0, 951_782_400_123.0, -62_198_755_200_000.0, 8.64e15, -8.64e15] {
|
||||
let text = iso_text(t);
|
||||
assert_eq!(parse_iso(text.as_bytes()), Some(t), "{text}");
|
||||
}
|
||||
assert_eq!(parse_iso(b"1970"), Some(0.0));
|
||||
assert_eq!(parse_iso(b"1970-01-01T00:00+01:00"), Some(-3_600_000.0));
|
||||
assert_eq!(parse_iso(b"1970-01-01T24:00:00Z"), Some(86_400_000.0));
|
||||
for bad in ["1970-13-01", "1970-02-30", "1970-01-01T24:00:01", "-000000-01-01", "1970-1-1", "1970-01-01T10", "1970-01-01 10:00"] {
|
||||
assert_eq!(parse_iso(bad.as_bytes()), None, "{bad}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-5
@@ -206,10 +206,12 @@ pub(crate) enum Class {
|
||||
Arguments(Box<ArgMap>),
|
||||
/// A `Boolean` object made by ToObject.
|
||||
Boolean,
|
||||
/// A `Number` object made by ToObject.
|
||||
Number,
|
||||
/// A `Number` object and its number.
|
||||
Number(f64),
|
||||
String(Str),
|
||||
RegExp,
|
||||
RegExp(Rc<crate::regexp_vm::Regex>),
|
||||
/// A `Date` object and its time value.
|
||||
Date(f64),
|
||||
ForIn(Box<ForIn>),
|
||||
}
|
||||
|
||||
@@ -223,9 +225,10 @@ impl Class {
|
||||
Class::Error => "Error",
|
||||
Class::Arguments(_) => "Arguments",
|
||||
Class::Boolean => "Boolean",
|
||||
Class::Number => "Number",
|
||||
Class::Number(_) => "Number",
|
||||
Class::String(_) => "String",
|
||||
Class::RegExp => "RegExp",
|
||||
Class::RegExp(_) => "RegExp",
|
||||
Class::Date(_) => "Date",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -528,6 +531,11 @@ fn trace_object(o: &Object, work: &mut Vec<u32>, strings: &mut StringSizes) -> u
|
||||
}
|
||||
Class::Arguments(map) => work.push(map.env.0),
|
||||
Class::String(s) => strings.add(s),
|
||||
Class::RegExp(re) => {
|
||||
if strings.seen.insert(Rc::as_ptr(re) as *const () as usize) {
|
||||
strings.bytes += re.heap_bytes();
|
||||
}
|
||||
}
|
||||
Class::ForIn(state) => {
|
||||
work.extend(state.object.iter().map(|o| o.0));
|
||||
strings.bytes += state.keys.iter().map(|k| string_bytes(k.len())).sum::<usize>();
|
||||
|
||||
+18
-12
@@ -68,6 +68,7 @@ pub(crate) struct Intrinsics {
|
||||
pub(crate) number_proto: ObjId,
|
||||
pub(crate) boolean_proto: ObjId,
|
||||
pub(crate) regexp_proto: ObjId,
|
||||
pub(crate) date_proto: ObjId,
|
||||
pub(crate) error_protos: [ObjId; 7],
|
||||
pub(crate) eval: ObjId,
|
||||
pub(crate) throw_type_error: ObjId,
|
||||
@@ -84,6 +85,7 @@ impl Intrinsics {
|
||||
number_proto: o,
|
||||
boolean_proto: o,
|
||||
regexp_proto: o,
|
||||
date_proto: o,
|
||||
error_protos: [o; 7],
|
||||
eval: o,
|
||||
throw_type_error: o,
|
||||
@@ -112,6 +114,7 @@ impl Intrinsics {
|
||||
self.number_proto,
|
||||
self.boolean_proto,
|
||||
self.regexp_proto,
|
||||
self.date_proto,
|
||||
self.eval,
|
||||
self.throw_type_error,
|
||||
];
|
||||
@@ -161,6 +164,8 @@ pub(crate) struct Interp {
|
||||
pub(crate) throw_site: Option<ThrowSite>,
|
||||
/// The last exception a run did not catch.
|
||||
pub(crate) last_thrown: Value,
|
||||
/// The clock and time zone `Date` reads.
|
||||
pub(crate) date_host: crate::date_builtins::DateHost,
|
||||
}
|
||||
|
||||
/// The least heap size a collection waits for.
|
||||
@@ -286,6 +291,7 @@ impl Interp {
|
||||
source_units: 0,
|
||||
throw_site: None,
|
||||
last_thrown: Value::Undefined,
|
||||
date_host: crate::date_builtins::DateHost::default(),
|
||||
};
|
||||
crate::builtins::install(&mut interp);
|
||||
interp.gc_threshold = interp.next_threshold();
|
||||
@@ -412,6 +418,13 @@ impl Interp {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Collects garbage now, for a caller whose work ran out of heap room, and sets when the
|
||||
/// next collection runs.
|
||||
pub(crate) fn collect_for_room(&mut self) {
|
||||
self.collect_garbage();
|
||||
self.gc_threshold = self.next_threshold();
|
||||
}
|
||||
|
||||
/// Charges code compiled at run time to the heap, reserving its estimated size and `extra`
|
||||
/// more first. The code is not reachable from the roots yet, so a collection here leaves it
|
||||
/// out of the live total and the charge adds it.
|
||||
@@ -1334,16 +1347,7 @@ impl Interp {
|
||||
|
||||
fn op_regexp(&mut self, i: u32) -> JsResult<()> {
|
||||
let Some((source, flags)) = self.frame()?.code.regexps.get(i as usize).cloned() else { return internal("regular expression out of range") };
|
||||
let proto = self.intrinsics.regexp_proto;
|
||||
let r = self.heap.alloc_object(Object::new(Class::RegExp, Some(proto)));
|
||||
let flag_units = flags.flat();
|
||||
let has = |c: u8| flag_units.contains(&u16::from(c));
|
||||
let (g, i_, m) = (has(b'g'), has(b'i'), has(b'm'));
|
||||
self.define(r, key("source"), Prop::Data { value: Value::String(source), flags: 0 })?;
|
||||
self.define(r, key("global"), Prop::Data { value: Value::Bool(g), flags: 0 })?;
|
||||
self.define(r, key("ignoreCase"), Prop::Data { value: Value::Bool(i_), flags: 0 })?;
|
||||
self.define(r, key("multiline"), Prop::Data { value: Value::Bool(m), flags: 0 })?;
|
||||
self.define(r, key("lastIndex"), Prop::Data { value: Value::Number(0.0), flags: WRITABLE })?;
|
||||
let r = crate::regexp_builtins::new_regexp(self, &source, &flags)?;
|
||||
self.push(Value::Object(r))
|
||||
}
|
||||
|
||||
@@ -1928,7 +1932,9 @@ impl Interp {
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
pub(crate) fn to_primitive(&mut self, v: &Value, hint: Hint) -> JsResult<Value> {
|
||||
let Value::Object(o) = v else { return Ok(v.clone()) };
|
||||
let order = if hint == Hint::String { ["toString", "valueOf"] } else { ["valueOf", "toString"] };
|
||||
// Without a hint, a Date object converts as with the String hint.
|
||||
let date = hint == Hint::Default && matches!(self.heap.object(*o).map(|ob| &ob.class), Some(Class::Date(_)));
|
||||
let order = if hint == Hint::String || date { ["toString", "valueOf"] } else { ["valueOf", "toString"] };
|
||||
for name in order {
|
||||
let f = self.get_from(*o, &key(name), v)?;
|
||||
if self.is_callable(&f) {
|
||||
@@ -2002,7 +2008,7 @@ impl Interp {
|
||||
Value::Object(o) => return Ok(*o),
|
||||
Value::Undefined | Value::Null => return Err(self.type_error("cannot convert undefined or null to an object")),
|
||||
Value::Bool(_) => (Class::Boolean, self.intrinsics.boolean_proto),
|
||||
Value::Number(_) => (Class::Number, self.intrinsics.number_proto),
|
||||
Value::Number(n) => (Class::Number(*n), self.intrinsics.number_proto),
|
||||
Value::String(s) => (Class::String(s.clone()), self.intrinsics.string_proto),
|
||||
};
|
||||
Ok(self.heap.alloc_object(Object::new(class, Some(proto))))
|
||||
|
||||
+14
-3
@@ -23,9 +23,13 @@
|
||||
//! verifier, and runs it on an interpreter that keeps JavaScript calls on its own frame stack, so
|
||||
//! deep recursion in a script does not use the native stack. Objects and environments live in an
|
||||
//! arena with a mark-and-sweep collector; strings built by repeated `+` are flattened and dropped
|
||||
//! with loops. The realm provides the language (sections 10 to 14 of ECMA-262 5.1) and a minimal
|
||||
//! set of built-ins: `Object` and `Object.prototype`'s basic methods, `Function` with
|
||||
//! `Function.prototype.call` and `apply`, `eval`, and the error constructors. Every run is bounded
|
||||
//! with loops. The realm provides the language (sections 10 to 14 of ECMA-262 5.1) and these
|
||||
//! built-ins: `Object` and `Object.prototype`'s basic methods, `Function` with
|
||||
//! `Function.prototype.call` and `apply`, `eval`, the error constructors, and `String`, `Number`,
|
||||
//! `RegExp` and `Date` (sections 15.5, 15.7, 15.9 and 15.10). Regular expressions run on a
|
||||
//! backtracking matcher that keeps its choice points on its own stack and takes fuel for every
|
||||
//! step; `Date` reads the clock and time zone the host sets with [`Realm::set_clock`] and
|
||||
//! [`Realm::set_time_zone`]. Every run is bounded
|
||||
//! by [`Limits`]: instructions run (fuel), heap bytes, frames, nested host calls, and an
|
||||
//! [`Interrupt`] flag another thread can set. A run that reaches a limit ends with
|
||||
//! [`RunErrorKind::Limit`], which a script's `try` cannot catch.
|
||||
@@ -36,14 +40,19 @@ mod builtins;
|
||||
mod bytecode;
|
||||
mod chars;
|
||||
mod compiler;
|
||||
mod date_builtins;
|
||||
mod error;
|
||||
mod heap;
|
||||
mod interp;
|
||||
mod lexer;
|
||||
mod number;
|
||||
mod number_builtins;
|
||||
mod parser;
|
||||
mod realm;
|
||||
mod regexp;
|
||||
mod regexp_builtins;
|
||||
mod regexp_vm;
|
||||
mod string_builtins;
|
||||
mod token;
|
||||
mod value;
|
||||
|
||||
@@ -173,3 +182,5 @@ impl LineTable {
|
||||
mod interp_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod text_tests;
|
||||
|
||||
+306
-2
@@ -1,4 +1,7 @@
|
||||
//! Number to string conversion (ECMA-262 5.1 section 9.8.1), for numeric property names.
|
||||
//! Number to string conversion (ECMA-262 5.1 section 9.8.1), for numeric property names, and the
|
||||
//! formats of `Number.prototype`'s `toString(radix)`, `toFixed`, `toExponential` and
|
||||
//! `toPrecision` (section 15.7.4). The fixed, exponential and precision formats round the
|
||||
//! number's exact decimal value, ties away from zero, as the specification's "larger n" says.
|
||||
|
||||
/// The string ToString gives for the number `x`: the shortest decimal digits that read back as
|
||||
/// `x`, laid out as section 9.8.1 says.
|
||||
@@ -38,9 +41,310 @@ pub fn to_string(x: f64) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// The exact decimal digits of a positive finite `x`, without trailing zeros, and the position of
|
||||
/// the decimal point: `x = 0.d1d2… × 10^point`.
|
||||
fn exact_digits(x: f64) -> (Vec<u8>, i64) {
|
||||
// A double's exact decimal expansion has at most 767 significant digits.
|
||||
let sci = format!("{x:.800e}");
|
||||
let (mantissa, exp) = sci.split_once('e').unwrap_or((&sci, "0"));
|
||||
let mut digits: Vec<u8> = mantissa.bytes().filter(u8::is_ascii_digit).map(|b| b - b'0').collect();
|
||||
while digits.len() > 1 && digits.last() == Some(&0) {
|
||||
digits.pop();
|
||||
}
|
||||
(digits, exp.parse::<i64>().unwrap_or(0) + 1)
|
||||
}
|
||||
|
||||
/// The shortest digits that read back as a positive finite `x`, and the decimal point's position.
|
||||
fn shortest_digits(x: f64) -> (Vec<u8>, i64) {
|
||||
let sci = format!("{x:e}");
|
||||
let (mantissa, exp) = sci.split_once('e').unwrap_or((&sci, "0"));
|
||||
(mantissa.bytes().filter(u8::is_ascii_digit).map(|b| b - b'0').collect(), exp.parse::<i64>().unwrap_or(0) + 1)
|
||||
}
|
||||
|
||||
/// Rounds `digits` (a number `0.d1d2… × 10^point`) to its first `keep` digits, a half rounding
|
||||
/// up. Returns exactly `keep` digits and the new point; `keep` may be 0 or negative, which leaves
|
||||
/// either nothing or a single `1` one place up.
|
||||
fn round_digits(digits: &[u8], point: i64, keep: i64) -> (Vec<u8>, i64) {
|
||||
if keep < 0 {
|
||||
return (Vec::new(), point);
|
||||
}
|
||||
let k = usize::try_from(keep).unwrap_or(usize::MAX);
|
||||
if digits.len() <= k {
|
||||
let mut out = digits.to_vec();
|
||||
out.resize(k, 0);
|
||||
return (out, point);
|
||||
}
|
||||
let mut out: Vec<u8> = digits.get(..k).unwrap_or_default().to_vec();
|
||||
if digits.get(k).is_some_and(|&d| d >= 5) {
|
||||
let mut i = out.len();
|
||||
loop {
|
||||
if i == 0 {
|
||||
out.insert(0, 1);
|
||||
out.truncate(k.max(1));
|
||||
return (out, point + 1);
|
||||
}
|
||||
i -= 1;
|
||||
match out.get_mut(i) {
|
||||
Some(d) if *d == 9 => *d = 0,
|
||||
Some(d) => {
|
||||
*d += 1;
|
||||
break;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
(out, point)
|
||||
}
|
||||
|
||||
fn digits_text(digits: &[u8]) -> String {
|
||||
digits.iter().map(|d| char::from(b'0' + d)).collect()
|
||||
}
|
||||
|
||||
/// `Number.prototype.toFixed` (section 15.7.4.5) for `f` digits after the point, 0 to 20.
|
||||
pub(crate) fn to_fixed(x: f64, f: usize) -> String {
|
||||
if x.is_nan() {
|
||||
return "NaN".into();
|
||||
}
|
||||
if x.abs() >= 1e21 {
|
||||
return to_string(x);
|
||||
}
|
||||
let sign = if x < 0.0 { "-" } else { "" };
|
||||
let x = x.abs();
|
||||
let fi = i64::try_from(f).unwrap_or(0);
|
||||
let n = if x == 0.0 {
|
||||
vec![0]
|
||||
} else {
|
||||
let (digits, point) = exact_digits(x);
|
||||
let (mut n, new_point) = round_digits(&digits, point, point + fi);
|
||||
if n.is_empty() {
|
||||
n.push(0);
|
||||
} else if new_point > point {
|
||||
// A carry past the first digit leaves one digit more than kept.
|
||||
n.push(0);
|
||||
n.truncate(usize::try_from(new_point + fi).unwrap_or(n.len()));
|
||||
}
|
||||
n
|
||||
};
|
||||
let mut m = digits_text(&n);
|
||||
let m_trim = m.trim_start_matches('0');
|
||||
m = if m_trim.is_empty() { "0".into() } else { m_trim.into() };
|
||||
if f == 0 {
|
||||
return format!("{sign}{m}");
|
||||
}
|
||||
if m.len() <= f {
|
||||
m = format!("{}{m}", "0".repeat(f + 1 - m.len()));
|
||||
}
|
||||
let (int, frac) = m.split_at(m.len() - f);
|
||||
format!("{sign}{int}.{frac}")
|
||||
}
|
||||
|
||||
/// `Number.prototype.toExponential` (section 15.7.4.6) for `f` digits after the point (0 to 20),
|
||||
/// or as many as the number needs when `f` is `None`.
|
||||
pub(crate) fn to_exponential(x: f64, f: Option<usize>) -> String {
|
||||
if !x.is_finite() {
|
||||
return to_string(x);
|
||||
}
|
||||
let sign = if x < 0.0 { "-" } else { "" };
|
||||
let x = x.abs();
|
||||
let (digits, e) = if x == 0.0 {
|
||||
(vec![0; f.unwrap_or(0) + 1], 0)
|
||||
} else {
|
||||
match f {
|
||||
None => {
|
||||
let (d, point) = shortest_digits(x);
|
||||
(d, point - 1)
|
||||
}
|
||||
Some(f) => {
|
||||
let (d, point) = exact_digits(x);
|
||||
let (d, point) = round_digits(&d, point, i64::try_from(f).unwrap_or(0) + 1);
|
||||
(d, point - 1)
|
||||
}
|
||||
}
|
||||
};
|
||||
let text = digits_text(&digits);
|
||||
let (first, rest) = text.split_at(1.min(text.len()));
|
||||
let mantissa = if rest.is_empty() { first.to_string() } else { format!("{first}.{rest}") };
|
||||
let esign = if e < 0 { '-' } else { '+' };
|
||||
format!("{sign}{mantissa}e{esign}{}", e.abs())
|
||||
}
|
||||
|
||||
/// `Number.prototype.toPrecision` (section 15.7.4.7) for `p` significant digits, 1 to 21.
|
||||
pub(crate) fn to_precision(x: f64, p: usize) -> String {
|
||||
if !x.is_finite() {
|
||||
return to_string(x);
|
||||
}
|
||||
let sign = if x < 0.0 { "-" } else { "" };
|
||||
let x = x.abs();
|
||||
let pi = i64::try_from(p).unwrap_or(1);
|
||||
let (digits, e) = if x == 0.0 {
|
||||
(vec![0; p], 0)
|
||||
} else {
|
||||
let (d, point) = exact_digits(x);
|
||||
let (d, point) = round_digits(&d, point, pi);
|
||||
(d, point - 1)
|
||||
};
|
||||
let m = digits_text(&digits);
|
||||
if e < -6 || e >= pi {
|
||||
let (first, rest) = m.split_at(1.min(m.len()));
|
||||
let mantissa = if rest.is_empty() { first.to_string() } else { format!("{first}.{rest}") };
|
||||
let esign = if e < 0 { '-' } else { '+' };
|
||||
return format!("{sign}{mantissa}e{esign}{}", e.abs());
|
||||
}
|
||||
if e == pi - 1 {
|
||||
return format!("{sign}{m}");
|
||||
}
|
||||
if e >= 0 {
|
||||
let (int, frac) = m.split_at(usize::try_from(e + 1).unwrap_or(0).min(m.len()));
|
||||
return format!("{sign}{int}.{frac}");
|
||||
}
|
||||
format!("{sign}0.{}{m}", "0".repeat(usize::try_from(-(e + 1)).unwrap_or(0)))
|
||||
}
|
||||
|
||||
/// `Number.prototype.toString(radix)` (section 15.7.4.2) for a radix of 2 to 36 other than 10:
|
||||
/// the integer part exactly, and fraction digits until the number would read back unchanged.
|
||||
pub(crate) fn to_radix_string(x: f64, radix: u32) -> String {
|
||||
if !x.is_finite() || x == 0.0 {
|
||||
return to_string(x);
|
||||
}
|
||||
const CHARS: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
|
||||
let r = f64::from(radix);
|
||||
let sign = if x < 0.0 { "-" } else { "" };
|
||||
let x = x.abs();
|
||||
let mut integer = x.floor();
|
||||
let mut fraction = x - integer;
|
||||
// Half the distance to the next double: digits past this resolution are not needed.
|
||||
let next = f64::from_bits(x.to_bits() + 1);
|
||||
let mut delta = (0.5 * (next - x)).max(f64::from_bits(1));
|
||||
let mut frac_digits: Vec<u8> = Vec::new();
|
||||
if fraction >= delta {
|
||||
loop {
|
||||
fraction *= r;
|
||||
delta *= r;
|
||||
let digit = fraction.floor();
|
||||
frac_digits.push(digit as u8);
|
||||
fraction -= digit;
|
||||
if (fraction > 0.5 || (fraction == 0.5 && (digit as u8) & 1 == 1)) && fraction + delta > 1.0 {
|
||||
// Round up, carrying into the integer part when every digit is the largest.
|
||||
loop {
|
||||
match frac_digits.last_mut() {
|
||||
None => {
|
||||
integer += 1.0;
|
||||
break;
|
||||
}
|
||||
Some(d) if u32::from(*d) + 1 == radix => {
|
||||
frac_digits.pop();
|
||||
}
|
||||
Some(d) => {
|
||||
*d += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if fraction < delta {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut int_digits: Vec<u8> = Vec::new();
|
||||
// Digits below the double's precision are zeros.
|
||||
while (integer / r) >= 9_007_199_254_740_992.0 {
|
||||
integer /= r;
|
||||
int_digits.push(0);
|
||||
}
|
||||
loop {
|
||||
let rem = integer % r;
|
||||
int_digits.push(rem as u8);
|
||||
integer = (integer - rem) / r;
|
||||
if integer <= 0.0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut out = String::from(sign);
|
||||
out.extend(int_digits.iter().rev().map(|&d| char::from(CHARS.get(usize::from(d)).copied().unwrap_or(b'0'))));
|
||||
if !frac_digits.is_empty() {
|
||||
out.push('.');
|
||||
out.extend(frac_digits.iter().map(|&d| char::from(CHARS.get(usize::from(d)).copied().unwrap_or(b'0'))));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::to_string;
|
||||
use super::{to_exponential, to_fixed, to_precision, to_radix_string, to_string};
|
||||
|
||||
#[test]
|
||||
fn fixed_exponential_and_precision_round_the_exact_value_with_ties_away_from_zero() {
|
||||
let fixed: &[(f64, usize, &str)] = &[
|
||||
(0.5, 0, "1"),
|
||||
(2.5, 0, "3"),
|
||||
(1.45, 1, "1.4"),
|
||||
(1.255, 2, "1.25"),
|
||||
(1.005, 2, "1.00"),
|
||||
(0.1, 20, "0.10000000000000000555"),
|
||||
(123.456, 2, "123.46"),
|
||||
(-1.5, 0, "-2"),
|
||||
(-0.0000001, 2, "-0.00"),
|
||||
(-0.0, 2, "0.00"),
|
||||
(0.0, 0, "0"),
|
||||
(99.995, 2, "100.00"),
|
||||
(9.5, 0, "10"),
|
||||
(1e21, 2, "1e+21"),
|
||||
(1000000000000000128.0, 0, "1000000000000000128"),
|
||||
(f64::NAN, 2, "NaN"),
|
||||
];
|
||||
for (x, f, want) in fixed {
|
||||
assert_eq!(to_fixed(*x, *f), *want, "({x}).toFixed({f})");
|
||||
}
|
||||
let exp: &[(f64, Option<usize>, &str)] = &[
|
||||
(123456.0, Some(2), "1.23e+5"),
|
||||
(123456.0, None, "1.23456e+5"),
|
||||
(0.00015, Some(1), "1.5e-4"),
|
||||
(1.25, Some(1), "1.3e+0"),
|
||||
(0.0, Some(2), "0.00e+0"),
|
||||
(0.0, None, "0e+0"),
|
||||
(-6.9e-11, Some(1), "-6.9e-11"),
|
||||
(9.99, Some(1), "1.0e+1"),
|
||||
(f64::INFINITY, Some(2), "Infinity"),
|
||||
];
|
||||
for (x, f, want) in exp {
|
||||
assert_eq!(to_exponential(*x, *f), *want, "({x}).toExponential({f:?})");
|
||||
}
|
||||
let prec: &[(f64, usize, &str)] = &[
|
||||
(123.456, 4, "123.5"),
|
||||
(0.000123, 2, "0.00012"),
|
||||
(0.0000001234, 2, "1.2e-7"),
|
||||
(123456.0, 2, "1.2e+5"),
|
||||
(123456.0, 6, "123456"),
|
||||
(0.0, 3, "0.00"),
|
||||
(1.0, 1, "1"),
|
||||
(99.5, 2, "1.0e+2"),
|
||||
(1e21, 21, "1.00000000000000000000e+21"),
|
||||
];
|
||||
for (x, p, want) in prec {
|
||||
assert_eq!(to_precision(*x, *p), *want, "({x}).toPrecision({p})");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radix_strings_give_exact_integers_and_enough_fraction_digits() {
|
||||
let cases: &[(f64, u32, &str)] = &[
|
||||
(255.0, 16, "ff"),
|
||||
(255.0, 2, "11111111"),
|
||||
(-255.0, 36, "-73"),
|
||||
(0.5, 2, "0.1"),
|
||||
(0.5, 16, "0.8"),
|
||||
(3.75, 2, "11.11"),
|
||||
(2f64.powi(60), 16, "1000000000000000"),
|
||||
(1e21, 36, "5v1j4f4ds7c000"),
|
||||
(0.1, 3, "0.0022002200220022002200220022002201"),
|
||||
];
|
||||
for (x, r, want) in cases {
|
||||
assert_eq!(to_radix_string(*x, *r), *want, "({x}).toString({r})");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbers_print_as_section_9_8_1_lays_them_out() {
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
//! `Number` (ECMA-262 5.1 section 15.7): the constructor, its constants and the methods of
|
||||
//! `Number.prototype`. The formats are in [`crate::number`].
|
||||
|
||||
use crate::builtins::{arg, constructor, data, method, native};
|
||||
use crate::heap::{Class, NativeKind, Object};
|
||||
use crate::interp::{ErrorType, Interp, JsResult};
|
||||
use crate::number;
|
||||
use crate::string_builtins::{reserve_units, to_integer};
|
||||
use crate::value::{Str, Value};
|
||||
|
||||
pub(crate) fn install(interp: &mut Interp) -> JsResult<()> {
|
||||
let proto = interp.intrinsics.number_proto;
|
||||
let ctor = native(interp, "Number", number_ctor, 1, NativeKind::Plain, true)?;
|
||||
constructor(interp, "Number", ctor, proto)?;
|
||||
let constants = [
|
||||
("MAX_VALUE", f64::MAX),
|
||||
("MIN_VALUE", f64::from_bits(1)),
|
||||
("NaN", f64::NAN),
|
||||
("NEGATIVE_INFINITY", f64::NEG_INFINITY),
|
||||
("POSITIVE_INFINITY", f64::INFINITY),
|
||||
];
|
||||
for (name, v) in constants {
|
||||
data(interp, ctor, name, Value::Number(v), 0)?;
|
||||
}
|
||||
method(interp, proto, "toString", to_string, 1)?;
|
||||
method(interp, proto, "toLocaleString", to_locale_string, 0)?;
|
||||
method(interp, proto, "valueOf", value_of, 0)?;
|
||||
method(interp, proto, "toFixed", to_fixed, 1)?;
|
||||
method(interp, proto, "toExponential", to_exponential, 1)?;
|
||||
method(interp, proto, "toPrecision", to_precision, 1)
|
||||
}
|
||||
|
||||
/// `Number(value)` and `new Number(value)` (sections 15.7.1 and 15.7.2).
|
||||
fn number_ctor(interp: &mut Interp, _: Value, args: &[Value], construct: bool) -> JsResult<Value> {
|
||||
let n = match args.first() {
|
||||
Some(v) => interp.to_number(v)?,
|
||||
None => 0.0,
|
||||
};
|
||||
if !construct {
|
||||
return Ok(Value::Number(n));
|
||||
}
|
||||
let proto = interp.intrinsics.number_proto;
|
||||
Ok(Value::Object(interp.heap.alloc_object(Object::new(Class::Number(n), Some(proto)))))
|
||||
}
|
||||
|
||||
/// The number a `Number.prototype` method works on: a number, or a Number object's number.
|
||||
fn this_number(interp: &mut Interp, this: &Value, name: &str) -> JsResult<f64> {
|
||||
match this {
|
||||
Value::Number(n) => Ok(*n),
|
||||
Value::Object(o) => match interp.heap.object(*o).map(|o| &o.class) {
|
||||
Some(Class::Number(n)) => Ok(*n),
|
||||
_ => Err(interp.type_error(&format!("Number.prototype.{name} was called on a value that is not a number"))),
|
||||
},
|
||||
_ => Err(interp.type_error(&format!("Number.prototype.{name} was called on a value that is not a number"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn text(interp: &mut Interp, s: &str) -> JsResult<Value> {
|
||||
reserve_units(interp, s.len())?;
|
||||
Ok(Value::String(Str::from(s)))
|
||||
}
|
||||
|
||||
/// `toString(radix)` (section 15.7.4.2).
|
||||
fn to_string(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let x = this_number(interp, &this, "toString")?;
|
||||
let radix = match arg(args, 0) {
|
||||
Value::Undefined => 10.0,
|
||||
r => to_integer(interp.to_number(&r)?),
|
||||
};
|
||||
if !(2.0..=36.0).contains(&radix) {
|
||||
return Err(interp.throw(ErrorType::Range, "the radix must be between 2 and 36"));
|
||||
}
|
||||
let s = if radix == 10.0 { number::to_string(x) } else { number::to_radix_string(x, radix as u32) };
|
||||
text(interp, &s)
|
||||
}
|
||||
|
||||
fn to_locale_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let x = this_number(interp, &this, "toLocaleString")?;
|
||||
text(interp, &number::to_string(x))
|
||||
}
|
||||
|
||||
fn value_of(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
this_number(interp, &this, "valueOf").map(Value::Number)
|
||||
}
|
||||
|
||||
/// `toFixed(fractionDigits)` (section 15.7.4.5).
|
||||
fn to_fixed(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let x = this_number(interp, &this, "toFixed")?;
|
||||
let f = to_integer(interp.to_number(&arg(args, 0))?);
|
||||
if !(0.0..=20.0).contains(&f) {
|
||||
return Err(interp.throw(ErrorType::Range, "toFixed() digits must be between 0 and 20"));
|
||||
}
|
||||
text(interp, &number::to_fixed(x, f as usize))
|
||||
}
|
||||
|
||||
/// `toExponential(fractionDigits)` (section 15.7.4.6).
|
||||
fn to_exponential(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let x = this_number(interp, &this, "toExponential")?;
|
||||
let digits = arg(args, 0);
|
||||
let f = to_integer(interp.to_number(&digits)?);
|
||||
if !x.is_finite() {
|
||||
return text(interp, &number::to_string(x));
|
||||
}
|
||||
let f = match digits {
|
||||
Value::Undefined => None,
|
||||
_ if !(0.0..=20.0).contains(&f) => return Err(interp.throw(ErrorType::Range, "toExponential() digits must be between 0 and 20")),
|
||||
_ => Some(f as usize),
|
||||
};
|
||||
text(interp, &number::to_exponential(x, f))
|
||||
}
|
||||
|
||||
/// `toPrecision(precision)` (section 15.7.4.7).
|
||||
fn to_precision(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let x = this_number(interp, &this, "toPrecision")?;
|
||||
let precision = arg(args, 0);
|
||||
if matches!(precision, Value::Undefined) {
|
||||
return text(interp, &number::to_string(x));
|
||||
}
|
||||
let p = to_integer(interp.to_number(&precision)?);
|
||||
if !x.is_finite() {
|
||||
return text(interp, &number::to_string(x));
|
||||
}
|
||||
if !(1.0..=21.0).contains(&p) {
|
||||
return Err(interp.throw(ErrorType::Range, "toPrecision() argument must be between 1 and 21"));
|
||||
}
|
||||
text(interp, &number::to_precision(x, p as usize))
|
||||
}
|
||||
@@ -209,6 +209,21 @@ impl Realm {
|
||||
let _ = interp.define(global, key(name), Prop::Data { value: v, flags: HIDDEN });
|
||||
}
|
||||
|
||||
/// Sets the clock `Date.now()` and `new Date()` read: the current time in milliseconds since
|
||||
/// 1970-01-01 UTC. By default it is the system clock; a wasm build has none, and its default
|
||||
/// gives NaN.
|
||||
pub fn set_clock(&mut self, clock: impl Fn() -> f64 + 'static) {
|
||||
self.interp.date_host.clock = std::rc::Rc::new(clock);
|
||||
}
|
||||
|
||||
/// Sets the time zone `Date` uses for local time: `offset` gives local time's offset from UTC
|
||||
/// in milliseconds at a time value (milliseconds since 1970-01-01 UTC), so it can follow
|
||||
/// daylight saving time. An offset that is not finite or is a day or more counts as 0. By
|
||||
/// default local time is UTC.
|
||||
pub fn set_time_zone(&mut self, offset: impl Fn(f64) -> f64 + 'static) {
|
||||
self.interp.date_host.offset = std::rc::Rc::new(offset);
|
||||
}
|
||||
|
||||
/// Collects garbage now and returns the heap's estimated size in bytes afterwards.
|
||||
pub fn collect_garbage(&mut self) -> usize {
|
||||
self.interp.collect_garbage();
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
//! `RegExp` (ECMA-262 5.1 section 15.10.3 to 15.10.7): the constructor, regular expression
|
||||
//! literals' objects, and `exec`, `test` and `toString`. Matching is in [`crate::regexp_vm`].
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::LimitKind;
|
||||
use crate::builtins::{arg, constructor, method, native};
|
||||
use crate::heap::{Class, NativeKind, OBJECT_BYTES, OPEN, ObjId, Object, PROP_BYTES, Prop, WRITABLE, key, string_bytes};
|
||||
use crate::interp::{Abrupt, ErrorType, Interp, JsResult};
|
||||
use crate::regexp_vm::{CompileFail, Flags, Matcher, Regex, compile, parse_flags, use_fuel_n};
|
||||
use crate::string_builtins::{new_array, reserve_units, rooted, set_element, substring_value, to_integer};
|
||||
use crate::value::{Str, Value};
|
||||
|
||||
pub(crate) fn install(interp: &mut Interp) -> JsResult<()> {
|
||||
let proto = interp.intrinsics.regexp_proto;
|
||||
// The prototype is itself a regular expression object, matching the empty string.
|
||||
let empty = compile_regex(interp, &[], &[])?;
|
||||
if let Some(p) = interp.heap.object_mut(proto) {
|
||||
p.class = Class::RegExp(empty);
|
||||
}
|
||||
define_flags(interp, proto, Str::from("(?:)"), false, false, false)?;
|
||||
let ctor = native(interp, "RegExp", regexp_ctor, 2, NativeKind::Plain, true)?;
|
||||
constructor(interp, "RegExp", ctor, proto)?;
|
||||
method(interp, proto, "exec", exec, 1)?;
|
||||
method(interp, proto, "test", test, 1)?;
|
||||
method(interp, proto, "toString", to_string, 0)
|
||||
}
|
||||
|
||||
/// The object and the compiled form of a RegExp object.
|
||||
pub(crate) fn regexp_of(interp: &Interp, v: &Value) -> Option<(ObjId, Rc<Regex>)> {
|
||||
let o = v.as_object()?;
|
||||
match &interp.heap.object(o)?.class {
|
||||
Class::RegExp(re) => Some((o, re.clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks and compiles a pattern, throwing a `SyntaxError` for a pattern or flags that are not
|
||||
/// valid. The compiled form's bytes are reserved on the heap.
|
||||
fn compile_regex(interp: &mut Interp, pattern: &[u16], flags: &[u16]) -> JsResult<Rc<Regex>> {
|
||||
let flags: Flags = match parse_flags(flags) {
|
||||
Ok(f) => f,
|
||||
Err(m) => return Err(interp.throw(ErrorType::Syntax, &m)),
|
||||
};
|
||||
let depth = interp.options.max_depth;
|
||||
if let Err(m) = crate::regexp::check(pattern, depth) {
|
||||
return Err(interp.throw(ErrorType::Syntax, &format!("invalid regular expression: {m}")));
|
||||
}
|
||||
// A compile that runs out of room collects garbage and tries once more with the room it frees.
|
||||
let mut collected = false;
|
||||
let re = loop {
|
||||
let room = interp.limits.heap_bytes.saturating_sub(interp.heap.bytes);
|
||||
match compile(pattern, flags, depth, room, &mut |n| use_fuel_n(interp, n)) {
|
||||
Ok(re) => break re,
|
||||
Err(CompileFail::Abrupt(Abrupt::Limit(LimitKind::Heap))) if !collected => {
|
||||
interp.collect_for_room();
|
||||
collected = true;
|
||||
}
|
||||
Err(CompileFail::Syntax(m)) => return Err(interp.throw(ErrorType::Syntax, &format!("invalid regular expression: {m}"))),
|
||||
Err(CompileFail::Abrupt(a)) => return Err(a),
|
||||
}
|
||||
};
|
||||
let bytes = re.heap_bytes();
|
||||
interp.reserve_heap(bytes)?;
|
||||
interp.heap.bytes = interp.heap.bytes.saturating_add(bytes);
|
||||
Ok(Rc::new(re))
|
||||
}
|
||||
|
||||
/// A new RegExp object whose `source` property is `source` (section 15.10.4.1, steps 7 to 12).
|
||||
fn make(interp: &mut Interp, source: Str, pattern: &[u16], flags: &[u16]) -> JsResult<ObjId> {
|
||||
let re = compile_regex(interp, pattern, flags)?;
|
||||
interp.reserve_heap(OBJECT_BYTES + 5 * (PROP_BYTES + string_bytes(10)))?;
|
||||
let (g, i, m) = (parse_flags(flags).unwrap_or_default().global, re.ignore_case, re.multiline);
|
||||
let proto = interp.intrinsics.regexp_proto;
|
||||
let r = interp.heap.alloc_object(Object::new(Class::RegExp(re), Some(proto)));
|
||||
define_flags(interp, r, source, g, i, m)?;
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Defines a RegExp object's `source`, `global`, `ignoreCase`, `multiline` and `lastIndex`
|
||||
/// (section 15.10.7).
|
||||
fn define_flags(interp: &mut Interp, r: ObjId, source: Str, g: bool, i: bool, m: bool) -> JsResult<()> {
|
||||
interp.define(r, key("source"), Prop::Data { value: Value::String(source), flags: 0 })?;
|
||||
interp.define(r, key("global"), Prop::Data { value: Value::Bool(g), flags: 0 })?;
|
||||
interp.define(r, key("ignoreCase"), Prop::Data { value: Value::Bool(i), flags: 0 })?;
|
||||
interp.define(r, key("multiline"), Prop::Data { value: Value::Bool(m), flags: 0 })?;
|
||||
interp.define(r, key("lastIndex"), Prop::Data { value: Value::Number(0.0), flags: WRITABLE })?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The object a regular expression literal evaluates to (section 7.8.5): a new RegExp object
|
||||
/// each time.
|
||||
pub(crate) fn new_regexp(interp: &mut Interp, source: &Str, flags: &Str) -> JsResult<ObjId> {
|
||||
let pattern = source.flat();
|
||||
make(interp, source.clone(), &pattern, &flags.flat())
|
||||
}
|
||||
|
||||
/// The `source` text for a pattern given to the constructor: `(?:)` for the empty pattern, and
|
||||
/// `/` and line terminators escaped, so that `/source/` reads back as the same pattern. Its
|
||||
/// length is reserved on the heap before it is built.
|
||||
pub(crate) fn source_text(interp: &mut Interp, pattern: &[u16]) -> JsResult<Str> {
|
||||
if pattern.is_empty() {
|
||||
return Ok(Str::from("(?:)"));
|
||||
}
|
||||
let mut len = 0usize;
|
||||
source_units(pattern, |u| len = len.saturating_add(u.len()));
|
||||
reserve_units(interp, len)?;
|
||||
let mut out = Vec::with_capacity(len);
|
||||
source_units(pattern, |u| out.extend_from_slice(u));
|
||||
Ok(Str::from_units(out))
|
||||
}
|
||||
|
||||
/// Passes the units of [`source_text`] for a non-empty pattern to `emit`, a piece at a time.
|
||||
fn source_units(pattern: &[u16], mut emit: impl FnMut(&[u16])) {
|
||||
const BACKSLASH: u16 = 0x5C;
|
||||
let mut in_class = false;
|
||||
let mut i = 0;
|
||||
while let Some(&u) = pattern.get(i) {
|
||||
match u {
|
||||
0x5C => {
|
||||
emit(pattern.get(i..i + 2).unwrap_or(&[BACKSLASH]));
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
0x5B => in_class = true,
|
||||
0x5D => in_class = false,
|
||||
0x2F if !in_class => emit(&[BACKSLASH]),
|
||||
_ => {}
|
||||
}
|
||||
match u {
|
||||
0x0A => emit(&[BACKSLASH, 0x6E]),
|
||||
0x0D => emit(&[BACKSLASH, 0x72]),
|
||||
0x2028 => emit(&[BACKSLASH, 0x75, 0x32, 0x30, 0x32, 0x38]),
|
||||
0x2029 => emit(&[BACKSLASH, 0x75, 0x32, 0x30, 0x32, 0x39]),
|
||||
_ => emit(&[u]),
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// `new RegExp(pattern, flags)` (section 15.10.4.1).
|
||||
pub(crate) fn new_regexp_from(interp: &mut Interp, pattern: &Value, flags: &Value) -> JsResult<ObjId> {
|
||||
if let Some((o, _)) = regexp_of(interp, pattern) {
|
||||
if !matches!(flags, Value::Undefined) {
|
||||
return Err(interp.type_error("flags cannot be given with a RegExp object"));
|
||||
}
|
||||
let pv = Value::Object(o);
|
||||
let source = interp.get_value(&pv, &key("source"))?;
|
||||
let source = interp.to_str(&source)?;
|
||||
let mut f = String::new();
|
||||
for (name, c) in [("global", 'g'), ("ignoreCase", 'i'), ("multiline", 'm')] {
|
||||
if interp.get_value(&pv, &key(name))?.truthy() {
|
||||
f.push(c);
|
||||
}
|
||||
}
|
||||
let units = source.flat();
|
||||
let pattern: &[u16] = if is_empty_group(&units) { &[] } else { &units };
|
||||
return make(interp, source.clone(), pattern, &f.encode_utf16().collect::<Vec<u16>>());
|
||||
}
|
||||
let p = if matches!(pattern, Value::Undefined) { Str::from("") } else { interp.to_str(pattern)? };
|
||||
let f = if matches!(flags, Value::Undefined) { Str::from("") } else { interp.to_str(flags)? };
|
||||
let units = p.flat();
|
||||
let source = source_text(interp, &units)?;
|
||||
make(interp, source, &units, &f.flat())
|
||||
}
|
||||
|
||||
fn is_empty_group(units: &[u16]) -> bool {
|
||||
units.iter().copied().eq("(?:)".encode_utf16())
|
||||
}
|
||||
|
||||
/// `RegExp(pattern, flags)` and `new RegExp(pattern, flags)` (sections 15.10.3 and 15.10.4).
|
||||
fn regexp_ctor(interp: &mut Interp, _: Value, args: &[Value], construct: bool) -> JsResult<Value> {
|
||||
let (pattern, flags) = (arg(args, 0), arg(args, 1));
|
||||
if !construct && regexp_of(interp, &pattern).is_some() && matches!(flags, Value::Undefined) {
|
||||
return Ok(pattern);
|
||||
}
|
||||
new_regexp_from(interp, &pattern, &flags).map(Value::Object)
|
||||
}
|
||||
|
||||
fn this_regexp(interp: &mut Interp, this: &Value, name: &str) -> JsResult<ObjId> {
|
||||
match regexp_of(interp, this) {
|
||||
Some((o, _)) => Ok(o),
|
||||
None => Err(interp.type_error(&format!("RegExp.prototype.{name} was called on a value that is not a RegExp object"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// The matching steps of `exec` (section 15.10.6.2, steps 3 to 11): the positions of the match
|
||||
/// and of its groups ([`crate::regexp_vm::NONE`] for a group that did not take part), or `None`.
|
||||
/// Reads `lastIndex` and `global`, and sets `lastIndex` as `exec` does.
|
||||
pub(crate) fn exec_raw(interp: &mut Interp, rx: ObjId, s: &Str) -> JsResult<Option<Vec<usize>>> {
|
||||
let Some((_, re)) = regexp_of(interp, &Value::Object(rx)) else { return Err(interp.type_error("not a RegExp object")) };
|
||||
let rv = Value::Object(rx);
|
||||
let last_index = key("lastIndex");
|
||||
let li = interp.get_value(&rv, &last_index)?;
|
||||
let li = to_integer(interp.to_number(&li)?);
|
||||
let global = interp.get_value(&rv, &key("global"))?.truthy();
|
||||
let units = s.flat();
|
||||
let i = if global { li } else { 0.0 };
|
||||
let found = if i < 0.0 || i > units.len() as f64 {
|
||||
None
|
||||
} else {
|
||||
let mut m = Matcher::new(&re, interp)?;
|
||||
if m.search(interp, &units, i as usize)? { Some(m.caps) } else { None }
|
||||
};
|
||||
match found {
|
||||
None => {
|
||||
interp.put(rx, last_index, Value::Number(0.0), true)?;
|
||||
Ok(None)
|
||||
}
|
||||
Some(caps) => {
|
||||
if global {
|
||||
let end = caps.get(1).copied().unwrap_or(0);
|
||||
interp.put(rx, last_index, Value::Number(end as f64), true)?;
|
||||
}
|
||||
Ok(Some(caps))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `exec`'s result for `rx` on `s`: the match array, or `null`.
|
||||
pub(crate) fn exec_value(interp: &mut Interp, rx: ObjId, s: &Str) -> JsResult<Value> {
|
||||
rooted(interp, |interp| {
|
||||
let Some(caps) = exec_raw(interp, rx, s)? else { return Ok(Value::Null) };
|
||||
let units = s.flat();
|
||||
let a = new_array(interp);
|
||||
interp.reserve_heap(2 * (PROP_BYTES + string_bytes(5)))?;
|
||||
let index = caps.first().copied().unwrap_or(0);
|
||||
interp.define(a, key("index"), Prop::Data { value: Value::Number(index as f64), flags: OPEN })?;
|
||||
interp.define(a, key("input"), Prop::Data { value: Value::String(s.clone()), flags: OPEN })?;
|
||||
for (n, pair) in caps.chunks(2).enumerate() {
|
||||
let v = match pair {
|
||||
[x, y] if *x != crate::regexp_vm::NONE && *y != crate::regexp_vm::NONE => substring_value(interp, &units, *x, *y)?,
|
||||
_ => Value::Undefined,
|
||||
};
|
||||
set_element(interp, a, n, v)?;
|
||||
}
|
||||
Ok(Value::Object(a))
|
||||
})
|
||||
}
|
||||
|
||||
/// `RegExp.prototype.exec` (section 15.10.6.2).
|
||||
fn exec(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let rx = this_regexp(interp, &this, "exec")?;
|
||||
let s = interp.to_str(&arg(args, 0))?;
|
||||
exec_value(interp, rx, &s)
|
||||
}
|
||||
|
||||
/// `RegExp.prototype.test` (section 15.10.6.3).
|
||||
fn test(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let rx = this_regexp(interp, &this, "test")?;
|
||||
let s = interp.to_str(&arg(args, 0))?;
|
||||
Ok(Value::Bool(exec_raw(interp, rx, &s)?.is_some()))
|
||||
}
|
||||
|
||||
/// `RegExp.prototype.toString` (section 15.10.6.4).
|
||||
fn to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let rx = this_regexp(interp, &this, "toString")?;
|
||||
let rv = Value::Object(rx);
|
||||
let source = interp.get_value(&rv, &key("source"))?;
|
||||
let source = interp.to_str(&source)?;
|
||||
let mut flags = String::from("/");
|
||||
for (name, c) in [("global", 'g'), ("ignoreCase", 'i'), ("multiline", 'm')] {
|
||||
if interp.get_value(&rv, &key(name))?.truthy() {
|
||||
flags.push(c);
|
||||
}
|
||||
}
|
||||
let head = interp.concat(&Str::from("/"), &source)?;
|
||||
interp.concat(&head, &Str::from(flags.as_str())).map(Value::String)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,699 @@
|
||||
//! `String` (ECMA-262 5.1 section 15.5): the constructor, `String.fromCharCode` and the methods of
|
||||
//! `String.prototype`, with `substr` from Annex B.
|
||||
//!
|
||||
//! The methods read a string's code units once (flattening a rope), take a unit of fuel for every
|
||||
//! [`UNITS_PER_FUEL`] code units they read or write, and reserve each new string's bytes on the
|
||||
//! heap before building it, so a method that would make a string past the heap limit ends the
|
||||
//! run instead of allocating it.
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::LimitKind;
|
||||
use crate::builtins::{arg, constructor, method, native};
|
||||
use crate::heap::{Class, NativeKind, OPEN, ObjId, Object, PROP_BYTES, Prop, string_bytes};
|
||||
use crate::interp::{Abrupt, Interp, JsResult};
|
||||
use crate::regexp_builtins::{exec_raw, new_regexp_from, regexp_of};
|
||||
use crate::regexp_vm::{Matcher, NONE, use_fuel_n};
|
||||
use crate::value::{Str, Value, index_key, is_str_space, to_uint32};
|
||||
|
||||
/// Code units a unit of fuel pays for when a built-in reads or writes text.
|
||||
pub(crate) const UNITS_PER_FUEL: usize = 16;
|
||||
|
||||
pub(crate) fn install(interp: &mut Interp) -> JsResult<()> {
|
||||
let proto = interp.intrinsics.string_proto;
|
||||
let ctor = native(interp, "String", string_ctor, 1, NativeKind::Plain, true)?;
|
||||
constructor(interp, "String", ctor, proto)?;
|
||||
method(interp, ctor, "fromCharCode", from_char_code, 1)?;
|
||||
let methods: [(&'static str, crate::heap::NativeFn, u32); 20] = [
|
||||
("toString", to_string, 0),
|
||||
("valueOf", to_string, 0),
|
||||
("charAt", char_at, 1),
|
||||
("charCodeAt", char_code_at, 1),
|
||||
("concat", concat, 1),
|
||||
("indexOf", index_of, 1),
|
||||
("lastIndexOf", last_index_of, 1),
|
||||
("localeCompare", locale_compare, 1),
|
||||
("match", match_, 1),
|
||||
("replace", replace, 2),
|
||||
("search", search, 1),
|
||||
("slice", slice, 2),
|
||||
("split", split, 2),
|
||||
("substring", substring, 2),
|
||||
("substr", substr, 2),
|
||||
("toLowerCase", to_lower_case, 0),
|
||||
("toLocaleLowerCase", to_lower_case, 0),
|
||||
("toUpperCase", to_upper_case, 0),
|
||||
("toLocaleUpperCase", to_upper_case, 0),
|
||||
("trim", trim, 0),
|
||||
];
|
||||
for (name, f, len) in methods {
|
||||
method(interp, proto, name, f, len)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---- helpers the other text built-ins share ----
|
||||
|
||||
/// ToInteger (section 9.4) applied to a number.
|
||||
pub(crate) fn to_integer(n: f64) -> f64 {
|
||||
if n.is_nan() {
|
||||
0.0
|
||||
} else if n.is_infinite() {
|
||||
n
|
||||
} else {
|
||||
n.trunc() + 0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes the fuel for reading or writing `n` code units.
|
||||
pub(crate) fn charge_units(interp: &mut Interp, n: usize) -> JsResult<()> {
|
||||
use_fuel_n(interp, (n / UNITS_PER_FUEL) as u64 + 1)
|
||||
}
|
||||
|
||||
/// Reserves the heap bytes of a new string of `len` code units before it is built, ending the
|
||||
/// run with the heap limit when it would not fit. Everything the caller still needs must be
|
||||
/// reachable from the roots.
|
||||
pub(crate) fn reserve_units(interp: &mut Interp, len: usize) -> JsResult<()> {
|
||||
if len > interp.limits.heap_bytes / 4 {
|
||||
return Err(Abrupt::Limit(LimitKind::Heap));
|
||||
}
|
||||
let bytes = string_bytes(len);
|
||||
interp.reserve_heap(bytes)?;
|
||||
interp.heap.bytes = interp.heap.bytes.saturating_add(bytes);
|
||||
charge_units(interp, len)
|
||||
}
|
||||
|
||||
/// A new string holding `units[start..end]`.
|
||||
pub(crate) fn substring_value(interp: &mut Interp, units: &[u16], start: usize, end: usize) -> JsResult<Value> {
|
||||
let part = units.get(start..end.max(start)).unwrap_or_default();
|
||||
reserve_units(interp, part.len())?;
|
||||
Ok(Value::String(Str::from_units(part.to_vec())))
|
||||
}
|
||||
|
||||
/// Runs `f`, then drops the temporary roots it added.
|
||||
pub(crate) fn rooted<T>(interp: &mut Interp, f: impl FnOnce(&mut Interp) -> JsResult<T>) -> JsResult<T> {
|
||||
let mark = interp.temp_roots.len();
|
||||
let r = f(interp);
|
||||
interp.temp_roots.truncate(mark);
|
||||
r
|
||||
}
|
||||
|
||||
/// A new, empty array, kept in the temporary roots: call it inside [`rooted`].
|
||||
pub(crate) fn new_array(interp: &mut Interp) -> ObjId {
|
||||
let proto = interp.intrinsics.array_proto;
|
||||
let a = interp.heap.alloc_object(Object::new(Class::Array { length: 0, length_writable: true }, Some(proto)));
|
||||
interp.temp_roots.push(Value::Object(a));
|
||||
a
|
||||
}
|
||||
|
||||
/// Sets element `i` of an array made by [`new_array`], reserving its bytes first.
|
||||
pub(crate) fn set_element(interp: &mut Interp, a: ObjId, i: usize, v: Value) -> JsResult<()> {
|
||||
let i = u32::try_from(i).map_err(|_| Abrupt::Limit(LimitKind::Heap))?;
|
||||
interp.temp_roots.push(v.clone());
|
||||
let reserved = interp.reserve_heap(PROP_BYTES + string_bytes(10));
|
||||
interp.temp_roots.pop();
|
||||
reserved?;
|
||||
interp.use_fuel()?;
|
||||
interp.define(a, index_key(i), Prop::Data { value: v, flags: OPEN })
|
||||
}
|
||||
|
||||
/// CheckObjectCoercible on `this`, then ToString.
|
||||
pub(crate) fn this_str(interp: &mut Interp, this: &Value, method: &str) -> JsResult<Str> {
|
||||
if this.is_nullish() {
|
||||
let message = format!("String.prototype.{method} was called on null or undefined");
|
||||
return Err(interp.type_error(&message));
|
||||
}
|
||||
interp.to_str(this)
|
||||
}
|
||||
|
||||
/// ToInteger of an argument.
|
||||
fn int_arg(interp: &mut Interp, args: &[Value], i: usize) -> JsResult<f64> {
|
||||
let n = interp.to_number(&arg(args, i))?;
|
||||
Ok(to_integer(n))
|
||||
}
|
||||
|
||||
/// Clamps an integer to `0..=len`.
|
||||
fn clamp(n: f64, len: usize) -> usize {
|
||||
if n <= 0.0 {
|
||||
0
|
||||
} else if n >= len as f64 {
|
||||
len
|
||||
} else {
|
||||
n as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// The first index at `from` or after where `needle` occurs in `hay`. Every position tried takes
|
||||
/// fuel.
|
||||
fn find(interp: &mut Interp, hay: &[u16], needle: &[u16], from: usize) -> JsResult<Option<usize>> {
|
||||
if needle.len() > hay.len() {
|
||||
return Ok(None);
|
||||
}
|
||||
let last = hay.len() - needle.len();
|
||||
let per = (needle.len() / UNITS_PER_FUEL) as u64 + 1;
|
||||
let mut steps = 0u64;
|
||||
let mut i = from;
|
||||
let mut found = None;
|
||||
while i <= last {
|
||||
steps += per;
|
||||
if steps >= 1024 {
|
||||
use_fuel_n(interp, std::mem::take(&mut steps))?;
|
||||
}
|
||||
if hay.get(i..i + needle.len()) == Some(needle) {
|
||||
found = Some(i);
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
use_fuel_n(interp, steps)?;
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
// ---- the constructor ----
|
||||
|
||||
/// `String(value)` and `new String(value)` (sections 15.5.1 and 15.5.2).
|
||||
fn string_ctor(interp: &mut Interp, _: Value, args: &[Value], construct: bool) -> JsResult<Value> {
|
||||
let s = match args.first() {
|
||||
Some(v) => interp.to_str(v)?,
|
||||
None => Str::from(""),
|
||||
};
|
||||
if !construct {
|
||||
return Ok(Value::String(s));
|
||||
}
|
||||
let proto = interp.intrinsics.string_proto;
|
||||
Ok(Value::Object(interp.heap.alloc_object(Object::new(Class::String(s), Some(proto)))))
|
||||
}
|
||||
|
||||
/// `String.fromCharCode(…)` (section 15.5.3.2).
|
||||
fn from_char_code(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let mut units = Vec::with_capacity(args.len());
|
||||
for a in args {
|
||||
let n = interp.to_number(a)?;
|
||||
units.push(to_uint32(n) as u16);
|
||||
}
|
||||
reserve_units(interp, units.len())?;
|
||||
Ok(Value::String(Str::from_units(units)))
|
||||
}
|
||||
|
||||
// ---- String.prototype ----
|
||||
|
||||
/// `toString` and `valueOf` (sections 15.5.4.2 and 15.5.4.3).
|
||||
fn to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
match &this {
|
||||
Value::String(_) => Ok(this),
|
||||
Value::Object(o) => match interp.heap.object(*o).map(|o| &o.class) {
|
||||
Some(Class::String(s)) => Ok(Value::String(s.clone())),
|
||||
_ => Err(interp.type_error("String.prototype.toString was called on a value that is not a string")),
|
||||
},
|
||||
_ => Err(interp.type_error("String.prototype.toString was called on a value that is not a string")),
|
||||
}
|
||||
}
|
||||
|
||||
fn char_at(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let s = this_str(interp, &this, "charAt")?;
|
||||
let pos = int_arg(interp, args, 0)?;
|
||||
if pos < 0.0 || pos >= s.len() as f64 {
|
||||
return Ok(Value::str(""));
|
||||
}
|
||||
let unit = s.flat().get(pos as usize).copied().unwrap_or(0);
|
||||
Ok(Value::String(Str::from_units(vec![unit])))
|
||||
}
|
||||
|
||||
fn char_code_at(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let s = this_str(interp, &this, "charCodeAt")?;
|
||||
let pos = int_arg(interp, args, 0)?;
|
||||
if pos < 0.0 || pos >= s.len() as f64 {
|
||||
return Ok(Value::Number(f64::NAN));
|
||||
}
|
||||
Ok(Value::Number(f64::from(s.flat().get(pos as usize).copied().unwrap_or(0))))
|
||||
}
|
||||
|
||||
fn concat(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let mut s = this_str(interp, &this, "concat")?;
|
||||
for a in args {
|
||||
let r = interp.to_str(a)?;
|
||||
s = interp.concat(&s, &r)?;
|
||||
}
|
||||
Ok(Value::String(s))
|
||||
}
|
||||
|
||||
fn index_of(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let s = this_str(interp, &this, "indexOf")?;
|
||||
let search = interp.to_str(&arg(args, 0))?;
|
||||
let pos = int_arg(interp, args, 1)?;
|
||||
let (hay, needle) = (s.flat(), search.flat());
|
||||
let start = clamp(pos, hay.len());
|
||||
let found = find(interp, &hay, &needle, start)?;
|
||||
Ok(Value::Number(found.map_or(-1.0, |i| i as f64)))
|
||||
}
|
||||
|
||||
fn last_index_of(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let s = this_str(interp, &this, "lastIndexOf")?;
|
||||
let search = interp.to_str(&arg(args, 0))?;
|
||||
let n = interp.to_number(&arg(args, 1))?;
|
||||
let pos = if n.is_nan() { f64::INFINITY } else { to_integer(n) };
|
||||
let (hay, needle) = (s.flat(), search.flat());
|
||||
let start = clamp(pos, hay.len());
|
||||
if needle.len() > hay.len() {
|
||||
return Ok(Value::Number(-1.0));
|
||||
}
|
||||
let mut k = start.min(hay.len() - needle.len());
|
||||
let per = (needle.len() / UNITS_PER_FUEL) as u64 + 1;
|
||||
loop {
|
||||
use_fuel_n(interp, per)?;
|
||||
if hay.get(k..k + needle.len()) == Some(&needle[..]) {
|
||||
return Ok(Value::Number(k as f64));
|
||||
}
|
||||
if k == 0 {
|
||||
return Ok(Value::Number(-1.0));
|
||||
}
|
||||
k -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// `localeCompare` (section 15.5.4.9): code unit order.
|
||||
fn locale_compare(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let s = this_str(interp, &this, "localeCompare")?;
|
||||
let that = interp.to_str(&arg(args, 0))?;
|
||||
let (a, b) = (s.flat(), that.flat());
|
||||
charge_units(interp, a.len().min(b.len()))?;
|
||||
Ok(Value::Number(match a.cmp(&b) {
|
||||
std::cmp::Ordering::Less => -1.0,
|
||||
std::cmp::Ordering::Equal => 0.0,
|
||||
std::cmp::Ordering::Greater => 1.0,
|
||||
}))
|
||||
}
|
||||
|
||||
fn slice(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let s = this_str(interp, &this, "slice")?;
|
||||
let len = s.len() as f64;
|
||||
let start = int_arg(interp, args, 0)?;
|
||||
let end = if matches!(arg(args, 1), Value::Undefined) { len } else { int_arg(interp, args, 1)? };
|
||||
let from = if start < 0.0 { (len + start).max(0.0) } else { start.min(len) };
|
||||
let to = if end < 0.0 { (len + end).max(0.0) } else { end.min(len) };
|
||||
if to <= from {
|
||||
return Ok(Value::str(""));
|
||||
}
|
||||
substring_value(interp, &s.flat(), from as usize, to as usize)
|
||||
}
|
||||
|
||||
fn substring(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let s = this_str(interp, &this, "substring")?;
|
||||
let len = s.len();
|
||||
let start = int_arg(interp, args, 0)?;
|
||||
let end = if matches!(arg(args, 1), Value::Undefined) { len as f64 } else { int_arg(interp, args, 1)? };
|
||||
let (a, b) = (clamp(start, len), clamp(end, len));
|
||||
substring_value(interp, &s.flat(), a.min(b), a.max(b))
|
||||
}
|
||||
|
||||
/// `substr` (Annex B.2.3). Its first step applies ToString to `this` with no object-coercible
|
||||
/// check, so `null` and `undefined` give substrings of `"null"` and `"undefined"`.
|
||||
fn substr(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
let s = interp.to_str(&this)?;
|
||||
let len = s.len() as f64;
|
||||
let start = int_arg(interp, args, 0)?;
|
||||
let length = if matches!(arg(args, 1), Value::Undefined) { f64::INFINITY } else { int_arg(interp, args, 1)? };
|
||||
let start = if start >= 0.0 { start } else { (len + start).max(0.0) };
|
||||
let count = length.max(0.0).min(len - start);
|
||||
if count <= 0.0 {
|
||||
return Ok(Value::str(""));
|
||||
}
|
||||
substring_value(interp, &s.flat(), start as usize, (start + count) as usize)
|
||||
}
|
||||
|
||||
/// The UTF-16 length of `c` mapped to lower or upper case.
|
||||
fn mapped_len(c: char, upper: bool) -> usize {
|
||||
if upper { c.to_uppercase().map(char::len_utf16).sum() } else { c.to_lowercase().map(char::len_utf16).sum() }
|
||||
}
|
||||
|
||||
/// Maps the characters of `units` to lower or upper case (Unicode's full mappings, final sigma
|
||||
/// included), passing lone surrogates through. The result's length is reserved before it is built.
|
||||
fn map_case(interp: &mut Interp, units: &[u16], upper: bool) -> JsResult<Vec<u16>> {
|
||||
let len: usize = char::decode_utf16(units.iter().copied()).map(|r| r.map_or(1, |c| mapped_len(c, upper))).sum();
|
||||
reserve_units(interp, len)?;
|
||||
let mut out = Vec::with_capacity(len);
|
||||
let mut run = String::new();
|
||||
let flush = |run: &mut String, out: &mut Vec<u16>| {
|
||||
let mapped = if upper { run.to_uppercase() } else { run.to_lowercase() };
|
||||
out.extend(mapped.encode_utf16());
|
||||
run.clear();
|
||||
};
|
||||
for r in char::decode_utf16(units.iter().copied()) {
|
||||
match r {
|
||||
Ok(c) => run.push(c),
|
||||
Err(e) => {
|
||||
flush(&mut run, &mut out);
|
||||
out.push(e.unpaired_surrogate());
|
||||
}
|
||||
}
|
||||
}
|
||||
flush(&mut run, &mut out);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn to_lower_case(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let s = this_str(interp, &this, "toLowerCase")?;
|
||||
let out = map_case(interp, &s.flat(), false)?;
|
||||
Ok(Value::String(Str::from_units(out)))
|
||||
}
|
||||
|
||||
fn to_upper_case(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let s = this_str(interp, &this, "toUpperCase")?;
|
||||
let out = map_case(interp, &s.flat(), true)?;
|
||||
Ok(Value::String(Str::from_units(out)))
|
||||
}
|
||||
|
||||
fn trim(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
|
||||
let s = this_str(interp, &this, "trim")?;
|
||||
let units = s.flat();
|
||||
// The scan reads every unit once at most.
|
||||
charge_units(interp, units.len())?;
|
||||
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);
|
||||
if start == 0 && end == units.len() {
|
||||
return Ok(Value::String(s));
|
||||
}
|
||||
substring_value(interp, &units, start, end)
|
||||
}
|
||||
|
||||
// ---- the methods that take a regular expression ----
|
||||
|
||||
/// The regular expression argument of `match` and `search`: a RegExp object, or a new one made
|
||||
/// from the value as `new RegExp(value)` would.
|
||||
fn regexp_arg(interp: &mut Interp, v: &Value) -> JsResult<ObjId> {
|
||||
if let Some((o, _)) = regexp_of(interp, v) {
|
||||
return Ok(o);
|
||||
}
|
||||
let o = new_regexp_from(interp, v, &Value::Undefined)?;
|
||||
interp.temp_roots.push(Value::Object(o));
|
||||
Ok(o)
|
||||
}
|
||||
|
||||
/// `match` (section 15.5.4.10).
|
||||
fn match_(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
rooted(interp, |interp| {
|
||||
let s = this_str(interp, &this, "match")?;
|
||||
let rx = regexp_arg(interp, &arg(args, 0))?;
|
||||
let global = interp.get_value(&Value::Object(rx), &crate::heap::key("global"))?.truthy();
|
||||
if !global {
|
||||
return crate::regexp_builtins::exec_value(interp, rx, &s);
|
||||
}
|
||||
let last_index = crate::heap::key("lastIndex");
|
||||
interp.put(rx, last_index.clone(), Value::Number(0.0), true)?;
|
||||
let a = new_array(interp);
|
||||
let mut n = 0usize;
|
||||
let mut previous = 0.0;
|
||||
let units = s.flat();
|
||||
while let Some(caps) = exec_raw(interp, rx, &s)? {
|
||||
let this_index = interp.get_value(&Value::Object(rx), &last_index)?;
|
||||
let this_index = interp.to_number(&this_index)?;
|
||||
if this_index == previous {
|
||||
previous = this_index + 1.0;
|
||||
interp.put(rx, last_index.clone(), Value::Number(previous), true)?;
|
||||
} else {
|
||||
previous = this_index;
|
||||
}
|
||||
let m = substring_value(interp, &units, caps.first().copied().unwrap_or(0), caps.get(1).copied().unwrap_or(0))?;
|
||||
set_element(interp, a, n, m)?;
|
||||
n += 1;
|
||||
}
|
||||
Ok(if n == 0 { Value::Null } else { Value::Object(a) })
|
||||
})
|
||||
}
|
||||
|
||||
/// `search` (section 15.5.4.12): the index of the first match from the start, ignoring
|
||||
/// `lastIndex` and `global`.
|
||||
fn search(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
rooted(interp, |interp| {
|
||||
let s = this_str(interp, &this, "search")?;
|
||||
let rx = regexp_arg(interp, &arg(args, 0))?;
|
||||
let Some((_, re)) = regexp_of(interp, &Value::Object(rx)) else { return Err(interp.type_error("not a regular expression")) };
|
||||
let units = s.flat();
|
||||
let mut m = Matcher::new(&re, interp)?;
|
||||
let found = m.search(interp, &units, 0)?;
|
||||
Ok(Value::Number(if found { m.caps.first().copied().unwrap_or(0) as f64 } else { -1.0 }))
|
||||
})
|
||||
}
|
||||
|
||||
/// A piece of a result string: a range of the input, or other text.
|
||||
enum Piece {
|
||||
Range(usize, usize),
|
||||
Text(Str),
|
||||
}
|
||||
|
||||
impl Piece {
|
||||
fn len(&self) -> usize {
|
||||
match self {
|
||||
Piece::Range(a, b) => b.saturating_sub(*a),
|
||||
Piece::Text(s) => s.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Joins pieces into a new string, reserving its length first.
|
||||
fn build(interp: &mut Interp, units: &[u16], pieces: &[Piece]) -> JsResult<Value> {
|
||||
let total = pieces.iter().map(Piece::len).fold(0usize, usize::saturating_add);
|
||||
reserve_units(interp, total)?;
|
||||
let mut out = Vec::with_capacity(total);
|
||||
for p in pieces {
|
||||
match p {
|
||||
Piece::Range(a, b) => out.extend_from_slice(units.get(*a..*b).unwrap_or_default()),
|
||||
Piece::Text(s) => out.extend_from_slice(&s.flat()),
|
||||
}
|
||||
}
|
||||
Ok(Value::String(Str::from_units(out)))
|
||||
}
|
||||
|
||||
/// The pieces a replacement pattern (section 15.5.4.11, Table 22) gives for one match: `caps`
|
||||
/// holds the match and its groups as pairs of positions in `units`.
|
||||
fn expand(template: &[u16], units: &[u16], caps: &[usize], out: &mut Vec<Piece>) {
|
||||
let groups = caps.len() / 2 - 1;
|
||||
let (start, end) = (caps.first().copied().unwrap_or(0), caps.get(1).copied().unwrap_or(0));
|
||||
let digit = |i: usize| template.get(i).filter(|&&u| (0x30..=0x39).contains(&u)).map(|&u| usize::from(u - 0x30));
|
||||
let group = |n: usize, out: &mut Vec<Piece>| {
|
||||
let (s, e) = (caps.get(2 * n).copied().unwrap_or(NONE), caps.get(2 * n + 1).copied().unwrap_or(NONE));
|
||||
if s != NONE && e != NONE {
|
||||
out.push(Piece::Range(s, e));
|
||||
}
|
||||
};
|
||||
let mut lit = 0;
|
||||
let mut i = 0;
|
||||
while let Some(&u) = template.get(i) {
|
||||
if u != 0x24 {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let flush = |out: &mut Vec<Piece>, to: usize| {
|
||||
if to > lit {
|
||||
out.push(Piece::Text(Str::from_units(template.get(lit..to).unwrap_or_default().to_vec())));
|
||||
}
|
||||
};
|
||||
let taken = match template.get(i + 1) {
|
||||
Some(0x24) => {
|
||||
flush(out, i + 1);
|
||||
lit = i + 2;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
Some(0x26) => {
|
||||
flush(out, i);
|
||||
out.push(Piece::Range(start, end));
|
||||
2
|
||||
}
|
||||
Some(0x60) => {
|
||||
flush(out, i);
|
||||
out.push(Piece::Range(0, start));
|
||||
2
|
||||
}
|
||||
Some(0x27) => {
|
||||
flush(out, i);
|
||||
out.push(Piece::Range(end, units.len()));
|
||||
2
|
||||
}
|
||||
_ => match (digit(i + 1), digit(i + 2)) {
|
||||
(Some(a), Some(b)) if (1..=groups).contains(&(a * 10 + b)) => {
|
||||
flush(out, i);
|
||||
group(a * 10 + b, out);
|
||||
3
|
||||
}
|
||||
(Some(a), _) if (1..=groups).contains(&a) => {
|
||||
flush(out, i);
|
||||
group(a, out);
|
||||
2
|
||||
}
|
||||
_ => {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
},
|
||||
};
|
||||
i += taken;
|
||||
lit = i;
|
||||
}
|
||||
if template.len() > lit {
|
||||
out.push(Piece::Text(Str::from_units(template.get(lit..).unwrap_or_default().to_vec())));
|
||||
}
|
||||
}
|
||||
|
||||
/// `replace` (section 15.5.4.11).
|
||||
fn replace(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
rooted(interp, |interp| {
|
||||
let s = this_str(interp, &this, "replace")?;
|
||||
let search_value = arg(args, 0);
|
||||
let replace_value = arg(args, 1);
|
||||
let units = s.flat();
|
||||
// Each match as the positions of the match and its groups.
|
||||
let mut matches: Vec<Vec<usize>> = Vec::new();
|
||||
if let Some((rx, _)) = regexp_of(interp, &search_value) {
|
||||
let global = interp.get_value(&search_value, &crate::heap::key("global"))?.truthy();
|
||||
if global {
|
||||
let last_index = crate::heap::key("lastIndex");
|
||||
interp.put(rx, last_index.clone(), Value::Number(0.0), true)?;
|
||||
let mut previous = 0.0;
|
||||
// The bytes the stored matches and the pieces of the result made from them hold,
|
||||
// kept within the heap room left.
|
||||
let mut stored = 0usize;
|
||||
while let Some(caps) = exec_raw(interp, rx, &s)? {
|
||||
stored = stored.saturating_add(24 + size_of::<usize>() * caps.len() + 2 * size_of::<Piece>());
|
||||
interp.reserve_heap(stored)?;
|
||||
let this_index = interp.get_value(&search_value, &last_index)?;
|
||||
let this_index = interp.to_number(&this_index)?;
|
||||
if this_index == previous {
|
||||
previous = this_index + 1.0;
|
||||
interp.put(rx, last_index.clone(), Value::Number(previous), true)?;
|
||||
} else {
|
||||
previous = this_index;
|
||||
}
|
||||
matches.push(caps);
|
||||
}
|
||||
} else if let Some(caps) = exec_raw(interp, rx, &s)? {
|
||||
matches.push(caps);
|
||||
}
|
||||
} else {
|
||||
let search = interp.to_str(&search_value)?;
|
||||
let needle = search.flat();
|
||||
if let Some(i) = find(interp, &units, &needle, 0)? {
|
||||
matches.push(vec![i, i + needle.len()]);
|
||||
}
|
||||
}
|
||||
let replacer = interp.is_callable(&replace_value).then(|| replace_value.clone());
|
||||
let template = match &replacer {
|
||||
Some(_) => None,
|
||||
None => Some(interp.to_str(&replace_value)?.flat()),
|
||||
};
|
||||
if matches.is_empty() {
|
||||
return Ok(Value::String(s));
|
||||
}
|
||||
let mut pieces = Vec::new();
|
||||
let mut last = 0usize;
|
||||
for caps in &matches {
|
||||
let (start, end) = (caps.first().copied().unwrap_or(0), caps.get(1).copied().unwrap_or(0));
|
||||
pieces.push(Piece::Range(last, start));
|
||||
match (&replacer, &template) {
|
||||
(Some(f), _) => {
|
||||
let mut call_args = Vec::with_capacity(caps.len() / 2 + 2);
|
||||
for pair in caps.chunks(2) {
|
||||
let v = match pair {
|
||||
[a, b] if *a != NONE && *b != NONE => substring_value(interp, &units, *a, *b)?,
|
||||
_ => Value::Undefined,
|
||||
};
|
||||
call_args.push(v);
|
||||
}
|
||||
call_args.push(Value::Number(start as f64));
|
||||
call_args.push(Value::String(s.clone()));
|
||||
let r = interp.call(f, &Value::Undefined, &call_args)?;
|
||||
let text = interp.to_str(&r)?;
|
||||
pieces.push(Piece::Text(text));
|
||||
}
|
||||
(None, Some(t)) => expand(t, &units, caps, &mut pieces),
|
||||
(None, None) => {}
|
||||
}
|
||||
use_fuel_n(interp, 1)?;
|
||||
last = end;
|
||||
}
|
||||
pieces.push(Piece::Range(last, units.len()));
|
||||
build(interp, &units, &pieces)
|
||||
})
|
||||
}
|
||||
|
||||
/// `split` (section 15.5.4.14).
|
||||
fn split(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
|
||||
rooted(interp, |interp| {
|
||||
let s = this_str(interp, &this, "split")?;
|
||||
let separator = arg(args, 0);
|
||||
let limit = arg(args, 1);
|
||||
let a = new_array(interp);
|
||||
let lim = if matches!(limit, Value::Undefined) { u32::MAX } else { to_uint32(interp.to_number(&limit)?) };
|
||||
let rx = regexp_of(interp, &separator);
|
||||
let sep = match &rx {
|
||||
Some(_) => None,
|
||||
None => Some(interp.to_str(&separator)?.flat()),
|
||||
};
|
||||
if lim == 0 {
|
||||
return Ok(Value::Object(a));
|
||||
}
|
||||
if matches!(separator, Value::Undefined) {
|
||||
set_element(interp, a, 0, Value::String(s))?;
|
||||
return Ok(Value::Object(a));
|
||||
}
|
||||
let units = s.flat();
|
||||
let size = units.len();
|
||||
let re: Option<Rc<crate::regexp_vm::Regex>> = rx.map(|(_, re)| re);
|
||||
let mut matcher = re.as_deref().map(|re| Matcher::new(re, interp)).transpose()?;
|
||||
// SplitMatch (section 15.5.4.14): the end of a separator match at `q` and the groups.
|
||||
let mut split_match = |interp: &mut Interp, q: usize| -> JsResult<Option<(usize, Vec<usize>)>> {
|
||||
match (&mut matcher, &sep) {
|
||||
(Some(m), _) => {
|
||||
if m.match_at(interp, &units, q)? {
|
||||
Ok(Some((m.caps.get(1).copied().unwrap_or(q), m.caps.get(2..).map(<[usize]>::to_vec).unwrap_or_default())))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
(None, Some(r)) => {
|
||||
use_fuel_n(interp, (r.len() / UNITS_PER_FUEL) as u64 + 1)?;
|
||||
if units.get(q..q + r.len()) == Some(&r[..]) { Ok(Some((q + r.len(), Vec::new()))) } else { Ok(None) }
|
||||
}
|
||||
(None, None) => Ok(None),
|
||||
}
|
||||
};
|
||||
if size == 0 {
|
||||
if split_match(interp, 0)?.is_none() {
|
||||
set_element(interp, a, 0, Value::String(s.clone()))?;
|
||||
}
|
||||
return Ok(Value::Object(a));
|
||||
}
|
||||
let mut n = 0usize;
|
||||
let (mut p, mut q) = (0usize, 0usize);
|
||||
while q != size {
|
||||
match split_match(interp, q)? {
|
||||
None => q += 1,
|
||||
Some((e, _)) if e == p => q += 1,
|
||||
Some((e, caps)) => {
|
||||
let t = substring_value(interp, &units, p, q)?;
|
||||
set_element(interp, a, n, t)?;
|
||||
n += 1;
|
||||
if n as u64 == u64::from(lim) {
|
||||
return Ok(Value::Object(a));
|
||||
}
|
||||
p = e;
|
||||
for pair in caps.chunks(2) {
|
||||
let v = match pair {
|
||||
[x, y] if *x != NONE && *y != NONE => substring_value(interp, &units, *x, *y)?,
|
||||
_ => Value::Undefined,
|
||||
};
|
||||
set_element(interp, a, n, v)?;
|
||||
n += 1;
|
||||
if n as u64 == u64::from(lim) {
|
||||
return Ok(Value::Object(a));
|
||||
}
|
||||
}
|
||||
q = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
let t = substring_value(interp, &units, p, size)?;
|
||||
set_element(interp, a, n, t)?;
|
||||
Ok(Value::Object(a))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
//! Tests of `String`, `Number`, `RegExp` and `Date` run through a realm.
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
use crate::{JsValue, LimitKind, Limits, Options, Realm, RunError, RunErrorKind};
|
||||
|
||||
fn run_in(realm: &mut Realm, src: &str) -> JsValue {
|
||||
match realm.run(src) {
|
||||
Ok(v) => v,
|
||||
Err(e) => panic!("{src}\n{e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(src: &str) -> JsValue {
|
||||
run_in(&mut Realm::new(), src)
|
||||
}
|
||||
|
||||
fn string(src: &str) -> String {
|
||||
match run(src) {
|
||||
JsValue::String(s) => s,
|
||||
other => panic!("{src}: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Script helpers that need no `Array.prototype` method: `show(a, sep)` joins an array (`null`
|
||||
/// shows as `null`, `undefined` elements as nothing), `nan(n)` tells NaN apart.
|
||||
const HELPERS: &str = "function show(a, sep) { if (a === null) return 'null'; var s = ''; \
|
||||
for (var i = 0; i < a.length; i++) s += (i ? (sep === undefined ? ',' : sep) : '') + (a[i] === undefined ? '' : a[i]); return s; } \
|
||||
function nan(n) { return n !== n ? 'NaN' : 'num'; }\n";
|
||||
|
||||
/// Runs `src` after [`HELPERS`] and checks it gives the string `want`.
|
||||
fn shows(src: &str, want: &str) {
|
||||
match run(&format!("{HELPERS}{src}")) {
|
||||
JsValue::String(s) => assert_eq!(s, want, "{src}"),
|
||||
other => panic!("{src}: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn truth(src: &str) {
|
||||
assert_eq!(run(src), JsValue::Bool(true), "{src}");
|
||||
}
|
||||
|
||||
fn fail_with(mut realm: Realm, src: &str) -> RunError {
|
||||
match realm.run(src) {
|
||||
Ok(v) => panic!("{src} completed with {v:?}"),
|
||||
Err(e) => e,
|
||||
}
|
||||
}
|
||||
|
||||
fn thrown(src: &str) -> String {
|
||||
let e = fail_with(Realm::new(), src);
|
||||
assert_eq!(e.kind, RunErrorKind::Thrown, "{src}: {e:?}");
|
||||
e.message
|
||||
}
|
||||
|
||||
// ---- String ----
|
||||
|
||||
#[test]
|
||||
fn string_converts_and_wraps() {
|
||||
assert_eq!(string("String(12.5) + String() + String(null)"), "12.5null");
|
||||
truth("var s = new String('ab'); typeof s === 'object' && s.length === 2 && s[1] === 'b' && s.valueOf() === 'ab'");
|
||||
assert_eq!(string("String.fromCharCode(104, 105, 65536 + 33)"), "hi!");
|
||||
assert!(thrown("String.prototype.toString.call({})").starts_with("TypeError"));
|
||||
assert!(thrown("String.prototype.trim.call(null)").starts_with("TypeError"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_methods_follow_section_15_5_4() {
|
||||
let cases: &[(&str, &str)] = &[
|
||||
("'abc'.charAt(1) + 'abc'.charAt(5) + 'abc'.charAt(-1)", "b"),
|
||||
("'' + 'abc'.charCodeAt(0) + nan('abc'.charCodeAt(3))", "97NaN"),
|
||||
("'a'.concat('b', 1, null)", "ab1null"),
|
||||
("'' + 'abcabc'.indexOf('c') + 'abcabc'.indexOf('c', 3) + 'abc'.indexOf('') + 'abc'.indexOf('', 9)", "2503"),
|
||||
("'' + 'abcabc'.lastIndexOf('c') + 'abcabc'.lastIndexOf('c', 4) + 'abc'.lastIndexOf('x')", "52-1"),
|
||||
("'' + 'a'.localeCompare('b') + 'b'.localeCompare('a') + 'a'.localeCompare('a')", "-110"),
|
||||
("'abcdef'.slice(1, -1) + '|' + 'abcdef'.slice(-2) + '|' + 'abc'.slice(2, 1)", "bcde|ef|"),
|
||||
("'abcdef'.substring(4, 1) + '|' + 'abcdef'.substring(-3, 2) + '|' + 'abc'.substring(1)", "bcd|ab|bc"),
|
||||
("'abcdef'.substr(-3, 2) + '|' + 'abcdef'.substr(1) + '|' + 'abc'.substr(1, -1)", "de|bcdef|"),
|
||||
("'\\u00dfa\\u03a3'.toUpperCase() + '|' + 'A\\u03a3'.toLowerCase() + '|' + 'A\\u03a3b'.toLowerCase()", "SSA\u{3a3}|a\u{3c2}|a\u{3c3}b"),
|
||||
("'[' + ' \\t\\n\\u00a0\\ufeffx y\\u2028 '.trim() + ']'", "[x y]"),
|
||||
("show('a,b,,c'.split(','), '|')", "a|b||c"),
|
||||
("show('abc'.split(''), '|') + ';' + 'abc'.split().length + ';' + show('abc'.split('', 2), '|')", "a|b|c;1;a|b"),
|
||||
("''.split('').length + ';' + ''.split('x').length", "0;1"),
|
||||
("show('a1b22c'.split(/(\\d)+/), '|')", "a|1|b|2|c"),
|
||||
("show('ab'.split(/(?:)/), '|') + ';' + show('ab'.split(/a*?/), '|')", "a|b;a|b"),
|
||||
("'aaa'.replace('a', '$&$&') + ';' + 'abc'.replace('b', '[$`|$\\'|$$]')", "aaaa;a[a|c|$]c"),
|
||||
("'John Smith'.replace(/(\\w+)\\s(\\w+)/, '$2, $1') + ';' + 'x'.replace(/x/, '$1$01$10')", "Smith, John;$1$01$10"),
|
||||
("'a-b-c'.replace(/-/g, function (m, i, s) { return '[' + i + s.length + ']'; })", "a[15]b[35]c"),
|
||||
("'aaa'.replace(/a*?/g, '-')", "-a-a-a-"),
|
||||
("'abc'.match(/b/).index + ';' + show('abab'.match(/a(b)/g), '|') + ';' + ('x'.match(/y/g) === null)", "1;ab|ab;true"),
|
||||
("'' + 'abc'.search(/c/) + 'abc'.search('b') + 'abc'.search(/x/)", "21-1"),
|
||||
];
|
||||
for (src, want) in cases {
|
||||
shows(src, want);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_methods_count_against_the_heap_before_allocating() {
|
||||
// Doubling with `concat` and `replace` stops at the heap limit, not at an allocation failure.
|
||||
let limits = Limits { heap_bytes: 8 << 20, ..Limits::default() };
|
||||
for src in [
|
||||
"var s = 'x'; for (;;) s = s.concat(s);",
|
||||
"var s = 'xx'; for (;;) s = s.replace(/x/g, 'xx');",
|
||||
"var s = 'x'; for (;;) s = (s + s).toUpperCase();",
|
||||
"var s = 'x'; for (;;) s = (s + s + s).slice(0) + s.substr(0);",
|
||||
] {
|
||||
let e = fail_with(Realm::with_options(Options::default(), limits.clone()), src);
|
||||
assert_eq!(e.kind, RunErrorKind::Limit(LimitKind::Heap), "{src}: {e:?}");
|
||||
}
|
||||
// A replacement that would build a string past the heap limit is refused before it is built.
|
||||
let limits = Limits { heap_bytes: 4 << 20, ..Limits::default() };
|
||||
let e = fail_with(
|
||||
Realm::with_options(Options::default(), limits),
|
||||
"var big = 'yyyyyyyyyyyyyyyy'; for (var i = 0; i < 15; i++) big = big + big; 'xxxxxxxxxx'.replace(/x/g, big)",
|
||||
);
|
||||
assert_eq!(e.kind, RunErrorKind::Limit(LimitKind::Heap), "{e:?}");
|
||||
}
|
||||
|
||||
// ---- Number ----
|
||||
|
||||
#[test]
|
||||
fn numbers_format_as_section_15_7_4_says() {
|
||||
let cases: &[(&str, &str)] = &[
|
||||
("(255).toString(16) + ' ' + (-255).toString(2) + ' ' + (0.5).toString(2) + ' ' + (1e21).toString(36)", "ff -11111111 0.1 5v1j4f4ds7c000"),
|
||||
("(123.456).toString() + ' ' + (1e21).toString(10) + ' ' + (0.000001).toString()", "123.456 1e+21 0.000001"),
|
||||
("(0.5).toFixed(0) + ' ' + (1.255).toFixed(2) + ' ' + (0.1).toFixed(20) + ' ' + (1e21).toFixed(2)", "1 1.25 0.10000000000000000555 1e+21"),
|
||||
("(123456).toExponential(2) + ' ' + (123456).toExponential() + ' ' + (0).toExponential(2)", "1.23e+5 1.23456e+5 0.00e+0"),
|
||||
(
|
||||
"(123.456).toPrecision(4) + ' ' + (0.0000001234).toPrecision(2) + ' ' + (123456).toPrecision(2) + ' ' + (5).toPrecision()",
|
||||
"123.5 1.2e-7 1.2e+5 5",
|
||||
),
|
||||
(
|
||||
"(-1.5).toFixed(0) + ' ' + (NaN).toFixed(2) + ' ' + (Infinity).toExponential(50) + ' ' + (-Infinity).toPrecision(50)",
|
||||
"-2 NaN Infinity -Infinity",
|
||||
),
|
||||
("new Number(5).toString(2) + ' ' + Number('0x10') + ' ' + Number() + ' ' + Number.MIN_VALUE", "101 16 0 5e-324"),
|
||||
];
|
||||
for (src, want) in cases {
|
||||
assert_eq!(string(src), *want, "{src}");
|
||||
}
|
||||
truth("Number.MAX_VALUE === 1.7976931348623157e308 && Number.POSITIVE_INFINITY === 1/0 && Number.NaN !== Number.NaN");
|
||||
truth(
|
||||
"typeof new Number(1) === 'object' && new Number(7).valueOf() === 7 && Object.prototype.toString.call(Number.prototype) === '[object Number]'",
|
||||
);
|
||||
for src in [
|
||||
"(1).toFixed(21)",
|
||||
"(1).toFixed(-1)",
|
||||
"(1).toExponential(21)",
|
||||
"(1).toPrecision(0)",
|
||||
"(1).toPrecision(22)",
|
||||
"(1).toString(1)",
|
||||
"(1).toString(37)",
|
||||
] {
|
||||
assert!(thrown(src).starts_with("RangeError"), "{src}");
|
||||
}
|
||||
assert!(thrown("Number.prototype.toFixed.call('1', 2)").starts_with("TypeError"));
|
||||
}
|
||||
|
||||
// ---- RegExp ----
|
||||
|
||||
#[test]
|
||||
fn regular_expressions_match_as_section_15_10_2_says() {
|
||||
let cases: &[(&str, &str)] = &[
|
||||
("show(/a(b)?c/.exec('ac'))", "ac,"),
|
||||
("show(/(z)((a+)?(b+)?(c))*/.exec('zaacbbbcac'))", "zaacbbbcac,z,ac,a,,c"),
|
||||
("show(/(a*)*/.exec('b'))", ","),
|
||||
("show(/(a*)b\\1+/.exec('baaaac'))", "b,"),
|
||||
("show(/(?=(a+))/.exec('baaabac'))", ",aaa"),
|
||||
("show(/(?=(a+))a*b\\1/.exec('baaabac'))", "aba,a"),
|
||||
("show(/(.*?)a(?!(a+)b\\2c)\\2(.*)/.exec('baaabaac'))", "baaabaac,ba,,abaac"),
|
||||
("show(/((a)|b)+/.exec('ab'))", "ab,b,"),
|
||||
("show(/a[a-z]{2,4}/.exec('abcdefghi'))", "abcde"),
|
||||
("show(/a[a-z]{2,4}?/.exec('abcdefghi'))", "abc"),
|
||||
("show(/(aa|aabaac|ba|b|c)*/.exec('aabaac'))", "aaba,ba"),
|
||||
("show(/[^\\d\\s]+/.exec('12 ab3'))", "ab"),
|
||||
("show(/\\bfoo\\B/.exec('a foox'))", "foo"),
|
||||
("show(/^b/m.exec('a\\nb')) + /^b/.test('a\\nb')", "bfalse"),
|
||||
("/ß/i.test('SS') + ' ' + show(/[a-z]+/i.exec('HeLLo')) + ' ' + /\\u017f/i.test('s')", "false HeLLo false"),
|
||||
("/[\\u017f]/i.test('S') + ' ' + /k/i.test('\\u212a')", "false false"),
|
||||
("show(/\\1(a)/.exec('aa')) + ';' + show(/(a\\1)/.exec('aa'))", "a,a;a,a"),
|
||||
("/\\0\\x41\\u0042\\cJ\\101/.test('\\0AB\\nA') + ' ' + /[\\b]/.test('\\b') + ' ' + /a{,2}/.test('a{,2}')", "true true true"),
|
||||
("/]/.test(']') + ' ' + show(/[\\d-z]+/.exec('5-z')) + ' ' + /\\8/.test('8')", "true 5-z true"),
|
||||
("/(?:a|b)*c/.exec('ababc').index + ' ' + /x*/.exec('yyy').index", "0 0"),
|
||||
("/[]/.test('a') + ' ' + /[^]/.test('\\n')", "false true"),
|
||||
];
|
||||
for (src, want) in cases {
|
||||
shows(src, want);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regexp_objects_follow_sections_15_10_4_to_15_10_7() {
|
||||
truth("var r = /a/g; r.lastIndex === 0 && r.global && !r.ignoreCase && !r.multiline && r.source === 'a'");
|
||||
truth(
|
||||
"var r = /a/g; r.exec('aa'); var i = r.lastIndex; r.exec('aa'); var j = r.lastIndex; r.exec('aa'); i === 1 && j === 2 && r.lastIndex === 0",
|
||||
);
|
||||
truth("var r = /a/; r.lastIndex = 5; r.test('a') && r.lastIndex === 5");
|
||||
truth("var r = new RegExp('a/b', 'im'); r.source === 'a\\\\/b' && r.ignoreCase && r.multiline && String(r) === '/a\\\\/b/im'");
|
||||
truth("String(new RegExp()) === '/(?:)/' && new RegExp('').test('x')");
|
||||
truth("var r = new RegExp('[/]'); String(r) === '/[/]/' && eval(String(r)).test('/') && eval(String(new RegExp('a/'))).source === 'a\\\\/'");
|
||||
truth("var r = /x/g; RegExp(r) === r && new RegExp(r) !== r && new RegExp(r).global");
|
||||
truth("var m = /(a)|(b)/.exec('b'); m.length === 3 && m[1] === undefined && m[2] === 'b' && m.index === 0 && m.input === 'b'");
|
||||
truth("Object.prototype.toString.call(RegExp.prototype) === '[object RegExp]' && Object.prototype.toString.call(/x/) === '[object RegExp]'");
|
||||
truth("var p = RegExp.prototype; p.hasOwnProperty('global') && p.hasOwnProperty('lastIndex') && p.source === '(?:)' && p.global === false");
|
||||
for src in ["new RegExp('(')", "new RegExp('a', 'gg')", "new RegExp('a', 'x')", "new RegExp('a**')", "new RegExp('[b-a]')"] {
|
||||
assert!(thrown(src).starts_with("SyntaxError"), "{src}");
|
||||
}
|
||||
assert!(thrown("new RegExp(/a/, 'g')").starts_with("TypeError"));
|
||||
assert!(thrown("RegExp.prototype.exec.call({}, 'a')").starts_with("TypeError"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_catastrophic_regular_expression_ends_by_fuel() {
|
||||
let limits = Limits { fuel: 2_000_000, ..Limits::default() };
|
||||
let src = "var s = ''; for (var i = 0; i < 40; i++) s += 'a'; /(a+)+$/.test(s + 'b')";
|
||||
let e = fail_with(Realm::with_options(Options::default(), limits.clone()), src);
|
||||
assert_eq!(e.kind, RunErrorKind::Limit(LimitKind::Fuel), "{e:?}");
|
||||
// The interrupt flag stops it too.
|
||||
let realm = Realm::with_options(Options::default(), Limits { fuel: u64::MAX, ..Limits::default() });
|
||||
realm.interrupt().set();
|
||||
let e = fail_with(realm, src);
|
||||
assert_eq!(e.kind, RunErrorKind::Limit(LimitKind::Interrupted), "{e:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_and_date_parsing_take_fuel_for_the_units_they_scan() {
|
||||
let limits = Limits { fuel: 40_000, ..Limits::default() };
|
||||
let build = "var s = ' '; for (var i = 0; i < 20; i++) s = s + s; s.charAt(0)";
|
||||
assert!(Realm::with_options(Options::default(), limits.clone()).run(build).is_ok());
|
||||
for scan in ["s.trim()", "Date.parse(s)", "new Date(s)"] {
|
||||
let e = fail_with(Realm::with_options(Options::default(), limits.clone()), &format!("{build}; {scan}"));
|
||||
assert_eq!(e.kind, RunErrorKind::Limit(LimitKind::Fuel), "{scan}: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_replace_counts_its_matches_against_the_heap() {
|
||||
let src = "var s = 'ab'; for (var i = 0; i < 17; i++) s = s + s; s.replace(/./g, '').length";
|
||||
let e = fail_with(Realm::with_options(Options::default(), Limits { heap_bytes: 4 << 20, ..Limits::default() }), src);
|
||||
assert_eq!(e.kind, RunErrorKind::Limit(LimitKind::Heap), "{e:?}");
|
||||
assert_eq!(run(src), JsValue::Number(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compiling_a_pattern_counts_against_the_heap() {
|
||||
let src = "var s = '[\\\\s\\\\S]a*'; for (var i = 0; i < 14; i++) s = s + s; new RegExp(s).source.length";
|
||||
let e = fail_with(Realm::with_options(Options::default(), Limits { heap_bytes: 2 << 20, ..Limits::default() }), src);
|
||||
assert_eq!(e.kind, RunErrorKind::Limit(LimitKind::Heap), "{e:?}");
|
||||
assert_eq!(run(src), JsValue::Number(131_072.0));
|
||||
// The compiler stops while it reads the pattern, before the code it builds passes the room.
|
||||
let pattern: Vec<u16> = "[\\s\\S]a*".repeat(1000).encode_utf16().collect();
|
||||
let flags = crate::regexp_vm::parse_flags(&[]).unwrap();
|
||||
let room = crate::regexp_vm::compile_bytes(400, 0, 0);
|
||||
let result = crate::regexp_vm::compile(&pattern, flags, 200, room, &mut |_| Ok(()));
|
||||
assert!(matches!(result, Err(crate::regexp_vm::CompileFail::Abrupt(crate::interp::Abrupt::Limit(LimitKind::Heap)))));
|
||||
assert!(crate::regexp_vm::compile(&pattern, flags, 200, usize::MAX, &mut |_| Ok(())).is_ok());
|
||||
// A literal pattern takes what its instructions hold, not the most a unit can take.
|
||||
let literal: Vec<u16> = "a".repeat(10_000).encode_utf16().collect();
|
||||
let room = crate::regexp_vm::compile_bytes(20_000, 0, 0);
|
||||
assert!(crate::regexp_vm::compile(&literal, flags, 200, room, &mut |_| Ok(())).is_ok());
|
||||
// A class's ranges count while they are read, even when they collapse into one range.
|
||||
let class: Vec<u16> = format!("[{}]", "a".repeat(10_000)).encode_utf16().collect();
|
||||
let result = crate::regexp_vm::compile(&class, flags, 200, 1000, &mut |_| Ok(()));
|
||||
assert!(matches!(result, Err(crate::regexp_vm::CompileFail::Abrupt(crate::interp::Abrupt::Limit(LimitKind::Heap)))));
|
||||
// The joining and flattening after the last unit count too.
|
||||
let ab: Vec<u16> = "ab".encode_utf16().collect();
|
||||
let room = crate::regexp_vm::compile_bytes(1, 0, 0);
|
||||
let result = crate::regexp_vm::compile(&ab, flags, 200, room, &mut |_| Ok(()));
|
||||
assert!(matches!(result, Err(crate::regexp_vm::CompileFail::Abrupt(crate::interp::Abrupt::Limit(LimitKind::Heap)))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compiling_a_pattern_collects_garbage_when_the_heap_is_full() {
|
||||
let mut realm = Realm::with_options(Options::default(), Limits { heap_bytes: 256 << 10, ..Limits::default() });
|
||||
let src = "var n = 0; for (var i = 0; i < 5000; i++) { var junk = 'x' + i + 'y' + i; new RegExp('(a|b)c[d-f]'); n++; } n";
|
||||
assert_eq!(run_in(&mut realm, src), JsValue::Number(5000.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_constructed_source_counts_against_the_heap_before_it_is_built() {
|
||||
let mut realm = Realm::new();
|
||||
let interp = realm.interp();
|
||||
interp.collect_for_room();
|
||||
let live = interp.heap.bytes;
|
||||
let pattern = vec![0x2028u16; 10_000];
|
||||
interp.limits.heap_bytes = live + 50_000;
|
||||
assert!(matches!(crate::regexp_builtins::source_text(interp, &pattern), Err(crate::interp::Abrupt::Limit(LimitKind::Heap))));
|
||||
interp.limits.heap_bytes = live + (1 << 20);
|
||||
let Ok(source) = crate::regexp_builtins::source_text(interp, &pattern) else { panic!("the source fits") };
|
||||
assert_eq!(source.flat().len(), 60_000);
|
||||
assert_eq!(string("String(new RegExp('a/\\\\/[/]\\n\\r\\u2028\\u2029'))"), "/a\\/\\/[/]\\n\\r\\u2028\\u2029/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_matcher_state_counts_against_the_heap() {
|
||||
let mut realm = Realm::with_options(Options::default(), Limits { heap_bytes: 1 << 20, ..Limits::default() });
|
||||
let pattern: Vec<u16> = "(a)".repeat(1000).encode_utf16().collect();
|
||||
let flags = crate::regexp_vm::parse_flags(&[]).unwrap();
|
||||
let Ok(re) = crate::regexp_vm::compile(&pattern, flags, 200, usize::MAX, &mut |_| Ok(())) else { panic!("the pattern compiles") };
|
||||
let interp = realm.interp();
|
||||
interp.collect_for_room();
|
||||
let live = interp.heap.bytes;
|
||||
interp.limits.heap_bytes = live + 1000;
|
||||
assert!(matches!(crate::regexp_vm::Matcher::new(&re, interp), Err(crate::interp::Abrupt::Limit(LimitKind::Heap))));
|
||||
interp.limits.heap_bytes = 1 << 20;
|
||||
assert!(crate::regexp_vm::Matcher::new(&re, interp).is_ok());
|
||||
// The stack takes only the room left: with room for the state and no more, backtracking ends
|
||||
// with the heap limit.
|
||||
let pattern: Vec<u16> = "a*b".encode_utf16().collect();
|
||||
let Ok(re) = crate::regexp_vm::compile(&pattern, flags, 200, usize::MAX, &mut |_| Ok(())) else { panic!("the pattern compiles") };
|
||||
let input: Vec<u16> = "aaaaaaaaaa".encode_utf16().collect();
|
||||
interp.limits.heap_bytes = live + 64;
|
||||
let Ok(mut m) = crate::regexp_vm::Matcher::new(&re, interp) else { panic!("the state fits") };
|
||||
assert!(matches!(m.search(interp, &input, 0), Err(crate::interp::Abrupt::Limit(LimitKind::Heap))));
|
||||
interp.limits.heap_bytes = 1 << 20;
|
||||
let Ok(mut m) = crate::regexp_vm::Matcher::new(&re, interp) else { panic!("the state fits") };
|
||||
assert!(matches!(m.search(interp, &input, 0), Ok(false)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deeply_nested_groups_give_a_syntax_error_and_never_overflow_the_stack() {
|
||||
let deep = |n: usize| format!("{}a{}", "(".repeat(n), ")".repeat(n));
|
||||
let src = format!("try {{ new RegExp('{}'); 'compiled' }} catch (e) {{ e.name }}", deep(50_000));
|
||||
assert_eq!(run(&src), JsValue::String("SyntaxError".into()));
|
||||
// With the depth limit raised, the same nesting compiles and matches on a small stack: the
|
||||
// compiler and the matcher keep their state on the heap.
|
||||
let handle = std::thread::Builder::new()
|
||||
.stack_size(256 << 10)
|
||||
.spawn(move || {
|
||||
let options = Options { max_depth: 60_000, ..Options::default() };
|
||||
let mut realm = Realm::with_options(options, Limits::default());
|
||||
let src = format!("var m = new RegExp('{}').exec('xa'); m.length + ':' + m[50000]", deep(50_000));
|
||||
realm.run(&src)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(handle.join().unwrap(), Ok(JsValue::String("50001:a".into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_matcher_stack_counts_against_the_heap() {
|
||||
let limits = Limits { heap_bytes: 4 << 20, ..Limits::default() };
|
||||
let src = "var s = 'ab'; for (var i = 0; i < 18; i++) s = s + s; /(?:a|b)*c/.test(s)";
|
||||
let e = fail_with(Realm::with_options(Options::default(), limits), src);
|
||||
assert_eq!(e.kind, RunErrorKind::Limit(LimitKind::Heap), "{e:?}");
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn arbitrary_patterns_and_inputs_never_panic(pattern in "[ab()|*+?{}\\[\\]^$.\\\\dws0-9,:=!-]{0,24}", input in "[ab\\n0-9 ]{0,24}", flags in "[gim]{0,2}") {
|
||||
let mut realm = Realm::with_options(Options::default(), Limits { fuel: 200_000, ..Limits::default() });
|
||||
let quote = |s: &str| s.replace('\\', "\\\\").replace('\'', "\\'").replace('\n', "\\n");
|
||||
let src = format!(
|
||||
"try {{ var r = new RegExp('{}', '{}'); var s = '{}'; [r.exec(s), s.replace(r, '$1'), s.split(r), s.match(r), s.search(r)].length }} catch (e) {{ e.name }}",
|
||||
quote(&pattern), flags, quote(&input)
|
||||
);
|
||||
match realm.run(&src) {
|
||||
Ok(_) => {}
|
||||
Err(e) => prop_assert!(matches!(e.kind, RunErrorKind::Limit(_)), "{src}: {e:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Date ----
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn arbitrary_date_strings_never_panic(text in "[0-9TZ:.+ ,()a-zA-Z-]{0,32}") {
|
||||
let mut realm = Realm::new();
|
||||
let src = format!("var t = Date.parse('{text}'); new Date('{text}').toString(); t");
|
||||
prop_assert!(realm.run(&src).is_ok(), "{src}");
|
||||
}
|
||||
}
|
||||
|
||||
fn date_realm(now: f64, offset_minutes: f64) -> Realm {
|
||||
let mut realm = Realm::new();
|
||||
realm.set_clock(move || now);
|
||||
realm.set_time_zone(move |_| offset_minutes * 60_000.0);
|
||||
realm
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dates_read_the_host_clock_and_time_zone() {
|
||||
// 2026-11-07T12:34:56.789Z.
|
||||
let now = 1_794_054_896_789.0;
|
||||
let mut utc = date_realm(now, 0.0);
|
||||
assert_eq!(run_in(&mut utc, "Date.now()"), JsValue::Number(now));
|
||||
assert_eq!(run_in(&mut utc, "new Date().getTime()"), JsValue::Number(now));
|
||||
assert_eq!(run_in(&mut utc, "new Date().toISOString()"), JsValue::String("2026-11-07T12:34:56.789Z".into()));
|
||||
assert_eq!(run_in(&mut utc, "String(new Date())"), JsValue::String("Sat Nov 07 2026 12:34:56 GMT+0000".into()));
|
||||
assert_eq!(run_in(&mut utc, "Date()"), JsValue::String("Sat Nov 07 2026 12:34:56 GMT+0000".into()));
|
||||
assert_eq!(run_in(&mut utc, "new Date().toUTCString()"), JsValue::String("Sat, 07 Nov 2026 12:34:56 GMT".into()));
|
||||
let mut east = date_realm(now, 90.0);
|
||||
assert_eq!(run_in(&mut east, "String(new Date())"), JsValue::String("Sat Nov 07 2026 14:04:56 GMT+0130".into()));
|
||||
assert_eq!(
|
||||
run_in(&mut east, "var d = new Date(); d.getHours() + ',' + d.getUTCHours() + ',' + d.getMinutes() + ',' + d.getTimezoneOffset()"),
|
||||
JsValue::String("14,12,4,-90".into())
|
||||
);
|
||||
assert_eq!(run_in(&mut east, "new Date(2026, 10, 7, 14, 4, 56, 789).getTime()"), JsValue::Number(now));
|
||||
// An offset of a day or more, or one that is not finite, counts as 0.
|
||||
for offset in [f64::MAX, 86_400_000.0, f64::NAN] {
|
||||
let mut odd = Realm::new();
|
||||
odd.set_clock(move || now);
|
||||
odd.set_time_zone(move |_| offset);
|
||||
assert_eq!(run_in(&mut odd, "var d = new Date(); d.getHours() + ',' + d.getTimezoneOffset()"), JsValue::String("12,0".into()), "{offset}");
|
||||
}
|
||||
let mut west = date_realm(now, -300.0);
|
||||
assert_eq!(run_in(&mut west, "new Date(2026, 0, 31).toDateString()"), JsValue::String("Sat Jan 31 2026".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dates_round_trip_through_their_strings() {
|
||||
let mut realm = date_realm(1_794_054_896_789.0, -420.0);
|
||||
for src in [
|
||||
"var d = new Date(); Date.parse(d.toString()) === Math_floor(d.getTime())",
|
||||
"var d = new Date(); Date.parse(d.toUTCString()) === Math_floor(d.getTime())",
|
||||
"var d = new Date(); Date.parse(d.toISOString()) === d.getTime()",
|
||||
"var d = new Date(-1e14); Date.parse(d.toISOString()) === d.getTime() && Date.parse(d.toString()) === d.getTime()",
|
||||
"new Date(new Date(5).toISOString()).getTime() === 5",
|
||||
"Date.UTC(2000, 1, 29) === Date.parse('2000-02-29') && Date.UTC(99, 0) === Date.UTC(1999, 0)",
|
||||
"Date.UTC(2000) === Date.UTC(2000, 0) && Date.UTC(1970) === 0 && isNaN_(Date.UTC())",
|
||||
"isNaN_(Date.parse('nonsense')) && isNaN_(new Date(8.64e15 + 1).getTime()) && new Date(8.64e15).getTime() === 8.64e15",
|
||||
"var d = new Date(2000, 0, 31); d.setMonth(1); d.getMonth() === 2 && d.getDate() === 2",
|
||||
"var d = new Date(NaN); d.setFullYear(2001); d.getFullYear() === 2001 && d.getMonth() === 0",
|
||||
"var d = new Date(0); d.setUTCHours(25, 61, 61, 1001); d.toISOString() === '1970-01-02T02:02:02.001Z'",
|
||||
"var d = new Date(0); d.setMinutes(); isNaN_(d.getTime())",
|
||||
"var d = new Date(2000, 0, 1); d.setYear(99); d.getFullYear() === 1999 && d.getYear() === 99",
|
||||
"Date.prototype.toGMTString === Date.prototype.toUTCString && String(new Date(NaN)) === 'Invalid Date'",
|
||||
"new Date(0).toJSON() === '1970-01-01T00:00:00.000Z' && new Date(NaN).toJSON() === null",
|
||||
"Object.prototype.toString.call(Date.prototype) === '[object Date]' && isNaN_(Date.prototype.getTime())",
|
||||
"var d = new Date(0); d + d === d.toString() + d.toString() && d - d === 0 && d == d.toString()",
|
||||
"isNaN_(Date.parse('1970-00-01')) && isNaN_(Date.parse('1970-13-01')) && isNaN_(Date.parse('1970-02-30'))",
|
||||
] {
|
||||
let full = format!("function Math_floor(t) {{ return t - t % 1000; }} function isNaN_(n) {{ return n !== n; }} {src}");
|
||||
assert_eq!(run_in(&mut realm, &full), JsValue::Bool(true), "{src}");
|
||||
}
|
||||
let e = fail_with(date_realm(0.0, 0.0), "new Date(NaN).toISOString()");
|
||||
assert!(e.message.starts_with("RangeError"), "{e:?}");
|
||||
let e = fail_with(date_realm(0.0, 0.0), "Date.prototype.getTime.call({})");
|
||||
assert!(e.message.starts_with("TypeError"), "{e:?}");
|
||||
}
|
||||
+110
-30
@@ -19,6 +19,11 @@
|
||||
//! passes when it completes, or, for `negative: phase: runtime`, when it throws an object whose
|
||||
//! `constructor` is the global its `type:` names. Its counts are printed apart from the parse
|
||||
//! counts, and its failures go to `run-failures.txt`.
|
||||
//!
|
||||
//! A third test runs the tests under `test/built-ins/String`, `Number`, `RegExp` and `Date` the same
|
||||
//! way, counting the tests that name an ES5 section apart from the other tests without a feature
|
||||
//! tag; its failures go to `builtins-failures.txt`. The interpreter tests fail only when the
|
||||
//! interpreter panics.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -203,8 +208,20 @@ const MISSING_BUILTINS: &str = "Array String Number Boolean Math JSON Date RegEx
|
||||
fromCharCode substring substr split replace match search exec test toUpperCase toLowerCase trim toFixed toPrecision toExponential \
|
||||
bind now isArray compareArray verifyProperty fnGlobalObject";
|
||||
|
||||
/// The stand-in for `String` that the harness's failure messages call.
|
||||
const HARNESS_STRING: &str = "function String(value) { return '' + value; }";
|
||||
/// The names in [`MISSING_BUILTINS`] that `String`, `Number`, `RegExp` and `Date` provide.
|
||||
const TEXT_BUILTINS: &str = "String Number Date RegExp charAt charCodeAt fromCharCode substring substr split replace match search exec test \
|
||||
toUpperCase toLowerCase trim toFixed toPrecision toExponential now";
|
||||
|
||||
/// String methods whose names `Array.prototype` also uses: provided for the tests under
|
||||
/// `built-ins/String/`, and still missing elsewhere, where the name usually means the array method.
|
||||
const STRING_METHODS_SHARED_WITH_ARRAY: &str = "indexOf lastIndexOf concat slice";
|
||||
|
||||
/// The names in [`MISSING_BUILTINS`] the interpreter still lacks, without those in each of
|
||||
/// `provided`.
|
||||
fn missing_builtins<'a>(provided: &[&str]) -> HashSet<&'a str> {
|
||||
let provided: HashSet<&str> = provided.iter().flat_map(|p| p.split_whitespace()).collect();
|
||||
MISSING_BUILTINS.split_whitespace().filter(|name| !provided.contains(name)).collect()
|
||||
}
|
||||
|
||||
/// The source names a missing built-in, as an identifier, a property name or a word inside a
|
||||
/// string literal (code passed to `eval` or `Function`), or does not parse as ES5 (the tests that
|
||||
@@ -249,32 +266,104 @@ fn es5_language_subset_runs() {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
/// The counts of one run of a set of tests through the interpreter.
|
||||
#[derive(Default)]
|
||||
struct RunCounts {
|
||||
tests: usize,
|
||||
skipped: usize,
|
||||
passed: usize,
|
||||
failed: usize,
|
||||
panicked: usize,
|
||||
failures: String,
|
||||
}
|
||||
|
||||
impl RunCounts {
|
||||
fn summary(&self) -> String {
|
||||
format!(
|
||||
"{} tests that need only the built-ins present, {} runs passed, {} failed ({} panicked), {} skipped",
|
||||
self.tests, self.passed, self.failed, self.panicked, self.skipped
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_language_subset(dir: &Path) {
|
||||
let language = dir.join("test").join("language");
|
||||
let missing = missing_builtins(&[TEXT_BUILTINS]);
|
||||
let (es5, _) = run_tests(dir, &language, &missing, false);
|
||||
let report = dir.join("run-failures.txt");
|
||||
if let Err(e) = std::fs::write(&report, &es5.failures) {
|
||||
println!("could not write {}: {e}", report.display());
|
||||
}
|
||||
println!(
|
||||
"test262 ES5 language subset through the interpreter: {} tests that need only the built-ins present, {} runs passed, \
|
||||
{} failed ({} panicked). Skipped {} tests that need other built-ins, use syntax after ES5 or are asynchronous. Failures: {}",
|
||||
es5.tests,
|
||||
es5.passed,
|
||||
es5.failed,
|
||||
es5.panicked,
|
||||
es5.skipped,
|
||||
report.display()
|
||||
);
|
||||
assert_eq!(es5.panicked, 0, "the interpreter panicked; see {}", report.display());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn es5_text_builtins_run() {
|
||||
let Some(dir) = suite_dir() else {
|
||||
println!("test262 built-ins: SKIPPED (no suite; run cargo xtask test262)");
|
||||
return;
|
||||
};
|
||||
let handle = std::thread::Builder::new().stack_size(64 << 20).spawn(move || run_text_builtins(&dir)).unwrap();
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
/// Runs the tests under `test/built-ins/` for `String`, `Number`, `RegExp` and `Date`: those that
|
||||
/// name an ES5 section and the other tests without a feature tag, counted apart.
|
||||
fn run_text_builtins(dir: &Path) {
|
||||
let mut report = String::new();
|
||||
let mut panicked = 0;
|
||||
for name in ["String", "Number", "RegExp", "Date"] {
|
||||
let root = dir.join("test").join("built-ins").join(name);
|
||||
assert!(root.is_dir(), "{} is not a test262 checkout", dir.display());
|
||||
let missing =
|
||||
if name == "String" { missing_builtins(&[TEXT_BUILTINS, STRING_METHODS_SHARED_WITH_ARRAY]) } else { missing_builtins(&[TEXT_BUILTINS]) };
|
||||
let (es5, later) = run_tests(dir, &root, &missing, true);
|
||||
println!("test262 built-ins/{name}, ES5 tests: {}. Other tests without a feature tag: {}.", es5.summary(), later.summary());
|
||||
report.push_str(&format!("# built-ins/{name}, ES5 tests\n{}\n# built-ins/{name}, other tests\n{}\n", es5.failures, later.failures));
|
||||
panicked += es5.panicked + later.panicked;
|
||||
}
|
||||
let path = dir.join("builtins-failures.txt");
|
||||
if let Err(e) = std::fs::write(&path, &report) {
|
||||
println!("could not write {}: {e}", path.display());
|
||||
}
|
||||
println!("test262 built-ins failures: {}", path.display());
|
||||
assert_eq!(panicked, 0, "the interpreter panicked; see {}", path.display());
|
||||
}
|
||||
|
||||
/// Runs the tests under `root` that have no feature tag and are not module, asynchronous or
|
||||
/// negative parse tests, each in a new realm after the harness. Returns the counts for the tests
|
||||
/// that name an ES5 section and, when `later` is set, for the others (otherwise those are left out).
|
||||
fn run_tests(dir: &Path, root: &Path, missing: &HashSet<&str>, later: bool) -> (RunCounts, RunCounts) {
|
||||
let harness_dir = dir.join("harness");
|
||||
// A file that cannot be read gives an empty source.
|
||||
let read = |p: &Path| String::from_utf8_lossy(&std::fs::read(p).unwrap_or_default()).into_owned();
|
||||
// `assert.js` formats a failed assertion's message with the global `String`, which the
|
||||
// interpreter does not provide yet; the stand-in converts with `+` so a failure reports the
|
||||
// assertion's message instead of a ReferenceError.
|
||||
let prelude = format!("{HARNESS_STRING}\n{}\n{}\n", read(&harness_dir.join("assert.js")), read(&harness_dir.join("sta.js")));
|
||||
let missing: HashSet<&str> = MISSING_BUILTINS.split_whitespace().collect();
|
||||
let prelude = format!("{}\n{}\n", read(&harness_dir.join("assert.js")), read(&harness_dir.join("sta.js")));
|
||||
let verbose = std::env::var_os("VECTORCRAFT_TEST262_VERBOSE").is_some();
|
||||
let (mut tests, mut skipped, mut passed, mut failed, mut panicked) = (0usize, 0usize, 0usize, 0usize, 0usize);
|
||||
let mut failures = String::new();
|
||||
for path in test_files(&language) {
|
||||
let (mut es5_counts, mut later_counts) = (RunCounts::default(), RunCounts::default());
|
||||
for path in test_files(root) {
|
||||
let source = read(&path);
|
||||
let meta = meta(&source);
|
||||
let negative_parse = matches!(meta.negative_phase.as_deref(), Some("parse" | "resolution"));
|
||||
if !meta.es5 || !meta.features.is_empty() || negative_parse || meta.flags.iter().any(|f| f == "module") {
|
||||
if (!meta.es5 && !later) || !meta.features.is_empty() || negative_parse || meta.flags.iter().any(|f| f == "module") {
|
||||
continue;
|
||||
}
|
||||
let counts = if meta.es5 { &mut es5_counts } else { &mut later_counts };
|
||||
let includes: String = meta.includes.iter().map(|i| read(&harness_dir.join(i)) + "\n").collect();
|
||||
if meta.flags.iter().any(|f| f == "async") || needs_missing_builtin(&source, &missing) || needs_missing_builtin(&includes, &missing) {
|
||||
skipped += 1;
|
||||
if meta.flags.iter().any(|f| f == "async") || needs_missing_builtin(&source, missing) || needs_missing_builtin(&includes, missing) {
|
||||
counts.skipped += 1;
|
||||
continue;
|
||||
}
|
||||
tests += 1;
|
||||
counts.tests += 1;
|
||||
let has = |flag: &str| meta.flags.iter().any(|f| f == flag);
|
||||
let modes: &[bool] = if has("raw") || has("noStrict") {
|
||||
&[false]
|
||||
@@ -294,27 +383,18 @@ fn run_language_subset(dir: &Path) {
|
||||
let negative_type = meta.negative_type.clone().filter(|_| meta.negative_phase.as_deref() == Some("runtime"));
|
||||
let outcome = std::panic::catch_unwind(|| run_one(&script, negative_type.as_deref()));
|
||||
match outcome {
|
||||
Ok(Ok(())) => passed += 1,
|
||||
Ok(Ok(())) => counts.passed += 1,
|
||||
Ok(Err(why)) => {
|
||||
failed += 1;
|
||||
failures.push_str(&format!("{rel} ({mode}): {why}\n"));
|
||||
counts.failed += 1;
|
||||
counts.failures.push_str(&format!("{rel} ({mode}): {why}\n"));
|
||||
}
|
||||
Err(_) => {
|
||||
failed += 1;
|
||||
panicked += 1;
|
||||
failures.push_str(&format!("{rel} ({mode}): PANIC\n"));
|
||||
counts.failed += 1;
|
||||
counts.panicked += 1;
|
||||
counts.failures.push_str(&format!("{rel} ({mode}): PANIC\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let report = dir.join("run-failures.txt");
|
||||
if let Err(e) = std::fs::write(&report, &failures) {
|
||||
println!("could not write {}: {e}", report.display());
|
||||
}
|
||||
println!(
|
||||
"test262 ES5 language subset through the interpreter: {tests} tests that need only the built-ins present, {passed} runs passed, \
|
||||
{failed} failed ({panicked} panicked). Skipped {skipped} tests that need other built-ins, use syntax after ES5 or are asynchronous. Failures: {}",
|
||||
report.display()
|
||||
);
|
||||
assert_eq!(panicked, 0, "the interpreter panicked; see {}", report.display());
|
||||
(es5_counts, later_counts)
|
||||
}
|
||||
|
||||
@@ -655,6 +655,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- File › Scripts › Check Script… reads a script written for Illustrator (`.jsx`, `.jsxinc` or a compiled `.jsxbin`) without running any of it and reports what it needs (the document model, files, dialogs, menu commands, features only Illustrator has), what it would do outside the document, the menu commands and document-model members it uses, its directives and included files, and the problems that would stop it loading (`script.check`, which also reads `.js` files); File › Open and drag-and-drop show the report for `.jsx`, `.jsxinc` and `.jsxbin` files.
|
||||
- `vectorcraft-js` parses JavaScript as ECMAScript 5.1 defines it, with the legacy octal forms, block functions and lenient regular expressions of Annex B, into a syntax tree with source spans; it reads the `#target`, `#include` and other preprocessor lines as directives without running them, and a script that breaks the grammar or an early-error rule gives a `SyntaxError` with its line and column. Nesting past the parser's depth limit (`Options::max_depth`, 200 by default) gives a `SyntaxError` instead of exhausting the stack. `cargo xtask test262` fetches the conformance suite at a pinned commit and parses its ES5 tests: 5,411 of 5,418 runs pass (the 7 others use `let` and `const`). Nothing runs scripts yet. Tests: `crates/js/src/tests.rs`, `crates/engine/tests/it/js_lexer_vs_scriptcheck.rs`.
|
||||
- `vectorcraft-js`'s `Realm` runs ECMAScript 5.1 scripts: the parsed script compiles to verified bytecode that a non-recursive interpreter runs, with closures, `this`, `arguments`, `with`, direct and indirect `eval`, `new Function`, `try`/`catch`/`finally`, labeled `break` and `continue`, and `for`-`in` in insertion order, over a mark-and-sweep heap. Built-ins are limited to `Object`, `Function` with `call` and `apply`, `eval` and the error types. Every run is bounded by fuel, heap bytes, frames, nested host calls and an interrupt flag (smaller defaults in the wasm build), and a run that reaches a limit stops with a limit error that `try` cannot catch. `cargo xtask test262` also runs the suite's ES5 language tests that need only these built-ins: 3,074 of 3,078 runs pass (the 4 others expect the completion values of later editions). The app does not call it yet. Tests: `crates/js/src/interp_tests.rs`, `crates/js/tests/test262.rs`.
|
||||
- `vectorcraft-js` has `String`, `Number`, `RegExp` and `Date` as ECMAScript 5.1 defines them (sections 15.5, 15.7, 15.9 and 15.10, https://262.ecma-international.org/5.1/), with Annex B's `substr`, `getYear`, `setYear` and `toGMTString`. `toFixed`, `toExponential`, `toPrecision` and `toString(radix)` format a number from its exact decimal value. Regular expressions (flags `g`, `i` and `m`) run on a backtracking matcher that keeps its choice points on the heap and takes fuel for every step, so a catastrophic pattern stops at the fuel limit and 50,000 nested groups give a `SyntaxError` at the default depth limit. A string method that would build a string past the heap limit stops before it allocates. `Date` reads the clock and time zone the host sets (`Realm::set_clock`, `Realm::set_time_zone`; local time is UTC by default). `cargo xtask test262` runs the suite's `built-ins` tests for the four (https://github.com/tc39/test262): of the runs of the ES5 tests that need only the built-ins present, 1,113 of 1,169 pass for `String`, 262 of 262 for `Number`, 878 of 908 for `RegExp` and 6 of 6 for `Date`. The others need `Array` and `Boolean` methods, or expect later editions' rules (built-in functions whose `length` can be deleted, flags given with a RegExp object, a negative `lastIndex`, `RegExp.prototype` as a plain object). The ES5 language tests that can now run give 3,742 of 3,746 passing runs. Tests: `crates/js/src/text_tests.rs`, `crates/js/tests/test262.rs`.
|
||||
- **UI:** Illustrator 2026 layout restyled to measured values:
|
||||
- Medium Dark theme, categorized and Advanced toolbars (a tool group's flyout opens on a right press, a long press or a press on its corner triangle), 35 pt document tabs, 33 pt panel tabs.
|
||||
- Hint bar, contextual task bar, 19 dock panels with ≡ menus.
|
||||
|
||||
+2
-1
@@ -44,7 +44,8 @@ commands:
|
||||
stats [--exact] count tests and lines per crate (--exact: ask the test harness via `-- --list`)
|
||||
test262 fetch the pinned ECMAScript conformance suite into corpus/test262 (sha256
|
||||
verified), parse its ES5 subset with vectorcraft-js and run the ES5 language
|
||||
tests its interpreter supports, printing the counts
|
||||
tests its interpreter supports and the String, Number, RegExp and Date
|
||||
built-ins tests, printing the counts
|
||||
web-fonts copy the craft-fonts faces in crates/text/web-fonts.txt into the web site (the Trunk
|
||||
post_build hook; a no-op without CRAFT_FONTS_DIR)
|
||||
";
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
//! BSD-3-Clause) at a pinned commit, fetched as GitHub's archive of that commit, checked against
|
||||
//! its SHA-256 and size, and unpacked into `corpus/test262/` (gitignored, never committed). It then
|
||||
//! runs `vectorcraft-js`'s `test262` tests, which parse the ES5 parse subset, run the ES5
|
||||
//! `test/language` tests that need only the interpreter's built-ins, and print each one's counts.
|
||||
//! `test/language` tests that need only the interpreter's built-ins, run the `test/built-ins` tests
|
||||
//! for `String`, `Number`, `RegExp` and `Date`, and print each one's counts.
|
||||
//!
|
||||
//! Moving the pin: change [`COMMIT`], run the command once with the new commit to see the
|
||||
//! archive's SHA-256 in the error, check the archive, and update [`SHA256`].
|
||||
|
||||
Reference in New Issue
Block a user