diff --git a/Cargo.lock b/Cargo.lock index 0bcdab28..17df1a8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.19.19" +version = "0.19.20" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.19.19" +version = "0.19.20" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.19.19" +version = "0.19.20" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.19.19" +version = "0.19.20" dependencies = [ "ansi_term", "bumpalo", @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.19.19" +version = "0.19.20" dependencies = [ "lru", "oxide_bytecode", @@ -1237,7 +1237,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.19.19" +version = "0.19.20" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1258,7 +1258,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.19.19" +version = "0.19.20" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1272,7 +1272,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.19.19" +version = "0.19.20" dependencies = [ "num-bigint", "oxc_allocator", @@ -1288,7 +1288,7 @@ dependencies = [ [[package]] name = "oxide_engine" -version = "0.19.19" +version = "0.19.20" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1300,7 +1300,7 @@ dependencies = [ [[package]] name = "oxide_fold" -version = "0.19.19" +version = "0.19.20" dependencies = [ "num-bigint", "oxide_bytecode", @@ -1312,7 +1312,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.19.19" +version = "0.19.20" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1321,7 +1321,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.19.19" +version = "0.19.20" dependencies = [ "bumpalo", "dashmap", @@ -1335,7 +1335,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.19.19" +version = "0.19.20" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1347,7 +1347,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.19.19" +version = "0.19.20" dependencies = [ "tracing", "tracing-appender", @@ -1356,11 +1356,11 @@ dependencies = [ [[package]] name = "oxide_net" -version = "0.19.19" +version = "0.19.20" [[package]] name = "oxide_parser" -version = "0.19.19" +version = "0.19.20" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1375,7 +1375,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.19.19" +version = "0.19.20" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1391,7 +1391,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.19.19" +version = "0.19.20" dependencies = [ "num-bigint", "num-traits", @@ -1404,7 +1404,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.19.19" +version = "0.19.20" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1424,7 +1424,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.19.19" +version = "0.19.20" dependencies = [ "bumpalo", "dashmap", @@ -1437,7 +1437,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.19.19" +version = "0.19.20" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 84cf83a6..7b7e45e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ ] [workspace.package] -version = "0.19.19" +version = "0.19.20" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_builtins/src/temporal/duration.rs b/crates/oxide_builtins/src/temporal/duration.rs index c2d20f07..a8ca6d53 100644 --- a/crates/oxide_builtins/src/temporal/duration.rs +++ b/crates/oxide_builtins/src/temporal/duration.rs @@ -1,7 +1,7 @@ //! Temporal.Duration:ISO 字符串与 property bag 解析、构造、getter、total/round、 //! 加减比较与格式化。 -use oxide_runtime_api::{to_number, to_string, NativeResult, ProtoKind, VmHost}; +use oxide_runtime_api::{to_number, to_number_full, to_string, NativeResult, ProtoKind, VmHost}; use oxide_types::object::JsObject; use oxide_types::value::JsValue; @@ -264,6 +264,18 @@ pub(crate) fn duration_like_values(vm: &mut H, val: JsValue) -> Resul } /// `Temporal.Duration` 构造器,保存十个整数时长分量。 +/// +/// # 步骤 +/// 1. 校验构造调用(非 `new` 形态抛 TypeError)。 +/// 2. 按年、月、周、日、时、分、秒、毫秒、微秒、纳秒顺序逐字段转换: +/// undefined 视为 0;BigInt 抛 TypeError;其余经完整 ToNumber +/// (对象走 valueOf 路径,Symbol 抛 TypeError);NaN、非整数、 +/// 非有限值抛 RangeError。 +/// 3. 共享校验器做符号一致性与范围检查。 +/// +/// # 注意事项 +/// 零分量归一为 +0:规范经整数往返(取整再还原为 Number), +/// 传入 −0 时 getter 仍返回 +0。 pub fn duration_constructor(vm: &mut H, args: &[u8]) -> NativeResult { let ctor_proto = vm.builtin_proto(ProtoKind::DurationProto); if !is_ctor_call(vm, args, ctor_proto) { @@ -277,11 +289,22 @@ pub fn duration_constructor(vm: &mut H, args: &[u8]) -> NativeResult if args.len() <= index + 1 { continue; } - let number = to_number(vm.reg(args[index + 1])); + let arg = vm.reg(args[index + 1]); + if arg.is_undefined() { + continue; + } + if arg.is_bigint() { + return NativeResult::Err(crate::error::create_type_error(vm, "cannot convert duration field to number")); + } + let number = match to_number_full(arg, vm) { + Ok(value) => value, + Err(error) => return NativeResult::Err(native_engine_error(vm, &error)), + }; if !number.is_finite() || number.fract() != 0.0 { return NativeResult::Err(crate::error::create_range_error(vm, "invalid duration")); } - *value = number; + // 零分量归一为 +0:规范整数往返(取整再还原 Number)使 −0 落为 +0。 + *value = if number == 0.0 { 0.0 } else { number }; } native_try!(validate_duration_values(vm, &values)); initialize_temporal_receiver(vm, args, JsObject::OBJ_TYPE_DURATION, values.map(JsValue::float)) diff --git a/crates/oxide_vm/tests/builtins_temporal_tests.rs b/crates/oxide_vm/tests/builtins_temporal_tests.rs index 46552ce2..8861144d 100644 --- a/crates/oxide_vm/tests/builtins_temporal_tests.rs +++ b/crates/oxide_vm/tests/builtins_temporal_tests.rs @@ -574,6 +574,32 @@ fn duration_constructor_exposes_components() { assert_eq!(str_val(&vm, r), "P1Y2M3W4D"); } +#[test] +fn duration_constructor_coerces_fields_per_spec() { + let mut vm = Vm::new(); + // undefined 分量视为 0 + let r = eval(&mut vm, "new Temporal.Duration(undefined).toString()").unwrap(); + assert_eq!(str_val(&vm, r), "PT0S"); + let r = eval(&mut vm, "new Temporal.Duration(1, undefined).toString()").unwrap(); + assert_eq!(str_val(&vm, r), "P1Y"); + // Symbol 与 BigInt 抛 TypeError + let r = eval(&mut vm, "try { new Temporal.Duration(Symbol()) } catch (e) { e.constructor.name }").unwrap(); + assert_eq!(str_val(&vm, r), "TypeError"); + let r = eval(&mut vm, "try { new Temporal.Duration(0n) } catch (e) { e.constructor.name }").unwrap(); + assert_eq!(str_val(&vm, r), "TypeError"); + // 对象经 valueOf 转换 + assert_eq!(num(&mut vm, "new Temporal.Duration({valueOf(){return 1}}).years"), 1.0); + // ±Infinity 抛 RangeError + let r = eval(&mut vm, "try { new Temporal.Duration(Infinity) } catch (e) { e.constructor.name }").unwrap(); + assert_eq!(str_val(&vm, r), "RangeError"); + let r = eval(&mut vm, "try { new Temporal.Duration(-Infinity) } catch (e) { e.constructor.name }").unwrap(); + assert_eq!(str_val(&vm, r), "RangeError"); + // 传入 −0 时 getter 返回 +0(整数往返归一) + assert!(eval(&mut vm, "Object.is(new Temporal.Duration(-0).years, 0)") + .unwrap() + .as_bool()); +} + #[test] fn duration_from_validates_property_bags_and_preserves_precision() { let mut vm = Vm::new();