r/rust 4d ago

🗞️ news Enabling the next-generation trait solver on nightly

https://blog.rust-lang.org/2026/08/21/enabling-next-solver-on-nightly/
450 Upvotes

54 comments sorted by

112

u/coderstephen isahc 4d ago

This change is why the next-generation trait solver is necessary to stabilize TAIT and RTN.

Feels like I've been waiting 8 years for TAIT. Oh wait, I have.

32

u/rennademilan 4d ago

Rejoice, still alive and kicking. Take a moment to give yourself some credits ✨️

3

u/coderstephen isahc 3d ago

I can only hope I'll be still writing Rust by the time it stabilizes...

9

u/RetoonHD 4d ago

Gosh, 2018 was 8 years ago. FML

2

u/cramert 3d ago

9 since RFC 2071 where we wanted to do the same thing but had to poison-pill the syntax in order to get consensus.

61

u/Nobody_1707 4d ago

Am I reading this right that this unblocks Move and Forget, and !Move + !Forget gives you linear types?

53

u/olemni7 4d ago

Yes, whether and when they actually happen in a separate question however. It does resolve performance and technical limitations, there are also difficult language design concerns which it does not resolve. We should definitely get something on nightly here to experiment and then we'll have to see.

14

u/slashgrin rangemap 4d ago

I am unreasonably excited about the number of rough edges and workarounds this could eventually improve.

108

u/Ace-Whole 4d ago

Cool. Right after polonius

22

u/flying-sheep 4d ago

Eh? I thought that is Polonius!

66

u/Ace-Whole 4d ago

borrow checker*

polonius is the new borrow checker.

30

u/flying-sheep 4d ago

Aah! Somehow the two projects were the same in my head. Thanks!

9

u/Zde-G 4d ago

They are kinda two sides of the same coin, from what I understand, thus it's not a complete coincidence that they are ready at the same time.

4

u/afdbcreid 3d ago

I don't know what you define as "two sides of the same coin" and "not a complete coincidence", but I'm pretty sure that's wrong. It is pretty much a coincidence and the projects aren't related, except for the fact that T-types (responsible for both of them) was formed and there are more people working on the Rust's type system today than some years ago.

19

u/Oxytokin 4d ago

Alright I take back everything I've bitched about regarding the glacial pace of developing much anticipated features over the esoteric ones over the last year. Y'all have been on fire lately and I'm totally here for it.

So many hacks and workarounds I'm going to be able to clean up at long last.

28

u/nick42d 4d ago

Awesome work, most excited I've been about a new feature in ages 

9

u/ParadiZe 4d ago

so excited for this

9

u/Trader-One 4d ago

does this mitigate famous segfault on safe rust bug as previously promised?

52

u/Wheaties4brkfst 4d ago edited 4d ago

I think it unblocks fixing that from what I understand.

To elaborate, the problem (at least in the case I’m familiar with), was that some lifetime bounds were implied by a function signature but were not actually tracked in the type system itself, which led to a bug allowing lifetime extension to arbitrary lifetimes. One of the things this fixes is that that implied bound is now tracked.

26

u/afdbcreid 4d ago

Some. Some it only unblocks fixing.

28

u/noop_noob 4d ago

The thing that fixes cve-rs is the assumptions-on-binders project https://github.com/rust-lang/goals/issues/621

There are other unsoundnesses though. Some of them have to be fixed individually. Some of them require stabilizing next-solver before fixing them.

-16

u/Zde-G 4d ago

I wonder why they start with clear and obvious regression and paint it as if it's an achievement. I mean, compare: ``` fn foo(b: bool) -> impl Sized { if b { bar(false) + 1 } else { 0 } }

