Skip to content

P3 B4a: delete the INT32 Number producer API and the prototype-word decoders - #14372

Merged
proggeramlug merged 7 commits into
mainfrom
perf/p3-b4a-delete-int32-producers
Nov 9, 2026
Merged

proggeramlug merged 7 commits into
mainfrom
perf/p3-b4a-delete-int32-producers

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

P3 B4a: delete the INT32 Number producer API and the JS-path decoders of the prototype word

Part of P3 "canonical Numbers" (DESIGN-p3 rev 3.1 §3 B4a, plus CWN's additions). After B1 (#14354), the exact-int32 predicate (#14353) and B2 (#14358), nothing produces an INT32-tagged JS Number, and C.prototype reads return the prototype object. This PR removes the means to make an INT32 Number, moves the last JS-visible use of the 0x7FFE prototype word (a bound method's receiver) to the prototype object, and deletes every decoder of that word in a JS-operation path.

Changes

1. Producer API (335c8f500b, builds on its own)

  • JSValue::int32 is deleted.
  • Its one non-test caller is the builtin constructor identity capture (alloc_builtin_constructor). It now writes builtin_constructor_identity_word: the same runtime-internal 0x7FFE word the realm image writes for RealmValue::Int32, named as an internal word (R4).
  • perry-runtime-manifest/src/realm.rs:293: RealmValue::Int32 (the constructor identity capture) is labelled Other, not Number.
  • The test helpers that built INT32 Numbers now build canonical doubles (about 55 sites, including perry-stdlib crypto/random.rs). value/tests.rs keeps raw 0x7FFE words where it tests the tag space and drops test_int32.
  • 96da3dd424: the tests of live INT32 consumers (array store canonicalization, the typed-array fast-set decline) still feed a raw 0x7FFE word. A double would not exercise those consumers, which B4b deletes.

2. The bound-method producer (CWN)

  • class_method_values.rs:42 (class_evaluation_method_value_for_name_bytes) built the BOUND_METHOD with the prototype word as its receiver (slot 0). The receiver is now the evaluation's prototype object, the value a C.prototype read returns.
    • The prototype fill (finish_class_evaluation_prototype) passes the prototype it is filling.
    • Other callers build the prototype first, which runs the fill, and then find the value it made.
  • Dispatch (bound.rs:39, :145, :177, :224) reads the owner off the prototype object with class_prototype_object_id. That is a structural answer from class_id_for_decl_prototype_object, not a table. Dispatch then runs the owner's body with the call-time this.
  • The word stays only as a method's internal home capture, under a new name (R4): class_method_home_word / class_method_home_id.
  • An evaluation's prototype now takes its static parent link from the declaration (73395de176, CWN ruling (b) on S7b).
    • The by-name method value needs the evaluation's prototype object, so a method read off an instance before any C.prototype read builds that prototype.
    • For an evaluation with no pinned heritage, class_evaluation_prototype_value (class_object_props.rs:127) copied the parent link from the template's declaration holder. That built the holder, which the S7b invariant forbids.
    • The link now comes from static_parent_prototype_bits, split out of decl_prototype_parent_bits (state.rs:1743) so both prototypes read one function. It returns the declared parent class's prototype, a reserved native parent's, or Object.prototype, and never reads the class's own holder.
    • S7b is unchanged and passes.

3. Deleted decoders

Line numbers are main 7776d5f491.

