Skip to content

fix(runtime): #5587 — subclassing a Temporal.<Type> gives the instance the Temporal brand - #5672

Merged
proggeramlug merged 2 commits into
mainfrom
worktree-fix-5587-temporal-subclassing
Jun 25, 2026
Merged

fix(runtime): #5587 — subclassing a Temporal.<Type> gives the instance the Temporal brand#5672
proggeramlug merged 2 commits into
mainfrom
worktree-fix-5587-temporal-subclassing

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

What

Fixes the dominant cluster behind #5587: built-ins/Temporal/**/subclassing-ignored.js (36 TypeError: value is not a function failures).

class X extends Temporal.Duration { constructor(){ super(...args) } } produced an empty plain object. super() ran the native Temporal constructor — which returns a fresh NaN-boxed cell rather than mutating the implicit this — and discarded the returned cell. The subclass instance had no Temporal brand, so instance.abs() resolved abs to undefined and threw.

How

Mirrors the existing class X extends Request/Response fetch-handle pattern: when a super() parent resolves to a Temporal constructor, run it and stash the returned cell on this under __perry_temporal_cell__. Both super() lowerings are covered:

  • js_fetch_or_value_super — non-spread super(a, b)
  • js_super_construct_applysuper(...spread) (the form the test262 helper uses), via the decl-time-recorded dynamic parent value

Method-call (native_call_method), property-get (get_field_by_name_tail), and instanceof dispatch recover the cell from there.

Also fixes the surrounding real-cell gaps the same tests exercise:

  • Object.getPrototypeOf(temporalCell) returned null; now resolves Temporal.<Type>.prototype via the live namespace, so assert.sameValue(Object.getPrototypeOf(result), construct.prototype) holds.
  • Reading a prototype method as a value (d.abs, not d.abs()) returned undefined, breaking the instance[method](...spread) read+apply form; now returns a bound method, gated on a real per-kind method predicate so unknown properties still read as undefined. Applies to real cells and subclass instances.
  • Construct-only Temporal ctors (PlainDate/PlainTime/Instant/…) threw "requires 'new'" when invoked from super(); new.target is now set for the call.

Verification

  • New regression test crates/perry/tests/issue_5587_temporal_subclass.rs (2 tests, top-level subclasses of Temporal.Duration and Temporal.PlainDate) — pass.
  • Existing subclass/super/array integration tests (issue_4908_subclass_native_member_base, issue_array_subclass_super_init) — pass (no regression).
  • Manual: top-level subclasses of Duration / PlainDate / Instant now have working instanceof, accessor getters, methods (fused / computed / spread / read-as-value), and getPrototypeOf.

Known remaining limitation (out of scope)

A class method/constructor defined inside a function captures an enclosing let by value, not by cell — so the test262 helper's assert.sameValue(called, 1) still fails for the in-function subclasses it builds. This is a general class-semantics bug (reproduces with a plain user-class subclass — let c=0; class K extends B { constructor(){ c++; super() } }; new K() leaves c === 0), unrelated to Temporal, and deserves its own fix. With this PR, the subclassing-ignored cases now run all the way through the Temporal logic and fail only on that called assertion (previously they threw value is not a function immediately).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Improved Temporal subclass support for super() calls, inherited prototype resolution, instanceof, and value-based method access.
  • Bug Fixes
    • Fixed cases where subclassed Temporal instances could lose their internal brand, fail inherited getters/methods, or return incorrect prototype objects.
  • Tests
    • Added regression tests covering Temporal.Duration and Temporal.PlainDate subclasses, including an aliasing super(...) edge case.

… the instance the Temporal brand

`class X extends Temporal.Duration { constructor(){ super(...args) } }` produced an
empty plain object: `super()` ran the native Temporal constructor (which returns a
fresh NaN-boxed cell rather than mutating the implicit `this`) and DISCARDED the
returned cell. The subclass instance then had no brand, so `instance.abs()` resolved
`abs` to `undefined` and threw `TypeError: value is not a function` — the dominant
failure (36 cases) in the test262 `built-ins/Temporal/**/subclassing-ignored.js`
cluster.

Mirror the existing `class X extends Request/Response` fetch-handle pattern: when a
`super()` parent resolves to a Temporal constructor, run it and stash the returned
cell on `this` under `__perry_temporal_cell__`. Both `super()` lowerings are covered
— `js_fetch_or_value_super` (non-spread `super(a, b)`) and `js_super_construct_apply`
(the `super(...spread)` form the helper actually uses, via the decl-time-recorded
dynamic parent value). Method-call, property-get, and `instanceof` dispatch recover
the cell from there.

