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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions