Skip to content

Free type aliases should not require all generic parameters to be used #140230

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
BoxyUwU opened this issue Apr 24, 2025 · 11 comments
Open

Free type aliases should not require all generic parameters to be used #140230

BoxyUwU opened this issue Apr 24, 2025 · 11 comments
Labels
A-variance Area: Variance (https://doc.rust-lang.org/nomicon/subtyping.html) C-bug Category: This is a bug. F-lazy_type_alias `#![feature(lazy_type_alias)]` T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-types Relevant to the types team, which will review and decide on the PR/issue.

Comments

@BoxyUwU
Copy link
Member

BoxyUwU commented Apr 24, 2025

I tried this code:

#![feature(lazy_type_alias)]

type Foo<T> = u32;

This current fails but I believe it should compile. I Imagine the reason we currently forbid this on stable is that as we have no representation for free type aliases in Ty any unused arguments to free type aliases would never be seen. With free type aliases having a proper representation as part of Ty under this feature gate it ought to be possible to soundly support this?

Tested on: nightly rust 1.88.0 2025-04-07 e643f59

@BoxyUwU BoxyUwU added the C-bug Category: This is a bug. label Apr 24, 2025
@rustbot rustbot added the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Apr 24, 2025
@BoxyUwU BoxyUwU added T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-types Relevant to the types team, which will review and decide on the PR/issue. F-lazy_type_alias `#![feature(lazy_type_alias)]` and removed needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. labels Apr 24, 2025
@fmease
Copy link
Member

fmease commented Apr 24, 2025

T is bivariant which we generally reject for "ADT-likes" and LTAs should behave very similarly to ADTs per T-lang consensus.

The current behavior works as intended but I guess we could allow it?

@fmease fmease added the A-variance Area: Variance (https://doc.rust-lang.org/nomicon/subtyping.html) label Apr 24, 2025
@fmease
Copy link
Member

fmease commented Apr 24, 2025

For context, if lazy_type_alias isn't enabled, we manually check if all type parameters are used. However, once lazy_type_alias is enabled, that check is suppressed and normal variance computation takes effect.

I guess we could permit lazy type aliases with bivariant type parameters (what about lifetime parameters, then?).

@fmease
Copy link
Member

fmease commented Apr 24, 2025

Obviously, struct Adt<T>(WeakBivariant<T>); would still get rejected.

@fmease
Copy link
Member

fmease commented Apr 24, 2025

Regarding soundness, permitting bivariant type/lifetime parameters on lazy type aliases might just work out (tm) because in "critical" contexts (checking if generic parameters are constrained) we actually expand_weak_alias_tys which is basically eager type alias expansion but localized.

Meaning we'd still reject pathological things like

#![feature(lazy_type_alias)]

type WeakDiscard</*bi*/ T> = (); // let's assume that were legal

type Demo = for<'flexible> fn(WeakDiscard<&'flexible ()>) -> &'flexible ();
//~^ ERROR return type references lifetime `'flexible`, which is not constrained by the fn input types

@BoxyUwU
Copy link
Member Author

BoxyUwU commented Apr 24, 2025

and LTAs should behave very similarly to ADTs per T-lang consensus

This feels very confusing to me. free type aliases are aliases not real types why should there be a similarity between free type aliases and adts instead of free type aliases and other forms of aliases? Where can I read about this lang consensus and what it means to "behave very similarly to ADTs". I'm also somewhat surprised this was even a lang decision, making free type aliases behave "properly" and defining what "properly" means seems very much like types territory to me as it is entirely subtle type system stuff.

that check is suppressed and normal variance computation takes effect

How come we do actual variance computation for free type aliases? Is it just so that uses of free type aliases in type definitions doesn't regress stable code from now making parameters invariant? More generally, is it just incidental that we forbid this because of the way we happen to implement things to allow type NotInvariant = &'static T; struct Foo<T>(NotInvariant<T>) to continue to compile?


In regards to that pathological case I wouldn't expect it to matter that we normalize free aliases from a soundness POV:

trait Trait<'a> {
    type Assoc;
}

impl<'a> Trait<'a> for () {
    type Assoc = ();
}

type Func<T> = for<'a> fn(<T as Trait<'a>>::Assoc) -> &'a u32;

We error on this code because we conservatively assume that aliases do not constrain there inputs. The worst case here should be that we regress stable code by being too conservative around free type aliases, rather than accidentally allowing too much.

Similarly, if we don't normalize free type aliases here, I would expect us to treat them like any other alias and conservatively error because of 'a potentially being unconstrained.

In general this is why it feels wrong to me to think about free type aliases as "adt like". We already have "sound enough" system for aliases and shouldn't need to reinvent any wheels. If we treat free type aliases the same as other aliases then we know its sound and correct. Then all that's left is adding in some special cases to take advantage of the fact that they're "trivially normalizeable" to make some checks smarter (like variance computation, or determining if lifetimes are constrained).

@fmease
Copy link
Member

fmease commented Apr 24, 2025

This feels very confusing to me. free type aliases are aliases not real types why should there be a similarity between free type aliases and adts instead of free type aliases and other forms of aliases? Where can I read about this lang consensus and what it means to "behave very similarly to ADTs

I admit I phrased this too hastily. I was referring to https://hackmd.io/LCUSAX5LSfqc4m7N4CWNPw#Type-aliases-today-leak-impl-details which talked about how LTAs should follow the behavior of ADTs wrt. implied outlives-bounds, not variances. So it's not accurate what I wrote.

