Skip to content

runtime: P3 B2, class prototype reads return the materialized prototype; the JS-visible INT32-space prototype word is gone - #14358

Merged
proggeramlug merged 7 commits into
mainfrom
perf/p3-b2-prototype-word
Nov 9, 2026
Merged

proggeramlug merged 7 commits into
mainfrom
perf/p3-b2-prototype-word

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

P3 B2: a prototype read on a class never returns the INT32-space prototype word. Design:
/root/CHB/DESIGN-p3-canonical-numbers.md rev 3.1 §3 B2 (REVIEW-p3 R4, D3). It follows NANB (eb9ec0d, canonical NaN
at foreign-float boundaries, R1) and B1 (#14354).

What changes

  • The prototype read (object/field_get_set/get_field_by_name.rs): for a registered class, it is now always
    class_decl_prototype_value(class_id).
    • A declared class builds its prototype lazily on that first read, exactly as before.
    • An anonymous-shape id (registered, with an ANONYMOUS declaration, so it has no class holder and no CLASS word:
      !class_identity_has_surface, class_registry/state.rs:1559) reads undefined: the declaration path's own
      answer, decided at the read and never at class registration (D3). A registered id with no declaration still has
      a prototype object, built on the read, which is unchanged.
    • Before this change, that fallback returned 0x7FFE | CLASS_PROTOTYPE_REF_FLAG | id, a JS-visible word in the INT32
      tag space. Top-16-only tests read it as the Number id (REVIEW-p3 H4).
  • GetPrototypeFromConstructor (class_registry/construct.rs new_target_custom_object_prototype): when the class's
    declared prototype is not an object, it returns None, so the default per-kind prototype applies, as the spec says
    for a non-object newTarget.prototype. Before, it returned the same word.
  • Unchanged:
    • The word stays runtime-internal, as a method's home for super and brand lookup (decl_prototype_birth.rs:181,
      symbol_members.rs:65, class_method_values.rs:42, class_ref_values.rs:202, state.rs:1418). It is read only
      by the runtime.
    • The internal 0x7FFE words keep their tag (R4). Renaming them INTERNAL_WORD_TAG is B4b.
    • Declared classes: same objects, same own keys, same laziness.
    • The old decode paths are not deleted. The prototype word is still JS-reachable through bound method closures:
      class_method_values.rs:42 builds a bound method whose receiver is class_prototype_ref_value(owner), decoded at
      closure/dispatch/bound.rs:39. So the ~60 class_prototype_ref_id decoders in JS-operation paths stay live until
      that producer changes, which is a separate decision. Nothing is materialized at registration, so there is
      no startup work.
  • B4a (deleting the producer API) is its own PR, per rev 3.1's phase order.
  • Overlaps:
    • PROTOPAR (builtin prototype own keys) changes builtin prototype setup and the realm declarations. This PR touches
      neither: class prototypes only, read path only. Its own-keys invariant holds here: a class prototype's own keys
      are the declaration's (controls below).
    • HOST3 (performance/Collator) does not overlap.

Tests

  • perry-runtime: object/field_get_set/undeclared_prototype_tests.rs:
    • an anonymous-shape id reads prototype as undefined;
    • a registered class reads it as its prototype object (a pointer that is not an INT32-space word, equal to
      class_decl_prototype_value).
  • parity: tests/parity/language/class/test_p3_b2_class_prototype.ts, compared with node. It covers:
    • C.prototype identity, read before and after the first new;
    • Object.getPrototypeOf(new C) === C.prototype;
    • static initializers (static x = this.prototype, static {});
    • instanceof;
    • derived classes, class expressions and anonymous classes;
    • defineProperty on the prototype, seen through instances;
    • the prototype as a Map key, an object key and an array index;
    • Reflect.construct with a class newTarget;
    • own keys (getOwnPropertyNames, constructor not enumerable);
    • typeof, Number(), String() and + 1 as detectors: a misread INT32 word would show as number.
  • Changelog: runtime-p3-b2-class-prototype-word.md.

Gates (qb9 x86-64; final head 3d4a2f5 = branch + main 43c5093; main artifacts built from 43c5093)

gate result
check -D warnings perry-wpfacts, perry-codegen and perry clean. perry-runtime has 4 dead-code errors, and the perry-stdlib build of perry-runtime adds unused imports; all are in files this PR does not touch and are pre-existing on main (as #14353 noted)
wpfacts 2054/0, plus the NUM seed audit 2/0 (merged head)
codegen lib 2838/0 (final head)
perry-runtime full suite 6605/0, including the 2 new B2 tests (final head)
vf (1538) json=0 abort=0 rc86=0, crc!=0=2 (the main baseline), final head
lost sites tssrc main vs B2: 66666/66666 site lines, diff 0; proven 24680 = 24680; skinbench 335/8021 both
startup Ir, 3 callgrind runs per arm (final head) hello: main 127752 ×3, B2 127752 ×3. hello2: main 633209 ×3, B2 633209 ×3. 300-class program: main 38410357–38413050, B2 38408719–38413047. No increase
.text hello 1238 = 1238; hello2 5926142 → 5926078 (−64 B); classes 6465726 → 6465662 (−64 B). Per symbol: class_value_get_field −35, new_target_custom_object_prototype −11, nothing else. The first cut grew by 1152 B because the shrunk new_target_custom_object_prototype got inlined into its three construct callers (+467, +404, +361). It is now #[inline(never)]: one out-of-line copy
IR census (parity corpus, 2523) IR differs in 0, compile rc in 0, output in 11, none real: 6 vary between runs of one binary in both arms (Date.now, Math.random, a PID, an elapsed-ms line, two counts that swap between runs; issue58 gave 3 distinct outputs in 10 runs on main as well); 5 fail to compile in both arms, so there is no output to compare
class-prototype controls vs node test_p3_b2_class_prototype.ts: main OK, B2 OK
file size clean
chess Linux instructions:u identical: search 37349.4, perft 895.9, evaluate 3183.1, legal 1248.3 M, main = B2 (neutral, as expected)

Files

  • runtime:
    • object/field_get_set/get_field_by_name.rs;
    • object/class_registry/construct.rs;
    • object/native_module/class_ref_values.rs (comment);
    • object/field_get_set.rs (the test module declaration);
    • object/field_get_set/undeclared_prototype_tests.rs (new).
  • parity fixture, changelog.

@coderabbitai

coderabbitai Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Explain Change →

Caution

Review failed

The pull request is closed.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@proggeramlug
proggeramlug marked this pull request as ready for review November 9, 2026 12:31
@proggeramlug
proggeramlug merged commit 7776d5f into main Nov 9, 2026
98 of 102 checks passed
@proggeramlug
proggeramlug deleted the perf/p3-b2-prototype-word branch November 9, 2026 12:32
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