fix(runtime,lower): in-operator walks Object.create protos; for-in skips deleted keys#6147
Conversation
…eted keys Two related prototype-chain correctness fixes. 1. `in` operator (#6145): `key in Object.create(proto)` wrongly returned false for inherited properties — the custom proto's own keys and Object.prototype members alike. Object.create models [[Prototype]] via a synthetic class_id → prototype object (CLASS_PROTOTYPE_OBJECTS), but `ordinary_has_property` only walked recorded static prototypes (object_static_prototype), which Object.create never populates, and the Object.prototype fallback bails on class_id != 0. Fix: when no static prototype is recorded, hop through the synthetic prototype object (class_prototype_object) and continue the walk — the same chain the field-GET path resolves via resolve_proto_chain_field. 2. for-in (#6146): a key deleted from the receiver before it is visited was still visited, because for-in snapshots the key list up front (ForInKeys). Fix: spill the receiver to a temp and wrap the loop body in `if (typeof obj === "string" ? true : (key in obj)) { ... }`, skipping keys no longer present. Primitive strings (the only primitive with a non-empty for-in snapshot) bypass the recheck since `in` throws on primitives — which is why fix (1) is a prerequisite for Object.create receivers. Applied at both desugar sites via shared helper guard_for_in_body.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds a ChangesFor-in Deletion Guard and Prototype Chain Fix
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
crates/perry-hir/src/lower_decl/body_stmt.rs (1)
1805-1877: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftConsider extracting the shared for-in desugaring to eliminate the duplicate with
lower_stmt_for_in.This arm is a near-verbatim copy of
lower/stmt_loops.rs::lower_stmt_for_in(receiver spill →ForInKeys→ keys/idx temps → body lowering → binding prepend →guard_for_in_body→Stmt::For); the only real difference is emitting intoresultvsmodule.init. This PR had to touch both copies in lockstep, which is exactly the failure mode that lets one path stay buggy after the other is fixed. A shared helper parameterized over the statement sink would keep the two for-in semantics from drifting.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/perry-hir/src/lower_decl/body_stmt.rs` around lines 1805 - 1877, The for-in lowering logic is duplicated between this branch and lower_stmt_for_in, so changes can drift and leave one path buggy. Extract the shared desugaring sequence into a helper that encapsulates the receiver spill, ForInKeys creation, key/index temps, body lowering, binding prepending, and guard_for_in_body wrapping, and parameterize it by the statement sink so it can emit into either result or module.init. Then update this ast::Stmt::ForIn arm and lower/stmt_loops.rs::lower_stmt_for_in to call the shared helper while keeping the existing semantics intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/perry-runtime/src/object/field_get_set/has_property.rs`:
- Around line 687-693: The synthetic prototype fallback in has_property
currently runs for arrays too, which can read class_id from an ArrayHeader and
incorrectly continue through class_prototype_object. Update the None branch in
has_property to skip the synthetic prototype lookup whenever cur_is_array is
true, so array property checks only follow the intended prototype chain and do
not spuriously change in results.
---
Nitpick comments:
In `@crates/perry-hir/src/lower_decl/body_stmt.rs`:
- Around line 1805-1877: The for-in lowering logic is duplicated between this
branch and lower_stmt_for_in, so changes can drift and leave one path buggy.
Extract the shared desugaring sequence into a helper that encapsulates the
receiver spill, ForInKeys creation, key/index temps, body lowering, binding
prepending, and guard_for_in_body wrapping, and parameterize it by the statement
sink so it can emit into either result or module.init. Then update this
ast::Stmt::ForIn arm and lower/stmt_loops.rs::lower_stmt_for_in to call the
shared helper while keeping the existing semantics intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 5a7d305f-223d-4276-b87d-81e8027028d0
📒 Files selected for processing (5)
crates/perry-hir/src/lower/for_head.rscrates/perry-hir/src/lower/mod.rscrates/perry-hir/src/lower/stmt_loops.rscrates/perry-hir/src/lower_decl/body_stmt.rscrates/perry-runtime/src/object/field_get_set/has_property.rs
…rototype nodes Address CodeRabbit review on #6147: a prototype hop in `ordinary_has_property` can land on a real `ArrayHeader` (`Foo.prototype = [1,2,3]`), which has no `class_id` field. Skip the synthetic-prototype lookup when `cur_is_array` so the array's `length`/`capacity` bytes are never misread as a class id.
Fixes #6145, #6146.
Summary
Two related prototype-chain correctness fixes.
1.
inoperator walks theObject.createprototype chain (runtime)key in Object.create(proto)wrongly returnedfalsefor every inheritedproperty — the custom proto's own keys and
Object.prototypemembers:Object.create(proto)models[[Prototype]]via a synthetic class_id →prototype object (
CLASS_PROTOTYPE_OBJECTS), the same mechanism the field-GETpath already walks (
resolve_proto_chain_field). But theinoperator's walk(
ordinary_has_property) only followed recorded static prototypes(
object_static_prototype), whichObject.createdoes not populate — so itstopped at the receiver's own keys. Worse, the
Object.prototypefallback(
ordinary_object_prototype_property_value) bails whenclass_id != 0, and anObject.createresult carries the synthetic class_id, so even"toString" in owas
false.Fix: when the static-prototype walk finds no recorded
[[Prototype]], hopthrough the synthetic prototype object (
class_prototype_object(class_id)) andcontinue — reaching the custom proto's keys and, past it,
Object.prototype.class/setPrototypeOf/__proto__were already correct and are unchanged.2. for-in skips keys deleted before they are visited (HIR)
Per
EnumerateObjectProperties, a property deleted during enumeration but notyet visited must not be visited. Perry snapshots the key list up front
(
ForInKeys), so a mid-iterationdeletestill visited the stale key:Fix: the for-in desugar now spills the receiver to a temp and wraps the loop
body in
if (typeof obj === "string" ? true : (key in obj)) { … }, skipping akey no longer present. A primitive string (the only primitive whose for-in
snapshot is non-empty) can't have keys deleted and the
inoperator throws onprimitives, so strings bypass the recheck. This is why fix (1) is a
prerequisite — the recheck relies on a correct
inforObject.createreceivers. Applied at both desugar sites (
lower/stmt_loops.rs,lower_decl/body_stmt.rs); shared helperguard_for_in_bodyinfor_head.rs.Validation
Byte-for-byte vs
node --experimental-strip-typesacross:inonown/custom-proto/
Object.prototype/absent/2-level-Object.create/create(null);inregression on class/setPrototypeOf/__proto__/plain; for-indelete-before-visit / delete-current / delete-past / delete-inherited,
for-in over string & number primitives, for-in over
Object.create(inheritedenumerated), and the same deletion inside a function body.
Out of scope (pre-existing, unchanged by this PR)
While testing I confirmed two separate pre-existing
ingaps that this PRdoes not touch (both already
falseon origin/main):<idx> in <small typed array>and static data props via<key> in <ClassRef>(e.g."s" in K).These use early-return paths in
js_object_has_property, not the genericprototype walk fixed here; filed separately.
Summary by CodeRabbit
for...initeration to better match JavaScript behavior when the receiver changes during looping.for...initeration.