Obviously, conceptually free/weak alias types are more structural, less nominal, unlike ADTs. I guess in my head I like to compare LTAs (the def site) with ADTs (the def site) for superficial reasons (you can define an inherent impl "for them", same variance behavior, they're free item kinds, …). It doesn't make much sense, I agree. I should retire that comparison.

How come we do actual variance computation for free type aliases? Is it just so that uses of free type aliases in type definitions doesn't regress stable code from now making parameters invariant?

Yes, exactly. "Is it just so": Well, you have to remember that LTAs didn't spring into existence because of T-types (#21903) but because of T-rustdoc (rust-lang/compiler-team#504). The original idea was to introduce a middle::ty representation for free alias types to better deal with inlined cross-crate re-exports (etc.) in rustdoc without performing any WF-checking at all for backward compatibility.

Therefore any behavior differences to other alias kinds like

serve the single purpose of replicating the existing eager type alias expansion (i.e., type_of on TyAlias paths during HIR ty lowering) "as much as reasonable" while still enabling rustdoc's original use case.


Of course, the feature LTA in its current form is not suited for rustdoc's use case because of all the WF-checking we're now performing and because of limitations of the old solver and because of other small differences (e.g., unused lifetime params aren't allowed because we reject most bivariant params as you noticed) — at least not in the current editions (which is still far from ideal, tracked in Lazy Type Aliases (LTA) (view)).

I remind you we both wrote privately about this in the past and you did suggest to me that we might be able to make WF-checking conditional on the edition which would be more than lovely. I'd love us to use AliasKind::Free in all editions, so rustdoc can take proper advantage of it (and get rid of HACKs like ad hoc HIR-based substitutions (this requires other work, too, ofc)). As mentioned, even without WF-checking, such a change would still be breaking a lot of code in theory.

@fmease
Copy link
Member

fmease commented Apr 24, 2025

You might wonder as to why we perform WF-checking for LTAs if their origin lies in that rustdoc/compiler MCP? Well, the PR that introduced AliasKind::Weak #108860 by oli made normalization check the own predicates (and therefore it was already immediately incompatible with the original vision of the T-rustdoc request). This was likely done to improve TAITs (which are always treated as AliasKind::Weak (i.e., even if LTAs aren't enabled) as you likely know).

Then *I* came along and introduced more and more WF-checks (#114228, #136432) and other incompatible changes (#114662) because I got carried away and saw LTAs as a way to finally fix #21903.

So right now LTAs are in a bit of an identity crisis: Backward-compatibly serving T-rustdoc's cross-crate story vs. a means to finally have type aliases that are checked for well-formedness — and I acknowledge that I am the cause. However, I'm glad that LTAs could be the solution for #21903 and thus it's good imo to focus on that while keeping in mind to hopefully soon create an off-switch for WF-checking of LTAs&Free/Weak somehow, so the rustdoc use case can still be achieved at some point.

@fmease
Copy link
Member

fmease commented Apr 24, 2025

Similarly, if we don't normalize free type aliases here, I would expect us to treat them like any other alias and conservatively error because of 'a potentially being unconstrained.

Yes, I'm more than aware of the way we conservatively treat other alias kinds as non-constraining. And I know that it would be beneficial if we could reduce the number of differences between all AliasKinds to a minimum.

Still, to me as a user of the language it would feel frustrating if free alias types would be treated as conservatively as projections (forcing its gen params to be invariant, unconditionally declaring its gen params unconstrained) given the fact that they can never be ridid, that they can always be reduced/normalized. Already, users open GH issues complaining about how restrained projections are handled (wrt. variance, constrainedness, no "arbitrary shorthand projections" (#22519), orphanck, …).

if we don't normalize free type aliases here

We can't normalize here without changing the existing behavior of normalizeable projections/alias types (and possibly introducing cycles), right? Therefore we expand which leaves all other kinds of alias types untouched / unnormalized.

@fmease
Copy link
Member

fmease commented Apr 24, 2025

In general this is why it feels wrong to me to think about free type aliases as "adt like". We already have "sound enough" system for aliases and shouldn't need to reinvent any wheels. If we treat free type aliases the same as other aliases then we know its sound and correct. Then all that's left is adding in some special cases to take advantage of the fact that they're "trivially normalizeable" [emphasis: mine] to make some checks smarter (like variance computation, or determining if lifetimes are constrained).

But that's exactly how I implemented it and how I imagined it to be?

In most cases, they are treated like every other AliasKind. Only in these few cases I listed (variances, constrainedness, inherent self tys, (soon) inferred outlives) I treat them specially via expand_weak_alias_tys/peel_off_weak_alias_tys (ofc I need to prove that that's sound / at least as sound as on master (which I believe it is)).

@fmease
Copy link
Member

fmease commented Apr 24, 2025

Similarly, if we don't normalize free type aliases here, I would expect us to treat them like any other alias and conservatively error because of 'a potentially being unconstrained.

Then all that's left is adding in some special cases to take advantage of the fact that they're "trivially normalizeable" to make some checks smarter (like variance computation, or determining if lifetimes are constrained).

Aren't these two statements of yours contradicting each other? Would you like us to treat AliasKind::Free like any other AliasKind during constrainedness checking or not?

@fmease
Copy link
Member

fmease commented Apr 24, 2025

cc @oli-obk for visibility (sry for the walls of text >.<)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-variance Area: Variance (https://doc.rust-lang.org/nomicon/subtyping.html) C-bug Category: This is a bug. F-lazy_type_alias `#![feature(lazy_type_alias)]` T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-types Relevant to the types team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

3 participants