Skip to content

Add Instance::fn_sig() returning a normalized, monomorphic signature #126

Description

@nihalpasham

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions