Skip to content

fix(runtime): subclasses of built-ins inherit Symbol.species (#11193) - #11525

Merged
proggeramlug merged 2 commits into
mainfrom
claude/dreamy-davinci-hgybgs
Sep 27, 2026
Merged

proggeramlug merged 2 commits into
mainfrom
claude/dreamy-davinci-hgybgs

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The issue's own repro already matches Node on main: #11198 added Buffer[Symbol.species] (FastBuffer) and #11338 added the accessors on the built-in constructors. The part still open, noted in #11338, was user subclasses. For class X extends Array {} (and Map, Set, Promise, RegExp, ArrayBuffer, Uint8Array, …), X[Symbol.species] was still undefined. This PR makes it answer X.

Finding the species made RegExp split and matchAll construct through the subclass. That exposed an older bug: constructing a RegExp, typed-array or ArrayBuffer subclass with no constructor of its own through a value never built the built-in. So this PR also fixes that; without it, "a,b".split(new MyRegExp(",")) would have started throwing.

Changes

  • X[Symbol.species] on subclasses
    • symbol/get.rs (class-ref arm of js_object_get_symbol_property_with_receiver): after the existing user-parent, class-expression-parent and function-parent steps, the lookup now reaches the built-in constructor the chain ends in. It reads the symbol there with the original receiver, so the inherited get [Symbol.species] returns the subclass.
    • Prototype refs share the 0x7FFE tag and are excluded, so X.prototype[Symbol.species] stays undefined.
    • object/class_registry/state.rs: builtin_parent_ctor_in_chain walks get_parent_class_id and returns the first stashed parent value that identify_global_builtin_constructor recognizes. It reuses the value js_register_class_parent_dynamic already stashes, so there is no new table and no new root holder.
  • Implicit constructor over an exotic built-in
    • codegen/method.rs: the synthesized standalone <Class>_constructor of a class with no constructor of its own skipped an exotic built-in base as "uncallable". That symbol is what every dynamic construct replays (new (R as any)(…), Reflect.construct, a species Construct), so those got a plain object with no [[RegExpMatcher]] or buffer.
    • It now emits js_builtin_subclass_construct(class_id, base, args) and rebinds this, as the inline new R() and an explicit super() already do. Class fields still initialize afterwards on the real instance.
    • lower_call/new_helpers.rs: exotic_builtin_base_in_chain covers RegExp, ArrayBuffer and the typed arrays. It shares the constructor-free chain walk with native_instance_base_in_chain, now factored out as ctorless_builtin_base. SharedArrayBuffer is left out because that constructor already reaches it through the dynamic-parent super dispatch.
  • New gap test: test-files/test_gap_11193_subclass_species_inherited.ts.

Still open, unchanged by this PR (same output before and after):

  • The Array and typed-array species consumers (map, filter, slice, …) still return plain results for subclass instances, because their default fast paths never read constructor.
  • A dynamic new (A as any)(3) of an Array subclass still has length 0.
  • flatMap on an Array-subclass instance is still "not a function".

Related issue

Fixes #11193. The Buffer half landed in #11198 and the built-in accessors in #11338.

Test plan

Linux x64, perry-dev builds of this branch and of main (63e8997), made with cargo build --profile perry-dev -p perry -p perry-runtime-static -p perry-stdlib-static, run with PERRY_NO_AUTO_OPTIMIZE=1 PERRY_NO_CACHE=1, and each arm linking its own runtime archives.

  • The issue's repro is byte-identical to Node on both main and this branch.
  • New gap test:
    • main: every subclass row reads species is ctor: false typeof: undefined, then it throws test is not a function.
    • This branch: byte-identical to Node.
  • test_gap_11193_builtin_species_accessor.ts (from fix(runtime): install get [Symbol.species] on built-in constructors (#11193) #11338) is still byte-identical to Node.
  • A/B, main vs this branch: 213 test-files/*.ts, every test that mentions extends <built-in>, Symbol., species or Reflect.construct.
    • 212 have identical output. The only diff is the new test.
    • Three http/net tests first failed to compile. Four parallel no-auto runtime rebuilds were racing, and one arm hit a stale-archive source-hash check. All three were rerun one at a time and are identical.
  • cargo test --profile perry-dev -p perry-codegen --lib: 1755 passed.
  • RUST_TEST_THREADS=1 cargo test --profile perry-dev -p perry-runtime --lib -- symbol class_registry species regex: 213 passed.
  • cargo fmt --all -- --check, scripts/check_file_size.sh, scripts/addr_class_inventory.py and scripts/gc_runtime_root_holders.py are clean.

Not run: the full gap suite, test262, cargo test --workspace, auto-optimize builds and macOS. The local oracle was Node 22, not the pinned 26.5.1; the A/B compares the two Perry builds directly, so the Node version doesn't affect it.

  • Perry-dev build clean (with LLVM 22.1.8)
  • cargo test --workspace (left to CI)
  • Added a test under test-files/
  • Updated docs/src/ (n/a)

Checklist

  • I have NOT bumped the workspace version or edited CLAUDE.md / CHANGELOG.md (maintainer handles these at merge)
  • My commits follow the fix: prefix convention

Generated by Claude Code

`class X extends Array {}` read `X[Symbol.species]` as undefined: a class
ref's symbol lookup never reached the built-in constructor its chain ends in,
because a built-in parent is recorded only as a reserved class id. Walk the
chain for the stashed built-in parent value and read the symbol off it with
the original receiver, so the inherited getter answers the subclass.

With the species found, RegExp split/matchAll construct through the subclass,
which exposed that the synthesized default constructor of a class extending
an exotic built-in (RegExp, ArrayBuffer, typed arrays) never constructed the
built-in. Every dynamic construct of such a class got a plain object with no
[[RegExpMatcher]]. Emit the built-in's Construct there, as the inline `new`
and an explicit `super()` already do.
@coderabbitai

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: ae5802ae-868c-4027-a0df-7a87289dc2d6

📥 Commits

Reviewing files that changed from the base of the PR and between 0dc6287 and 848f703.

📒 Files selected for processing (8)
  • changelog.d/11525-subclass-species-inherited.md
  • crates/perry-codegen/src/codegen/method.rs
  • crates/perry-codegen/src/lower_call/mod.rs
  • crates/perry-codegen/src/lower_call/new_helpers.rs
  • crates/perry-runtime/src/object/class_registry.rs
  • crates/perry-runtime/src/object/class_registry/state.rs
  • crates/perry-runtime/src/symbol/get.rs
  • test-files/test_gap_11193_subclass_species_inherited.ts
 _________________________________________________
< This code is so clever it forgot to be correct. >
 -------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

proggeramlug pushed a commit that referenced this pull request Sep 27, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DJy7JpaPhRaPy7vPbfJJh
@proggeramlug
proggeramlug force-pushed the claude/dreamy-davinci-hgybgs branch from 3adc1ac to 848f703 Compare September 27, 2026 13:02
@proggeramlug
proggeramlug merged commit c66f7c4 into main Sep 27, 2026
22 of 23 checks passed
@proggeramlug
proggeramlug deleted the claude/dreamy-davinci-hgybgs branch September 27, 2026 13:02
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.

Symbol.species is undefined on every built-in constructor; Buffer[Symbol.species] (FastBuffer) missing breaks undici's llhttp callbacks

2 participants