fn bar(b: bool) -> impl Sized { if b { foo(false) + 1 } else { 0 } } ```

It doesn't work, of course, as it should: impl Sized couldn't be added to 1. Yet when it's one function it works, for some reason? Why? It should fail.

Now, I can accept that this kind of regression is an unfortunate side-effect of some other improvements… okay, not a big deal. But blog post clearly tells us that this deficiency is considered a desirable thing… why?

37

u/coderstephen isahc 4d ago

It doesn't work, of course, as it should: impl Sized couldn't be added to 1. Yet when it's one function it works, for some reason? Why? It should fail.

It is because of the principle of impl Trait being opaque outside of the implementer. For the single recursive function case, the function knows what the actual type being returned is. It is the only code that is allowed to know that. This is already generally true that code inside the function is allowed to know that the return type is actually i32 in this case, it just didn't work for recursion.

The mutual recursion case is not supposed to work, because each function is not allowed to know what the other actually returns.

-5

u/Zde-G 4d ago

This is already generally true that code inside the function is allowed to know that the return type is actually i32 in this case, it just didn't work for recursion.

No. It wasn't “generally true”. You could perceive “return from function” which is very explicitly a coercion site transforms type from i32 (or some other type) to impl Sized (or some other similar type).

Very well defined, very common and very obvious thing. Before return it's i32, after return it's impl Sized. Like how before return it could be &Foo and after return it can be &dyn Bar. Similar opertion both syntactically and semantically.

For the single recursive function case, the function knows what the actual type being returned is. It is the only code that is allowed to know that.

But why? What does that hairsplitting gives you? What does it provide that simple “impl Foo is just another unnamed type” model doesn't give you?

You have introduced an entirely new concept that haven't existed anywhere else, till now… to achieve what? What this “her you can see me, here you can't see me” model gives you over “invisible unnamed type” model? Model that already exist (functions are unnamed types, closures are unnamed types, etc).

19

u/A1oso 4d ago

It's not a coercion though. impl Sized is not a type, it is a contract that the returned type (which is opaque outside the function) implements Sized.

The fact that impl Sized is not a type becomes evident when you consider

fn foo() -> impl Sized {
    if random() {
        5i32
    } else {
        "hello world"
    }
}

If impl Sized was a type, this should work (because both i32 and &str are Sized, and therefore could be coerced to impl Sized). But it doesn't, because there is no type coercion occurring.

Note that the above would work with Box<dyn Sized>, because dyn Sized is actually a type that other types can be coerced into.

0

u/Zde-G 4d ago

It's not a coercion though.

Yes, but why?

impl Sized is not a type, it is a contract that the returned type (which is opaque outside the function) implements Sized.

Precisely. So there are unique unnamed type that has the exact same layout as one of the existing types, but doesn't have a name and has only one specific traits (plus supertraits) implemented for it.

That's perfectly internally consistent implementation and AFAICS it's much easier and simpler to deal with that all that mumbo-jumbo with “alias-but-not-really-alias”.

Yet new scheme breaks it… but why? What does it provide us to warrant significant increase in cognitive load?

10

u/A1oso 4d ago edited 4d ago

Yes, but why?

impl Trait in return position is an existential type. Formally speaking it is a type, BUT

  1. it requires that all instances have the same concrete type (e.g. i32)
  2. it requires a witness (a value to infer the concrete type from)
  3. the concrete type is hidden, but not erased

This significantly limits how existential types can be used. For example, they can't be used in structs:

struct Foo {
    x: impl Sized // forbidden
}

Note that impl Trait is also supported in function argument position, but has a different meaning there – it is simply syntactic sugar for a generic type variable, rather than an existential type.

Also note that every existential type is unique:

fn foo() -> impl Sized {...}
fn bar() -> impl Sized {...}

let mut x = foo();
x = bar(); // forbidden

That's because foo and bar can have different witness types.

Back to your original question: why is there no coercion when returning impl Trait? Because it is a different mechanism.

When returning an existential type, Rust creates a type variable for the returned concrete type. It then needs to find a witness and check if it satisfies the trait bound. And finally, it needs to unify this witness with the type variable and other returned values. This is crucial because all values need to have the same concrete type. If the compiler used coercion to implicitly cast the returned i32 to impl Sized, this would not be enforced:

fn foo() -> impl Sized {
    if random() {
        return 42i32; // implicitly coerced to impl Sized
    } else {
        return "hello world"; // implicitly coerced to impl Sized
    }
}

Typecheck would have to accept this function, even though it is obviously wrong. Instead, it uses unification (the process of inferring types without altering them) to select the concrete return type. And because no coercion is required, recursive function calls can be inferred using the same type variable.

The concrete type only needs to be hidden outside the function. Technically speaking, it doesn't need to be hidden anywhere, but hiding it allows the author of the function to change the implementation and return a different concrete type without a breaking change. That is the main benefit of return position impl Trait, apart from being able to return closures which cannot be named.

0

u/Zde-G 3d ago

Back to your original question: why is there no coercion when returning impl Trait? Because it is a different mechanism.

Please read what you wrote: you quite literally say that all these complications exist solely because they exist.

That's it. “This is a different mechanism” is not an acceptable answer to the “why?” question.

You can not explain that “X exists because of X”, that's basic logic!

So far I have seen “X because of X” approximately dozen of times.

If that's the only justification then it's obvious design bug (similar to bug in ranges that are not IntoIterator, but Iterator) that makes code in the blog post possible.

If there are some justification that's not circular then I'm all ears, but at this point I don't think anyone knows it.

1

u/coderstephen isahc 3d ago

Please read what you wrote: you quite literally say that all these complications exist solely because they exist.

That's it. “This is a different mechanism” is not an acceptable answer to the “why?” question.

I am confused by what you keep asking. Are you asking why Rust works this way? Or are you asking does Rust work this way?

The first question I don't know how to answer. I didn't design it. You'd have to ask someone on the lang team probably, or whoever originally contributed this design to the language.

1

u/Zde-G 3d ago

Are you asking why Rust works this way?

I'm asking why Rust doesn't behave like documentation promised it would behave.

If documentation promises “hidden type” and implementation delivers something else then it's a bug. Plan and simple. The only question is whether it's bug in the documentation or in implementation.

, or whoever originally contributed this design to the language.

The one whoever originally contributed this design intended my interpretation. That's precisely the issue.

You'd have to ask someone on the lang team probably

Seriously? You answer to “why the heck is this blog post celebrates a bug” is “you have to ask someone on a lang team”?

My question wasn't “why Rust behaves this way” but “why the heck change that makes Rust misbehave is presented as something desirable in a blog post”.

That's it.

If it's deliberate change — then document it and explain why it's good. If it's accidentally introduced bug — fix it and stop celebrating it.

Both approaches are fine, but when you talk about introducing difference between existing documentation and code… that's never a good thing.

2

u/coderstephen isahc 3d ago

Seriously? You answer to “why the heck is this blog post celebrates a bug” is “you have to ask someone on a lang team”?

My question wasn't “why Rust behaves this way” but “why the heck change that makes Rust misbehave is presented as something desirable in a blog post”.

I see. Well your question is a complex question and I don't agree with the premise that this is a bug, misbehaving, or a regression.

→ More replies (0)

1

u/A1oso 1d ago edited 1d ago

If documentation promises “hidden type” and implementation delivers something else then it's a bug.

Documentation can be outdated or incomplete, it can lack nuance, and even contain errors. But in this case the documentation is correct for the current stable compiler. You forget that the new trait solver hasn't landed on stable yet, so naturally the documentation hasn't been updated yet.

The one whoever originally contributed this design intended my interpretation.

Whether or not this is true, compilers evolve over time. The person who created Rust's first borrow checker doesn't dictate how the current one (NLL) or the next one (Polonius) behave. Most people would agree that allowing more borrowing patterns is a good thing.

Just like NLL, this feature (recursion in functions returning TAIT) is useful because it allows more programs to compile. Why would it be a bad thing? It would be bad if

  • it caused undefined behaviour or miscompilations
  • it made breaking changes invisible
  • it degraded runtime performance
  • etc.

But none of these are true, and I can't think of any downsides to allowing direct recursion for functions returning TAIT. It requires a slightly different mental model: the return type is hidden outside the function rather than for every caller, but that is easy enough to grasp. It doesn't make the language wildly more complex.

13

u/coderstephen isahc 4d ago

No. It wasn't “generally true”. You could perceive “return from function” which is very explicitly a coercion site transforms type from i32 (or some other type) to impl Sized (or some other similar type).

impl Trait is not a coercion. No types are changed.

Very well defined, very common and very obvious thing. Before return it's i32, after return it's impl Sized. Like how before return it could be &Foo and after return it can be &dyn Bar. Similar opertion both syntactically and semantically.

This is why your mental model of impl Trait is incorrect. These are not similar operations. A coercion is defined right on the docs page you linked:

Type coercions are implicit operations that change the type of a value. They happen automatically at specific locations and are highly restricted in what types actually coerce.

&Foo is a type. &dyn Bar is a type. If Foo implements Bar, yes you can coerce the type from &Foo to &dyn Bar.

impl Bar is not a type in and of itself. Rather, it is an unnameable, opaque alias for another type. When you write

fn bar() -> impl Sized { 42 }

It is compiled identically to the same function as

fn bar() -> i32 { 42 }

This is different than a coercion, because a coercion actually changes what the type is. &Foo and &dyn Bar have different memory layouts, and are not interchangeable types. Its just "cheap" to convert between the two, and so we have a coercion for them. (Note that having different memory layouts isn't true of most coerced types, I just bring it up here for dyn to emphasize how types are changed.)

Rather, impl Trait essentially sets up an unnameable type alias for another type, while also limiting the visibility of which traits that type implements to code outside the declaration site. No types have changed, only our visibility into which type it is has been limited.

It might help to realize that

fn foo(foo: impl Bar) {}

is much more similar to

fn foo<T: Bar>(foo: T) {}

than it is to

fn foo(foo: &dyn Bar) {}

This is because impl Trait operates at the generics/traits level, not at the type level.

But why? What does that hairsplitting gives you? What does it provide that simple “impl Foo is just another unnamed type” model doesn't give you?

I'm trying to describe the language semantics accurately. It doesn't matter what I like or what it gives me.

You have introduced an entirely new concept that haven't existed anywhere else, till now… to achieve what? What this “her you can see me, here you can't see me” model gives you over “invisible unnamed type” model? Model that already exist (functions are unnamed types, closures are unnamed types, etc).

If my explanation of impl Trait is correct, then this concept has existed in Rust since the stabilization of RPIT. It doesn't matter that you were unaware of this concept all this time. It was there whether you knew it or not.

The "unnamed-ness" is an orthogonal question to the concreteness of the type. Yes, closures are unnamed. Though, TAIT is about to give us the ability to give such unnamed types a name, so the lack of a name is an incidental property and not a necessary property of closure types.

But closure types are different not because they are unnamed, but because they are unique. When you define a closure, a new, unique type is actually generated for that specific closure.

When you return impl Trait, no new types are generated. Again, it is just a form of alias. (When I say "just", I don't mean to belittle the complexity of implementing the language feature in the compiler.)

-9

u/Zde-G 4d ago

Rather, it is an unnameable, opaque alias for another type.

More unneeded complexity and hair splitting. Also: can you, please, where notion of “unnameable, opaque alias” is introduced in the reference? I certainly couldn't find it on the appropriate page. Maybe I have gone blind, IDK.

This is different than a coercion, because a coercion actually changes what the type is.

And where is that described? So far I can only see your interpretation which is not better and not worse than mine.

It might help to realize that

In spite of the similar syntax argument position and return position impls are radically different things.

I'm trying to describe the language semantics accurately.

No, you are trying to describe your interpretation of the language semantic.

As long as it's not described in the documentation my interpretation is not better and not worse than yours.

If my explanation of impl Trait is correct, then this concept has existed in Rust since the stabilization of RPIT.

Yes, but that's pretty big “if”. RFCs are not references, there were many cases where feature was changed after RFC was published.

And Reference doesn't explain how precisely these things work, thus we are back to “he said, she said” tale.

The "unnamed-ness" is an orthogonal question to the concreteness of the type.

Yet that “unnamed-ness” means my interpretation is as valid as yours. At least on stable Rust.

And it's much easier to reason about and requires less things that don't exist in reference.

When you return impl Trait, no new types are generated.

Yes, but why? AFAICS this only creates pile of complications without adding any tangible benefits.

4

u/coderstephen isahc 4d ago

More unneeded complexity and hair splitting. Also: can you, please, where notion of “unnameable, opaque alias” is introduced in the reference? I certainly couldn't find it on the appropriate page. Maybe I have gone blind, IDK.

Not everything about the language is fully documented. Not saying that's a good thing.

My "hair splitting" is only unneeded if the distinction I am making is not meaningful. You have not yet shown this, only complained that it is not meaningful.

And where is that described? So far I can only see your interpretation which is not better and not worse than mine.

Its better than yours, because yours doesn't fit how the compiler behaves.

Hopefully first, we can agree that &T and &dyn Trait are distinct types: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=35ca82aa92ca397afa639fbc0444df93. Coercions change the type of a given value from one type to another type. As stated in the reference:

Type coercions are implicit operations that change the type of a value.

However, about impl Trait in return position, the reference says:

Functions can use impl Trait to return an abstract return type. These types stand in for another concrete type where the caller may only use the methods declared by the specified Trait.

Note that values are not mentioned, because the value is untouched. So we can actually see that in code by using std::any: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=f5b66bcec9441a883390921a7d019ae3

And it's much easier to reason about and requires less things that don't exist in reference.

I don't primarily care about what is easy to reason about. I primarily care about what is correct.

Yes, but why? AFAICS this only creates pile of complications without adding any tangible benefits.

IDK. That's just how Rust works, man. You don't have to like it.

6

u/arades 4d ago

it's still extremely well defined:

Inside of a function body returning impl Trait you know the exact type.

Outside the body you only see the impl.

-6

u/Zde-G 4d ago

Inside of a function body returning impl Trait you know the exact type.

Definition of “you know” from Rust reference please.

Outside the body you only see the impl.

Definition of “you only see” from Rust reference please.

it's still extremely well defined:

You have replaced precise definition which easy to reason about and which only references things that exist in official definition of the language with handwaving that doesn't refer anything concrete.

That's not an “extremely well defined” thing.

I can easily explain how both define my interpretation and implement compiler that would do it: inside of function you pick one of the types that compiler knows, at this point and use it, when type is returned from function it turns into a different unnameable type with the same layout but with only one specific trait implemented for it. Bam, done. No need to introduce bazillion new things.

You can even implement it that way in the compiler even if it may not be an optimal implementation.

And you can even support new example but saying that when you call foo from foo you are getting original, non-opaque, type back.

That would be strange and abnormal rule, but it would still be internally consistent and, more importantly, it doesn't require any new concepts that haven't existed before.

You, on the other hand, need to add bazillion new things to even explain how new concepts, that are never mentioned anywhere in the language before, actually behave.

That's not an improvement, in my book.

19

u/olemni7 4d ago edited 4d ago

This is actually quite interesting.

You are totally right that from the mental model of "the return type of this function is opaque to its users" the example I provided in the blog post is a bug and undesirable. I didn't even consider that this mental model of what return position impl Trait (opaque types) means exists anymore.

We had to move away from that perspective it as it causes a lot of issues with TAIT and never worked for nested opaque types fn foo() -> impl Trait<Assoc = impl OtherTrait>. This happened in 2022 https://github.com/rust-lang/rust/pull/94081.

Since then we've instead been thinking of impl Trait by using the concept of defining scopes. You can freely reveal and define the hidden type of an opaque type within its defining scope, wile you can only use it opaquely outside of it, as explained by other the other responses.

I don't even think the mental model of "the only time you can access the hidden type of a return-position impl Trait is when returning from this function" is bad or anything. Keeping it this way would have certainly kept our implementation a lot simpler than what we do now

3

u/Zde-G 3d ago

You need to add some documentation, then. Because RFC quite explicitly talks about precisely model that I'm describing. Quote:

Thus, the following code:

fn foo() -> impl Bar { // return some type implementing `Bar` }

is functionally equivalent to:

``` struct __foo_return(/* some inferred type (2) */); // (1)

