TAIT: Values of associated constants are not bounded by defining scopes

I couldn't find out if this was expected behavior for type in impl trait. Maybe someone can complement my knowledge on this unstable feature. 'Existential values' in particular are not discussed in the feature RFC or other documents I could turn up. However given a strong analogue to associated types, I'm assuming this code compiles, unintentionally.


mod b {
    pub trait PtrSize {
        const PtrSize: u16;
    }
    
    struct A;
    
    impl PtrSize for A {
        const PtrSize: u16 = core::mem::size_of::<usize>() as u16;
    }
    
    pub type Platform = impl PtrSize;
    
    fn _internal() -> Platform {
        A
    }
}

fn main() {
    let _: [u8; <b::Platform as b:: PtrSize>::PtrSize as usize] = [0; 8];
}

I expected to see this happen: The code fails to compile. The impl does not mention any bound or constraint on the value of the associated constant. The constant is thus not part of the explicit interface of the existential type Platform. It should be invalid to depend on the value of this constant in the manner within type equivalence checking. Compare this to the equivalent code of associated types:

mod a {
    pub trait Iterator {
        type Item;
    }
    
    struct A;
    
    impl Iterator for A {
        type Item = u8;
    }

    pub type T = impl Iterator;
    
    fn _internal() -> T {
        A
    }
}

fn main() {
    let _: <a::T as Iterator>::Item = 0u8;
    
/*
    error[E0308]: mismatched types
  --> src/main.rs:40:42
   |
14 |     pub type T = impl Iterator;
   |                  ------------- the expected opaque type
...
40 |     let _: <a::T as a::Iterator>::Item = 0u8;
   |            ---------------------------   ^^^ expected associated type, found `u8`
   |            |
   |            expected due to this
   |
   = note: expected associated type `<T as a::Iterator>::Item`
                         found type `u8`
   = help: consider constraining the associated type `<T as a::Iterator>::Item` to `u8` or calling a method that returns `<T as a::Iterator>::Item`
*/

}

Instead, this happened: the code compiles on some platforms, and fails type checking on others.

Meta

version: 1.75.0-nightly
(2023-11-06 189d6c71f3bb6c52113b)

Addendum

Besides the issue, it turns up a very peculiar question: should we have an 'impl'-like feature for constants outright? In particular in reference to a long-standing problem with SemVer / stability of const and const fn. The following doesn't 100% relate to the issue itself but explains my thoughts of reasoning on the differences between associated / constant type vs. value treatment by the compiler.

One could make the case that each constant declaration is akin to constraining the value type to a singleton value, in the form of a common canonical type for each particular comparable value. An existential value would instead treat this as a new type instead of the canonical one. (Still with an implied bound that allows those types in the constructor of arrays etc, let's not stretch it too far). Now the question is whether one could and should utilize the existing declaration to decide.

It's not quite clear what const fn is then doing under this analogy. Sure, functions are just relations of input values to output values but traits do not work as an analoge. Rust's type instances are for a single Self type and tuples are very ad-hoc for this equivalence.

const A: usize = const { B + C };

This would be solving a very complicated type equivalence, where B and C are known, and together determine the type of the result. The case of overflow can be covered by there not necessarily existing an instance of the mentioned operation families. Let's use as a notational framework the idea that operations are traits taking N additional arguments, and with an associated Output type. Then some canonical internal type (let's call it Termination) is utilized to denote the success operations by (not) providing an instance of the operation traits for it.

// Notational equivalence to the above.
type A = <Termination as Plus<B, C>>::Output;

Under the combined type-analogy, code blocks are just transitive closure of type resolution. And now obviously each of these results could be made existential instead. Note that const-fn declarations are just code blocks and thus, under this analogy, just transitive type resolution. Does this match our usual privacy intuition? It's usually that the body of a function is an implementation detail but here is clearly gets made into a public part of the declaration. That's a little more formalism around the above mentioned long-standing SemVer problem.

Aside: All said, I don't think it's possible, feasible, or desired to change any of the existing semantics of const fn. What would it even be like to it into an 'existential' form? Keep in mind that even other constant declarations must not be able to observe the specific value of the result then. This includes failure to resolve further which would ruin the transitive translation of blocks. Unless the existential function result comes with its own set of additional guarantees.

Maybe the surrounding scope should be able to observe the body? In a similar manner as const-computation in generic type parameters is allowed / declared as an input dependency? It seems cumbersome and that there should be some cute syntax just waiting to be discovered.

Isn't there some fundamental connection to different type equivalence and Extensional type theory here? (Or is it impredicative?)

The equivalent code written with a generic errors with

error: generic parameters may not be used in const operations
  --> src/lib.rs
   |
LL |     let _: [u8; <T as b::PtrSize>::PtrSize as usize] = [0; 8];
   |                  ^ cannot perform const operation using `T`
   |
   = note: type parameters may not be used in const expressions
   = help: use `#![feature(generic_const_exprs)]` to allow generic const expressions

so opaque TAIT should be getting a similar error (outside of the transparent defining scope). Just replace "{generic|type} parameters" with "opaque type aliases". Even with the feature and WF bound, it still errors with

error[E0308]: mismatched types
  --> src/lib.rs
   |
LL |     let _: [u8; <T as b::PtrSize>::PtrSize as usize] = [0; 8];
   |                                                        ^^^^^^ expected `<T as b::PtrSize>::PtrSize as usize`, found `8`
   |
   = note: expected constant `<T as b::PtrSize>::PtrSize as usize`
              found constant `8`

so the machinery for opaque/symbolic consts does exist, at least partially.


On the aside/addendum, I've mentioned an idea of "fickle consts" before, where unifying with a "fickle const" or otherwise using it in types outside of universal usage would warn. The "fickle" property (effect?) would propagate through const fn in a simple way: if the fn uses a fickle const but isn't itself marked fickle, it'd warn, and if a fickle parameter is given, the output is considered fickle.

Curious, but not exactly unexpected, that the generic parameter is rejected as expected. Should it be written as a bug report then and would you consider that comparison to be more convincing?

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.