Observation: local variables of Rust are also addressable.
Simplification: treat local variables as heap-allocated, i.e. pointer types.
bool
, int
Note: Types of local variables of Rust programs are all pointer types.
Not describing in detail due to time limit.
Unsafe types opts out of typing rules, so no way to prove safety from the rules!
Solution: take the semantic approach.
What is a type? a certain set of values, or, a predicate on values.
Example: in lambda calculus with booleans,
What is a type? a certain set of values, or, a predicate on values.
What predicate? using which logic?
Separation Logic!
A logic that describes a heap.
Separation logic is a substructural logic.
Example: Consider the following heap:
Also, after an implication is applied to a value, the value is consumed.
A logic that describes a heap.
Separation logic is a substructural logic.
Perfect logic to describe Rust types!
P.S. Not every separation logic is affine, but the one used in Rustbelt, i.e. Iris, is.
Associate every type
(Ignore why we name it "own" for now, will be explained later.)
Notice:
Recall: types that can be freely duplicated via bit-wise copy are Copy types.
Consequence: given
Proposition that can be freely copied (i.e.
Therefore, the interpretation of Copy types can always be written as:
E.g. for
Associate every type
Notice:
Recall: types that can be duplicated via bit-wise copy are Copy types.
Try to duplicate
Property: for any Copy type
What's the difference between mutable references and owned pointers?
Recall how we tracked references in Rust type system: lifetimes.
Solution: lifetime logic.
Intuition:
We'll head back to the precise definition of lifetime logic later.
Meaning: ownership of a value of type
Question: what can we say about shared references universally?
Not so interesting! Is that true?
Interior mutability!
Many types have their own sharing reference behavior deviating from the universal rules!
Solution: let every type define their own sharing reference behavior, i.e. sharing predicate.
Leveraging the sharing predicate to describe the behavior of shared references.
Leveraging the sharing predicate to describe the behavior of shared references.
Laws for sharing predicates:
Recall: for Copy types
Define:
Define: for Copy types
Recall: for mutable references,
Intuition: † fractured borrow
†: no need to understand the details. Just treat them as full borrows.
Things we used but not defined yet:
let mut v = Vec::new();
v.push(0);
{ // <- Vec<i32>
let mut head = v.index_mut(0);
*head = 23;
}
println!("{:?}", v);
given that
index_mut: for<'a> fn(&'a mut Vec<i32>, usize) -> &'a mut i32
index_mut: for<'a> fn(&'a mut Vec<i32>, usize) -> &'a mut i32
{
let mut head = v.index_mut(0); // <- Vec<i32>
'a
'a
index_mut: for<'a> fn(&'a mut Vec<i32>, usize) -> &'a mut i32
{
let mut head = v.index_mut(0); // <- Vec<i32> * [κ] * ([κ] -* [†κ])
need to provide 'a
Can always
index_mut: for<'a> fn(&'a mut Vec<i32>, usize) -> &'a mut i32
{
let mut head = v.index_mut(0); // <- &κ mut Vec<i32> * [κ] * ([†κ] -* Vec<i32>) * ([κ] -* [†κ])
need to provide 'a
(done)
need to pass a mutable reference of lifetime 'a
Given an owned resource
It's important for
Consider
But for
What if
Whatever we do about
index_mut: for<'a> fn(&'a mut Vec<i32>, usize) -> &'a mut i32
{
let mut head = v.index_mut(0); // <- inside `index_mut`
&κ Vec<i32>
into the accessed &κ i32
and the rest &κ Vec<i32>
&k i32
to the caller, and drop the restindex_mut: for<'a> fn(&'a mut Vec<i32>, usize) -> &'a mut i32
{
let mut head = v.index_mut(0); // <- inside `index_mut`
&κ Vec<i32>
into the accessed &κ i32
and the rest &κ Vec<i32>
&κ i32
to the caller, and drop the rest{
let mut head = v.index_mut(0); // <- &κ mut i32 * [κ] * ([†κ] -* Vec<i32>) * ([κ] -* [†κ])
let mut head = v.index_mut(0);
*head = 23; // <- i32 * (i32 -* &κ mut i32 * [κ]) * ([†κ] -* Vec<i32>) * ([κ] -* [†κ])
Need to access the resource of mutable reference head
.
Given a full borrow
It's important to return things you borrowed!: lifetime token is such a certificate.
*head = 23; // <- &κ mut i32 * [κ] * ([†κ] -* Vec<i32>) * ([κ] -* [†κ])
}
*head = 23;
} // <- [κ] * ([†κ] -* Vec<i32>) * ([κ] -* [†κ])
*head = 23;
} // <- ([†κ] -* Vec<i32>) * [†κ]
*head = 23;
} // <- Vec<i32>
Fractured borrow
Intuition:
Typing judgments are defined as
After the instruction, the type context is updated to
Interpretation of typing judgments:
FTLR (Foundamental Theorem of Logical Relations):
Syntactic typing rules are sound w.r.t. semantic typing rules.
Adequacy: a semantically well-typed program never gets stuck (no invalid memory access or data race).
Collary: every rust program that consists of syntactically well-typed safe code and semantically well-typed unsafe code, is safe to execute.
Example: Mutex is a product of flag (true: locked, false: unlocked) and the resource.