Skip to content

fix(codegen): declare cross-module method wrappers in consumer TU (#1126) - #1170

Merged
proggeramlug merged 1 commit into
mainfrom
fix/1126-cross-module-method-wrapper-decl
May 20, 2026
Merged

fix(codegen): declare cross-module method wrappers in consumer TU (#1126)#1170
proggeramlug merged 1 commit into
mainfrom
fix/1126-cross-module-method-wrapper-decl

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Fixes #1126. Expr::SuperPropertyGet (value form const f = super.foo) emits js_closure_alloc_singleton(@__perry_wrap_<fn>) against the resolved parent method's wrapper. When the parent class lives in a different module — the common npm-package pattern where a derived class overrides hook methods on an imported parent (rxjs's OperatorSubscriber extends Subscriber, delegating `_next` / `_error` / `_complete` to `super`) — the wrapper's `define` is in the source class's LLVM TU, but the consumer TU never forward-declared the symbol. clang failed per-TU IR validation with:

error: use of undefined value '@__perry_wrap_perry_method_..._Subscriber___complete'

That cascaded: OperatorSubscriber.ts couldn't compile → the empty-stub fallback only emits the __init symbol (not class methods/constructors) → every downstream rxjs operator that referenced OperatorSubscriber::unsubscribe or its constructor failed to link (LNK2019 on MSVC, Undefined symbol on macOS ld). 226 modules succeeded, 1 failure, link death. The repro is the canonical 5-line rxjs Subject test from the issue body — every rxjs user hits this.

Fix

crates/perry-codegen/src/expr/mod.rs Expr::SuperPropertyGet: push (__perry_wrap_<fn>, double, [i64]) into pending_declares before emitting the singleton call.

LlModule::declare_function (module.rs:67) is dedupe-by-name and the comment explicitly notes:

If a function with the same name is later defined in this module, the declaration is dropped at to_ir time so LLVM doesn't see both.

So this is safe for the same-module case (the declaration is dropped, no duplicate symbol) and unblocks the cross-module case. Signature is informational per the existing pattern at expr/mod.rs:12331:

The signature is for LLVM-IR well-formedness only — the runtime closure-call machinery casts the func_ptr to its own ABI at dispatch time

Test plan

  • Issue-body 5-line rxjs repro (import { Subject } from 'rxjs'; new Subject().subscribe(v => console.log(v)); .next(1)): pre-fix dies with Error compiling module 'OperatorSubscriber.ts' + 28 downstream LNK2019 undefined-symbol errors. Post-fix all 227 modules compile, link succeeds, the 2.0 MB binary prints 1.
  • Same-module value-form super.foo smoke test (class B { greet(n){...} } class D extends B { f(n){ const g = super.greet; return g(n) } }) still prints Hello, world (the declare drops at to_ir time, no LLVM duplicate-symbol).
  • cargo test --release -p perry-codegen clean
  • cargo test --release --workspace --exclude perry-ui-... clean

The cascading link-error path from #1126's body (when ANY module fails to compile, the empty-stub fallback doesn't synthesize method/constructor symbols for downstream modules) is not separately fixed in this PR — but no longer fires because there's no failed-module to stub.

`Expr::SuperPropertyGet` (value form `const f = super.foo`) emits
`js_closure_alloc_singleton(@__perry_wrap_<fn>)` against the resolved
parent method's wrapper symbol. When the parent class lives in a
*different* module (the common npm-package pattern of a derived class
overriding hook methods on an imported parent — rxjs's
`OperatorSubscriber extends Subscriber` delegating `_next` / `_error` /
`_complete` to `super`), the wrapper's `define` is in the source class's
LLVM TU but the consumer TU never forward-declared the symbol. clang
failed per-TU IR validation with:

  error: use of undefined value '@__perry_wrap_perry_method_..._Subscriber___complete'

That cascaded: `OperatorSubscriber.ts` couldn't compile, the empty-stub
fallback only emits the `__init` symbol (not class methods/constructors),
so every downstream rxjs operator that referenced
`OperatorSubscriber::unsubscribe` or its constructor failed to link
(`LNK2019` on MSVC, `Undefined symbol` on macOS ld). 226 modules
succeeded, 1 failure, link death. The repro is the canonical 5-line
`import { Subject } from 'rxjs'; new Subject().subscribe(...); .next(1)`.

Fix in `crates/perry-codegen/src/expr/mod.rs` `Expr::SuperPropertyGet`:
push `(__perry_wrap_<fn>, double, [i64])` into `pending_declares` before
the singleton call. `LlModule::declare_function` (`module.rs:67`) is
dedupe-by-name and the comment explicitly notes "If a function with
the same name is later *defined* in this module, the declaration is
dropped at `to_ir` time" — so this is safe for the same-module case
(becomes a no-op) and unblocks the cross-module case. Signature is
informational per the existing pattern at `expr/mod.rs:12331` ("The
signature is for LLVM-IR well-formedness only — the runtime
closure-call machinery casts the func_ptr to its own ABI at dispatch
time").

Verified:
- 5-line rxjs repro from the issue body: pre-fix
  `Error compiling module 'OperatorSubscriber.ts'` + 28 downstream
  `LNK2019` undefined-symbol errors. Post-fix all 227 modules compile,
  link succeeds, the 2.0 MB binary prints `1`.
- Same-module value-form `super.foo` smoke test:
  `class B { greet(n){...} } class D extends B { f(n){ const g =
  super.greet; return g(n) } }` still prints `Hello, world` (declare
  drops at to_ir, no LLVM duplicate-symbol).
- `cargo test --release -p perry-codegen` clean
- `cargo test --release --workspace --exclude perry-ui-...` clean

The cascading link-error path from #1126's body (empty-stub fallback
doesn't synthesize method/constructor symbols) is not separately
fixed, but no longer fires because there's no failed-module to stub.
@proggeramlug
proggeramlug force-pushed the fix/1126-cross-module-method-wrapper-decl branch from a6ec57d to 7d9ac46 Compare May 20, 2026 11:15
@proggeramlug proggeramlug changed the title v0.5.1018: fix(codegen) — declare cross-module method wrappers in consumer TU (#1126) fix(codegen): declare cross-module method wrappers in consumer TU (#1126) May 20, 2026
@proggeramlug
proggeramlug merged commit e2a1d8f into main May 20, 2026
9 checks passed
@proggeramlug
proggeramlug deleted the fix/1126-cross-module-method-wrapper-decl branch May 20, 2026 11:26
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.

rxjs import error

1 participant