file:function:line removed
proxy.rs:proxy_arg_is_object:152 the "is an object" arm for the word
weakref.rs:is_valid_weak_target:195 the prototype-word arm of CanBeHeldWeakly
util_inherits.rs:set_super_property:124 not-a-prototype guard
symbol/iterator.rs:js_object_get_own_property_symbols:48 redirect to the decl prototype's own symbols
symbol/get.rs:js_object_get_symbol_property_with_receiver:472 own-only static symbol lookup for the word
symbol/get.rs:js_object_get_symbol_property_with_receiver:480 is_proto_ref_receiver flag; the dynamic- and builtin-parent arms are now unconditional
closure/dispatch/bound.rs:bound_target_declared_name:664 or_else(class_ref_word) fallback, plus the now-dead non-pointer class tail
closure/dispatch/bound.rs:function_bind_observable:901 the class-word bind arm, and target_is_closure with it (a class is always its function object)
object/property_key.rs:set_property_key_resolved:199 guard
object/field_set_by_name.rs:js_object_set_field_by_name:482 is_prototype_ref: the write through class_prototype_set_bytes, and its guards
object/polymorphic_index.rs:js_object_get_index_polymorphic:180 0x7FFE arm of the indexed get
object/class_constructors.rs:capture_owner_for_template:118 prototype early return
object/class_value.rs:class_ref_word:126 0x7FFE arm. It now decodes only a class function object and delegates to class_value_id_bits
object/native_module/class_ref_values.rs:class_ref_id:54 0x7FFE arm. It now delegates to class_value_id
object/mod.rs:232 the class_prototype_ref_id re-export (replaced by class_method_home_id)
object/native_module.rs:is_static_bound_method_value:1527 && class_prototype_ref_id(..).is_none()
object/native_module.rs:canonical_bound_method_receiver:1535 the whole function (its only arm was the word); callers use the captured value
object/delete_rest.rs:js_object_delete_field_value:888 prototype delete (static key)
object/delete_rest.rs:delete_dynamic_value:942 prototype delete (dynamic or symbol key)
object/descriptors.rs:class_ref_wtf8_own_property:18 prototype branch of the WTF-8 own-property lookup
object/descriptors.rs:own_property:602 the prototype-vs-static deleted-key choice (static kept)
object/descriptors.rs:own_property:613, 623, 641, 702 static guards: #x public static field, dynamic static accessor, synthesized name, own static field
object/descriptors.rs:own_property:672 prototype accessor reflection
object/descriptors.rs:own_property:686 prototype method and constructor data descriptors
object/descriptors.rs:js_object_get_own_property_names_shape:1347 the prototype own-names list
object/native_module/class_method_bind.rs:js_class_method_bind:106 the prototype exclusion in receiver_is_constructor_ref
object/native_module/class_method_bind.rs:js_class_method_bind:115 the class_method_slot_value shortcut for the word
object/object_ops/define_property.rs:apply_property_descriptor:533 redirect of defineProperty to the decl prototype
object/object_ops/define_property.rs:apply_property_descriptor:557 the is_static argument (and the instance arm of define_declared_class_accessor)
object/object_ops/define_property.rs:apply_property_descriptor:605, 651 static guards
object/object_ops/define_property.rs:apply_property_descriptor:682 the static-vs-prototype discriminator (#7190)
object/field_get_set/has_property.rs:object_has_property_generic:472 own-only static symbol lookup
object/field_get_set/has_property.rs:object_has_property_generic:525 guard on the Function.prototype fallback for in
object/field_get_set/get_field_by_name.rs:class_value_get_field:122 is_prototype_ref: the prototype constructor, method and accessor reads, and every !is_prototype_ref guard; the parameter of class_value_get_wtf8_field
object/property_ops/metadata.rs:normalize_target_bits:182 fold of the word onto the constructor metadata key
object/property_ops/set.rs:own_set_descriptor:536 && …is_none()
object/property_ops/common.rs:reflect_value_is_object:104 the "is an object" tail for the word
object/descriptors/builders.rs:symbol_own_property_descriptor:58 redirect to the decl prototype's symbol descriptor
object/object_ops/prototype.rs:get_prototype_of_resolved:596, 607 guards
object/object_ops/prototype.rs:get_prototype_of_resolved:645 the root → Object.prototype arm for the word; the whole class_ref_word block (unreachable after the class_value_id block above it), and the is_class_ref primitive guard
object/field_get_set/enumeration.rs:js_object_keys_value:181 guard
object/global_this/array_error.rs:function_prototype_to_string_thunk:670 guard
object/native_call_method/common_methods.rs:dispatch_common:226 guard
object/native_call_method/primitive_methods.rs:class_receiver_arm:75 the vtable-call arm for the word
object/instanceof/proxy_rhs.rs:ordinary_proxy_has_instance:70 && …is_none()
object/object_ops/has_own.rs:object_has_own_generic:187 symbol hasOwn on the decl prototype
object/object_ops/has_own.rs:js_object_property_is_enumerable:637 guard

Helpers left without callers, deleted:

  • canonical_bound_method_receiver
  • delete_class_prototype_key
  • decl_prototype_own_accessor(_bytes)
  • class_chain_getter_value(_bytes)
  • class_static_prototype_is_nulled
  • the class_prototype_set_bytes re-export

Parameters removed:

  • this from dispatch_symbol_bound_method (only static symbol methods are built)
  • is_static from define_declared_class_accessor
  • is_prototype_ref from class_value_get_wtf8_field
  • key_value from class_ref_wtf8_own_property

Kept (R4):

  • class_method_home_id, at js_class_method_home (super, class_super_chain.rs:34) and class_method_value_target (class_ref_values.rs). These are the readers of the method-home capture.
  • contracts.rs:409 proto_class_id, the check-mode classifier, which B4b deletes with the other classifiers.

Tests:

  • Tests that fed the word to a JS operation now use the prototype object (bound_method_receiver_tests, object/tests.rs lex method).
  • class_value/tests.rs asserts that the home word decodes to no class.
  • synthetic_prototype_ref_folds_onto_constructor_key tested only the deleted fold and is removed.

Evidence the decoders were dead

Before deleting anything, a probe build made class_prototype_ref_id print P3B4A-PROBE <caller> on every hit (#[track_caller]). The home readers were already on class_method_home_id.

  • Parity: 8107 programs (all of tests/parity), compiled and run: 0 hits.
  • perry-runtime suite (--nocapture): 2 hits, both from unit tests that build the word by hand and pass it in: the metadata fold test (deleted) and the class_ref_word test (now asserting None).

Controls

tests/parity/language/class/test_p3_b4a_prototype_paths.ts has one block per path:

  • descriptors, defineProperty, hasOwn, delete, symbol get, proxy, weakref (WeakRef/WeakMap/WeakSet), and instanceof (including Symbol.hasInstance);
  • bound methods of class prototypes: C.prototype.m.bind(x) and const f = C.prototype.m; f.call(o) / apply / a bare call;
  • a per-evaluation class (factory), a method copied onto another class's prototype, and super through a method value.

Its output equals node, as does B2's control. One field was dropped from the control: C.prototype.m.bind(o).length is 0 on main and 1 in node. That bug predates this PR and does not sit on a B4a path.

Gates

  • Final head: 01d23ecbeb against main 8d5b774210 (qb9, x86 Linux): the S7b fix plus a merge of main. The rows marked † were re-run there.
  • Other rows: run on de1d335128 against main c30a986394. The S7b fix touches only the evaluation-prototype parent link.
gate result
-D warnings per crate wpfacts, codegen, perry, runtime-manifest: 0. perry-runtime: 4 errors, perry-stdlib: 56. All are pre-existing: none is in a file this PR touches (webassembly_wrapper_state.rs, cell_meta.rs, gc/mark.rs, shapes.rs, plus global_this.rs and others under stdlib)
wpfacts suite 2070 passed, 0 failed
codegen suite lib 2838 passed. tests/large_object_barriers.rs::large_local_array_push_inbounds_store_emits_precise_slot_barrier fails on main c30a986394 too (pre-existing; this PR changes no codegen file)
perry-runtime full suite † 6628 passed, 0 failed (S7b included)
runtime-manifest suite 99 passed
controls vs node † test_p3_b4a_prototype_paths OK, test_p3_b2_class_prototype OK
vf † n=1538, json=0, abort=0, rc86=0, crc!=0=2 (the same 2 as B2)
lost sites (tssrc site verdicts) 64169/64169 lines, diff 0, lost 0; proven 24549 of 64169 in both arms
.text hello † 1238 = 1238
.text hello2 † 5913662 → 5902462 (−11200 B, −11033 over 210 symbols)
.text classes (300-class fixture) † 6453246 → 6441918 (−11328 B, −11114 over 212 symbols)
startup Ir hello 127752 ×3, identical
startup Ir hello2 631072 ×3, identical
startup Ir classes main 38383318 / 38383879 / 38382826; branch 38384563 / 38383692 / 38385766. Within noise (below)
IR census, tests/parity/language n=2528, irdiff 0, crcdiff 0, outdiff 11, all explained (below)
chess Ir (CHB-measure) † search 37366.5 → 37366.7 M, perft 895.9 = 895.9, evaluate 3183.1 = 3183.1, legal 1248.2 = 1248.2; fingerprints equal

.text growth, per symbol († run; hello2 and classes are the same)

  • apply_property_descriptor::{closure#42} +12394 is the same closure renumbered: {closure#43} is −12394, net 0.
  • class_evaluation_method_value_for_name +852: the home prototype is built and rooted here now.
  • The rest (js_array_set_index_or_string +606, js_object_get_own_property_descriptors +536, js_object_keys_value +536, class_prototype_addr +477, …) are LLVM inlining shifts: smaller callees from the deletions now inline. They are outweighed by the removals: class_value_get_field −2035, get_prototype_of_resolved −1635, class_prototype_set_bytes −1040, define_declared_class_accessor −929, js_object_get_own_property_names_shape −844, …

Startup Ir, classes

  • main's own three runs spread by about 1050 Ir.
  • The per-function callgrind diff (run 1) puts the delta in memcmp (+2617) and HashMap<_, _, RandomState>::insert (+365): seeded-hash noise.
  • The deterministic delta from this PR is about +50 Ir (class_closure_id/class_value_id now reached through the delegating class_ref_id, which is −21).

IR census output diffs

Every one is a compile failure in both arms or nondeterministic output:

  • Compile fails in both arms (crc 1/1):
    • class/test_gap_s8b_computed_fields
    • function/test_gap_10554_fn_identity_own_module
    • module/test_gap_cjs_factory_slot
    • module/test_gap_11275_reexport_value_kinds
    • module/test_issue_5431_dollar_named_export_call
  • Output varies across reruns:
    • array/test_issue63_arr (main varies)
    • array/test_issue63_escape
    • date/test_date
    • function/test_gap_this_param_receiver_routes
    • math/test_math
  • Timing: object/test_issue58_object_string prints elapsed: 7 ms vs 6 ms from Date.now(), and is equal on rerun.

Follow-ups

  • Predates this PR: C.prototype.m.bind(o).length is 0 in perry and 1 in node, on main as well. It is not a B4a path, so the control does not print it.
  • Predates this PR: perry-codegen/tests/large_object_barriers.rs::large_local_array_push_inbounds_store_emits_precise_slot_barrier also fails on main.
  • B4b: the INT32 consumers, the check-mode classifiers (including contracts.rs:409, the last class_method_home_id reader outside the home paths), and the 0x7FFE rename to INTERNAL_WORD_TAG.

DESIGN-p3 rev 3.1 §3 B4a deletes the producer API. B1 audited the NUM
declarations and the exact-int32 predicate confirmed that nothing produces an
INT32-tagged JS Number; this removes the means to make one.

- `JSValue::int32` is gone.
- Its one non-test caller, the builtin constructor identity capture
  (`alloc_builtin_constructor`), writes `builtin_constructor_identity_word`:
  the same runtime-internal 0x7FFE word the realm image writes for
  `RealmValue::Int32`, named as an internal word, never a Number (R4).
- The realm schema labels `RealmValue::Int32` (the constructor identity
  capture) `Other`, not `Number` (`realm.rs:293`).
- The test helpers that built INT32 Numbers now build canonical doubles.
  `value/tests.rs` keeps raw 0x7FFE words where it tests the tag space, and
  drops `test_int32` (the deleted constructor).
…t; delete the JS-path decoders of the prototype word (P3 B4a)

After B2 a `C.prototype` read returns the prototype object. The last place the
0x7FFE prototype word still sat in a JS-value position was the receiver of an
evaluation's by-name method value (`class_evaluation_method_value_for_name`,
`class_method_values.rs:42`), a BOUND_METHOD closure whose slot 0 is read as a
receiver.

- That receiver is now the evaluation's prototype object, the value a
  `C.prototype` read of the evaluation returns. The prototype fill passes the
  prototype it is filling; other callers build it first, which fills and
  finds the value.
- Bound-method dispatch reads the owner off the prototype object
  (`class_prototype_object_id`, structural: `class_id_for_decl_prototype_object`)
  and runs the owner's body with the call-time `this`.
- The word stays only as a method's internal home capture, under a new name:
  `class_method_home_word` / `class_method_home_id` (R4). Its readers are
  `js_class_method_home` (super) and `class_method_value_target`; the
  check-mode classifier in contracts.rs keeps it until B4b.
- Every other decoder of the word is deleted: the `class_prototype_ref_id`
  calls in descriptors, defineProperty, hasOwn, delete, symbol get/own
  symbols, proxy, weakref, instanceof, property get/set, enumeration, bind and
  others, and the 0x7FFE arms of `class_ref_id` and `class_ref_word`, which
  now decode only a class function object (both delegate to
  `class_value_id`). Helpers left without callers are deleted with them
  (`canonical_bound_method_receiver`, `delete_class_prototype_key`,
  `decl_prototype_own_accessor`, `class_chain_getter_value`,
  `class_static_prototype_is_nulled`), as is the class-word branch of
  `Function.prototype.bind` and of `Object.getPrototypeOf`.
- Tests that fed the word to a JS operation use the prototype object;
  `synthetic_prototype_ref_folds_onto_constructor_key` tested only the
  deleted fold and is removed.
- Controls: tests/parity/language/class/test_p3_b4a_prototype_paths.ts, one
  block per path plus bound methods of declared and per-evaluation classes,
  against node.
The store-side canonicalization tests (array push/set/from/raw helpers) and
the typed-array fast-set decline test pin what a live consumer does with a
tag word. Those consumers stay until B4b, so their tests keep a raw word;
a canonical double does not exercise them.
…the declaration, not the template holder (P3 B4a, S7b)

A by-name evaluation method value now carries the evaluation's prototype
object, so a method read before that prototype exists builds it. Its
[[Prototype]] for an evaluation with no pinned heritage was copied from the
template's declaration holder, which built that holder: the S7b invariant
forbids exactly that.

The link now comes from the declaration's static parent facts:
`static_parent_prototype_bits`, split out of `decl_prototype_parent_bits`, so
both prototypes read one function. It answers with the declared parent
class's prototype, a reserved native parent's, or Object.prototype, and never
reads the class's own holder.
@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 merged commit 593a341 into main Nov 9, 2026
95 of 102 checks passed
@proggeramlug
proggeramlug deleted the perf/p3-b4a-delete-int32-producers branch November 9, 2026 16:15
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