Skip to content

feat(runtime,codegen): Object.getOwnPropertyDescriptors (#1791/#1758) - #1804

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-schema-1791
May 25, 2026
Merged

feat(runtime,codegen): Object.getOwnPropertyDescriptors (#1791/#1758)#1804
proggeramlug merged 1 commit into
mainfrom
worktree-schema-1791

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Implements Object.getOwnPropertyDescriptors(obj) (the plural form) — the
first 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 end
to end, but the plural had no dedicated HIR variant. It 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 0.0 (null) and the call threw
TypeError: value is not a function.

effect's SchemaAST.annotations clones an AST node during Schema.ts module
init
via:

Object.create(Object.getPrototypeOf(ast), Object.getOwnPropertyDescriptors(ast))

so the import threw before any user code ran.

Localization

PERRY_DEBUG_SYMBOLS=1 + lldb (break js_throw_type_error_not_a_function) +
--trace llvm pinned the throw to AST.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)

  • 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 (js_object_get_own_property_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) +
    the codegen dispatch list, the ref/mut walkers, a stable_hash tag, and the
    JS emitter (perry-codegen-js).

Validation

  • Byte-for-byte with node --experimental-strip-types across
    plain / empty / alias / accessor-descriptor / clone-via-Object.create shapes
    (new gap test test_gap_object_get_own_property_descriptors.ts).
  • effect/Effect deep import stays green (result: 42).
  • Class-expr epic gap suite (identity / static-this / extends-static /
    inherited-static-method / inherited-rest-static / new-instanceof /
    static-iife / static-symbol): all pass.
  • Object-method tests (define-property, getPrototypeOf, indirect-define-property
    Object.defineProperty with TypeError: value is not a function #886, accessor Object.defineProperty accessor descriptors: getter this binding broken (returns NaN) #450, etc.): all pass.
  • 0 regressions. test_issue_685 remains the documented pre-existing
    class-object static-field value-read known-failure (params type: string vs
    object), unrelated to this change.

Remaining (reported, not chained here)

Full effect/Schema init hits a distinct second blocker once this lands:
TypeError: Cannot read properties of undefined (reading '_tag') deeper in
Schema.ts init.

Backtrace:

js_throw_type_error_property_access
  perry_closure_..._SchemaAST_ts__239 (+39352, a ~5.8k-line recursive AST dispatcher)
  perry_closure_..._Schema_ts__211
  perry_closure_..._Schema_ts__227
  node_modules_..._Schema_ts__init_body

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._tag matches node), so the _tag-undefined is a more specific
data-flow issue in Schema's struct construction — to be localized in a
follow-up.

Refs #1791, #1758, #1785, #1772.

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.
@proggeramlug
proggeramlug merged commit d5439f5 into main May 25, 2026
10 checks passed
@proggeramlug
proggeramlug deleted the worktree-schema-1791 branch May 25, 2026 15:56
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant