feat(runtime,codegen): Object.getOwnPropertyDescriptors (#1791/#1758) - #1804
Merged
Conversation
The plural `Object.getOwnPropertyDescriptors(obj)` had no dedicated HIR variant. The singular `getOwnPropertyDescriptor(obj, key)` was wired end to end, but the plural fell through both the literal-callee recogniser (`lower/expr_call/native_module.rs`) and the esbuild-alias recogniser (`destructuring/var_decl.rs`), so codegen lowered the callee to a constant null and the call threw `TypeError: value is not a function`. This is the first blocker on `import * as S from "effect/Schema"` (epic #1785 / #1772): effect's `SchemaAST.annotations` clones an AST node via `Object.create(Object.getPrototypeOf(ast), Object.getOwnPropertyDescriptors(ast))` during Schema.ts module init, so the import threw before any user code ran. Localized via PERRY_DEBUG_SYMBOLS=1 + lldb (break js_throw_type_error_not_a_function) + --trace llvm: the callee lowered to `bitcast double 0.0` in `AST.annotations`. Changes (mirror the singular descriptor path): - runtime `js_object_get_own_property_descriptors` (object_ops.rs): enumerate own keys via getOwnPropertyNames, build a fresh object mapping each key to its descriptor. Non-rooted builder allocations follow the existing js_object_entries / getOwnPropertyNames convention. - new HIR `Expr::ObjectGetOwnPropertyDescriptors(Box<Expr>)`; lowered in both the direct-call and Object-alias paths; whitelisted in the alias gate. - codegen arm (misc_methods.rs) + extern decl (runtime_decls/strings.rs) + dispatch list, walkers (ref/mut), stable_hash tag, and JS emitter. Byte-for-byte with node across plain/empty/alias/accessor/clone-via-create shapes. effect/Effect deep import stays green (result: 42). Class-expr epic gap suite + Object-method tests: 0 regressions (#685 value-read remains the documented pre-existing known-failure). NOTE: a distinct second blocker remains for full Schema init — a `Cannot read properties of undefined (reading '_tag')` deeper in Schema.ts (SchemaAST closure __239, reached via Schema __227/__211). Reported separately rather than chained here. Refs #1791, #1758, #1785, #1772.
This was referenced May 25, 2026
proggeramlug
added a commit
that referenced
this pull request
May 25, 2026
) (#1811) Functions are objects in JS, so `key in fn` is valid. `js_object_has_property` treated a closure pointer as an `ObjectHeader` and read `(*obj_ptr).keys_array` at the closure's capture-slot offset — a NaN-boxed value, not a real `*ArrayHeader` — then crashed in `js_array_length` → `is_registered_set` dereferencing the tagged value. This is the last blocker on `import * as S from "effect/Schema"` MODULE INIT (after #1804/#1809/#1810): effect's `dual`-wrapped helpers do `<key> in someClosure` deep in the fiber runtime. Localized via PERRY_DEBUG_SYMBOLS=1 + lldb `-k` crash hooks + a conditional breakpoint on `js_array_length` with a tagged arg: the receiver had GC type 0x04 (GC_TYPE_CLOSURE) and its first field was a code pointer (a fiberRuntime closure body). Fix: `js_object_has_property` detects a `GC_TYPE_CLOSURE` receiver (sibling to the existing GC_TYPE_ARRAY fast path) and mirrors the closure read path (`js_object_get_field_by_name`: `length` → arity, others → CLOSURE_DYNAMIC_PROPS) — present-and-not-undefined ⇒ true. Matches node for absent keys (false, no crash), `length`, and dynamically-assigned props. With this, `import * as S from "effect/Schema"` initializes byte-identical to node (`ok object`). `effect/Effect` deep import stays green (42). New gap test `test_gap_in_operator_closure.ts`. (`'name'`/`'prototype' in fn` remain a separate pre-existing closure-property gap — `fn.name` also reads undefined — intentionally not asserted.) NOTE: actually USING Schema (`S.decodeUnknownSync(S.Number)(42)`) and the `effect` barrel hit further distinct blockers (ParseResult `_tag`-undefined; `value is not a function`). Reported separately. Refs #1758, #1785, #1772, #1791.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements
Object.getOwnPropertyDescriptors(obj)(the plural form) — thefirst blocker stopping
import * as S from "effect/Schema"from initializing(epic #1785 / #1772, capstone #1791 / #1758).
The singular
Object.getOwnPropertyDescriptor(obj, key)was already wired endto end, but the plural had no dedicated HIR variant. It fell through both
the literal-callee recogniser (
lower/expr_call/native_module.rs) and theesbuild-alias recogniser (
destructuring/var_decl.rs), so codegen lowered thecallee to a constant
0.0(null) and the call threwTypeError: value is not a function.effect's
SchemaAST.annotationsclones an AST node during Schema.ts moduleinit via:
so the import threw before any user code ran.
Localization
PERRY_DEBUG_SYMBOLS=1+lldb(breakjs_throw_type_error_not_a_function) +--trace llvmpinned the throw toAST.annotations(SchemaAST closure__182), where the callee lowered to%r5 = bitcast double 0.0 to i64→js_closure_call1(null, ast). Minimal repro:Object.getOwnPropertyDescriptors({a:1}).Changes (mirror the singular-descriptor path)
js_object_get_own_property_descriptors(object_ops.rs):enumerate own keys via
getOwnPropertyNames, build a fresh object mappingeach key to its descriptor (
js_object_get_own_property_descriptor).Non-rooted builder allocations follow the existing
js_object_entries/getOwnPropertyNamesconvention.Expr::ObjectGetOwnPropertyDescriptors(Box<Expr>); lowered in boththe direct-call and
Object-alias paths; whitelisted in the alias gate.misc_methods.rs) + extern decl (runtime_decls/strings.rs) +the codegen dispatch list, the ref/mut walkers, a
stable_hashtag, and theJS emitter (
perry-codegen-js).Validation
node --experimental-strip-typesacrossplain / empty / alias / accessor-descriptor / clone-via-
Object.createshapes(new gap test
test_gap_object_get_own_property_descriptors.ts).effect/Effectdeep import stays green (result: 42).inherited-static-method / inherited-rest-static / new-instanceof /
static-iife / static-symbol): all pass.
Object.definePropertywith TypeError: value is not a function #886, accessorObject.definePropertyaccessor descriptors: getterthisbinding broken (returns NaN) #450, etc.): all pass.test_issue_685remains the documented pre-existingclass-object static-field value-read known-failure (
params type: stringvsobject), unrelated to this change.Remaining (reported, not chained here)
Full
effect/Schemainit hits a distinct second blocker once this lands:TypeError: Cannot read properties of undefined (reading '_tag')deeper inSchema.ts init.
Backtrace:
The simple class-object static-field value-read this looked like (the
deferred #1787 / #685 sub-item) reproduces correctly in isolation now
(
make(x).ast._tagmatches node), so the_tag-undefined is a more specificdata-flow issue in Schema's struct construction — to be localized in a
follow-up.
Refs #1791, #1758, #1785, #1772.