TyKind::fn_sig() returns the declared signature with generic args substituted but not normalized. For a fully-monomorphized callee that means associated-type projections survive into the result, even though every argument is concrete and exactly one impl applies.
Minimized from a user report against our codegen backend (NVlabs/cuda-oxide#133):
struct Foo;
impl Mul for &Foo { type Output = Foo; }
let c = a * b; // a, b: &Foo
The MIR body from Instance::body() is fully normalized (BodyBuilder runs instantiate_mir_and_normalize_erasing_regions), so the local is stored as c: Foo. But asking the Call terminator's callee, FnDef(Mul::mul, [&Foo, &Foo]), for its signature gives:
declared fn mul(Self, Rhs) -> <Self as Mul<Rhs>>::Output
substituted fn mul(&Foo, &Foo) -> <&Foo as Mul<&Foo>>::Output <- fn_sig returns this
normalized fn mul(&Foo, &Foo) -> Foo <- never happens
So the return type comes back as a still-open projection, and since rustc_public has no normalize-like API, a consumer can't do the lookup themselves without escaping to rustc_internal::internal() + rustc_middle.
What the bridge does today
CompilerCtxt::fn_sig instantiates and skips normalization (one of the skip_norm_wip() sites, see rust-lang/rust#155345), while the sibling def_ty_with_args does normalize, via instantiate_and_normalize_erasing_regions.
Why not just normalize TyKind::fn_sig
It can be called with still-generic args, and normalizing in a generic context needs a typing env. We talked this through on Zulip: supporting only the monomorphized case is the agreed scope, and Instance (post-mono by construction) is the natural home.
Proposed API
impl Instance {
/// The signature of this instance: generic args applied,
/// associated types normalized, binder kept intact.
pub fn fn_sig(&self) -> Result<PolyFnSig, Error>;
}
Instance::fn_sig() mirroring Instance::fn_abi() was @makai410's suggestion in the thread: instantiate with the instance's args, then normalize with TypingEnv::fully_monomorphized() (which leaves bound regions intact, so it composes fine with the binder). The binder is kept: as @Nadrieril points out below, for<'a> fn(&'a ()) and fn(&'_ ()) are different types, so a binder-free return would be lossy. Consumers that only care about ABI can erase past the binder themselves.
(Edited: return type changed from FnSig to PolyFnSig per the discussion below.)
Alternatives we considered
- Normalizing inside the bridge
fn_sig when the args happen to be fully concrete: discussed and dropped on Zulip, since it changes a shared surface's behavior while generic callers still need the current semantics.
- A general
Ty::normalize(): more broadly useful, but a much bigger design (it needs a typing-env story) and not required for this case.
TyKind::fn_sig()returns the declared signature with generic args substituted but not normalized. For a fully-monomorphized callee that means associated-type projections survive into the result, even though every argument is concrete and exactly one impl applies.Minimized from a user report against our codegen backend (NVlabs/cuda-oxide#133):
The MIR body from
Instance::body()is fully normalized (BodyBuilderrunsinstantiate_mir_and_normalize_erasing_regions), so the local is stored asc: Foo. But asking theCallterminator's callee,FnDef(Mul::mul, [&Foo, &Foo]), for its signature gives:So the return type comes back as a still-open projection, and since
rustc_publichas no normalize-like API, a consumer can't do the lookup themselves without escaping torustc_internal::internal()+rustc_middle.What the bridge does today
CompilerCtxt::fn_siginstantiates and skips normalization (one of theskip_norm_wip()sites, see rust-lang/rust#155345), while the siblingdef_ty_with_argsdoes normalize, viainstantiate_and_normalize_erasing_regions.Why not just normalize
TyKind::fn_sigIt can be called with still-generic args, and normalizing in a generic context needs a typing env. We talked this through on Zulip: supporting only the monomorphized case is the agreed scope, and
Instance(post-mono by construction) is the natural home.Proposed API
Instance::fn_sig()mirroringInstance::fn_abi()was @makai410's suggestion in the thread: instantiate with the instance's args, then normalize withTypingEnv::fully_monomorphized()(which leaves bound regions intact, so it composes fine with the binder). The binder is kept: as @Nadrieril points out below,for<'a> fn(&'a ())andfn(&'_ ())are different types, so a binder-free return would be lossy. Consumers that only care about ABI can erase past the binder themselves.(Edited: return type changed from
FnSigtoPolyFnSigper the discussion below.)Alternatives we considered
fn_sigwhen the args happen to be fully concrete: discussed and dropped on Zulip, since it changes a shared surface's behavior while generic callers still need the current semantics.Ty::normalize(): more broadly useful, but a much bigger design (it needs a typing-env story) and not required for this case.