Exclusion Typing
The structural rules of lifetimes#
I have been developing an implementation of references for my type system, drawing from Rust’s lifetimes. This post is a quick description of the design I’m running with at the moment, and relies on concepts from Rust about lifetimes and mutable references themselves - for anyone who wants to learn more about those I’d recommend the Rustonomicon, possibly Oxide an approachable subset of rust described in typing rules, and RustBelt is still relevant as a very thorough description of rust’s borrow checker - in many ways, my system behaves as the type theory of the logic used in rustbelt.
I had a few goals here:
- Investigate how to resolve the long standing feature requests in Rust borrow checking, in particular:
- Supporting self references within structures
- Looking into whether we can make
Pinsignatures a more native part of the lifetime checker - The polonius signature, aka the
HashMap::get_or_insertapi. #[may_dangle]as a first class feature- Partial initialization that can cross function boundaries
- Owning references
- Mutability parametricity
- Provide a description of borrow checking that is easier for language developers to follow, and make it easier to implement lifetimes in existing frontends.
- Build a system that can accurately preserve the invariants on separation logic assertions
and can support extensions well, like dependently typed lifetimes,
abstracted view types,
two phase borrows,
group borrowing,
and GhostCell. In this context,
Cellalso works out as an instance of GhostCell. - Develop a syntactic typing discipline that captures all necessary details in traditional typing judgements, using structural rules to model sharing. In particular, I want a type checker that doesnt require semantic types or separate reasoning on extents or the control flow graph separately from the type checking inference rules.
That last paragraph in particular uses type theory terminology that might be new to some readers. I’d like to share a more theory-focused writeup soon, but this post is focused on how programs are written with these exclusion types.
I’ve encountered exciting answers to all of these points, so let’s have a look at the system!
We’ll begin with a nice classic example
fn swizzle<'l>(list: &'l mut Vec<u32>) {
let v = &list[list.len() - 1];
list.push(4);
print(*v);
}
A function’s body gets permission to use its parameters as locals, so we set up the typing context:
named l: _, local list: &l mut Vec<u32>, perm_list: exl(?list, l), live l
We also have this extra property in the context: live l. In rust, lifetime parameters on functions
are always live. This is explicitly tracked in our typing context because we need to be able to shrink
the liveness requirement for expressions that touch less of the mutable environment.
The other new binding is the permission to access the list local: exl(?list, l). This is live, because
it depends on the live permission l, and by passing this permission around we can build expressions that use the list.
Now lets go through the lines of the function and see how they change the context:
| Line | Statement | perm_list |
perm_list₁ |
shr_pl |
v |
perm_list₂ |
|---|---|---|---|---|---|---|
| 1 | |
|
||||
| 2 | |
|
|
|
|
|
| 3 | |
|
|
|
|
|
The variables are listed out horizontally, and their state in the context is in the cells of each row.
For example, the v variable is introduced on line 1 and becomes visible in line 2’s context with the &pl1 u32 type.
The first thing that this should highlight is that permission types in the context can change.
That is to say, they’re flow typed and their types depend on the control flow of which operations have led to the current expression.
This is the critical element that the type checker uses to see the conflict here:
Line 2 consumes the perm_list1: exl(?list, l) to push to the list.
It restores the exl(?list, l) of course, reborrowing the permission to use in the push method,
but we’ve already broken the dependency of the reference that was made on line 1.
Now, when we go to use the &pl1 u32 on line 3, we’ll find that the permission is damaged.
Understanding this pattern is the core intuition of the system - this would likely be a sufficient explanation for tutorial documentation to get users started in the language. Of course, for implementers it leaves many questions.
- How do these permissions split to access individual fields
- How do we actually compute the types of shared and exclusive references, forming reborrows and shortening lifetimes
- How do we make function calls with these permissions
.. see more here later <3