Summary
Object.getOwnPropertyDescriptor(obj, "x") returns undefined when the key is passed as a string literal. The same call works correctly when the key is passed via a variable binding. Discovered while doing runtime-parity work for anon-shape classes (Phase 3 of the Static Hermes object-layout plan) — this bug is pre-existing, not related to that work. It reproduces on plain object literals with the same code shape.
Repro
const p = { x: 1 };
// FAILS — returns undefined
console.log(Object.getOwnPropertyDescriptor(p, "x"));
// WORKS — returns { value: 1, writable: true, enumerable: true, configurable: true }
const k = "x";
console.log(Object.getOwnPropertyDescriptor(p, k));
Also works if Object.keys(p) is called first (any call that exercises the keys path seems to "prime" subsequent descriptor lookups).
Root cause (sketched)
Runtime-side instrumentation of js_object_get_own_property_descriptor showed:
- Literal-key call:
obj_value_bits = 0x7FFF_... (STRING_TAG), extract_obj_ptr returns null → function returns undefined.
- Variable-key call:
obj_value_bits = 0x7FFD_... (POINTER_TAG), extracts the real object pointer, returns the descriptor.
So the value bound as obj_value in the FFI call is being filled with the string pointer, not the object pointer. Either the compile-time dispatch is swapping arg positions, or the call-site codegen is computing the wrong NaN-box layout when the second arg is a literal string.
Expected path:
- HIR lowering at
crates/perry-hir/src/lower.rs:5403-5407 ("getOwnPropertyDescriptor" arm): correctly binds obj = args[0], key = args[1] into Expr::ObjectGetOwnPropertyDescriptor(obj, key).
- Codegen at
crates/perry-codegen/src/expr.rs:5107-5116: correctly calls js_object_get_own_property_descriptor(o, k) in that order.
Both sites read correct. The bug is upstream — likely in how args is assembled from call.args before the Object-static-method dispatch, or how the literal-string arg is lowered in the codegen string pool.
Why this is not blocking Phase 3
With Phase 3 (anon-class synthesis for object literals) active, the bug reproduces identically. With Phase 3 disabled, same behavior. The gap test regressions I initially attributed to Phase 3 (test_gap_array_methods, test_gap_error_extensions, test_gap_fetch_response, test_gap_object_methods, test_gap_proxy_reflect) are all pre-existing failures that trace back to this or related runtime-side bugs, not Phase 3 itself.
Impact
test_gap_object_methods fails on the getOwnPropertyDescriptor block (lines 82-100 of the gap test).
- Any user code doing
Object.getOwnPropertyDescriptor(o, "literal") silently gets undefined — no error, just wrong result.
- Probably affects other Object.* methods that take string-literal keys too; bears a wider audit.
Suggested investigation
- Add a temporary
eprintln! in js_object_get_own_property_descriptor to confirm arg bits across (a) literal key, (b) variable key, (c) computed key.
- Examine the LLVM IR emitted at the failing call site vs the working one to see where the arg tagging diverges.
- Walk the compile path from
Expr::Call to Expr::ObjectGetOwnPropertyDescriptor to see if the string literal is being handled differently (e.g. inlined as a raw pointer without NaN-box wrapping on one path).
Summary
Object.getOwnPropertyDescriptor(obj, "x")returnsundefinedwhen the key is passed as a string literal. The same call works correctly when the key is passed via a variable binding. Discovered while doing runtime-parity work for anon-shape classes (Phase 3 of the Static Hermes object-layout plan) — this bug is pre-existing, not related to that work. It reproduces on plain object literals with the same code shape.Repro
Also works if
Object.keys(p)is called first (any call that exercises the keys path seems to "prime" subsequent descriptor lookups).Root cause (sketched)
Runtime-side instrumentation of
js_object_get_own_property_descriptorshowed:obj_value_bits = 0x7FFF_...(STRING_TAG),extract_obj_ptrreturns null → function returnsundefined.obj_value_bits = 0x7FFD_...(POINTER_TAG), extracts the real object pointer, returns the descriptor.So the value bound as
obj_valuein the FFI call is being filled with the string pointer, not the object pointer. Either the compile-time dispatch is swapping arg positions, or the call-site codegen is computing the wrong NaN-box layout when the second arg is a literal string.Expected path:
crates/perry-hir/src/lower.rs:5403-5407("getOwnPropertyDescriptor"arm): correctly bindsobj = args[0],key = args[1]intoExpr::ObjectGetOwnPropertyDescriptor(obj, key).crates/perry-codegen/src/expr.rs:5107-5116: correctly callsjs_object_get_own_property_descriptor(o, k)in that order.Both sites read correct. The bug is upstream — likely in how
argsis assembled fromcall.argsbefore the Object-static-method dispatch, or how the literal-string arg is lowered in the codegen string pool.Why this is not blocking Phase 3
With Phase 3 (anon-class synthesis for object literals) active, the bug reproduces identically. With Phase 3 disabled, same behavior. The gap test regressions I initially attributed to Phase 3 (
test_gap_array_methods,test_gap_error_extensions,test_gap_fetch_response,test_gap_object_methods,test_gap_proxy_reflect) are all pre-existing failures that trace back to this or related runtime-side bugs, not Phase 3 itself.Impact
test_gap_object_methodsfails on thegetOwnPropertyDescriptorblock (lines 82-100 of the gap test).Object.getOwnPropertyDescriptor(o, "literal")silently getsundefined— no error, just wrong result.Suggested investigation
eprintln!injs_object_get_own_property_descriptorto confirm arg bits across (a) literal key, (b) variable key, (c) computed key.Expr::CalltoExpr::ObjectGetOwnPropertyDescriptorto see if the string literal is being handled differently (e.g. inlined as a raw pointer without NaN-box wrapping on one path).