Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 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
e54bb17
Apply suggestions from code review
tall-vase Oct 31, 2025
b35abf8
Address structure from last round of feedback
Nov 3, 2025
935a974
Editing pass + borrowedfd slide
Nov 3, 2025
d2c0d4f
Make sure the phantomdata-03 instructor changes work
Nov 3, 2025
edbda01
Another editing pass
Nov 3, 2025
7f0bd82
Minor rephrasing
Nov 4, 2025
c86ff9a
Apply suggestions from code review
tall-vase Nov 6, 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
8 changes: 8 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,14 @@
- [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)
- [Lifetimes and Borrows: the Abstract Rules](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)
- [Mutually Exclusive References / "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 Types (implementation)](idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-02-types-implemented.md)
- [PhantomData: Lifetimes for External Resources](idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-03-lifetimes.md)
- [PhantomData: OwnedFd & BorrowedFd](idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-04-borrowedfd.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
116 changes: 116 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,116 @@
---
minutes: 15
---

# Using the Borrow checker to enforce Invariants

The borrow checker, while added to enforce memory ownership, can 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>

- We've seen the borrow checker prevent memory safety bugs (use-after-free, data
races).

- We've also used types to shape and restrict 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.

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

Adoption was slow at first, but some new projects began designing their APIs
around generics from the beginning.

Since then, users and developers of the language expanded the use of generics
to other areas of type-safe API design:
- Class information can be held onto via Java's `Class<T>` or Guava's
`TypeToken<T>`.
- The Builder pattern can be implemented using Recursive Generics.

We aim to do something similar here: Even though the borrow checker was
introduced to prevent use-after-free and data races, we treat it as 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,108 @@
---
minutes: 15
---

# Mutually Exclusive References / "Aliasing XOR Mutability"

We can use the mutual exclusion of `&T` and `&mut T` references to prevent data
from being used before it is ready.

```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 by `tx`.
// 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: In this database API queries are kicked off for asynchronous
execution and the results are only available once the whole transaction is
finished.

A user might think that queries are executed immediately, and try to read
results before they are made available. This API misuse could make the app
read incomplete or incorrect data.

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 of users has deep
knowledge of the system the API represents.

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

- The code might read results before they are ready if the programmer assumes
that the queries execute immediately rather than kicked off for asynchronous
execution.

- The constructor for the `Transaction` type takes a mutable reference to the
database connection, and stores it in the returned `Transaction` value.

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 reference is mutable to completely lock out the `DatabaseConnection` from
other usage, such as starting further transactions or reading the results.

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

Demonstrate: uncomment the `db.results()` line. Doing so will result in a
compile error, as `db` is already mutably borrowed.

- 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 there
is no active transactions."

If the query results were placed in a public struct field, this invariant
could be violated.

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

# Lifetimes and Borrows: the Abstract Rules

```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 demo_exclusive() {
let mut value = Data(Internal);
let shared = shared_use(&value);
// let exclusive = exclusive_use(&mut value); // ❌🔨
let shared_again = &shared;
}

fn demo_denied() {
let value = Data(Internal);
deny_future_use(value);
// let shared = 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.


# fn main() {}
```

<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 the `demo` functions would cause
compilation errors, Why?

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

`demo_denied`: Because `value` is consumed 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 rarely specify lifetimes because the Rust compiler allows
us to *elide* them in most cases. 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.

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
minutes: 5
---

# PhantomData 1/4: De-duplicating Same Data & Semantics

The newtype pattern can sometimes come up against the DRY principle, how do we
solve this?

<!-- dprint-ignore-start -->
```rust,editable,compile_fail
pub struct UserId(u64);
impl ChatUser for UserId { /* ... */ }

pub struct PatronId(u64);
impl ChatUser for PatronId { /* ... */ }

pub struct ModeratorId(u64);
impl ChatUser for ModeratorId { /* ... */ }
impl ChatModerator for ModeratorId { /* ... */ }

pub struct AdminId(u64);
impl ChatUser for AdminId { /* ... */ }
impl ChatModerator for AdminId { /* ... */ }
impl ChatAdmin for AdminId { /* ... */ }

// And so on ...
fn main() {}
```
<!-- dprint-ignore-end -->

<details>

- Problem: We want to use the newtype pattern to differentiate permissions, but
we're having to implement the same traits over and over again for the same
data.

- Ask: Assume the details of each implementation here are the same between
types, what are ways we can avoid repeating ourselves?

Expect:
- Make this an enum, not distinct data types.
- Bundle the user ID with permission tokens like
`struct Admin(u64, UserPermission, ModeratorPermission, AdminPermission);`
- Adding a type parameter which encodes permissions.
- Mentioning `PhantomData` ahead of schedule (it's in the title).

</details>
Loading
Loading