From 335c8f500b26df35bdb5e9cb058c49e71524f722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 9 Nov 2026 14:22:41 +0100 Subject: [PATCH 1/5] runtime: no API builds an INT32 Number (P3 B4a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- crates/perry-runtime-manifest/src/realm.rs | 7 +++++-- .../src/array/dense_move_tests.rs | 4 ++-- .../src/array/dynamic_numeric_key_tests.rs | 9 ++------- .../src/array/negative_numeric_key_tests.rs | 7 +------ .../src/array/sort_numeric_tests.rs | 2 +- .../perry-runtime/src/array/subclass_tests.rs | 2 +- crates/perry-runtime/src/array/tests.rs | 2 +- .../src/array/typed_array_receiver_tests.rs | 2 +- crates/perry-runtime/src/box/tests.rs | 4 ++-- .../perry-runtime/src/closure/births_tests.rs | 2 +- .../src/closure/dispatch/value_call.rs | 5 +---- .../src/closure/receiver_param_tests.rs | 2 +- crates/perry-runtime/src/closure/tests.rs | 2 +- .../src/fs/received_diagnostic_tests.rs | 5 +---- .../gc/tests/copying/constructor_identity.rs | 2 +- crates/perry-runtime/src/json/mod.rs | 6 +++--- .../src/json/stringify_tojson_probe_tests.rs | 2 +- crates/perry-runtime/src/native_abi.rs | 14 ++++---------- .../src/node_api_host/v10_tests.rs | 4 ++-- .../src/object/class_value/tests.rs | 4 ++-- .../object/global_this/declaration_tests.rs | 4 +++- .../src/object/global_this_tables.rs | 11 ++++++++++- .../perry-runtime/src/object/property_key.rs | 2 +- .../src/object/shape_store_tests.rs | 6 +----- crates/perry-runtime/src/object/tests.rs | 2 +- .../src/object/weakref_proto_thunks.rs | 10 +++++----- crates/perry-runtime/src/param_type_guard.rs | 2 +- .../perry-runtime/src/string/compare/keys.rs | 7 +++++-- .../src/string/concat_parts_tests.rs | 4 ++-- .../src/string/number_to_string_box_tests.rs | 4 ++-- .../src/string/provenance_tests.rs | 2 +- crates/perry-runtime/src/string/tests.rs | 2 +- crates/perry-runtime/src/tty.rs | 10 ++-------- .../perry-runtime/src/typed_feedback/tests.rs | 2 +- crates/perry-runtime/src/typedarray/mod.rs | 2 +- .../perry-runtime/src/value/dynamic_arith.rs | 2 +- crates/perry-runtime/src/value/jsvalue.rs | 8 -------- crates/perry-runtime/src/value/tests.rs | 18 ++++-------------- .../perry-runtime/src/value/to_string_radix.rs | 5 +---- crates/perry-runtime/src/weakref.rs | 2 +- crates/perry-stdlib/src/crypto/random.rs | 2 +- 41 files changed, 79 insertions(+), 115 deletions(-) diff --git a/crates/perry-runtime-manifest/src/realm.rs b/crates/perry-runtime-manifest/src/realm.rs index 564987cdc9c..71f1d656199 100644 --- a/crates/perry-runtime-manifest/src/realm.rs +++ b/crates/perry-runtime-manifest/src/realm.rs @@ -290,11 +290,14 @@ impl RealmProperty { RealmValue::Undefined => "Undefined".into(), RealmValue::Null => "Null".into(), RealmValue::Boolean(_) => "Boolean".into(), - RealmValue::NumberBits(_) | RealmValue::Int32(_) => "Number".into(), + RealmValue::NumberBits(_) => "Number".into(), RealmValue::String(_) => "String".into(), RealmValue::Symbol(_) => "Symbol".into(), RealmValue::Reference(r) => format!("Reference({r:?})"), - RealmValue::RawReference(_) + // A constructor identity capture (`realm_facts.rs`): a + // runtime-internal 0x7FFE word, never a JS Number (P3 B4a). + RealmValue::Int32(_) + | RealmValue::RawReference(_) | RealmValue::Bytes(_) | RealmValue::RawWord(_) | RealmValue::Dynamic(_) => "Other".into(), diff --git a/crates/perry-runtime/src/array/dense_move_tests.rs b/crates/perry-runtime/src/array/dense_move_tests.rs index 40a028d138b..ff9b322b884 100644 --- a/crates/perry-runtime/src/array/dense_move_tests.rs +++ b/crates/perry-runtime/src/array/dense_move_tests.rs @@ -72,14 +72,14 @@ fn dense_moves_preserve_the_existing_numeric_layout_without_a_rebuild() { Some(NumericArrayLayout::RawF64) ); - let arr = js_array_unshift_f64(arr, f64::from_bits(crate::value::JSValue::int32(0).bits())); + let arr = js_array_unshift_f64(arr, f64::from(0)); assert_eq!( unsafe { array_numeric_layout(arr) }, Some(NumericArrayLayout::RawF64) ); assert_eq!(js_array_get_f64(arr, 0), 0.0); - let item = [f64::from_bits(crate::value::JSValue::int32(9).bits())]; + let item = [f64::from(9)]; let mut out = arr; js_array_splice(arr, 2, 1, item.as_ptr(), 1, &mut out); assert_eq!( diff --git a/crates/perry-runtime/src/array/dynamic_numeric_key_tests.rs b/crates/perry-runtime/src/array/dynamic_numeric_key_tests.rs index 113f8c0c6f7..c2920f9817e 100644 --- a/crates/perry-runtime/src/array/dynamic_numeric_key_tests.rs +++ b/crates/perry-runtime/src/array/dynamic_numeric_key_tests.rs @@ -71,12 +71,7 @@ fn non_element_numeric_keys_do_not_truncate_on_regular_lazy_or_materialized_arra "packed state={state}, key={key}" ); } - for (key, expected) in [ - (0.0, 11.0), - (-0.0, 11.0), - (1.0, 22.0), - (f64::from_bits(JSValue::int32(2).bits()), 33.0), - ] { + for (key, expected) in [(0.0, 11.0), (-0.0, 11.0), (1.0, 22.0), (f64::from(2), 33.0)] { assert_eq!(js_dyn_index_get(array.get_nanbox_f64(), key), expected); assert_eq!( js_packed_arraylike_index_get(array.get_nanbox_f64(), key), @@ -132,5 +127,5 @@ fn boxed_integer_keys_keep_their_object_property_semantics() { }) }; assert_eq!(read(2.0), 42.0); - assert_eq!(read(f64::from_bits(JSValue::int32(2).bits())), 42.0); + assert_eq!(read(f64::from(2)), 42.0); } diff --git a/crates/perry-runtime/src/array/negative_numeric_key_tests.rs b/crates/perry-runtime/src/array/negative_numeric_key_tests.rs index dd7939f129a..f24a780bf33 100644 --- a/crates/perry-runtime/src/array/negative_numeric_key_tests.rs +++ b/crates/perry-runtime/src/array/negative_numeric_key_tests.rs @@ -14,12 +14,7 @@ fn negative_key_proof_requires_an_ordinary_array_without_own_named_properties() let mut arr = js_array_alloc(4); arr = js_array_push_f64(arr, 19.0); assert!(negative_integer_key_is_absent(arr)); - for key in [ - -1.0, - -4294967296.0, - -1e21, - f64::from_bits(crate::JSValue::int32(-1).bits()), - ] { + for key in [-1.0, -4294967296.0, -1e21, f64::from(-1)] { assert_eq!(read(arr, key).to_bits(), TAG_UNDEFINED); } assert_eq!(read(arr, -0.0), 19.0); diff --git a/crates/perry-runtime/src/array/sort_numeric_tests.rs b/crates/perry-runtime/src/array/sort_numeric_tests.rs index 7c271fe30f5..7489d19a454 100644 --- a/crates/perry-runtime/src/array/sort_numeric_tests.rs +++ b/crates/perry-runtime/src/array/sort_numeric_tests.rs @@ -106,7 +106,7 @@ fn numeric_sort_admits_dense_numbers_without_a_raw_f64_brand() { let a = array(&[3.0, 1.0, i32::MAX as f64, 2.0]); unsafe { crate::array::header::clear_array_numeric_layout(a); - note_array_slot(a, 2, crate::value::JSValue::int32(i32::MAX).bits()); + note_array_slot(a, 2, f64::from(i32::MAX).to_bits()); assert!(array_numeric_layout(a).is_none()); assert!(try_sort_numeric(a, false)); // The operation need not change a producer's conservative layout. diff --git a/crates/perry-runtime/src/array/subclass_tests.rs b/crates/perry-runtime/src/array/subclass_tests.rs index 1a426d5e945..adf9abf1cbf 100644 --- a/crates/perry-runtime/src/array/subclass_tests.rs +++ b/crates/perry-runtime/src/array/subclass_tests.rs @@ -434,7 +434,7 @@ fn dense_array_subclass_numeric_tail_store_preserves_tagged_fallbacks() { js_array_push_f64(obj as *mut ArrayHeader, 11.0); assert_eq!(array_subclass_fast_pop(receiver), Some(11.0)); - let tagged_i32 = f64::from_bits(crate::value::JSValue::int32(123).bits()); + let tagged_i32 = f64::from(123); assert_eq!( array_subclass_fast_push_one(receiver, tagged_i32), Some(1.0) diff --git a/crates/perry-runtime/src/array/tests.rs b/crates/perry-runtime/src/array/tests.rs index 84882834b92..321a467827c 100644 --- a/crates/perry-runtime/src/array/tests.rs +++ b/crates/perry-runtime/src/array/tests.rs @@ -55,7 +55,7 @@ fn assert_numeric_raw_values(arr: *mut ArrayHeader, expected: &[f64]) { } fn int32_jsvalue_bits(value: i32) -> u64 { - crate::value::JSValue::int32(value).bits() + f64::from(value).to_bits() } fn assert_canonical_raw_slot(arr: *mut ArrayHeader, index: u32, expected: f64) { diff --git a/crates/perry-runtime/src/array/typed_array_receiver_tests.rs b/crates/perry-runtime/src/array/typed_array_receiver_tests.rs index a79b3396dfa..7077687067b 100644 --- a/crates/perry-runtime/src/array/typed_array_receiver_tests.rs +++ b/crates/perry-runtime/src/array/typed_array_receiver_tests.rs @@ -1362,7 +1362,7 @@ fn js_put_value_set_number_key_on_a_typed_array_is_its_numeric_index() { let _serialized = crate::array::test_serialize(); let ta = typed(UINT16, &[0.0; 4]); let t = boxed(ta); - let int32 = |n: i32| f64::from_bits(crate::value::JSValue::int32(n).bits()); + let int32 = |n: i32| f64::from(n); crate::proxy::js_put_value_set(t, 3.0, 70000.0, t, 0); crate::proxy::js_put_value_set(t, int32(2), 7.0, t, 0); crate::proxy::js_put_value_set(t, 1.5, 9.0, t, 0); // canonical numeric, not an integer: nothing diff --git a/crates/perry-runtime/src/box/tests.rs b/crates/perry-runtime/src/box/tests.rs index 9aa255da338..e6362c1550c 100644 --- a/crates/perry-runtime/src/box/tests.rs +++ b/crates/perry-runtime/src/box/tests.rs @@ -52,7 +52,7 @@ fn box_set_get_roundtrips_for_real_box() { #[test] fn box_bits_roundtrips_non_number_tags_exactly() { let cases = [ - crate::value::JSValue::int32(-17).bits(), + f64::from(-17).to_bits(), crate::value::JSValue::try_short_string(b"ok") .unwrap() .bits(), @@ -76,7 +76,7 @@ fn box_bits_roundtrips_non_number_tags_exactly() { #[test] fn trusted_box_access_matches_valid_public_access_and_tdz_suppression() { - let initial = crate::value::JSValue::int32(17).bits(); + let initial = f64::from(17).to_bits(); let replacement = crate::value::JSValue::try_short_string(b"next") .unwrap() .bits(); diff --git a/crates/perry-runtime/src/closure/births_tests.rs b/crates/perry-runtime/src/closure/births_tests.rs index 3eb8dcce715..2c8c1f9e54e 100644 --- a/crates/perry-runtime/src/closure/births_tests.rs +++ b/crates/perry-runtime/src/closure/births_tests.rs @@ -35,7 +35,7 @@ fn record( fn a_table_bears_each_record_into_its_scope_slot() { let scope = crate::r#box::scope::js_scope_alloc(4, crate::value::TAG_UNDEFINED as i64) as u64; let scope_word = scope | crate::value::POINTER_TAG; - let value = crate::value::JSValue::int32(7).bits(); + let value = f64::from(7).to_bits(); // Source 0 is the scope object itself, source 1 a plain value. let words = [scope_word, value]; let indices = [0u32, 1, 1]; diff --git a/crates/perry-runtime/src/closure/dispatch/value_call.rs b/crates/perry-runtime/src/closure/dispatch/value_call.rs index 22d80760fda..2edafae702a 100644 --- a/crates/perry-runtime/src/closure/dispatch/value_call.rs +++ b/crates/perry-runtime/src/closure/dispatch/value_call.rs @@ -559,10 +559,7 @@ mod slice_body_tests { fn array_bridge_keeps_legacy_int32_conversion() { let info = crate::fn_info!(probe, 2; plain()); let closure = js_closure_alloc(info, 0); - let args = [ - f64::from_bits(crate::value::JSValue::int32(3).bits()), - f64::from_bits(crate::value::JSValue::int32(5).bits()), - ]; + let args = [f64::from(3), f64::from(5)]; assert_eq!( unsafe { js_closure_call_array(closure as i64, JsThis::from_f64(42.0), args.as_ptr(), 2) diff --git a/crates/perry-runtime/src/closure/receiver_param_tests.rs b/crates/perry-runtime/src/closure/receiver_param_tests.rs index 40032edb451..f8f5b91e330 100644 --- a/crates/perry-runtime/src/closure/receiver_param_tests.rs +++ b/crates/perry-runtime/src/closure/receiver_param_tests.rs @@ -68,7 +68,7 @@ static PROBE_WIDE: JsFunctionInfo = const UNDEF: u64 = crate::value::TAG_UNDEFINED; fn receiver() -> f64 { - f64::from_bits(crate::value::JSValue::int32(4242).bits()) + f64::from(4242) } fn this() -> JsThis { diff --git a/crates/perry-runtime/src/closure/tests.rs b/crates/perry-runtime/src/closure/tests.rs index a5319acd83a..ad1f8a1c42d 100644 --- a/crates/perry-runtime/src/closure/tests.rs +++ b/crates/perry-runtime/src/closure/tests.rs @@ -26,7 +26,7 @@ fn test_closure_capture_bits_roundtrip_tagged_values() { crate::value::TAG_NULL, crate::value::TAG_FALSE, crate::value::TAG_TRUE, - crate::value::JSValue::int32(-17).bits(), + f64::from(-17).to_bits(), crate::value::JSValue::try_short_string(b"cap") .unwrap() .bits(), diff --git a/crates/perry-runtime/src/fs/received_diagnostic_tests.rs b/crates/perry-runtime/src/fs/received_diagnostic_tests.rs index dfc5936fd10..37045f60db4 100644 --- a/crates/perry-runtime/src/fs/received_diagnostic_tests.rs +++ b/crates/perry-runtime/src/fs/received_diagnostic_tests.rs @@ -59,10 +59,7 @@ fn received_primitives_and_numbers() { ] { check(value, &format!("type number ({expected})")); } - check( - f64::from_bits(JSValue::int32(-42).bits()), - "type number (-42)", - ); + check(f64::from(-42), "type number (-42)"); } #[test] diff --git a/crates/perry-runtime/src/gc/tests/copying/constructor_identity.rs b/crates/perry-runtime/src/gc/tests/copying/constructor_identity.rs index 02979cc5ea4..d8fffe40c98 100644 --- a/crates/perry-runtime/src/gc/tests/copying/constructor_identity.rs +++ b/crates/perry-runtime/src/gc/tests/copying/constructor_identity.rs @@ -26,7 +26,7 @@ fn builtin_constructor_header_fact_survives_real_evacuation() { ); assert_eq!( crate::closure::js_closure_get_capture_bits(after, 0), - crate::JSValue::int32(declaration.identity as i32).bits() + crate::object::builtin_constructor_identity_word(declaration.identity as u32) ); } } diff --git a/crates/perry-runtime/src/json/mod.rs b/crates/perry-runtime/src/json/mod.rs index a2249a77fa3..049131e9e98 100644 --- a/crates/perry-runtime/src/json/mod.rs +++ b/crates/perry-runtime/src/json/mod.rs @@ -1638,9 +1638,9 @@ mod tests { // Same INT32_TAG funnel for array elements (drizzle raw-mode rows, // integer arrays over IPC). let mut arr = crate::array::js_array_alloc(3); - arr = crate::array::js_array_push_f64(arr, f64::from_bits(JSValue::int32(1).bits())); - arr = crate::array::js_array_push_f64(arr, f64::from_bits(JSValue::int32(2).bits())); - arr = crate::array::js_array_push_f64(arr, f64::from_bits(JSValue::int32(-9).bits())); + arr = crate::array::js_array_push_f64(arr, f64::from(1)); + arr = crate::array::js_array_push_f64(arr, f64::from(2)); + arr = crate::array::js_array_push_f64(arr, f64::from(-9)); let boxed = crate::value::js_nanbox_pointer(arr as i64); let output = unsafe { js_json_stringify(boxed, TYPE_ARRAY) }; assert_eq!(unsafe { str_from_header(output).unwrap() }, "[1,2,-9]"); diff --git a/crates/perry-runtime/src/json/stringify_tojson_probe_tests.rs b/crates/perry-runtime/src/json/stringify_tojson_probe_tests.rs index ec0cc4219f4..38f78badacf 100644 --- a/crates/perry-runtime/src/json/stringify_tojson_probe_tests.rs +++ b/crates/perry-runtime/src/json/stringify_tojson_probe_tests.rs @@ -89,7 +89,7 @@ fn json_tojson_key_probe_rejects_inline_and_nonstring_values_without_allocation( for value in [ JSValue::null(), JSValue::undefined(), - JSValue::int32(42), + JSValue::number(f64::from(42)), JSValue::bool(true), JSValue::from_bits(crate::value::TAG_HOLE), JSValue::string_ptr(std::ptr::null_mut()), diff --git a/crates/perry-runtime/src/native_abi.rs b/crates/perry-runtime/src/native_abi.rs index 01dfd849823..c05c60c9b7d 100644 --- a/crates/perry-runtime/src/native_abi.rs +++ b/crates/perry-runtime/src/native_abi.rs @@ -524,7 +524,7 @@ mod tests { assert_eq!(js_typed_f64_arg_guard(12.5), 1); assert_eq!(js_typed_f64_arg_to_raw(12.5), 12.5); - let int32 = f64::from_bits(crate::value::JSValue::int32(-7).bits()); + let int32 = f64::from(-7); assert_eq!(js_typed_f64_arg_guard(int32), 1); assert_eq!(js_typed_f64_arg_to_raw(int32), -7.0); @@ -535,7 +535,7 @@ mod tests { #[test] fn typed_i32_arg_guard_is_non_throwing_and_int32_only() { - let tagged = f64::from_bits(crate::value::JSValue::int32(-7).bits()); + let tagged = f64::from(-7); assert_eq!(js_typed_i32_arg_guard(tagged), 1); assert_eq!(js_typed_i32_arg_to_raw(tagged), -7); @@ -574,10 +574,7 @@ mod tests { assert_eq!(js_typed_i1_arg_to_raw(f), 0); assert_eq!(js_typed_i1_arg_guard(1.0), 0); - assert_eq!( - js_typed_i1_arg_guard(f64::from_bits(JSValue::int32(1).bits())), - 0 - ); + assert_eq!(js_typed_i1_arg_guard(f64::from(1)), 0); let s = crate::string::js_string_from_bytes(b"yes".as_ptr(), 3); let string = f64::from_bits(JSValue::string_ptr(s).bits()); assert_eq!(js_typed_i1_arg_guard(string), 0); @@ -595,10 +592,7 @@ mod tests { assert_ne!(js_typed_string_arg_to_raw(short), 0); assert_eq!(js_typed_string_arg_guard(42.0), 0); - assert_eq!( - js_typed_string_arg_guard(f64::from_bits(JSValue::int32(7).bits())), - 0 - ); + assert_eq!(js_typed_string_arg_guard(f64::from(7)), 0); assert_eq!(js_typed_string_arg_guard(f64::from_bits(TAG_TRUE)), 0); } diff --git a/crates/perry-runtime/src/node_api_host/v10_tests.rs b/crates/perry-runtime/src/node_api_host/v10_tests.rs index 419c184cee6..12ac170fcf4 100644 --- a/crates/perry-runtime/src/node_api_host/v10_tests.rs +++ b/crates/perry-runtime/src/node_api_host/v10_tests.rs @@ -200,7 +200,7 @@ fn created_integers_stay_numbers_when_a_class_id_shares_their_value() { unsafe { napi_create_uint32(env, REGISTERED_CLASS, &mut uint32) }, NapiStatus::Ok ); - let tagged_int32 = handle(env, JSValue::int32(REGISTERED_CLASS as i32).bits()); + let tagged_int32 = handle(env, f64::from(REGISTERED_CLASS as i32).to_bits()); for value in [int32, uint32, tagged_int32] { assert_eq!(type_of(env, value), NapiValueType::Number); let mut read = 0; @@ -213,7 +213,7 @@ fn created_integers_stay_numbers_when_a_class_id_shares_their_value() { // An INT32 word is a number even when a class id shares its payload: the // class-id immediate is a runtime-internal key, never a value. - let int32_word = handle(env, JSValue::int32(REGISTERED_CLASS as i32).bits()); + let int32_word = handle(env, f64::from(REGISTERED_CLASS as i32).to_bits()); assert_eq!(type_of(env, int32_word), NapiValueType::Number); let mut read = 0.0; assert_eq!( diff --git a/crates/perry-runtime/src/object/class_value/tests.rs b/crates/perry-runtime/src/object/class_value/tests.rs index 8943648ae15..af1899ce1c6 100644 --- a/crates/perry-runtime/src/object/class_value/tests.rs +++ b/crates/perry-runtime/src/object/class_value/tests.rs @@ -554,7 +554,7 @@ fn an_int32_equal_to_a_registered_class_id_is_a_number() { fn an_int32_is_neither_a_dynamic_parent_nor_a_reflect_object() { let cid = 0x6A11; register(cid); - let int = f64::from_bits(crate::value::JSValue::int32(cid as i32).bits()); + let int = f64::from(cid as i32); assert_eq!( crate::object::class_registry::parent_static::dynamic_value_class_id(int), 0 @@ -573,7 +573,7 @@ fn metadata_on_an_int32_cannot_alias_a_class() { let _no_gc = crate::gc::GcSuppressScope::new(); let cid = 0x6A12; register(cid); - let int = f64::from_bits(crate::value::JSValue::int32(cid as i32).bits()); + let int = f64::from(cid as i32); let class = class_value(cid); let key = crate::value::js_nanbox_string(crate::string::js_string_from_bytes( b"number-class-metadata".as_ptr(), diff --git a/crates/perry-runtime/src/object/global_this/declaration_tests.rs b/crates/perry-runtime/src/object/global_this/declaration_tests.rs index b7fa0d99f5b..f769f6524d1 100644 --- a/crates/perry-runtime/src/object/global_this/declaration_tests.rs +++ b/crates/perry-runtime/src/object/global_this/declaration_tests.rs @@ -91,7 +91,9 @@ fn objops_instance_identity_is_declared_and_body_checked() { crate::closure::js_closure_set_capture_bits( ordinary, 0, - crate::JSValue::int32(index as i32 + 1).bits(), + super::super::super::global_this_tables::builtin_constructor_identity_word( + index as u32 + 1, + ), ); let ordinary_info = crate::closure::closure_info(ordinary).unwrap(); assert!(unsafe { diff --git a/crates/perry-runtime/src/object/global_this_tables.rs b/crates/perry-runtime/src/object/global_this_tables.rs index dcad808e0f0..225a961a842 100644 --- a/crates/perry-runtime/src/object/global_this_tables.rs +++ b/crates/perry-runtime/src/object/global_this_tables.rs @@ -378,6 +378,15 @@ pub(crate) fn constructor_identity(value: f64) -> Option u64 { + crate::value::INT32_TAG | identity as u64 +} + /// Birth consumes one declaration, so code, header identity and call capture /// cannot disagree. The fact precedes publication and survives moving GC. pub(crate) fn alloc_builtin_constructor( @@ -390,7 +399,7 @@ pub(crate) fn alloc_builtin_constructor( crate::closure::js_closure_set_capture_bits( closure, 0, - crate::JSValue::int32(identity as i32).bits(), + builtin_constructor_identity_word(identity as u32), ); // Scalar declaration fact, installed before publication. The ordinary // birth has already checked the capture bound and initialized all slots. diff --git a/crates/perry-runtime/src/object/property_key.rs b/crates/perry-runtime/src/object/property_key.rs index fb512109709..414ae7f29ea 100644 --- a/crates/perry-runtime/src/object/property_key.rs +++ b/crates/perry-runtime/src/object/property_key.rs @@ -521,7 +521,7 @@ mod property_key_tests { assert_eq!(js_to_property_key(sym).to_bits(), sym.to_bits()); assert_eq!(string_value_to_rust(js_to_property_key(42.0)), "42"); - let int_key = f64::from_bits(crate::value::JSValue::int32(7).bits()); + let int_key = f64::from(7); assert_eq!(string_value_to_rust(js_to_property_key(int_key)), "7"); assert_eq!( string_value_to_rust(js_to_property_key(f64::from_bits(crate::value::TAG_TRUE))), diff --git a/crates/perry-runtime/src/object/shape_store_tests.rs b/crates/perry-runtime/src/object/shape_store_tests.rs index 17ec263c572..73da6b112cd 100644 --- a/crates/perry-runtime/src/object/shape_store_tests.rs +++ b/crates/perry-runtime/src/object/shape_store_tests.rs @@ -166,11 +166,7 @@ fn own_number_store_preserves_lanes_and_generalizes_a_function_slot() { super::super::js_object_get_field_by_name_f64(obj, atom), f64::INFINITY ); - assert!(try_own_store( - obj, - atom, - f64::from_bits(crate::JSValue::int32(42).bits()) - )); + assert!(try_own_store(obj, atom, f64::from(42))); assert_eq!( super::super::js_object_get_field_by_name_f64(obj, atom), 42.0 diff --git a/crates/perry-runtime/src/object/tests.rs b/crates/perry-runtime/src/object/tests.rs index e96c6d2bfe5..113262b9b96 100644 --- a/crates/perry-runtime/src/object/tests.rs +++ b/crates/perry-runtime/src/object/tests.rs @@ -36,7 +36,7 @@ fn test_global_this_builtin_constructor_value(name: &str) -> f64 { crate::closure::js_closure_set_capture_bits( closure_ptr, 0, - JSValue::int32(identity as i32 + 1).bits(), + crate::object::builtin_constructor_identity_word(identity as u32 + 1), ); super::native_module::set_bound_native_closure_name(closure_ptr, name); if let Some(len) = crate::object::builtin_constructor_spec_length(name) { diff --git a/crates/perry-runtime/src/object/weakref_proto_thunks.rs b/crates/perry-runtime/src/object/weakref_proto_thunks.rs index 70c43f64488..5dd37f56f2d 100644 --- a/crates/perry-runtime/src/object/weakref_proto_thunks.rs +++ b/crates/perry-runtime/src/object/weakref_proto_thunks.rs @@ -360,7 +360,7 @@ mod tests { _c: *const crate::closure::ClosureHeader, _this: crate::closure::JsThis, ) -> f64 { - f64::from_bits(JSValue::int32(FOREIGN_SENTINEL).bits()) + f64::from(FOREIGN_SENTINEL) } extern "C" fn foreign_method_thunk_1( @@ -368,7 +368,7 @@ mod tests { _this: crate::closure::JsThis, _a: f64, ) -> f64 { - f64::from_bits(JSValue::int32(FOREIGN_SENTINEL).bits()) + f64::from(FOREIGN_SENTINEL) } /// A plain object carrying its own `method_name` function property — the @@ -400,7 +400,7 @@ mod tests { /// Removing the brand check makes every one of these return `undefined`. #[test] fn folded_weak_helpers_delegate_a_foreign_receiver_to_its_own_method() { - let sentinel = JSValue::int32(FOREIGN_SENTINEL).bits(); + let sentinel = f64::from(FOREIGN_SENTINEL).to_bits(); let deref_recv = plain_object_with_method( "deref", @@ -412,7 +412,7 @@ mod tests { "a foreign receiver's own `deref` must run, not the WeakRef intrinsic" ); - let key = f64::from_bits(JSValue::int32(1).bits()); + let key = f64::from(1); for name in ["get", "has", "delete"] { let recv = plain_object_with_method( name, @@ -466,7 +466,7 @@ mod tests { delegate_if_not_weak_collection(wm_val, "get", &[]).is_none(), "a real WeakMap must NOT be delegated away" ); - let v = f64::from_bits(JSValue::int32(42).bits()); + let v = f64::from(42); crate::weakref::js_weakmap_set(wm_val, target_val, v); assert_eq!( crate::weakref::js_weakmap_get(wm_val, target_val).to_bits(), diff --git a/crates/perry-runtime/src/param_type_guard.rs b/crates/perry-runtime/src/param_type_guard.rs index 7c10937bafd..f8c06b57a53 100644 --- a/crates/perry-runtime/src/param_type_guard.rs +++ b/crates/perry-runtime/src/param_type_guard.rs @@ -1148,7 +1148,7 @@ mod tests { fn primitive_descriptors_reject_lying_values() { let number = one_node(&[OP_NUMBER]); assert_eq!(guard(JSValue::number(12.5), &number), 1); - assert_eq!(guard(JSValue::int32(12), &number), 1); + assert_eq!(guard(JSValue::number(f64::from(12)), &number), 1); assert_eq!(guard(JSValue::bool(true), &number), 0); let boolean = one_node(&[OP_BOOLEAN]); diff --git a/crates/perry-runtime/src/string/compare/keys.rs b/crates/perry-runtime/src/string/compare/keys.rs index 408ba756e47..fff7ea42f1f 100644 --- a/crates/perry-runtime/src/string/compare/keys.rs +++ b/crates/perry-runtime/src/string/compare/keys.rs @@ -188,7 +188,7 @@ mod tests_sso_helpers { for stored in [ JSValue::undefined(), JSValue::null(), - JSValue::int32(42), + JSValue::number(f64::from(42)), JSValue::bool(true), ] { unsafe { @@ -263,7 +263,10 @@ mod tests_sso_helpers { js_string_key_bytes(heap, &mut buf), Some(b"longish".as_ref()) ); - assert_eq!(js_string_key_bytes(JSValue::int32(7), &mut buf), None); + assert_eq!( + js_string_key_bytes(JSValue::number(f64::from(7)), &mut buf), + None + ); } } } diff --git a/crates/perry-runtime/src/string/concat_parts_tests.rs b/crates/perry-runtime/src/string/concat_parts_tests.rs index dd72c569e64..a5c9eedfaa6 100644 --- a/crates/perry-runtime/src/string/concat_parts_tests.rs +++ b/crates/perry-runtime/src/string/concat_parts_tests.rs @@ -105,8 +105,8 @@ fn other_value_kinds_take_the_general_arm_with_chain_results() { f64::from_bits(0x7FFC_0000_0000_0002), f64::from_bits(0x7FFC_0000_0000_0004), f64::from_bits(0x7FFC_0000_0000_0003), - f64::from_bits(JSValue::int32(7).bits()), - f64::from_bits(JSValue::int32(-7).bits()), + f64::from(7), + f64::from(-7), ]; for other in others { let a = parts_call(&[literal("<"), value(other), literal(">")]); diff --git a/crates/perry-runtime/src/string/number_to_string_box_tests.rs b/crates/perry-runtime/src/string/number_to_string_box_tests.rs index a29272d3d19..447877354cd 100644 --- a/crates/perry-runtime/src/string/number_to_string_box_tests.rs +++ b/crates/perry-runtime/src/string/number_to_string_box_tests.rs @@ -183,8 +183,8 @@ fn coercion_box_twins_match_their_pointer_originals_on_other_primitives() { f64::from_bits(crate::value::TAG_NULL), f64::from_bits(crate::value::TAG_TRUE), f64::from_bits(crate::value::TAG_FALSE), - f64::from_bits(JSValue::int32(-42).bits()), - f64::from_bits(JSValue::int32(123_456).bits()), + f64::from(-42), + f64::from(123_456), ]; for value in values { assert_eq!( diff --git a/crates/perry-runtime/src/string/provenance_tests.rs b/crates/perry-runtime/src/string/provenance_tests.rs index d05bb687ea5..33b9c6c0714 100644 --- a/crates/perry-runtime/src/string/provenance_tests.rs +++ b/crates/perry-runtime/src/string/provenance_tests.rs @@ -150,7 +150,7 @@ fn index_conversion_matches_saturation_and_numeric_coercion() { assert_eq!(slice_ops::js_string_position_to_index(n), expected); } for n in [i32::MIN, -1, 0, 1, i32::MAX] { - let boxed = f64::from_bits(crate::value::JSValue::int32(n).bits()); + let boxed = f64::from(n); assert_eq!(js_string_index_to_i32(boxed), n); assert_eq!(char_ops::js_string_number_index_to_i32(boxed), n); assert_eq!(char_ops::js_string_end_index_to_i32(boxed, 17), n); diff --git a/crates/perry-runtime/src/string/tests.rs b/crates/perry-runtime/src/string/tests.rs index 173714bd8ee..611fab48ef0 100644 --- a/crates/perry-runtime/src/string/tests.rs +++ b/crates/perry-runtime/src/string/tests.rs @@ -957,7 +957,7 @@ fn concat_box_delegates_a_non_string_operand_to_the_dynamic_add() { // with the empty string. assert_eq!(js_string_concat_box(40.0, 2.0), 42.0); // An int32-tagged operand must decode to its value, not to its boxed bits. - let int42 = f64::from_bits(crate::value::JSValue::int32(42).bits()); + let int42 = f64::from(42); assert_eq!(text(js_string_concat_box(heap("n="), int42)), "n=42"); // undefined / null keep their ToString forms. let undef = f64::from_bits(crate::value::JSValue::undefined().bits()); diff --git a/crates/perry-runtime/src/tty.rs b/crates/perry-runtime/src/tty.rs index e6a8671fb62..1cee53ae902 100644 --- a/crates/perry-runtime/src/tty.rs +++ b/crates/perry-runtime/src/tty.rs @@ -1401,14 +1401,8 @@ mod tests { assert_eq!(tty_isatty_fd_arg(f64::INFINITY), None); assert_eq!(tty_isatty_fd_arg(f64::NEG_INFINITY), None); - assert_eq!( - tty_isatty_fd_arg(f64::from_bits(JSValue::int32(7).bits())), - Some(7) - ); - assert_eq!( - tty_isatty_fd_arg(f64::from_bits(JSValue::int32(-1).bits())), - None - ); + assert_eq!(tty_isatty_fd_arg(f64::from(7)), Some(7)); + assert_eq!(tty_isatty_fd_arg(f64::from(-1)), None); assert_eq!(tty_isatty_fd_arg(TAG_TRUE_F64), None); assert_eq!(tty_isatty_fd_arg(TAG_FALSE_F64), None); diff --git a/crates/perry-runtime/src/typed_feedback/tests.rs b/crates/perry-runtime/src/typed_feedback/tests.rs index 3b8da1bfae7..86c28881b9b 100644 --- a/crates/perry-runtime/src/typed_feedback/tests.rs +++ b/crates/perry-runtime/src/typed_feedback/tests.rs @@ -547,7 +547,7 @@ fn typed_feedback_array_get_fallback_reads_an_object_backed_array_subclass_dense let actual = js_typed_feedback_array_index_get_fallback_boxed(26, receiver, 1.0); assert_eq!(actual.to_bits(), 22.0f64.to_bits()); - let boxed_index = f64::from_bits(crate::value::JSValue::int32(2).bits()); + let boxed_index = f64::from(2); let actual = js_typed_feedback_array_index_get_fallback_boxed(26, receiver, boxed_index); assert_eq!(actual.to_bits(), 33.0f64.to_bits()); diff --git a/crates/perry-runtime/src/typedarray/mod.rs b/crates/perry-runtime/src/typedarray/mod.rs index 9be1a97af4c..7b4bfc10941 100644 --- a/crates/perry-runtime/src/typedarray/mod.rs +++ b/crates/perry-runtime/src/typedarray/mod.rs @@ -537,7 +537,7 @@ mod tests { crate::value::TAG_FALSE, crate::value::TAG_NULL, crate::value::TAG_UNDEFINED, - crate::value::JSValue::int32(5).bits(), + f64::from(5).to_bits(), 0x7FF9_0100_0000_0035, // short string "5" ] { assert!( diff --git a/crates/perry-runtime/src/value/dynamic_arith.rs b/crates/perry-runtime/src/value/dynamic_arith.rs index 06124824895..fd8e90b76f7 100644 --- a/crates/perry-runtime/src/value/dynamic_arith.rs +++ b/crates/perry-runtime/src/value/dynamic_arith.rs @@ -1172,7 +1172,7 @@ mod tests { } } fn int32(n: i32) -> f64 { - f64::from_bits(JSValue::int32(n).bits()) + f64::from(n) } // An int32-tagged operand (e.g. a better-sqlite3 integer column) must be diff --git a/crates/perry-runtime/src/value/jsvalue.rs b/crates/perry-runtime/src/value/jsvalue.rs index 71872948b1b..da9a611f58e 100644 --- a/crates/perry-runtime/src/value/jsvalue.rs +++ b/crates/perry-runtime/src/value/jsvalue.rs @@ -44,14 +44,6 @@ impl JSValue { } } - /// Create an i32 value (stored in payload, faster than f64 for integers) - #[inline] - pub const fn int32(value: i32) -> Self { - Self { - bits: INT32_TAG | ((value as u32) as u64), - } - } - /// Create a pointer value (for heap-allocated objects) #[inline] pub fn pointer(ptr: *const u8) -> Self { diff --git a/crates/perry-runtime/src/value/tests.rs b/crates/perry-runtime/src/value/tests.rs index dfb27acef49..e813d4b8827 100644 --- a/crates/perry-runtime/src/value/tests.rs +++ b/crates/perry-runtime/src/value/tests.rs @@ -44,17 +44,6 @@ fn test_number() { assert_eq!(neg.as_number(), -123.456); } -#[test] -fn test_int32() { - let v = JSValue::int32(42); - assert!(v.is_int32()); - assert_eq!(v.as_int32(), 42); - - let neg = JSValue::int32(-100); - assert!(neg.is_int32()); - assert_eq!(neg.as_int32(), -100); -} - #[test] fn test_truthiness() { assert!(!JSValue::undefined().to_bool()); @@ -148,7 +137,8 @@ fn test_short_string_tag_distinct_from_others() { let pointer = JSValue { bits: POINTER_TAG | 0x5678, }; - let int32 = JSValue::int32(42); + // A runtime-internal 0x7FFE word (P3 B4a: no API builds an INT32 Number). + let int32 = JSValue::from_bits(INT32_TAG | 42); let number = JSValue::number(3.14); let undef = JSValue::undefined(); assert!(sso.is_short_string()); @@ -171,7 +161,7 @@ fn test_tagged_values_are_not_numbers() { assert!(!JSValue::from_bits(BIGINT_TAG | 0x1234).is_number()); assert!(!JSValue::from_bits(JS_HANDLE_TAG | 0x5678).is_number()); assert!(!JSValue::undefined().is_number()); - assert!(!JSValue::int32(42).is_number()); + assert!(!JSValue::from_bits(INT32_TAG | 42).is_number()); assert!(JSValue::from_bits(0x7FF8_0000_0000_0000).is_number()); assert!(JSValue::number(f64::NAN).is_number()); } @@ -230,7 +220,7 @@ fn strict_equality_rejects_equal_and_distinct_numeric_nan_payloads() { 0.0, -0.0, 1.0, - f64::from_bits(JSValue::int32(1).bits()), + f64::from_bits(INT32_TAG | 1), f64::from_bits(TAG_UNDEFINED), f64::from_bits(TAG_NULL), f64::from_bits(TAG_TRUE), diff --git a/crates/perry-runtime/src/value/to_string_radix.rs b/crates/perry-runtime/src/value/to_string_radix.rs index 392c5534e56..8e423c7180b 100644 --- a/crates/perry-runtime/src/value/to_string_radix.rs +++ b/crates/perry-runtime/src/value/to_string_radix.rs @@ -418,10 +418,7 @@ mod radix_tostring_tests { // ToInteger truncation. assert_eq!(coerce_validate_radix(2.9), Some(2)); // int32-boxed radix. - assert_eq!( - coerce_validate_radix(f64::from_bits(JSValue::int32(16).bits())), - Some(16) - ); + assert_eq!(coerce_validate_radix(f64::from(16)), Some(16)); // String radix coerces via ToNumber. let s = crate::string::js_string_from_bytes(b"16".as_ptr(), 2); assert_eq!( diff --git a/crates/perry-runtime/src/weakref.rs b/crates/perry-runtime/src/weakref.rs index c9ff7c7b8a6..c8eff9660dd 100644 --- a/crates/perry-runtime/src/weakref.rs +++ b/crates/perry-runtime/src/weakref.rs @@ -1680,7 +1680,7 @@ mod tests { // exact value bits it was given. let wm = js_weakmap_new(); let wm_val = f64::from_bits(JSValue::pointer(wm as *const u8).bits()); - let v = f64::from_bits(JSValue::int32(42).bits()); + let v = f64::from(42); js_weakmap_set(wm_val, fresh_sym, v); let got = js_weakmap_get(wm_val, fresh_sym); assert_eq!( diff --git a/crates/perry-stdlib/src/crypto/random.rs b/crates/perry-stdlib/src/crypto/random.rs index 11e035f9192..7b33b317fbd 100644 --- a/crates/perry-stdlib/src/crypto/random.rs +++ b/crates/perry-stdlib/src/crypto/random.rs @@ -1256,7 +1256,7 @@ mod tests { } fn int32(value: i32) -> f64 { - f64::from_bits(perry_runtime::JSValue::int32(value).bits()) + f64::from(value) } /// The bytes behind a `Buffer` the dispatcher returned, or empty when it From 3c2fb88176178d2ed9faa9ac25292b6c75e660b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 9 Nov 2026 14:27:07 +0100 Subject: [PATCH 2/5] runtime: a class prototype's bound method carries the prototype object; 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. --- .../runtime-p3-b4a-delete-int32-producers.md | 1 + .../src/closure/dispatch/bound.rs | 95 +++---- crates/perry-runtime/src/contracts.rs | 2 +- .../src/object/bound_method_receiver_tests.rs | 6 +- .../src/object/class_constructors.rs | 2 - .../src/object/class_registry.rs | 13 +- .../object/class_registry/decl_accessors.rs | 55 ---- .../class_registry/decl_prototype_birth.rs | 2 +- .../parent_static/symbol_members.rs | 2 +- .../src/object/class_registry/state.rs | 8 +- .../src/object/class_super_chain.rs | 2 +- .../perry-runtime/src/object/class_value.rs | 11 +- .../src/object/class_value/tests.rs | 13 +- .../perry-runtime/src/object/delete_rest.rs | 38 --- .../perry-runtime/src/object/descriptors.rs | 239 ++++++------------ .../src/object/descriptors/builders.rs | 4 - .../field_get_set/class_object_props.rs | 2 + .../src/object/field_get_set/enumeration.rs | 42 ++- .../object/field_get_set/get_field_by_name.rs | 129 +++------- .../get_field_by_name/class_wtf8.rs | 29 +-- .../src/object/field_get_set/has_property.rs | 22 +- .../src/object/field_set_by_name.rs | 19 +- .../src/object/global_this/array_error.rs | 12 +- .../src/object/instanceof/proxy_rhs.rs | 1 - crates/perry-runtime/src/object/mod.rs | 2 +- .../src/object/native_call_method.rs | 4 +- .../native_call_method/common_methods.rs | 12 +- .../native_call_method/primitive_methods.rs | 16 +- .../perry-runtime/src/object/native_module.rs | 24 +- .../object/native_module/class_method_bind.rs | 27 +- .../native_module/class_method_values.rs | 35 ++- .../object/native_module/class_ref_values.rs | 48 ++-- .../object_ops/define_class_accessor.rs | 37 +-- .../src/object/object_ops/define_property.rs | 146 ++++------- .../src/object/object_ops/has_own.rs | 55 ++-- .../src/object/object_ops/prototype.rs | 68 +---- .../src/object/polymorphic_index.rs | 9 - .../perry-runtime/src/object/property_key.rs | 22 +- .../src/object/property_ops/common.rs | 8 +- .../src/object/property_ops/metadata.rs | 29 +-- .../src/object/property_ops/set.rs | 5 +- crates/perry-runtime/src/object/tests.rs | 22 +- crates/perry-runtime/src/proxy.rs | 4 - crates/perry-runtime/src/symbol/get.rs | 33 +-- crates/perry-runtime/src/symbol/iterator.rs | 4 - crates/perry-runtime/src/util_inherits.rs | 8 +- crates/perry-runtime/src/weakref.rs | 19 +- .../class/test_p3_b4a_prototype_paths.ts | 125 +++++++++ 48 files changed, 558 insertions(+), 953 deletions(-) create mode 100644 changelog.d/runtime-p3-b4a-delete-int32-producers.md create mode 100644 tests/parity/language/class/test_p3_b4a_prototype_paths.ts diff --git a/changelog.d/runtime-p3-b4a-delete-int32-producers.md b/changelog.d/runtime-p3-b4a-delete-int32-producers.md new file mode 100644 index 00000000000..b1358c299a9 --- /dev/null +++ b/changelog.d/runtime-p3-b4a-delete-int32-producers.md @@ -0,0 +1 @@ +Cleanup: nothing in the runtime can build an INT32-tagged Number anymore. `JSValue::int32` is gone, its test uses now build plain doubles, and the realm schema no longer labels the constructor identity capture as a Number. A bound method of a class prototype (an evaluation's by-name method value) now carries the prototype object as its receiver, not the internal `0x7FFE` prototype word, so that word never reaches a JS operation. The decoders of that word in JS-operation paths (property descriptors, `defineProperty`, `hasOwn`, `delete`, symbol reads, proxies, weak collections, `instanceof` and others) are deleted. The word stays as a method's internal home, read only by `super` lookup (P3 B4a). diff --git a/crates/perry-runtime/src/closure/dispatch/bound.rs b/crates/perry-runtime/src/closure/dispatch/bound.rs index 45b23561926..f3b9cff49a5 100644 --- a/crates/perry-runtime/src/closure/dispatch/bound.rs +++ b/crates/perry-runtime/src/closure/dispatch/bound.rs @@ -36,7 +36,7 @@ pub(crate) unsafe fn bound_method_source_func_ptr(closure: *const ClosureHeader) std::str::from_utf8(std::slice::from_raw_parts(method_name_ptr, method_name_len)).ok()?; let receiver = js_closure_get_capture_f64(closure, 0); - if let Some(class_id) = crate::object::class_prototype_ref_id(receiver) { + if let Some(class_id) = crate::object::class_prototype_object_id(receiver) { return crate::object::class_method_slot_target(class_id, name) .map(|(func_ptr, ..)| func_ptr); } @@ -123,9 +123,14 @@ pub unsafe fn dispatch_bound_method( // name-based interpretation of the captures below (slots 3/4 are not part // of the name layout). if method_name_ptr == crate::object::SYMBOL_BOUND_METHOD_NAME.as_ptr() as *const i8 { - return dispatch_symbol_bound_method(closure, namespace_obj, this, args); + return dispatch_symbol_bound_method(closure, namespace_obj, args); } + // A receiver that is a class's prototype object names the method's OWNER + // (`C.prototype.m` read as a value, an evaluation's by-name method), not + // its `this`: the call-time `this` is the receiver. + let owner_prototype_id = crate::object::class_prototype_object_id(namespace_obj); + // Private-method value (`const f = this.#m; f.call(o)`): a `#`-named method // read off an instance yields the OWNER class's method function. Unlike a // public method, its invocation must dispatch the OWNER's `#m` body with the @@ -135,14 +140,14 @@ pub unsafe fn dispatch_bound_method( // simply bind the owner's body to whatever `this` the call supplies (spec: // `PrivateMethodOrAccessorAdd` installs the shared function; calling it does // no brand check). The canonical closure captures the owner class's - // prototype-ref in slot 0, which carries the owner id. + // prototype object in slot 0, which names the owner id. if method_name_len > 0 && !method_name_ptr.is_null() { if let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts( method_name_ptr as *const u8, method_name_len, )) { if name.starts_with('#') { - if let Some(owner_id) = crate::object::class_prototype_ref_id(namespace_obj) { + if let Some(owner_id) = owner_prototype_id { if let Some((func_ptr, param_count, has_synth_args, has_rest)) = crate::object::class_method_slot_target(owner_id, name) { @@ -174,7 +179,7 @@ pub unsafe fn dispatch_bound_method( }; } } - if crate::object::class_prototype_ref_id(namespace_obj).is_none() { + if owner_prototype_id.is_none() { if let (Some(owner_id), Some(brand)) = (crate::object::class_ref_id(namespace_obj), private_brand) { @@ -218,10 +223,13 @@ pub unsafe fn dispatch_bound_method( // object — user-visible as `[object Object]` instead of the method's result, // exactly the failure the comment below describes for the self-shadowing `bind` // case. - let owner_proto_ref = namespace_obj; - let call_receiver = crate::object::canonical_bound_method_receiver(owner_proto_ref, this); + let call_receiver = if owner_prototype_id.is_some() { + this.as_f64() + } else { + namespace_obj + }; if method_name_len > 0 && !method_name_ptr.is_null() { - if let Some(owner_id) = crate::object::class_prototype_ref_id(owner_proto_ref) { + if let Some(owner_id) = owner_prototype_id { if let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts( method_name_ptr as *const u8, method_name_len, @@ -290,14 +298,13 @@ pub unsafe fn dispatch_bound_method( } /// #6173: invoke a symbol-bound class-method closure. `receiver` is capture -/// slot 0 (a NaN-boxed instance/prototype-ref, or the INT32 class ref for a -/// static method); the resolved func_ptr and packed param_count/has_rest/ +/// slot 0 (a NaN-boxed instance, or the class constructor for a static +/// method); the resolved func_ptr and packed param_count/has_rest/ /// is_static meta live in slots 3/4 (see `SYMBOL_BOUND_METHOD_NAME`). /// Mirrors the direct-call symbol dispatch in `js_native_call_method_value`. unsafe fn dispatch_symbol_bound_method( closure: *const ClosureHeader, - captured_receiver: f64, - this: crate::closure::JsThis, + receiver: f64, args: &[f64], ) -> f64 { let func_ptr = js_closure_get_capture_ptr(closure, 3) as usize; @@ -311,7 +318,6 @@ unsafe fn dispatch_symbol_bound_method( let param_count = (meta & 0xFFFF_FFFF) as u32; let has_rest = (meta >> 32) & 1 == 1; let is_static = (meta >> 33) & 1 == 1; - let receiver = crate::object::canonical_bound_method_receiver(captured_receiver, this); if is_static { // The static method runs with the class ref as `this`, exactly like // the direct-call path. The one-shot static-`this` override (armed by @@ -658,17 +664,8 @@ unsafe fn bound_target_declared_name(target_value: f64) -> String { if !target_closure.is_null() && is_closure_ptr(target_closure as usize) { return crate::builtins::function_name_for_closure(target_closure).unwrap_or_default(); } - return String::new(); } - let target_class_id = crate::object::class_ref_id(target_value).or_else(|| { - crate::object::class_prototype_ref_id(target_value) - .is_none() - .then(|| crate::object::class_value::class_ref_word(target_value.to_bits())) - .flatten() - }); - target_class_id - .and_then(crate::object::class_name_for_id) - .unwrap_or_default() + String::new() } /// Lazily synthesize and cache a bound function's `.name`. `ptr` must be a @@ -887,41 +884,20 @@ unsafe fn function_bind_observable( let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32); crate::exception::throw_core(crate::error::CoreError::type_error(msg)); } - // A pointer target is a closure (a class function object is one) or a - // callable native handle; only a non-pointer target can be the legacy - // INT32 class form, so only it pays the class probe. - let target_is_closure = if closure_target { - true - } else if target_jv.is_pointer() { - // Callable handles and proxies retain the existing conservative - // pass-through until they use the bound-closure representation. - return target_value; - } else if crate::object::class_ref_id(target_value) - .or_else(|| { - crate::object::class_prototype_ref_id(target_value) - .is_none() - .then(|| crate::object::class_value::class_ref_word(target_value.to_bits())) - .flatten() - }) - .is_some() - { - // ClassRefs are callable/constructable INT32-tagged values rather - // than heap closures. They still need a real BoundFunction wrapper - // so `new C.bind(_, ...args)()` prepends its captured arguments. - false - } else { + // A target that is not a closure (a class function object is one) is a + // callable native handle or a proxy: they retain the existing + // conservative pass-through until they use the bound-closure + // representation. A class is always its function object (P3 B4a: no + // 0x7FFE class word reaches `bind`). + if !closure_target { return target_value; - }; + } // Body metadata is immutable image data, so this descriptor survives // every moving collection below. Share it across metadata reads and // native-adapter admission; ordinary compiled and bound-method bodies // do not need to read a second receiver operand for resolution. - let target_info = if target_is_closure { - (*target_jv.as_pointer::()).info - } else { - std::ptr::null() - }; + let target_info = (*target_jv.as_pointer::()).info; // Root the bind target across every allocating call below (`this` // boxing, `Get(Target, "name")` — which may run a user getter — the @@ -966,7 +942,7 @@ unsafe fn function_bind_observable( // The birth shape proves name and length are untouched own data // properties. For a compiled body their values are immutable body facts; // use those facts instead of probing its property bag and metadata readers. - let declared_metadata = target_is_closure && { + let declared_metadata = { let target = JSValue::from_bits(target_h.get_nanbox_f64().to_bits()).as_pointer::(); (*target).shape_id == crate::closure::shape::birth_shape_for_body(target_info) @@ -995,7 +971,7 @@ unsafe fn function_bind_observable( crate::closure::info_length(&*target_info) .or_else(|| crate::closure::info_arity(&*target_info)) .unwrap_or(0) as f64 - } else if target_is_closure { + } else { let target = JSValue::from_bits(target_h.get_nanbox_f64().to_bits()).as_pointer::(); if crate::object::has_own_helpers::closure_own_key_present(target as usize, "length") { @@ -1011,8 +987,6 @@ unsafe fn function_bind_observable( } else { 0.0 } - } else { - 0.0 }; // Spec step 12-13: `Get(Target, "name")` must run now, synchronously — a @@ -1026,14 +1000,14 @@ unsafe fn function_bind_observable( // An untouched base Function shape proves its intrinsic name has no // override, accessor or deletion. Its immutable body name can keep the // existing lazy snapshot; an actual own name still runs Get now. - let declared_name = target_is_closure && { + let declared_name = { let target = JSValue::from_bits(target_h.get_nanbox_f64().to_bits()).as_pointer::(); (*target).shape_id == crate::closure::shape::birth_shape_for_body(target_info) && target_info.as_ref().map(|info| info.code) != Some(crate::closure::BOUND_FUNCTION_FUNC_PTR) }; - let name_hint = if target_is_closure && !declared_name { + let name_hint = if !declared_name { target_h .across_nanbox(|| { let tclosure = @@ -1050,10 +1024,7 @@ unsafe fn function_bind_observable( // dangling capture slot 3 (exactly the "lazily-derived name retains a // stale address" failure mode this fix must avoid). // Get(Target, name) yields an empty bound-name suffix for a non-string. - let name_hint = if target_is_closure - && !declared_name - && !JSValue::from_bits(name_hint.to_bits()).is_any_string() - { + let name_hint = if !declared_name && !JSValue::from_bits(name_hint.to_bits()).is_any_string() { let empty = crate::string::js_string_from_bytes(b"".as_ptr(), 0); f64::from_bits(JSValue::string_ptr(empty).bits()) } else { diff --git a/crates/perry-runtime/src/contracts.rs b/crates/perry-runtime/src/contracts.rs index 54aa1c86b90..e611a6b7bad 100644 --- a/crates/perry-runtime/src/contracts.rs +++ b/crates/perry-runtime/src/contracts.rs @@ -406,7 +406,7 @@ fn violate(fact: &str, c: &Claim, cid: u32, op: &str, detail: &str) -> ! { /// The class whose prototype `v` is: a `C.prototype` reference immediate or /// the declared prototype object itself. fn proto_class_id(v: f64) -> Option { - if let Some(cid) = crate::object::class_prototype_ref_id(v) { + if let Some(cid) = crate::object::class_method_home_id(v) { return Some(cid); } let bits = v.to_bits(); diff --git a/crates/perry-runtime/src/object/bound_method_receiver_tests.rs b/crates/perry-runtime/src/object/bound_method_receiver_tests.rs index a38e5387982..3f0c22c2fe8 100644 --- a/crates/perry-runtime/src/object/bound_method_receiver_tests.rs +++ b/crates/perry-runtime/src/object/bound_method_receiver_tests.rs @@ -63,9 +63,6 @@ unsafe fn register_class() -> (f64, f64) { record_setter_11201 as *const () as usize as i64, 1, ); - let proto_ref = super::native_module::class_prototype_ref_value(RECEIVER_TEST_CLASS_ID); - let method = - super::native_module::js_class_method_bind(proto_ref, METHOD.as_ptr(), METHOD.len()); // `class_decl_prototype_value` materializes `C.prototype` only for a // named class, exactly as codegen registers one. const CLASS_NAME: &[u8] = b"Receiver11201"; @@ -75,6 +72,9 @@ unsafe fn register_class() -> (f64, f64) { CLASS_NAME.len() as u32, ); let proto = super::class_registry::class_decl_prototype_value(RECEIVER_TEST_CLASS_ID); + // `C.prototype.whoAmI`: the read's receiver is the prototype object (P3 + // B2/B4a), never the 0x7FFE prototype word. + let method = super::native_module::js_class_method_bind(proto, METHOD.as_ptr(), METHOD.len()); (method, proto) } diff --git a/crates/perry-runtime/src/object/class_constructors.rs b/crates/perry-runtime/src/object/class_constructors.rs index 4f4d7e412fc..b2b32290c1c 100644 --- a/crates/perry-runtime/src/object/class_constructors.rs +++ b/crates/perry-runtime/src/object/class_constructors.rs @@ -115,8 +115,6 @@ pub(crate) fn capture_owner_for_template(start: f64, class_id: u32) -> Option Option { - decl_prototype_own_accessor_bytes(class_id, name.as_bytes()) -} - -pub(crate) fn decl_prototype_own_accessor_bytes(class_id: u32, name: &[u8]) -> Option { - let proto = class_decl_prototype_value(class_id); - let js = crate::JSValue::from_bits(proto.to_bits()); - if !js.is_pointer() { - return None; - } - let obj = js.as_pointer::(); - // The live shape alone decides whether the key is an accessor. A - // declaration cannot resurrect a deleted or replaced property. - let holds = unsafe { - crate::object::key_attrs::attrs_live_in_keys(obj as usize) - && crate::object::key_attrs::object_key_is_accessor(obj, name) - }; - holds.then_some(proto) -} - /// The accessor a read or write of `name` on an instance of `class_id` meets /// on the prototype chain, with the object that holds it: the class's decl /// prototype (materialized on demand) and its ancestors, stopping at the first @@ -307,34 +280,6 @@ unsafe fn accessor_is_shadowed(start: f64, holder: *const ObjectHeader, name: &[ true } -/// `[[Get]]` of `name` through the class prototype chain. -/// `None` when no accessor answers (the caller -/// keeps resolving), otherwise the getter's result — `undefined` for a -/// setter-only accessor. `this_of` supplies the receiver and is called only -/// when an accessor answers (it may consume the inherited-read receiver -/// override). A compiled class getter is called directly; a getter installed -/// by `defineProperty` runs as a closure. -/// -/// # Safety -/// `this_of` returns a live receiver. -pub(crate) unsafe fn class_chain_getter_value( - class_id: u32, - name: &str, - this_of: impl FnOnce() -> f64, -) -> Option<(crate::JSValue, usize)> { - let acc = class_proto_accessor(class_id, name)?.1; - Some(invoke_instance_getter(acc, this_of())) -} - -pub(crate) unsafe fn class_chain_getter_value_bytes( - class_id: u32, - name: &[u8], - this_of: impl FnOnce() -> f64, -) -> Option<(crate::JSValue, usize)> { - let acc = class_proto_accessor_bytes(class_id, name)?.1; - Some(invoke_instance_getter(acc, this_of())) -} - /// Generic instance read, resolved through the receiver's current chain. /// The receiver override is consumed only when an accessor actually answers. pub(crate) unsafe fn instance_chain_getter_value( diff --git a/crates/perry-runtime/src/object/class_registry/decl_prototype_birth.rs b/crates/perry-runtime/src/object/class_registry/decl_prototype_birth.rs index 590647f5cae..068a0627f09 100644 --- a/crates/perry-runtime/src/object/class_registry/decl_prototype_birth.rs +++ b/crates/perry-runtime/src/object/class_registry/decl_prototype_birth.rs @@ -178,7 +178,7 @@ pub(super) fn decl_prototype_born_final(class_id: u32, parent_bits: u64) -> Opti let m = super::declarations::class_method_decl_at(class_id, s.at.method_index()?)?; // The entry is this member's own body: it is named `[desc]`. let f = crate::object::native_module::class_method_declaration_value_bytes( - class_prototype_ref_value(class_id), + class_method_home_word(class_id), display.as_bytes(), m.code as usize, m.param_count, diff --git a/crates/perry-runtime/src/object/class_registry/parent_static/symbol_members.rs b/crates/perry-runtime/src/object/class_registry/parent_static/symbol_members.rs index 164acfbeede..7f07648d877 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static/symbol_members.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static/symbol_members.rs @@ -62,7 +62,7 @@ pub(crate) unsafe fn install_symbol_method( let home = if is_class_object_value(owner.get_nanbox_f64()) { owner.get_nanbox_f64() } else { - crate::object::class_prototype_ref_value(class_id) + crate::object::class_method_home_word(class_id) }; let method = scope.root_nanbox_f64( crate::object::native_module::class_method_declaration_value( diff --git a/crates/perry-runtime/src/object/class_registry/state.rs b/crates/perry-runtime/src/object/class_registry/state.rs index 3cdb08b159b..8be374077d6 100644 --- a/crates/perry-runtime/src/object/class_registry/state.rs +++ b/crates/perry-runtime/src/object/class_registry/state.rs @@ -920,12 +920,6 @@ fn class_recorded_prototype_bits(class_id: u32) -> Option { .then_some(actual) } -/// True when `Object.setPrototypeOf(Ctor, null)` explicitly severed the -/// constructor's prototype chain, as opposed to never having linked one. -pub(crate) fn class_static_prototype_is_nulled(class_id: u32) -> bool { - class_recorded_prototype_bits(class_id) == Some(crate::value::TAG_NULL) -} - /// The constructor-side `[[Prototype]]` recorded for `class_id`, or null. pub(crate) fn class_static_prototype(class_id: u32) -> *mut ObjectHeader { match class_recorded_prototype_bits(class_id) { @@ -1415,7 +1409,7 @@ fn install_class_decl_prototype_method_fields(proto: *mut ObjectHeader, class_id /// is a ConstFn lane (step 5C), and a later store of anything else to it /// generalizes the lane through the store check. fn learn_decl_prototype_method_lanes(proto: *mut ObjectHeader, class_id: u32) { - let home = super::super::class_prototype_ref_value(class_id).to_bits(); + let home = super::super::class_method_home_word(class_id).to_bits(); unsafe { crate::object::shapes::learn_object_constfn_lanes(proto, |_, bits| { class_method_entry_object_of(bits, home) diff --git a/crates/perry-runtime/src/object/class_super_chain.rs b/crates/perry-runtime/src/object/class_super_chain.rs index afa75f841e6..3eba267a50c 100644 --- a/crates/perry-runtime/src/object/class_super_chain.rs +++ b/crates/perry-runtime/src/object/class_super_chain.rs @@ -31,7 +31,7 @@ pub unsafe extern "C" fn js_class_method_home(mut owner: f64, is_static: i32) -> if is_static != 0 { return owner; } - if let Some(cid) = super::class_prototype_ref_id(owner) { + if let Some(cid) = super::class_method_home_id(owner) { return super::class_registry::class_decl_prototype_value(cid); } if let Some(cid) = constructor { diff --git a/crates/perry-runtime/src/object/class_value.rs b/crates/perry-runtime/src/object/class_value.rs index 52b85805dd7..022f589a40f 100644 --- a/crates/perry-runtime/src/object/class_value.rs +++ b/crates/perry-runtime/src/object/class_value.rs @@ -117,16 +117,11 @@ pub(crate) static CLASS_CONSTRUCTOR_INFO: crate::closure::JsFunctionInfo = ); /// The class a class-REF word names: a class function object (the -/// constructor) or a `C.prototype` reference word (`0x7FFE` with -/// `CLASS_PROTOTYPE_REF_FLAG`, which is never an int32 number). `bits` is a -/// NaN-boxed VALUE word. An INT32 number is neither (#11414). +/// constructor). `bits` is a NaN-boxed VALUE word. An INT32 number is none +/// (#11414), and neither is a method's home word. #[inline] pub(crate) fn class_ref_word(bits: u64) -> Option { - match bits >> 48 { - 0x7FFE => super::class_prototype_ref_id(f64::from_bits(bits)), - 0x7FFD => class_closure_id((bits & crate::value::POINTER_MASK) as usize), - _ => None, - } + class_value_id_bits(bits) } /// [`class_ref_word`] for a word that arrived through a POINTER-typed diff --git a/crates/perry-runtime/src/object/class_value/tests.rs b/crates/perry-runtime/src/object/class_value/tests.rs index af1899ce1c6..4616f99df36 100644 --- a/crates/perry-runtime/src/object/class_value/tests.rs +++ b/crates/perry-runtime/src/object/class_value/tests.rs @@ -6,7 +6,6 @@ fn static_property_lookups_do_not_mint_builtin_class_values() { assert!(class_static_get(cid, "from").is_none()); assert!(class_static_own_accessor(cid, "from").is_none()); assert!(crate::object::class_registry::class_static_prototype(cid).is_null()); - assert!(!crate::object::class_registry::class_static_prototype_is_nulled(cid)); assert!(class_value_cached(cid).is_none()); } } @@ -521,8 +520,9 @@ fn first_evaluation_receives_its_heritage_before_later_evaluations() { } /// #11414: an INT32 word is a number, also when it equals a registered class -/// id; only the class function object names the class, and the `C.prototype` -/// ref word (bit 32 set, never an int32 number) keeps naming its class. +/// id; only the class function object names the class. A method's home word +/// (bit 32 set, never an int32 number) is runtime-internal and names no class +/// as a value either. #[test] fn an_int32_equal_to_a_registered_class_id_is_a_number() { let cid = 0x6A10; @@ -533,9 +533,10 @@ fn an_int32_equal_to_a_registered_class_id_is_a_number() { assert_eq!(class_ref_word(int.to_bits()), None); assert_eq!(crate::object::class_ref_id(int), None); assert_eq!(class_ref_word(class.to_bits()), Some(cid)); - let proto_ref = crate::object::class_prototype_ref_value(cid); - assert_eq!(class_ref_word(proto_ref.to_bits()), Some(cid)); - assert_eq!(class_value_id(proto_ref), None); + let home = crate::object::class_method_home_word(cid); + assert_eq!(class_ref_word(home.to_bits()), None); + assert_eq!(crate::object::class_ref_id(home), None); + assert_eq!(class_value_id(home), None); assert!(matches!( crate::builtins::arithmetic::classify_value_typeof(int), crate::builtins::arithmetic::ValueTypeofTag::Number diff --git a/crates/perry-runtime/src/object/delete_rest.rs b/crates/perry-runtime/src/object/delete_rest.rs index bcd1cb3453f..21d4f042568 100644 --- a/crates/perry-runtime/src/object/delete_rest.rs +++ b/crates/perry-runtime/src/object/delete_rest.rs @@ -855,23 +855,6 @@ fn class_delete_own_key(class_id: u32, name: &str) -> i32 { 1 } -fn delete_class_prototype_key(class_id: u32, name: &str) -> i32 { - // The members are real properties of the class's prototype object - // (materialized first): the delete happens there. - let proto = super::class_registry::class_decl_prototype_value(class_id); - let js = crate::JSValue::from_bits(proto.to_bits()); - if !js.is_pointer() { - return 1; - } - let scope = crate::gc::RuntimeHandleScope::new(); - let proto = scope.root_nanbox_f64(proto); - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - js_object_delete_field( - (proto.get_nanbox_f64().to_bits() & crate::value::POINTER_MASK) as *mut ObjectHeader, - key, - ) -} - /// `delete prim.field` (static key): once RequireObjectCoercible has rejected /// null/undefined, a primitive receiver (number/boolean/…) has no deletable own /// property, so `delete` is a no-op that evaluates to `true` (spec ToObject of a @@ -885,16 +868,6 @@ pub extern "C" fn js_object_delete_field_value( ) -> i32 { #[cfg(perry_contract_checks)] crate::contracts::hard_value(obj_value, "delete"); - if let Some(class_id) = super::class_prototype_ref_id(obj_value) { - if key.is_null() { - return 1; - } - return unsafe { - super::has_own_helpers::str_from_string_header(key) - .map(|name| delete_class_prototype_key(class_id, name)) - .unwrap_or(1) - }; - } // A class reference (`delete C.m` for a `static m()`) is INT32-tagged, so // `is_pointer` is false and the guard below would no-op it. But a static // member delete must still unregister the method/field. `js_object_delete_field` @@ -939,17 +912,6 @@ pub extern "C" fn js_object_delete_dynamic_value(obj_value: f64, key: f64) -> i3 pub(crate) fn delete_dynamic_value(obj_value: f64, key: f64) -> i32 { #[cfg(perry_contract_checks)] crate::contracts::hard_value(obj_value, "delete"); - if let Some(class_id) = super::class_prototype_ref_id(obj_value) { - if unsafe { crate::symbol::js_is_symbol(key) } != 0 { - let proto = super::class_registry::class_decl_prototype_value(class_id); - return unsafe { crate::symbol::js_object_delete_symbol_property(proto, key) }; - } - return unsafe { - super::native_module::metadata_key_to_string(key) - .map(|name| delete_class_prototype_key(class_id, &name)) - .unwrap_or(1) - }; - } // Class-ref receiver (`delete C["m"]`): see `js_object_delete_field_value`. if let Some(class_id) = super::native_module::class_ref_id(obj_value) { // A symbol key is an own symbol property of the class function object. diff --git a/crates/perry-runtime/src/object/descriptors.rs b/crates/perry-runtime/src/object/descriptors.rs index 6840c19ff55..7ea5536446b 100644 --- a/crates/perry-runtime/src/object/descriptors.rs +++ b/crates/perry-runtime/src/object/descriptors.rs @@ -12,29 +12,8 @@ pub(crate) use own_keys::{OwnNamesSnapshot, OwnProperty}; unsafe fn class_ref_wtf8_own_property( obj_value: f64, class_id: u32, - key_value: f64, name: &[u8], ) -> Option { - let is_prototype = super::class_prototype_ref_id(obj_value).is_some(); - if is_prototype { - if super::class_registry::class_proto_key_deleted_bytes(class_id, name) { - return None; - } - if let Some(proto) = - super::class_registry::decl_prototype_own_accessor_bytes(class_id, name) - { - return own_property(proto, key_value); - } - if super::native_module::class_has_own_method_bytes(class_id, name) { - if let Some(value) = - super::class_registry::class_method_slot_value_bytes(class_id, name) - { - return OwnProperty::data(f64::from_bits(value), true, false, true); - } - } - return None; - } - if super::class_registry::class_static_key_deleted_bytes(class_id, name) { return None; } @@ -591,54 +570,39 @@ pub(crate) unsafe fn own_property(obj_value: f64, key_value: f64) -> Option name, Err(_) => { - return class_ref_wtf8_own_property( - obj_value, - class_id, - key_value, - &method_name_bytes, - ) + return class_ref_wtf8_own_property(obj_value, class_id, &method_name_bytes) } }; - if if class_prototype_ref_id(obj_value).is_some() { - super::class_registry::class_proto_key_deleted(class_id, &method_name) - } else { - super::class_registry::class_static_key_deleted(class_id, &method_name) - } { + if super::class_registry::class_static_key_deleted(class_id, &method_name) { return None; } // Private registry entries retain their source spelling, but // a public computed static field named `"#x"` is a distinct // String property and must remain reflectable. if method_name.starts_with('#') { - if super::class_prototype_ref_id(obj_value).is_none() { - if let Some(v) = super::class_registry::class_own_static_field_value( - class_id, - &method_name, - ) { - return OwnProperty::data(v, true, true, true); - } + if let Some(v) = + super::class_registry::class_own_static_field_value(class_id, &method_name) + { + return OwnProperty::data(v, true, true, true); } return None; } - if super::class_prototype_ref_id(obj_value).is_none() { - if let Some((acc, attrs)) = - super::class_registry::class_dynamic_static_accessor_descriptor( - class_id, - &method_name, - obj_value, - ) - { - let undef = crate::value::TAG_UNDEFINED; - return OwnProperty::accessor( - f64::from_bits(if acc.get == 0 { undef } else { acc.get }), - f64::from_bits(if acc.set == 0 { undef } else { acc.set }), - attrs.enumerable(), - attrs.configurable(), - ); - } + if let Some((acc, attrs)) = + super::class_registry::class_dynamic_static_accessor_descriptor( + class_id, + &method_name, + obj_value, + ) + { + let undef = crate::value::TAG_UNDEFINED; + return OwnProperty::accessor( + f64::from_bits(if acc.get == 0 { undef } else { acc.get }), + f64::from_bits(if acc.set == 0 { undef } else { acc.set }), + attrs.enumerable(), + attrs.configurable(), + ); } if method_name == "name" - && super::class_prototype_ref_id(obj_value).is_none() && super::class_registry::lookup_static_method_in_chain(class_id, "name") .is_none() // #7190: an own static `name` — installed by @@ -664,56 +628,28 @@ pub(crate) unsafe fn own_property(obj_value: f64, key_value: f64) -> Option f64 { return f64::from_bits((out as u64) | 0x7FFD_0000_0000_0000); } if let Some(class_id) = class_ref_id(obj_value) { - let is_prototype_ref = super::class_prototype_ref_id(obj_value).is_some(); // The class function object's own keys, in its bag's creation // order: a key deleted and defined again is a new key and comes // last, as for any object. - if !is_prototype_ref { - if let Some(fo) = crate::object::class_value::class_value_if_minted(class_id) { - let ptr = fo as usize; - // This agent's live class closure (the enclosing `unsafe`). - let names = crate::closure::bag_own_key_names_bytes(ptr); - let mut out: Vec> = Vec::new(); - for intrinsic in [b"length".as_slice(), b"name", b"prototype"] { - if !names.iter().any(|n| n == intrinsic) - && !crate::closure::closure_is_key_deleted( - ptr, + if let Some(fo) = crate::object::class_value::class_value_if_minted(class_id) { + let ptr = fo as usize; + // This agent's live class closure (the enclosing `unsafe`). + let names = crate::closure::bag_own_key_names_bytes(ptr); + let mut out: Vec> = Vec::new(); + for intrinsic in [b"length".as_slice(), b"name", b"prototype"] { + if !names.iter().any(|n| n == intrinsic) + && !crate::closure::closure_is_key_deleted( + ptr, + std::str::from_utf8(intrinsic).unwrap(), + ) + && (intrinsic == b"prototype" + || !super::class_registry::class_static_key_deleted( + class_id, std::str::from_utf8(intrinsic).unwrap(), - ) - && (intrinsic == b"prototype" - || !super::class_registry::class_static_key_deleted( - class_id, - std::str::from_utf8(intrinsic).unwrap(), - )) - { - out.push(intrinsic.to_vec()); - } - } - for name in names { - if name.starts_with(b"#") - || super::field_get_set::is_internal_runtime_key_bytes(&name) - { - continue; - } - push_unique_name_bytes(&mut out, name); + )) + { + out.push(intrinsic.to_vec()); } - sort_property_name_bytes_ecma(&mut out); - let result = crate::array::js_array_alloc(out.len() as u32); - for name in out { - let str_ptr = - crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - crate::array::js_array_push(result, JSValue::string_ptr(str_ptr)); + } + for name in names { + if name.starts_with(b"#") + || super::field_get_set::is_internal_runtime_key_bytes(&name) + { + continue; } - return f64::from_bits((result as u64) | 0x7FFD_0000_0000_0000); + push_unique_name_bytes(&mut out, name); } - } - let mut names: Vec> = if is_prototype_ref { - vec![b"constructor".to_vec()] - } else { - vec![b"length".to_vec(), b"name".to_vec(), b"prototype".to_vec()] - }; - for name in - super::class_registry::class_own_string_member_names(class_id, !is_prototype_ref) - { - let deleted = if is_prototype_ref { - super::class_registry::class_proto_key_deleted_bytes(class_id, &name) - } else { - super::class_registry::class_static_key_deleted_bytes(class_id, &name) - }; - if !deleted { - push_unique_name_bytes(&mut names, name); + sort_property_name_bytes_ecma(&mut out); + let result = crate::array::js_array_alloc(out.len() as u32); + for name in out { + let str_ptr = + crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + crate::array::js_array_push(result, JSValue::string_ptr(str_ptr)); } + return f64::from_bits((result as u64) | 0x7FFD_0000_0000_0000); } - if !is_prototype_ref { - for name in super::class_registry::class_own_dynamic_prop_names(class_id) { - push_unique_name_bytes(&mut names, name.into_bytes()); - } - // Accessor properties a `defineProperty` added. - for name in crate::object::class_value::class_static_accessor_names_bytes(class_id) - { + let mut names: Vec> = + vec![b"length".to_vec(), b"name".to_vec(), b"prototype".to_vec()]; + for name in super::class_registry::class_own_string_member_names(class_id, true) { + if !super::class_registry::class_static_key_deleted_bytes(class_id, &name) { push_unique_name_bytes(&mut names, name); } } + for name in super::class_registry::class_own_dynamic_prop_names(class_id) { + push_unique_name_bytes(&mut names, name.into_bytes()); + } + // Accessor properties a `defineProperty` added. + for name in crate::object::class_value::class_static_accessor_names_bytes(class_id) { + push_unique_name_bytes(&mut names, name); + } names.retain(|n| { !super::field_get_set::is_internal_runtime_key_bytes(n) // A deleted `length` / `name` is no longer own. - && !(!is_prototype_ref - && (n.as_slice() == b"length" || n.as_slice() == b"name") + && !((n.as_slice() == b"length" || n.as_slice() == b"name") && super::class_registry::class_static_key_deleted_bytes(class_id, n)) }); sort_property_name_bytes_ecma(&mut names); diff --git a/crates/perry-runtime/src/object/descriptors/builders.rs b/crates/perry-runtime/src/object/descriptors/builders.rs index 3a420b9b700..c80e291e136 100644 --- a/crates/perry-runtime/src/object/descriptors/builders.rs +++ b/crates/perry-runtime/src/object/descriptors/builders.rs @@ -55,10 +55,6 @@ pub(crate) unsafe fn symbol_own_property_descriptor(obj_value: f64, key_value: f if sym_key == 0 { return f64::from_bits(crate::value::TAG_UNDEFINED); } - if let Some(cid) = super::class_prototype_ref_id(obj_value) { - let proto = super::class_registry::class_decl_prototype_value(cid); - return symbol_own_property_descriptor(proto, key_value); - } let owner = super::class_ref_id(obj_value) .and_then(super::class_value::class_value_if_minted) .map(|ptr| ptr as usize) diff --git a/crates/perry-runtime/src/object/field_get_set/class_object_props.rs b/crates/perry-runtime/src/object/field_get_set/class_object_props.rs index 1fbb06dae68..95f51a5add0 100644 --- a/crates/perry-runtime/src/object/field_get_set/class_object_props.rs +++ b/crates/perry-runtime/src/object/field_get_set/class_object_props.rs @@ -261,6 +261,8 @@ pub(crate) unsafe fn finish_class_evaluation_prototype( class_id, &name, class_value, + proto + .with_mut_ptr::(|p| crate::value::js_nanbox_pointer(p as i64)), ) }); proto.with_mut_ptr::(|proto| { diff --git a/crates/perry-runtime/src/object/field_get_set/enumeration.rs b/crates/perry-runtime/src/object/field_get_set/enumeration.rs index 80a094a663a..bdf872b2fd7 100644 --- a/crates/perry-runtime/src/object/field_get_set/enumeration.rs +++ b/crates/perry-runtime/src/object/field_get_set/enumeration.rs @@ -172,33 +172,31 @@ pub extern "C" fn js_object_keys_value(value: f64) -> *mut ArrayHeader { ) }; } - // A class constructor ref `C` is an INT32-tagged value (not a pointer), so it - // would otherwise fall through to the empty-array tail below. Its enumerable + // A class constructor `C` is a class function object (not an ObjectHeader), + // so it would otherwise fall through to the empty-array tail below. Its enumerable // own keys are the static fields registered in CLASS_DYNAMIC_PROPS — built-in // `length`/`name`/`prototype` and static methods are non-enumerable. Backs // `Object.keys(C)` / `for (k in C)` (test262 class/elements static-field-*). if let Some(class_id) = super::super::class_ref_id(value) { - if super::super::class_prototype_ref_id(value).is_none() { - // Static accessors are defined before static fields, so an - // enumerable one (#10480) precedes them. - let mut names: Vec = - crate::object::class_value::class_static_accessor_names(class_id) - .into_iter() - .filter(|name| { - crate::object::class_value::class_static_own_accessor(class_id, name) - .is_some_and(|(_, enumerable, _)| enumerable) - }) - .collect(); - names.extend(super::super::class_registry::class_own_enumerable_field_names(class_id)); - super::super::descriptors::sort_property_names_ecma(&mut names); - let arr = crate::array::js_array_alloc(names.len().max(1) as u32); - let mut out = arr; - for name in names { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - out = crate::array::js_array_push(out, JSValue::string_ptr(key)); - } - return out; + // Static accessors are defined before static fields, so an + // enumerable one (#10480) precedes them. + let mut names: Vec = + crate::object::class_value::class_static_accessor_names(class_id) + .into_iter() + .filter(|name| { + crate::object::class_value::class_static_own_accessor(class_id, name) + .is_some_and(|(_, enumerable, _)| enumerable) + }) + .collect(); + names.extend(super::super::class_registry::class_own_enumerable_field_names(class_id)); + super::super::descriptors::sort_property_names_ecma(&mut names); + let arr = crate::array::js_array_alloc(names.len().max(1) as u32); + let mut out = arr; + for name in names { + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + out = crate::array::js_array_push(out, JSValue::string_ptr(key)); } + return out; } if jv.is_reference() { let ptr = jv.reference_payload(); diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs index 24fecd73f44..68dbb3b9125 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs @@ -119,7 +119,6 @@ pub(crate) fn class_value_get_field( } } let class_value = crate::object::class_value::boxed_class_word(bits); - let is_prototype_ref = super::super::class_prototype_ref_id(class_value).is_some(); unsafe { let name_ptr = (key as *const u8).add(std::mem::size_of::()); let name_len = (*key).byte_len as usize; @@ -127,47 +126,9 @@ pub(crate) fn class_value_get_field( let name = std::str::from_utf8(name_bytes).unwrap_or(""); if name.is_empty() && !name_bytes.is_empty() { let name_bytes = name_bytes.to_vec(); - return class_value_get_wtf8_field( - class_value, - class_id, - is_prototype_ref, - key, - &name_bytes, - ); - } - // v0.5.752: class_ref.constructor synthesizes back to the - // same class ref so drizzle's - // `Object.getPrototypeOf(value).constructor === Class` chain - // collapses correctly (with v0.5.751's getPrototypeOf - // returning the class ref for instance receivers). Refs - // #420 / #618 followup. - if is_prototype_ref - && name == "constructor" - && class_id != 0 - && class_has_own_method(class_id, name) - { - let value = crate::object::class_method_slot_value(class_id, name) - .map(f64::from_bits) - .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); - return JSValue::from_bits(value.to_bits()); - } - if name == "constructor" - && is_prototype_ref - && class_id != 0 - && is_class_id_registered(class_id) - { - let value = if is_prototype_ref { - super::super::class_constructor_ref_value(class_id) - } else { - class_value - }; - return JSValue::from_bits(value.to_bits()); + return class_value_get_wtf8_field(class_value, class_id, key, &name_bytes); } - if name == "prototype" - && class_id != 0 - && is_class_id_registered(class_id) - && !is_prototype_ref - { + if name == "prototype" && class_id != 0 && is_class_id_registered(class_id) { // P3 B2 (DESIGN-p3 rev 3.1 §3, REVIEW-p3 D3): the declaration // path is the answer. A declared class builds its prototype // lazily on this first read (`class_decl_prototype_value`); an id @@ -192,36 +153,31 @@ pub(crate) fn class_value_get_field( // evaluation first, preserving its own-property and pinned // static-parent semantics; a miss continues through the // ordinary ClassRef registry path below. - if !is_prototype_ref { - if let Some(class_object) = - super::super::class_registry::class_object_value_for_cid(class_id) - { - let class_object = JSValue::from_bits(class_object.to_bits()); - if class_object.is_pointer() { - let class_object = class_object.as_pointer::(); - if !class_object.is_null() && class_object as usize != obj as usize { - let value = js_object_get_field_by_name(class_object, key); - if !value.is_undefined() { - return value; - } + if let Some(class_object) = + super::super::class_registry::class_object_value_for_cid(class_id) + { + let class_object = JSValue::from_bits(class_object.to_bits()); + if class_object.is_pointer() { + let class_object = class_object.as_pointer::(); + if !class_object.is_null() && class_object as usize != obj as usize { + let value = js_object_get_field_by_name(class_object, key); + if !value.is_undefined() { + return value; } } } } - if !is_prototype_ref { - if let Some(value) = crate::object::class_value::constructor_property_get( - (bits & crate::value::POINTER_MASK) as usize, - key, - class_id, - ) { - return value; - } + if let Some(value) = crate::object::class_value::constructor_property_get( + (bits & crate::value::POINTER_MASK) as usize, + key, + class_id, + ) { + return value; } - // Instance (prototype) methods must only resolve when reading - // off the prototype ref (`C.prototype.m`), NOT off the class ref - // itself (`C.m`). In JS a class object does not expose its - // prototype methods as static members: `class C { m(){} }` has - // `C.m === undefined` (the method lives on `C.prototype`). The + // Instance (prototype) methods never resolve off the class ref + // (`C.m`). In JS a class object does not expose its prototype + // methods as static members: `class C { m(){} }` has + // `C.m === undefined` (the method lives on `C.prototype`). An // earlier unconditional lookup leaked instance methods onto the // class ref, so `C.m` returned a (mis-bound) function. This // broke NestJS interceptor/guard/pipe resolution: its @@ -232,24 +188,6 @@ pub(crate) fn class_value_get_field( // returned `{}`, which rxjs `innerFrom` then rejected. Real // static methods are resolved below via // `lookup_static_method_in_chain`. - if is_prototype_ref && class_id != 0 && class_has_own_method(class_id, name) { - let value = crate::object::class_method_slot_value(class_id, name) - .map(f64::from_bits) - .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); - return JSValue::from_bits(value.to_bits()); - } - if is_prototype_ref { - // Class accessors are properties of the class prototype - // chain (charter step 3); `this` is the prototype ref. - if let Some((v, _)) = - super::super::class_registry::class_chain_getter_value(class_id, name, || { - class_value - }) - { - return v; - } - return JSValue::undefined(); - } // Empty-string is a legal static member key (`static get ''()`); // the `!name.is_empty()` guard below skips it, so resolve a // static accessor named "" here (Test262 accessor-name-static @@ -473,7 +411,6 @@ pub(crate) fn class_value_get_field( } if name == "length" && class_id != 0 - && !is_prototype_ref && !super::super::class_registry::class_static_key_deleted(class_id, name) { if let Some(length) = super::super::class_registry::class_length_for_id(class_id) { @@ -485,11 +422,9 @@ pub(crate) fn class_value_get_field( // (`const bind = C.bind`) just as the closure path does; // the captured ClassRef is accepted by native method // dispatch and by `js_function_bind`. - if !is_prototype_ref { - if let Some(method) = super::reified_function_method_name(name) { - let value = crate::closure::reify_function_method_value(class_value, method); - return JSValue::from_bits(value.to_bits()); - } + if let Some(method) = super::reified_function_method_name(name) { + let value = crate::closure::reify_function_method_value(class_value, method); + return JSValue::from_bits(value.to_bits()); } // No own static / inherited entry resolved the name. A class // constructor is a function, so a bare read of `.caller` or @@ -497,9 +432,8 @@ pub(crate) fn class_value_get_field( // on `Function.prototype` — strict-mode throws (Perry only // compiles strict code). Placed last so any own static field, // accessor, or `defineProperty`-installed data prop of that - // name takes precedence. Prototype-refs (`C.prototype`) are - // plain objects and are excluded. - if !is_prototype_ref && matches!(name, "caller" | "arguments") { + // name takes precedence. + if matches!(name, "caller" | "arguments") { crate::fs::validate::throw_type_error_with_code( "Restricted function property access", "ERR_INVALID_ARG_TYPE", @@ -509,12 +443,9 @@ pub(crate) fn class_value_get_field( // so a user method or expando installed there // (`Function.prototype.myHelper = fn`) is readable through // `C.myHelper` exactly as through a closure. - if !is_prototype_ref { - if let Some(v) = - crate::closure::function_prototype_inherited_get(0, name, class_value) - { - return JSValue::from_bits(v.to_bits()); - } + if let Some(v) = crate::closure::function_prototype_inherited_get(0, name, class_value) + { + return JSValue::from_bits(v.to_bits()); } } // The built-in constructor object's `constructor` value is diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name/class_wtf8.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name/class_wtf8.rs index a566b716fd6..ec41f045483 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name/class_wtf8.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name/class_wtf8.rs @@ -1,45 +1,20 @@ //! WTF-8 static property reads for legacy class references. use super::*; -/// `C.key` for a class constructor value (its function object, or the legacy -/// INT32 immediate / `C.prototype` reference): the class-static lookup. Split +/// `C.key` for a class constructor value (its function object): the +/// class-static lookup. Split /// out so a class function object is routed here BEFORE the closure arms of /// the generic read (`get_field_by_name_after_site_miss`), which it /// would otherwise walk end to end first. pub(super) unsafe fn class_value_get_wtf8_field( class_value: f64, class_id: u32, - is_prototype_ref: bool, key: *const crate::StringHeader, name: &[u8], ) -> JSValue { let scope = crate::gc::RuntimeHandleScope::new(); let class_value = scope.root_nanbox_f64(class_value); let key = scope.root_string_ptr(key); - if is_prototype_ref { - if crate::object::native_module::class_has_own_method_bytes(class_id, name) { - if let Some(value) = - crate::object::class_registry::class_method_slot_value_bytes(class_id, name) - { - return JSValue::from_bits(value); - } - } - if let Some((value, _)) = - crate::object::class_registry::class_chain_getter_value_bytes(class_id, name, || { - class_value.get_nanbox_f64() - }) - { - return value; - } - return key.with_const_ptr::(|key| { - crate::object::class_value::class_prototype_get( - class_id, - key, - class_value.get_nanbox_f64(), - ) - }); - } - // A capture-carrying class declaration may be referenced through its // template ClassRef from an earlier-created helper closure. Runtime-added // static properties live on the current evaluation's heap class object, diff --git a/crates/perry-runtime/src/object/field_get_set/has_property.rs b/crates/perry-runtime/src/object/field_get_set/has_property.rs index 309ff63d879..661f4d996f9 100644 --- a/crates/perry-runtime/src/object/field_get_set/has_property.rs +++ b/crates/perry-runtime/src/object/field_get_set/has_property.rs @@ -469,12 +469,7 @@ fn object_has_property_generic(obj: f64, key: f64) -> f64 { let bits = obj.to_bits(); if let Some(class_id) = crate::object::class_value::class_ref_word(bits) { // Symbol key path. - let found = if crate::object::class_prototype_ref_id(obj).is_some() { - crate::symbol::class_static_symbol_lookup(class_id, key) - } else { - crate::symbol::class_static_symbol_lookup_in_chain(class_id, key) - }; - if found.is_some() { + if crate::symbol::class_static_symbol_lookup_in_chain(class_id, key).is_some() { return nanbox_true; } // #6149: string key on a class ref (`"prototype" in C`, @@ -520,15 +515,12 @@ fn object_has_property_generic(obj: f64, key: f64) -> f64 { } // #11492: a constructor's chain ends at %Function.prototype%; // a user member installed there is `in` every class, as - // `C.myHelper` reads it. A prototype ref is excluded — its - // chain is `Object.prototype`, not `Function.prototype`. - if crate::object::class_prototype_ref_id(obj).is_none() { - if let Some(proto_ptr) = - crate::closure::function_prototype_fallback_target(0, name) - { - let proto = crate::value::js_nanbox_pointer(proto_ptr as i64); - return js_object_has_property(f64::from_bits(proto.to_bits()), key); - } + // `C.myHelper` reads it. + if let Some(proto_ptr) = + crate::closure::function_prototype_fallback_target(0, name) + { + let proto = crate::value::js_nanbox_pointer(proto_ptr as i64); + return js_object_has_property(f64::from_bits(proto.to_bits()), key); } } } diff --git a/crates/perry-runtime/src/object/field_set_by_name.rs b/crates/perry-runtime/src/object/field_set_by_name.rs index 7ba695b4ba5..b0ba4a36646 100644 --- a/crates/perry-runtime/src/object/field_set_by_name.rs +++ b/crates/perry-runtime/src/object/field_set_by_name.rs @@ -479,17 +479,7 @@ pub extern "C" fn js_object_set_field_by_name( let name = name_storage.as_slice(); let name_str = std::str::from_utf8(name).ok(); let recv = crate::object::class_value::boxed_class_word(bits); - let is_prototype_ref = super::class_prototype_ref_id(recv).is_some(); - if is_prototype_ref { - super::class_registry::class_prototype_set_bytes( - class_id, - name, - value.to_bits(), - ); - return; - } - if !is_prototype_ref - && name_str == Some("name") + if name_str == Some("name") && !super::class_registry::class_static_key_deleted_bytes(class_id, name) && super::class_registry::lookup_static_method_in_chain_bytes( class_id, name, @@ -512,11 +502,8 @@ pub extern "C" fn js_object_set_field_by_name( } // Writing `.caller` / `.arguments` on a class constructor // hits the poison-pill %ThrowTypeError% accessor inherited - // from Function.prototype. Prototype refs are plain objects. - if !is_prototype_ref - && !has_own_data - && matches!(name_str, Some("caller" | "arguments")) - { + // from Function.prototype. + if !has_own_data && matches!(name_str, Some("caller" | "arguments")) { crate::fs::validate::throw_type_error_with_code( "Restricted function property access", "ERR_INVALID_ARG_TYPE", diff --git a/crates/perry-runtime/src/object/global_this/array_error.rs b/crates/perry-runtime/src/object/global_this/array_error.rs index b6e4f9fe857..319b42914b6 100644 --- a/crates/perry-runtime/src/object/global_this/array_error.rs +++ b/crates/perry-runtime/src/object/global_this/array_error.rs @@ -664,15 +664,13 @@ pub(crate) extern "C" fn function_prototype_to_string_thunk( let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); } - // A class reference (INT32-tagged registered class id) is a function + // A class constructor (a class function object) is a function // value; #9413 retains its source text at compile time, so answer with // that and keep the NativeFunction form only for classes with none. - if super::super::class_prototype_ref_id(this_val).is_none() { - if let Some(cid) = super::super::native_module::class_ref_id(this_val) { - let s = super::super::class_registry::class_ref_to_string(cid); - let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); - } + if let Some(cid) = super::super::native_module::class_ref_id(this_val) { + let s = super::super::class_registry::class_ref_to_string(cid); + let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + return f64::from_bits(JSValue::string_ptr(str_ptr).bits()); } // A per-evaluation class object (`ClassExprFresh`) is a function too. if let Some(source) = super::super::field_get_set::class_object_source_text(this_val) { diff --git a/crates/perry-runtime/src/object/instanceof/proxy_rhs.rs b/crates/perry-runtime/src/object/instanceof/proxy_rhs.rs index 36a4088747f..33e0eb2e5ef 100644 --- a/crates/perry-runtime/src/object/instanceof/proxy_rhs.rs +++ b/crates/perry-runtime/src/object/instanceof/proxy_rhs.rs @@ -67,7 +67,6 @@ pub(super) fn ordinary_proxy_has_instance(constructor: f64, value: f64) -> bool )); if !rhs_is_object_value(prototype.get_nanbox_f64()) && class_ref_id(prototype.get_nanbox_f64()).is_none() - && class_prototype_ref_id(prototype.get_nanbox_f64()).is_none() { throw_type_error(b"Function has non-object prototype in instanceof check"); } diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 6f16ca646c9..5d04c36da08 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -229,7 +229,7 @@ pub(crate) use native_module::class_ref_id; pub(crate) use native_module::install_native_module_vtable; pub(crate) use native_module::{class_instance_has_method, class_method_value_target}; pub(crate) use native_module::{ - class_method_entry_source_func_ptr, class_prototype_ref_id, SYMBOL_BOUND_METHOD_NAME, + class_method_entry_source_func_ptr, class_method_home_id, SYMBOL_BOUND_METHOD_NAME, }; mod native_module_crypto_key_object; mod native_module_crypto_random; diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index 4751df69e5c..0d663351743 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -1750,8 +1750,8 @@ pub(crate) unsafe fn native_call_method_tower( let args = refreshed_args(); // The proxy is the call's receiver, matching the receiver semantics // of a normal `obj.method(args)` invocation. A canonical class-method - // value takes it via `canonical_bound_method_receiver` (#6699 routes a - // proxy receiver through there). + // value takes it as its call-time `this` in `dispatch_bound_method` + // (#6699 routes a proxy receiver through there). let proxy_class_id = crate::proxy_hooks::proxy_target_class_id(object_handle.get_nanbox_f64()); let is_static_class_method = proxy_class_id.is_some_and(|class_id| { diff --git a/crates/perry-runtime/src/object/native_call_method/common_methods.rs b/crates/perry-runtime/src/object/native_call_method/common_methods.rs index 240f543c830..03c46842f0a 100644 --- a/crates/perry-runtime/src/object/native_call_method/common_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/common_methods.rs @@ -217,18 +217,16 @@ pub(super) unsafe fn dispatch_common( // Common string methods on string values "toString" => { - // A class REFERENCE (INT32-tagged registered class id) is a + // A class constructor (a class function object) is a // function value: `C.toString()` must produce the class source, // not the numeric rendering of its class id ("1"). #9413 retains // that source at compile time; classes perry synthesized (no // registered source) still get the NativeFunction form, which // Test262's assertToStringOrNativeFunction accepts. - if super::class_prototype_ref_id(object).is_none() { - if let Some(cid) = super::native_module::class_ref_id(object) { - let s = super::class_registry::class_ref_to_string(cid); - let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); - return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); - } + if let Some(cid) = super::native_module::class_ref_id(object) { + let s = super::class_registry::class_ref_to_string(cid); + let str_ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + return Some(f64::from_bits(JSValue::string_ptr(str_ptr).bits())); } // A per-evaluation class object (`ClassExprFresh`) is a function too. if let Some(source) = super::field_get_set::class_object_default_to_string(object) { diff --git a/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs index 0483a6d6519..8085f0c73b3 100644 --- a/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs +++ b/crates/perry-runtime/src/object/native_call_method/primitive_methods.rs @@ -72,21 +72,7 @@ unsafe fn class_receiver_arm( ) -> Option { let refreshed_args = || crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(arg_handles); let _ = (root_scope, &refreshed_args); - if crate::object::class_prototype_ref_id(object).is_some() { - if let Some((func_ptr, param_count, has_synthetic_arguments, has_rest)) = - crate::object::class_registry::class_method_slot_target(class_id, method_name) - { - return Some(crate::object::class_registry::call_vtable_method( - func_ptr, - object.to_bits() as i64, - args_ptr, - args_len, - param_count, - has_synthetic_arguments, - has_rest, - )); - } - } else if class_id != 0 + if class_id != 0 && crate::object::class_registry::lookup_static_method_in_chain(class_id, method_name) .is_some() { diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index c592d74d673..e5ab50bf337 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -1420,8 +1420,8 @@ include!("native_module/class_method_values.rs"); /// symbol method has no string name to re-resolve at call time, so the /// closure instead carries the already-resolved dispatch data in two extra /// capture slots: -/// slot 0: receiver (NaN-boxed instance/prototype-ref, or the INT32 class -/// ref for a static method) +/// slot 0: receiver (NaN-boxed instance, or the class constructor for a +/// static method) /// slot 1: `SYMBOL_BOUND_METHOD_NAME.as_ptr()` — the discriminant, compared /// by ADDRESS in `dispatch_bound_method`, never by content /// slot 2: `SYMBOL_BOUND_METHOD_NAME.len()` @@ -1524,23 +1524,7 @@ pub(crate) fn is_static_bound_method_value(value: f64) -> bool { return false; } let captured = crate::closure::js_closure_get_capture_f64(closure, 0); - (class_ref_id(captured).is_some() && class_prototype_ref_id(captured).is_none()) - || class_registry::is_class_object_value(captured) -} - -pub(crate) fn canonical_bound_method_receiver( - captured: f64, - call_this: crate::closure::JsThis, -) -> f64 { - if class_prototype_ref_id(captured).is_some() { - // The captured prototype identifies the method's owner, not its receiver. - // Class methods are strict: every call-site value, including null, - // undefined, primitives and arrays, must reach the body unchanged. - // Dispatch resolves the body from the captured owner independently. - call_this.as_f64() - } else { - captured - } + class_ref_id(captured).is_some() || class_registry::is_class_object_value(captured) } /// The `class_id` of `instance`, when `instance` really is a class instance. @@ -1573,7 +1557,7 @@ fn class_id_from_method_receiver_known(instance: f64, class_ref: Option) -> // the call-site `this` was the `assert` function) would mis-substitute the // function object as the receiver and dispatch `assert.method(...)` // instead of `C.prototype.method`, bypassing the generator wrapper's - // param prologue. See `canonical_bound_method_receiver`. + // param prologue. See `dispatch_bound_method`. if crate::closure::is_closure_ptr(obj as usize) { return None; } diff --git a/crates/perry-runtime/src/object/native_module/class_method_bind.rs b/crates/perry-runtime/src/object/native_module/class_method_bind.rs index 9d70acd965c..bc1fd5070a4 100644 --- a/crates/perry-runtime/src/object/native_module/class_method_bind.rs +++ b/crates/perry-runtime/src/object/native_module/class_method_bind.rs @@ -98,29 +98,18 @@ pub extern "C" fn js_class_method_bind( // whose bare invocation read `this.options` off an unconstructed // receiver. Fall through to `build_bound_method_closure`: its // call-time dispatch (`js_native_call_method`'s 0x7FFE arm) - // resolves statics-first for constructor refs. PROTOTYPE refs - // (`C.prototype.m`) keep the canonical path — the instance method - // is exactly what they name. - let receiver_class_ref = class_ref_id(instance); - let receiver_is_constructor_ref = - receiver_class_ref.is_some() && class_prototype_ref_id(instance).is_none(); - if !receiver_is_constructor_ref && bound_native_method_length(name).is_none() { - if let Some(class_id) = - class_id_from_method_receiver_known(instance, receiver_class_ref) - { + // resolves statics-first for constructor refs. + if class_ref_id(instance).is_none() && bound_native_method_length(name).is_none() { + if let Some(class_id) = class_id_from_method_receiver_known(instance, None) { let private_owner = super::take_private_method_owner_hint(name); if private_owner.is_none() && !crate::object::native_call_method::class_holder::name_is_not_a_prototype_method(name.as_bytes()) { if let Some(own) = unsafe { own_property_shadow(instance, name.as_bytes()) } { return own; } - if super::class_prototype_ref_id(instance).is_some() { - if let Some(value) = crate::object::class_method_slot_value(class_id, name) { return f64::from_bits(value); } - } else { - let scope = crate::gc::RuntimeHandleScope::new(); - let recv = scope.root_nanbox_f64(instance); - let key = crate::object::native_call_method::class_holder::MethodKey::bytes(name.as_bytes()); - if let Some(value) = unsafe { crate::object::native_call_method::class_holder::class_instance_property_value(&recv, &key) } { - return f64::from_bits(value); - } + let scope = crate::gc::RuntimeHandleScope::new(); + let recv = scope.root_nanbox_f64(instance); + let key = crate::object::native_call_method::class_holder::MethodKey::bytes(name.as_bytes()); + if let Some(value) = unsafe { crate::object::native_call_method::class_holder::class_instance_property_value(&recv, &key) } { + return f64::from_bits(value); } } if let Some(owner) = private_owner diff --git a/crates/perry-runtime/src/object/native_module/class_method_values.rs b/crates/perry-runtime/src/object/native_module/class_method_values.rs index a0473f53f8e..b5f94b37e06 100644 --- a/crates/perry-runtime/src/object/native_module/class_method_values.rs +++ b/crates/perry-runtime/src/object/native_module/class_method_values.rs @@ -3,17 +3,47 @@ pub(crate) fn class_evaluation_method_value_for_name( method_name: &str, evaluation_brand: f64, ) -> f64 { + // The method's home is this evaluation's prototype object, the value a + // `C.prototype` read of `evaluation_brand` returns. Building it fills + // every method value of the evaluation (`finish_class_evaluation_prototype`, + // which passes the prototype it is filling), so the read below finds the + // value that fill made. + let scope = crate::gc::RuntimeHandleScope::new(); + let brand = scope.root_nanbox_f64(evaluation_brand); + let home = scope.root_nanbox_f64(evaluation_method_home(owner_class_id, evaluation_brand)); class_evaluation_method_value_for_name_bytes( owner_class_id, method_name.as_bytes(), - evaluation_brand, + brand.get_nanbox_f64(), + home.get_nanbox_f64(), ) } +/// The prototype object a method of `owner_class_id`'s evaluation +/// `evaluation_brand` belongs to: that evaluation's prototype when the brand +/// is its class object, else the declared prototype (what B2's `prototype` +/// read returns for the class). +fn evaluation_method_home(owner_class_id: u32, evaluation_brand: f64) -> f64 { + if class_registry::is_class_object_value(evaluation_brand) { + let class = JSValue::from_bits(evaluation_brand.to_bits()).as_pointer::(); + if !class.is_null() && crate::object::js_object_get_class_id(class) == owner_class_id { + // SAFETY: a live class object of `owner_class_id`. + let proto = + unsafe { crate::object::field_get_set::class_object_prototype_value(class) }; + return f64::from_bits(proto.bits()); + } + } + class_registry::class_decl_prototype_value(owner_class_id) +} + +/// `home` is the method's prototype object (P3 B4a: a bound method value's +/// receiver is never the 0x7FFE prototype word; dispatch reads the owner off +/// the prototype object, `class_prototype_object_id`). pub(crate) fn class_evaluation_method_value_for_name_bytes( owner_class_id: u32, method_name: &[u8], evaluation_brand: f64, + home: f64, ) -> f64 { // A template compiled with method entries gives each evaluation's // prototype its own function object per method (`__eclo`): that @@ -37,9 +67,10 @@ pub(crate) fn class_evaluation_method_value_for_name_bytes( let scope = crate::gc::RuntimeHandleScope::new(); let brand = scope.root_nanbox_f64(evaluation_brand); + let home = scope.root_nanbox_f64(home); let name = intern_class_method_name_bytes(owner_class_id, method_name); let method = build_bound_method_closure_with_private_brand( - class_prototype_ref_value(owner_class_id), + home.get_nanbox_f64(), name.as_ptr(), name.len(), Some(brand.get_nanbox_f64()), diff --git a/crates/perry-runtime/src/object/native_module/class_ref_values.rs b/crates/perry-runtime/src/object/native_module/class_ref_values.rs index f91240e9732..f0de850cc57 100644 --- a/crates/perry-runtime/src/object/native_module/class_ref_values.rs +++ b/crates/perry-runtime/src/object/native_module/class_ref_values.rs @@ -25,14 +25,21 @@ pub(crate) fn class_constructor_key_bits(class_id: u32) -> u64 { 0x7FFE_0000_0000_0000u64 | (class_id as u64 & 0xFFFF_FFFF) } -pub(crate) fn class_prototype_ref_value(class_id: u32) -> f64 { +/// A class method's home word: the capture-0 marker of a method declaration +/// value (`class_method_declaration_value_bytes`) naming the class whose +/// prototype is the method's `[[HomeObject]]`. Runtime-internal, in the 0x7FFE +/// space (DESIGN-p3 §1.2, R4): only `js_class_method_home` and +/// `class_method_value_target` read it, and it is never a JS value. +pub(crate) fn class_method_home_word(class_id: u32) -> f64 { f64::from_bits( 0x7FFE_0000_0000_0000u64 | CLASS_PROTOTYPE_REF_FLAG | (class_id as u64 & 0xFFFF_FFFF), ) } +/// The class a [`class_method_home_word`] names. Only for a method's home +/// capture: a JS value is never this word. #[inline] -pub(crate) fn class_prototype_ref_id(value: f64) -> Option { +pub(crate) fn class_method_home_id(value: f64) -> Option { let bits = value.to_bits(); if (bits >> 48) == 0x7FFE && (bits & CLASS_PROTOTYPE_REF_FLAG) != 0 { let class_id = (bits & 0xFFFF_FFFF) as u32; @@ -43,21 +50,27 @@ pub(crate) fn class_prototype_ref_id(value: f64) -> Option { None } -/// A class constructor OR its `C.prototype` reference -> the class id. The -/// constructor half is [`super::class_value::class_value_id`] (the function object); -/// callers that mean only the constructor ask that directly. +/// The class whose prototype object `value` is: its declared prototype or one +/// evaluation's (`class_id_for_decl_prototype_object`, a structural answer +/// read off the object). A bound method value whose receiver is a class's +/// prototype is that class's unbound method: dispatch runs the owner's body +/// with the call-time `this` (P3 B4a: the receiver is the prototype object, +/// never the 0x7FFE prototype word). #[inline] -pub(crate) fn class_ref_id(value: f64) -> Option { - let bits = value.to_bits(); - match bits >> 48 { - // A `C.prototype` reference word; an INT32 number is never a class. - 0x7FFE => class_prototype_ref_id(value), - // A class function object (one pre-filter for any other pointer). - 0x7FFD => { - super::class_value::class_closure_id((bits & crate::value::POINTER_MASK) as usize) - } - _ => None, +pub(crate) fn class_prototype_object_id(value: f64) -> Option { + let value = JSValue::from_bits(value.to_bits()); + if !value.is_pointer() { + return None; } + super::class_registry::class_id_for_decl_prototype_object(value.as_pointer::() as usize) +} + +/// A class constructor -> the class id: `value` is a class function object. +/// Anything else (an INT32 number, a method's home word, any other value) is +/// no class. +#[inline] +pub(crate) fn class_ref_id(value: f64) -> Option { + super::class_value::class_value_id(value) } pub(crate) unsafe fn metadata_key_to_string(value: f64) -> Option { @@ -202,7 +215,7 @@ pub(crate) fn class_prototype_method_value_at( .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); }; let value = class_method_declaration_value_bytes( - class_prototype_ref_value(class_id), + class_method_home_word(class_id), method_name, body, params, @@ -441,8 +454,7 @@ pub(crate) unsafe fn class_method_value_target(value: u64) -> Option<(usize, u32 return None; } let home = f64::from_bits(crate::closure::js_closure_get_capture_bits(closure, 0)); - if class_prototype_ref_id(home).is_none() && !super::class_registry::is_class_object_value(home) - { + if class_method_home_id(home).is_none() && !super::class_registry::is_class_object_value(home) { return None; } let low = crate::closure::js_closure_get_capture_bits(closure, 1); diff --git a/crates/perry-runtime/src/object/object_ops/define_class_accessor.rs b/crates/perry-runtime/src/object/object_ops/define_class_accessor.rs index 41a83cd983c..3a3dceec4d6 100644 --- a/crates/perry-runtime/src/object/object_ops/define_class_accessor.rs +++ b/crates/perry-runtime/src/object/object_ops/define_class_accessor.rs @@ -1,23 +1,19 @@ -//! `Object.defineProperty` onto a DECLARED class accessor (#10480) reached -//! through a class REF value: `C` (static accessors) or a `C.prototype` ref. +//! `Object.defineProperty` onto a DECLARED static class accessor (#10480) +//! reached through the class constructor `C`. //! -//! An instance accessor is a real accessor property of the class's decl -//! prototype, so a define through the prototype ref is the ordinary define on -//! that object. A static accessor is an accessor property of the class -//! function object's own-property object (`object::class_value`), attributes -//! with its key. +//! A static accessor is an accessor property of the class function object's +//! own-property object (`object::class_value`), attributes with its key. (An +//! instance accessor is a real accessor property of the class's decl +//! prototype object, so a define on `C.prototype` is the ordinary define on +//! that object and never reaches here.) use super::*; -/// ValidateAndApplyPropertyDescriptor for the declared accessor `name` of -/// `class_id` (`is_static` selects `C` over `C.prototype`). +/// ValidateAndApplyPropertyDescriptor for the declared static accessor `name` +/// of `class_id`. /// /// `None` means this is not a declared accessor definition and the caller's /// path decides; `Some(verdict)` is the definition's verdict. /// -/// * Instance: when the prototype ref reflects `name` through the decl -/// prototype (`decl_prototype_own_accessor`), the define applies to that -/// object — attributes, replacement and conversion alike — and its verdict -/// is returned. /// * Static, not a live declared accessor → `None`. /// * Static, current accessor non-configurable → the spec's rejections are /// `Some(false)`, exactly as for any other property. @@ -29,24 +25,9 @@ use super::*; /// existing path is unchanged. pub(super) unsafe fn define_declared_class_accessor( class_id: u32, - is_static: bool, name: &str, descriptor: &DescView<'_>, ) -> Option { - if !is_static { - let scope = crate::gc::RuntimeHandleScope::new(); - let Some(proto) = super::super::class_registry::decl_prototype_own_accessor(class_id, name) - else { - return None; - }; - let proto = scope.root_nanbox_u64(proto.to_bits()); - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let key = f64::from_bits(crate::value::JSValue::string_ptr(key).bits()); - let key = scope.root_nanbox_f64(key); - return Some(super::define_own_property_decoded( - &scope, &proto, &key, descriptor, - )); - } let Some((acc, enumerable, configurable)) = crate::object::class_value::class_static_own_accessor(class_id, name) else { diff --git a/crates/perry-runtime/src/object/object_ops/define_property.rs b/crates/perry-runtime/src/object/object_ops/define_property.rs index 2f476cc680a..cb5a3d16293 100644 --- a/crates/perry-runtime/src/object/object_ops/define_property.rs +++ b/crates/perry-runtime/src/object/object_ops/define_property.rs @@ -528,13 +528,6 @@ pub(crate) unsafe fn apply_property_descriptor( return true; } - // Legacy prototype refs denote the same ordinary object as a - // reflective C.prototype read. Define the descriptor on that holder. - if let Some(cid) = super::super::class_prototype_ref_id(obj_value) { - let proto = super::super::class_registry::class_decl_prototype_value(cid); - let proto = scope.root_nanbox_u64(proto.to_bits()); - return define_own_property_decoded(scope, &proto, &key_handle, descriptor); - } // Constructor refs use their static property storage. if let Some(target_cid) = super::super::class_ref_id(obj_value) { if crate::symbol::js_is_symbol(key_value) != 0 { @@ -549,14 +542,10 @@ pub(crate) unsafe fn apply_property_descriptor( ); } if let Some(name) = super::super::metadata_key_to_string(key_value) { - // #10480: a declared accessor — instance on the prototype ref, - // static on the class ref — keeps its get/set under a generic - // descriptor; only its attributes change. + // #10480: a declared static accessor keeps its get/set under a + // generic descriptor; only its attributes change. if let Some(verdict) = super::define_class_accessor::define_declared_class_accessor( - target_cid, - super::super::class_prototype_ref_id(obj_value).is_none(), - &name, - descriptor, + target_cid, &name, descriptor, ) { return verdict; } @@ -602,8 +591,7 @@ pub(crate) unsafe fn apply_property_descriptor( let has_get = descriptor.has_named(b"get"); let has_set = descriptor.has_named(b"set"); - if super::super::class_prototype_ref_id(obj_value).is_none() && (has_get || has_set) - { + if has_get || has_set { let get_field = descriptor.read_named(b"get"); let get_field = scope.root_nanbox_u64(get_field.bits()); let set_field = descriptor.read_named(b"set"); @@ -648,14 +636,10 @@ pub(crate) unsafe fn apply_property_descriptor( // static keeps every attribute the descriptor omits (a // declared `static x` is writable+enumerable+configurable); // a new key defaults them to false. - let existing_static = super::super::class_prototype_ref_id(obj_value) - .is_none() - .then(|| { - super::super::class_registry::class_own_static_field_value( - target_cid, &name, - ) - }) - .flatten() + let existing_static = + super::super::class_registry::class_own_static_field_value( + target_cid, &name, + ) .map(|_| { super::super::class_registry::class_static_defined_attrs( target_cid, &name, @@ -663,76 +647,58 @@ pub(crate) unsafe fn apply_property_descriptor( .unwrap_or((true, true, true)) }); if !value_field.is_undefined() || has_value || existing_static.is_some() { - // #7190: `C` and `C.prototype` both answer - // `class_ref_id` with the SAME class id — the arm this - // sits in exists because `C.prototype` maps back to the - // class in Perry. So the two receivers were - // indistinguishable here, and every define took the - // prototype route. That is right for - // `Object.defineProperty(C.prototype, …)` (the drizzle - // `applyMixins` case this arm was written for) and wrong - // for `Object.defineProperty(C, …)`, which is a STATIC - // own property: `C.zzz` came back `undefined`, and so - // did `new C().zzz`, so the value went nowhere at all. - // - // `class_prototype_ref_id` is the discriminator — it - // answers only for the prototype ref — and it is what - // `descriptors.rs` already uses to tell the two apart - // when reporting descriptors. - if super::super::class_prototype_ref_id(obj_value).is_none() { - // A static own property lands in CLASS_DYNAMIC_PROPS, - // the same table `static x = …` and - // `js_class_register_static_field` write to, so the - // existing static read path finds it with no new - // lookup. - if has_value || existing_static.is_none() { - super::super::class_registry::class_dynamic_prop_root_store( - target_cid, - &name, - f64::from_bits(value_field.bits()), - ); - } - // A data descriptor is non-enumerable unless it - // says otherwise; a `static x = …` field IS - // enumerable, and both share CLASS_DYNAMIC_PROPS. - // Record which this was, or `Object.keys(C)` gains - // a key node does not report. - // ECMA-262 [[DefineOwnProperty]]: a field the - // descriptor omits DEFAULTS to false on a new - // property but is RETAINED on an existing one. The - // built-in `name`/`length` slots are - // `configurable: true`, which is why redefining - // `name` without saying `configurable` must stay - // configurable — while a brand-new key must not. - let has_cfg = descriptor.has_named(b"configurable"); - let configurable = if has_cfg { - crate::value::js_is_truthy(f64::from_bits( - descriptor.read_named(b"configurable").bits(), - )) != 0 - } else { - existing_static - .map(|(_, _, cfg)| cfg) - .unwrap_or(matches!(name.as_str(), "name" | "length")) - }; - let writable = if descriptor.has_named(b"writable") { - descriptor.flag(b"writable").unwrap_or(false) - } else { - existing_static.is_some_and(|(w, _, _)| w) - }; - let enumerable = if descriptor.has_named(b"enumerable") { - descriptor.flag(b"enumerable").unwrap_or(false) - } else { - existing_static.is_some_and(|(_, e, _)| e) - }; - super::super::class_registry::class_static_set_defined_attrs( + // A static own property lands in CLASS_DYNAMIC_PROPS, + // the same table `static x = …` and + // `js_class_register_static_field` write to, so the + // existing static read path finds it with no new + // lookup. + if has_value || existing_static.is_none() { + super::super::class_registry::class_dynamic_prop_root_store( target_cid, &name, - writable, - enumerable, - configurable, + f64::from_bits(value_field.bits()), ); - return true; } + // A data descriptor is non-enumerable unless it + // says otherwise; a `static x = …` field IS + // enumerable, and both share CLASS_DYNAMIC_PROPS. + // Record which this was, or `Object.keys(C)` gains + // a key node does not report. + // ECMA-262 [[DefineOwnProperty]]: a field the + // descriptor omits DEFAULTS to false on a new + // property but is RETAINED on an existing one. The + // built-in `name`/`length` slots are + // `configurable: true`, which is why redefining + // `name` without saying `configurable` must stay + // configurable — while a brand-new key must not. + let has_cfg = descriptor.has_named(b"configurable"); + let configurable = if has_cfg { + crate::value::js_is_truthy(f64::from_bits( + descriptor.read_named(b"configurable").bits(), + )) != 0 + } else { + existing_static + .map(|(_, _, cfg)| cfg) + .unwrap_or(matches!(name.as_str(), "name" | "length")) + }; + let writable = if descriptor.has_named(b"writable") { + descriptor.flag(b"writable").unwrap_or(false) + } else { + existing_static.is_some_and(|(w, _, _)| w) + }; + let enumerable = if descriptor.has_named(b"enumerable") { + descriptor.flag(b"enumerable").unwrap_or(false) + } else { + existing_static.is_some_and(|(_, e, _)| e) + }; + super::super::class_registry::class_static_set_defined_attrs( + target_cid, + &name, + writable, + enumerable, + configurable, + ); + return true; } } } diff --git a/crates/perry-runtime/src/object/object_ops/has_own.rs b/crates/perry-runtime/src/object/object_ops/has_own.rs index e527e29196e..57e7376c191 100644 --- a/crates/perry-runtime/src/object/object_ops/has_own.rs +++ b/crates/perry-runtime/src/object/object_ops/has_own.rs @@ -184,16 +184,11 @@ fn object_has_own_generic(obj_value: f64, key_value: f64) -> f64 { if crate::symbol::js_is_symbol(key_value) != 0 { let sym_key = crate::symbol::sym_key_from_f64(key_value); if let Some(class_id) = super::super::class_ref_id(obj_value) { - let is_prototype = super::super::class_prototype_ref_id(obj_value).is_some(); - let present = if is_prototype { - let proto = super::super::class_registry::class_decl_prototype_value(class_id); - crate::symbol::js_object_has_own_symbol(proto, key_value) - } else { - crate::symbol::class_static_symbol_lookup(class_id, key_value).is_some() - || super::super::class_registry::class_has_own_symbol_member( - class_id, sym_key, true, - ) - }; + let present = crate::symbol::class_static_symbol_lookup(class_id, key_value) + .is_some() + || super::super::class_registry::class_has_own_symbol_member( + class_id, sym_key, true, + ); return f64::from_bits(if present { TAG_TRUE } else { TAG_FALSE }); } let present = crate::symbol::js_object_has_own_symbol(obj_value, key_value); @@ -634,28 +629,24 @@ pub extern "C" fn js_object_property_is_enumerable(obj_value: f64, key_value: f6 // `verifyProperty(C, "f", …)`'s isEnumerable check failed (test262 // class/elements static-field-declaration & friends). if let Some(class_id) = super::super::class_ref_id(obj_value) { - if super::super::class_prototype_ref_id(obj_value).is_none() { - if let Some(key_name) = - super::super::has_own_helpers::str_from_string_header(key_str) - { - let is_static_field = - !super::super::field_get_set::is_internal_runtime_key(key_name) - && super::super::class_registry::class_own_static_field_value( - class_id, key_name, - ) - .is_some(); - // #10480: a declared static accessor is non-enumerable by - // ClassBody default, but a generic descriptor can flip it - // (Object.defineProperty(C, "x", { enumerable: true })). - let is_enumerable_static_accessor = - crate::object::class_value::class_static_own_accessor(class_id, key_name) - .is_some_and(|(_, enumerable, _)| enumerable); - return f64::from_bits(if is_static_field || is_enumerable_static_accessor { - TAG_TRUE - } else { - TAG_FALSE - }); - } + if let Some(key_name) = super::super::has_own_helpers::str_from_string_header(key_str) { + let is_static_field = + !super::super::field_get_set::is_internal_runtime_key(key_name) + && super::super::class_registry::class_own_static_field_value( + class_id, key_name, + ) + .is_some(); + // #10480: a declared static accessor is non-enumerable by + // ClassBody default, but a generic descriptor can flip it + // (Object.defineProperty(C, "x", { enumerable: true })). + let is_enumerable_static_accessor = + crate::object::class_value::class_static_own_accessor(class_id, key_name) + .is_some_and(|(_, enumerable, _)| enumerable); + return f64::from_bits(if is_static_field || is_enumerable_static_accessor { + TAG_TRUE + } else { + TAG_FALSE + }); } } diff --git a/crates/perry-runtime/src/object/object_ops/prototype.rs b/crates/perry-runtime/src/object/object_ops/prototype.rs index 0382d416e78..70d9d578a41 100644 --- a/crates/perry-runtime/src/object/object_ops/prototype.rs +++ b/crates/perry-runtime/src/object/object_ops/prototype.rs @@ -370,12 +370,7 @@ pub(crate) fn get_prototype_of_resolved(obj_value: f64) -> f64 { // null (15.2.3.2-1*). { let jv = crate::value::JSValue::from_bits(obj_value.to_bits()); - // An INT32-tagged value may be a class ref (same 0x7FFE tag as small - // integers) — those must keep flowing to the class resolution below. - let is_class_ref = super::super::class_ref_id(obj_value).is_some(); - let wrapper = if is_class_ref { - None - } else if jv.is_number() { + let wrapper = if jv.is_number() { Some("Number") } else if jv.is_any_string() { Some("String") @@ -590,67 +585,6 @@ pub(crate) fn get_prototype_of_resolved(obj_value: f64) -> f64 { crate::value::js_nanbox_pointer(parent as i64) }; } - if let Some(class_id) = crate::object::class_value::class_ref_word(bits) { - // An explicit `Object.setPrototypeOf(Ctor, obj)` wins over every - // derived answer below — it IS the constructor's [[Prototype]]. - if super::super::class_prototype_ref_id(obj_value).is_none() { - let static_proto = super::super::class_registry::class_static_prototype(class_id); - if !static_proto.is_null() { - return f64::from_bits( - crate::value::js_nanbox_pointer(static_proto as i64).to_bits(), - ); - } - if super::super::class_registry::class_static_prototype_is_nulled(class_id) { - return f64::from_bits(TAG_NULL); - } - } - if super::super::class_prototype_ref_id(obj_value).is_none() { - // A class whose heritage is a runtime function value has no Perry - // parent class id. Its constructor's [[Prototype]] is that exact - // function object (not Function.prototype), as observed by - // Object.getPrototypeOf(D) and static `super` lookup. - let dynamic_parent = super::super::js_get_dynamic_parent_value(class_id); - let parent_value = crate::value::JSValue::from_bits(dynamic_parent.to_bits()); - if !parent_value.is_undefined() && !parent_value.is_null() { - return dynamic_parent; - } - } - if let Some(parent_id) = get_parent_class_id(class_id) { - // #8343 followup: `NATIVE_MODULE_CLASS_ID` (0xFFFFFFFE) is a - // sentinel, not a real class. A prior `Object.create(proto)` whose - // `proto` was a native-module namespace object registered it as a - // synthetic class's parent. Returning the raw sentinel as an - // INT32-tagged class ref (`-2`) trips `Object.create` with - // `TypeError: Object prototype may only be an Object or null: -2`. - // Treat it as a root: a native-module namespace's [[Prototype]] is - // %Object.prototype%, so the synthetic class whose proto was that - // namespace inherits Object.prototype too. - // A builtin parent id (`extends Error`) is not a compiled class - // and never gets a class function object: its constructor is the - // dynamic parent value above, and without one this is a root. - if parent_id != 0 - && parent_id != super::super::native_module::NATIVE_MODULE_CLASS_ID - && crate::object::is_class_id_registered(parent_id) - { - return crate::object::class_value::class_value(parent_id); - } - } - // Root of the class hierarchy. In JS `Object.getPrototypeOf` of a base - // class *constructor* is `Function.prototype`, and of a base class's - // `.prototype` object is `Object.prototype` — NOT null. class-transformer - // walks `Object.getPrototypeOf(target.prototype.constructor)` and then - // dereferences `.prototype` on the result; the old `null` made that - // `null.prototype` throw, blocking class-transformer/class-validator on - // any flat (no-`extends`) DTO. (#420 followup) - if super::super::class_prototype_ref_id(obj_value).is_some() { - let proto = crate::object::builtin_prototype_value("Object"); - if proto.to_bits() != crate::value::TAG_UNDEFINED { - return proto; - } - return f64::from_bits(TAG_NULL); - } - return function_prototype_or_null(); - } // Heap-pointer receiver — return the input value itself. For // class-id-tagged instances, `.constructor` then returns the class // ref (via the constructor intercept in js_object_get_field_by_name, diff --git a/crates/perry-runtime/src/object/polymorphic_index.rs b/crates/perry-runtime/src/object/polymorphic_index.rs index 2926311701c..5fe206463a2 100644 --- a/crates/perry-runtime/src/object/polymorphic_index.rs +++ b/crates/perry-runtime/src/object/polymorphic_index.rs @@ -175,15 +175,6 @@ pub extern "C" fn js_object_get_index_polymorphic(obj_handle: i64, idx: f64) -> idx, ); } - // A `C.prototype` ref word; an INT32 number has no elements. - 0x7FFE => { - if crate::object::class_prototype_ref_id(f64::from_bits(obj_handle as u64)) - .is_some() - { - return crate::value::js_dyn_index_get(f64::from_bits(obj_handle as u64), idx); - } - return f64::from_bits(crate::value::TAG_UNDEFINED); - } _ => return f64::from_bits(crate::value::TAG_UNDEFINED), } } else { diff --git a/crates/perry-runtime/src/object/property_key.rs b/crates/perry-runtime/src/object/property_key.rs index 414ae7f29ea..624da925135 100644 --- a/crates/perry-runtime/src/object/property_key.rs +++ b/crates/perry-runtime/src/object/property_key.rs @@ -189,19 +189,17 @@ unsafe fn set_property_key_resolved(obj_value: f64, key: f64, value: f64) -> f64 if key_str.is_null() { return value; } - // Class constructor/prototype refs are INT32-tagged values, not real - // `ObjectHeader`s — `extract_obj_ptr` returns null for them, so a - // `C.prototype[key] = v` / `C[key] = v` write silently no-op'd here. The - // get side already passes the raw NaN-boxed bits into the by-name dispatch - // (which has a dedicated 0x7FFE class-ref branch); mirror that on the set - // side so static-accessor and prototype instance-setter dispatch run. + // A class constructor is a class function object, not a real + // `ObjectHeader` — `extract_obj_ptr` returns null for it, so a + // `C[key] = v` write silently no-op'd here. The get side already passes + // the raw NaN-boxed bits into the by-name dispatch (which has a dedicated + // class-ref branch); mirror that on the set side so static-accessor + // dispatch runs. if let Some(class_id) = super::class_ref_id(obj_value) { - if super::class_prototype_ref_id(obj_value).is_none() { - let name_ptr = crate::string::string_data(key_str); - let name_len = (*key_str).byte_len as usize; - if std::slice::from_raw_parts(name_ptr, name_len) == b"prototype" { - crate::error::throw_immutable_write(class_id, "prototype"); - } + let name_ptr = crate::string::string_data(key_str); + let name_len = (*key_str).byte_len as usize; + if std::slice::from_raw_parts(name_ptr, name_len) == b"prototype" { + crate::error::throw_immutable_write(class_id, "prototype"); } js_object_set_field_by_name(obj_value.to_bits() as *mut ObjectHeader, key_str, value); return value; diff --git a/crates/perry-runtime/src/object/property_ops/common.rs b/crates/perry-runtime/src/object/property_ops/common.rs index de950974454..b22a89d2d8b 100644 --- a/crates/perry-runtime/src/object/property_ops/common.rs +++ b/crates/perry-runtime/src/object/property_ops/common.rs @@ -92,16 +92,12 @@ pub(crate) fn reflect_value_is_object(value: f64) -> bool { // One shared Type(value) is Object answer: a POINTER_TAG word names a // GC-headed object (honest pointers), so its header byte decides, and // native ids are objects. Raw untagged words keep the allocator proof; - // class constructors are callable function objects. Only a flagged - // prototype reference needs the non-pointer compatibility path. + // class constructors are callable function objects. if crate::builtins::arithmetic::js_value_is_object(value) { return true; } let top16 = value.to_bits() >> 48; - if top16 == 0 { - return crate::object::js_value_is_heap_object(value); - } - crate::object::class_prototype_ref_id(value).is_some() + top16 == 0 && crate::object::js_value_is_heap_object(value) } /// `CreateListFromArrayLike(value)` — collect indexed `0..length` properties of diff --git a/crates/perry-runtime/src/object/property_ops/metadata.rs b/crates/perry-runtime/src/object/property_ops/metadata.rs index 526118aebef..dc8d96f3573 100644 --- a/crates/perry-runtime/src/object/property_ops/metadata.rs +++ b/crates/perry-runtime/src/object/property_ops/metadata.rs @@ -166,9 +166,9 @@ fn metadata_keys_for(target: f64, property_key: f64, inherited: bool) -> f64 { /// class constructor (`ClassRef`), and several existing tests /// (`test_decorators_legacy_property_metadata.ts`) read it back off the class. /// To satisfy BOTH access shapes we treat a class's prototype and its -/// constructor as one metadata bucket: any class-prototype target — the -/// synthetic `class_prototype_ref` value or the live decl-prototype heap object -/// — folds onto the stable class-constructor key (`INT32_TAG | class_id`). +/// constructor as one metadata bucket: a class-prototype target (the live +/// decl-prototype heap object) folds onto the stable class-constructor key +/// (`class_constructor_key_bits`). /// /// This is the runtime "compatibility shim" alternative to re-targeting the /// emit (REFLECT-METADATA-SCOPING.md, Task B). It is also GC-safe: the @@ -178,10 +178,6 @@ fn metadata_keys_for(target: f64, property_key: f64, inherited: bool) -> f64 { /// no dedicated GC scanner. Non-prototype targets (constructors, method `.value` /// closures, plain objects) pass through unchanged. fn normalize_target_bits(target: f64) -> u64 { - // Synthetic class-prototype ref → fold onto the class constructor key. - if let Some(cid) = crate::object::class_prototype_ref_id(target) { - return crate::object::class_constructor_key_bits(cid); - } // A class constructor (either form) -> its stable key: the function object // is a heap cell whose address is not a durable key. if let Some(cid) = crate::object::class_value::class_value_id(target) { @@ -275,25 +271,10 @@ mod tests { .insert(cid); } - // A class's synthetic prototype-ref and its live decl-prototype heap object - // both fold onto the class-constructor key, so `getMetadata(design:type, + // A class's live decl-prototype heap object folds onto the + // class-constructor key, so `getMetadata(design:type, // SomeClass.prototype, prop)` (class-transformer) finds metadata stored // against `ClassRef(SomeClass)` (Perry's emit). - #[test] - fn synthetic_prototype_ref_folds_onto_constructor_key() { - let cid = 0x4242; - register_test_class(cid); - let ctor_key = crate::object::class_constructor_key_bits(cid); - - let proto_ref = crate::object::class_prototype_ref_value(cid); - assert_eq!(normalize_target_bits(proto_ref), ctor_key); - - // The constructor VALUE (its function object) folds onto the same key. - let ctor_ref = crate::object::class_constructor_ref_value(cid); - assert_ne!(ctor_ref.to_bits(), ctor_key, "the value is not the key"); - assert_eq!(normalize_target_bits(ctor_ref), ctor_key); - } - #[test] fn decl_prototype_heap_object_folds_onto_constructor_key() { let cid = 0x5151; diff --git a/crates/perry-runtime/src/object/property_ops/set.rs b/crates/perry-runtime/src/object/property_ops/set.rs index 84f6faa515a..9c7524b867a 100644 --- a/crates/perry-runtime/src/object/property_ops/set.rs +++ b/crates/perry-runtime/src/object/property_ops/set.rs @@ -531,10 +531,7 @@ pub(crate) fn own_set_descriptor(target: f64, key: f64) -> Option bool { if top == 0 && bits > 0x10000 { return true; } - // A `C.prototype` ref word is an object (an INT32 number is not). - if top == 0x7FFE { - return crate::object::class_prototype_ref_id(f64::from_bits(bits)).is_some(); - } false } diff --git a/crates/perry-runtime/src/symbol/get.rs b/crates/perry-runtime/src/symbol/get.rs index 26207c65bd0..7d81ed7e8bd 100644 --- a/crates/perry-runtime/src/symbol/get.rs +++ b/crates/perry-runtime/src/symbol/get.rs @@ -451,8 +451,8 @@ pub(crate) unsafe fn js_object_get_symbol_property_with_receiver( } return f64::from_bits(TAG_UNDEFINED); } - // Check the class's static symbols first when receiver is a class ref - // (top16 == 0x7FFE, INT32_TAG). + // Check the class's static symbols first when receiver is a class + // constructor. let bits = obj_f64.to_bits(); if let Some(class_id) = crate::object::class_value::class_ref_word(bits) { // A legacy class word names the same function object as its minted @@ -469,24 +469,16 @@ pub(crate) unsafe fn js_object_get_symbol_property_with_receiver( return value; } } - let static_lookup = if crate::object::class_prototype_ref_id(obj_f64).is_some() { - class_static_symbol_lookup(class_id, sym_f64) - } else { - super::class_static_symbol_lookup_in_chain(class_id, sym_f64) - }; - if let Some(vb) = static_lookup { + if let Some(vb) = super::class_static_symbol_lookup_in_chain(class_id, sym_f64) { return f64::from_bits(vb); } - let is_proto_ref_receiver = crate::object::class_prototype_ref_id(obj_f64).is_some(); // A constructor whose heritage was a runtime value inherits from that // exact value, the same edge `Object.getPrototypeOf(C)` reports. A // fresh class evaluation is not reachable through the template-id // tables below, so continue the ordinary [[Get]] on the actual parent. - if !is_proto_ref_receiver { - let parent = crate::object::js_get_dynamic_parent_value(class_id); - if crate::object::is_class_object_value(parent) { - return js_object_get_symbol_property_with_receiver(parent, sym_f64, receiver_f64); - } + let parent = crate::object::js_get_dynamic_parent_value(class_id); + if crate::object::is_class_object_value(parent) { + return js_object_get_symbol_property_with_receiver(parent, sym_f64, receiver_f64); } // #1758: a class ref whose own static symbols miss may inherit the // symbol from a class-expression parent (`class Sub extends make(...) {}` @@ -519,16 +511,9 @@ pub(crate) unsafe fn js_object_get_symbol_property_with_receiver( // `get [Symbol.species]` accessor — live on its constructor closure, // which the chain edge (a reserved class id) does not reach. Read it // there with the original receiver, so the inherited species getter - // answers `X`, not `Array`. Statics only: a prototype ref shares this - // tag, and `X.prototype` must not see the constructor's symbols. - if !is_proto_ref_receiver { - if let Some(parent_ctor) = crate::object::builtin_parent_ctor_in_chain(class_id) { - return js_object_get_symbol_property_with_receiver( - parent_ctor, - sym_f64, - receiver_f64, - ); - } + // answers `X`, not `Array`. + if let Some(parent_ctor) = crate::object::builtin_parent_ctor_in_chain(class_id) { + return js_object_get_symbol_property_with_receiver(parent_ctor, sym_f64, receiver_f64); } return f64::from_bits(TAG_UNDEFINED); } diff --git a/crates/perry-runtime/src/symbol/iterator.rs b/crates/perry-runtime/src/symbol/iterator.rs index 763d65358fa..75dbb73d630 100644 --- a/crates/perry-runtime/src/symbol/iterator.rs +++ b/crates/perry-runtime/src/symbol/iterator.rs @@ -45,10 +45,6 @@ pub unsafe extern "C" fn js_object_get_own_property_symbols(obj_f64: f64) -> i64 return (arr.to_bits() & POINTER_MASK) as i64; } if let Some(class_id) = crate::object::class_ref_id(obj_f64) { - if crate::object::class_prototype_ref_id(obj_f64).is_some() { - let proto = crate::object::class_decl_prototype_value(class_id); - return js_object_get_own_property_symbols(proto); - } let mut entries = class_static_symbol_keys_for_class(class_id); let mut arr = crate::array::js_array_alloc(entries.len() as u32); for sym_ptr_usize in entries.drain(..) { diff --git a/crates/perry-runtime/src/util_inherits.rs b/crates/perry-runtime/src/util_inherits.rs index d8d6d93f4d6..61170bd0282 100644 --- a/crates/perry-runtime/src/util_inherits.rs +++ b/crates/perry-runtime/src/util_inherits.rs @@ -121,11 +121,9 @@ fn set_super_property(ctor: f64, super_ctor: f64) { // or ObjectHeader pointers. Store super_ in the constructor-side static // property table, matching Object.defineProperty(C, "super_", ...). if let Some(class_id) = crate::object::class_ref_id(ctor) { - if crate::object::class_prototype_ref_id(ctor).is_none() { - crate::object::class_dynamic_prop_root_store(class_id, "super_", super_ctor); - crate::object::class_static_set_defined_attrs(class_id, "super_", true, false, true); - return; - } + crate::object::class_dynamic_prop_root_store(class_id, "super_", super_ctor); + crate::object::class_static_set_defined_attrs(class_id, "super_", true, false, true); + return; } let key = named_key(b"super_"); let attrs = PropertyAttrs::new(true, false, true); diff --git a/crates/perry-runtime/src/weakref.rs b/crates/perry-runtime/src/weakref.rs index c8eff9660dd..49676f342c4 100644 --- a/crates/perry-runtime/src/weakref.rs +++ b/crates/perry-runtime/src/weakref.rs @@ -182,18 +182,15 @@ pub(crate) fn is_valid_weak_target(value: f64) -> bool { return true; } - // A class reference (e.g. an imported `class Foo {}` passed as a value, or a - // class prototype) is NaN-boxed as an INT32-tagged (`0x7FFE`) class-ref ID, - // not a heap pointer. In JS such a value is a function/object and therefore - // "CanBeHeldWeakly" (ES2023) — a valid WeakMap/WeakSet/WeakRef key. NestJS - // relies on this: `InitializeOnPreviewAllowlist.add(InternalCoreModule)` does + // A class constructor (e.g. an imported `class Foo {}` passed as a value) is + // a function and therefore "CanBeHeldWeakly" (ES2023) — a valid + // WeakMap/WeakSet/WeakRef key. NestJS relies on this: + // `InitializeOnPreviewAllowlist.add(InternalCoreModule)` does // `weakmap.set(InternalCoreModule, true)`, and module-token factories key a - // WeakMap by the module class. Cross-module class imports arrive as class-ref - // values (not closure pointers), so without this branch they were rejected - // with "Invalid value used as weak map key" only inside a full module graph. - if crate::object::class_ref_id(value).is_some() - || crate::object::class_prototype_ref_id(value).is_some() - { + // WeakMap by the module class. Without this branch cross-module class + // imports were rejected with "Invalid value used as weak map key" only + // inside a full module graph. + if crate::object::class_ref_id(value).is_some() { return true; } diff --git a/tests/parity/language/class/test_p3_b4a_prototype_paths.ts b/tests/parity/language/class/test_p3_b4a_prototype_paths.ts new file mode 100644 index 00000000000..885369cfbe3 --- /dev/null +++ b/tests/parity/language/class/test_p3_b4a_prototype_paths.ts @@ -0,0 +1,125 @@ +// P3 B4a controls (DESIGN-p3 rev 3.1 §3): every JS operation that once decoded +// the 0x7FFE `C.prototype` word now sees the prototype object, and a bound +// method of a class prototype runs its owner's body with the call-time `this`. +// One block per path: descriptors, defineProperty, has_own, delete, symbol +// get, proxy, weakref, instanceof, then bound methods (declared classes and +// per-evaluation classes, whose method values carry the prototype object). + +const tag = Symbol("tag"); +class A { + v = 1; + m(): string { return "A.m:" + (this === undefined ? "undef" : typeof this === "object" ? (this as any).v : String(this)); } + get g(): number { return 2; } + set g(_x: number) {} + static s(): string { return "A.s"; } + [tag](): string { return "A.sym:" + (this as any)?.v; } + #p(): string { return "A.#p:" + this.v; } + callP(): string { return this.#p(); } +} +class B extends A { + m(): string { return "B.m>" + super.m(); } +} + +// descriptors +const dm = Object.getOwnPropertyDescriptor(A.prototype, "m")!; +const dg = Object.getOwnPropertyDescriptor(A.prototype, "g")!; +console.log("desc m", typeof dm.value, dm.writable, dm.enumerable, dm.configurable); +console.log("desc g", typeof dg.get, typeof dg.set, dg.enumerable, dg.configurable); +console.log("desc none", Object.getOwnPropertyDescriptor(A.prototype, "v"), Object.getOwnPropertyDescriptor(B.prototype, "g")); +console.log("descs", Object.keys(Object.getOwnPropertyDescriptors(A.prototype)).join("|")); +console.log("names", Object.getOwnPropertyNames(A.prototype).join("|"), Object.getOwnPropertySymbols(A.prototype).length); + +// defineProperty +Object.defineProperty(A.prototype, "k", { value: 7, writable: false, enumerable: true, configurable: true }); +Object.defineProperty(A.prototype, "acc", { get() { return "acc:" + (this as any).v; }, configurable: true }); +const a = new A(); +console.log("define", (a as any).k, (a as any).acc, Object.keys(A.prototype).join("|"), + Object.getOwnPropertyDescriptor(A.prototype, "k")!.writable); +Object.defineProperty(A.prototype, "m", { value: function (this: any) { return "redefined:" + this.v; }, configurable: true, writable: true }); +console.log("redefine", a.m(), new B().m()); + +// has_own +console.log("hasOwn", Object.hasOwn(A.prototype, "m"), Object.hasOwn(A.prototype, "v"), Object.hasOwn(A.prototype, "constructor"), + A.prototype.hasOwnProperty("g"), Object.prototype.hasOwnProperty.call(B.prototype, "m"), Object.hasOwn(B.prototype, "g"), + "m" in B.prototype, "g" in B.prototype, "zz" in A.prototype); + +// delete +console.log("delete", delete (A.prototype as any).k, Object.hasOwn(A.prototype, "k"), (a as any).k); +console.log("delete m", delete (A.prototype as any).m, typeof (a as any).m, typeof new B().m); + +// symbol get +console.log("symbol", typeof (A.prototype as any)[tag], (A.prototype as any)[tag].call({ v: 9 }), (a as any)[tag](), + (A.prototype as any)[Symbol.iterator], Object.prototype.toString.call(A.prototype)); +(A.prototype as any)[Symbol.toStringTag] = "Ay"; +console.log("toStringTag", Object.prototype.toString.call(a), String(a)); + +// proxy +const px = new Proxy(A.prototype, {}); +const pa: any = Object.create(px); +pa.v = 5; +console.log("proxy", typeof px.callP, px.constructor === A, "g" in px, Object.getPrototypeOf(px) === Object.prototype, + pa.g, typeof (px as any)[tag]); +console.log("proxy target", Reflect.ownKeys(px).length === Reflect.ownKeys(A.prototype).length); + +// weakref +const wr = new WeakRef(A.prototype); +const wm = new WeakMap([[A.prototype, "a"], [B.prototype, "b"]]); +const ws = new WeakSet([A.prototype]); +console.log("weak", wr.deref() === A.prototype, wm.get(A.prototype), wm.get(Object.getPrototypeOf(new B())), ws.has(A.prototype), + ws.has(B.prototype), wm.has(Object.getPrototypeOf(a))); + +// instanceof +console.log("instanceof", A.prototype instanceof A, B.prototype instanceof A, B.prototype instanceof B, a instanceof A, + Object.create(A.prototype) instanceof A, A.prototype.isPrototypeOf(new B()), Object.getPrototypeOf(B.prototype) === A.prototype); +class Hy { static [Symbol.hasInstance](x: any) { return x === A.prototype; } } +console.log("hasInstance", (A.prototype as any) instanceof Hy, (a as any) instanceof Hy); + +// bound methods of class prototypes (declared class) +class C { + v = "c"; + m(this: any, x?: string): string { return "C.m:" + (this === undefined ? "undef" : this.v) + (x ?? ""); } + static st(this: any): string { return "C.st:" + (this === C); } +} +const o = { v: "o" }; +const bm = C.prototype.m.bind(o); +const f = C.prototype.m; +console.log("bind", bm(), bm("!"), bm.name, bm.length, f.call(o), f.call(new C(), "?"), f.apply(o, ["#"])); +let threw = "no"; +try { (f as any)(); } catch (e) { threw = (e as Error).constructor.name; } +console.log("bare call", threw, C.prototype.m.call(undefined as any) === "C.m:undef"); +const sb = C.st.bind(null); +console.log("static bind", C.st(), (C.st as any).call(C), typeof sb); + +// bound methods of class prototypes (a per-evaluation class: one prototype per call) +function make(tagv: string) { + return class Ev { + v = tagv; + m(this: any): string { return "Ev.m:" + tagv + ":" + (this === undefined ? "undef" : this.v); } + #q(): string { return "Ev.#q:" + this.v; } + q(): string { return this.#q(); } + static who(): string { return "Ev.who:" + tagv; } + }; +} +const E1 = make("one"); +const E2 = make("two"); +const e1 = new E1(); +const em = E1.prototype.m; +console.log("eval", em.call({ v: "x" }), em.call(e1), E1.prototype.m.bind(new E2())(), E2.prototype.m.call(e1), + E1.prototype.m === E2.prototype.m, E1.prototype !== E2.prototype, e1.q(), E1.who()); +const eb = e1.m; +console.log("eval detached", eb.call({ v: "d" }), typeof eb, eb.name, Object.getPrototypeOf(e1) === E1.prototype); +const ed = Object.getOwnPropertyDescriptor(E1.prototype, "m")!; +console.log("eval desc", typeof ed.value, ed.value === E1.prototype.m, Object.hasOwn(E1.prototype, "q"), + delete (E1.prototype as any).q, typeof (e1 as any).q, typeof new E2().q); + +// copied onto another class's prototype (mysql2's Command copy) +class P { r(this: any): string { return "P.r:" + this.constructor.name; } } +class Q {} +(Q.prototype as any).r = P.prototype.r; +console.log("copied", (new Q() as any).r(), new P().r()); + +// super through a bound method value +class S1 { v = "s1"; m(this: any): string { return "S1.m:" + this.v; } } +class S2 extends S1 { m(this: any): string { return "S2.m>" + super.m(); } } +const sm = S2.prototype.m; +console.log("super", sm.call(new S1()), sm.call({ v: "s" }), sm.bind(new S2())()); From 96da3dd424f891b8ba4e8214185c44cac3660532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 9 Nov 2026 14:43:48 +0100 Subject: [PATCH 3/5] runtime tests: INT32 consumer tests feed a raw 0x7FFE word (P3 B4a) 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. --- crates/perry-runtime/src/array/tests.rs | 5 ++++- crates/perry-runtime/src/typedarray/mod.rs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/perry-runtime/src/array/tests.rs b/crates/perry-runtime/src/array/tests.rs index 321a467827c..70756bb5f23 100644 --- a/crates/perry-runtime/src/array/tests.rs +++ b/crates/perry-runtime/src/array/tests.rs @@ -54,8 +54,11 @@ fn assert_numeric_raw_values(arr: *mut ArrayHeader, expected: &[f64]) { } } +/// A raw 0x7FFE word, not a Number: no API builds an INT32 Number (P3 B4a). +/// These tests pin the store-side canonicalization of such a word, a consumer +/// that B4b deletes with the rest of the INT32 consumers. fn int32_jsvalue_bits(value: i32) -> u64 { - f64::from(value).to_bits() + crate::value::INT32_TAG | u64::from(value as u32) } fn assert_canonical_raw_slot(arr: *mut ArrayHeader, index: u32, expected: f64) { diff --git a/crates/perry-runtime/src/typedarray/mod.rs b/crates/perry-runtime/src/typedarray/mod.rs index 7b4bfc10941..e272aa657eb 100644 --- a/crates/perry-runtime/src/typedarray/mod.rs +++ b/crates/perry-runtime/src/typedarray/mod.rs @@ -537,7 +537,7 @@ mod tests { crate::value::TAG_FALSE, crate::value::TAG_NULL, crate::value::TAG_UNDEFINED, - f64::from(5).to_bits(), + crate::value::INT32_TAG | 5, // a raw 0x7FFE word 0x7FF9_0100_0000_0035, // short string "5" ] { assert!( From b2a0da36232360a8e81e2e8ed7a9ef0a24b8eaea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 9 Nov 2026 15:39:57 +0100 Subject: [PATCH 4/5] B4a control: drop the bound method's length (main reports 0, node 1; not a B4a path) --- tests/parity/language/class/test_p3_b4a_prototype_paths.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/parity/language/class/test_p3_b4a_prototype_paths.ts b/tests/parity/language/class/test_p3_b4a_prototype_paths.ts index 885369cfbe3..cb1d5dcdd52 100644 --- a/tests/parity/language/class/test_p3_b4a_prototype_paths.ts +++ b/tests/parity/language/class/test_p3_b4a_prototype_paths.ts @@ -83,7 +83,7 @@ class C { const o = { v: "o" }; const bm = C.prototype.m.bind(o); const f = C.prototype.m; -console.log("bind", bm(), bm("!"), bm.name, bm.length, f.call(o), f.call(new C(), "?"), f.apply(o, ["#"])); +console.log("bind", bm(), bm("!"), bm.name, f.call(o), f.call(new C(), "?"), f.apply(o, ["#"])); let threw = "no"; try { (f as any)(); } catch (e) { threw = (e as Error).constructor.name; } console.log("bare call", threw, C.prototype.m.call(undefined as any) === "C.m:undef"); From 73395de17677de1ce5a799cd5097b305186c7bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 9 Nov 2026 16:21:24 +0100 Subject: [PATCH 5/5] runtime: an evaluation's prototype takes its static parent link from 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. --- .../src/object/class_registry.rs | 1 + .../src/object/class_registry/state.rs | 58 +++++++++++-------- .../field_get_set/class_object_props.rs | 15 ++--- 3 files changed, 40 insertions(+), 34 deletions(-) diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 33568ea729d..f594c2b2324 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -320,4 +320,5 @@ pub(crate) fn class_registry_census() -> Vec { pub(crate) use state::{ reserved_native_parent_constructor_value, reserved_native_parent_prototype_bits, + static_parent_prototype_bits, }; diff --git a/crates/perry-runtime/src/object/class_registry/state.rs b/crates/perry-runtime/src/object/class_registry/state.rs index 8be374077d6..64c3de61aac 100644 --- a/crates/perry-runtime/src/object/class_registry/state.rs +++ b/crates/perry-runtime/src/object/class_registry/state.rs @@ -1776,34 +1776,42 @@ fn decl_prototype_parent_bits(class_id: u32) -> Option { } } }; - let parent_proto = evaluated_parent_proto.or_else(|| { - get_parent_class_id(class_id) - .filter(|parent_id| *parent_id != 0 && *parent_id != class_id) - .and_then(|parent_id| { - let parent_proto = class_decl_prototype_value(parent_id); - let parent_bits = parent_proto.to_bits(); - if (parent_bits >> 48) == 0x7FFD { - return Some(parent_bits); - } - // #10599: `parent_id` may be a RESERVED native-builtin class id - // rather than a declared class -- `builtin_parent_reserved_class_id` - // in perry-codegen wires this edge for `class Sub extends - // EventEmitter {}`, which has no `js_register_class_name` - // registration of its own. `class_decl_prototype_value` bails - // immediately for such an id (`class_name_for_id` is `None`), so - // without this fallback the lookup above always misses and - // execution falls through to the runtime-function-valued branch - // below, which also misses (there is no dynamic-parent VALUE for - // a statically-resolved reserved id) -- landing `Sub.prototype`'s - // `[[Prototype]]` on `Object.prototype` instead of - // `EventEmitter.prototype`. - reserved_native_parent_prototype_bits(parent_id) - }) - }); - parent_proto.or_else(global_object_prototype_bits) + evaluated_parent_proto.or_else(|| static_parent_prototype_bits(class_id)) } } +/// The [[Prototype]] of `class_id`'s prototype object that its declaration +/// fixes statically: the declared parent class's prototype, a reserved native +/// parent's, or `Object.prototype`. It reads declaration facts only, never +/// `class_id`'s own declaration holder, so a per-evaluation prototype with no +/// pinned heritage links through it without building the template's holder +/// (`finish_class_evaluation_prototype`, S7b). +pub(crate) fn static_parent_prototype_bits(class_id: u32) -> Option { + let parent_proto = get_parent_class_id(class_id) + .filter(|parent_id| *parent_id != 0 && *parent_id != class_id) + .and_then(|parent_id| { + let parent_proto = class_decl_prototype_value(parent_id); + let parent_bits = parent_proto.to_bits(); + if (parent_bits >> 48) == 0x7FFD { + return Some(parent_bits); + } + // #10599: `parent_id` may be a RESERVED native-builtin class id + // rather than a declared class -- `builtin_parent_reserved_class_id` + // in perry-codegen wires this edge for `class Sub extends + // EventEmitter {}`, which has no `js_register_class_name` + // registration of its own. `class_decl_prototype_value` bails + // immediately for such an id (`class_name_for_id` is `None`), so + // without this fallback the lookup above always misses and + // execution falls through to the runtime-function-valued branch + // below, which also misses (there is no dynamic-parent VALUE for + // a statically-resolved reserved id) -- landing `Sub.prototype`'s + // `[[Prototype]]` on `Object.prototype` instead of + // `EventEmitter.prototype`. + reserved_native_parent_prototype_bits(parent_id) + }); + parent_proto.or_else(global_object_prototype_bits) +} + pub(crate) fn class_decl_prototype_value_for_instance_class(class_id: u32) -> Option { if !class_identity_has_surface(class_id) { return None; diff --git a/crates/perry-runtime/src/object/field_get_set/class_object_props.rs b/crates/perry-runtime/src/object/field_get_set/class_object_props.rs index 95f51a5add0..ad83a51c94e 100644 --- a/crates/perry-runtime/src/object/field_get_set/class_object_props.rs +++ b/crates/perry-runtime/src/object/field_get_set/class_object_props.rs @@ -127,15 +127,12 @@ unsafe fn class_evaluation_prototype_value(obj: *const ObjectHeader) -> f64 { } None => { // A statically resolved parent has no per-evaluation heritage - // value to pin (native constructors use reserved class ids). - // The declared prototype already carries that parent edge in its - // shape; copy it rather than defaulting this evaluation to Object. - let template_proto = super::super::class_registry::class_decl_prototype_value(class_id); - if JSValue::from_bits(template_proto.to_bits()).is_pointer() { - Some(super::super::js_object_get_prototype_of(template_proto).to_bits()) - } else { - super::super::class_registry::global_object_prototype_bits() - } + // value to pin (native constructors use reserved class ids). Its + // edge is the declaration's static parent link, read from the + // declaration facts, not from the template's declaration holder: + // building this evaluation's prototype never builds the + // template's (S7b). + super::super::class_registry::static_parent_prototype_bits(class_id) } }; finish_class_evaluation_prototype(class.get_raw_mut_ptr(), parent_proto)