Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
8caf8c9
refactor(runtime): one reader for "which class does this constructor …
Sep 26, 2026
a463fd5
fix(runtime,codegen): a class used as a value is its function object …
Sep 26, 2026
4fbe113
perf(codegen,runtime): class values cost a cached load; gates for the…
Sep 26, 2026
db5918c
perf(runtime): reject an ordinary function before proving a class fun…
Sep 26, 2026
2e6b145
refactor(runtime): class statics are the class function object's own …
Sep 26, 2026
6a5cd0d
fix(runtime): a class function object as a dynamic parent, a bind tar…
Sep 26, 2026
a10b3e9
perf(runtime): class probes off the hot bind and callable paths
Sep 26, 2026
888124b
chore: root-holder inventory for the class-value table and the delete…
Sep 26, 2026
1aed5f5
perf(runtime,codegen): a class function object reaches the class look…
Sep 26, 2026
31737ff
perf(runtime): class constructors are a shape fact; ordinary receiver…
Sep 26, 2026
5317821
perf(runtime): keep value_is_callable inlined (bind path)
Sep 26, 2026
c77b882
perf(runtime): class reads use the caller's key header; ordinary func…
Sep 26, 2026
9a9107c
perf(runtime): one class probe per method-value read; the verdict cac…
Sep 27, 2026
221c633
refactor(runtime): a class constructor's own [[Prototype]] is recorde…
Sep 26, 2026
ea96c7a
fix(runtime,codegen): a declared static's compiled alias follows the …
Sep 26, 2026
0c5906e
refactor(runtime): class static symbols are own symbol properties of …
Sep 26, 2026
a2c21fb
fix(runtime): a class constructor's name and length are own data of i…
Sep 27, 2026
3a24e30
chore: gate inventories after the port onto the function-own-props base
Sep 27, 2026
83dbfb7
changelog: name the fragments after PR #11609
Sep 28, 2026
a77b52e
Merge remote-tracking branch 'origin/main' into mq-11609
Sep 28, 2026
6921b7e
Merge remote-tracking branch 'origin/main' into mq-11609
Sep 28, 2026
ed76b13
wasm32: regenerate runtime_abi.tsv for the class-as-function-object s…
Sep 28, 2026
1cdb55f
Merge remote-tracking branch 'origin/main' into mq-11609
Sep 28, 2026
df94a3a
fix(codegen): a class expression's capture refresh re-reads its captu…
Sep 28, 2026
dcc7591
Merge origin/main: adopt generated gc-call-effects tables (#11565)
Sep 28, 2026
ff0eaeb
gc_effects: regenerate tables for the class function object entries
Sep 28, 2026
4e5c40e
feat(runtime): class static accessors are accessor properties of the …
Sep 28, 2026
fe05e88
changelog: name the fragment after PR #11651
Sep 28, 2026
4eb457f
runtime: trace a class function object's statics bag from the class-v…
Sep 28, 2026
19cbd0c
runtime: move class-method binding out of native_module.rs
Sep 28, 2026
98c407d
runtime: repoint the class-method-bind holder entries; read the stati…
Sep 28, 2026
e6296cf
fix(codegen): a class capture refresh roots its capture array in one …
Sep 29, 2026
155704a
Merge main into perf-class-function-objects
Sep 29, 2026
a37764a
Merge commit 'refs/tmp/p11609' into HEAD
Sep 29, 2026
54d9d03
gc_effects: regenerate linux-x86_64 table after merging perf-class-fu…
Sep 29, 2026
3a51d4d
Merge branch 'main' into perf-class-function-objects
proggeramlug Sep 29, 2026
b5ce445
Merge branch 'perf-class-function-objects' into perf-class-static-acc…
proggeramlug Sep 29, 2026
4e41a3b
Merge main into perf-class-function-objects
Sep 29, 2026
d147168
Merge perf-class-function-objects into perf-class-static-accessors
Sep 29, 2026
6383cff
Merge main into perf-class-static-accessors
Sep 29, 2026
f2eab3a
Merge branch 'main' into perf-class-static-accessors
proggeramlug Sep 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions changelog.d/11651-class-static-accessor-properties.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Class static accessors (`static get x()` / `static set x(v)`) are now real
accessor properties of the class's function object, so reflection,
`defineProperty`, `delete`, `Object.keys`, `propertyIsEnumerable`, `super.x`
and inherited reads all see one property with its attributes. A write to a
getter-only static is rejected (#11521): strict assignment throws the
TypeError node throws, and `Reflect.set` returns `false`, on the class and on
subclasses. A static walk that reaches a builtin parent (`class E extends
Error`) stops there instead of minting a class function object for it.
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/gc_effects/linux-x86_64.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -2145,7 +2145,7 @@ js_object_get_symbol_property_ic_miss Reenters
js_object_get_symbol_then_field_ic_miss Reenters
js_object_group_by Reenters
js_object_has_own Reenters
js_object_has_own_symbol Reenters
js_object_has_own_symbol AllocOnly
js_object_has_property Reenters
js_object_is Reenters
js_object_is_extensible Reenters
Expand Down
79 changes: 71 additions & 8 deletions crates/perry-runtime/src/closure/props.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub(crate) unsafe fn bag_of(ptr: usize) -> *mut ObjectHeader {
}

/// Allocate the bag if absent and install it with the store barrier.
unsafe fn bag_ensure(ptr: usize) -> *mut ObjectHeader {
pub(crate) unsafe fn bag_ensure(ptr: usize) -> *mut ObjectHeader {
let existing = bag_of(ptr);
if !existing.is_null() {
return existing;
Expand All @@ -61,11 +61,14 @@ unsafe fn object_own_get(obj: *const ObjectHeader, key: &[u8]) -> Option<f64> {
// come from the same descriptor.
if let Some(d) = crate::object::shapes::object_shape_descriptor(obj) {
if d.object_kind == crate::object::shapes::ShapeObjectKind::Ordinary && d.keys != 0 {
let slot = crate::object::keys_find_slot_by_bytes_resolved(
d.keys as usize as *const crate::array::ArrayHeader,
d.logical_key_count,
key,
)?;
let keys = d.keys as usize as *const crate::array::ArrayHeader;
let slot =
crate::object::keys_find_slot_by_bytes_resolved(keys, d.logical_key_count, key)?;
// An accessor key's slot holds its getter/setter pair, never a
// data value.
if crate::object::key_attrs::key_is_accessor_at(keys, slot as u32) {
return None;
}
let value =
crate::object::object_field_at_with_live(obj, slot, d.live_inline_slot_count);
if value.bits() == crate::value::TAG_HOLE {
Expand All @@ -80,6 +83,9 @@ unsafe fn object_own_get(obj: *const ObjectHeader, key: &[u8]) -> Option<f64> {
return None;
}
let slot = crate::object::keys_find_slot_by_bytes_resolved(arr, keys.count(), key)?;
if crate::object::key_attrs::key_is_accessor_at(arr, slot as u32) {
return None;
}
let live = crate::object::object_live_slot_count(obj);
let value = crate::object::object_field_at_with_live(obj, slot, live);
if value.bits() == crate::value::TAG_HOLE {
Expand Down Expand Up @@ -122,7 +128,7 @@ pub(crate) unsafe fn bag_set(ptr: usize, key: &str, value: f64) {
/// `ptr` is a proven, live closure cell.
pub(crate) unsafe fn bag_remove(ptr: usize, key: &str) -> bool {
let bag = bag_of(ptr);
if bag.is_null() || object_own_get(bag, key.as_bytes()).is_none() {
if bag.is_null() || !bag_has_own(ptr, key.as_bytes()) {
return false;
}
let _no_move = crate::gc::GcSuppressScope::new();
Expand All @@ -131,6 +137,61 @@ pub(crate) unsafe fn bag_remove(ptr: usize, key: &str) -> bool {
true
}

/// Does the function own `key` — a data OR an accessor property?
///
/// # Safety
/// `ptr` is a proven, live closure cell.
pub(crate) unsafe fn bag_has_own(ptr: usize, key: &[u8]) -> bool {
let bag = bag_of(ptr);
if bag.is_null() {
return false;
}
let keys = crate::object::object_keys(bag);
let arr = keys.arr();
if arr.is_null() {
return false;
}
let Some(slot) = crate::object::keys_find_slot_by_bytes_resolved(arr, keys.count(), key) else {
return false;
};
crate::object::key_attrs::key_is_accessor_at(arr, slot as u32)
|| crate::object::object_field_at_with_live(
bag,
slot,
crate::object::object_live_slot_count(bag),
)
.bits()
!= crate::value::TAG_HOLE
}

/// The function's own ACCESSOR property names, in creation order.
///
/// # Safety
/// `ptr` is a proven, live closure cell.
pub(crate) unsafe fn bag_accessor_names(ptr: usize) -> Vec<String> {
let bag = bag_of(ptr);
if bag.is_null() {
return Vec::new();
}
let keys = crate::object::object_keys(bag);
let arr = keys.arr();
if arr.is_null() {
return Vec::new();
}
let mut out = Vec::new();
for i in 0..keys.count() {
if !crate::object::key_attrs::key_is_accessor_at(arr, i) {
continue;
}
let key = JSValue::from_bits(crate::array::js_array_get_f64(arr, i).to_bits());
let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN];
if let Some(bytes) = crate::string::js_string_key_bytes(key, &mut scratch) {
out.push(String::from_utf8_lossy(bytes).into_owned());
}
}
out
}

/// Every own data property in ECMA-262 own-key order: integer indices
/// ascending, then other strings in creation order.
///
Expand All @@ -151,7 +212,9 @@ pub(crate) unsafe fn bag_snapshot(ptr: usize) -> Vec<(String, f64)> {
let mut strings: Vec<(String, f64)> = Vec::new();
for i in 0..keys.count() {
let value = crate::object::object_field_at_with_live(bag, i, live);
if value.bits() == crate::value::TAG_HOLE {
if value.bits() == crate::value::TAG_HOLE
|| crate::object::key_attrs::key_is_accessor_at(arr, i)
{
continue;
}
let key = JSValue::from_bits(crate::array::js_array_get_f64(arr, i).to_bits());
Expand Down
13 changes: 5 additions & 8 deletions crates/perry-runtime/src/object/class_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,13 @@ mod prototype_methods;
pub(crate) mod prototype_objects;
mod registration;
mod state;
mod static_accessor_attrs;
pub(crate) mod verdict_classes;
mod vm_brand;

// ── static_accessor_attrs.rs ────────────────────────────────────────────────
pub(crate) use static_accessor_attrs::{
set_static_accessor_attrs, static_accessor_attrs, static_accessor_attrs_in_use,
static_accessor_descriptor, static_declared_accessor_ptrs, static_enumerable_accessor_names,
CLASS_ACCESSOR_DEFAULT_ATTRS,
};
// Static accessors are accessor properties of the class function object
// (`object::class_value`); the ClassBody defaults are shared with instance
// accessors.
pub(crate) use crate::object::class_value::CLASS_ACCESSOR_DEFAULT_ATTRS;

// ── state.rs ────────────────────────────────────────────────────────────────
pub(crate) use state::async_resource_prototype_value;
Expand Down Expand Up @@ -201,7 +198,7 @@ pub(crate) use gc_roots::{
// ── registration.rs ─────────────────────────────────────────────────────────
pub(crate) use registration::{
class_accessor_function_value, class_accessor_source_func_ptr, class_own_accessor_ptrs,
class_own_static_accessor_ptrs, invalidate_class_string_member_order,
class_registered_static_accessor_ptrs, invalidate_class_string_member_order,
};
pub use registration::{
is_class_id_registered, js_register_class_getter, js_register_class_method,
Expand Down
111 changes: 0 additions & 111 deletions crates/perry-runtime/src/object/class_registry/parent_static.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1136,117 +1136,6 @@ pub(crate) unsafe fn class_symbol_setter_apply(
})
}

pub(crate) unsafe fn class_static_accessor_getter_value(
class_id: u32,
name: &str,
receiver: f64,
) -> Option<f64> {
let guard = CLASS_STATIC_ACCESSORS.read().ok();
let map = guard.as_ref().and_then(|guard| guard.as_ref());
let mut cid = class_id;
let mut depth = 0usize;
while cid != 0 && depth < 32 {
// A descriptor installed by `defineProperty` replaces an existing
// class-body accessor at the same inheritance level.
if let Some(result) = class_dynamic_static_accessor_getter_value(cid, name, receiver) {
return Some(result);
}
if let Some(accessors) = map.and_then(|map| map.get(&cid)) {
if let Some(&(getter, _)) = accessors.get(name) {
if getter == 0 {
return Some(f64::from_bits(crate::value::TAG_UNDEFINED));
}
// #10911: when this getter was reached by walking the STATIC
// prototype chain -- a subclass reading an accessor declared
// on its parent class OBJECT -- `receiver` is that parent, the
// object the getter lives on. `resolve_proto_chain_field_inner`
// stashes the class the read actually started from, exactly as
// it does for instance getters (see `class_getter_this`), and
// spec OrdinaryGet threads that Receiver through. Bind `this`
// to it, or `Sub.accessor` runs with `this === Base`.
//
// Effect's `static get ast() { return getClassSchema(this).ast }`
// is this shape: the schema memoised against the base class, so
// decoded errors were built from the base and were not
// `instanceof` their own class (#10891).
//
// `this` and the capture/private OWNER are two different
// things here and must not be collapsed: `this` is the class
// the read started from, while the owner is the evaluation the
// getter was FOUND on -- the object whose `__perry_ctor_caps`
// hold its captured variables and whose brand gates `#x`.
// `js_class_capture_value_for_receiver` prefers the owner, so
// binding it to the subclass would lose every capture.
let owner = receiver;
let receiver = crate::object::field_get_set::accessor_receiver_override_take()
.unwrap_or(receiver);
// Static accessor bodies use the same receiver-resolving
// prologue as static methods. In particular, a fresh class
// expression must expose its per-evaluation class object as
// `this`, not the shared compile-time ClassRef.
crate::object::static_this_arm_if_unarmed(receiver);
crate::object::static_private_owner_push(owner);
let f: extern "C" fn() -> f64 = std::mem::transmute(getter);
let result = f();
crate::object::static_private_owner_pop();
crate::object::static_this_disarm();
return Some(result);
}
}
match get_parent_class_id(cid) {
Some(p) if p != 0 && p != cid => {
cid = p;
depth += 1;
}
_ => break,
}
}
None
}

pub(crate) unsafe fn class_static_accessor_setter_apply(
class_id: u32,
name: &str,
receiver: f64,
value: f64,
) -> bool {
let guard = CLASS_STATIC_ACCESSORS.read().ok();
let map = guard.as_ref().and_then(|guard| guard.as_ref());
let mut cid = class_id;
let mut depth = 0usize;
while cid != 0 && depth < 32 {
if let Some(applied) =
class_dynamic_static_accessor_setter_apply(cid, name, receiver, value)
{
return applied;
}
if let Some(accessors) = map.and_then(|map| map.get(&cid)) {
if let Some(&(_, setter)) = accessors.get(name) {
if setter != 0 {
// Mirror the getter path: the compiled static-accessor
// prologue consumes this override and binds `this` to the
// actual constructor value for this evaluation.
crate::object::static_this_arm_if_unarmed(receiver);
crate::object::static_private_owner_push(receiver);
let f: extern "C" fn(f64) -> f64 = std::mem::transmute(setter);
let _ = f(value);
crate::object::static_private_owner_pop();
crate::object::static_this_disarm();
}
return true;
}
}
match get_parent_class_id(cid) {
Some(p) if p != 0 && p != cid => {
cid = p;
depth += 1;
}
_ => break,
}
}
false
}

/// Apply an instance `set name(v)` accessor from the class vtable chain,
/// invoking it with the `(this, value)` calling convention class setters use.
/// Returns `true` if a setter was found and called. Used when a write targets
Expand Down
Loading
Loading