diff --git a/crates/perry-codegen/src/expr/property_get/generic_dispatch.rs b/crates/perry-codegen/src/expr/property_get/generic_dispatch.rs index 90916b17d6..5498d76ecc 100644 --- a/crates/perry-codegen/src/expr/property_get/generic_dispatch.rs +++ b/crates/perry-codegen/src/expr/property_get/generic_dispatch.rs @@ -316,6 +316,25 @@ pub(crate) fn lower_generic_property_get( let object_type_ok = ctx.block().icmp_eq(I32, &object_type, "1"); let is_object = ctx.block().and(I1, &is_object, &object_type_ok); + // #6080: a receiver that has ever had a property/accessor descriptor + // installed (`Object.defineProperty`) needs descriptor-aware dispatch — + // an accessor must fire on reads, a non-writable slot must reject stores. + // The PIC hit path is a raw slot load: if the site was primed on a plain + // data property and `defineProperty` later converts that key to a getter + // (or a different descriptor), `keys_array` is unchanged, so the stale + // hit path would return the raw slot and bypass the getter entirely. + // OBJ_FLAG_HAS_DESCRIPTORS lives in the GcHeader `_reserved` i16 at + // offset -6; force a miss (→ `js_object_get_field_ic_miss`, which honors + // descriptors) whenever it is set. Mirrors the guard in + // `class_field_inline_guard.rs`. Cost: 1 sub + load i16 + and + cmp, + // folded into the existing `hit` cond_br. + let reserved_addr = ctx.block().sub(I64, &safe_obj_handle, "6"); + let reserved_ptr = ctx.block().inttoptr(I64, &reserved_addr); + let reserved = ctx.block().load(crate::types::I16, &reserved_ptr); + let has_desc = ctx.block().and(crate::types::I16, &reserved, "2048"); // OBJ_FLAG_HAS_DESCRIPTORS (0x800) + let no_desc = ctx.block().icmp_eq(crate::types::I16, &has_desc, "0"); + let is_object = ctx.block().and(I1, &is_object, &no_desc); + // Load obj->keys_array at offset 16 of ObjectHeader. let keys_addr = ctx.block().add(I64, &safe_obj_handle, "16"); let keys_ptr_p = ctx.block().inttoptr(I64, &keys_addr); diff --git a/test-files/test_gap_6080_defineproperty_after_prime.ts b/test-files/test_gap_6080_defineproperty_after_prime.ts new file mode 100644 index 0000000000..fe24ca79d9 --- /dev/null +++ b/test-files/test_gap_6080_defineproperty_after_prime.ts @@ -0,0 +1,21 @@ +// #6080: the read inline-cache must not bypass a descriptor installed AFTER +// the call-site was primed. Priming f(a) caches a raw slot for `x`; a later +// Object.defineProperty converting `x` to a getter (or redefining the data +// value) must be honored on the next read, not served stale from the cache. +function f(o: any) { return o.x; } +const a: any = { x: 1 }; +console.log(f(a)); // prime -> 1 +Object.defineProperty(a, "x", { get() { return 42; } }); +console.log(f(a)); // must observe the getter -> 42 + +function g(o: any) { return o.y; } +const b: any = { y: 10 }; +g(b); // prime +Object.defineProperty(b, "y", { value: 99, writable: false }); +console.log(g(b)); // must observe redefined value -> 99 + +// A receiver that gets a descriptor must still read its OTHER, untouched +// own keys correctly (slow path), and normal objects keep hitting the cache. +const d: any = { p: 1, q: 2 }; +Object.defineProperty(d, "p", { get() { return 100; } }); +console.log(d.q, d.p); // 2 100