From 11cc03dedc5bf2f9644a5d2166f6f518986cff7b Mon Sep 17 00:00:00 2001 From: jesperkha Date: Tue, 3 Mar 2026 13:16:47 +0100 Subject: [PATCH 1/6] Add constructor Add create_temporal_plain_time --- .../builtins/temporal/plain_time.rs | 40 +++++- .../plain_time/plain_time_constructor.rs | 122 +++++++++++++++++- 2 files changed, 152 insertions(+), 10 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/temporal/plain_time.rs b/nova_vm/src/ecmascript/builtins/temporal/plain_time.rs index ebfe4c452..29ad116e7 100644 --- a/nova_vm/src/ecmascript/builtins/temporal/plain_time.rs +++ b/nova_vm/src/ecmascript/builtins/temporal/plain_time.rs @@ -12,10 +12,10 @@ pub(crate) use plain_time_prototype::*; use crate::{ ecmascript::{ - Agent, ExceptionType, InternalMethods, InternalSlots, JsResult, OrdinaryObject, - ProtoIntrinsics, Value, object_handle, + Agent, ExceptionType, Function, InternalMethods, InternalSlots, JsResult, OrdinaryObject, + ProtoIntrinsics, Value, object_handle, ordinary_populate_from_constructor, }, - engine::{Bindable, NoGcScope}, + engine::{Bindable, GcScope, NoGcScope}, heap::{ ArenaAccess, ArenaAccessMut, BaseIndex, CompactionLists, CreateHeapData, Heap, HeapMarkAndSweep, HeapSweepWeakReference, WorkQueues, arena_vec_access, @@ -99,3 +99,37 @@ fn require_internal_slot_temporal_plain_time<'a>( )), } } + +/// ### [4.5.11 CreateTemporalTime](https://tc39.es/proposal-temporal/#sec-temporal-createtemporaltime) +pub(crate) fn create_temporal_plain_time<'gc>( + agent: &mut Agent, + plain_time: temporal_rs::PlainTime, + new_target: Option, + gc: GcScope<'gc, '_>, +) -> JsResult<'gc, TemporalPlainTime<'gc>> { + // 1. If newTarget is not present, set newTarget to %Temporal.PlainTime%. + let new_target = new_target.unwrap_or_else(|| { + agent + .current_realm_record() + .intrinsics() + .temporal_plain_time() + .into() + }); + // 2. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.PlainTime.prototype%", « [[InitializedTemporalTime]], [[Time]] »). + // 3. Set object.[[Time]] to time. + // 4. Return object. + let object = agent.heap.create(PlainTimeRecord { + object_index: None, + plain_time, + }); + Ok( + TemporalPlainTime::try_from(ordinary_populate_from_constructor( + agent, + object.unbind().into(), + new_target, + ProtoIntrinsics::TemporalPlainTime, + gc, + )?) + .unwrap(), + ) +} diff --git a/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_constructor.rs b/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_constructor.rs index dfdaa712f..ca51af655 100644 --- a/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_constructor.rs +++ b/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_constructor.rs @@ -5,10 +5,11 @@ use crate::{ ecmascript::{ Agent, ArgumentsList, BUILTIN_STRING_MEMORY, Behaviour, Builtin, - BuiltinIntrinsicConstructor, JsResult, Object, Realm, String, Value, - builders::BuiltinFunctionBuilder, + BuiltinIntrinsicConstructor, ExceptionType, Function, JsResult, Object, Realm, String, + Value, builders::BuiltinFunctionBuilder, create_temporal_plain_time, + temporal_err_to_js_err, to_integer_with_truncation, }, - engine::{GcScope, NoGcScope}, + engine::{Bindable as _, GcScope, NoGcScope, Scopable}, heap::IntrinsicConstructorIndexes, }; @@ -25,14 +26,121 @@ impl BuiltinIntrinsicConstructor for TemporalPlainTimeConstructor { } impl TemporalPlainTimeConstructor { + /// ### [4.1.1 Temporal.PlainTime](https://tc39.es/proposal-temporal/#sec-temporal.plaintime) fn constructor<'gc>( agent: &mut Agent, _: Value, - _args: ArgumentsList, - _new_target: Option, - gc: GcScope<'gc, '_>, + args: ArgumentsList, + new_target: Option, + mut gc: GcScope<'gc, '_>, ) -> JsResult<'gc, Value<'gc>> { - Err(agent.todo("Temporal.PlainTime", gc.into_nogc())) + let hours = args.get(1).scope(agent, gc.nogc()); + let minutes = args.get(2).scope(agent, gc.nogc()); + let seconds = args.get(3).scope(agent, gc.nogc()); + let milliseconds = args.get(4).scope(agent, gc.nogc()); + let microseconds = args.get(5).scope(agent, gc.nogc()); + let nanoseconds = args.get(6).scope(agent, gc.nogc()); + + let new_target = new_target.bind(gc.nogc()); + + // 1. If NewTarget is undefined, throw a TypeError exception. + let Some(new_target) = new_target else { + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "calling a builtin Temporal.PlainTime constructor without new is forbidden", + gc.into_nogc(), + )); + }; + + let Ok(new_target) = Function::try_from(new_target) else { + unreachable!() + }; + let new_target = new_target.scope(agent, gc.nogc()); + + // 2. If hour is undefined, set hour to 0; else set hour to ? ToIntegerWithTruncation(hour). + let h = if hours.get(agent).is_undefined() { + Ok(0) + } else { + u8::try_from( + to_integer_with_truncation(agent, hours.get(agent), gc.reborrow()).unbind()?, + ) + }; + + // 3. If minute is undefined, set minute to 0; else set minute to ? ToIntegerWithTruncation(minute). + let m = if minutes.get(agent).is_undefined() { + Ok(0) + } else { + u8::try_from( + to_integer_with_truncation(agent, minutes.get(agent), gc.reborrow()).unbind()?, + ) + }; + + // 4. If second is undefined, set second to 0; else set second to ? ToIntegerWithTruncation(second). + let s = if seconds.get(agent).is_undefined() { + Ok(0) + } else { + u8::try_from( + to_integer_with_truncation(agent, seconds.get(agent), gc.reborrow()).unbind()?, + ) + }; + + // 5. If millisecond is undefined, set millisecond to 0; else set millisecond to ? ToIntegerWithTruncation(millisecond). + let ms = if milliseconds.get(agent).is_undefined() { + Ok(0) + } else { + u16::try_from( + to_integer_with_truncation(agent, milliseconds.get(agent), gc.reborrow()) + .unbind()?, + ) + }; + + // 6. If microsecond is undefined, set microsecond to 0; else set microsecond to ? ToIntegerWithTruncation(microsecond). + let mis = if microseconds.get(agent).is_undefined() { + Ok(0) + } else { + u16::try_from( + to_integer_with_truncation(agent, microseconds.get(agent), gc.reborrow()) + .unbind()?, + ) + }; + + // 7. If nanosecond is undefined, set nanosecond to 0; else set nanosecond to ? ToIntegerWithTruncation(nanosecond). + let ns = if nanoseconds.get(agent).is_undefined() { + Ok(0) + } else { + u16::try_from( + to_integer_with_truncation(agent, nanoseconds.get(agent), gc.reborrow()) + .unbind()?, + ) + }; + + // 8. If IsValidTime(hour, minute, second, millisecond, microsecond, nanosecond) is false, throw a RangeError exception. + // 9. Let time be CreateTimeRecord(hour, minute, second, millisecond, microsecond, nanosecond). + let time = if let ( + Ok(hour), + Ok(minute), + Ok(second), + Ok(millisecond), + Ok(microsecond), + Ok(nanosecond), + ) = (h, m, s, ms, mis, ns) + { + temporal_rs::PlainTime::try_new( + hour, + minute, + second, + millisecond, + microsecond, + nanosecond, + ) + .map_err(|err| temporal_err_to_js_err(agent, err, gc.nogc())) + .unbind()? + } else { + todo!() // TODO: create range error + }; + + // 10. Return ? CreateTemporalTime(time, NewTarget). + create_temporal_plain_time(agent, time, Some(new_target.get(agent)), gc).map(Value::from) } pub(crate) fn create_intrinsic(agent: &mut Agent, realm: Realm<'static>, _gc: NoGcScope) { From 4449a0221a2130dda5023e61c05919e39c8498de Mon Sep 17 00:00:00 2001 From: jesperkha Date: Tue, 3 Mar 2026 13:28:08 +0100 Subject: [PATCH 2/6] Add RangeError exception --- .../builtins/temporal/plain_time/plain_time_constructor.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_constructor.rs b/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_constructor.rs index ca51af655..9f806bb39 100644 --- a/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_constructor.rs +++ b/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_constructor.rs @@ -136,7 +136,11 @@ impl TemporalPlainTimeConstructor { .map_err(|err| temporal_err_to_js_err(agent, err, gc.nogc())) .unbind()? } else { - todo!() // TODO: create range error + return Err(agent.throw_exception_with_static_message( + ExceptionType::RangeError, + "not a valid time", + gc.into_nogc(), + )); }; // 10. Return ? CreateTemporalTime(time, NewTarget). From c6f8ac63ffb4c2712c35011fa40bfaec424640cc Mon Sep 17 00:00:00 2001 From: jesperkha Date: Tue, 3 Mar 2026 15:50:03 +0100 Subject: [PATCH 3/6] Test results --- tests/expectations.json | 10 +--------- tests/metrics.json | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/expectations.json b/tests/expectations.json index 9fde238da..d1585ef0b 100644 --- a/tests/expectations.json +++ b/tests/expectations.json @@ -3826,13 +3826,8 @@ "built-ins/Temporal/PlainTime/basic.js": "FAIL", "built-ins/Temporal/PlainTime/compare/argument-cast.js": "FAIL", "built-ins/Temporal/PlainTime/compare/argument-number.js": "FAIL", - "built-ins/Temporal/PlainTime/compare/argument-string-calendar-annotation-invalid-key.js": "FAIL", "built-ins/Temporal/PlainTime/compare/argument-string-calendar-annotation.js": "FAIL", - "built-ins/Temporal/PlainTime/compare/argument-string-critical-unknown-annotation.js": "FAIL", "built-ins/Temporal/PlainTime/compare/argument-string-date-with-utc-offset.js": "FAIL", - "built-ins/Temporal/PlainTime/compare/argument-string-minus-sign.js": "FAIL", - "built-ins/Temporal/PlainTime/compare/argument-string-multiple-calendar.js": "FAIL", - "built-ins/Temporal/PlainTime/compare/argument-string-multiple-time-zone.js": "FAIL", "built-ins/Temporal/PlainTime/compare/argument-string-no-implicit-midnight.js": "FAIL", "built-ins/Temporal/PlainTime/compare/argument-string-time-designator-required-for-disambiguation.js": "FAIL", "built-ins/Temporal/PlainTime/compare/argument-string-time-separators.js": "FAIL", @@ -3853,7 +3848,6 @@ "built-ins/Temporal/PlainTime/compare/prop-desc.js": "FAIL", "built-ins/Temporal/PlainTime/compare/use-internal-slots.js": "FAIL", "built-ins/Temporal/PlainTime/compare/year-zero.js": "FAIL", - "built-ins/Temporal/PlainTime/constructor.js": "FAIL", "built-ins/Temporal/PlainTime/from/argument-object-leap-second.js": "FAIL", "built-ins/Temporal/PlainTime/from/argument-object.js": "FAIL", "built-ins/Temporal/PlainTime/from/argument-plaindatetime.js": "FAIL", @@ -3899,7 +3893,6 @@ "built-ins/Temporal/PlainTime/from/prop-desc.js": "FAIL", "built-ins/Temporal/PlainTime/from/subclassing-ignored.js": "FAIL", "built-ins/Temporal/PlainTime/from/year-zero.js": "FAIL", - "built-ins/Temporal/PlainTime/get-prototype-from-constructor-throws.js": "FAIL", "built-ins/Temporal/PlainTime/hour-undefined.js": "FAIL", "built-ins/Temporal/PlainTime/infinity-throws-rangeerror.js": "FAIL", "built-ins/Temporal/PlainTime/microsecond-undefined.js": "FAIL", @@ -3973,7 +3966,6 @@ "built-ins/Temporal/PlainTime/prototype/round/name.js": "FAIL", "built-ins/Temporal/PlainTime/prototype/round/not-a-constructor.js": "FAIL", "built-ins/Temporal/PlainTime/prototype/round/options-read-before-algorithmic-validation.js": "FAIL", - "built-ins/Temporal/PlainTime/prototype/round/options-wrong-type.js": "FAIL", "built-ins/Temporal/PlainTime/prototype/round/prop-desc.js": "FAIL", "built-ins/Temporal/PlainTime/prototype/round/rounding-cross-midnight.js": "FAIL", "built-ins/Temporal/PlainTime/prototype/round/roundingincrement-hours.js": "FAIL", @@ -4241,7 +4233,6 @@ "built-ins/Temporal/PlainTime/prototype/with/length.js": "FAIL", "built-ins/Temporal/PlainTime/prototype/with/name.js": "FAIL", "built-ins/Temporal/PlainTime/prototype/with/not-a-constructor.js": "FAIL", - "built-ins/Temporal/PlainTime/prototype/with/options-invalid.js": "FAIL", "built-ins/Temporal/PlainTime/prototype/with/options-object.js": "FAIL", "built-ins/Temporal/PlainTime/prototype/with/options-read-before-algorithmic-validation.js": "FAIL", "built-ins/Temporal/PlainTime/prototype/with/options-undefined.js": "FAIL", @@ -6660,6 +6651,7 @@ "staging/sm/ArrayBuffer/slice-species.js": "FAIL", "staging/sm/AsyncGenerators/for-await-of-error.js": "CRASH", "staging/sm/BigInt/Number-conversion-rounding.js": "FAIL", + "staging/sm/BigInt/large-bit-length.js": "TIMEOUT", "staging/sm/Date/dst-offset-caching-1-of-8.js": "TIMEOUT", "staging/sm/Date/dst-offset-caching-2-of-8.js": "TIMEOUT", "staging/sm/Date/dst-offset-caching-3-of-8.js": "TIMEOUT", diff --git a/tests/metrics.json b/tests/metrics.json index d6b67a54f..8c4bb81e0 100644 --- a/tests/metrics.json +++ b/tests/metrics.json @@ -4,7 +4,7 @@ "fail": 6931, "pass": 40369, "skip": 3326, - "timeout": 18, + "timeout": 19, "unresolved": 37 }, "total": 50733 From a596e1cb6735326c163ece8c61734e8a6fe605ae Mon Sep 17 00:00:00 2001 From: jesperkha Date: Mon, 9 Mar 2026 13:45:24 +0100 Subject: [PATCH 4/6] Let temporal-rs handle parameter validation --- .../plain_time/plain_time_constructor.rs | 68 ++++++++----------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_constructor.rs b/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_constructor.rs index 9f806bb39..bd87f7150 100644 --- a/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_constructor.rs +++ b/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_constructor.rs @@ -2,6 +2,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use std::u8; + use crate::{ ecmascript::{ Agent, ArgumentsList, BUILTIN_STRING_MEMORY, Behaviour, Builtin, @@ -58,90 +60,80 @@ impl TemporalPlainTimeConstructor { let new_target = new_target.scope(agent, gc.nogc()); // 2. If hour is undefined, set hour to 0; else set hour to ? ToIntegerWithTruncation(hour). - let h = if hours.get(agent).is_undefined() { - Ok(0) + let hour = if hours.get(agent).is_undefined() { + 0 } else { u8::try_from( to_integer_with_truncation(agent, hours.get(agent), gc.reborrow()).unbind()?, ) + .unwrap_or(u8::MAX) }; // 3. If minute is undefined, set minute to 0; else set minute to ? ToIntegerWithTruncation(minute). - let m = if minutes.get(agent).is_undefined() { - Ok(0) + let minute = if minutes.get(agent).is_undefined() { + 0 } else { u8::try_from( to_integer_with_truncation(agent, minutes.get(agent), gc.reborrow()).unbind()?, ) + .unwrap_or(u8::MAX) }; // 4. If second is undefined, set second to 0; else set second to ? ToIntegerWithTruncation(second). - let s = if seconds.get(agent).is_undefined() { - Ok(0) + let second = if seconds.get(agent).is_undefined() { + 0 } else { u8::try_from( to_integer_with_truncation(agent, seconds.get(agent), gc.reborrow()).unbind()?, ) + .unwrap_or(u8::MAX) }; // 5. If millisecond is undefined, set millisecond to 0; else set millisecond to ? ToIntegerWithTruncation(millisecond). - let ms = if milliseconds.get(agent).is_undefined() { - Ok(0) + let millisecond = if milliseconds.get(agent).is_undefined() { + 0 } else { u16::try_from( to_integer_with_truncation(agent, milliseconds.get(agent), gc.reborrow()) .unbind()?, ) + .unwrap_or(u16::MAX) }; // 6. If microsecond is undefined, set microsecond to 0; else set microsecond to ? ToIntegerWithTruncation(microsecond). - let mis = if microseconds.get(agent).is_undefined() { - Ok(0) + let microsecond = if microseconds.get(agent).is_undefined() { + 0 } else { u16::try_from( to_integer_with_truncation(agent, microseconds.get(agent), gc.reborrow()) .unbind()?, ) + .unwrap_or(u16::MAX) }; // 7. If nanosecond is undefined, set nanosecond to 0; else set nanosecond to ? ToIntegerWithTruncation(nanosecond). - let ns = if nanoseconds.get(agent).is_undefined() { - Ok(0) + let nanosecond = if nanoseconds.get(agent).is_undefined() { + 0 } else { u16::try_from( to_integer_with_truncation(agent, nanoseconds.get(agent), gc.reborrow()) .unbind()?, ) + .unwrap_or(u16::MAX) }; // 8. If IsValidTime(hour, minute, second, millisecond, microsecond, nanosecond) is false, throw a RangeError exception. // 9. Let time be CreateTimeRecord(hour, minute, second, millisecond, microsecond, nanosecond). - let time = if let ( - Ok(hour), - Ok(minute), - Ok(second), - Ok(millisecond), - Ok(microsecond), - Ok(nanosecond), - ) = (h, m, s, ms, mis, ns) - { - temporal_rs::PlainTime::try_new( - hour, - minute, - second, - millisecond, - microsecond, - nanosecond, - ) - .map_err(|err| temporal_err_to_js_err(agent, err, gc.nogc())) - .unbind()? - } else { - return Err(agent.throw_exception_with_static_message( - ExceptionType::RangeError, - "not a valid time", - gc.into_nogc(), - )); - }; + let time = temporal_rs::PlainTime::try_new( + hour, + minute, + second, + millisecond, + microsecond, + nanosecond, + ) + .map_err(|err| temporal_err_to_js_err(agent, err, gc.nogc())) + .unbind()?; // 10. Return ? CreateTemporalTime(time, NewTarget). create_temporal_plain_time(agent, time, Some(new_target.get(agent)), gc).map(Value::from) From e1a2cc9533e1877c4a9ea837d346989ea2ead514 Mon Sep 17 00:00:00 2001 From: jesperkha Date: Mon, 9 Mar 2026 14:44:07 +0100 Subject: [PATCH 5/6] Remove std::u8 import --- .../builtins/temporal/plain_time/plain_time_constructor.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_constructor.rs b/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_constructor.rs index bd87f7150..896d42612 100644 --- a/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_constructor.rs +++ b/nova_vm/src/ecmascript/builtins/temporal/plain_time/plain_time_constructor.rs @@ -2,8 +2,6 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use std::u8; - use crate::{ ecmascript::{ Agent, ArgumentsList, BUILTIN_STRING_MEMORY, Behaviour, Builtin, From 4d4080d23fc6ed7efafb497dbd84fefb321d49b6 Mon Sep 17 00:00:00 2001 From: Aapo Alasuutari Date: Mon, 9 Mar 2026 18:56:59 +0200 Subject: [PATCH 6/6] chore(test262): Update expectations --- tests/expectations.json | 3 --- tests/metrics.json | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/expectations.json b/tests/expectations.json index d1585ef0b..a3d0d1bb1 100644 --- a/tests/expectations.json +++ b/tests/expectations.json @@ -3893,14 +3893,12 @@ "built-ins/Temporal/PlainTime/from/prop-desc.js": "FAIL", "built-ins/Temporal/PlainTime/from/subclassing-ignored.js": "FAIL", "built-ins/Temporal/PlainTime/from/year-zero.js": "FAIL", - "built-ins/Temporal/PlainTime/hour-undefined.js": "FAIL", "built-ins/Temporal/PlainTime/infinity-throws-rangeerror.js": "FAIL", "built-ins/Temporal/PlainTime/microsecond-undefined.js": "FAIL", "built-ins/Temporal/PlainTime/millisecond-undefined.js": "FAIL", "built-ins/Temporal/PlainTime/minute-undefined.js": "FAIL", "built-ins/Temporal/PlainTime/nanosecond-undefined.js": "FAIL", "built-ins/Temporal/PlainTime/negative-infinity-throws-rangeerror.js": "FAIL", - "built-ins/Temporal/PlainTime/negative-zero.js": "FAIL", "built-ins/Temporal/PlainTime/prototype/add/argument-duration-max.js": "FAIL", "built-ins/Temporal/PlainTime/prototype/add/argument-duration-out-of-range.js": "FAIL", "built-ins/Temporal/PlainTime/prototype/add/argument-duration-precision-exact-numerical-values.js": "FAIL", @@ -6651,7 +6649,6 @@ "staging/sm/ArrayBuffer/slice-species.js": "FAIL", "staging/sm/AsyncGenerators/for-await-of-error.js": "CRASH", "staging/sm/BigInt/Number-conversion-rounding.js": "FAIL", - "staging/sm/BigInt/large-bit-length.js": "TIMEOUT", "staging/sm/Date/dst-offset-caching-1-of-8.js": "TIMEOUT", "staging/sm/Date/dst-offset-caching-2-of-8.js": "TIMEOUT", "staging/sm/Date/dst-offset-caching-3-of-8.js": "TIMEOUT", diff --git a/tests/metrics.json b/tests/metrics.json index 8c4bb81e0..03b9dac85 100644 --- a/tests/metrics.json +++ b/tests/metrics.json @@ -1,10 +1,10 @@ { "results": { "crash": 52, - "fail": 6931, - "pass": 40369, + "fail": 6920, + "pass": 40380, "skip": 3326, - "timeout": 19, + "timeout": 18, "unresolved": 37 }, "total": 50733