Skip to content

fix(codegen,runtime): bind this for static-method calls on class-object values (#1758) - #1809

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

fix(codegen,runtime): bind this for static-method calls on class-object values (#1758)#1809
proggeramlug merged 1 commit into
mainfrom
worktree-schema-1758-b

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Fixes this-binding for a static method call on a class-object value
reached through the dynamic dispatch path — the #1787 "broken dynamic-dispatch
tower" that the class-object epic (#1785) previously avoided but never fixed.

When the receiver of a static-method call is an un-inlined factory-call
result
(so its class isn't statically known), the call fell into the
instance-method dispatch tower, which passed the receiver as arg0 and never
set IMPLICIT_THIS. A static method reading this.<staticField> then saw
undefined.

This is the next effect/Schema init blocker after #1804:

class BigInt$ extends transformOrFail(...).annotations(...) {}

The .annotations() receiver is an un-inlined factory call, so
static annotations(x) { return make(mergeSchemaAnnotations(this.ast, x)) }
read this.ast === undefined and threw
Cannot read properties of undefined (reading '_tag') during Schema.ts init.

Localization

PERRY_DEBUG_SYMBOLS=1 + lldb (b js_throw_type_error_property_access) + an
effect-source probe pinned the failing field to nanos (BigInt$), then
--trace llvm showed the inline dispatch's idispatch.case0 emitting
perry_static_…__withThis(recv, arg) — receiver as arg0, no IMPLICIT_THIS.

Fix (two complementary dispatch paths)

Both delegate to the existing js_class_static_method_call, which binds this,
walks the class_id parent chain, and applies static arity/rest semantics:

  • codegen (lower_call/property_get.rs): in the instance-method dispatch
    tower, a perry_static_* implementor routes to js_class_static_method_call
    instead of the instance-style fname(recv, args…) direct call. Raw user args
    are captured before the issue-Async miscompile: an extra synchronous statement before await MongoClient.connect() drops the next await silently #235 padding / rest-bundling mangling.
  • runtime (object/native_call_method.rs): the dynamic dispatcher detects a
    class-object receiver (is_class_object_value) and, when the method resolves
    in the static chain, delegates; otherwise falls through to the generic
    own-field path. lookup_static_method_in_chain is now pub(crate).

Validation

  • New gap test test_gap_class_expr_extends_static_call_this.ts — byte-for-byte
    with node across: statically-known receiver (tower), any-typed receiver
    (runtime dynamic path), a no-this static method, and the const-bound
    (already-working) path.
  • Class-expr epic gap suite + Object/class dispatch tests: 0 regressions.
  • Broad 50-test class/method/inheritance sweep: the 8 failures
    (decorators / static-block / mixins / node-error-format / primitive-typeerror)
    fail identically on origin/main — confirmed pre-existing, not regressions.

Remaining (continuing in a follow-up)

effect/Schema init still has further class-object blockers past this, in the
symbol subsystem (which breaks effect's isSchemadual
transformOrFail):

  • symbol in classObject (the in operator / Predicate.hasProperty) returns
    false even for an own static symbol property.
  • inherited static symbol reads (Sub[TypeId] where Sub extends make(...))
    return undefined.

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

…ject values (#1758)

A STATIC method call on a class-object VALUE reached through the *dynamic*
dispatch path passed the receiver as arg0 and never set IMPLICIT_THIS, so the
method's `this.<staticField>` read `undefined`. This is the #1787 "broken
dynamic-dispatch tower" that the epic previously *avoided* (by routing
ClassExprFresh/const-bound receivers to the compile-time static path) but never
fixed for un-inlined receivers.

It surfaces in effect's
  `class BigInt$ extends transformOrFail(...).annotations(...) {}`
(epic #1785 / #1758): the `.annotations()` receiver is an un-inlined factory
call, so its class isn't statically known. The static method
`annotations(x) { return make(mergeSchemaAnnotations(this.ast, x)) }` then read
`this.ast === undefined` and threw
`Cannot read properties of undefined (reading '_tag')` during Schema.ts init.

Two complementary dispatch paths, both now bind `this` + walk the class_id
parent chain via the existing `js_class_static_method_call`:

- **codegen** (`lower_call/property_get.rs`): in the instance-method dispatch
  tower, a `perry_static_*` implementor is routed to
  `js_class_static_method_call` (set IMPLICIT_THIS, no recv-prepend, static
  arity/rest) instead of the instance-style `fname(recv, args…)` direct call.
  Raw user args are captured before the issue-#235 padding / rest-bundling.
- **runtime** (`object/native_call_method.rs`): the dynamic dispatcher detects
  a class-object receiver (`is_class_object_value`) and, when the method
  resolves in the static chain, delegates to `js_class_static_method_call`;
  otherwise falls through to the generic own-field path. `lookup_static_method_in_chain`
  is now `pub(crate)` for the probe.

Verified byte-for-byte with node across the tower (statically-known receiver),
the dynamic (any-typed receiver), the no-`this` static method, and the
const-bound (already-working) paths. Class-expr epic gap suite + Object/class
dispatch tests: 0 regressions (the 8 pre-existing failures —
decorators/static-block/mixins/node-error-format — fail identically on
origin/main).

NOTE: `effect/Schema` init has further blockers past this — `symbol in
classObject` (the `in` operator) and inherited static *symbol* reads on
class-objects both misbehave, breaking effect's `isSchema` / `dual`. Continuing
in a follow-up.

Refs #1758, #1785, #1772, #1791.
proggeramlug added a commit that referenced this pull request May 25, 2026
…1758) (#1810)

Two bugs in symbol-keyed property access on class values, both breaking
effect's `isSchema` (and therefore `dual` / `transformOrFail`):

- `Sym in classObject` (the `in` operator / `Predicate.hasProperty`) returned
  false even for an OWN static `[Sym]` property: `js_object_has_property`'s
  generic path rejects non-string keys outright, and its INT32 class-ref path
  only checked own `CLASS_STATIC_SYMBOLS`.
- `Sub[Sym]` where `class Sub extends make(...)` returned undefined: inherited
  static symbols weren't walked through the class-expression prototype chain
  (`js_object_get_symbol_property` only checked own props / own static symbols).

effect's `isSchema(u) = hasProperty(u, TypeId) && isObject(u[TypeId])` with
`BigIntFromSelf = class extends make(bigIntKeyword) {}` (carries `[TypeId]`
only via inheritance) returned false, so `transformOrFail`'s `dual` predicate
(`isSchema(args[0]) && isSchema(args[1])`) was false and `transformOrFail`
degraded to a curried function — `makeTransformationClass` never ran, leaving a
downstream schema's `.ast` undefined (`Cannot read properties of undefined`).

Fix:
- new `resolve_proto_chain_symbol` (class_registry.rs): symbol-keyed analogue of
  `resolve_proto_chain_field` — walks `CLASS_PROTOTYPE_OBJECTS` + the class_id
  parent chain, checking each prototype object's OWN symbol props via a new
  non-recursive `own_symbol_property` helper (symbol.rs).
- `js_object_get_symbol_property`: on an own miss, walk the chain — for both
  INT32 class refs and POINTER class-objects.
- `js_object_has_property`: delegate symbol keys to `js_object_get_symbol_property`
  (own + inherited, class-ref + class-object), mirroring the existing string-key
  "present-and-not-undefined" semantics.

New gap test `test_gap_class_object_static_symbol.ts` — byte-for-byte with node
across own / inherited / absent / `isSchema`-shape cases. Class/Object/symbol
regression sweep: 0 regressions (the pre-existing failures fail identically on
origin/main).

NOTE: with this + #1809 (extends-static-method `this`-binding), `effect/Schema`
init gets PAST the `_tag`-of-undefined TypeError and now reaches a distinct
SIGSEGV deeper in effect's fiber runtime (`js_object_has_property` →
`js_array_length` on a malformed `keys_array`). Reported separately.

Refs #1758, #1785, #1772, #1791.
@proggeramlug
proggeramlug merged commit bcc1dec into main May 25, 2026
10 checks passed
@proggeramlug
proggeramlug deleted the worktree-schema-1758-b branch May 25, 2026 17:23
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