Trait bounds are not yet enforced in type definitions

f6ff5b2
Opened by Markus Westerlind at 2024-12-21 05:10:52

Opening an issue since I could not find any information on when/if this will be enforced. Would be really useful for some generalizing I am trying for parser-combinators which I can't do currently without complicating the API.

Specifically I'd like to use a trait bound in a type definition to access an associated type to avoid passing it seperately but due to this issue I simply get a compiler error.

Simplified code which exhibits the error:

trait Stream {
    type Item;
}

trait Parser {
    type Input: Stream;
}

fn test<T>(u: T) -> T
    where T: Parser {
    panic!()
}

type Res<I: Stream> = (I, <I as Stream>::Item);

impl <'a> Stream for &'a str { type Item = char; }
impl <I> Parser for fn (I) -> Res<I> where I: Stream { type Input = I; }

//Works
//fn f(_: &str) -> (&str, char) {

//Errors
fn f(_: &str) -> Res<&str> {
    panic!()
}

fn main() {
    let _ = test(f as fn (_) -> _);
}

  1. At first glance this is probably related to the internal messy-ness of bounds vs. where-clauses. I'm not sure if the predicate constraints for type aliases are propagated correctly. You should be able to use a newtype as a work around:

    struct Res<I: Stream>((I, <I as Stream>::Item))
    

    Seems to work just fine on playground with the newtype: http://is.gd/krOTZv.

    cc @nikomatsakis

    Jared Roesch at 2015-02-03 22:35:15

  2. I would use a newtype but since the type I want to define is a specialized Result type that isn't possible in my case. I didn't really mention that since it is tangential to the bug itself.

    Markus Westerlind at 2015-02-03 23:07:04

  3. Looks like warnings are now generated for this case, but not consistently[0].

    Should this be a compile-time error instead?

    [0] https://github.com/rust-lang/rust/issues/20222#issuecomment-95012057

    Tamir Duberstein at 2015-04-22 03:29:21

  4. What is the status on trait bounds in type definitions? I worked around it the best I could for combine but it won't be as clean as it could be until bounds are allowed.

    Markus Westerlind at 2015-08-09 19:54:48

  5. Where bounds are allowed, they're not checked properly.

    I'm using this one and it compiles from Rust 1.0 or later

    pub type MapFn<I, B> where I: Iterator = iter::Map<I, fn(I::Item) -> B>;
    

    bluss at 2015-11-26 23:03:22

  6. Nominating for discussion. Seems like backcompat hazard we should consider trying to address with a feature gate.

    Niko Matsakis at 2015-12-15 20:03:00

  7. Since the bounds provide writing types you couldn't do otherwise, they are very useful. A feature gate would be a breaking change without recourse(?).

    bluss at 2015-12-15 20:30:09

  8. triage: P-medium

    I think this might be not that hard to fix though!

    Niko Matsakis at 2015-12-17 21:33:57

  9. Unless I'm missing something, this isn't a huge problem because bad things will still fail to type check right? For example the following doesn't compile.

    trait Foo {
        fn foo(&self) -> u32 {
            1
        }
    }
    
    type Bar<F: Foo> = F;
    
    fn foo(f: Bar<&str>) -> Bar<&str> {
        f
    }
    
    fn main() {
        let bar: Bar<&str> = "";
        println!("{:?}", foo(bar.foo()));
    }
    

    Nathan Lilienthal at 2016-02-09 06:39:56

  10. @nixpulvis I think the argument for checking type definitions is analogous to our philosophy about other polymorphic code: we don't do C++ style post template-instantiation type-checking; instead we check the polymorphic code at the definition site (even though both strategies are sound)

    Or maybe you are just saying the backwards incompatibility hit won't be that bad? (To which I say: famous last words. .. ;)

    Felix S Klock II at 2016-02-09 08:37:49

  11. @pnkfelix I was really just posting to confirm that I wasn't missing something. The warning sounds a bit scarier than it is in reality. I think this is tangental to #30503, as I do feel having type aliases which aren't check could be useful.

    Nathan Lilienthal at 2016-02-09 15:07:51

  12. So this issue is a regular annoyance. It's worth trying to figure out just what we want to do, however!

    The key problem is that type aliases are "invisible" to the type checker. You could almost just remove where-clauses from them altogether, but that we need them to resolve associated types in things like type Foo<I: Iterator> = I::Item. Interestingly, in cases like that, if in fact Foo<X> is applied to some type X where X: Iterator does not hold, the code should still fail to compile today, despite that warning that the trait bound is "unenforced". This is because the trait bound is implied by the type itself, which expands to <I as Iterator>::Item.

    Enforcing all type bounds could be tricky, but may well be readily achievable. There is some backwards compatibility to worry about. I guess how we would implement it would be to extend the "item type" associated with a type item. This might be a small patch, actually.

    Another option would be to detect if the bound is needed (i.e., consumed by an associated type), and warn only if that is not the case. That would just be accepting that we allow where-clauses here even when they are not needed and will not be unenforced. Doesn't feel very good though.

    Seems like we might want to start by trying for the "better" fix, and then measuring the impact...?

    Niko Matsakis at 2016-08-25 16:49:10

  13. Interestingly, in cases like that, if in fact Foo<X> is applied to some type X where X: Iterator does not hold, the code should still fail to compile today, despite that warning that the trait bound is "unenforced". This is because the trait bound is implied by the type itself, which expands to <I as Iterator>::Item.

    @nikomatsakis Would it be possible to remove that warning for these cases then? It's quite confusing.

    Ingvar Stepanyan at 2017-02-17 16:08:04

  14. @RReverser I agree. I just added a to do item to try and investigate disabling the warnings.

    Niko Matsakis at 2017-02-21 21:59:30

  15. Workaround: if you're using an associated type on the type parameter, you can replace type T<U: Bound> = V<U::Assoc> with type T<U> = V<<U as Bound>::Assoc>, which avoids this warning until there's a way to disable it.

    (Interestingly, even #[allow(warnings)] fails to disable this warning.)

    James Kay at 2017-03-15 17:48:32

  16. (Interestingly, even #[allow(warnings)] fails to disable this warning.)

    Yeah, was exactly my problem. Thanks for the workaround, I'll give it a try!

    Ingvar Stepanyan at 2017-03-15 17:54:35

  17. @Twey thank you, that did it!

    Ingvar Stepanyan at 2017-03-20 11:22:16

  18. (Interestingly, even #[allow(warnings)] fails to disable this warning.)

    With https://github.com/rust-lang/rust/pull/48326 merged, this warning is now a normal lint and can be disabled with #[allow(warnings)].

    However, what I did not realize is that the bounds are actually not entirely ignored -- type T<U: Bound> = U::Assoc compiles while type T<U> = U::Assoc does not. I'm currently preparing a PR to at least improve the wording (and sorry for the mess), but I am wondering what the best strategy is here.

    Given that this bug is unfixed for three years, it seems overly optimistic to expect a proper implementation of enforcing these bounds any time soon. Also, I think the warning is crucial because currently, the compiler accepts code such as the following:

    use std::cell::Cell;
    type SVec<T: Send> = Vec<T>;
    fn foo(_x: SVec<&Cell<i32>>) {}
    pub fn bar() {
      foo(Vec::new());
    }
    

    This is clearly nonsensical -- a type declared with SVec<T: Send> should only be allowed to contain Send types. This is also a footgun, because a user could expect the compiler to actually enforce this bound! I think this code should be a hard error eventually (e.g. in the next epoch).

    The best option I can think of currently is to make people write <T as Trait>::Assoc. That seems to be the best practice as of now. It is slightly surprising that you can use T as Trait without having T: Trait, but that is consistent with being able to write

    struct Sendable<T: Send>(T);
    type MySendable<T> = Sendable<T>; // no error here!
    

    So, if type aliases are ever checked for well-formedness, that would require a transition period anyway.

    Ralf Jung at 2018-03-10 10:41:23

  19. Given that this bug is unfixed for three years, it seems overly optimistic to expect a proper implementation of enforcing these bounds any time soon. Also, I think the warning is crucial because currently, the compiler accepts code such as the following:

    My expectation here has been that we will do the following:

    (1) Implement lazy normalization (underway) (2) In Epoch 2018 (hopefully we get this done in time...), handle type aliases not as "eagerly normalizing" in the type-checker, but rather as "lazy normalizing", just like an associated type (2a) A side-effect would be strict enforcement of bounds (2b) But in Epoch 2015, we would just warn and treat as we do today

    Niko Matsakis at 2018-03-12 15:52:35

  20. The best option I can think of currently is to make people write <T as Trait>::Assoc. That seems to be the best practice as of now. It is slightly surprising that you can use T as Trait without having T: Trait, but that is consistent with being able to write

    Yes, that's probably the most forwards compatible thing you can do, and yes it's somewhat surprising (but consistent). I tend to make an alias in practice just so it is shorter (and to not get warnings):

    type Assoc<T> = <T as Trait>::Assoc;
    type SomethingElse<T> = Vec<Assoc<T>>;
    

    Niko Matsakis at 2018-03-12 15:53:52

  21. (1) Implement lazy normalization (underway) (2) In Epoch 2018 (hopefully we get this done in time...), handle type aliases not as "eagerly normalizing" in the type-checker, but rather as "lazy normalizing", just like an associated type

    Yes, that's probably the most forwards compatible thing you can do, and yes it's somewhat surprising

    Okay, so given that, do you think the approach I took in https://github.com/rust-lang/rust/pull/48909 makes sense?

    Ralf Jung at 2018-03-12 16:37:03

  22. @RalfJung are you asking if your suggestion to use <T as Trait>::Foo form makes sense? I think it does, I mean -- as you said -- we can't exactly break that usage, at least not without some kind of opt-in, for back-compat reasons. I can take a look at the PR in terms of giving an improved suggestion.

    Niko Matsakis at 2018-03-13 16:03:51

  23. @RalfJung are you asking if your suggestion to use <T as Trait>::Foo form makes sense?

    Yes. Originally I intended this to be a step towards turning the lint into a hard error eventually, but of course if this gets implemented properly that's even better. :) I can't imagine how to implement this in a nice backwards-compatible way, but well, you seem to have some idea :D

    Ralf Jung at 2018-03-13 16:24:14

  24. Any progress here lately? (Trying to have a go at implementing trait aliases here.)

    CC @nikomatsakis

    Alexander Regueiro at 2018-10-10 19:15:11

  25. Is there any chance this will be implemented in edition 2021?

    waffle at 2020-09-21 09:34:58

  26. this would be such a juicy feature, is there any chance this will ever be implemented?

    Cedric Schwyter at 2022-05-19 20:37:58

  27. I just encounter a case where depending on conditional compilation, my type alias could change but I wanted to make sure that whatever concrete type was chosen, at the end it will implement a trait. I posted about it here

    I wanted to do something like this:

    #[cfg(target_arch = "x86_64")]
    type MyAlias: MyTrait = A;
    #[cfg(target_arch = "riscv64")]
    type MyAlias: MyTrait = B;
    

    Esteban Blanc at 2022-06-10 18:49:37

  28. To give an update to the people subscribed to this issue, the (incomplete) feature lazy_type_alias implements the expected semantics for type aliases:

    • where-clauses and bounds on type parameters of type aliases are enforced
    • type aliases are checked for well-formedness
    • where-clauses are trailing instead of leading just like with associated types (see also #89122)

    The feature is nominated for stabilization in a future edition (202X). Its tracking issue #112792 basically supersedes this issue in my humble opinion. See also https://github.com/rust-lang/rust/labels/F-lazy_type_alias.

    León Orell Valerian Liehr at 2023-08-25 13:22:48