Try   HackMD

const eval in the type system, Reveal::All

Reveal::All allows us to look at specializable associated types and into opaque types. Looking into an opaque type when typechecking or borrowchecking a function in its defining scope causes a query cycle.

Const evaluation currently uses Reveal::All, even when evaluating const arguments during typeck. Should this keep being the case?

The defining scope of an RPIT is only the defining function, so this only causes query cycles if typechecking the defining function requires CTFE using that defining function. If CTFE tries to evaluate the defining function, this feels expected to cycle, if it merely converts the defining function to a function pointer, query cycles seem at least somewhat expected.

For TAIT the defining scope is a whole module, so query cycles may be a bit less expected. The cycle errors still looks fairly understandable, see this example, so even that isn't too horrible.

How can CTFE cause cycles when revealing opaque types?

The most frequent ways such cycles can happen is if CTFE actually uses a value of the opaque type. Alternatively, it could convert a function with the opaque type in the signature to a function object.

A weirder way to get cycles is by using an opaque type in the where-bounds of some const evaluation. Using an opaque type in the where-bounds isn't really too meaningful, so this shouldn't be something people frequently encounter.

It should also be possible, to get a cycle by using a TAIT for an associated type which is then revealed when trying to resolve an instance during CTFE inside of the defining scope. I was not able to quickly write an example for this, so this case should also be very infrequent.

When adding implied bounds to the environment we may get opaque types into the param_env which would result in cycles during CTFE as well. These cycles can be avoided however[1].

should CTFE reveal opaque types if it didn't cause cycles?

The idea of opaque types is to hide the underlying type when used outside of the defining scope. By revealing that type during CTFE during typeck, changing the underlying type can cause compilation errors. This adds additional ways in which changing the underlying type can be a breaking change.

The most relevant use would be std::mem::size_of::<OpaqueType>() as an array length. This feels desirable. As size_of is already not part of our stability guarantees, this feels like acceptable.

As typeck of the code used by CTFE still doesn't use the underlying type, observing the underlying type of opaque types is pretty much restricted to things already observable at runtime. The only difference is that these changes can cause compilation errors when encountered during CTFE.

Things like const fn returns_iter() -> impl Iterator<Item = usize> feel like they should be usable in type system constants and rely on the underlying type of this RPIT.

It generally feels like revealing the underlying type during typesystem CTFE is desirable.

How to deal with these query cycles?

All of them are either expected or very rare.

Instead of query cycles, we could have (imperfect) checks to emit better errors, but this doesn't feel too useful and is something we can always do at a later point if necessary.

We could completely stop revealing opaque types which has been tried in #101478 but caused breakage on crater. As revealing opaque types is generally desirable this feels undesirable.

An alternative solution is to lazily reveal opaque types whenever their underlying value is actually needed, which has been experimented with in #102657. When lazily revealing an opaque type in some value we also have to reveal it in the ParamEnv for the trait system to remain consistent. That is pretty fragile however as we have to remember which opaque types in the ParamEnv were revealed whenever we keep using the revealed value somewhere. It is unclear how to best guarantee this. This solution makes me uncomfortable and has to "infect" large parts of CTFE and other parts of the compiler used by CTFE.

I therefore think we should keep using Reveal::All in CTFE, even during typeck.


  1. We can erase implied outlives-bounds them from the ParamEnv before CTFE if necessary, as CTFE must not care about regions. I don't yet know how to deal with implied trait bounds. ↩︎