Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
50 changes: 46 additions & 4 deletions crates/perry-codegen/src/codegen/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,46 @@ pub(super) fn init_static_fields_early(
&[(crate::types::I32, &cid_str), (I64, &func_ptr_i64)],
);
}
// Uninitialized, non-computed static fields (`static foo;`, `static "g";`,
// `static 0;`) are own data properties of the constructor with value
// `undefined` per ClassDefinitionEvaluation. Their value is a compile-time
// constant (`undefined`) with no dependency on user lets, and a class name
// is in TDZ before its declaration, so registering them here — before user
// code — is observably identical to registering at the class-decl position
// and strictly earlier than the `init_static_fields_late` fallback that
// previously handled them (which ran AFTER user statements, so
// `Object.keys(C)` / `getOwnPropertyDescriptor(C, "foo")` immediately after
// the declaration saw nothing). test262 class/elements static-as-valid-
// static-field & friends. Initialized and computed-key fields are emitted
// inline at their source position elsewhere and are skipped here.
for c in &hir.classes {
let Some(&class_id) = ctx.class_ids.get(&c.name) else {
continue;
};
if class_id == 0 {
continue;
}
for sf in &c.static_fields {
if sf.key_expr.is_some() || sf.init.is_some() || sf.name.starts_with('#') {
continue;
}
let idx = ctx.strings.intern(&sf.name);
let entry = ctx.strings.entry(idx);
let bytes_ref = format!("@{}", entry.bytes_global);
let len_str = entry.byte_len.to_string();
let cid_str = class_id.to_string();
let undef = crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED));
ctx.block().call_void(
"js_class_register_static_field",
&[
(crate::types::I32, &cid_str),
(crate::types::PTR, &bytes_ref),
(crate::types::I64, &len_str),
(DOUBLE, &undef),
],
);
}
}
Ok(())
}

Expand Down Expand Up @@ -744,11 +784,13 @@ pub(super) fn init_static_fields_late(
let g_ref = format!("@{}", global_name);
crate::expr::emit_root_nanbox_store_on_block(ctx.block(), &v, &g_ref);
emit_static_field_registration(ctx, &v);
} else {
let undef =
crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED));
emit_static_field_registration(ctx, &undef);
}
// Uninitialized non-computed static fields are now registered in
// `init_static_fields_early` (before user code) with value
// `undefined`. Re-registering here — after user statements — would
// clobber any `C.foo = …` the program performed between the class
// declaration and module-init end, so the no-init `else` branch was
// intentionally removed.
}
}
// Static blocks — emitted as synthetic static methods with the
Expand Down
22 changes: 22 additions & 0 deletions crates/perry-runtime/src/object/class_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,28 @@ pub(crate) fn class_own_static_field_value(class_id: u32, name: &str) -> Option<
})
}

/// Enumerable own string keys of a class constructor: the static fields (and
/// runtime `C.x = …` assignments) recorded in CLASS_DYNAMIC_PROPS. The built-in
/// `length`/`name`/`prototype` slots and static *methods*/*accessors* are
/// non-enumerable, so they are intentionally excluded — this is exactly the set
/// `Object.keys(C)` / `for (k in C)` must yield. Private (`#`) keys are filtered
/// here too (never reflectable). Returned unsorted; the caller applies ECMA
/// ordering. (test262 class/elements static-field-declaration & friends.)
pub(crate) fn class_own_enumerable_field_names(class_id: u32) -> Vec<String> {
CLASS_DYNAMIC_PROPS.with(|m| {
m.borrow()
.get(&class_id)
.map(|props| {
props
.keys()
.filter(|k| !k.starts_with('#'))
.cloned()
.collect()
})
.unwrap_or_default()
})
}

pub(crate) fn class_delete_own_dynamic_prop(class_id: u32, name: &str) {
CLASS_DYNAMIC_PROPS.with(|m| {
if let Some(props) = m.borrow_mut().get_mut(&class_id) {
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/object/descriptors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ fn property_name_array_index(name: &str) -> Option<u32> {
Some(value)
}

fn sort_property_names_ecma(names: &mut Vec<String>) {
pub(crate) fn sort_property_names_ecma(names: &mut Vec<String>) {
let mut indexed = Vec::new();
let mut rest = Vec::new();
for name in names.drain(..) {
Expand Down
18 changes: 18 additions & 0 deletions crates/perry-runtime/src/object/field_get_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,24 @@ 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
// 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::class_ref_id(value) {
if super::class_prototype_ref_id(value).is_none() {
let mut names = super::class_registry::class_own_enumerable_field_names(class_id);
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_pointer() {
let ptr = jv.as_pointer::<u8>() as usize;
if crate::value::addr_class::is_small_handle(ptr) {
Expand Down
18 changes: 18 additions & 0 deletions crates/perry-runtime/src/object/object_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,24 @@ pub extern "C" fn js_object_property_is_enumerable(obj_value: f64, key_value: f6
return f64::from_bits(TAG_FALSE);
}

// ClassRef receiver (INT32-tagged constructor, not a heap object): the
// only enumerable own string keys are the static FIELDS recorded in
// CLASS_DYNAMIC_PROPS — `length`/`name`/`prototype` and static
// methods/accessors are non-enumerable. `extract_obj_ptr` below would
// null out on the INT32 payload and report every key non-enumerable, so
// `verifyProperty(C, "f", …)`'s isEnumerable check failed (test262
// class/elements static-field-declaration & friends).
if let Some(class_id) = super::class_ref_id(obj_value) {
if super::class_prototype_ref_id(obj_value).is_none() {
if let Some(key_name) = super::has_own_helpers::str_from_string_header(key_str) {
let is_static_field = !key_name.starts_with('#')
&& super::class_registry::class_own_static_field_value(class_id, key_name)
.is_some();
return f64::from_bits(if is_static_field { TAG_TRUE } else { TAG_FALSE });
}
}
}

// String primitives: index keys in range are enumerable own props;
// "length" is a non-enumerable own prop; everything else absent.
if obj_jv.is_any_string() {
Expand Down