Found while writing the #6941 regression suite (an assertion had to be dropped because of it). Nothing to do with GC — a plain spec gap, reproducible with zero allocation pressure.
js_object_has_property (crates/perry-runtime/src/object/field_get_set/has_property.rs) only runs ToPropertyKey when the key is a number:
let key = {
let kv = JSValue::from_bits(key.to_bits());
if kv.is_number() {
unsafe { crate::object::js_to_property_key(key) }
} else {
key
}
};
That number arm was added for Next.js's Number(digest.at(-2)) in RedirectStatusCode. But per spec RelationalExpression in ShiftExpression is ToPropertyKey(lval) for every key type, so an object key must have its Symbol.toPrimitive / toString / valueOf invoked. Today it is compared as a raw pointer and never matches.
Repro:
const obj: any = { here: 1 };
const k: any = { toString(): string { return "here"; } };
console.log("in with object key:", (k as any) in obj);
console.log("in with string key:", "here" in obj);
Node 26.5.0 (the pinned oracle):
in with object key: true
in with string key: true
Perry (main @ 0bb03e8):
in with object key: false
in with string key: true
Two things to get right in the fix:
Found while writing the #6941 regression suite (an assertion had to be dropped because of it). Nothing to do with GC — a plain spec gap, reproducible with zero allocation pressure.
js_object_has_property(crates/perry-runtime/src/object/field_get_set/has_property.rs) only runsToPropertyKeywhen the key is a number:That number arm was added for Next.js's
Number(digest.at(-2)) in RedirectStatusCode. But per specRelationalExpression in ShiftExpressionisToPropertyKey(lval)for every key type, so an object key must have itsSymbol.toPrimitive/toString/valueOfinvoked. Today it is compared as a raw pointer and never matches.Repro:
Node 26.5.0 (the pinned oracle):
Perry (
main@ 0bb03e8):Two things to get right in the fix:
js_to_property_keyis GC-capable, so the receiver has to be rooted across it — the number arm was fixed that way in fix(runtime): root receivers and stored values across GC-capable property-key coercions (#6935) #6941; widening the arm must keep that rooting. See runtime: audit ToPropertyKey receiver/value rooting — raw receiver and stored value held across GC-capable key coercions #6935 for the bug class.