Merge pull request #6194 from dotCooCoo/feat/js-builtins-text

JavaScript: a realm has the String, Number, RegExp and Date built-ins
This commit is contained in:
BFlat
2026-11-09 07:48:04 +08:00
committed by GitHub
19 changed files with 4570 additions and 76 deletions
Generated
+1
View File
@@ -6384,6 +6384,7 @@ dependencies = [
name = "vectorcraft-js"
version = "0.21.0"
dependencies = [
"chrono",
"proptest",
"unicode-general-category",
]
+1
View File
@@ -10,6 +10,7 @@ rust-version.workspace = true
workspace = true
[dependencies]
chrono = { workspace = true }
unicode-general-category = "1"
[dev-dependencies]
+15 -9
View File
@@ -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,31 +17,32 @@ 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;
data(interp, global, name, Value::Object(ctor), HIDDEN)
}
/// Creates the built-ins and the global object's properties.
/// Creates the built-ins and the global object's properties. The caller installs them before it
/// sets the realm's limits, so no limit stops the install.
pub(crate) fn install(interp: &mut Interp) {
if let Err(e) = try_install(interp) {
// Installing touches only objects it has just made, so this does not happen; a realm
@@ -65,7 +67,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 +100,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<()> {
@@ -131,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)
}
+845
View File
@@ -0,0 +1,845 @@
//! `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, HeldBytes, Hint, Interp, JsResult};
use crate::string_builtins::{charge_units, flat_charged, 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 starts = |w: &str, prefix: &str| w.get(..prefix.len()).is_some_and(|head| head.eq_ignore_ascii_case(prefix));
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;
// The words are read one at a time, and none is copied.
let words = s.split(|c: char| c.is_whitespace() || c == ',').take_while(|w| !w.starts_with('(')).filter(|w| !w.is_empty());
for w in words {
if DAYS.iter().any(|d| starts(w, d)) && month.is_none() && date.is_none() && w.len() >= 3 && !w.contains(':') {
continue;
}
if let Some(mi) = MONTHS.iter().position(|mo| starts(w, mo)) {
month = Some(mi as f64);
continue;
}
let zone = if starts(w, "gmt") || starts(w, "utc") { w.get(3..) } else { w.eq_ignore_ascii_case("z").then_some("") };
if let Some(rest) = zone {
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,
};
// Five units are enough to tell whether four digits remain without the colons.
let digits: String = digits.chars().filter(|&c| c != ':').take(5).collect();
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(':') {
if w.split(':').count() > 3 {
return None;
}
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. The text the parsers read is held against
/// the heap limit while they read it.
fn parse_date(interp: &mut Interp, s: &Str) -> JsResult<f64> {
let units = flat_charged(interp, s)?;
charge_units(interp, units.len())?;
let mut held = HeldBytes::new(interp);
// Each code unit becomes at most three UTF-8 bytes.
held.add(interp, units.len().saturating_mul(3))?;
let text = String::from_utf16_lossy(&units);
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
View File
@@ -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>();
+84 -14
View File
@@ -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,56 @@ 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 bytes native code holds outside the heap's cells through [`HeldBytes`].
pub(crate) held: Rc<HeldCount>,
}
/// The bytes all live [`HeldBytes`] hold, and the bytes dropped holders released that the heap's
/// total still counts.
#[derive(Default)]
pub(crate) struct HeldCount {
now: std::cell::Cell<usize>,
released: std::cell::Cell<usize>,
}
/// Bytes that native code holds in Rust memory while it works (a matcher's state and stack, a
/// replacement being built, a pattern being compiled), counted against the heap limit. They are
/// in the heap's total, and in the live total of each collection, until the holder is dropped;
/// the heap's total gives them back before the next limit check.
pub(crate) struct HeldBytes {
count: Rc<HeldCount>,
bytes: usize,
}
impl HeldBytes {
pub(crate) fn new(interp: &Interp) -> Self {
Self { count: interp.held.clone(), bytes: 0 }
}
/// The bytes this holder holds.
#[cfg(test)]
pub(crate) fn bytes(&self) -> usize {
self.bytes
}
/// Holds `n` more bytes, ending the run with the heap limit when they would not fit after a
/// collection. Everything the caller still needs must be reachable from the roots.
pub(crate) fn add(&mut self, interp: &mut Interp, n: usize) -> JsResult<()> {
interp.reserve_heap(n)?;
interp.heap.bytes = interp.heap.bytes.saturating_add(n);
self.bytes = self.bytes.saturating_add(n);
self.count.now.set(self.count.now.get().saturating_add(n));
Ok(())
}
}
impl Drop for HeldBytes {
fn drop(&mut self) {
self.count.now.set(self.count.now.get().saturating_sub(self.bytes));
self.count.released.set(self.count.released.get().saturating_add(self.bytes));
}
}
/// The least heap size a collection waits for.
@@ -277,8 +330,10 @@ impl Interp {
global,
global_env,
intrinsics: Intrinsics::placeholder(),
fuel: limits.fuel,
limits,
fuel: u64::MAX,
// The built-ins are installed without limits; the realm's limits apply from the
// first run.
limits: Limits { fuel: u64::MAX, heap_bytes: usize::MAX, frames: usize::MAX, reentry: usize::MAX },
options,
interrupt: Arc::new(AtomicBool::new(false)),
reentry: 0,
@@ -286,8 +341,12 @@ impl Interp {
source_units: 0,
throw_site: None,
last_thrown: Value::Undefined,
date_host: crate::date_builtins::DateHost::default(),
held: Rc::default(),
};
crate::builtins::install(&mut interp);
interp.fuel = limits.fuel;
interp.limits = limits;
interp.gc_threshold = interp.next_threshold();
interp
}
@@ -388,6 +447,7 @@ impl Interp {
fn tick(&mut self) -> JsResult<()> {
self.use_fuel()?;
self.settle_held();
if self.heap.bytes > self.gc_threshold {
self.collect_garbage();
if self.heap.bytes > self.limits.heap_bytes {
@@ -401,6 +461,7 @@ impl Interp {
/// Ends the run with the heap limit when `bytes` more would not fit in it after a
/// collection. Everything the caller still needs must be reachable from the roots.
pub(crate) fn reserve_heap(&mut self, bytes: usize) -> JsResult<()> {
self.settle_held();
if self.heap.bytes.saturating_add(bytes) <= self.limits.heap_bytes {
return Ok(());
}
@@ -412,6 +473,19 @@ impl Interp {
Ok(())
}
/// Takes the bytes that dropped [`HeldBytes`] released off the heap's total.
fn settle_held(&mut self) {
let released = self.held.released.replace(0);
self.heap.bytes = self.heap.bytes.saturating_sub(released);
}
/// Collects garbage now and sets when the next collection runs.
#[cfg(test)]
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.
@@ -439,6 +513,9 @@ impl Interp {
roots.objects.extend(self.intrinsics.all());
roots.envs.push(self.global_env);
self.heap.collect(&roots);
// The collection counts the live total again, so bytes already released are not in it.
self.held.released.set(0);
self.heap.bytes = self.heap.bytes.saturating_add(self.held.now.get());
}
/// Runs frames until the boundary frame on top returns, and returns its value.
@@ -1334,16 +1411,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))
}
@@ -1938,7 +2006,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) {
@@ -2012,7 +2082,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
View File
@@ -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
View File
@@ -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() {
+127
View File
@@ -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))
}
+15
View File
@@ -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();
+2 -9
View File
@@ -176,15 +176,8 @@ fn class_atom(pattern: &[u16], i: usize) -> Result<(Option<u32>, usize), String>
_ => (Some(u32::from(b'\\')), i + 1),
},
'0'..='7' => {
let mut value = 0;
let mut j = i + 1;
while j < i + 4
&& let Some(d) = pattern.get(j).and_then(|&u| char::from_u32(u32::from(u))).and_then(|ch| ch.to_digit(8))
{
value = value * 8 + d;
j += 1;
}
(Some(value), j)
let (value, end) = crate::regexp_vm::octal(pattern, i + 1);
(Some(u32::from(value)), end)
}
_ => (Some(next), i + 2),
})
+275
View File
@@ -0,0 +1,275 @@
//! `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::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::{ErrorType, HeldBytes, Interp, JsResult};
use crate::regexp_vm::{CompileFail, Cost, Flags, Matcher, Regex, compile, parse_flags, use_fuel_n};
use crate::string_builtins::{flat_charged, 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, &[], &[], 0)?;
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 check takes one unit of fuel for each unit of the pattern. What compiling holds is held against the heap limit as it grows. The compiled form's
/// bytes are then reserved together with `extra` more and added to the heap, so a caller that
/// stores the result in an object it allocates next reserves that object here.
fn compile_regex(interp: &mut Interp, pattern: &[u16], flags: &[u16], extra: usize) -> 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;
use_fuel_n(interp, pattern.len() as u64)?;
if let Err(m) = crate::regexp::check(pattern, depth) {
return Err(interp.throw(ErrorType::Syntax, &format!("invalid regular expression: {m}")));
}
let mut held = HeldBytes::new(interp);
let compiled = compile(pattern, flags, depth, &mut |cost| match cost {
Cost::Fuel(n) => use_fuel_n(interp, n),
Cost::Heap(n) => held.add(interp, n),
});
drop(held);
let re = match compiled {
Ok(re) => re,
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.saturating_add(extra))?;
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, 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 = flat_charged(interp, source)?;
let flags = flat_charged(interp, flags)?;
make(interp, source.clone(), &pattern, &flags)
}
/// 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 => {
// A backslash before a line terminator escapes the terminator as itself (section
// 15.10.2.10); the terminator's escape written below matches the same unit.
if pattern.get(i + 1).is_some_and(|&n| crate::chars::is_line_terminator(u32::from(n))) {
i += 1;
continue;
}
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 = flat_charged(interp, &source)?;
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 = flat_charged(interp, &p)?;
let flags = flat_charged(interp, &f)?;
let source = source_text(interp, &units)?;
make(interp, source, &units, &flags)
}
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 = flat_charged(interp, s)?;
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 = flat_charged(interp, s)?;
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
+788
View File
@@ -0,0 +1,788 @@
//! `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, whose flat copy counts as a new
//! string), 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, HeldBytes, 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<()> {
reserve_string(interp, len)?;
charge_units(interp, len)
}
/// [`reserve_units`] without the fuel, for a caller that took it as it read the units.
fn reserve_string(interp: &mut Interp, len: usize) -> JsResult<()> {
check_string_room(interp, len)?;
let bytes = string_bytes(len);
interp.heap.bytes = interp.heap.bytes.saturating_add(bytes);
Ok(())
}
/// Ends the run with the heap limit when a new string of `len` code units would not fit, after
/// a collection if one is needed. Everything the caller still needs must be reachable from the
/// roots.
fn check_string_room(interp: &mut Interp, len: usize) -> JsResult<()> {
if len > interp.limits.heap_bytes / 4 {
return Err(Abrupt::Limit(LimitKind::Heap));
}
interp.reserve_heap(string_bytes(len))
}
/// Pushes `item` onto `list`, first holding the bytes of the list's new capacity when it must
/// grow.
fn push_held<T>(interp: &mut Interp, held: &mut HeldBytes, list: &mut Vec<T>, item: T) -> JsResult<()> {
if list.len() == list.capacity() {
let more = list.capacity().max(4);
held.add(interp, more.saturating_mul(size_of::<T>()))?;
list.reserve_exact(more);
}
list.push(item);
Ok(())
}
/// The code units of `s`. Flattening a rope makes a copy of its units, so for a rope that copy's
/// bytes are reserved and added to the heap, and its units charged as fuel, first.
pub(crate) fn flat_charged(interp: &mut Interp, s: &Str) -> JsResult<Rc<[u16]>> {
if s.is_rope() {
reserve_units(interp, s.len())?;
}
Ok(s.flat())
}
/// 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 = flat_charged(interp, &s)?.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(flat_charged(interp, &s)?.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) = (flat_charged(interp, &s)?, flat_charged(interp, &search)?);
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) = (flat_charged(interp, &s)?, flat_charged(interp, &search)?);
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) = (flat_charged(interp, &s)?, flat_charged(interp, &that)?);
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(""));
}
let units = flat_charged(interp, &s)?;
substring_value(interp, &units, 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));
let units = flat_charged(interp, &s)?;
substring_value(interp, &units, 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(""));
}
let units = flat_charged(interp, &s)?;
substring_value(interp, &units, 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 units = flat_charged(interp, &s)?;
let out = map_case(interp, &units, 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 units = flat_charged(interp, &s)?;
let out = map_case(interp, &units, 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 = flat_charged(interp, &s)?;
// 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 = flat_charged(interp, &s)?;
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 = flat_charged(interp, &s)?;
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, a range of the replacement pattern, or the
/// units of a string a replacement function returned.
enum Piece {
Range(usize, usize),
Template(usize, usize),
Units(Rc<[u16]>),
}
impl Piece {
fn len(&self) -> usize {
match self {
Piece::Range(a, b) | Piece::Template(a, b) => b.saturating_sub(*a),
Piece::Units(u) => u.len(),
}
}
}
/// The pieces of a result string and the length they add up to.
#[derive(Default)]
struct Pieces {
list: Vec<Piece>,
len: usize,
}
impl Pieces {
/// Adds `piece`, holding its bytes and taking fuel for its units, and ends the run with the
/// heap limit once the result would not fit.
fn add(&mut self, interp: &mut Interp, held: &mut HeldBytes, piece: Piece) -> JsResult<()> {
let n = piece.len();
if n == 0 {
return Ok(());
}
push_held(interp, held, &mut self.list, piece)?;
self.len = self.len.saturating_add(n);
charge_units(interp, n)?;
check_string_room(interp, self.len)
}
/// Joins the pieces into a new string, reserving its length first. `units` is the input and
/// `template` the replacement pattern.
fn build(&self, interp: &mut Interp, units: &[u16], template: &[u16]) -> JsResult<Value> {
reserve_string(interp, self.len)?;
let mut out = Vec::with_capacity(self.len);
for p in &self.list {
match p {
Piece::Range(a, b) => out.extend_from_slice(units.get(*a..*b).unwrap_or_default()),
Piece::Template(a, b) => out.extend_from_slice(template.get(*a..*b).unwrap_or_default()),
Piece::Units(u) => out.extend_from_slice(u),
}
}
Ok(Value::String(Str::from_units(out)))
}
}
/// A part of a replacement pattern (section 15.5.4.11, Table 22).
#[derive(Clone, Copy)]
enum Part {
/// The pattern's own text in a range.
Text(usize, usize),
/// `$&`
Matched,
/// `` $` ``
Before,
/// `$'`
After,
/// `$n` or `$nn`
Group(usize),
}
impl Part {
/// The piece this part gives for one match: `caps` holds the match and its groups as pairs of
/// positions in an input of `len` units.
fn piece(self, caps: &[usize], len: usize) -> Option<Piece> {
let (start, end) = (caps.first().copied().unwrap_or(0), caps.get(1).copied().unwrap_or(0));
match self {
Part::Text(a, b) => Some(Piece::Template(a, b)),
Part::Matched => Some(Piece::Range(start, end)),
Part::Before => Some(Piece::Range(0, start)),
Part::After => Some(Piece::Range(end, len)),
Part::Group(n) => {
let (s, e) = (caps.get(2 * n).copied().unwrap_or(NONE), caps.get(2 * n + 1).copied().unwrap_or(NONE));
(s != NONE && e != NONE).then_some(Piece::Range(s, e))
}
}
}
}
/// Reads a replacement pattern once into its parts, for a regular expression with `groups`
/// groups. The parts are held against the heap limit, and the units read take fuel.
fn parse_template(interp: &mut Interp, held: &mut HeldBytes, template: &[u16], groups: usize) -> JsResult<Vec<Part>> {
charge_units(interp, template.len())?;
let digit = |i: usize| template.get(i).filter(|&&u| (0x30..=0x39).contains(&u)).map(|&u| usize::from(u - 0x30));
let mut parts = Vec::new();
let mut lit = 0;
let mut i = 0;
while let Some(&u) = template.get(i) {
if u != 0x24 {
i += 1;
continue;
}
let (part, taken) = match template.get(i + 1) {
// `$$` gives one `$`: the text up to and including the first.
Some(0x24) => {
push_held(interp, held, &mut parts, Part::Text(lit, i + 1))?;
lit = i + 2;
i += 2;
continue;
}
Some(0x26) => (Part::Matched, 2),
Some(0x60) => (Part::Before, 2),
Some(0x27) => (Part::After, 2),
_ => match (digit(i + 1), digit(i + 2)) {
(Some(a), Some(b)) if (1..=groups).contains(&(a * 10 + b)) => (Part::Group(a * 10 + b), 3),
(Some(a), _) if (1..=groups).contains(&a) => (Part::Group(a), 2),
_ => {
i += 1;
continue;
}
},
};
if i > lit {
push_held(interp, held, &mut parts, Part::Text(lit, i))?;
}
push_held(interp, held, &mut parts, part)?;
i += taken;
lit = i;
}
if template.len() > lit {
push_held(interp, held, &mut parts, Part::Text(lit, template.len()))?;
}
Ok(parts)
}
/// `replace` (section 15.5.4.11). The matches, the parsed replacement pattern and the pieces of
/// the result are held against the heap limit while the result is put together, and the
/// result's length is checked against the heap as each piece is added.
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 = flat_charged(interp, &s)?;
let mut held = HeldBytes::new(interp);
// 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;
while let Some(caps) = exec_raw(interp, rx, &s)? {
held.add(interp, size_of::<usize>().saturating_mul(caps.capacity()))?;
push_held(interp, &mut held, &mut matches, caps)?;
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;
}
}
} else if let Some(caps) = exec_raw(interp, rx, &s)? {
matches.push(caps);
}
} else {
let search = interp.to_str(&search_value)?;
let needle = flat_charged(interp, &search)?;
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 => {
let t = interp.to_str(&replace_value)?;
Some(flat_charged(interp, &t)?)
}
};
if matches.is_empty() {
return Ok(Value::String(s));
}
let template: &[u16] = template.as_deref().unwrap_or_default();
// Every match has the same number of groups.
let groups = matches.first().map_or(0, |caps| (caps.len() / 2).saturating_sub(1));
let parts = if replacer.is_none() { parse_template(interp, &mut held, template, groups)? } else { Vec::new() };
let mut pieces = Pieces::default();
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.add(interp, &mut held, Piece::Range(last, start))?;
match &replacer {
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)?;
let text = flat_charged(interp, &text)?;
pieces.add(interp, &mut held, Piece::Units(text))?;
}
None => {
// Reading the parts takes fuel even when they give empty pieces.
use_fuel_n(interp, (parts.len() / UNITS_PER_FUEL) as u64)?;
for part in &parts {
if let Some(p) = part.piece(caps, units.len()) {
pieces.add(interp, &mut held, p)?;
}
}
}
}
use_fuel_n(interp, 1)?;
last = end;
}
pieces.add(interp, &mut held, Piece::Range(last, units.len()))?;
pieces.build(interp, &units, template)
})
}
/// `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 => {
let sep = interp.to_str(&separator)?;
Some(flat_charged(interp, &sep)?)
}
};
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 = flat_charged(interp, &s)?;
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))
})
}
+633
View File
@@ -0,0 +1,633 @@
//! Tests of `String`, `Number`, `RegExp` and `Date` run through a realm.
use proptest::prelude::*;
use crate::regexp_vm::{INST_BYTES, ITEM_BYTES};
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"),
// An octal escape starting with 4 to 7 takes two digits, so `\477-z` is `'` and the range `7-z`.
("show(/[\\477-z]+/.exec(\"'8y\")) + ' ' + new RegExp('[\\\\477-z]').test('8') + ' ' + /[\\377]/.test('\\xff')", "'8y true true"),
];
for (src, want) in cases {
shows(src, want);
}
}
#[test]
fn a_realm_made_under_low_limits_has_every_built_in() {
let low = Limits { fuel: 0, heap_bytes: 1, ..Limits::default() };
let mut realm = Realm::with_options(Options::default(), low.clone());
assert_eq!(realm.limits(), &low);
realm.set_limits(Limits::default());
assert_eq!(
run_in(&mut realm, "typeof String + typeof Number + typeof RegExp + typeof Date + typeof /a/.exec + typeof new Date(0).getTime"),
JsValue::String("function".repeat(6))
);
}
#[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.length";
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 replace_holds_its_pieces_and_checks_the_result_as_it_grows() {
// Each `$&` gives a piece for every match: the pieces count against the heap, so the run ends
// with the heap limit long before the 4,194,304 pieces would take the fuel.
let limits = Limits { heap_bytes: 4 << 20, fuel: 500_000, ..Limits::default() };
let src = "var s = 'a'; for (var i = 0; i < 12; i++) s += s; var t = '$&'; for (var i = 0; i < 10; i++) t += t; s.replace(/a/g, t).length";
let e = fail_with(Realm::with_options(Options::default(), limits), src);
assert_eq!(e.kind, RunErrorKind::Limit(LimitKind::Heap), "{e:?}");
// A long literal pattern is not copied for each match, and the result's length is checked as
// the pieces are added, so the run ends with the heap limit before the fuel for 16,384
// copies of the pattern is spent.
let limits = Limits { heap_bytes: 4 << 20, fuel: 1_000_000, ..Limits::default() };
let src = "var s = 'a'; for (var i = 0; i < 14; i++) s += s; var t = 'x'; for (var i = 0; i < 11; i++) t += t; s.replace(/a/g, t).length";
let e = fail_with(Realm::with_options(Options::default(), limits), src);
assert_eq!(e.kind, RunErrorKind::Limit(LimitKind::Heap), "{e:?}");
// The parts of the pattern give the same result as before.
shows("'abcabc'.replace(/(b)(c)/g, '[$$|$&|$`|$\\'|$2$1|$3|$0|$10]')", "a[$|bc|a|abc|cb|$3|$0|b0]a[$|bc|abca||cb|$3|$0|b0]");
}
#[test]
fn flattening_a_rope_counts_against_the_heap() {
// Three ropes of about 524,288 units, each 1 MiB once flat, with a 3 MiB heap.
let build = "var s = 'x'; for (var i = 0; i < 19; i++) s = s + s; var t = s + 'y', u = s + 'z';";
let limits = Limits { heap_bytes: 3 << 20, ..Limits::default() };
let built = Realm::with_options(Options::default(), limits.clone()).run(&format!("{build} s.length + t.length + u.length"));
assert_eq!(built, Ok(JsValue::Number(1_572_866.0)));
for read in [
"charAt(0)",
"charCodeAt(0)",
"indexOf('q')",
"lastIndexOf('q')",
"localeCompare('a')",
"slice(0, 1)",
"substring(0, 1)",
"substr(0, 1)",
"trim()",
"match(/q/)",
"search(/q/)",
"replace(/q/, '')",
"replace('q', '')",
] {
let src = format!("{build} s.{read}; t.{read}; u.{read}; 0");
let e = fail_with(Realm::with_options(Options::default(), limits.clone()), &src);
assert_eq!(e.kind, RunErrorKind::Limit(LimitKind::Heap), "{read}: {e:?}");
}
for call in ["/q/.exec(X)", "/q/.test(X)", "Date.parse(X)", "new RegExp(X)"] {
let src = format!("{build} {}; {}; {}; 0", call.replace('X', "s"), call.replace('X', "t"), call.replace('X', "u"));
let e = fail_with(Realm::with_options(Options::default(), limits.clone()), &src);
assert_eq!(e.kind, RunErrorKind::Limit(LimitKind::Heap), "{call}: {e:?}");
}
}
#[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 = "[\\s\\S]a*".repeat(1000);
assert!(stopped_by(&compile_within(&pattern, 400 * (ITEM_BYTES + INST_BYTES), u64::MAX).0, LimitKind::Heap));
assert!(compile_within(&pattern, usize::MAX, u64::MAX).0.is_ok());
// A literal pattern takes what its instructions hold, not the most a unit can take.
assert!(compile_within(&"a".repeat(10_000), 20_000 * (ITEM_BYTES + INST_BYTES), u64::MAX).0.is_ok());
// A class's ranges count while they are read, even when they collapse into one range.
assert!(stopped_by(&compile_within(&format!("[{}]", "a".repeat(10_000)), 1000, u64::MAX).0, LimitKind::Heap));
// So do the ranges of class escapes, and each atom and range read takes fuel.
let escapes = format!("[{}]", "\\w".repeat(10_000));
assert!(stopped_by(&compile_within(&escapes, 64 << 10, u64::MAX).0, LimitKind::Heap));
assert!(stopped_by(&compile_within(&escapes, usize::MAX, 20_000).0, LimitKind::Fuel));
// The lists count with the room they reserve as they double: 4097 literal units hold room for
// 8192 items, and the flattened code after the last unit adds 4098 instructions.
let literal = "a".repeat(4097);
let (compiled, bytes, _) = compile_within(&literal, usize::MAX, u64::MAX);
assert!(compiled.is_ok());
assert!(bytes >= 8192 * ITEM_BYTES + 4098 * INST_BYTES, "{bytes}");
assert!(stopped_by(&compile_within(&literal, 8192 * ITEM_BYTES + 4000 * INST_BYTES, u64::MAX).0, LimitKind::Heap));
}
#[test]
fn compiling_a_pattern_takes_fuel_for_each_scan_of_it() {
// The scan for capturing groups takes a unit of fuel for each unit of the pattern, and so does
// the compile itself.
let literal = "a".repeat(100_000);
let (compiled, _, fuel) = compile_within(&literal, usize::MAX, u64::MAX);
assert!(compiled.is_ok());
assert!(fuel >= 200_000, "{fuel}");
assert!(stopped_by(&compile_within(&literal, usize::MAX, 150_000).0, LimitKind::Fuel));
// The syntax check before compiling takes a unit for each unit too.
let mut realm = Realm::new();
let interp = realm.interp();
let (source, flags) = (crate::value::Str::from(literal.as_str()), crate::value::Str::from(""));
interp.fuel = 1 << 40;
assert!(crate::regexp_builtins::new_regexp(interp, &source, &flags).is_ok());
let used = (1u64 << 40) - interp.fuel;
assert!(used >= 300_000, "{used}");
interp.fuel = 250_000;
assert!(matches!(crate::regexp_builtins::new_regexp(interp, &source, &flags), Err(crate::interp::Abrupt::Limit(LimitKind::Fuel))));
}
#[test]
fn held_bytes_leave_the_heap_total_when_their_holder_is_dropped() {
let mut realm = Realm::new();
let interp = realm.interp();
interp.collect_for_room();
let live = interp.heap.bytes;
interp.limits.heap_bytes = live + 10_000;
let mut held = crate::interp::HeldBytes::new(interp);
assert!(held.add(interp, 6000).is_ok());
assert_eq!(interp.heap.bytes, live + 6000);
assert!(matches!(crate::interp::HeldBytes::new(interp).add(interp, 6000), Err(crate::interp::Abrupt::Limit(LimitKind::Heap))));
drop(held);
// The next check gives the bytes back without a collection.
assert!(interp.reserve_heap(0).is_ok());
assert_eq!(interp.heap.bytes, live);
let mut again = crate::interp::HeldBytes::new(interp);
assert!(again.add(interp, 6000).is_ok());
assert_eq!(interp.heap.bytes, live + 6000);
}
/// Compiles `pattern` with no flags through a charge that allows `heap` bytes and `fuel` units,
/// and returns the result with the bytes and fuel the compiler asked for.
fn compile_within(pattern: &str, heap: usize, fuel: u64) -> (Result<crate::regexp_vm::Regex, crate::regexp_vm::CompileFail>, usize, u64) {
use crate::regexp_vm::Cost;
let units: Vec<u16> = pattern.encode_utf16().collect();
let flags = crate::regexp_vm::parse_flags(&[]).unwrap();
let (mut bytes, mut used) = (0usize, 0u64);
let result = crate::regexp_vm::compile(&units, flags, 200, &mut |cost| {
match cost {
Cost::Heap(n) => bytes = bytes.saturating_add(n),
Cost::Fuel(n) => used = used.saturating_add(n),
}
if bytes > heap {
return Err(crate::interp::Abrupt::Limit(LimitKind::Heap));
}
if used > fuel {
return Err(crate::interp::Abrupt::Limit(LimitKind::Fuel));
}
Ok(())
});
(result, bytes, used)
}
/// Whether compiling stopped with the limit `kind`.
fn stopped_by(result: &Result<crate::regexp_vm::Regex, crate::regexp_vm::CompileFail>, kind: LimitKind) -> bool {
matches!(result, Err(crate::regexp_vm::CompileFail::Abrupt(crate::interp::Abrupt::Limit(k))) if *k == kind)
}
#[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/");
// A backslash before a line terminator reads back as the terminator's escape.
let escaped = "var r = new RegExp(String.fromCharCode(92, 10, 124, 92, 0x2028)); var back = eval(String(r));";
assert_eq!(
string(&format!("{escaped} String(r) + ' ' + r.test('\\n') + back.test('\\n') + back.test('\\u2028') + back.test('x')")),
"/\\n|\\u2028/ truetruetruefalse"
);
}
#[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 Ok(re) = compile_within(&"(a)".repeat(1000), usize::MAX, u64::MAX).0 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 Ok(re) = compile_within("a*b", usize::MAX, u64::MAX).0 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))));
drop(m);
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)));
// What the matcher holds is on the heap while it lives, so other allocations cannot use the
// same room, and a collection after it is dropped takes it off again.
let held = m.held_bytes();
assert!(held > 0);
assert!(interp.heap.bytes >= live + held, "{} < {live} + {held}", interp.heap.bytes);
interp.collect_for_room();
assert!(interp.heap.bytes >= live + held, "{} < {live} + {held}", interp.heap.bytes);
drop(m);
interp.collect_for_room();
assert_eq!(interp.heap.bytes, live);
}
#[test]
fn a_backreference_takes_fuel_for_the_units_it_compares() {
let mut realm = Realm::with_options(Options::default(), Limits::default());
let interp = realm.interp();
let mut fuel_for = |pattern: &str, input: &str, want: bool| {
let input: Vec<u16> = input.encode_utf16().collect();
let Ok(re) = compile_within(pattern, usize::MAX, u64::MAX).0 else { panic!("the pattern compiles") };
let Ok(mut m) = crate::regexp_vm::Matcher::new(&re, interp) else { panic!("the state fits") };
interp.fuel = 1 << 40;
assert!(matches!(m.match_at(interp, &input, 0), Ok(hit) if hit == want), "{pattern}");
(1u64 << 40) - interp.fuel
};
// The same 20,000 iterations, each running a one-unit backreference instead of a character:
// each comparison takes a step besides the instruction's own.
let a = "a".repeat(20_001);
let (by_char, by_backref) = (fuel_for("(a)(?:a)*$", &a, true), fuel_for("(a)(?:\\1)*$", &a, true));
assert!(by_backref >= by_char + 20_000, "{by_backref} < {by_char} + 20000");
// A long comparison takes a step for each 16 units it compares.
let long = fuel_for("^(a{10000})\\1", &a, true);
let plain = fuel_for("^(a{10000})", &a, true);
assert!(long >= plain + 625, "{long} < {plain} + 625");
// One that fails on its first unit takes a step for those 16 units only.
let ab = format!("{}{}", "a".repeat(10_000), "b".repeat(10_000));
let (by_backref, by_char) = (fuel_for("^(a{10000})\\1", &ab, false), fuel_for("^(a{10000})c", &ab, false));
assert!(by_backref <= by_char + 2, "{by_backref} > {by_char} + 2");
}
#[test]
fn clearing_matcher_state_takes_fuel() {
let limits = Limits { fuel: 500_000, ..Limits::default() };
let groups = "var p = ''; for (var i = 0; i < 1000; i++) p += '(x)'; var s = ''; for (var i = 0; i < 2000; i++) s += 'a';";
// Each iteration of the `*` clears the 1000 groups inside it, though only `a` runs.
let src = format!("{groups} new RegExp('(?:a|' + p + ')*').test(s)");
let e = fail_with(Realm::with_options(Options::default(), limits.clone()), &src);
assert_eq!(e.kind, RunErrorKind::Limit(LimitKind::Fuel), "{e:?}");
// Each attempt at a new position clears the state of all 1001 groups.
let src = format!("{groups} new RegExp(p + 'z').test(s.replace(/a/g, 'b'))");
let e = fail_with(Realm::with_options(Options::default(), limits), &src);
assert_eq!(e.kind, RunErrorKind::Limit(LimitKind::Fuel), "{e:?}");
}
#[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'))",
"Date.parse('thu, 01 JAN 1970 00:00:00 gmt') === 0 && Date.parse('jan 2 1970 utc+01:00') === 82800000",
"isNaN_(Date.parse('Jan 1 1970 1:2:3:4 GMT')) && isNaN_(Date.parse('Jan 1 1970 GMT+01000')) && isNaN_(Date.parse('Jan 1 1970 x'))",
] {
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:?}");
}
+118 -32
View File
@@ -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 when the
//! interpreter panics, when a harness or test file cannot be read, or when no ES5 test runs.
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,110 @@ 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");
assert!(language.is_dir(), "{} is not a test262 checkout", dir.display());
let missing = missing_builtins(&[TEXT_BUILTINS]);
let (es5, _) = run_tests(dir, &language, &missing, false);
assert!(es5.tests > 0, "no ES5 tests ran under {}", language.display());
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);
assert!(es5.tests > 0, "no ES5 tests ran under {}", root.display());
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 read = |p: &Path| {
let bytes = std::fs::read(p).map_err(|e| format!("cannot read {}: {e}", p.display()));
assert!(bytes.is_ok(), "{}", bytes.as_ref().err().map_or("", String::as_str));
String::from_utf8_lossy(&bytes.unwrap_or_default()).into_owned()
};
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 +389,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)
}
+1
View File
@@ -688,6 +688,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`). This entry covers parsing; the next entry covers running scripts. 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` and `toPrecision` format a number from its exact decimal value, and `toString(radix)` writes any base from 2 to 36. Regular expressions (flags `g`, `i` and `m`) run on a backtracking matcher whose choice points and capture state count against the heap limit and which 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, and `RegExp.prototype`'s `global`, `ignoreCase` and `multiline` properties that can be deleted). 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
View File
@@ -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 -1
View File
@@ -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`].