Skip to content

fix(runtime): resolve the subclass in Promise SpeciesConstructor (#5907)#6702

Merged
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:fix/5907-promise-species-subclass
Jul 19, 2026
Merged

fix(runtime): resolve the subclass in Promise SpeciesConstructor (#5907)#6702
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:fix/5907-promise-species-subclass

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Promise.prototype.{then,catch,finally} run SpeciesConstructor(receiver, %Promise%), which reads receiver.constructor[Symbol.species]. Perry installs no Promise[Symbol.species] accessor, so that read always returned undefined and SpeciesConstructor fell back to the intrinsic %Promise%. The effect: every class X extends Promise chain collapsed onto the native fast path and never routed through NewPromiseCapability(X), so subclass then/catch/finally produced the wrong number of chained promises.

Root cause

In promise_species_constructor (crates/perry-runtime/src/promise/then.rs), step 5 mapped an undefined species to %Promise%. Since the standard Promise[Symbol.species] getter (which returns this) is never installed, the read is always undefined, so the subclass was lost and the fast path was always taken.

Fix

Emulate the standard getter directly in promise_species_constructor: an absent @@species on a Promise-branded constructor resolves to C itself — the value real engines observe from the inherited getter. Now:

  • the intrinsic Promise still resolves to %Promise% (fast path unchanged for plain promises);
  • a class X extends Promise resolves to X, so X.prototype.{then,catch,finally} chain a subclass promise via NewPromiseCapability(X) (ECMA-262 27.2.5.4.1);
  • the emulation is guarded to Promise/subclass constructors — a reassigned non-Promise .constructor carries no such inherited getter and keeps the %Promise% default;
  • an explicit null species still yields the spec default.

Validation

scripts/test262_subset.py --dir built-ins/Promise (serial):

pass diff runtime-fail
before 563 3 8
after 565 2 7

Newly passing (0 regressions, diffed by test path):

  • built-ins/Promise/prototype/finally/subclass-resolve-count.js
  • built-ins/Promise/prototype/finally/subclass-reject-count.js

Also verified: 14 promise/async gap tests still match the Node oracle byte-for-byte; the promise-scoped perry-runtime unit tests pass; rustfmt --check clean on the changed file.

Code-only PR (no version bump / CHANGELOG / CLAUDE.md — maintainer folds metadata). Refs #5907.

The deeper remaining built-ins/Promise failures the issue tracks (subclass constructors that override this by returning a plain object — then/capability-executor-*, then/deferred-is-resolved-value — and the per-step observable PromiseResolve count in finally/{resolved,rejected}-observable-then-calls-PromiseResolve) are out of scope here; they need derived-constructor return-value semantics and the full finally routing, respectively.

Summary by CodeRabbit

  • Bug Fixes
    • Improved Promise.prototype.then handling of @@species by correctly distinguishing null vs undefined reads.
    • null @@species continues to fall back to the intrinsic %Promise%.
    • undefined @@species now resolves to the receiver constructor only when it’s Promise-branded; otherwise it falls back to %Promise%, improving compatibility with Promise subclasses.

…ryTS#5907)

`Promise.prototype.{then,catch,finally}` run SpeciesConstructor(receiver,
%Promise%), which reads `receiver.constructor[Symbol.species]`. Perry installs
no `Promise[Symbol.species]` accessor, so that read was always `undefined` and
SpeciesConstructor fell back to the intrinsic %Promise% — collapsing every
`class X extends Promise` chain onto the native fast path and never routing
through NewPromiseCapability(X).

Emulate the standard getter (which returns `this`) directly in
`promise_species_constructor`: an absent `@@species` on a Promise-branded
constructor resolves to `C` itself, so subclass then/catch/finally chain a
subclass promise via NewPromiseCapability. Restricted to Promise/subclass
constructors — a reassigned non-Promise `.constructor` keeps the %Promise%
default — and an explicit `null` species still yields the spec default.

Fixes built-ins/Promise/prototype/finally/subclass-{resolve,reject}-count
(test262). built-ins/Promise: 563 -> 565 pass, zero regressions.

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

coderabbitai Bot commented Jul 19, 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: f0e463ae-76b0-40b3-acc2-ce3284b3992a

📥 Commits

Reviewing files that changed from the base of the PR and between e411836 and 09e7cf8.

📒 Files selected for processing (1)
  • crates/perry-runtime/src/promise/then.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/perry-runtime/src/promise/then.rs

📝 Walkthrough

Walkthrough

Promise species resolution now detects Promise-branded constructors, including Promise subclasses. Undefined species returns the receiver constructor only for branded constructors, while null and non-Promise constructors fall back to the intrinsic Promise.

Changes

Promise species resolution

Layer / File(s) Summary
Resolve Promise species constructors
crates/perry-runtime/src/promise/then.rs
Adds Promise-brand detection and updates @@species handling for undefined and null species values.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the Promise species-constructor subclass fix.
Description check ✅ Passed The description is detailed and covers the summary, root cause, fix, validation, and issue reference, though some template sections are omitted.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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: 1

🤖 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/promise/then.rs`:
- Around line 1094-1097: Root the NaN-boxed object value c with
RuntimeHandleScope before the Step 4 @@species lookup, create a handle via
create_handle_f64, and reload c with get_nanbox_f64 after
js_object_get_symbol_property completes. Use the reloaded c for
is_promise_brand_constructor and the return path, preserving existing species
handling.
🪄 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: 881b4a70-5e4b-45b0-aff2-9e533b579f74

📥 Commits

Reviewing files that changed from the base of the PR and between e7626a1 and e411836.

📒 Files selected for processing (1)
  • crates/perry-runtime/src/promise/then.rs

Comment thread crates/perry-runtime/src/promise/then.rs
Address review: a user `Symbol.species` accessor runs arbitrary code that can
allocate and evacuate the (movable, heap-pointer) constructor, so holding the
NaN-boxed `C` across `js_object_get_symbol_property` and reusing it afterward
(the new `undefined`-species emulation returns `C`) risked a stale pointer.
Root `C` with a `RuntimeHandleScope` before the read and reload it from the
rewritten handle. No-op for class-ref constructors (non-pointer ids); built-ins/
Promise stays at 565 pass, zero regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@proggeramlug
proggeramlug merged commit 49f95e8 into PerryTS:main Jul 19, 2026
2 checks passed
@proggeramlug
proggeramlug deleted the fix/5907-promise-species-subclass branch July 19, 2026 20:03
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