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
15 changes: 14 additions & 1 deletion crates/perry-runtime/src/object/field_set_by_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,14 @@ pub extern "C" fn js_object_set_field_by_name(
{
let o = raw as *mut ObjectHeader;
let class_id = (*o).class_id;
if class_id != 0
// #6595: a per-evaluation CLASS OBJECT must never take
// this lane — it skips the #6530
// `mirror_class_object_static_write` hook every in-body
// completion runs, and (template cid, key) plans recorded
// by instances sharing the cid would falsely certify it.
// See the matching gates at the plan record sites.
if (*o).object_type == crate::error::OBJECT_TYPE_REGULAR
&& class_id != 0
&& class_id != NATIVE_MODULE_CLASS_ID
&& super::prop_plan::store_plan_check(class_id, key as usize)
{
Expand Down Expand Up @@ -1035,9 +1042,14 @@ pub extern "C" fn js_object_set_field_by_name(
| crate::gc::OBJ_FLAG_NULL_PROTO
| crate::gc::OBJ_FLAG_HAS_DESCRIPTORS;
let obj_class_id = (*obj).class_id;
// #6595: class objects (`OBJECT_TYPE_CLASS`) are excluded — their
// writes must always reach the `mirror_class_object_static_write`
// completions, and their cid is shared with their instances so a
// plan keyed on it conflates two different prototype chains.
let plan_eligible = !key.is_null()
&& obj_class_id != 0
&& obj_class_id != NATIVE_MODULE_CLASS_ID
&& (*obj).object_type == crate::error::OBJECT_TYPE_REGULAR
&& (*gc_header)._reserved & PLAN_BLOCKING_FLAGS == 0;
let plan_fast = plan_eligible
&& super::prop_plan::store_plan_check(obj_class_id, interned_key as usize);
Expand Down Expand Up @@ -1203,6 +1215,7 @@ pub extern "C" fn js_object_set_field_by_name(
if !plan_fast
&& obj_class_id != 0
&& obj_class_id != NATIVE_MODULE_CLASS_ID
&& (*obj).object_type == crate::error::OBJECT_TYPE_REGULAR
&& obj_flags & PLAN_BLOCKING_FLAGS == 0
{
super::prop_plan::store_plan_record(obj_class_id, interned_key as usize);
Expand Down
15 changes: 15 additions & 0 deletions crates/perry-runtime/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1354,8 +1354,23 @@ fn ordinary_set_with_receiver(target: f64, key: f64, value: f64, receiver: f64)
let key_ptr = crate::builtins::js_string_coerce(key)
as *const crate::StringHeader;
let interned = crate::object::interned_key_ptr(key_ptr);
// #6595: a per-evaluation CLASS OBJECT (what a
// capture-carrying class materializes as,
// `object_type == OBJECT_TYPE_CLASS`) shares its
// template cid with its instances, and its own-data
// writes must reach the #6530
// `mirror_class_object_static_write` hook in
// `js_object_set_field_by_name`. Recording a plan
// here armed the mirror-free fast lane for the very
// store being vetted, so from the second same-shaped
// class on (shape-transition cache hit) post-class
// statics like bundled zod's `ZodX.create` vanished
// from ClassRef static dispatch. Class objects
// neither record nor honor store plans.
let plan_eligible = header._reserved & CHAIN_DIVERGE == 0
&& class_id != crate::object::NATIVE_MODULE_CLASS_ID
&& (*(addr as *const crate::ObjectHeader)).object_type
== crate::error::OBJECT_TYPE_REGULAR
&& interned != 0;
if plan_eligible
&& crate::object::prop_plan::store_plan_check(class_id, interned)
Expand Down
103 changes: 103 additions & 0 deletions test-files/test_gap_6595_repeated_capture_class_statics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// #6595 (regression of #6530 introduced by #6541): post-class arrow statics
// on REPEATED same-shaped capture-carrying classes. The receiver-based
// `[[Set]]` records a store plan for (template_cid, key) BEFORE the store
// reaches `js_object_set_field_by_name`, so from the SECOND same-shaped class
// object on (identical key-append sequence → shape-transition cache hit) the
// static write took the raw fast lane and skipped the #6530
// `CLASS_DYNAMIC_PROPS` mirror — sibling-method dispatch through the INT32
// ClassRef then missed and returned the class ref itself.
//
// Every class here must be CAPTURE-CARRYING (methods referencing outer
// locals) or it stays an INT32 ClassRef and the statics never touch the
// heap-class-object store path. One class alone can't catch the bug either:
// the first class of a shape misses the transition cache and falls to the
// mirroring path. Bundled zod has ~32 same-shaped classes; two suffice.

function makeModule() {
const tag = (s: string) => "[" + s + "]";
const isUndef = (x: any) => typeof x === "undefined";
const isNull = (x: any) => x === null;

class Base {
_def: any;
constructor(def: any) {
this._def = def;
this.parse = this.parse.bind(this);
this.optional = this.optional.bind(this);
this.nullable = this.nullable.bind(this);
}
parse(x: any) {
return this._parse({ data: x, errorMap: this._def.errorMap });
}
_parse(ctx: any): any {
return "base:" + ctx.data;
}
optional() {
return (SubOpt as any).create(this, this._def);
}
nullable() {
return (SubNull as any).create(this, this._def);
}
label() {
return tag(this._def.name);
}
}

class SubOpt extends Base {
_parse(ctx: any) {
if (isUndef(ctx.data)) return undefined;
return "opt:" + ctx.errorMap + ":" + ctx.data;
}
unwrap() {
return this._def.innerType;
}
}

class SubNull extends Base {
_parse(ctx: any) {
if (isNull(ctx.data)) return null;
return "null:" + ctx.errorMap + ":" + ctx.data;
}
unwrap() {
return this._def.innerType;
}
}

// Identical post-class static write sequence on same-shaped class objects:
// the first write learns the shape-transition edge, the second must still
// mirror into the ClassRef-world static table.
(SubOpt as any).create = (inner: any, def: any) =>
new SubOpt({ innerType: inner, errorMap: "eo", name: "opt" });
(SubNull as any).create = (inner: any, def: any) =>
new SubNull({ innerType: inner, errorMap: "en", name: "null" });

return { Base, SubOpt, SubNull };
}

const m = makeModule();
const b = new m.Base({ errorMap: "bm", name: "base" });

// First same-shaped class: static via sibling-method ClassRef dispatch.
const o = b.optional();
console.log("opt typeof:", typeof o);
console.log("opt instanceof SubOpt:", o instanceof m.SubOpt);
console.log("opt _def set:", o._def !== undefined);
console.log("opt parse undef:", o.parse(undefined));
console.log("opt parse val:", o.parse(7));
console.log("opt label:", o.label());

// SECOND same-shaped class — the transition-cache-hit static write.
const n = b.nullable();
console.log("null typeof:", typeof n);
console.log("null instanceof SubNull:", n instanceof m.SubNull);
console.log("null _def set:", n._def !== undefined);
console.log("null parse null:", n.parse(null));
console.log("null parse val:", n.parse(3));
console.log("null label:", n.label());
console.log("null unwrap is base:", n.unwrap() === b);

// Chained: optional-of-nullable exercises both static dispatches in sequence.
const chained = b.nullable().optional();
console.log("chain typeof:", typeof chained);
console.log("chain parse undef:", chained.parse(undefined));
console.log("chain parse val:", chained.parse(5));
Loading