What does the compiler know about types?


#1

I’ve spent five hours this evening trying to resolve this error:

src/record.rs:61:13: 61:23 error: type mismatch resolving `<R as jmap::record::Record>::Partial == jmap::contact::PartialContact`:
 expected associated type,
    found struct `jmap::contact::PartialContact` [E0271]

I found it in the end. Deep in some of my library code I’d inadvertently returned a concrete type instead of a generic type. It just happened to match up with the may I was calling it so it failed oddly.

Finding the problem was made difficult because because I’ve got some fairly deep parameterised types with associated types, across several modules in two crates. Because this was happening in the course of refactoring a lot of code to use associated types and was the first time I’ve used them, I spent a lot of time trying different ways of expressing what I wanted, to no avail. It didn’t help that I wasn’t able to produce an isolated test case, because the problem was actually nothing to do with any of this code - I had it right, the bug was elsewhere.

Through all of this I found myself thinking that this would have been so much easier if the compiler could tell me what it currently knows about the types involved in a portion of code, and in particular why it expects a particular type and where it got that expectation from.

I’m still fairly new to Rust, and so still at the point where I spend a lot of time responding to type errors by moving things around, changing moves to borrows and back and so forth. Its beginner stuff, so I’m not bothered. But it seems to me that if we’re going to have a language where the type system is so integral to everything, then we need tools to very clearly understand what’s going on.

So what is to be done here? Are there tools to inspect rustc’s view of the world during compile? Are there plans? Are there good strategies for working through type errors that I haven’t learned yet? What else? Discuss!


#2

Aside: In the course of my experimenting I managed to induce a compiler crash. Now I know what’s happening I’ll try and reduce it to a test case. Its been a crazy evening!


#3

I’m not sure about the exact case you have here, but a good technique for debugging type errors (especially if you are relying on a lot of type inference) is to start explicitly naming types for your variables. This helps the compiler give you errors that are closer to where the actual problem is.


#4

Thanks, that’s good advice. Its sort of what I did, except I removed code to try and isolate exactly which bits were involved in rustc’s idea of things.

Still, this shows exactly why a tool showing where rustc got its expectations from would be a good thing. Instead of me putting obstacles in the compiler’s way to get it complain at me, over and over again, why can’t it just say “I expected C, because of B at foo.rs:23, because of A at bar.rs:42”. It must know that in some fashion and it would be lovely if it could tell me.


#5

I think I see what you’re asking for, but I think in practice it would be too verbose, and in practice you’ll get better results by using your knowledge of what you’re trying to code to give the best hints to the compiler.

Here’s a simple example. It’s a bit contrived, but I think it serves as a good illustration:

fn main() {

    let x = vec!(0);

    let mut result = x.iter().map(|e| e * x.len());
    let first = result.next();

    let value: i32 = first.unwrap() - 1;
    println!("Value is {}", value);

}

Compiling this will result in a type error:

a.rs:7:22: 7:40 error: mismatched types:
 expected `i32`,
    found `usize`
(expected i32,
    found usize) [E0308]
a.rs:7     let value: i32 = first.unwrap() - 1;
                            ^~~~~~~~~~~~~~~~~~

But this might be a little confusing – where is a usize coming from?

I think what you are asking for is the compiler to tell us why it thinks the result of first.unwrap() - 1 is a usize. Well, it thinks it’s a usize because it thinks first.unwrap() is returning a usize because first is an Option<T> where T is a usize because result.next() returned an Option<Self::Item> where Self::Item is a usize because map() returned a value of type Map<Self, F>, where F is a FnMut(Self::Item) -> B, etc, etc. There is a lot of inference going on here! and to print it all out doesn’t seem that useful.

But if we go back to our code and start to annotate our variables, we can pretty quickly narrow in on the problem. In this example, I know that I really want to be starting with a vector of signed 32-bit integers, I can make that explicit, instead of implicit:

let x: Vec<i32> = vec!(0);

And now I get a compile error that is must closer to the actual problem:

a.rs:4:39: 4:50 error: the trait `core::ops::Mul<usize>` is not implemented for the type `&i32` [E0277]
a.rs:4     let mut result = x.iter().map(|e| e * x.len());

Now it’s much easier to notice that x.len() is returning a usize. In this case, I fix this problem by casting:

x.len() as i32