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
10 changes: 10 additions & 0 deletions crates/perry-codegen-js/src/emit/exprs_more.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1545,6 +1545,16 @@ impl JsEmitter {
self.emit_expr(target);
self.output.push(')');
}
Expr::ReflectIsExtensible(target) => {
self.output.push_str("Reflect.isExtensible(");
self.emit_expr(target);
self.output.push(')');
}
Expr::ReflectPreventExtensions(target) => {
self.output.push_str("Reflect.preventExtensions(");
self.emit_expr(target);
self.output.push(')');
}
// Fallback for HIR variants the JS emitter doesn't model directly
// (e.g. TypedArrayNew). Emit `undefined` so the emitted JS still
// parses; these paths are unused for the LLVM-backend sweeps.
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/collectors/i32_locals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,8 @@ pub fn collect_localset_ids_in_expr_filtered(
| Expr::ObjectIsFrozen(operand)
| Expr::ObjectIsSealed(operand)
| Expr::ObjectIsExtensible(operand)
| Expr::ReflectIsExtensible(operand)
| Expr::ReflectPreventExtensions(operand)
| Expr::SetSize(operand)
| Expr::SetClear(operand)
| Expr::ArrayFrom(operand)
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/collectors/refs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ pub fn collect_ref_ids_in_expr(e: &perry_hir::Expr, out: &mut HashSet<u32>) {
| Expr::ObjectIsFrozen(operand)
| Expr::ObjectIsSealed(operand)
| Expr::ObjectIsExtensible(operand)
| Expr::ReflectIsExtensible(operand)
| Expr::ReflectPreventExtensions(operand)
| Expr::SetSize(operand)
| Expr::SetClear(operand)
| Expr::ArrayFrom(operand)
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1852,6 +1852,8 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
| Expr::ReflectConstruct { .. }
| Expr::ReflectDefineProperty { .. }
| Expr::ReflectGetPrototypeOf(..)
| Expr::ReflectIsExtensible(..)
| Expr::ReflectPreventExtensions(..)
| Expr::ReflectDefineMetadata { .. }
| Expr::ReflectGetMetadata { .. }
| Expr::ReflectGetOwnMetadata { .. }
Expand Down
29 changes: 26 additions & 3 deletions crates/perry-codegen/src/expr/proxy_reflect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,32 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
))
}
Expr::ReflectGetPrototypeOf(target) => {
// Pragmatic: the test only checks `=== Dog.prototype`, which
// the compiler folds to a compile-time bool. Return target.
lower_expr(ctx, target)
// #2757: return the actual [[Prototype]] (shared with
// Object.getPrototypeOf), not the target object itself. The
// `=== Class.prototype` comparison is still folded to a constant
// bool at lowering time (lower_expr.rs); this path handles every
// other (value-returning) use.
let t = lower_expr(ctx, target)?;
Ok(ctx
.block()
.call(DOUBLE, "js_reflect_get_prototype_of", &[(DOUBLE, &t)]))
}
Expr::ReflectIsExtensible(target) => {
// #2762: Reflect-specific — boolean result + TypeError on
// non-object, distinct from Object.isExtensible.
let t = lower_expr(ctx, target)?;
Ok(ctx
.block()
.call(DOUBLE, "js_reflect_is_extensible", &[(DOUBLE, &t)]))
}
Expr::ReflectPreventExtensions(target) => {
// #2762: Reflect-specific — boolean result + TypeError on
// non-object, distinct from Object.preventExtensions (which
// returns the object).
let t = lower_expr(ctx, target)?;
Ok(ctx
.block()
.call(DOUBLE, "js_reflect_prevent_extensions", &[(DOUBLE, &t)]))
}
Expr::ReflectDefineMetadata {
key,
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-codegen/src/runtime_decls/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ pub fn declare_phase_b_objects(module: &mut LlModule) {
DOUBLE,
&[DOUBLE, DOUBLE, DOUBLE],
);
module.declare_function("js_reflect_get_prototype_of", DOUBLE, &[DOUBLE]);
module.declare_function("js_reflect_is_extensible", DOUBLE, &[DOUBLE]);
module.declare_function("js_reflect_prevent_extensions", DOUBLE, &[DOUBLE]);
module.declare_function(
"js_reflect_define_metadata",
DOUBLE,
Expand Down
5 changes: 5 additions & 0 deletions crates/perry-hir/src/ir/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2102,6 +2102,11 @@ pub enum Expr {
descriptor: Box<Expr>,
},
ReflectGetPrototypeOf(Box<Expr>),
// #2762: Reflect.isExtensible / Reflect.preventExtensions have
// Reflect-specific semantics (boolean result, TypeError on non-object)
// distinct from the Object.* helpers, so they use dedicated variants.
ReflectIsExtensible(Box<Expr>),
ReflectPreventExtensions(Box<Expr>),
ReflectDefineMetadata {
key: Box<Expr>,
value: Box<Expr>,
Expand Down
8 changes: 6 additions & 2 deletions crates/perry-hir/src/lower/expr_call/native_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1059,12 +1059,16 @@ pub(super) fn try_native_module_methods(
}
"setPrototypeOf" => return Ok(Ok(Expr::Bool(true))),
"isExtensible" => {
// #2762: Reflect-specific semantics (boolean +
// TypeError on non-object), NOT Object.isExtensible.
let target = args.into_iter().next().unwrap_or(Expr::Undefined);
return Ok(Ok(Expr::ObjectIsExtensible(Box::new(target))));
return Ok(Ok(Expr::ReflectIsExtensible(Box::new(target))));
}
"preventExtensions" => {
// #2762: Reflect-specific semantics (boolean +
// TypeError on non-object), NOT Object.preventExtensions.
let target = args.into_iter().next().unwrap_or(Expr::Undefined);
return Ok(Ok(Expr::ObjectPreventExtensions(Box::new(target))));
return Ok(Ok(Expr::ReflectPreventExtensions(Box::new(target))));
}
_ => {}
}
Expand Down
4 changes: 3 additions & 1 deletion crates/perry-hir/src/stable_hash/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ impl SH for Expr {
Expr::StringSplit(a, b) => { tag(h, 282); a.as_ref().hash(h); b.as_ref().hash(h); }
Expr::StringFromCharCode(e) => { tag(h, 283); e.as_ref().hash(h); }
Expr::StringFromCodePoint(e) => { tag(h, 284); e.as_ref().hash(h); }
Expr::StringRaw { call_site, substitutions } => { tag(h, 12043); call_site.as_ref().hash(h); substitutions.hash(h); }
Expr::StringRaw { call_site, substitutions } => { tag(h, 12047); call_site.as_ref().hash(h); substitutions.hash(h); }
Expr::StringAt { string, index } => { tag(h, 285); string.as_ref().hash(h); index.as_ref().hash(h); }
Expr::StringCodePointAt { string, index } => { tag(h, 286); string.as_ref().hash(h); index.as_ref().hash(h); }
Expr::MapNew => tag(h, 287),
Expand Down Expand Up @@ -549,6 +549,8 @@ impl SH for Expr {
Expr::ReflectConstruct { target, args } => { tag(h, 439); target.as_ref().hash(h); args.as_ref().hash(h); }
Expr::ReflectDefineProperty { target, key, descriptor, } => { tag(h, 440); target.as_ref().hash(h); key.as_ref().hash(h); descriptor.as_ref().hash(h); }
Expr::ReflectGetPrototypeOf(e) => { tag(h, 441); e.as_ref().hash(h); }
Expr::ReflectIsExtensible(e) => { tag(h, 12045); e.as_ref().hash(h); }
Expr::ReflectPreventExtensions(e) => { tag(h, 12046); e.as_ref().hash(h); }
Expr::ReflectDefineMetadata { key, value, target, property_key, } => { tag(h, 12023); key.as_ref().hash(h); value.as_ref().hash(h); target.as_ref().hash(h); property_key.hash(h); }
Expr::ReflectGetMetadata { key, target, property_key, } => { tag(h, 12024); key.as_ref().hash(h); target.as_ref().hash(h); property_key.hash(h); }
Expr::ReflectGetOwnMetadata { key, target, property_key, } => { tag(h, 455); key.as_ref().hash(h); target.as_ref().hash(h); property_key.hash(h); }
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-hir/src/walker/expr_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ where
| Expr::ProxyRevoke(v)
| Expr::ReflectOwnKeys(v)
| Expr::ReflectGetPrototypeOf(v)
| Expr::ReflectIsExtensible(v)
| Expr::ReflectPreventExtensions(v)
| Expr::DateGetTime(v)
| Expr::DateToISOString(v)
| Expr::DateGetFullYear(v)
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-hir/src/walker/expr_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ where
| Expr::ProxyRevoke(v)
| Expr::ReflectOwnKeys(v)
| Expr::ReflectGetPrototypeOf(v)
| Expr::ReflectIsExtensible(v)
| Expr::ReflectPreventExtensions(v)
| Expr::DateGetTime(v)
| Expr::DateToISOString(v)
| Expr::DateGetFullYear(v)
Expand Down
113 changes: 3 additions & 110 deletions crates/perry-runtime/src/object/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ mod native_module_dispatch;
mod native_module_stream;
mod object_ops;
mod polymorphic_index;
mod reflect_support;
mod util_types;
pub use alloc::*;
pub use assert::*;
Expand Down Expand Up @@ -71,6 +72,7 @@ pub(crate) use native_module_dispatch::*;
pub(crate) use native_module_stream::*;
pub use object_ops::*;
pub use polymorphic_index::*;
pub(crate) use reflect_support::*;
pub use util_types::*;

static HTTP_METHODS_CACHE: AtomicU64 = AtomicU64::new(0);
Expand Down Expand Up @@ -1888,113 +1890,4 @@ pub(super) unsafe fn rebuild_array_layout_from_slots(arr: *mut ArrayHeader) {
}
}
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_object_alloc_and_fields() {
let obj = js_object_alloc(1, 3);

// Check header
assert_eq!(js_object_get_class_id(obj), 1);

// Fields should be undefined initially
let f0 = js_object_get_field(obj, 0);
assert!(f0.is_undefined());

// Set and get a field
js_object_set_field(obj, 0, JSValue::number(42.0));
let f0 = js_object_get_field(obj, 0);
assert!(f0.is_number());
assert_eq!(f0.as_number(), 42.0);

// Set another field
js_object_set_field(obj, 2, JSValue::bool(true));
let f2 = js_object_get_field(obj, 2);
assert!(f2.is_bool());
assert!(f2.as_bool());

// Clean up
js_object_free(obj);
}

#[test]
fn test_object_to_value_roundtrip() {
let obj = js_object_alloc(5, 2);
js_object_set_field(obj, 0, JSValue::number(123.0));

let value = js_object_to_value(obj);
assert!(value.is_pointer());

let obj2 = js_value_to_object(value);
assert_eq!(js_object_get_class_id(obj2), 5);

let f0 = js_object_get_field(obj2, 0);
assert_eq!(f0.as_number(), 123.0);

js_object_free(obj);
}

#[test]
fn text_encoding_stream_globals_construct_readable_writable_shape() {
unsafe {
let global = js_get_global_this();
let global_ptr = crate::value::js_nanbox_get_pointer(global) as *const ObjectHeader;
assert!(!global_ptr.is_null());

for ctor_name in ["TextEncoderStream", "TextDecoderStream"] {
let ctor_key =
crate::string::js_string_from_bytes(ctor_name.as_ptr(), ctor_name.len() as u32);
let ctor = js_object_get_field_by_name(global_ptr, ctor_key);
assert!(
ctor.is_pointer(),
"{ctor_name} should be a closure-backed global"
);

let ctor_ptr = ctor.as_pointer::<crate::closure::ClosureHeader>();
assert_eq!((*ctor_ptr).type_tag, crate::closure::CLOSURE_MAGIC);

let instance =
js_new_function_construct(f64::from_bits(ctor.bits()), std::ptr::null(), 0);
for field in ["readable", "writable"] {
let key =
crate::string::js_string_from_bytes(field.as_ptr(), field.len() as u32);
let key_box = f64::from_bits(JSValue::string_ptr(key).bits());
let present = js_object_has_property(instance, key_box);
assert_ne!(
crate::value::js_is_truthy(present),
0,
"{ctor_name} instance should expose {field}"
);
}
}
}
}

#[test]
fn transition_cache_lookup_rejects_mutated_edge_target() {
let key = crate::string::js_string_from_bytes(b"id".as_ptr(), 2);
let keys = crate::array::js_array_alloc(4);
let keys = crate::array::js_array_push(keys, JSValue::string_ptr(key));
let keys = crate::array::js_array_push(keys, JSValue::string_ptr(key));

transition_cache_insert(0, key, keys as usize, 0);

assert!(
transition_cache_lookup(0, key).is_none(),
"slot 0 cache edge must not hit after its keys array grows past length 1"
);

let slot = transition_cache_slot(0, key as usize);
with_transition_cache(|t| unsafe {
// GC_STORE_AUDIT(ROOT): test cleanup writes non-pointer sentinels into scanned TRANSITION_CACHE_GLOBAL roots.
(*t)[slot] = TransitionEntry {
prev_keys: 0,
key_ptr: 0,
next_keys: 0,
slot_idx: 0,
target_len: 0,
};
});
}
}
mod tests;
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/object/object_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ pub(crate) unsafe fn extract_obj_ptr(value: f64) -> *mut ObjectHeader {
}

/// Helper: get GcHeader for an object pointer
unsafe fn gc_header_for(obj: *const ObjectHeader) -> *mut crate::gc::GcHeader {
pub(super) unsafe fn gc_header_for(obj: *const ObjectHeader) -> *mut crate::gc::GcHeader {
(obj as *mut u8).sub(crate::gc::GC_HEADER_SIZE) as *mut crate::gc::GcHeader
}

Expand Down
91 changes: 91 additions & 0 deletions crates/perry-runtime/src/object/reflect_support.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! Reflect-specific support predicates (#2756/#2758/#2760/#2762).
//!
//! These helpers expose just enough of an object's recorded metadata
//! (extensibility flag, own-key presence, per-property writable/configurable
//! attributes) for `crate::proxy`'s `Reflect.*` entry points to compute the
//! correct boolean results that Node returns — without the Reflect code
//! reaching into object internals directly. Split out of `object_ops.rs` to
//! keep that file under the 2000-line lint cap.

use super::object_ops::{extract_obj_ptr, gc_header_for};

/// Is `value` a heap object that codegen would treat as a target? Returns
/// `false` for primitives, null/undefined, class refs, and other non-pointer
/// tags. Used by `Reflect.preventExtensions` / `Reflect.isExtensible` to throw
/// a `TypeError` on non-object targets (whereas the `Object.*` helpers tolerate
/// them).
pub(crate) fn js_value_is_heap_object(value: f64) -> bool {
unsafe { !extract_obj_ptr(value).is_null() }
}

/// Does the heap object behind `value` currently carry the `OBJ_FLAG_NO_EXTEND`
/// flag? Returns `false` for non-objects.
pub(crate) fn obj_value_no_extend(value: f64) -> bool {
unsafe {
let obj = extract_obj_ptr(value);
if obj.is_null() || (obj as usize) <= 0x10000 {
return false;
}
let gc = gc_header_for(obj);
(*gc)._reserved & crate::gc::OBJ_FLAG_NO_EXTEND != 0
}
}

/// Does the heap object behind `value` have an own (string-keyed) property
/// named `key`? Used to distinguish "define a new property on a non-extensible
/// object" (fails) from "redefine an existing one" (may succeed). Symbol keys
/// are resolved through the symbol side-table.
pub(crate) fn obj_value_has_own_key(value: f64, key: f64) -> bool {
unsafe {
if crate::symbol::js_is_symbol(key) != 0 {
let v = crate::symbol::js_object_get_symbol_property(value, key);
return v.to_bits() != crate::value::TAG_UNDEFINED;
}
let obj = extract_obj_ptr(value);
if obj.is_null() {
return false;
}
let key_str = crate::builtins::js_string_coerce(key);
if key_str.is_null() {
return false;
}
let keys = (*obj).keys_array;
if keys.is_null() || (keys as usize) < 0x10000 {
return false;
}
let key_count = crate::array::js_array_length(keys) as usize;
for i in 0..key_count {
let stored = crate::array::js_array_get(keys, i as u32);
if crate::string::js_string_key_matches(stored, key_str) {
return true;
}
}
false
}
}

/// Look up the writable/configurable attributes Perry has recorded for
/// `(value, key)`. Returns `None` when no descriptor has been installed (the JS
/// default of all-true applies). The booleans are `(writable, configurable)`.
pub(crate) fn obj_value_attrs(value: f64, key: f64) -> Option<(bool, bool)> {
unsafe {
let obj = extract_obj_ptr(value);
if obj.is_null() {
return None;
}
let k = key_to_rust_string(key)?;
super::get_property_attrs(obj as usize, &k).map(|a| (a.writable(), a.configurable()))
}
}

unsafe fn key_to_rust_string(value: f64) -> Option<String> {
let key_str = crate::builtins::js_string_coerce(value);
if key_str.is_null() {
return None;
}
let name_ptr = (key_str as *const u8).add(std::mem::size_of::<crate::StringHeader>());
let name_len = (*key_str).byte_len as usize;
std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len))
.ok()
.map(|s| s.to_string())
}
Loading