fix(codegen,runtime): bind this for static-method calls on class-object values (#1758) - #1809
Merged
Conversation
…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.
This was referenced May 25, 2026
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
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 was referenced May 26, 2026
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
Fixes
this-binding for a static method call on a class-object valuereached 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
arg0and neverset
IMPLICIT_THIS. A static method readingthis.<staticField>then sawundefined.This is the next
effect/Schemainit blocker after #1804:The
.annotations()receiver is an un-inlined factory call, sostatic annotations(x) { return make(mergeSchemaAnnotations(this.ast, x)) }read
this.ast === undefinedand threwCannot read properties of undefined (reading '_tag')during Schema.ts init.Localization
PERRY_DEBUG_SYMBOLS=1+ lldb (b js_throw_type_error_property_access) + aneffect-source probe pinned the failing field to
nanos(BigInt$), then--trace llvmshowed the inline dispatch'sidispatch.case0emittingperry_static_…__withThis(recv, arg)— receiver as arg0, noIMPLICIT_THIS.Fix (two complementary dispatch paths)
Both delegate to the existing
js_class_static_method_call, which bindsthis,walks the class_id parent chain, and applies static arity/rest semantics:
lower_call/property_get.rs): in the instance-method dispatchtower, a
perry_static_*implementor routes tojs_class_static_method_callinstead of the instance-style
fname(recv, args…)direct call. Raw user argsare captured before the issue-Async miscompile: an extra synchronous statement before await MongoClient.connect() drops the next await silently #235 padding / rest-bundling mangling.
object/native_call_method.rs): the dynamic dispatcher detects aclass-object receiver (
is_class_object_value) and, when the method resolvesin the static chain, delegates; otherwise falls through to the generic
own-field path.
lookup_static_method_in_chainis nowpub(crate).Validation
test_gap_class_expr_extends_static_call_this.ts— byte-for-bytewith node across: statically-known receiver (tower), any-typed receiver
(runtime dynamic path), a no-
thisstatic method, and the const-bound(already-working) path.
(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/Schemainit still has further class-object blockers past this, in thesymbol subsystem (which breaks effect's
isSchema→dual→transformOrFail):symbol in classObject(theinoperator /Predicate.hasProperty) returnsfalseeven for an own static symbol property.Sub[TypeId]whereSub extends make(...))return
undefined.Refs #1758, #1785, #1772, #1791.