Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 54 additions & 8 deletions crates/perry-runtime/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1050,7 +1050,17 @@ pub extern "C" fn js_date_apply_setter(
} else {
unsafe { std::slice::from_raw_parts(args_ptr, argc) }
};
// setTime: single value, replaces the whole time.
// Spec: every `Date.prototype.set*` reads `thisTimeValue` (the receiver's
// current `[[DateValue]]`) BEFORE coercing any argument via ToNumber. A
// user `valueOf` on an argument can re-enter and mutate this very cell
// (test262 `set*/date-value-read-before-tonumber-when-date-is-{valid,
// invalid}`); the timestamp captured here is the one the rebuild must use,
// not whatever the cell holds after the ToNumber side effects. The brand
// check (`this` must be a Date) happens earlier, in the reflective setter
// thunks (`object::date_proto_thunks`) and on the codegen instance path.
let captured = date_cell_timestamp(date);
// setTime: single value, replaces the whole time. The old value is unused
// (beyond the brand check above), so no read-before ordering applies.
if field == 7 {
let v = if argc == 0 {
f64::NAN
Expand All @@ -1059,6 +1069,36 @@ pub extern "C" fn js_date_apply_setter(
};
return date_cell_store(date, v);
}
// setYear (annexB): like setFullYear but rebases a truncated year in
// `0..=99` to `1900 + y`, operates in local time, and has no UTC variant.
if field == 8 {
let y_raw = if argc == 0 {
f64::NAN
} else {
jsvalue_to_number(args[0])
};
let yyyy = if y_raw.is_nan() {
f64::NAN
} else {
let yi = y_raw.trunc();
if (0.0..=99.0).contains(&yi) {
1900.0 + yi
} else {
y_raw
}
};
return rebuild_local_with(
date,
captured,
Some(yyyy),
None,
None,
None,
None,
None,
None,
);
}
// `req(0)` is the *leading*, required component: an omitted leading
// argument coerces to NaN (e.g. `setHours()` → Invalid Date). Trailing
// optional components use `opt(i)`: an omitted (or `undefined`) trailing
Expand Down Expand Up @@ -1092,9 +1132,9 @@ pub extern "C" fn js_date_apply_setter(
_ => return date_cell_store(date, f64::NAN),
};
if is_utc != 0 {
rebuild_with(date, year, month0, day, hour, minute, second, ms)
rebuild_with(date, captured, year, month0, day, hour, minute, second, ms)
} else {
rebuild_local_with(date, year, month0, day, hour, minute, second, ms)
rebuild_local_with(date, captured, year, month0, day, hour, minute, second, ms)
}
}