Also fixes the surrounding real-cell gaps the same tests exercise:
- `Object.getPrototypeOf(temporalCell)` returned `null`; now resolves
  `Temporal.<Type>.prototype` via the live namespace, so
  `assert.sameValue(Object.getPrototypeOf(result), construct.prototype)` holds.
- Reading a prototype method as a value (`d.abs`, not `d.abs()`) returned
  `undefined`, breaking the `instance[method](...spread)` read+apply form; now
  returns a bound method (gated on a real per-kind method predicate so unknown
  properties still read as `undefined`). Applies to real cells and subclass
  instances.
- Construct-only Temporal ctors (`PlainDate`/`PlainTime`/`Instant`/…) threw
  "requires 'new'" when invoked from `super()`; `new.target` is now set for the call.

Known remaining limitation (separate, non-Temporal bug): a class method/constructor
defined inside a function captures an enclosing `let` by value, not by cell, so the
helper's `assert.sameValue(called, 1)` still fails for in-function subclasses. This
affects all classes (reproduces with a plain user-class subclass), not just Temporal,
and is out of scope here; with it the subclassing-ignored cases now run all the way
through the Temporal logic and fail only on that `called` assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a43d7cc-626b-4048-ae0b-2cccbed70373

📥 Commits

Reviewing files that changed from the base of the PR and between e014e82 and bfc847e.

📒 Files selected for processing (6)
  • crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-runtime/src/object/global_this/fetch_globals.rs
  • crates/perry-runtime/src/object/instanceof.rs
  • crates/perry-runtime/src/temporal/dispatch.rs
  • crates/perry/tests/issue_5587_temporal_subclass.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/perry-runtime/src/object/global_this.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/perry-runtime/src/object/instanceof.rs
  • crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
  • crates/perry-runtime/src/object/global_this/fetch_globals.rs
  • crates/perry-runtime/src/temporal/dispatch.rs

📝 Walkthrough

Walkthrough

Adds Temporal subclass construction, prototype lookup, instanceof, property reads, and method dispatch support. It also adds Temporal-specific helper lookup functions and regression tests covering Temporal.Duration and Temporal.PlainDate subclasses.

Changes

Temporal subclass support

Layer / File(s) Summary
Temporal helpers and exports
crates/perry-runtime/src/object/field_get_set.rs, crates/perry-runtime/src/temporal/dispatch.rs, crates/perry-runtime/src/object/global_this.rs, crates/perry-runtime/src/object/global_this/math_temporal.rs
Adds the Temporal subclass cell field, method allowlists, live Temporal.<Type>.prototype lookup, and re-exports for the new helper entry points.
Temporal super construction
crates/perry-runtime/src/object/global_this/fetch_globals.rs, crates/perry-runtime/src/object/class_constructors.rs
Adds Temporal-specific super() handling that invokes the Temporal parent constructor and stores the returned cell on the subclass instance.
Prototype and instanceof resolution
crates/perry-runtime/src/object/object_ops/prototype.rs, crates/perry-runtime/src/object/instanceof.rs
Resolves Temporal subclass prototypes through the runtime namespace and extends instanceof to recognize stashed Temporal subclass cells.
Property and method dispatch
crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs, crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs, crates/perry-runtime/src/object/native_call_method.rs
Routes Temporal subclass property reads through the stashed cell, returns bound methods for Temporal method names, and dispatches native calls through Temporal method handling.
Regression tests
crates/perry/tests/issue_5587_temporal_subclass.rs
Adds regression tests for Temporal.Duration and Temporal.PlainDate subclasses across inherited fields, instanceof, computed method reads, and returned prototype types.

Sequence Diagram(s)

sequenceDiagram
  participant js_fetch_or_value_super
  participant temporal_subclass_super as crate::object::global_this::fetch_globals::temporal_subclass_super
  participant temporal_constructor
  participant attach_temporal_cell_to_this

  js_fetch_or_value_super->>temporal_subclass_super: handle Temporal subclass super()
  temporal_subclass_super->>temporal_constructor: invoke Temporal parent constructor
  temporal_constructor-->>temporal_subclass_super: returned cell
  temporal_subclass_super->>attach_temporal_cell_to_this: stash cell on subclass this
  attach_temporal_cell_to_this-->>js_fetch_or_value_super: handled
