(Warning, half a blog-post ahead. I guess I should just make this a full blog post over the week-end, now that I already wrote all this text.^^)
In some sense, I am building two type systems, and I think the confusion here is coming from that. First of all, I am building a “syntactic type system” (usually I’d just call this the type system), which is an attempt to formally capture (an over-approximation of) the safe code that Rust accepts. This looks a lot like most type systems do: There’s a grammar of legal types, there are some contexts tracking the types of variables and the lifetimes, there are inference rules telling you e.g. when you can load from a pointer, when you can call a function, and so on.
This type system is an over-approximation because I provide more flexibility than rustc does when it comes to lifetimes. However, a design goal of this type system is for every rustc-accepted program not using unsafe blocks (that falls into the fragment I cover) to be (syntactically) well-typed. Notice that the program can call external, unknown functions; this corresponds to type-checking the program in a non-empty context. So, we can derive typing judgments like “If we assume that Cell::new, Cell::get, … have the type given in libstd, then we can (syntactically) derive that this program is well-typed”.
Next, there is a “semantic type system”, which I usually call the “(semantic) model of the Rust type system”. This model defines, for every type present in the syntactic system, a formal contract that values of the given type have to satisfy. In particular, for function types, the contract says that if all arguments satisfy their contract (as given by the argument types), then the function must execute safely and return something satisfying the contract of the return type.
Notice how in defining the contract for function types, we only cared about the contract of the argument and return types – the “actual types” (as in, their syntactic representation as an instance of the grammar) does not matter at all! This leads us to the idea that really, any contract is a valid “semantic type”. This idea is incredibly powerful. We can now write down the contract saying “this is a legal 32-bit integer value representing a prime number”. Let’s call this type i32even. This type does not exist syntactically, but since we have defined its contract, we now also know the contract e.g. for types like fn(i32even) -> i32even or Option<i32even>. Things compose.
So what I do to verify unsafe code is that I define semantic types like Cell<T> that do not exist syntactically. I can then compose these types with things like function types to obtain the contract of fn(T) -> Cell<T>, and then I can show that Cell::new actually satisfies this contract. This is the sense in which my “types include richer invariants”, and the sens in which I can “type” unsafe code – I can semantically type it, but not syntactically. However, since syntactic well-typedness implies semantic well-typedness, the two notions compose together.
This is absolutely correct. However, it may be that “two-phase borrows” also constitute a significant change.
However, “lifetimes with holes” are certainly way further from what my mental model of a lifetime is.[quote=“nikomatsakis, post:24, topic:4886”]
let mut foo: Box<Vec<u32>>;
let mut bar: Box<Vec<u32>>;
let mut x: &mut Box<Vec<u32>> = &mut foo;
let y = &mut *x;
x = &mut bar;
y.push(…); // pushes into foo
Currently, we do not accept this code, which leads to some contortions around loops, but I would like to do so (I seem to recall us talking about this one time, right?). I’ve not thought very deeply about it just yet.
[/quote]
Ah, yes we talked about this. I do not understand however how this is related to nested method calls or two-phase borrowing. In my eyes, the only problem here is that rustc thinks x is being reborrowed, but actually it would be much smarter if it was moved instead. Actually, a tiny modification makes rustc accept this program (as has been pointed out to be by Jacques-Henri Jourdan):
let mut foo: Box<Vec<u32>>; let mut bar: Box<Vec<u32>>;
let mut x: &mut Box<Vec<u32>> = &mut foo;
let y = &mut *{x}; // the curly braces make rustc consider this a move out of x, i.e., x is uninitialized after this line
x = &mut bar;
y.push(...);