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
43 changes: 43 additions & 0 deletions crates/perry-hir/src/lower/for_head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,46 @@ pub(crate) fn for_head_binding_stmts(
}
}
}

/// Wrap a desugared for-in loop body so a key that is deleted from the
/// receiver *before it is visited* is skipped, per ECMAScript for-in deletion
/// semantics (EnumerateObjectProperties: "If a property that has not yet been
/// visited during enumeration is deleted, then it will not be visited").
///
/// The keys are snapshotted once (`ForInKeys`), so without this guard a key
/// deleted mid-iteration would still be visited. `obj_id` holds the receiver
/// (spilled to a temp by the caller so it can be re-read each iteration),
/// `keys_id`/`idx_id` the snapshot array and cursor.
///
/// A primitive string is the only primitive whose for-in snapshot is non-empty
/// (its indices); its keys cannot be deleted, and the `in` operator *throws* on
/// a primitive receiver — so strings bypass the recheck and are always visited.
/// Objects/functions go through `key in obj`, which is `false` for a deleted
/// key and skips it. Nullish receivers never reach here (empty snapshot).
pub(crate) fn guard_for_in_body(
obj_id: LocalId,
keys_id: LocalId,
idx_id: LocalId,
body: Vec<Stmt>,
) -> Vec<Stmt> {
let guard = Expr::Conditional {
condition: Box::new(Expr::Compare {
op: CompareOp::Eq,
left: Box::new(Expr::TypeOf(Box::new(Expr::LocalGet(obj_id)))),
right: Box::new(Expr::String("string".to_string())),
}),
then_expr: Box::new(Expr::Bool(true)),
else_expr: Box::new(Expr::In {
property: Box::new(Expr::IndexGet {
object: Box::new(Expr::LocalGet(keys_id)),
index: Box::new(Expr::LocalGet(idx_id)),
}),
object: Box::new(Expr::LocalGet(obj_id)),
}),
};
vec![Stmt::If {
condition: guard,
then_branch: body,
else_branch: None,
}]
}
2 changes: 1 addition & 1 deletion crates/perry-hir/src/lower/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ mod stmt;
mod unimpl_hints;
pub(crate) use stmt::*;
mod for_head;
pub(crate) use for_head::{for_head_binding_stmts, predefine_for_head};
pub(crate) use for_head::{for_head_binding_stmts, guard_for_in_body, predefine_for_head};
mod stmt_loops;
pub(crate) use stmt_loops::{
insert_iterator_close_on_abrupt, lazy_iter_for_stmt, lazy_or_index_elem, lower_stmt_for_in,
Expand Down
17 changes: 15 additions & 2 deletions crates/perry-hir/src/lower/stmt_loops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1893,15 +1893,25 @@ pub(crate) fn lower_stmt_for_in(
// lowered below can reference them).
let head_binding = predefine_for_head(ctx, &for_in_stmt.left, Type::String)?;

// Lower the object expression
// Lower the object expression once, spilling it into a temp so each
// iteration can re-check that the current key still exists on the
// receiver (for-in deletion semantics — see `guard_for_in_body`).
let obj_expr = lower_expr(ctx, &for_in_stmt.right)?;
let obj_id = ctx.fresh_local();
module.init.push(Stmt::Let {
id: obj_id,
name: format!("__forin_obj_{}", obj_id),
ty: Type::Any,
mutable: false,
init: Some(obj_expr),
});

// for-in enumerates the receiver's own AND inherited enumerable string
// keys (deduplicated), and is a no-op — not a throw — on null/undefined.
// `ForInKeys` carries those semantics; `ObjectKeys` (Object.keys) would
// throw on nullish and miss inherited keys. Refs language/statements/for-in
// S12.6.4_A1/A2 (nullish) and A6/A6.1 (prototype chain).
let keys_expr = Expr::ForInKeys(Box::new(obj_expr));
let keys_expr = Expr::ForInKeys(Box::new(Expr::LocalGet(obj_id)));

// Create internal variables for the keys array and index
let keys_id = ctx.fresh_local();
Expand Down Expand Up @@ -1929,6 +1939,9 @@ pub(crate) fn lower_stmt_for_in(
loop_body.insert(i, stmt);
}

// Skip keys deleted from the receiver before they are visited.
let loop_body = guard_for_in_body(obj_id, keys_id, idx_id, loop_body);

// Create the for loop:
// for (let __i = 0; __i < __keys.length; __i++) { ... }
module.init.push(Stmt::For {
Expand Down
15 changes: 14 additions & 1 deletion crates/perry-hir/src/lower_decl/body_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1809,10 +1809,20 @@ pub fn lower_body_stmt(ctx: &mut LoweringContext, stmt: &ast::Stmt) -> Result<Ve
let head_binding =
crate::lower::predefine_for_head(ctx, &for_in_stmt.left, Type::String)?;

// Spill the receiver into a temp so each iteration can re-check
// that the current key still exists (for-in deletion semantics).
let obj_expr = lower_expr(ctx, &for_in_stmt.right)?;
let obj_id = ctx.fresh_local();
result.push(Stmt::Let {
id: obj_id,
name: format!("__forin_obj_{}", obj_id),
ty: Type::Any,
mutable: false,
init: Some(obj_expr),
});
// for-in: own + inherited enumerable keys, nullish-safe (no throw).
// See lower/stmt_loops.rs::lower_stmt_for_in for the rationale.
let keys_expr = Expr::ForInKeys(Box::new(obj_expr));
let keys_expr = Expr::ForInKeys(Box::new(Expr::LocalGet(obj_id)));
let keys_id = ctx.fresh_local();
let idx_id = ctx.fresh_local();

Expand All @@ -1837,6 +1847,9 @@ pub fn lower_body_stmt(ctx: &mut LoweringContext, stmt: &ast::Stmt) -> Result<Ve
loop_body.insert(i, stmt);
}

// Skip keys deleted from the receiver before they are visited.
let loop_body = crate::lower::guard_for_in_body(obj_id, keys_id, idx_id, loop_body);

// Create the for loop
result.push(Stmt::For {
init: Some(Box::new(Stmt::Let {
Expand Down
28 changes: 25 additions & 3 deletions crates/perry-runtime/src/object/field_get_set/has_property.rs
Original file line number Diff line number Diff line change
Expand Up @@ -675,9 +675,31 @@ unsafe fn ordinary_has_property(
}
cur = p as *const ObjectHeader;
}
// No explicit prototype recorded — the default `Object.prototype`
// applies (handled below), so stop the explicit walk here.
None => break,
// No explicit static `[[Prototype]]` recorded. But `Object.create(proto)`
// and `Function.prototype = obj` model the prototype link via a synthetic
// class_id → prototype object (`CLASS_PROTOTYPE_OBJECTS`), which the
// recorded-static-prototype walk above can't see. Without hopping it,
// `key in Object.create({ key: … })` — and even inherited
// `Object.prototype` members on such a receiver (its synthetic class_id
// makes the `Object.prototype` tail below bail) — were wrongly reported
// absent. Hop through that synthetic prototype object and continue; the
// field-GET path resolves the same chain via `resolve_proto_chain_field`.
None => {
// A prototype hop can land on a real `ArrayHeader` (`Foo.prototype
// = [1,2,3]`), whose layout has no `class_id` field — reading one
// would misinterpret the array's `length`/`capacity` as a class id
// and could spuriously hop. Arrays never model a synthetic
// prototype, so skip the lookup for them.
if !cur_is_array {
let synth_proto =
crate::object::class_prototype_object(unsafe { (*cur).class_id });
if !synth_proto.is_null() && synth_proto as *const ObjectHeader != cur {
cur = synth_proto as *const ObjectHeader;
continue;
}
}
break;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
}
// Wall 10 — a class instance's prototype METHODS / GETTERS / SETTERS live in
Expand Down
Loading