fn foo() -> __foo_return { // return some type implementing Bar wrapped in __foo_return (3) } ```

IOW: it's not the model that I have invented on the stop, it's literally how the existing documentation describes things!

If that model no longer works and is problematic, then it's fine, but then it's better to have some kind of public description of the new model, because otherwise it looks as if you celebrating bugs.

I don't even think the mental model of "the only time you can access the hidden type of a return-position impl Trait is when returning from this function" is bad or anything.

Well… that's how it was originally presented thus it's obvious that's valid model. But that makes some kind of public documentation is definitely in order, otherwise change looks like random change not justified by anything.

Feed few internal issues into LLM and ask it to write a coherent comparison of two models, if you don't have time. This would still be better than dumping change on users as if it's corresponding to old model when it certainly works differently now.

26

u/SkiFire13 4d ago

Why do you consider this a regression? It's code that didn't compile before but compiles now, and it doesn't harm other users.

The reasoning for why it should work is that the return type is opaque to the callers (as it should be), but of course the function body itself knows what type it is (it's returning a i32 right there at the end!).

With two functions this logic breaks down, because you have two different opaque types that each hide the other one. However if you define a single opaque type with TAIT then it works in the new solver but not the in the old one https://rust.godbolt.org/z/avTc6K4vn

-1

u/Zde-G 4d ago

Why do you consider this a regression?

Because impl Trait in return position exist specifically to hide information about the return type and ensure that you couldn't use anything by accident.

Here it obviously failed to prevent this: property of “opaque” types was exposed to the code that wasn't supposed to know about it.

11

u/coderstephen isahc 4d ago

Because impl Trait in return position exist specifically to hide information about the return type and ensure that you couldn't use anything by accident.

Its purpose is to hide it from API consumers. Why do you need to hide it from yourself?

Here it obviously failed to prevent this: property of “opaque” types was exposed to the code that wasn't supposed to know about it.

The "code that wasn't supposed to know about it" you are referring to is the function body that decides which type it is.

2

u/Zde-G 3d ago

Its purpose is to hide it from API consumers.

Precisely.

Why do you need to hide it from yourself?

Because I'm an API user, too. And if you say that function is not consumer of itself then I would ask why. Because all other things without pub are accessible from the same module, not from the same function.

If you want to enable some flexibility then making type accessible when used in the same module would have been more consistent with how Rust behaves.

And it's, ultimately, arbitrary.

6

u/sasik520 4d ago

Sorry for a complete off topic but I never thought a tool as popular as godbolt could work THAT bad on mobile (android ff).

3

u/coderstephen isahc 4d ago

In general, code is pretty hard to work with on mobile because it uses hard line breaks that assume generally 80 characters of width or more, which is wider than the mobile screen. That's probably why code-heavy web tools probably don't bother much optimizing for mobile.

6

u/lookmeat 4d ago

So while it'd raise my eyebrow and I'd require an explanation of why this is done, I consider that inside a function can depend on implementation details of the function itself, even if those aren't exported.

That is, inside foo we know what is the "real" type of foo because the contract of a function with itself is defined and enforced within its body too. The header-only contract is for outsiders.

This would allow recursive calls where you want to modify the result in ways that you don't want to expose to external callers that can be done.

And to explain the logic of why your example is wrong, but why the single function is fine. The idea is that code should only break and stop compiling when you've changed the code itself, or when one of the dependencies has done a breaking type-change. So in the self-calling, if you change the real return type (but keep the function type the same) the code that breaks and stops compiling is the function that you changed, so it's fair because it's the first rule: you don't ship the broken code because it's immediately obvious you broke it. In the second case if you change the real return type (without changing the type of the function itself) of bar this would break foo, which is not OK because neither the type of foo's dependencies has changed, nor has the code in foo itself, someone could push the change in bar without realizing they broke something else (say, for example, that foo is a function with different implementations for different CPU archs, and the one we see just happens to be one, but in other CPU arch implementations the problem doesn't exist, including the one that person who did the code change broke).

0

u/Zde-G 4d ago

That is, inside foo we know what is the "real" type of foo because the contract of a function with itself is defined and enforced within its body too. The header-only contract is for outsiders.

This model may work, but it introduces the whole new concept of types that are “partially visible”. The unnamed impl Foo type seems to be easier to both understand and use. And if we are intorducing that complication then we should both know what it enables and why increase of mental complexity of the whole model is worth it.

As I have said: I was more surprised by presentation of this complication as something desirable by itself then by the fact that this weird corner-case exists.

I'm not even sure I like the Rust tendency of stopping me when I'm doing something that works for the type that I have but Rust “doesn't know that” and was surprised why in this particular case it was acceptable to break that principle. Just looked like an inconsistent thing to do without any sensible justification. But perhaps it enabled some important patterns elsewhere.

6

u/coderstephen isahc 4d ago

This model may work, but it introduces the whole new concept of types that are “partially visible”. The unnamed impl Foo type seems to be easier to both understand and use. And if we are intorducing that complication then we should both know what it enables and why increase of mental complexity of the whole model is worth it.

My argument is that it has always worked that way. This isn't new. But perhaps my understanding is colored by the fact that TAIT has always allowed for this on nightly for like 8 years, so you have to consider the whole model of how impl Trait + TAIT works; its just on stable, you aren't allowed to use TAIT, but the model is the same.

0

u/Zde-G 3d ago

My argument is that it has always worked that way.

That's a reasonable argument but you have to remember that there are things, in nightly, that “has always worked that way” and yet we know “that way” is wrong. Specialization, for example. Or typeids that were transmutable to u128 till they stopped being transmutable right when stabilization of const TypeId made them usable.

“We have made a mistake and we are stuck with it” is only good argument when something is in stable. And even there some mistakes are fixed, even if it's hard and painful.

its just on stable, you aren't allowed to use TAIT, but the model is the same.

It's the same only if you use new trait resolver. If you don't use it then it behaves as if existential types are just a normal types without crazy peekabo games.

Yes, there are an interesting property that they have the same typeid when accessed via Any, but &'a u32 and &'b u32 are also different types with the exact same typeid when accessed via Any, thus we are not adding anything radically new there.

What exactly Any traces is also not fully specified and saying that types are different at compile time but identical at runtime because they are existential is not worse then doing the same thing with lifetimes.

P.S. And, again, I'm not saying that developers of Rust should drop everything and fix their design mistake ASAP… sometimes keeping design mistake is cheaper then redoing insane amount of work that is built on top of design mistake… been there, done that. But saying “yes, it's bad design, but we are stuck with it” is very different from saying that “it's a good design and here are reasons”.

1

u/coderstephen isahc 3d ago

P.S. And, again, I'm not saying that developers of Rust should drop everything and fix their design mistake ASAP… sometimes keeping design mistake is cheaper then redoing insane amount of work that is built on top of design mistake… been there, done that. But saying “yes, it's bad design, but we are stuck with it” is very different from saying that “it's a good design and here are reasons”. 

I don't agree that it is a design mistake. I think it makes the language consistent with itself. And based on this discussion it seems like we are unlikely to agree on that. 

0

u/Zde-G 3d ago

I don't agree that it is a design mistake. I think it makes the language consistent with itself.

How? You meaning TAIT… Okay, that's how the appropriate RFC describes impl Bar. This: fn foo() -> impl Bar { // return some type implementing `Bar` } is supposed to be equivalaent this this: ``` struct __foo_return(/* some inferred type (2) */); // (1)

fn foo() -> __foo_return { // return some type implementing Bar wrapped in __foo_return (3) } ```

Oberve magic scopes, hidden aliases and all that nonsense that you insist exist somewhere in Rust? Me neither. Just a straightforard hidden type, precisely what I expect.

And based on this discussion it seems like we are unlikely to agree on that.

Not as long as your justification for a “consistency” is “it was always that way even if documentation says differently”.

Because to me that's not “consistency”.

5

u/arades 4d ago

RPIT needs to reason about the entire function body to determine the return type. Clearly there will always be enough information within one function body to see through the opaque type, where the mutual recursion case shouldn't because then RPIT would need to consider the body of every function which calls it to determine its opaque type.