Expand Down Expand Up @@ -1291,6 +1331,7 @@ pub extern "C" fn js_date_get_timezone_offset(timestamp: f64) -> f64 {
#[allow(clippy::too_many_arguments)]
fn rebuild_with(
date: f64,
timestamp: f64,
year: Option<f64>,
month0: Option<f64>,
day: Option<f64>,
Expand All @@ -1299,12 +1340,15 @@ fn rebuild_with(
second: Option<f64>,
millisecond: Option<f64>,
) -> f64 {
let timestamp = date_cell_timestamp(date);
let (cy, cm0, cd, ch, cmi, cs, cur_ms) = if timestamp.is_nan() {
// Setting the year revives an Invalid Date (ECMA MakeDate seeds from
// year/0/1); any other component setter on an Invalid Date is a no-op.
// year/0/1); any other component setter on an Invalid Date returns NaN
// WITHOUT writing `[[DateValue]]` (spec step "If t is NaN, return NaN"
// precedes SetDateValue), so a `valueOf` that mutated the cell during
// argument coercion keeps its effect (test262
// `date-value-read-before-tonumber-when-date-is-invalid`).
if year.is_none() {
return date_cell_store(date, timestamp);
return f64::NAN;
}
(1970i64, 0i64, 1i64, 0i64, 0i64, 0i64, 0i64)
} else {
Expand Down Expand Up @@ -1358,6 +1402,7 @@ fn rebuild_with(
#[allow(clippy::too_many_arguments)]
fn rebuild_local_with(
date: f64,
timestamp: f64,
year: Option<f64>,
month0: Option<f64>,
day: Option<f64>,
Expand All @@ -1366,10 +1411,11 @@ fn rebuild_local_with(
second: Option<f64>,
millisecond: Option<f64>,
) -> f64 {
let timestamp = date_cell_timestamp(date);
let (cy, cm0, cd, ch, cmi, cs, cur_ms) = if timestamp.is_nan() {
// See `rebuild_with`: a NaN time value with no year override returns NaN
// without touching `[[DateValue]]`.
if year.is_none() {
return date_cell_store(date, timestamp);
return f64::NAN;
}
(1970i64, 0i64, 1i64, 0i64, 0i64, 0i64, 0i64)
} else {
Expand Down
105 changes: 105 additions & 0 deletions crates/perry-runtime/src/object/date_proto_thunks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,108 @@ pub(crate) fn install_date_proto_getters(proto_obj: *mut ObjectHeader) {
super::global_this::install_proto_method(proto_obj, name, ptr, 0);
}
}

// --- Setters ---------------------------------------------------------------
//
// Like the getters, the instance fast path (`d.setHours(...)`) is lowered by
// codegen straight to `js_date_apply_setter`. But `Date.prototype.setHours` is
// also a plain value (`.call`/`.apply`, method extraction, the legacy
// `setYear`/`setUTC*` family with no codegen fast path), and on that reflective
// path the methods were generic no-ops: `setDate.call(realDate, 1)` no-op'd and
// `setDate.call(nonDate, 1)` silently produced garbage instead of throwing.
//
// Each setter performs `thisTimeValue(this)` (TypeError if `this` is not a
// Date) and is variadic, so these thunks read the `IMPLICIT_THIS` receiver,
// brand-check it, collect the `rest` arguments, and dispatch to the same
// `js_date_apply_setter` the instance path uses. `js_date_apply_setter` reads
// `[[DateValue]]` BEFORE coercing the arguments, so the read-before-ToNumber
// ordering holds on the reflective path too.

/// Resolve the `IMPLICIT_THIS` receiver to a Date value, or throw a `TypeError`
/// (`thisTimeValue` brand check) when it is not a Date. Returns the NaN-boxed
/// `DateCell` value (the setter dispatch needs the receiver itself, not just
/// its timestamp, so it can mutate the cell in place).
fn require_date_this() -> f64 {
let this = f64::from_bits(IMPLICIT_THIS.with(|c| c.get()));
if crate::date::is_date_value(this) {
this
} else {
super::object_ops::throw_object_type_error(b"this is not a Date object.")
}
}

/// `field`/`is_utc` selectors mirror `crate::date::js_date_apply_setter`:
/// 0=FullYear 1=Month 2=Date 3=Hours 4=Minutes 5=Seconds 6=Milliseconds
/// 7=Time, plus 8=setYear (annexB; local-only). `is_utc != 0` picks the UTC
/// rebuild.
macro_rules! date_setter_thunk {
($name:ident, $is_utc:expr, $field:expr) => {
extern "C" fn $name(_closure: *const crate::closure::ClosureHeader, rest: f64) -> f64 {
let this = require_date_this();
let args = super::global_this::global_this_rest_array_values(rest);
crate::date::js_date_apply_setter(
this,
$is_utc,
$field,
args.as_ptr(),
args.len() as i32,
)
}
};
}

date_setter_thunk!(date_set_time, 0, 7);
date_setter_thunk!(date_set_full_year, 0, 0);
date_setter_thunk!(date_set_month, 0, 1);
date_setter_thunk!(date_set_date, 0, 2);
date_setter_thunk!(date_set_hours, 0, 3);
date_setter_thunk!(date_set_minutes, 0, 4);
date_setter_thunk!(date_set_seconds, 0, 5);
date_setter_thunk!(date_set_milliseconds, 0, 6);
date_setter_thunk!(date_set_year, 0, 8);
date_setter_thunk!(date_set_utc_full_year, 1, 0);
date_setter_thunk!(date_set_utc_month, 1, 1);
date_setter_thunk!(date_set_utc_date, 1, 2);
date_setter_thunk!(date_set_utc_hours, 1, 3);
date_setter_thunk!(date_set_utc_minutes, 1, 4);
date_setter_thunk!(date_set_utc_seconds, 1, 5);
date_setter_thunk!(date_set_utc_milliseconds, 1, 6);

/// Install the brand-checked `Date.prototype` setter thunks. Called from
/// `global_this::populate_builtin_prototype_methods`'s `"Date"` arm AFTER the
/// no-op block, so these overwrite the no-op setter entries. Each is installed
/// as a variadic (`rest`) method so the optional trailing components arrive in
/// the `rest` array; `spec_length` is the ECMAScript `.length`.
pub(crate) fn install_date_proto_setters(proto_obj: *mut ObjectHeader) {
if proto_obj.is_null() {
return;
}
// (name, func_ptr, spec `.length`)
let methods: &[(&str, *const u8, u32)] = &[
("setTime", date_set_time as *const u8, 1),
("setFullYear", date_set_full_year as *const u8, 3),
("setMonth", date_set_month as *const u8, 2),
("setDate", date_set_date as *const u8, 1),
("setHours", date_set_hours as *const u8, 4),
("setMinutes", date_set_minutes as *const u8, 3),
("setSeconds", date_set_seconds as *const u8, 2),
("setMilliseconds", date_set_milliseconds as *const u8, 1),
("setYear", date_set_year as *const u8, 1),
("setUTCFullYear", date_set_utc_full_year as *const u8, 3),
("setUTCMonth", date_set_utc_month as *const u8, 2),
("setUTCDate", date_set_utc_date as *const u8, 1),
("setUTCHours", date_set_utc_hours as *const u8, 4),
("setUTCMinutes", date_set_utc_minutes as *const u8, 3),
("setUTCSeconds", date_set_utc_seconds as *const u8, 2),
(
"setUTCMilliseconds",
date_set_utc_milliseconds as *const u8,
1,
),
];
for (name, ptr, length) in methods.iter().copied() {
// call_fixed_arity = 0: every argument arrives in the `rest` array, so
// one thunk shape covers the 0..=4-arg setters uniformly.
super::global_this::install_proto_method_rest_with_length(proto_obj, name, ptr, length, 0);
}
}
4 changes: 4 additions & 0 deletions crates/perry-runtime/src/object/global_this.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4073,6 +4073,10 @@ fn populate_builtin_prototype_methods(builtin_name: &str, proto_obj: *mut Object
// MUST run after the OBJECT_PROTO_METHODS block, which would
// otherwise re-clobber `valueOf` with the generic Object no-op.
date_proto_thunks::install_date_proto_getters(proto_obj);
// Same treatment for the mutating setters: `Date.prototype.setX`
// brand-checks `this`, reads `[[DateValue]]` before coercing args,
// then mutates the cell. Also after the OBJECT_PROTO_METHODS block.
date_proto_thunks::install_date_proto_setters(proto_obj);
install_proto_method(
proto_obj,
"isPrototypeOf",
Expand Down