Skip to content

fix(runtime): resolve static-getter method calls on call-expression heritage (#10210) - #10213

Closed
proggeramlug wants to merge 1 commit into
PerryTS:mainfrom
proggeramlug:fix/10210-static-getter-call
Closed

proggeramlug wants to merge 1 commit into
PerryTS:mainfrom
proggeramlug:fix/10210-static-getter-call

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #10210 (OpenCode v1.18.30 runtime bootstrap wall, tracker #10107).

What was wrong

Inside a static getter of a class whose extends clause is a call expression — effect v4's Context.Service<..>()(id) returns function KeyClass(){} with Object.setPrototypeOf(KeyClass, ServiceProto)this.of(x) threw of is not a function, while the plain read this.of returned the function. OpenCode hits it in src/effect/config-service.ts (static get layer()tag.of(config)) while AppRuntime builds RuntimeFlags, so every command that needs the runtime (models, run, serve, the TUI) died before the first log line.

Two gaps, both on the miss path only (no change for calls that resolved before):

  1. js_native_call_method, class-object receiver (native_call_method/primitive_methods.rs): the arm resolved only the static-method vtable. It now mirrors the class-ref arm (Next.js standalone: deferred-require binding captured by-value (as unresolved thunk) by a class constructor → 'undefined is not a constructor' #5437): on a vtable miss it reads the property exactly as the read path does (static accessors, the per-evaluation parent class object's statics, the function-valued ancestor's swapped prototype) and calls the closure with this bound to the receiver. A non-callable result keeps falling through to the generic scan and the usual not-a-function error.
  2. Class-ref read paths (field_get_set/get_field_by_name.rs, symbol/get.rs): the function-valued parent edge was looked up only on the receiver's own class id (class_parent_closure). They now use parent_closure_in_chain, which super() dispatch already used, so a subclass of the class that extends <function> inherits those statics too.

Verification (perrymaster, Linux x86_64, release build)

The probe matrix from the issue, node vs the patched compiler — identical on every line that is a call or a read:

N1 nested+sub getter this.of(x)            {"n":1}
N2 nested+sub getter f=this.of; f.call     ["function",{"n":2}]
N3 nested+sub getter this['of'](x)         {"n":3}
N4 nested+sub getter tag=this; tag.of(x)   {"n":4}
N5 nested no-sub getter this.of(x)         {"n":1}
N6 top-level+sub getter this.of(x)         {"n":6}
N7 top-level no-sub getter this.of(x)      {"n":6}
N10 nested+sub method this.of(x)           {"n":10}

The pure-TS mirror of OpenCode's ConfigService.Service + RuntimeFlags (static get layer()Layer.effect(tag, () => tag.of(...)), static configLayer(input) { return Layer.succeed(this, this.of(input)) }) now prints the node output on all six lines.

New test crates/perry/tests/issue_10210_static_getter_call.rs (10 lines of expected node output, including the inherited read on a grandchild and calling the function a static getter returns on a dynamic receiver) passes; the sibling class/static regression tests (class_static_symbol_inheritance, class_expr_dynamic_parent_field_order, issue_4908_subclass_native_member_base, capture_rereg_renamed_class, instanceof_classexpr_rhs_captured, aliased_native_class_import) still pass.

Not covered (documented on #10210)

  • this inside the static getter is still the holder object, not the receiver, and a nested capturing class is a ClassExprFresh object while the class name inside its own body lowers to an INT32 ClassRef (perry-hir/src/lower_decl/body_stmt.rs:281 vs :362) — the identity checks in the issue (this === Sub, Object.getPrototypeOf(Sub) === T) stay false. Dispatch is correct; only identity diverges.
  • KnownTopLevelClass.staticGetter() (calling the function a static getter returns) on a class that extends a call expression still throws: the compile-time static tower routes it to js_class_static_method_call, whose miss path walks CLASS_DYNAMIC_PROPS and the parent chain but not CLASS_STATIC_ACCESSORS. Same shape as gap 1, one helper over; OpenCode does not use it.

https://claude.ai/code/session_01As1fetJAqDFib4n7Wm5Suo

Summary by CodeRabbit

  • Bug Fixes

    • Fixed calls to static getters and methods on classes extending function-valued expressions.
    • Improved static member resolution across parent and ancestor classes, including inherited accessors and symbol- or string-keyed properties.
    • Ensured functions returned by static properties are invoked with the correct class receiver.
    • Resolved runtime bootstrap failures affecting class-based applications.
  • Tests

    • Added regression coverage for nested classes, inherited static members, aliased receivers, and functions returned by static getters.

…eritage (PerryTS#10210)

A static getter on a class whose `extends` clause is a call expression
(effect v4 `class Svc extends Context.Service<Svc, Shape>()(id) {}`, i.e. a
plain `function KeyClass(){}` whose [[Prototype]] was swapped with
`Object.setPrototypeOf`) threw `of is not a function` on `this.of(x)` while
the plain read `this.of` returned the function.

Two gaps, both on the miss path only:

- The dynamic method-call dispatcher's class-object arm resolved nothing but
  the static-method vtable. It now mirrors the class-ref arm (PerryTS#5437): when the
  vtable misses, read the property exactly as the read path does (static
  accessors, the per-evaluation parent class object's statics, the
  function-valued ancestor's swapped prototype) and call the closure with
  `this` bound to the receiver.
- The class-ref read paths (string and symbol keys) looked for the
  function-valued parent edge only on the receiver's own class id. They now
  walk the class chain like `super()` dispatch already did, so a subclass of
  the class that `extends <function>` inherits those statics too.

Unblocks OpenCode v1.18.30's runtime bootstrap (`ConfigService.Service` →
`static get layer()` → `tag.of(config)` while AppRuntime builds RuntimeFlags).

Claude-Session: https://claude.ai/code/session_01As1fetJAqDFib4n7Wm5Suo
@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 8ad38a23-2672-46eb-bc13-958d08e921b8

📥 Commits

Reviewing files that changed from the base of the PR and between 64f5249 and 14126f3.

📒 Files selected for processing (5)
  • changelog.d/10210-static-getter-call-path.md
  • crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
  • crates/perry-runtime/src/object/native_call_method/primitive_methods.rs
  • crates/perry-runtime/src/symbol/get.rs
  • crates/perry/tests/issue_10210_static_getter_call.rs

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The runtime now resolves static properties through ancestor class chains and invokes closure-valued static properties with the class receiver. A regression test covers getters, methods, inherited lookups, symbols, and call-expression-derived classes.

Changes

Static getter call-path resolution

Layer / File(s) Summary
Runtime lookup and dispatch
crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs, crates/perry-runtime/src/object/symbol/get.rs, crates/perry-runtime/src/object/native_call_method/primitive_methods.rs
Static lookup now walks ancestor closures. Static primitive dispatch reads unresolved properties, rebinds closure receivers, and invokes the closures.
Regression validation and record
crates/perry/tests/issue_10210_static_getter_call.rs, changelog.d/10210-static-getter-call-path.md
The regression test compiles and runs call-expression-derived classes. It checks static getter calls, inherited properties, closure calls, and expected output. The changelog records the updated lookup behavior.

Priority: ⬆️ High

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: High

Sequence Diagram(s)

sequenceDiagram
  participant StaticClassReceiver
  participant dispatch_primitive
  participant js_object_get_field_by_name
  participant Closure
  StaticClassReceiver->>dispatch_primitive: call static property
  dispatch_primitive->>js_object_get_field_by_name: resolve property
  js_object_get_field_by_name-->>dispatch_primitive: return closure
  dispatch_primitive->>Closure: rebind this and invoke
  Closure-->>StaticClassReceiver: return result
Loading

Merge Risk: ⚪ Minimal · up to 14126

The change includes the required static lookup and receiver-bound invocation behavior with regression coverage for the affected class-chain scenarios. No merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 4 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the runtime fix for static-getter method calls on call-expression heritage classes. It is concise and directly related to the primary change.
Description check ✅ Passed The description provides a clear problem statement, explains the two implementation changes, references issue #10210, documents verification results, and identifies out-of-scope behavior. It does not …
Linked Issues check ✅ Passed For [#10210], the runtime now falls back from static vtable dispatch to property lookup for class-object method calls. The fallback rebinds the closure receiver and preserves the normal non-callable e…
Out of Scope Changes check ✅ Passed The changed runtime files implement [#10210] method dispatch and class-chain property lookup. The new test provides regression coverage for the issue. The changelog documents the same fix. No unrelate…
Full details: Docstring Coverage

Explanation

Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 4 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Landed via merge train #10216 (rebase-merged; main 5d3bf86823, tree identical to the train), cherry-picked onto 44e78debf9 with the version bump to 0.5.1559. Validation and CI attribution are in #10216.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant