Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b771e7f
Initial draft of the 'borrow checker invariants' section
tall-vase Aug 27, 2025
3831858
Elaborate more on the cryptography example
tall-vase Aug 27, 2025
a81b55a
Begin expanding the slide notes, rename the external resources sectio…
tall-vase Sep 1, 2025
0b4318f
Get this section into a 'review ready' state.
tall-vase Sep 1, 2025
728af30
Reset all minutes on the borrowck invariants section to 0
tall-vase Sep 1, 2025
a37a272
General editing pass based off feedback
tall-vase Sep 8, 2025
88852f2
Minor editing
tall-vase Sep 9, 2025
0aa4f21
Another editing pass
tall-vase Sep 11, 2025
37e69fe
minor grammar edit
tall-vase Sep 11, 2025
595ca8e
Another editing pass
tall-vase Sep 15, 2025
2a88748
Formatting pass
Sep 22, 2025
31284d6
Merge branch 'main' into idiomatic/typesystem-borrowchecker
tall-vase Sep 22, 2025
e7f874b
fix test errors, make sure compilation succeeds after erroneous lines…
Sep 24, 2025
808de4f
Address lints
Sep 24, 2025
85b70f0
Address lints
Sep 24, 2025
44dff2a
Apply suggestions from code review
tall-vase Oct 10, 2025
ebf00a2
Partially address feedback
Oct 10, 2025
7b72587
Add TODO
Oct 10, 2025
9f49ba5
Formatting pass
Oct 10, 2025
6fcc471
Apply suggestions from code review
tall-vase Oct 13, 2025
2d3f915
Further address feedback and implement the phantomdata slide
Oct 13, 2025
26cfe2b
Apply suggestions from code review
tall-vase Oct 21, 2025
d55ce18
Make some comments doc comments
tall-vase Oct 23, 2025
fc3f3de
Address latest structural feedback
Oct 29, 2025
a1b3a53
Merge branch 'main' into idiomatic/typesystem-borrowchecker
tall-vase Oct 29, 2025
d8fb34a
Remove compile_fail marker
Oct 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,12 @@
- [Serializer: implement Struct](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/struct.md)
- [Serializer: implement Property](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/property.md)
- [Serializer: Complete implementation](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/complete.md)
- [Borrow checking invariants](idiomatic/leveraging-the-type-system/borrow-checker-invariants.md)
- [Generalizing "Ownership"](idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md)
- [Single-use values](idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md)
- [Aliasing XOR Mutability](idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md)
- [PhantomData and Types](idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-01-types.md)
- [PhantomData and Lifetimes](idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-02-lifetimes.md)
- [Token Types](idiomatic/leveraging-the-type-system/token-types.md)
- [Permission Tokens](idiomatic/leveraging-the-type-system/token-types/permission-tokens.md)
- [Token Types with Data: Mutex Guards](idiomatic/leveraging-the-type-system/token-types/mutex-guard.md)
Expand Down
108 changes: 108 additions & 0 deletions src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
---
minutes: 15
---

# Using the Borrow checker to enforce Invariants

The borrow checker, while added to enforce memory ownership, can be leveraged
model other problems and prevent API misuse.

```rust,editable
/// Doors can be open or closed, and you need the right key to lock or unlock
/// one. Modelled with a Shared key and Owned door.
pub struct DoorKey {
pub key_shape: u32,
}
pub struct LockedDoor {
lock_shape: u32,
}
pub struct OpenDoor {
lock_shape: u32,
}

fn open_door(key: &DoorKey, door: LockedDoor) -> Result<OpenDoor, LockedDoor> {
if door.lock_shape == key.key_shape {
Ok(OpenDoor { lock_shape: door.lock_shape })
} else {
Err(door)
}
}

fn close_door(key: &DoorKey, door: OpenDoor) -> Result<LockedDoor, OpenDoor> {
if door.lock_shape == key.key_shape {
Ok(LockedDoor { lock_shape: door.lock_shape })
} else {
Err(door)
}
}

fn main() {
let key = DoorKey { key_shape: 7 };
let closed_door = LockedDoor { lock_shape: 7 };
let opened_door = open_door(&key, closed_door);
if let Ok(opened_door) = opened_door {
println!("Opened the door with key shape '{}'", key.key_shape);
} else {
eprintln!(
"Door wasn't opened! Your key only opens locks with shape '{}'",
key.key_shape
);
}
}
```

<details>

- The borrow checker has been used to prevent use-after-free and multiple
mutable references up until this point, and we've used types to shape and
restrict use of APIs already using
[the Typestate pattern](../leveraging-the-type-system/typestate-pattern.md).

- Language features are often introduced for a specific purpose.

Over time, users may develop ways of using a feature in ways that were not
predicted when they were introduced.

In 2004, Java 5 introduced Generics with the
[main stated purpose of enabling type-safe collections](https://jcp.org/en/jsr/detail?id=14).

Since then, users and developers of the language expanded the use of generics
to other areas of type-safe API design.
<!-- TODO: Reference how this was adopted -->

What we aim to do here is similar: Even though the borrow checker was
introduced to prevent use-after-free and data races, it is just another API
design tool. It can be used to model program properties that have nothing to
do with preventing memory safety bugs.

- To use the borrow checker as a problem solving tool, we will need to "forget"
that the original purpose of it is to prevent mutable aliasing in the context
of preventing use-after-frees and data races.

We should imagine working within situations where the rules are the same but
the meaning is slightly different.

- This example uses ownership and borrowing are used to model the state of a
physical door.

`open_door` **consumes** a `LockedDoor` and returns a new `OpenDoor`. The old
`LockedDoor` value is no longer available.

If the wrong key is used, the door is left locked. It is returned as an `Err`
case of the `Result`.

It is a compile-time error to try and use a door that has already been opened.

- Similarly, `lock_door` consumes an `OpenDoor`, preventing closing the door
twice at compile time.

- The rules of the borrow checker exist to prevent memory safety bugs, but the
underlying logical system does not "know" what memory is.

All the borrow checker does is enforce a specific set of rules of how users
can order operations.

This is just one case of piggy-backing onto the rules of the borrow checker to
design APIs to be harder or impossible to misuse.

</details>
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
minutes: 15
---

# Mutually Exclusive References / "Aliasing XOR Mutability"

We can use the mutual exclusion of `&T` and `&mut T` references for a single
value to model some constraints.

```rust,editable
pub struct QueryResult;
pub struct DatabaseConnection {/* fields omitted */}

impl DatabaseConnection {
pub fn new() -> Self {
Self {}
}
pub fn results(&self) -> &[QueryResult] {
&[] // fake results
}
}

pub struct Transaction<'a> {
connection: &'a mut DatabaseConnection,
}

impl<'a> Transaction<'a> {
pub fn new(connection: &'a mut DatabaseConnection) -> Self {
Self { connection }
}
pub fn query(&mut self, _query: &str) {
// Send the query over, but don't wait for results.
}
pub fn commit(self) {
// Finish executing the transaction and retrieve the results.
}
}

fn main() {
let mut db = DatabaseConnection::new();

// The transaction `tx` mutably borrows `db`.
let mut tx = Transaction::new(&mut db);
tx.query("SELECT * FROM users");

// This won't compile because `db` is already mutably borrowed.
// let results = db.results(); // ❌🔨

// The borrow of `db` ends when `tx` is consumed by `commit`.
tx.commit();

// Now it is possible to borrow `db` again.
let results = db.results();
}
```

<details>

- Motivation: When working with a database API, a user might imagine that
transactions are being committed "as they go" and try to read results in
between queries being added to the transaction. This fundamental misuse of the
API could lead to confusion as to why nothing is happening.

While an obvious misunderstanding, situations such as this can happen in
practice.

Ask: Has anyone misunderstood an API by not reading the docs for proper use?

Expect: Examples of early-career or in-university mistakes and
misunderstandings.

As an API grows in size and user base, a smaller percentage may have "total"
knowledge of the system the API represents.

- This example shows how we can use Aliasing XOR Mutability prevent this kind of
misuse

This might happen if the user is working under the false assumption that the
queries being written to the transaction happen "immediately" rather than
being queued up and performed together.

- The constructor for the Transaction type takes a mutable reference to the
database connection, which it holds onto that reference.

The explicit lifetime here doesn't have to be intimidating, it just means
"`Transaction` is outlived by the `DatabaseConnection` that was passed to it"
in this case.

The `mut` keyword in the type lets us determine that there is just one of
these references present per variable of type `DatabaseConnection`.

- While a `Transaction` exists, we can't touch the `DatabaseConnection` variable
that was created from it.

Demonstrate: uncomment the `db.results()` line.

- This lifetime parameter for `Transaction` needs to come from somewhere, in
this case it is derived from the lifetime of the owned `DatabaseConnection`
from which an exclusive reference is being passed.

- As laid out in [generalizing ownership](generalizing-ownership.md) and
[the opening slide for this section](../borrow-checker-invariants.md) we can
look at the ways Mutable References and Shareable References interact to see
if they fit with the invariants we want to uphold for an API.

- Note: The query results not being public and placed behind a getter function
lets us enforce the invariant "users can only look at query results if they
are not also writing to a transaction."

If they're publicly available to the user outside of the definition module
then this invariant can be invalidated.

</details>
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
minutes: 5
---

# Generalizing Ownership: Lifetimes Refresher

The logic of the borrow checker, while modelled off "memory ownership", can be
abstracted away from that use case to model other problems where we want to
prevent API misuse.

```rust,editable
// An internal data type to have something to hold onto.
pub struct Internal;
// The "outer" data.
pub struct Data(Internal);

fn shared_use(value: &Data) -> &Internal {
&value.0
}
fn exclusive_use(value: &mut Data) -> &mut Internal {
&mut value.0
}
fn deny_future_use(value: Data) {}

fn main() {
let mut value = Data(Internal);
let shared = shared_use(&value);
// let exclusive = exclusive_use(&mut value); // ❌🔨
let shared_again = &shared;
deny_future_use(value);
// let shared_again_again = shared_use(&value); // ❌🔨
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code is a good demonstration of the borrow checker rules, but it doesn't connect to a real-world problem.

Is it possible to find a bit more engaging example to illustrate the point? For example, make a simplified implementation of BorrowedFd and illustrate the same points with that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about it more, a large chunk of this slide can be moved to the introductory slide for this chapter and the rest can be re-purposed as a brief borrowck refresher.

```

<details>

- This example re-frames the borrow checker rules away from references and
towards semantic meaning in non-memory-safety settings. Nothing is being
mutated, nothing is being sent across threads.

- In rust's borrow checker we have access to three different ways of "taking" a
value:

- Owned value `T`. Value is dropped when the scope ends, unless it is not
returned to another scope.

- Shared Reference `&T`. Allows aliasing but prevents mutable access while
shared references are in use.

- Mutable Reference `&mut T`. Only one of these is allowed to exist for a
value at any one point, but can be used to create shared references.

- Ask: The two commented-out lines in `main` would cause compilation errors,
Why?

1: Because the `shared` value is still aliased after the `exclusive` reference
is taken.

2: Because `value` is consumed (AKA dropped) the line before the
`shared_again_again` reference is taken from `&value`.

- Remember that every `&T` and `&mut T` has a lifetime, just one the user
doesn't have to annotate or think about most of the time. We get to avoid
annotating a lot of lifetimes because the rust compiler allows a user to elide
the majority of them. See:
[Lifetime Elision](../../../lifetimes/lifetime-elision.md)

</details>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This slide might be a place where an analogy might help some listeners understand what we are trying to get at when we say that the borrow checker and lifetimes are just another API design tool. Here's one possible analogy. WDYT?

Generics in Java were added primarily to support type-safe collections. In fact, Java 5 added generic type arguments to existing standard library collection types that were previously non-generic! So the language designers had a clear primary use case in mind. However, generics turned out to be useful in many other API designs. So it would be too narrow-minded to present Java generics as "a language feature for type-safe collections."

Similarly, the lifetimes and the borrow checker were introduced in Rust for compile-time memory safety guarantees, but their applicability in API design is broader. We (the Rust community) are still discovering design patterns and trying to understand what these tools can do for API design beyond memory safety.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a good thing to bring up, I'll pull together some references to drop in. If you've got suggestions on pieces covering this I'd be happy to hear about them, but I understand linkrot and the ephemeral nature of back-channel discussion of the time may have gotten to most of it.

Copy link
Collaborator

@gribozavr gribozavr Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSR 14 which introduced generics lists only one goal specific to a use case, and it is "Good collections support. The core Collections APIs and similar APIs are perhaps the most important customers of genericity, so it is essential that they work well as, and with, generic classes." Furthermore, this is the #1 goal of the proposal overall.

An empirical research article that I could find, Java generics adoption: how new features are introduced, championed, or ignored studies how generics were adopted in practice. It includes a data-driven argument that the most common parameterized types are collections (the only non-collection-related type in Table 1 is Class<?>). This aligns with my intuition: the primary use case is collections, but there are other cases where generics turned out to be useful (for example, Class<?> in the standard library, TypeToken<?> in Google's Guava to work around type erasure in Java's generics, or the "recursive generics" pattern similar to CRTP in C++).

One source that I had high hopes for, ACM's History of programming languages journal, unfortunately does have a piece on Java.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonderful, thank you!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs some more elaboration from me, I'll be reading more on this to resolve the standing TODO.

Loading