Skip to content

fix(runtime): Temporal.PlainTime/PlainYearMonth/PlainMonthDay/Now test262 parity (33.6%→82.1%)#4818

Merged
proggeramlug merged 1 commit into
mainfrom
temporal-pt-parity
Jun 9, 2026
Merged

fix(runtime): Temporal.PlainTime/PlainYearMonth/PlainMonthDay/Now test262 parity (33.6%→82.1%)#4818
proggeramlug merged 1 commit into
mainfrom
temporal-pt-parity

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Raises built-ins/Temporal test262 parity dramatically. The fixes split into Temporal-specific work (my assigned types) and cross-cutting fixes that lift every Temporal type (and general codegen):

Scope Before After Δ
Assigned dirs (PlainTime/YearMonth/MonthDay/Now/toStringTag) 33.6% (426/1269) 86.8% (1101/1269) +675
Full built-ins/Temporal (all 9 types) 33.1% (1525/4603) 74.9% (3449/4603) +1924

Zero regressions across the full built-ins/Temporal suite (verified by diffing failure sets against the origin/main baseline binary), and zero non-Temporal regressions across a built-ins language shard-0/12 run. Scored Perry-only (self-validating; the Node oracle has no Temporal). Sweep jobs=16 timeout=90 on the shared Linux box (contention at higher jobs yields spurious "Wrote executable" compile-fails — all real failures are runtime-fails).

Root causes fixed

  1. instanceof for Temporal values (~241) — Temporal cells are NaN-boxed with no JS class chain, so x instanceof Temporal.PlainTime was always false (every TemporalHelpers.assert* opens with it). temporal_ctor_kind (matches the ctor closure's func_ptr) wired into js_instanceof_dynamic; Temporal.X member RHS routed through the dynamic-instanceof path in HIR lowering.

  2. Closure-call wrappers dropped args beyond the 5th (cross-cutting general codegen fix) — __perry_wrap_* wrappers hardcoded a 5-arg signature (stale "js_closure_call only goes up to 5" comment). Any function/method with >5 params invoked as a value or via the method-dispatch tower (obj.m(...), apply, callbacks) silently zeroed args 6+. Surfaced via assertDuration (12 params) / assertPlainTime (7). Cap raised to 16 (4 wrapper sites) + js_native_call_value's per-arity dispatch extended 8→16 (+array fallback). The dispatch tower already pads short calls to the declared arity.

  3. Real Temporal.<Type>.prototype objects (~176) — prototypes were empty, so prop-desc / branding / length / name / builtin / not-a-constructor introspection all failed. Populated each prototype (all 9 types) with brand-checking accessor getters and method functions (spec .length/.name, non-constructable) sharing two generic thunks, plus @@toStringTag + constructor. Built without touching globalThis (population runs inside the singleton init where GLOBAL_THIS_READY is still false — the obvious js_function_prototype_value_for_read route deadlocks on js_get_global_this); registered in the synthetic-class-id prototype cache so new Temporal.X() doesn't overwrite it.

  4. Options / coercion spec compliance (~150+, all types) — toString honors its options (calendarName / fractionalSecondDigits / smallestUnit / roundingMode, plus ZDT offset/timeZoneName and Instant timeZone) across Duration/PlainDate/PlainDateTime/PlainTime/PlainYearMonth/PlainMonthDay/ZonedDateTime/Instant; until/since/add/subtract/round/with/from parse their option bags; GetOption ToString-coerces present-but-wrong-type values (number→RangeError, Symbol→TypeError) instead of ignoring them; roundingIncrement validates NaN/Infinity/0/0.9/1e9+1; GetOptionsObject rejects primitives; ToTemporalX of a non-string primitive / Symbol / empty property bag → TypeError; non-finite numeric fields → RangeError; from() applies overflow (constrain default); YearMonth/MonthDay coerce via from_partial (monthCode support); calendar must be a string. Fixed .toString(optionsObj) mis-routing to the numeric toString(radix) path for Temporal receivers.

  5. Cell immutability / getPrototypeOf — writes to a Temporal cell are no-ops (fixes segfaults from subclassing probes that did instance.constructor = …); Object.getPrototypeOf(temporalValue) returns its prototype via a per-kind class-id registry instead of derefing the cell.

Key files

  • crates/perry-codegen/src/codegen/artifacts.rs (wrapper arity 5→16), expr/instance_misc1.rs, crates/perry-hir/src/lower/lower_expr.rs (instanceof routing)
  • crates/perry-runtime/src/closure/dispatch.rs (arity 8→16), value/to_string.rs (radix routing), value/dyn_index.rs + object/field_set_by_name.rs (cell-write no-op), object/object_ops.rs (getPrototypeOf)
  • crates/perry-runtime/src/object/{instanceof.rs, global_this.rs, temporal_proto.rs (new)}
  • crates/perry-runtime/src/temporal/* (options + all type modules)

Remaining (deferred)

order-of-operations observable read-ordering; leap-second string parsing; @@toStringTag writable descriptor (pre-existing symbol-attr staleness, affects Now too); Temporal subclassing/@@species; PlainTime/YM/MD valueOf interception; a few calendar/field edge cases.

@proggeramlug proggeramlug force-pushed the temporal-pt-parity branch 2 times, most recently from ec460a1 to debbb67 Compare June 9, 2026 06:47
@proggeramlug proggeramlug merged commit 5cee242 into main Jun 9, 2026
13 checks passed
@proggeramlug proggeramlug deleted the temporal-pt-parity branch June 9, 2026 07:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant