fix(codegen): declare cross-module method wrappers in consumer TU (#1126) - #1170
Merged
Merged
Conversation
`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
force-pushed
the
fix/1126-cross-module-method-wrapper-decl
branch
from
May 20, 2026 11:15
a6ec57d to
7d9ac46
Compare
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 #1126.
Expr::SuperPropertyGet(value formconst f = super.foo) emitsjs_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'sOperatorSubscriber 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:That cascaded:
OperatorSubscriber.tscouldn't compile → the empty-stub fallback only emits the__initsymbol (not class methods/constructors) → every downstream rxjs operator that referencedOperatorSubscriber::unsubscribeor its constructor failed to link (LNK2019on MSVC,Undefined symbolon 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.rsExpr::SuperPropertyGet: push(__perry_wrap_<fn>, double, [i64])intopending_declaresbefore emitting the singleton call.LlModule::declare_function(module.rs:67) is dedupe-by-name and the comment explicitly notes: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:Test plan
import { Subject } from 'rxjs'; new Subject().subscribe(v => console.log(v)); .next(1)): pre-fix dies withError compiling module 'OperatorSubscriber.ts'+ 28 downstreamLNK2019undefined-symbol errors. Post-fix all 227 modules compile, link succeeds, the 2.0 MB binary prints1.super.foosmoke test (class B { greet(n){...} } class D extends B { f(n){ const g = super.greet; return g(n) } }) still printsHello, world(the declare drops at to_ir time, no LLVM duplicate-symbol).cargo test --release -p perry-codegencleancargo test --release --workspace --exclude perry-ui-...cleanThe 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.