Skip to content

Object.getOwnPropertyDescriptor(obj, "key") returns undefined when key is a string literal #150

@proggeramlug

Description

@proggeramlug

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

  1. 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.
  2. Examine the LLVM IR emitted at the failing call site vs the working one to see where the arg tagging diverges.
  3. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions