diff --git a/Cargo.lock b/Cargo.lock index a404c42e..5b1a342e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.19.15" +version = "0.19.16" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.19.15" +version = "0.19.16" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.19.15" +version = "0.19.16" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.19.15" +version = "0.19.16" dependencies = [ "ansi_term", "bumpalo", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.19.15" +version = "0.19.16" dependencies = [ "lru", "oxide_bytecode", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.19.15" +version = "0.19.16" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1258,7 +1258,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.19.15" +version = "0.19.16" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1272,7 +1272,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.19.15" +version = "0.19.16" dependencies = [ "num-bigint", "oxc_allocator", @@ -1288,7 +1288,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.19.15" +version = "0.19.16" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1300,7 +1300,7 @@ dependencies = [ [[package]] name = "oxide_fold" -version = "0.19.15" +version = "0.19.16" dependencies = [ "num-bigint", "oxide_bytecode", @@ -1312,7 +1312,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.19.15" +version = "0.19.16" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1321,7 +1321,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.19.15" +version = "0.19.16" dependencies = [ "bumpalo", "dashmap", @@ -1335,7 +1335,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.19.15" +version = "0.19.16" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1347,7 +1347,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.19.15" +version = "0.19.16" dependencies = [ "tracing", "tracing-appender", @@ -1356,11 +1356,11 @@ dependencies = [ [[package]] name = "oxide_net" -version = "0.19.15" +version = "0.19.16" [[package]] name = "oxide_parser" -version = "0.19.15" +version = "0.19.16" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1375,7 +1375,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.19.15" +version = "0.19.16" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1391,7 +1391,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.19.15" +version = "0.19.16" dependencies = [ "num-bigint", "num-traits", @@ -1404,7 +1404,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.19.15" +version = "0.19.16" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1424,7 +1424,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.19.15" +version = "0.19.16" dependencies = [ "bumpalo", "dashmap", @@ -1437,7 +1437,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.19.15" +version = "0.19.16" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index d71008bc..b4a97cb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ ] [workspace.package] -version = "0.19.15" +version = "0.19.16" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_builtins/src/date.rs b/crates/oxide_builtins/src/date.rs index e02eb5f8..3d810ad6 100644 --- a/crates/oxide_builtins/src/date.rs +++ b/crates/oxide_builtins/src/date.rs @@ -1181,3 +1181,109 @@ pub fn date_value_of(vm: &mut H, args: &[u8]) -> NativeResult { let ms = get_timestamp(obj); NativeResult::Ok(JsValue::float(ms)) } + +/// `Date.prototype.toTemporalInstant()`:把时间戳换算为 epoch 纳秒并构造 Instant。 +/// +/// # 步骤 +/// 1. `date_this` 品牌守卫(非 Date 对象抛 TypeError)。 +/// 2. 取 `[[DateValue]]`,非有限(Invalid Date)抛 RangeError。 +/// 3. 毫秒 × 10^6 得纳秒,交 `make_instant` 构造 Instant 对象。 +/// +/// # 边界 +/// 有效 Date 时间戳恒为整数毫秒且在 TimeClip 包络内(|t| ≤ 8.64e15), +/// 乘积恒在 i128 内,无溢出面。 +pub fn date_to_temporal_instant(vm: &mut H, args: &[u8]) -> NativeResult { + let obj = unsafe { &*native_try!(date_this(vm, args)) }; + let ms = get_timestamp(obj); + if !ms.is_finite() { + return NativeResult::Err(crate::error::create_range_error(vm, "invalid date")); + } + let ns = (ms as i128) * 1_000_000; + crate::temporal::make_instant(vm, ns) +} + +/// OrdinaryToPrimitive:按 [first, second] 序逐个查 `toString`/`valueOf`, +/// 首个产出非对象结果的方法返回其结果;两臂均不产出原始值抛 TypeError。 +/// +/// # 步骤 +/// 1. 按 hint 定方法序(string 族 toString 优先,number 族 valueOf 优先)。 +/// 2. 逐臂 `Get(O, name)`(receiver = O),null/undefined/非可调用跳过。 +/// 3. 调用方法(this = O),结果非对象即返回。 +/// +/// # 注意事项 +/// 用户 getter 与调用抛错经 `take_uncaught_value` 恢复原始异常值,不降级。 +fn ordinary_to_primitive(vm: &mut H, obj: &JsObject, this_val: JsValue, try_first: &str) -> NativeResult { + let (first, second) = if try_first == "string" { + ("toString", "valueOf") + } else { + ("valueOf", "toString") + }; + for name in [first, second] { + let si = vm.perm_intern(name); + let method = match vm.ordinary_get(obj, si, this_val) { + Ok(m) => m, + Err(msg) => { + return NativeResult::Err( + vm.take_uncaught_value() + .unwrap_or_else(|| crate::error::create_type_error(vm, &msg)), + ); + } + }; + if method.is_undefined() || method.is_null() { + continue; + } + if !crate::iterator::is_callable(method) { + continue; + } + let result = match vm.call_function_sync(method, this_val, &[]) { + Ok(r) => r, + Err(msg) => { + return NativeResult::Err( + vm.take_uncaught_value() + .unwrap_or_else(|| crate::error::create_type_error(vm, &msg)), + ); + } + }; + if !result.is_object() { + return NativeResult::Ok(result); + } + } + NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert object to primitive value")) +} + +/// `Date.prototype[Symbol.toPrimitive](hint)`:OrdinaryToPrimitive 原始值转换。 +/// +/// # 步骤 +/// 1. this 非对象抛 TypeError(不做 Date 品牌检查,普通对象合法)。 +/// 2. hint 须为原始字符串值,严格比较("string"/"default"/"number"), +/// 其余(含 String 对象、带 toString 的对象)抛 TypeError,不做强转。 +/// 3. 按 hint 定 tryFirst 走 OrdinaryToPrimitive。 +/// +/// # 注意事项 +/// 不复用 runtime `to_primitive`:其先查 @@toPrimitive 再走 OrdinaryToPrimitive, +/// 语义漂移且对 Date 自身会递归。 +pub fn date_to_primitive(vm: &mut H, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if !this_val.is_object() { + return NativeResult::Err(crate::error::create_type_error(vm, "called on incompatible receiver")); + } + let obj_ptr = this_val.as_js_object_ptr(); + if obj_ptr.is_null() { + return NativeResult::Err(crate::error::create_type_error(vm, "called on incompatible receiver")); + } + let obj = unsafe { &*obj_ptr }; + + let hint_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + if !hint_val.is_string() { + return NativeResult::Err(crate::error::create_type_error(vm, "invalid hint")); + } + // SAFETY: hint_val 已确认是字符串值。 + let hint = unsafe { (*hint_val.as_string_ptr()).to_owned_string() }; + let try_first = match hint.as_str() { + "string" | "default" => "string", + "number" => "number", + _ => return NativeResult::Err(crate::error::create_type_error(vm, "invalid hint")), + }; + + ordinary_to_primitive(vm, obj, this_val, try_first) +} diff --git a/crates/oxide_builtins/src/temporal/mod.rs b/crates/oxide_builtins/src/temporal/mod.rs index c33e6441..36b6c5d8 100644 --- a/crates/oxide_builtins/src/temporal/mod.rs +++ b/crates/oxide_builtins/src/temporal/mod.rs @@ -11,6 +11,7 @@ mod time_zone; mod zoned_date_time; use common::*; +pub(crate) use common::make_instant; use difference::*; pub use duration::*; pub use instant::*; diff --git a/crates/oxide_vm/src/bindings/bind_date.rs b/crates/oxide_vm/src/bindings/bind_date.rs index 606d638e..295ee5c6 100644 --- a/crates/oxide_vm/src/bindings/bind_date.rs +++ b/crates/oxide_vm/src/bindings/bind_date.rs @@ -1,5 +1,8 @@ -use crate::bindings::{apply_binding_table, bind_global_value, configure_native_constructor, BindEnv}; +use crate::bindings::{ + apply_binding_table, bind_global_value, bind_well_known_method, configure_native_constructor, BindEnv, +}; use oxide_types::object::{JsObject, PropAttributes}; +use oxide_types::private_key::{encode_symbol_key, WELL_KNOWN_SYMBOL_TO_PRIMITIVE}; use oxide_types::value::JsValue; /// 把 Date 构造器与原型方法绑定到 global。 @@ -134,9 +137,25 @@ pub fn bind_date(env: BindEnv<'_>, global: &mut JsObject) { 0, ), ("valueOf", oxide_builtins::date::date_value_of:: as *const (), 0), + ("toTemporalInstant", oxide_builtins::date::date_to_temporal_instant:: as *const (), 0), ("toSource", oxide_builtins::to_source::to_source_date:: as *const (), 0), ], ); + // [Symbol.toPrimitive] 方法:bind 默认描述符可写,规范为不可写,绑定后改 meta。 + bind_well_known_method( + env, + proto, + WELL_KNOWN_SYMBOL_TO_PRIMITIVE, + "[Symbol.toPrimitive]", + oxide_builtins::date::date_to_primitive:: as *const (), + 1, + ); + let to_prim_key = encode_symbol_key(env.realm_id, WELL_KNOWN_SYMBOL_TO_PRIMITIVE); + if let Some(pos) = env.shape_forge().lookup_position(proto.shape_id(), to_prim_key) { + proto.set_data_meta(pos, PropAttributes::new(false, false, true)); + proto.bump_generation(); + } + bind_global_value(env.core, global, "Date", JsValue::from_js_object(ctor_ptr)); } diff --git a/crates/oxide_vm/tests/builtins_date_tests.rs b/crates/oxide_vm/tests/builtins_date_tests.rs index 3409958d..a487cd57 100644 --- a/crates/oxide_vm/tests/builtins_date_tests.rs +++ b/crates/oxide_vm/tests/builtins_date_tests.rs @@ -730,3 +730,287 @@ fn date_constructor_object_arg_error_propagates() { .unwrap(); assert_eq!(str_val(&vm, r), "yes"); } + +// ── toTemporalInstant ── + +#[test] +fn date_to_temporal_instant_epoch_values() { + let mut vm = Vm::new(); + // 有效日期换算为 epoch 纳秒(毫秒 × 10^6),逐值与 BigInt 字面量比较。 + let cases = [ + ("new Date(0)", "0n"), + ("new Date(123456789)", "123456789000000n"), + ("new Date(-123456789)", "-123456789000000n"), + // TimeClip 包络两端(±8.64e15 ms)。 + ("new Date(8.64e15)", "8640000000000000000000n"), + ("new Date(-8.64e15)", "-8640000000000000000000n"), + ]; + for (date, ns) in cases { + let r = eval(&mut vm, &format!("{date}.toTemporalInstant().epochNanoseconds === {ns} ? 1 : 0")) + .unwrap(); + assert_eq!(r.as_int(), 1, "date = {date}"); + } +} + +#[test] +fn date_to_temporal_instant_invalid_date_throws_range_error() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "var d = new Date(NaN); \ + try { d.toTemporalInstant(); 'no' } catch (e) { e instanceof RangeError ? 'yes' : 'no' }", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "yes"); +} + +#[test] +fn date_to_temporal_instant_non_date_throws_type_error() { + let mut vm = Vm::new(); + let f = |vm: &mut Vm, this: &str| { + eval( + vm, + &format!( + "var m = Date.prototype.toTemporalInstant; \ + try {{ m.call({this}); 'no' }} catch (e) {{ e instanceof TypeError ? 'yes' : 'no' }}" + ), + ) + .unwrap() + }; + for this in ["{}", "[]", "function(){}", "Date.prototype", "0", "null", "undefined", "Symbol()"] { + let r = f(&mut vm, this); + assert_eq!(str_val(&vm, r), "yes", "this = {this}"); + } +} + +#[test] +fn date_to_temporal_instant_prop_desc() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "var d = Object.getOwnPropertyDescriptor(Date.prototype, 'toTemporalInstant'); \ + d.writable === true && d.enumerable === false && d.configurable === true ? 1 : 0", + ) + .unwrap(); + assert_eq!(r.as_int(), 1); +} + +#[test] +fn date_to_temporal_instant_length_name() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "Date.prototype.toTemporalInstant.length === 0 && \ + Date.prototype.toTemporalInstant.name === 'toTemporalInstant' ? 1 : 0", + ) + .unwrap(); + assert_eq!(r.as_int(), 1); +} + +// ── [Symbol.toPrimitive] ── + +#[test] +fn date_to_primitive_number_hint_first_valid() { + let mut vm = Vm::new(); + // number 提示 valueOf 优先:valueOf 产出原始值即返回,toString 不被引用。 + let r = eval( + &mut vm, + "var vo = 0, ts = 0; \ + var obj = { valueOf: function() { vo++; return 'valueOf test'; }, \ + get toString() { ts++; } }; \ + var v = Date.prototype[Symbol.toPrimitive].call(obj, 'number'); \ + v === 'valueOf test' && vo === 1 && ts === 0 ? 1 : 0", + ) + .unwrap(); + assert_eq!(r.as_int(), 1); +} + +#[test] +fn date_to_primitive_string_hint_first_valid() { + let mut vm = Vm::new(); + // string 提示 toString 优先:toString 产出原始值即返回,valueOf 不被引用。 + let r = eval( + &mut vm, + "var vo = 0, ts = 0; \ + var obj = { get valueOf() { vo++; }, \ + toString: function() { ts++; return 'toString test'; } }; \ + var v = Date.prototype[Symbol.toPrimitive].call(obj, 'string'); \ + v === 'toString test' && vo === 0 && ts === 1 ? 1 : 0", + ) + .unwrap(); + assert_eq!(r.as_int(), 1); +} + +#[test] +fn date_to_primitive_default_hint_uses_string_order() { + let mut vm = Vm::new(); + // default 提示与 string 同序(toString 优先)。 + let r = eval( + &mut vm, + "var obj = { valueOf: function() { return 1; }, \ + toString: function() { return 'def'; } }; \ + Date.prototype[Symbol.toPrimitive].call(obj, 'default') === 'def' ? 1 : 0", + ) + .unwrap(); + assert_eq!(r.as_int(), 1); +} + +#[test] +fn date_to_primitive_number_hint_first_non_callable() { + let mut vm = Vm::new(); + // valueOf 非可调用时回落 toString。 + let r = eval( + &mut vm, + "var obj = { valueOf: null, toString: function() { return 'ts'; } }; \ + Date.prototype[Symbol.toPrimitive].call(obj, 'number') === 'ts' ? 1 : 0", + ) + .unwrap(); + assert_eq!(r.as_int(), 1); +} + +#[test] +fn date_to_primitive_number_hint_first_invalid() { + let mut vm = Vm::new(); + // valueOf 返回对象(非原始值)时回落 toString。 + let r = eval( + &mut vm, + "var obj = { valueOf: function() { return {}; }, \ + toString: function() { return 'ts'; } }; \ + Date.prototype[Symbol.toPrimitive].call(obj, 'number') === 'ts' ? 1 : 0", + ) + .unwrap(); + assert_eq!(r.as_int(), 1); +} + +#[test] +fn date_to_primitive_no_callables_throws_type_error() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "var obj = { valueOf: null, toString: null }; \ + try { Date.prototype[Symbol.toPrimitive].call(obj, 'number'); 'no' } \ + catch (e) { e instanceof TypeError ? 'yes' : 'no' }", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "yes"); +} + +#[test] +fn date_to_primitive_invalid_hint_throws_type_error() { + let mut vm = Vm::new(); + let f = |vm: &mut Vm, hint: &str| { + eval( + vm, + &format!( + "var d = new Date(0); \ + try {{ d[Symbol.toPrimitive]({hint}); 'no' }} \ + catch (e) {{ e instanceof TypeError ? 'yes' : 'no' }}" + ), + ) + .unwrap() + }; + // 缺参、undefined、null、空串、非原始串(String 对象 / 带 toString 的对象)均抛 TypeError。 + for hint in [ + "", + "undefined", + "null", + "''", + "'String'", + "new String('number')", + "{ toString: function() { 'number'; } }", + ] { + let r = f(&mut vm, hint); + assert_eq!(str_val(&vm, r), "yes", "hint = {hint}"); + } +} + +#[test] +fn date_to_primitive_non_object_this_throws_type_error() { + let mut vm = Vm::new(); + let f = |vm: &mut Vm, this: &str| { + eval( + vm, + &format!( + "try {{ Date.prototype[Symbol.toPrimitive].call({this}, 'string'); 'no' }} \ + catch (e) {{ e instanceof TypeError ? 'yes' : 'no' }}" + ), + ) + .unwrap() + }; + for this in ["undefined", "null", "86", "''", "true"] { + let r = f(&mut vm, this); + assert_eq!(str_val(&vm, r), "yes", "this = {this}"); + } +} + +#[test] +fn date_to_primitive_works_on_plain_object() { + let mut vm = Vm::new(); + // 非 Date 品牌方法:普通对象合法(仅要求 this 为对象)。 + let r = eval( + &mut vm, + "var obj = { valueOf: function() { return 42; } }; \ + Date.prototype[Symbol.toPrimitive].call(obj, 'number') === 42 ? 1 : 0", + ) + .unwrap(); + assert_eq!(r.as_int(), 1); +} + +#[test] +fn date_to_primitive_prop_desc_length_name() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "var d = Object.getOwnPropertyDescriptor(Date.prototype, Symbol.toPrimitive); \ + d.writable === false && d.enumerable === false && d.configurable === true ? 1 : 0", + ) + .unwrap(); + assert_eq!(r.as_int(), 1); + let r = eval( + &mut vm, + "Date.prototype[Symbol.toPrimitive].length === 1 && \ + Date.prototype[Symbol.toPrimitive].name === '[Symbol.toPrimitive]' ? 1 : 0", + ) + .unwrap(); + assert_eq!(r.as_int(), 1); +} + +#[test] +fn date_to_primitive_user_throw_propagates_original() { + let mut vm = Vm::new(); + // 用户方法抛错经原始异常值传播(不降级为 TypeError)。 + let r = eval( + &mut vm, + "var obj = { valueOf: function() { throw 'boom'; } }; \ + try { Date.prototype[Symbol.toPrimitive].call(obj, 'number'); 'no' } \ + catch (e) { e === 'boom' ? 'yes' : 'no' }", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "yes"); +} + +#[test] +fn date_to_primitive_called_as_function_no_strict_this() { + let mut vm = Vm::new(); + // 顶层函数形态调用(this 为 undefined)抛 TypeError,且不触发 toString/valueOf 查找。 + let r = eval( + &mut vm, + "var toPrimitive = Date.prototype[Symbol.toPrimitive]; \ + try { toPrimitive('default'); 'no' } catch (e) { e instanceof TypeError ? 'yes' : 'no' }", + ) + .unwrap(); + assert_eq!(str_val(&vm, r), "yes"); +} + +// 既有 Date 强转路径行为回归:绑定 [Symbol.toPrimitive] 后,VM 的 ToPrimitive +// 对 Date 先经原型方法再走 OrdinaryToPrimitive,可观察行为与直接 valueOf/toString 等价。 +#[test] +fn date_to_primitive_existing_coercion_regression() { + let mut vm = Vm::new(); + // +new Date(0) 经 valueOf 通道。 + let r = eval(&mut vm, "+new Date(0)").unwrap(); + assert_eq!(r.as_double(), 0.0); + // "" + new Date(0) 经 toString 通道(得到字符串)。 + let r = eval(&mut vm, "typeof ('' + new Date(0))").unwrap(); + assert_eq!(str_val(&vm, r), "string"); +}