Loading
sequenceDiagram
  participant get_field_by_name_object_tail
  participant temporal_subclass_cell
  participant has_method as crate::temporal::dispatch::has_method
  participant js_class_method_bind

  get_field_by_name_object_tail->>temporal_subclass_cell: read subclass cell
  temporal_subclass_cell-->>get_field_by_name_object_tail: Option<f64>
  get_field_by_name_object_tail->>has_method: check Temporal method name
  has_method-->>get_field_by_name_object_tail: true / false
  get_field_by_name_object_tail->>js_class_method_bind: bind method name
  js_class_method_bind-->>get_field_by_name_object_tail: callable
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PerryTS/perry#5613: Both PRs touch js_fetch_or_value_super super-dispatch; this one adds Temporal-specific handling there.

Poem

I thump through Temporal fields so neat,
With subclass brands that stay complete.
My little paws bind methods bright,
And instanceof hops true at night.
🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the fix, but it misses the required template sections like Summary, Changes, Related issue, Test plan, and Checklist. Reformat it to the repo template and add Summary, Changes, Related issue, Test plan with commands, Screenshots/output, and Checklist items.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main fix: Temporal subclass instances keep the Temporal brand.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-5587-temporal-subclassing

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/object/global_this/fetch_globals.rs`:
- Around line 477-486: The non-spread Temporal superclass path in
fetch_globals.rs is only checking the immediate parent_val, so aliased or stale
Temporal parents can bypass temporal_subclass_super and fall through to the
generic super handling. Update the branch in fetch_globals::global_this to
recover the dynamic parent from the subclass instance’s class id first,
mirroring the spread-path alias recovery, then pass that resolved parent into
temporal_subclass_super so Temporal new.target and stash handling are preserved
for aliased heritage values.

In `@crates/perry-runtime/src/object/instanceof.rs`:
- Around line 121-129: The Temporal brand check in instanceof logic only
recognizes the 0x7FFD pointer-tagged receiver form, so module-scoped raw I64
heap objects can incorrectly fail the subclass check. Update the receiver
handling in the instanceof path around the
value.to_bits()/temporal_subclass_cell flow to also accept the top16 == 0
raw-I64 representation, then perform the same temporal_kind(cell) comparison for
that case so Temporal.<Type> subclass instances are branded correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8c93270b-4c3f-4ccd-a1be-a8c03a9efe90

📥 Commits

Reviewing files that changed from the base of the PR and between a8f3342 and e014e82.

📒 Files selected for processing (12)
  • crates/perry-runtime/src/object/class_constructors.rs
  • crates/perry-runtime/src/object/field_get_set.rs
  • crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
  • crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs
  • crates/perry-runtime/src/object/global_this.rs
  • crates/perry-runtime/src/object/global_this/fetch_globals.rs
  • crates/perry-runtime/src/object/global_this/math_temporal.rs
  • crates/perry-runtime/src/object/instanceof.rs
  • crates/perry-runtime/src/object/native_call_method.rs
  • crates/perry-runtime/src/object/object_ops/prototype.rs
  • crates/perry-runtime/src/temporal/dispatch.rs
  • crates/perry/tests/issue_5587_temporal_subclass.rs

Comment thread crates/perry-runtime/src/object/global_this/fetch_globals.rs
Comment thread crates/perry-runtime/src/object/instanceof.rs
…rs) + rustfmt/clippy

- fetch_globals: non-spread `super()` now recovers the Temporal parent from the
  decl-time class-id stash when the immediate heritage value is a stale alias
  (`const D = Temporal.Duration; class X extends D`), mirroring the
  Request/Response recovery. (CodeRabbit major)
- instanceof: the Temporal subclass-brand check now also accepts the raw-I64
  receiver form (top16 == 0), how module-level object vars are stored, not just
  the NaN-boxed 0x7FFD form. (CodeRabbit minor)
- has_method: `matches!(x, Some(_))` → `.is_some()`; rustfmt.
- New regression test for the aliased-heritage path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@proggeramlug
proggeramlug merged commit 3bee36c into main Jun 25, 2026
27 of 28 checks passed
@proggeramlug
proggeramlug deleted the worktree-fix-5587-temporal-subclassing branch June 25, 2026 05:47
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