Skip to content
1 change: 1 addition & 0 deletions changelog.d/runtime-p3-b4a-delete-int32-producers.md
Original file line number Diff line number Diff line change
@@ -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).
7 changes: 5 additions & 2 deletions crates/perry-runtime-manifest/src/realm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
4 changes: 2 additions & 2 deletions crates/perry-runtime/src/array/dense_move_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
9 changes: 2 additions & 7 deletions crates/perry-runtime/src/array/dynamic_numeric_key_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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);
}
7 changes: 1 addition & 6 deletions crates/perry-runtime/src/array/negative_numeric_key_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/array/sort_numeric_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/array/subclass_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion crates/perry-runtime/src/array/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
crate::value::JSValue::int32(value).bits()
crate::value::INT32_TAG | u64::from(value as u32)
}

fn assert_canonical_raw_slot(arr: *mut ArrayHeader, index: u32, expected: f64) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions crates/perry-runtime/src/box/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/closure/births_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
95 changes: 33 additions & 62 deletions crates/perry-runtime/src/closure/dispatch/bound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand All @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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::<ClosureHeader>()).info
} else {
std::ptr::null()
};
let target_info = (*target_jv.as_pointer::<ClosureHeader>()).info;

// Root the bind target across every allocating call below (`this`
// boxing, `Get(Target, "name")` — which may run a user getter — the
Expand Down Expand Up @@ -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::<ClosureHeader>();
(*target).shape_id == crate::closure::shape::birth_shape_for_body(target_info)
Expand Down Expand Up @@ -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::<ClosureHeader>();
if crate::object::has_own_helpers::closure_own_key_present(target as usize, "length") {
Expand All @@ -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
Expand All @@ -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::<ClosureHeader>();
(*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 =
Expand All @@ -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 {
Expand Down
5 changes: 1 addition & 4 deletions crates/perry-runtime/src/closure/dispatch/value_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/closure/receiver_param_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/closure/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32> {
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();
Expand Down
Loading
Loading