From b771e7ff303d0fe4b2f4347eecf386464713948e Mon Sep 17 00:00:00 2001
From: tall-vase <228449146+tall-vase@users.noreply.github.com>
Date: Wed, 27 Aug 2025 16:07:20 +0100
Subject: [PATCH 01/24] Initial draft of the 'borrow checker invariants'
section
---
src/SUMMARY.md | 5 +++
.../borrow-checker-invariants.md | 17 ++++++++
.../aliasing-xor-mutability.md | 41 +++++++++++++++++++
.../external-resources.md | 26 ++++++++++++
.../generalizing-ownership.md | 15 +++++++
.../single-use-values.md | 41 +++++++++++++++++++
6 files changed, 145 insertions(+)
create mode 100644 src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
create mode 100644 src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
create mode 100644 src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/external-resources.md
create mode 100644 src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
create mode 100644 src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
diff --git a/src/SUMMARY.md b/src/SUMMARY.md
index 8b67ecaf9cd8..5cc5b55ffcf6 100644
--- a/src/SUMMARY.md
+++ b/src/SUMMARY.md
@@ -437,6 +437,11 @@
- [Semantic Confusion](idiomatic/leveraging-the-type-system/newtype-pattern/semantic-confusion.md)
- [Parse, Don't Validate](idiomatic/leveraging-the-type-system/newtype-pattern/parse-don-t-validate.md)
- [Is It Encapsulated?](idiomatic/leveraging-the-type-system/newtype-pattern/is-it-encapsulated.md)
+ - [Borrow checking invariants](idiomatic/leveraging-the-type-system/borrow-checker-invariants.md)
+ - [Generalising "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)
+ - [External Resources](idiomatic/leveraging-the-type-system/borrow-checker-invariants/external-resources.md)
---
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
new file mode 100644
index 000000000000..1756a6d263a5
--- /dev/null
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
@@ -0,0 +1,17 @@
+---
+minutes: 0
+---
+
+# Using the Borrow checker to enforce Invariants
+
+We can use the rules of the borrow checker to model things other than what is valid to mutate and read at various points in a program.
+
+
+
+- The borrow checker has been used to prevent use-after-free and multiple mutable references up until this point.
+
+- But the constraints of the borrow checker can be used to prevent misuse of APIs.
+
+- Interior, private mutability or reference counting in data types lets an API designer shift the meaning of ownership to a different (but analogous) set of semantics as they need to, even to the point where some API designers have managed to model self-referential types this way.
+
+
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
new file mode 100644
index 000000000000..8c0bed3543ff
--- /dev/null
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
@@ -0,0 +1,41 @@
+---
+minutes: 0
+---
+
+# 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
+fn main() {
+pub struct TransactionInterface(/* some kind of interior state */);
+
+pub struct DatabaseConnection {
+ transaction: TransactionInterface,
+}
+
+impl DatabaseConnection {
+ pub fn new() -> Self { Self { transaction: TransactionInterface() } }
+ pub fn get_transaction(&self) -> &TransactionInterface { &self.transaction }
+ pub fn commit(&mut self) {}
+}
+
+pub fn do_something_with_transaction(transaction: &TransactionInterface) {}
+
+let mut db = DatabaseConnection::new();
+let transaction = db.get_transaction();
+do_something_with_transaction(transaction);
+db.commit();
+do_something_with_transaction(transaction); // 🛠️❌
+}
+```
+
+
+
+- Aliasing XOR Mutability is a constraint that lets us model a bunch of non-cpu-bound-race-condition related circumstances.
+
+- This is an instance of the "Aliasing XOR Mutability" being used to articulate "You can do X or you can do Y, but not both" in the API.
+
+- TODO: Namedropping other things to work with this.
+
+
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/external-resources.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/external-resources.md
new file mode 100644
index 000000000000..7991bc7827fc
--- /dev/null
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/external-resources.md
@@ -0,0 +1,26 @@
+---
+minutes: 0
+---
+
+# External Resources & Lifetime "Connections"
+
+We can define types that capture lifetimes through parameters to prevent misuse.
+
+
+```rust, editable
+fn main() {
+
+
+
+}
+```
+
+
+
+- BorrowedFd / OwnedFd uses this to model Unix-like resource handles (file descriptors), guaranteeing that while.
+
+- These lifetimes are "held onto" with instances of `PhantomData`, a type that lets us have type parameters without needing "values" of that type.
+
+- Lifetimes need to come from somewhere!
+
+
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
new file mode 100644
index 000000000000..0156ded2cab7
--- /dev/null
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -0,0 +1,15 @@
+---
+minutes: 0
+---
+
+The logic of the borrow checker, while tied to "memory ownership", can be abstracted away from this central use case to model other problems and prevent API misuse.
+
+1. Mutual Exclusion of "&T" and "&mut T" references means we can look for places when designing an API where we need to model mutual exclusion.
+2. The "consumption" of owned values means we can model values that can be used "only once"
+3. Lifetime parameters & `PhantomData` let us define restrictive relationships between different values.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
new file mode 100644
index 000000000000..5085ca140ada
--- /dev/null
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -0,0 +1,41 @@
+---
+minutes: 0
+---
+
+# Single-use values
+
+In some circumstances we want values that can be used _exactly once_. One critical example of this is in cryptography, one-use values "Nonces"
+
+```rust,editable
+fn main() {
+pub struct Key;
+
+// Pretend this is a cryptographically unique, use-once number.
+pub struct Nonce(u32);
+
+unsafe fn new_nonce_from_raw(nonce: u32) -> Nonce {
+ Nonce(nonce)
+}
+
+let nonce = unsafe { new_nonce_from_raw(1337) };
+let data = [1u8, 2u8, 3u8, 4u8];
+let key = Key;
+
+// The key and data can be re-used, copied, etc. but the nonce cannot.
+fn encrypt(nonce: Nonce, key: &Key, data: &[u8]) {}
+
+encrypt(nonce, &key, &data);
+encrypt(nonce, &key, &data); // 🛠️❌
+}
+```
+
+
+- Owned "consumption" lets us model use-once values.
+
+- Not implementing clone/copy here & making the interior type opaque (as per the newtype pattern) is _intentional_, as it prevents multiple uses of the same, API-controlled value.
+
+- A Nonce is a additional piece of random, unique data during an encryption process that helps prevent "replay attacks".
+
+- TODO: put a reference to how this is used in the Real World in rust cryptography.
+
+
\ No newline at end of file
From 383185876c19f59b7c840dffb82b52de9ca3aa6e Mon Sep 17 00:00:00 2001
From: tall-vase <228449146+tall-vase@users.noreply.github.com>
Date: Wed, 27 Aug 2025 16:30:08 +0100
Subject: [PATCH 02/24] Elaborate more on the cryptography example
---
.../single-use-values.md | 22 +++++++++++++------
1 file changed, 15 insertions(+), 7 deletions(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
index 5085ca140ada..9c6d652782a5 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -4,7 +4,7 @@ minutes: 0
# Single-use values
-In some circumstances we want values that can be used _exactly once_. One critical example of this is in cryptography, one-use values "Nonces"
+In some circumstances we want values that can be used _exactly once_. One critical example of this is in cryptography: "Nonces."
```rust,editable
fn main() {
@@ -13,29 +13,37 @@ pub struct Key;
// Pretend this is a cryptographically unique, use-once number.
pub struct Nonce(u32);
+// It's unsafe to declare a nonce directly! In practice,
+// this would be done with an RNG source, and potentially
+// a timestamp.
unsafe fn new_nonce_from_raw(nonce: u32) -> Nonce {
Nonce(nonce)
}
let nonce = unsafe { new_nonce_from_raw(1337) };
-let data = [1u8, 2u8, 3u8, 4u8];
+let data_1: [u8; 4] = [1, 2, 3, 4];
+let data_2: [u8; 4] = [4, 3, 2, 1];
let key = Key;
// The key and data can be re-used, copied, etc. but the nonce cannot.
fn encrypt(nonce: Nonce, key: &Key, data: &[u8]) {}
-encrypt(nonce, &key, &data);
-encrypt(nonce, &key, &data); // 🛠️❌
+encrypt(nonce, &key, &data_1);
+encrypt(nonce, &key, &data_2); // 🛠️❌
}
```
-- Owned "consumption" lets us model use-once values.
+- Owned "consumption" lets us model single-once values.
- Not implementing clone/copy here & making the interior type opaque (as per the newtype pattern) is _intentional_, as it prevents multiple uses of the same, API-controlled value.
-- A Nonce is a additional piece of random, unique data during an encryption process that helps prevent "replay attacks".
+- I.e. A Nonce is a additional piece of random, unique data during an encryption process that helps prevent "replay attacks".
-- TODO: put a reference to how this is used in the Real World in rust cryptography.
+- In practice people have ended up re-using nonces in circumstances where security is important, making it possible for private key information to be derived by attackers.
+
+- By tying nonce creation and consumption up in rust's ownership model, and by not implementing clone/copy on sensitive single-use data, we can prevent this kind of dangerous misuse.
+
+- TODO: put a reference to how this is used in the Real World in rust cryptography. Find sources on nonce misuse.
\ No newline at end of file
From a81b55a234407ee2a9065730ae1c02ae2dfd3169 Mon Sep 17 00:00:00 2001
From: tall-vase <228449146+tall-vase@users.noreply.github.com>
Date: Mon, 1 Sep 2025 11:17:01 +0100
Subject: [PATCH 03/24] Begin expanding the slide notes, rename the external
resources section to lifetime connections
---
src/SUMMARY.md | 2 +-
.../aliasing-xor-mutability.md | 4 ++
.../external-resources.md | 26 ---------
.../generalizing-ownership.md | 13 ++++-
.../lifetime-connections.md | 57 +++++++++++++++++++
.../single-use-values.md | 2 +-
6 files changed, 73 insertions(+), 31 deletions(-)
delete mode 100644 src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/external-resources.md
create mode 100644 src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
diff --git a/src/SUMMARY.md b/src/SUMMARY.md
index 5cc5b55ffcf6..2afbd9b20590 100644
--- a/src/SUMMARY.md
+++ b/src/SUMMARY.md
@@ -441,7 +441,7 @@
- [Generalising "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)
- - [External Resources](idiomatic/leveraging-the-type-system/borrow-checker-invariants/external-resources.md)
+ - [Lifetime Relationships & External Resources](idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md)
---
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
index 8c0bed3543ff..7c7ef3325862 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
@@ -32,10 +32,14 @@ do_something_with_transaction(transaction); // 🛠️❌
+- This slide loosely models a database connection, though the database tools you use may not look exactly like this.
+
- Aliasing XOR Mutability is a constraint that lets us model a bunch of non-cpu-bound-race-condition related circumstances.
- This is an instance of the "Aliasing XOR Mutability" being used to articulate "You can do X or you can do Y, but not both" in the API.
+- Not every mode of "mutual exclusion" can be modelled this way.
+
- TODO: Namedropping other things to work with this.
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/external-resources.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/external-resources.md
deleted file mode 100644
index 7991bc7827fc..000000000000
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/external-resources.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-minutes: 0
----
-
-# External Resources & Lifetime "Connections"
-
-We can define types that capture lifetimes through parameters to prevent misuse.
-
-
-```rust, editable
-fn main() {
-
-
-
-}
-```
-
-
-
-- BorrowedFd / OwnedFd uses this to model Unix-like resource handles (file descriptors), guaranteeing that while.
-
-- These lifetimes are "held onto" with instances of `PhantomData`, a type that lets us have type parameters without needing "values" of that type.
-
-- Lifetimes need to come from somewhere!
-
-
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
index 0156ded2cab7..19507c86f9f3 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -4,12 +4,19 @@ minutes: 0
The logic of the borrow checker, while tied to "memory ownership", can be abstracted away from this central use case to model other problems and prevent API misuse.
-1. Mutual Exclusion of "&T" and "&mut T" references means we can look for places when designing an API where we need to model mutual exclusion.
-2. The "consumption" of owned values means we can model values that can be used "only once"
-3. Lifetime parameters & `PhantomData` let us define restrictive relationships between different values.
+```rust,editable
+pub struct APIClient;
+pub struct Data {api_client: APIClient, }
+```
+- This example shows how we're using the borrow checker for easy, toy logic.
+
+1. Mutual Exclusion of "&T" and "&mut T" references means we can look for places when designing an API where we need to model mutual exclusion.
+
+2. The "consumption" of owned values means we can model values that can be used "only once"
+3. Lifetime parameters & `PhantomData` let us define restrictive relationships between different values.
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
new file mode 100644
index 000000000000..8331430ae5c3
--- /dev/null
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
@@ -0,0 +1,57 @@
+---
+minutes: 0
+---
+
+# Lifetime "Connections" & External Resources
+
+Using `PhantomData` in conjunction with lifetimes lets us say "this value may own its data, but it can only live as long as the value that generated it" in rust's type system.
+
+```rust,editable
+fn main() {
+use std::marker::PhantomData;
+pub struct Tag;
+pub struct ErasedData<'a>{data: String, _phantom: PhantomData<&'a ()>};
+impl <'a> ErasedData<'a> {
+ pub fn get(&self) -> &str {
+ &self.data
+ }
+}
+pub struct TaggedData{data: String, _phantom: PhantomData};
+impl TaggedData {
+ pub fn new(data: String) -> Self {Self {data, _phantom: PhantomData} }
+ pub fn consume(self) {}
+ pub fn get_erased(&self) -> ErasedData<'_> {
+ // has an owned String, but _phantom holds onto the lifetime of the TaggedData
+ // that created it.
+ ErasedData { data: self.data.clone(), _phantom: PhantomData }
+ }
+}
+
+let tagged_data: TaggedData = TaggedData::new("Real Data".to_owned());
+// Get the erased-but-still-linked data.
+let erased_owned_and_linked = tagged_data.get_erased();
+tagged_data.consume();
+// The data is owned by `erased_owned_and_linked` but still connected to `tagged_data`.
+println!("{}", erased_owned_and_linked.get()); // ❌🔨
+}
+```
+
+
+
+- PhantomData lets developers "tag" types with type and lifetime parameters that are not "really" present in the struct or enum.
+
+- PhantomData can be used with the Typestate pattern to have data with the same structure i.e. `TaggedData` can have methods or trait implementations that `TaggedData` doesn't.
+
+- But it can also be used to encode a connection between the lifetime of one value and another, while both values still maintain separate owned data within them.
+
+- This is really useful for modelling a bunch of relationships between data, where we want to establish that while a type has owned values within it is still connected to another piece of data and can only live as long as it.
+
+- Consider a case where you want to return owned data from a method, but you don't want that data to live longer than the value that created it.
+
+- `BorrowedFd` / `OwnedFd` uses these captured lifetimes to enforce the invariant that "if this file descriptor exists, the OS file descriptor is still open" because a `BorrowedFd`'s lifetime parameter demands that there exists another value in your program that has the same lifetime as it, and this has been encoded by the API designer to mean _that value is what keeps the access to the file open_.
+
+- Lifetimes need to come from somewhere! We can't build functions of the form `fn lifetime_shenanigans<'a>(owned: OwnedData) -> &'b Data`. Most often a lifetime comes from the lifetime `'a` from `&'a self` in a method, but not always!
+
+- This encoding is _exceptionally powerful_ when combined with unsafe, as the ways one can manipulate lifetimes becomes almost arbitrary. This is dangerous, but when combined with tools like formal, mechanically-verified proofs we can _safely encode cyclic types_ (see: the GhostCell paper.)
+
+
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
index 9c6d652782a5..8b6bd0581502 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -44,6 +44,6 @@ encrypt(nonce, &key, &data_2); // 🛠️❌
- By tying nonce creation and consumption up in rust's ownership model, and by not implementing clone/copy on sensitive single-use data, we can prevent this kind of dangerous misuse.
-- TODO: put a reference to how this is used in the Real World in rust cryptography. Find sources on nonce misuse.
+- Cryptography Nuance: There is still the case where a nonce may be used twice if it's created through purely a pseudo-random process with no additional metadata, and that circumstance can't be avoided through this particular method. This kind of API prevents one kind of misuse, but not all kinds.
\ No newline at end of file
From 0b4318ff260123cf99f0b075f897464e7229e0e8 Mon Sep 17 00:00:00 2001
From: tall-vase <228449146+tall-vase@users.noreply.github.com>
Date: Mon, 1 Sep 2025 17:07:58 +0100
Subject: [PATCH 04/24] Get this section into a 'review ready' state.
---
src/SUMMARY.md | 2 +-
.../borrow-checker-invariants.md | 49 +++++++++++++++++--
.../aliasing-xor-mutability.md | 17 ++++---
.../generalizing-ownership.md | 40 ++++++++++++---
.../lifetime-connections.md | 10 ++--
.../single-use-values.md | 4 +-
6 files changed, 96 insertions(+), 26 deletions(-)
diff --git a/src/SUMMARY.md b/src/SUMMARY.md
index 2afbd9b20590..b1f03bcf341a 100644
--- a/src/SUMMARY.md
+++ b/src/SUMMARY.md
@@ -438,7 +438,7 @@
- [Parse, Don't Validate](idiomatic/leveraging-the-type-system/newtype-pattern/parse-don-t-validate.md)
- [Is It Encapsulated?](idiomatic/leveraging-the-type-system/newtype-pattern/is-it-encapsulated.md)
- [Borrow checking invariants](idiomatic/leveraging-the-type-system/borrow-checker-invariants.md)
- - [Generalising "Ownership"](idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.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)
- [Lifetime Relationships & External Resources](idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
index 1756a6d263a5..e6ee2855c97c 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
@@ -1,17 +1,58 @@
---
-minutes: 0
+minutes: 10
---
# Using the Borrow checker to enforce Invariants
-We can use the rules of the borrow checker to model things other than what is valid to mutate and read at various points in a program.
+The logic of the borrow checker, while tied to "memory ownership", can be abstracted away from this central use case to model other problems and prevent API misuse.
+
+```rust,editable
+fn main() {
+// Doors can be open or closed, and you need the right key to lock or unlock one.
+// Modelled with Shared Key and Owned Door. Nothing to do with "memory safety"!
+pub struct DoorKey { pub key_shape: u32 }
+pub struct ClosedDoor { lock_shape: u32 }
+pub struct OpenDoor { lock_shape: u32 }
+
+fn open_door(key: &DoorKey, door: ClosedDoor) -> Result {
+ 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 {
+ if door.lock_shape == key.key_shape {
+ Ok(ClosedDoor{lock_shape: door.lock_shape})
+ } else {
+ Err(door)
+ }
+}
+
+let key = DoorKey{ key_shape: 7 };
+let closed_door = ClosedDoor{ 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);
+}
+}
+```
- The borrow checker has been used to prevent use-after-free and multiple mutable references up until this point.
-- But the constraints of the borrow checker can be used to prevent misuse of APIs.
+- This example uses the ownership & borrowing rules to model the opening and closing of a door. We can try to open a door with a key, but if it's the wrong key the door is still closed (here represented as an error.)
+
+- The rules of the borrow checker fundamentally exist to prevent developers from accessing, changing, and holding onto data in memory in unpredictable ways without preventing _writing software_. The underlying logical system does not "know" what memory is. All it does is enforce a specific set of rules of how different operations affect what possible later operations are.
+
+- Those rules can apply to many other cases, so we can piggy-back onto the rules of the borrow checker to design APIs to be harder or impossible to misuse. Even when there's little or no actual "memory safety" concerns in the problem domain.
+
+- This section will walk through some of those different domains.
-- Interior, private mutability or reference counting in data types lets an API designer shift the meaning of ownership to a different (but analogous) set of semantics as they need to, even to the point where some API designers have managed to model self-referential types this way.
+- Interior, private mutability or reference counting in data types may let an API designer shift the meaning of ownership to a different (but analogous) set of semantics as they need to, even to the point where some API designers have managed to model self-referential types this way.
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
index 7c7ef3325862..5248deef1b19 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
@@ -1,5 +1,5 @@
---
-minutes: 0
+minutes: 10
---
# Aliasing XOR Mutability
@@ -15,7 +15,7 @@ pub struct DatabaseConnection {
}
impl DatabaseConnection {
- pub fn new() -> Self { Self { transaction: TransactionInterface() } }
+ pub fn new() -> Self { Self { transaction: TransactionInterface(/* again, pretend there's some interior state */) } }
pub fn get_transaction(&self) -> &TransactionInterface { &self.transaction }
pub fn commit(&mut self) {}
}
@@ -32,14 +32,17 @@ do_something_with_transaction(transaction); // 🛠️❌
-- This slide loosely models a database connection, though the database tools you use may not look exactly like this.
+- This example shows how we can use the "Aliasing XOR Mutability" rule when it comes to shared and mutable references to model safe access to transactions for a database. This is a loose sketch of such a model, and rust database frameworks you use may not look anything like this in practice.
-- Aliasing XOR Mutability is a constraint that lets us model a bunch of non-cpu-bound-race-condition related circumstances.
+- As laid out in [generalizing ownership]("./generalizing-ownership.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.
-- This is an instance of the "Aliasing XOR Mutability" being used to articulate "You can do X or you can do Y, but not both" in the API.
+- Here we want to be able to write to a transaction, which has some internal breaking of rust rules we don't concern ourselves with, before then committing that transaction.
-- Not every mode of "mutual exclusion" can be modelled this way.
+- By having the "commit transaction" method take a mutable reference, regardless of if mutation is happening, the borrow checker prevents references to the internal transaction surface persisting between calls to that method.
-- TODO: Namedropping other things to work with this.
+- The transaction itself can be modelled with a shareable reference, not necessarily because the interior state stays the same while we use it but because this prevents using the "commit transaction" functionality until the transaction is "over."
+
+
+- Tangential: We could instead have the `get_transaction` method return a mutable reference off a mutable reference to self (`fn get_transaction(&mut self) -> &mut TransactionInterface`) but we're trying to show off the ways shareable and mutable references exclude each other here.
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
index 19507c86f9f3..28173a74c075 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -1,22 +1,46 @@
---
-minutes: 0
+minutes: 5
---
-The logic of the borrow checker, while tied to "memory ownership", can be abstracted away from this central use case to model other problems and prevent API misuse.
+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
-pub struct APIClient;
-pub struct Data {api_client: APIClient, }
+pub struct InternalData;
+pub struct Value(InternalData);
+
+fn shared_use(value: &Value) -> &InternalData {
+ &value.0
+}
+
+fn exclusive_use(value: &mut Value) -> &mut InternalData {
+ &mut value.0
+}
+
+fn deny_future_use(value: Value) {}
+
+let mut value = Value(InternalData);
+let deny_mut = shared_use(&value);
+let try_to_deny_immutable = exclusive_use(&mut value); // ❌🔨
+let more_mut_denial = &deny_mut;
+deny_future_use(value);
+let even_more_mut_denial = shared_use(&value); // ❌🔨
```
-- This example shows how we're using the borrow checker for easy, toy logic.
+- 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 concurrency, instead imagining and working within situations where the rules are the same but the meaning is slightly different.
+
+- In rust's borrow checker we have access to three different ways of "taking" a value:
+
+
+- Owned value. Very permissive case of what you can do with it, but demands that nothing else is using it in any context and "drops" the value when scope ends (unless that scope returns this value) (see: RAII.)
+
+- Mutable Reference, while holding onto a mutable reference we can still "dispatch" to methods and functions that take an immutable, shared reference of the value but only as long as we're not aliasing immutable, sharable references to related data "after" that dispatch.
-1. Mutual Exclusion of "&T" and "&mut T" references means we can look for places when designing an API where we need to model mutual exclusion.
+- Shareable Reference, allows aliasing but prevents mutable access while any of these exist. We can't "dispatch" to methods and functions that take mutable reference when all we have is a shared reference.
-2. The "consumption" of owned values means we can model values that can be used "only once"
+- Important to note that every `&T` and `&mut T` has an _implicit lifetime._ We get to avoid annotating a lot of lifetimes because the rust compiler can infer the majority of them. See: [Lifetime Elision]("../../../../../lifetimes/lifetime-elision.md")
-3. Lifetime parameters & `PhantomData` let us define restrictive relationships between different values.
+- Potentially relevant: show how we can replace a lot of the `&` and `&mut` here with `&'a` and `&'a mut`.
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
index 8331430ae5c3..fd3f1ed93de9 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
@@ -1,5 +1,5 @@
---
-minutes: 0
+minutes: 20
---
# Lifetime "Connections" & External Resources
@@ -48,10 +48,12 @@ println!("{}", erased_owned_and_linked.get()); // ❌🔨
- Consider a case where you want to return owned data from a method, but you don't want that data to live longer than the value that created it.
-- `BorrowedFd` / `OwnedFd` uses these captured lifetimes to enforce the invariant that "if this file descriptor exists, the OS file descriptor is still open" because a `BorrowedFd`'s lifetime parameter demands that there exists another value in your program that has the same lifetime as it, and this has been encoded by the API designer to mean _that value is what keeps the access to the file open_.
+- [`BorrowedFd`](https://rust-lang.github.io/rfcs/3128-io-safety.html#ownedfd-and-borrowedfdfd) uses these captured lifetimes to enforce the invariant that "if this file descriptor exists, the OS file descriptor is still open" because a `BorrowedFd`'s lifetime parameter demands that there exists another value in your program that has the same lifetime as it, and this has been encoded by the API designer to mean _that value is what keeps the access to the file open_. Its counterpart `OwnedFd` is instead a file descriptor that closes that file on drop.
-- Lifetimes need to come from somewhere! We can't build functions of the form `fn lifetime_shenanigans<'a>(owned: OwnedData) -> &'b Data`. Most often a lifetime comes from the lifetime `'a` from `&'a self` in a method, but not always!
+- Lifetimes need to come from somewhere! We can't build functions of the form `fn lifetime_shenanigans<'a>(owned: OwnedData) -> &'b Data` (without tying `'b` to `'a` in some way). Lifetime elision hides where a lot of lifetimes come from, but that doesn't mean the explicitly named lifetimes "come from nowhere."
-- This encoding is _exceptionally powerful_ when combined with unsafe, as the ways one can manipulate lifetimes becomes almost arbitrary. This is dangerous, but when combined with tools like formal, mechanically-verified proofs we can _safely encode cyclic types_ (see: the GhostCell paper.)
+- This way of encoding information in types is _exceptionally powerful_ when combined with unsafe, as the ways one can manipulate lifetimes becomes almost arbitrary. This is also dangerous, but when combined with tools like external, mechanically-verified proofs _we can safely encode cyclic/self-referential types while encoding lifetime & safety expectations in the relevant data types._
+
+- The [GhostCell (2021)](https://plv.mpi-sws.org/rustbelt/ghostcell/) paper and its [relevant implementation](https://gitlab.mpi-sws.org/FP/ghostcell) show this kind of work off. While the borrow checker is restrictive, there are still ways to use escape hatches and then _show that the ways you used those escape hatches are consistent and safe._
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
index 8b6bd0581502..9c17a3ebab0f 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -1,5 +1,5 @@
---
-minutes: 0
+minutes: 10
---
# Single-use values
@@ -36,7 +36,7 @@ encrypt(nonce, &key, &data_2); // 🛠️❌
- Owned "consumption" lets us model single-once values.
-- Not implementing clone/copy here & making the interior type opaque (as per the newtype pattern) is _intentional_, as it prevents multiple uses of the same, API-controlled value.
+- Not implementing clone/copy here and making the interior type opaque (as per the newtype pattern) is _intentional_, as it prevents multiple uses of the same, API-controlled value.
- I.e. A Nonce is a additional piece of random, unique data during an encryption process that helps prevent "replay attacks".
From 728af304be3320fa07f6526184f4a194692719e4 Mon Sep 17 00:00:00 2001
From: tall-vase <228449146+tall-vase@users.noreply.github.com>
Date: Mon, 1 Sep 2025 17:10:26 +0100
Subject: [PATCH 05/24] Reset all minutes on the borrowck invariants section to
0
---
.../leveraging-the-type-system/borrow-checker-invariants.md | 2 +-
.../borrow-checker-invariants/aliasing-xor-mutability.md | 2 +-
.../borrow-checker-invariants/generalizing-ownership.md | 2 +-
.../borrow-checker-invariants/lifetime-connections.md | 2 +-
.../borrow-checker-invariants/single-use-values.md | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
index e6ee2855c97c..1b9bfd2015ce 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
@@ -1,5 +1,5 @@
---
-minutes: 10
+minutes: 0
---
# Using the Borrow checker to enforce Invariants
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
index 5248deef1b19..aa2461205428 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
@@ -1,5 +1,5 @@
---
-minutes: 10
+minutes: 0
---
# Aliasing XOR Mutability
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
index 28173a74c075..f9a20706864b 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -1,5 +1,5 @@
---
-minutes: 5
+minutes: 0
---
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.
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
index fd3f1ed93de9..28bc47eb0c45 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
@@ -1,5 +1,5 @@
---
-minutes: 20
+minutes: 0
---
# Lifetime "Connections" & External Resources
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
index 9c17a3ebab0f..a8b4b4f1d226 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -1,5 +1,5 @@
---
-minutes: 10
+minutes: 0
---
# Single-use values
From a37a272881db65a764738f8a86291593b107b21f Mon Sep 17 00:00:00 2001
From: tall-vase <228449146+tall-vase@users.noreply.github.com>
Date: Mon, 8 Sep 2025 16:30:36 +0100
Subject: [PATCH 06/24] General editing pass based off feedback
---
.../borrow-checker-invariants.md | 14 +++---
.../aliasing-xor-mutability.md | 45 ++++++++++---------
.../generalizing-ownership.md | 30 ++++++++-----
.../lifetime-connections.md | 4 +-
.../single-use-values.md | 11 +----
5 files changed, 52 insertions(+), 52 deletions(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
index 1b9bfd2015ce..bef45dd902b9 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
@@ -11,10 +11,10 @@ fn main() {
// Doors can be open or closed, and you need the right key to lock or unlock one.
// Modelled with Shared Key and Owned Door. Nothing to do with "memory safety"!
pub struct DoorKey { pub key_shape: u32 }
-pub struct ClosedDoor { lock_shape: u32 }
+pub struct LockedDoor { lock_shape: u32 }
pub struct OpenDoor { lock_shape: u32 }
-fn open_door(key: &DoorKey, door: ClosedDoor) -> Result {
+fn open_door(key: &DoorKey, door: LockedDoor) -> Result {
if door.lock_shape == key.key_shape {
Ok(OpenDoor{lock_shape: door.lock_shape})
} else {
@@ -22,16 +22,16 @@ fn open_door(key: &DoorKey, door: ClosedDoor) -> Result {
}
}
-fn close_door(key: &DoorKey, door: OpenDoor) -> Result {
+fn close_door(key: &DoorKey, door: OpenDoor) -> Result {
if door.lock_shape == key.key_shape {
- Ok(ClosedDoor{lock_shape: door.lock_shape})
+ Ok(LockedDoor{lock_shape: door.lock_shape})
} else {
Err(door)
}
}
let key = DoorKey{ key_shape: 7 };
-let closed_door = ClosedDoor{ lock_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);
@@ -45,9 +45,9 @@ if let Ok(opened_door) = opened_door {
- The borrow checker has been used to prevent use-after-free and multiple mutable references up until this point.
-- This example uses the ownership & borrowing rules to model the opening and closing of a door. We can try to open a door with a key, but if it's the wrong key the door is still closed (here represented as an error.)
+- This example uses the ownership & borrowing rules to model the opening and closing of a door. We can try to open a door with a key, but if it's the wrong key the door is still closed (here represented as an error) and the key persists regardless.
-- The rules of the borrow checker fundamentally exist to prevent developers from accessing, changing, and holding onto data in memory in unpredictable ways without preventing _writing software_. The underlying logical system does not "know" what memory is. All it does is enforce a specific set of rules of how different operations affect what possible later operations are.
+- The rules of the borrow checker exist to prevent developers from accessing, changing, and holding onto data in memory in unpredictable ways without being so restrictive to the point where it prevents _writing software_. The underlying logical system does not "know" what memory is. All it does is enforce a specific set of rules of how different operations affect what possible later operations are.
- Those rules can apply to many other cases, so we can piggy-back onto the rules of the borrow checker to design APIs to be harder or impossible to misuse. Even when there's little or no actual "memory safety" concerns in the problem domain.
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
index aa2461205428..79e362599e55 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
@@ -2,47 +2,48 @@
minutes: 0
---
-# Aliasing XOR Mutability
+# Mutually Exclusive References, or "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
-fn main() {
-pub struct TransactionInterface(/* some kind of interior state */);
+pub struct Transaction(/* some kind of interior state */);
+pub struct QueryResult(String);
pub struct DatabaseConnection {
- transaction: TransactionInterface,
+ transaction: Transaction,
+ query_results: Vec,
}
impl DatabaseConnection {
- pub fn new() -> Self { Self { transaction: TransactionInterface(/* again, pretend there's some interior state */) } }
- pub fn get_transaction(&self) -> &TransactionInterface { &self.transaction }
- pub fn commit(&mut self) {}
+ pub fn new() -> Self { Self { transaction: Transaction(/* again, pretend there's some interior state */), query_results: vec![] } }
+ pub fn get_transaction(&mut self) -> &mut Transaction { &mut self.transaction }
+ pub fn results(&self) -> &[QueryResult] { &self.query_results }
+ pub fn commit(&mut self) { println!("Transaction committed!") }
}
-pub fn do_something_with_transaction(transaction: &TransactionInterface) {}
+pub fn do_something_with_transaction(transaction: &mut Transaction) {}
-let mut db = DatabaseConnection::new();
-let transaction = db.get_transaction();
-do_something_with_transaction(transaction);
-db.commit();
-do_something_with_transaction(transaction); // 🛠️❌
+fn main() {
+ let mut db = DatabaseConnection::new();
+ let mut transaction = db.get_transaction();
+ do_something_with_transaction(transaction);
+ let assumed_the_transactions_happened_immediately = db.results(); // ❌🔨
+ do_something_with_transaction(transaction);
+ // Works, as the lifetime of "transaction" as a reference ended above.
+ let assumed_the_transactions_happened_immediately_again = db.results();
+ db.commit();
}
```
-- This example shows how we can use the "Aliasing XOR Mutability" rule when it comes to shared and mutable references to model safe access to transactions for a database. This is a loose sketch of such a model, and rust database frameworks you use may not look anything like this in practice.
-
-- As laid out in [generalizing ownership]("./generalizing-ownership.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.
-
-- Here we want to be able to write to a transaction, which has some internal breaking of rust rules we don't concern ourselves with, before then committing that transaction.
+- Aliasing XOR Mutability is a shorthand for "we can have multiple immutable references, a single mutable reference, but not both."
-- By having the "commit transaction" method take a mutable reference, regardless of if mutation is happening, the borrow checker prevents references to the internal transaction surface persisting between calls to that method.
+- This example shows how we can use the mutual exclusion of these kinds of references when it comes to prevent a user from reading query results while using the transaction API, something that 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 transaction itself can be modelled with a shareable reference, not necessarily because the interior state stays the same while we use it but because this prevents using the "commit transaction" functionality until the transaction is "over."
+- As laid out in [generalizing ownership](generalizing-ownership.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.
-
-- Tangential: We could instead have the `get_transaction` method return a mutable reference off a mutable reference to self (`fn get_transaction(&mut self) -> &mut TransactionInterface`) but we're trying to show off the ways shareable and mutable references exclude each other here.
+- By having the query results not public and placed behind a getter function, we can enforce the invariant "users of this API are not looking at the query results at the same time as they are writing to a transaction."
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
index f9a20706864b..6582539beae6 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -2,44 +2,50 @@
minutes: 0
---
+# Generalizing Ownership
+
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
-pub struct InternalData;
-pub struct Value(InternalData);
+// An internal data type to have something to hold onto.
+pub struct Internal;
+// The "outer" data.
+pub struct Data(Internal);
-fn shared_use(value: &Value) -> &InternalData {
+fn shared_use(value: &Data) -> &Internal {
&value.0
}
-fn exclusive_use(value: &mut Value) -> &mut InternalData {
+fn exclusive_use(value: &mut Data) -> &mut Internal {
&mut value.0
}
-fn deny_future_use(value: Value) {}
+fn deny_future_use(value: Data) {}
-let mut value = Value(InternalData);
-let deny_mut = shared_use(&value);
+let mut value = Data(Internal);
+let deny_mut = shared_use(&value);
let try_to_deny_immutable = exclusive_use(&mut value); // ❌🔨
let more_mut_denial = &deny_mut;
deny_future_use(value);
let even_more_mut_denial = shared_use(&value); // ❌🔨
```
-
+
+
+- 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.
-- 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 concurrency, instead imagining and working within situations where the rules are the same but the meaning is slightly different.
+- 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 concurrency & dangling pointers, instead imagining and working within situations where the rules are the same but the meaning is slightly different.
- In rust's borrow checker we have access to three different ways of "taking" a value:
- Owned value. Very permissive case of what you can do with it, but demands that nothing else is using it in any context and "drops" the value when scope ends (unless that scope returns this value) (see: RAII.)
-- Mutable Reference, while holding onto a mutable reference we can still "dispatch" to methods and functions that take an immutable, shared reference of the value but only as long as we're not aliasing immutable, sharable references to related data "after" that dispatch.
+- Mutable Reference. While holding onto a mutable reference we can still "dispatch" to methods and functions that take an immutable, shared reference of the value but only as long as we're not aliasing immutable, shared references to related data "after" that dispatch.
-- Shareable Reference, allows aliasing but prevents mutable access while any of these exist. We can't "dispatch" to methods and functions that take mutable reference when all we have is a shared reference.
+- Shared Reference. Allows aliasing but prevents mutable access while any of these exist. We can't "dispatch" to methods and functions that take mutable reference when all we have is a shared reference.
-- Important to note that every `&T` and `&mut T` has an _implicit lifetime._ We get to avoid annotating a lot of lifetimes because the rust compiler can infer the majority of them. See: [Lifetime Elision]("../../../../../lifetimes/lifetime-elision.md")
+- Important to remember that every `&T` and `&mut T` has an _implicit lifetime._ We get to avoid annotating a lot of lifetimes because the rust compiler can infer the majority of them. See: [Lifetime Elision](../../../lifetimes/lifetime-elision.md)
- Potentially relevant: show how we can replace a lot of the `&` and `&mut` here with `&'a` and `&'a mut`.
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
index 28bc47eb0c45..b101631df620 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
@@ -10,13 +10,13 @@ Using `PhantomData` in conjunction with lifetimes lets us say "this value may ow
fn main() {
use std::marker::PhantomData;
pub struct Tag;
-pub struct ErasedData<'a>{data: String, _phantom: PhantomData<&'a ()>};
+pub struct ErasedData<'a>{data: String, _phantom: PhantomData<&'a ()>}
impl <'a> ErasedData<'a> {
pub fn get(&self) -> &str {
&self.data
}
}
-pub struct TaggedData{data: String, _phantom: PhantomData};
+pub struct TaggedData{data: String, _phantom: PhantomData}
impl TaggedData {
pub fn new(data: String) -> Self {Self {data, _phantom: PhantomData} }
pub fn consume(self) {}
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
index a8b4b4f1d226..73227d644d5a 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -13,14 +13,7 @@ pub struct Key;
// Pretend this is a cryptographically unique, use-once number.
pub struct Nonce(u32);
-// It's unsafe to declare a nonce directly! In practice,
-// this would be done with an RNG source, and potentially
-// a timestamp.
-unsafe fn new_nonce_from_raw(nonce: u32) -> Nonce {
- Nonce(nonce)
-}
-
-let nonce = unsafe { new_nonce_from_raw(1337) };
+let nonce = Nonce(1337);
let data_1: [u8; 4] = [1, 2, 3, 4];
let data_2: [u8; 4] = [4, 3, 2, 1];
let key = Key;
@@ -34,7 +27,7 @@ encrypt(nonce, &key, &data_2); // 🛠️❌
```
-- Owned "consumption" lets us model single-once values.
+- Owned "consumption" lets us model single-use values.
- Not implementing clone/copy here and making the interior type opaque (as per the newtype pattern) is _intentional_, as it prevents multiple uses of the same, API-controlled value.
From 88852f2fbeafea74eb74851251798e849c25c0ff Mon Sep 17 00:00:00 2001
From: tall-vase <228449146+tall-vase@users.noreply.github.com>
Date: Tue, 9 Sep 2025 10:41:43 +0100
Subject: [PATCH 07/24] Minor editing
---
.../leveraging-the-type-system/borrow-checker-invariants.md | 5 +++--
.../borrow-checker-invariants/aliasing-xor-mutability.md | 6 ++++++
.../borrow-checker-invariants/generalizing-ownership.md | 4 ++--
3 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
index bef45dd902b9..995520db10ed 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
@@ -43,9 +43,10 @@ if let Ok(opened_door) = opened_door {
-- The borrow checker has been used to prevent use-after-free and multiple mutable references up until this point.
+
+- 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.
-- This example uses the ownership & borrowing rules to model the opening and closing of a door. We can try to open a door with a key, but if it's the wrong key the door is still closed (here represented as an error) and the key persists regardless.
+- This example uses the ownership & borrowing rules to model the locking and unlocking of a door. We can try to open a door with a key, but if it's the wrong key the door is still closed (here represented as an error) and the key persists regardless.
- The rules of the borrow checker exist to prevent developers from accessing, changing, and holding onto data in memory in unpredictable ways without being so restrictive to the point where it prevents _writing software_. The underlying logical system does not "know" what memory is. All it does is enforce a specific set of rules of how different operations affect what possible later operations are.
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
index 79e362599e55..a624a12f548e 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
@@ -46,4 +46,10 @@ fn main() {
- By having the query results not public and placed behind a getter function, we can enforce the invariant "users of this API are not looking at the query results at the same time as they are writing to a transaction."
+- The example API can still be circumvented, how so?
+
+ - The user could access the transaction solely through `db.get_transaction()`, leaving the lifetime too temporary to prevent access to `db.results()`.
+ - How could we avoid this by working in other concepts from "Leveraging the Type System"?
+
+
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
index 6582539beae6..2e5f3e70c99a 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -32,7 +32,7 @@ let even_more_mut_denial = shared_use(&value); // ❌🔨
-- 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.
+- 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.
- 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 concurrency & dangling pointers, instead imagining and working within situations where the rules are the same but the meaning is slightly different.
@@ -45,7 +45,7 @@ let even_more_mut_denial = shared_use(&value); // ❌🔨
- Shared Reference. Allows aliasing but prevents mutable access while any of these exist. We can't "dispatch" to methods and functions that take mutable reference when all we have is a shared reference.
-- Important to remember that every `&T` and `&mut T` has an _implicit lifetime._ We get to avoid annotating a lot of lifetimes because the rust compiler can infer the majority of them. See: [Lifetime Elision](../../../lifetimes/lifetime-elision.md)
+- Important to remember that every `&T` and `&mut T` has an _implicit lifetime._ We get to avoid annotating a lot of lifetimes because the rust compiler can infer the majority of them. See: [Lifetime Elision](../../../lifetimes/lifetime-elision.md).
- Potentially relevant: show how we can replace a lot of the `&` and `&mut` here with `&'a` and `&'a mut`.
From 0aa4f215258f558862615acb54b43b72a29f9e97 Mon Sep 17 00:00:00 2001
From: tall-vase <228449146+tall-vase@users.noreply.github.com>
Date: Thu, 11 Sep 2025 11:02:07 +0100
Subject: [PATCH 08/24] Another editing pass
---
.../aliasing-xor-mutability.md | 23 ++++++++----
.../generalizing-ownership.md | 12 +++----
.../lifetime-connections.md | 4 +--
.../single-use-values.md | 36 +++++++++++--------
4 files changed, 46 insertions(+), 29 deletions(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
index a624a12f548e..7afcb8535286 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
@@ -38,18 +38,29 @@ fn main() {
-- Aliasing XOR Mutability is a shorthand for "we can have multiple immutable references, a single mutable reference, but not both."
+- Aliasing XOR Mutability means "we can have multiple immutable references, a single mutable reference, but not both."
-- This example shows how we can use the mutual exclusion of these kinds of references when it comes to prevent a user from reading query results while using the transaction API, something that 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.
+- This example shows how we can use the mutual exclusion of these kinds of references when it comes to dissuade a user from reading query results while using the transaction API, something that 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.
+
+- By borrowing one field of a struct under a mutable / exclusive reference we prevent access to the other fields of that struct under a shared / non-exclusive reference until the lifetime of that borrow ends.
- As laid out in [generalizing ownership](generalizing-ownership.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.
-- By having the query results not public and placed behind a getter function, we can enforce the invariant "users of this API are not looking at the query results at the same time as they are writing to a transaction."
+- In this case, having the query results not public and placed behind a getter function, we can enforce the invariant "users of this API are not looking at the query results at the same time as they are writing to a transaction."
-- The example API can still be circumvented, how so?
+
- - The user could access the transaction solely through `db.get_transaction()`, leaving the lifetime too temporary to prevent access to `db.results()`.
- - How could we avoid this by working in other concepts from "Leveraging the Type System"?
+
+The "don't look at query results while building a " API can still be circumvented, how so?
+
+
+ -
+ The user could access the transaction solely through `db.get_transaction()`, leaving the lifetime too temporary to prevent access to `db.results()`.
+
+ -
+ How could we avoid this by working in other concepts from "Leveraging the Type System"?
+
+
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
index 2e5f3e70c99a..e241d1c34b0c 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -32,20 +32,20 @@ let even_more_mut_denial = shared_use(&value); // ❌🔨
-- 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.
+- 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.
- 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 concurrency & dangling pointers, instead imagining and working within situations where the rules are the same but the meaning is slightly different.
- In rust's borrow checker we have access to three different ways of "taking" a value:
-
-- Owned value. Very permissive case of what you can do with it, but demands that nothing else is using it in any context and "drops" the value when scope ends (unless that scope returns this value) (see: RAII.)
+
+ - Owned value `T`. Very permissive case of what you can do with it, but demands that nothing else is using it in any context and drops the value when scope ends (unless that scope returns this value) (see: RAII.)
-- Mutable Reference. While holding onto a mutable reference we can still "dispatch" to methods and functions that take an immutable, shared reference of the value but only as long as we're not aliasing immutable, shared references to related data "after" that dispatch.
+ - Mutable Reference `&mut T`. While holding onto a mutable reference we can still "dispatch" to methods and functions that take an immutable, shared reference of the value but only as long as we're not aliasing immutable, shared references to related data "after" that dispatch.
-- Shared Reference. Allows aliasing but prevents mutable access while any of these exist. We can't "dispatch" to methods and functions that take mutable reference when all we have is a shared reference.
+ - Shared Reference `&T`. Allows aliasing but prevents mutable access while any of these exist. We can't "dispatch" to methods and functions that take mutable references when all we have is a shared reference.
-- Important to remember that every `&T` and `&mut T` has an _implicit lifetime._ We get to avoid annotating a lot of lifetimes because the rust compiler can infer the majority of them. See: [Lifetime Elision](../../../lifetimes/lifetime-elision.md).
+- Remember that every `&T` and `&mut T` has an _implicit lifetime._ We get to avoid annotating a lot of lifetimes because the rust compiler can infer the majority of them. See: [Lifetime Elision](../../../lifetimes/lifetime-elision.md).
- Potentially relevant: show how we can replace a lot of the `&` and `&mut` here with `&'a` and `&'a mut`.
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
index b101631df620..18888cb65c03 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
@@ -38,9 +38,9 @@ println!("{}", erased_owned_and_linked.get()); // ❌🔨
-- PhantomData lets developers "tag" types with type and lifetime parameters that are not "really" present in the struct or enum.
+- `PhantomData` lets developers "tag" types with type and lifetime parameters that are not "really" present in the struct or enum.
-- PhantomData can be used with the Typestate pattern to have data with the same structure i.e. `TaggedData` can have methods or trait implementations that `TaggedData` doesn't.
+- `PhantomData` can be used with the Typestate pattern to have data with the same structure i.e. `TaggedData` can have methods or trait implementations that `TaggedData` doesn't.
- But it can also be used to encode a connection between the lifetime of one value and another, while both values still maintain separate owned data within them.
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
index 73227d644d5a..582a3fa1cf08 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -8,35 +8,41 @@ In some circumstances we want values that can be used _exactly once_. One critic
```rust,editable
fn main() {
-pub struct Key;
+
+mod cryptography {
+ pub struct Key;
+ // Pretend this is a cryptographically unique, use-once number.
+ pub struct Nonce(u32);
+ // And pretend this is using cryptographically secure
+ pub fn new_nonce() -> Nonce { Nonce(std::time::UNIX_EPOCH.elapsed().unwrap_or_default().subsec_nanos()) }
+
+ pub fn encrypt(nonce: Nonce, key: &Key, data: &[u8]) {}
+}
-// Pretend this is a cryptographically unique, use-once number.
-pub struct Nonce(u32);
+use cryptography::*;
-let nonce = Nonce(1337);
+let nonce = new_nonce();
let data_1: [u8; 4] = [1, 2, 3, 4];
let data_2: [u8; 4] = [4, 3, 2, 1];
let key = Key;
// The key and data can be re-used, copied, etc. but the nonce cannot.
-fn encrypt(nonce: Nonce, key: &Key, data: &[u8]) {}
-
encrypt(nonce, &key, &data_1);
encrypt(nonce, &key, &data_2); // 🛠️❌
}
```
-- Owned "consumption" lets us model single-use values.
-
-- Not implementing clone/copy here and making the interior type opaque (as per the newtype pattern) is _intentional_, as it prevents multiple uses of the same, API-controlled value.
-
-- I.e. A Nonce is a additional piece of random, unique data during an encryption process that helps prevent "replay attacks".
-
-- In practice people have ended up re-using nonces in circumstances where security is important, making it possible for private key information to be derived by attackers.
+- Owned "consumption" of values lets us model things that need to be single-use.
-- By tying nonce creation and consumption up in rust's ownership model, and by not implementing clone/copy on sensitive single-use data, we can prevent this kind of dangerous misuse.
+- By keeping constructors private and not implementing clone/copy for a type and making the interior type opaque (as per the newtype pattern) is _intentional_, as it prevents multiple uses of the same, API-controlled value.
-- Cryptography Nuance: There is still the case where a nonce may be used twice if it's created through purely a pseudo-random process with no additional metadata, and that circumstance can't be avoided through this particular method. This kind of API prevents one kind of misuse, but not all kinds.
+- In the above example, a Nonce is a additional piece of random, unique data during an encryption process that helps prevent "replay attacks".
+
+ - In practice people have ended up re-using nonces in circumstances where security is important, making it possible for private key information to be derived by attackers.
+
+ - By tying nonce creation and consumption up in rust's ownership model, and by not implementing clone/copy on sensitive single-use data, we can prevent this kind of dangerous misuse.
+
+ - Cryptography Nuance: There is still the case where a nonce may be used twice if it's created through purely a pseudo-random process with no additional metadata, and that circumstance can't be avoided through this particular method. This kind of API prevents one kind of misuse, but not all kinds.
\ No newline at end of file
From 37e69fefa1d39140258f25919f7e98e0cda2f449 Mon Sep 17 00:00:00 2001
From: tall-vase <228449146+tall-vase@users.noreply.github.com>
Date: Thu, 11 Sep 2025 11:03:05 +0100
Subject: [PATCH 09/24] minor grammar edit
---
.../borrow-checker-invariants/single-use-values.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
index 582a3fa1cf08..943777b7c958 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -35,7 +35,7 @@ encrypt(nonce, &key, &data_2); // 🛠️❌
- Owned "consumption" of values lets us model things that need to be single-use.
-- By keeping constructors private and not implementing clone/copy for a type and making the interior type opaque (as per the newtype pattern) is _intentional_, as it prevents multiple uses of the same, API-controlled value.
+- By keeping constructors private and not implementing clone/copy for a type, making the interior type opaque (as per the newtype pattern), we can prevent multiple uses of the same, API-controlled value.
- In the above example, a Nonce is a additional piece of random, unique data during an encryption process that helps prevent "replay attacks".
From 595ca8e6cb122742fac636026ac605c67c8b1e00 Mon Sep 17 00:00:00 2001
From: tall-vase <228449146+tall-vase@users.noreply.github.com>
Date: Mon, 15 Sep 2025 12:59:20 +0100
Subject: [PATCH 10/24] Another editing pass
---
.../borrow-checker-invariants.md | 8 ++-----
.../aliasing-xor-mutability.md | 4 ++--
.../generalizing-ownership.md | 4 +---
.../lifetime-connections.md | 22 ++++++++++++-------
.../single-use-values.md | 11 ++++++----
5 files changed, 26 insertions(+), 23 deletions(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
index 995520db10ed..802727158694 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
@@ -48,12 +48,8 @@ if let Ok(opened_door) = opened_door {
- This example uses the ownership & borrowing rules to model the locking and unlocking of a door. We can try to open a door with a key, but if it's the wrong key the door is still closed (here represented as an error) and the key persists regardless.
-- The rules of the borrow checker exist to prevent developers from accessing, changing, and holding onto data in memory in unpredictable ways without being so restrictive to the point where it prevents _writing software_. The underlying logical system does not "know" what memory is. All it does is enforce a specific set of rules of how different operations affect what possible later operations are.
+- The rules of the borrow checker exist to prevent developers from accessing, changing, and holding onto data in memory in unpredictable ways without being so restrictive that it would prevent _writing software_. The underlying logical system does not "know" what memory is. All it does is enforce a specific set of rules of how different operations affect what later operations are possible.
-- Those rules can apply to many other cases, so we can piggy-back onto the rules of the borrow checker to design APIs to be harder or impossible to misuse. Even when there's little or no actual "memory safety" concerns in the problem domain.
-
-- This section will walk through some of those different domains.
-
-- Interior, private mutability or reference counting in data types may let an API designer shift the meaning of ownership to a different (but analogous) set of semantics as they need to, even to the point where some API designers have managed to model self-referential types this way.
+- Those rules can apply to many other cases: We can piggy-back onto the rules of the borrow checker to design APIs to be harder or impossible to misuse, even when there's little or no "memory safety" concerns in the problem domain. This section will walk through some of those different domains.
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
index 7afcb8535286..3b340e1d0533 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
@@ -40,7 +40,7 @@ fn main() {
- Aliasing XOR Mutability means "we can have multiple immutable references, a single mutable reference, but not both."
-- This example shows how we can use the mutual exclusion of these kinds of references when it comes to dissuade a user from reading query results while using the transaction API, something that 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.
+- This example shows how we can use the mutual exclusion of these kinds of references to dissuade a user from reading query results while using a transaction API, something that 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.
- By borrowing one field of a struct under a mutable / exclusive reference we prevent access to the other fields of that struct under a shared / non-exclusive reference until the lifetime of that borrow ends.
@@ -51,7 +51,7 @@ fn main() {
-The "don't look at query results while building a " API can still be circumvented, how so?
+The "don't look at query results while building a transaction" invariant can still be circumvented, how so?
-
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
index e241d1c34b0c..ecdbe251a3a2 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -39,7 +39,7 @@ let even_more_mut_denial = shared_use(&value); // ❌🔨
- In rust's borrow checker we have access to three different ways of "taking" a value:
- - Owned value `T`. Very permissive case of what you can do with it, but demands that nothing else is using it in any context and drops the value when scope ends (unless that scope returns this value) (see: RAII.)
+ - Owned value `T`. Very permissive case, to the point where mutability can be re-set, but demands that nothing else is using it in any context and drops the value when scope ends (unless that scope returns this value) (see: RAII.)
- Mutable Reference `&mut T`. While holding onto a mutable reference we can still "dispatch" to methods and functions that take an immutable, shared reference of the value but only as long as we're not aliasing immutable, shared references to related data "after" that dispatch.
@@ -47,6 +47,4 @@ let even_more_mut_denial = shared_use(&value); // ❌🔨
- Remember that every `&T` and `&mut T` has an _implicit lifetime._ We get to avoid annotating a lot of lifetimes because the rust compiler can infer the majority of them. See: [Lifetime Elision](../../../lifetimes/lifetime-elision.md).
-- Potentially relevant: show how we can replace a lot of the `&` and `&mut` here with `&'a` and `&'a mut`.
-
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
index 18888cb65c03..0063d03530e8 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
@@ -39,21 +39,27 @@ println!("{}", erased_owned_and_linked.get()); // ❌🔨
- `PhantomData` lets developers "tag" types with type and lifetime parameters that are not "really" present in the struct or enum.
-
-- `PhantomData` can be used with the Typestate pattern to have data with the same structure i.e. `TaggedData` can have methods or trait implementations that `TaggedData` doesn't.
-
-- But it can also be used to encode a connection between the lifetime of one value and another, while both values still maintain separate owned data within them.
+
+ `PhantomData` can be used with the Typestate pattern to have data with the same structure i.e. `TaggedData` can have methods or trait implementations that `TaggedData` doesn't.
+
+ It can also be used to encode a connection between the lifetime of one value and another, while both values still maintain separate owned data within them.
- This is really useful for modelling a bunch of relationships between data, where we want to establish that while a type has owned values within it is still connected to another piece of data and can only live as long as it.
-- Consider a case where you want to return owned data from a method, but you don't want that data to live longer than the value that created it.
+ Consider a case where you want to return owned data from a method, but you don't want that data to live longer than the value that created it.
+
+- Lifetimes need to come from somewhere! We can't build functions of the form `fn lifetime_shenanigans<'a>(owned: OwnedData) -> &'b Data` (without tying `'b` to `'a` in some way).
+
+ Lifetime elision hides where a lot of lifetimes come from, but that doesn't mean the explicitly named lifetimes "come from nowhere."
+
+ Suggestion: Show off un-eliding the lifetimes in `get_erased` in this example.
-- [`BorrowedFd`](https://rust-lang.github.io/rfcs/3128-io-safety.html#ownedfd-and-borrowedfdfd) uses these captured lifetimes to enforce the invariant that "if this file descriptor exists, the OS file descriptor is still open" because a `BorrowedFd`'s lifetime parameter demands that there exists another value in your program that has the same lifetime as it, and this has been encoded by the API designer to mean _that value is what keeps the access to the file open_. Its counterpart `OwnedFd` is instead a file descriptor that closes that file on drop.
+- [`BorrowedFd`](https://rust-lang.github.io/rfcs/3128-io-safety.html#ownedfd-and-borrowedfdfd) uses these captured lifetimes to enforce the invariant that "if this file descriptor exists, the OS file descriptor is still open" because a `BorrowedFd`'s lifetime parameter demands that there exists another value in your program that has the same lifetime as it, and this has been encoded by the API designer to mean _that value is what keeps the access to the file open_.
-- Lifetimes need to come from somewhere! We can't build functions of the form `fn lifetime_shenanigans<'a>(owned: OwnedData) -> &'b Data` (without tying `'b` to `'a` in some way). Lifetime elision hides where a lot of lifetimes come from, but that doesn't mean the explicitly named lifetimes "come from nowhere."
+ Its counterpart `OwnedFd` is instead a file descriptor that closes that file on drop.
- This way of encoding information in types is _exceptionally powerful_ when combined with unsafe, as the ways one can manipulate lifetimes becomes almost arbitrary. This is also dangerous, but when combined with tools like external, mechanically-verified proofs _we can safely encode cyclic/self-referential types while encoding lifetime & safety expectations in the relevant data types._
-- The [GhostCell (2021)](https://plv.mpi-sws.org/rustbelt/ghostcell/) paper and its [relevant implementation](https://gitlab.mpi-sws.org/FP/ghostcell) show this kind of work off. While the borrow checker is restrictive, there are still ways to use escape hatches and then _show that the ways you used those escape hatches are consistent and safe._
+ The [GhostCell (2021)](https://plv.mpi-sws.org/rustbelt/ghostcell/) paper and its [relevant implementation](https://gitlab.mpi-sws.org/FP/ghostcell) show this kind of work off. While the borrow checker is restrictive, there are still ways to use escape hatches and then _show that the ways you used those escape hatches are consistent and safe._
\ No newline at end of file
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
index 943777b7c958..80612436ef8d 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -8,14 +8,17 @@ In some circumstances we want values that can be used _exactly once_. One critic
```rust,editable
fn main() {
-
+
mod cryptography {
pub struct Key;
- // Pretend this is a cryptographically unique, use-once number.
+ // Pretend this is a cryptographically sound, single-use number.
pub struct Nonce(u32);
- // And pretend this is using cryptographically secure
- pub fn new_nonce() -> Nonce { Nonce(std::time::UNIX_EPOCH.elapsed().unwrap_or_default().subsec_nanos()) }
+ // And pretend this is cryptographically sound random generator function.
+ pub fn new_nonce() -> Nonce {
+ Nonce(std::time::UNIX_EPOCH.elapsed().unwrap_or_default().subsec_nanos())
+ }
+ // We consume a nonce, but not the key or the data.
pub fn encrypt(nonce: Nonce, key: &Key, data: &[u8]) {}
}
From 2a8874863d9f1e9cd6f73b0823508f3318063c02 Mon Sep 17 00:00:00 2001
From: tall-vase
Date: Mon, 22 Sep 2025 11:47:34 +0100
Subject: [PATCH 11/24] Formatting pass
---
.../borrow-checker-invariants.md | 89 +++++++++-----
.../aliasing-xor-mutability.md | 45 +++++--
.../generalizing-ownership.md | 47 +++++---
.../lifetime-connections.md | 114 ++++++++++++------
.../single-use-values.md | 71 ++++++-----
5 files changed, 240 insertions(+), 126 deletions(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
index 802727158694..9f5c34f8c4a3 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
@@ -4,52 +4,77 @@ minutes: 0
# Using the Borrow checker to enforce Invariants
-The logic of the borrow checker, while tied to "memory ownership", can be abstracted away from this central use case to model other problems and prevent API misuse.
+The logic of the borrow checker, while tied to "memory ownership", can be
+abstracted away from this central use case to model other problems and prevent
+API misuse.
```rust,editable
fn main() {
-// Doors can be open or closed, and you need the right key to lock or unlock one.
-// Modelled with Shared Key and Owned Door. Nothing to do with "memory safety"!
-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 {
- if door.lock_shape == key.key_shape {
- Ok(OpenDoor{lock_shape: door.lock_shape})
- } else {
- Err(door)
+ // Doors can be open or closed, and you need the right key to lock or unlock one.
+ // Modelled with Shared Key and Owned Door. Nothing to do with "memory safety"!
+ pub struct DoorKey {
+ pub key_shape: u32,
+ }
+ pub struct LockedDoor {
+ lock_shape: u32,
+ }
+ pub struct OpenDoor {
+ lock_shape: u32,
}
-}
-fn close_door(key: &DoorKey, door: OpenDoor) -> Result {
- if door.lock_shape == key.key_shape {
- Ok(LockedDoor{lock_shape: door.lock_shape})
- } else {
- Err(door)
+ fn open_door(key: &DoorKey, door: LockedDoor) -> Result {
+ if door.lock_shape == key.key_shape {
+ Ok(OpenDoor { lock_shape: door.lock_shape })
+ } else {
+ Err(door)
+ }
}
-}
-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);
-}
+ fn close_door(key: &DoorKey, door: OpenDoor) -> Result {
+ if door.lock_shape == key.key_shape {
+ Ok(LockedDoor { lock_shape: door.lock_shape })
+ } else {
+ Err(door)
+ }
+ }
+
+ 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
+ );
+ }
}
```
-- 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.
-- This example uses the ownership & borrowing rules to model the locking and unlocking of a door. We can try to open a door with a key, but if it's the wrong key the door is still closed (here represented as an error) and the key persists regardless.
+- 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.
+
+- This example uses the ownership & borrowing rules to model the locking and
+ unlocking of a door. We can try to open a door with a key, but if it's the
+ wrong key the door is still closed (here represented as an error) and the key
+ persists regardless.
-- The rules of the borrow checker exist to prevent developers from accessing, changing, and holding onto data in memory in unpredictable ways without being so restrictive that it would prevent _writing software_. The underlying logical system does not "know" what memory is. All it does is enforce a specific set of rules of how different operations affect what later operations are possible.
+- The rules of the borrow checker exist to prevent developers from accessing,
+ changing, and holding onto data in memory in unpredictable ways without being
+ so restrictive that it would prevent _writing software_. The underlying
+ logical system does not "know" what memory is. All it does is enforce a
+ specific set of rules of how different operations affect what later operations
+ are possible.
-- Those rules can apply to many other cases: We can piggy-back onto the rules of the borrow checker to design APIs to be harder or impossible to misuse, even when there's little or no "memory safety" concerns in the problem domain. This section will walk through some of those different domains.
+- Those rules can apply to many other cases: We can piggy-back onto the rules of
+ the borrow checker to design APIs to be harder or impossible to misuse, even
+ when there's little or no "memory safety" concerns in the problem domain. This
+ section will walk through some of those different domains.
-
\ No newline at end of file
+
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
index 3b340e1d0533..125f0774dd7c 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
@@ -4,7 +4,8 @@ minutes: 0
# Mutually Exclusive References, or "Aliasing XOR Mutability"
-We can use the mutual exclusion of `&T` and `&mut T` references for a single value to model some constraints.
+We can use the mutual exclusion of `&T` and `&mut T` references for a single
+value to model some constraints.
```rust,editable
pub struct Transaction(/* some kind of interior state */);
@@ -16,10 +17,21 @@ pub struct DatabaseConnection {
}
impl DatabaseConnection {
- pub fn new() -> Self { Self { transaction: Transaction(/* again, pretend there's some interior state */), query_results: vec![] } }
- pub fn get_transaction(&mut self) -> &mut Transaction { &mut self.transaction }
- pub fn results(&self) -> &[QueryResult] { &self.query_results }
- pub fn commit(&mut self) { println!("Transaction committed!") }
+ pub fn new() -> Self {
+ Self {
+ transaction: Transaction(/* again, pretend there's some interior state */),
+ query_results: vec![],
+ }
+ }
+ pub fn get_transaction(&mut self) -> &mut Transaction {
+ &mut self.transaction
+ }
+ pub fn results(&self) -> &[QueryResult] {
+ &self.query_results
+ }
+ pub fn commit(&mut self) {
+ println!("Transaction committed!")
+ }
}
pub fn do_something_with_transaction(transaction: &mut Transaction) {}
@@ -38,15 +50,26 @@ fn main() {
-- Aliasing XOR Mutability means "we can have multiple immutable references, a single mutable reference, but not both."
+- Aliasing XOR Mutability means "we can have multiple immutable references, a
+ single mutable reference, but not both."
-- This example shows how we can use the mutual exclusion of these kinds of references to dissuade a user from reading query results while using a transaction API, something that 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.
+- This example shows how we can use the mutual exclusion of these kinds of
+ references to dissuade a user from reading query results while using a
+ transaction API, something that 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.
-- By borrowing one field of a struct under a mutable / exclusive reference we prevent access to the other fields of that struct under a shared / non-exclusive reference until the lifetime of that borrow ends.
+- By borrowing one field of a struct under a mutable / exclusive reference we
+ prevent access to the other fields of that struct under a shared /
+ non-exclusive reference until the lifetime of that borrow ends.
-- As laid out in [generalizing ownership](generalizing-ownership.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.
+- As laid out in [generalizing ownership](generalizing-ownership.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.
-- In this case, having the query results not public and placed behind a getter function, we can enforce the invariant "users of this API are not looking at the query results at the same time as they are writing to a transaction."
+- In this case, having the query results not public and placed behind a getter
+ function, we can enforce the invariant "users of this API are not looking at
+ the query results at the same time as they are writing to a transaction."
@@ -63,4 +86,4 @@ The "don't look at query results while building a transaction" invariant can sti
-
\ No newline at end of file
+
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
index ecdbe251a3a2..5868504d203a 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -4,7 +4,9 @@ minutes: 0
# Generalizing Ownership
-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.
+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.
@@ -30,21 +32,38 @@ deny_future_use(value);
let even_more_mut_denial = shared_use(&value); // ❌🔨
```
-
+
-- 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.
+- 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.
-- 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 concurrency & dangling pointers, instead imagining and working within situations where the rules are the same but the meaning is slightly different.
+- 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 concurrency & dangling pointers, instead imagining and working within
+ situations where the rules are the same but the meaning is slightly different.
-- In rust's borrow checker we have access to three different ways of "taking" a value:
+- In rust's borrow checker we have access to three different ways of "taking" a
+ value:
- - Owned value `T`. Very permissive case, to the point where mutability can be re-set, but demands that nothing else is using it in any context and drops the value when scope ends (unless that scope returns this value) (see: RAII.)
-
- - Mutable Reference `&mut T`. While holding onto a mutable reference we can still "dispatch" to methods and functions that take an immutable, shared reference of the value but only as long as we're not aliasing immutable, shared references to related data "after" that dispatch.
-
- - Shared Reference `&T`. Allows aliasing but prevents mutable access while any of these exist. We can't "dispatch" to methods and functions that take mutable references when all we have is a shared reference.
-
-- Remember that every `&T` and `&mut T` has an _implicit lifetime._ We get to avoid annotating a lot of lifetimes because the rust compiler can infer the majority of them. See: [Lifetime Elision](../../../lifetimes/lifetime-elision.md).
-
-
\ No newline at end of file
+ - Owned value `T`. Very permissive case, to the point where mutability can be
+ re-set, but demands that nothing else is using it in any context and drops
+ the value when scope ends (unless that scope returns this value) (see:
+ RAII.)
+
+ - Mutable Reference `&mut T`. While holding onto a mutable reference we can
+ still "dispatch" to methods and functions that take an immutable, shared
+ reference of the value but only as long as we're not aliasing immutable,
+ shared references to related data "after" that dispatch.
+
+ - Shared Reference `&T`. Allows aliasing but prevents mutable access while any
+ of these exist. We can't "dispatch" to methods and functions that take
+ mutable references when all we have is a shared reference.
+
+- Remember that every `&T` and `&mut T` has an _implicit lifetime._ We get to
+ avoid annotating a lot of lifetimes because the rust compiler can infer the
+ majority of them. See:
+ [Lifetime Elision](../../../lifetimes/lifetime-elision.md).
+
+
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
index 0063d03530e8..ffef98198a95 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
@@ -4,62 +4,98 @@ minutes: 0
# Lifetime "Connections" & External Resources
-Using `PhantomData` in conjunction with lifetimes lets us say "this value may own its data, but it can only live as long as the value that generated it" in rust's type system.
+Using `PhantomData` in conjunction with lifetimes lets us say "this value may
+own its data, but it can only live as long as the value that generated it" in
+rust's type system.
```rust,editable
fn main() {
-use std::marker::PhantomData;
-pub struct Tag;
-pub struct ErasedData<'a>{data: String, _phantom: PhantomData<&'a ()>}
-impl <'a> ErasedData<'a> {
- pub fn get(&self) -> &str {
- &self.data
+ use std::marker::PhantomData;
+ pub struct Tag;
+ pub struct ErasedData<'a> {
+ data: String,
+ _phantom: PhantomData<&'a ()>,
}
-}
-pub struct TaggedData{data: String, _phantom: PhantomData}
-impl TaggedData {
- pub fn new(data: String) -> Self {Self {data, _phantom: PhantomData} }
- pub fn consume(self) {}
- pub fn get_erased(&self) -> ErasedData<'_> {
- // has an owned String, but _phantom holds onto the lifetime of the TaggedData
- // that created it.
- ErasedData { data: self.data.clone(), _phantom: PhantomData }
+ impl<'a> ErasedData<'a> {
+ pub fn get(&self) -> &str {
+ &self.data
+ }
+ }
+ pub struct TaggedData {
+ data: String,
+ _phantom: PhantomData,
+ }
+ impl TaggedData {
+ pub fn new(data: String) -> Self {
+ Self { data, _phantom: PhantomData }
+ }
+ pub fn consume(self) {}
+ pub fn get_erased(&self) -> ErasedData<'_> {
+ // has an owned String, but _phantom holds onto the lifetime of the TaggedData
+ // that created it.
+ ErasedData { data: self.data.clone(), _phantom: PhantomData }
+ }
}
-}
-let tagged_data: TaggedData = TaggedData::new("Real Data".to_owned());
-// Get the erased-but-still-linked data.
-let erased_owned_and_linked = tagged_data.get_erased();
-tagged_data.consume();
-// The data is owned by `erased_owned_and_linked` but still connected to `tagged_data`.
-println!("{}", erased_owned_and_linked.get()); // ❌🔨
+ let tagged_data: TaggedData = TaggedData::new("Real Data".to_owned());
+ // Get the erased-but-still-linked data.
+ let erased_owned_and_linked = tagged_data.get_erased();
+ tagged_data.consume();
+ // The data is owned by `erased_owned_and_linked` but still connected to `tagged_data`.
+ println!("{}", erased_owned_and_linked.get()); // ❌🔨
}
```
-- `PhantomData` lets developers "tag" types with type and lifetime parameters that are not "really" present in the struct or enum.
-
- `PhantomData` can be used with the Typestate pattern to have data with the same structure i.e. `TaggedData` can have methods or trait implementations that `TaggedData` doesn't.
-
- It can also be used to encode a connection between the lifetime of one value and another, while both values still maintain separate owned data within them.
+- `PhantomData` lets developers "tag" types with type and lifetime parameters
+ that are not "really" present in the struct or enum.
+
+ `PhantomData` can be used with the Typestate pattern to have data with the
+ same structure i.e. `TaggedData` can have methods or trait
+ implementations that `TaggedData` doesn't.
+
+ It can also be used to encode a connection between the lifetime of one value
+ and another, while both values still maintain separate owned data within them.
+
+- This is really useful for modelling a bunch of relationships between data,
+ where we want to establish that while a type has owned values within it is
+ still connected to another piece of data and can only live as long as it.
+
+ Consider a case where you want to return owned data from a method, but you
+ don't want that data to live longer than the value that created it.
-- This is really useful for modelling a bunch of relationships between data, where we want to establish that while a type has owned values within it is still connected to another piece of data and can only live as long as it.
+- Lifetimes need to come from somewhere! We can't build functions of the form
+ `fn lifetime_shenanigans<'a>(owned: OwnedData) -> &'b Data` (without tying
+ `'b` to `'a` in some way).
- Consider a case where you want to return owned data from a method, but you don't want that data to live longer than the value that created it.
+ Lifetime elision hides where a lot of lifetimes come from, but that doesn't
+ mean the explicitly named lifetimes "come from nowhere."
-- Lifetimes need to come from somewhere! We can't build functions of the form `fn lifetime_shenanigans<'a>(owned: OwnedData) -> &'b Data` (without tying `'b` to `'a` in some way).
-
- Lifetime elision hides where a lot of lifetimes come from, but that doesn't mean the explicitly named lifetimes "come from nowhere."
-
Suggestion: Show off un-eliding the lifetimes in `get_erased` in this example.
-- [`BorrowedFd`](https://rust-lang.github.io/rfcs/3128-io-safety.html#ownedfd-and-borrowedfdfd) uses these captured lifetimes to enforce the invariant that "if this file descriptor exists, the OS file descriptor is still open" because a `BorrowedFd`'s lifetime parameter demands that there exists another value in your program that has the same lifetime as it, and this has been encoded by the API designer to mean _that value is what keeps the access to the file open_.
+- [`BorrowedFd`](https://rust-lang.github.io/rfcs/3128-io-safety.html#ownedfd-and-borrowedfdfd)
+ uses these captured lifetimes to enforce the invariant that "if this file
+ descriptor exists, the OS file descriptor is still open" because a
+ `BorrowedFd`'s lifetime parameter demands that there exists another value in
+ your program that has the same lifetime as it, and this has been encoded by
+ the API designer to mean _that value is what keeps the access to the file
+ open_.
- Its counterpart `OwnedFd` is instead a file descriptor that closes that file on drop.
+ Its counterpart `OwnedFd` is instead a file descriptor that closes that file
+ on drop.
-- This way of encoding information in types is _exceptionally powerful_ when combined with unsafe, as the ways one can manipulate lifetimes becomes almost arbitrary. This is also dangerous, but when combined with tools like external, mechanically-verified proofs _we can safely encode cyclic/self-referential types while encoding lifetime & safety expectations in the relevant data types._
+- This way of encoding information in types is _exceptionally powerful_ when
+ combined with unsafe, as the ways one can manipulate lifetimes becomes almost
+ arbitrary. This is also dangerous, but when combined with tools like external,
+ mechanically-verified proofs _we can safely encode cyclic/self-referential
+ types while encoding lifetime & safety expectations in the relevant data
+ types._
- The [GhostCell (2021)](https://plv.mpi-sws.org/rustbelt/ghostcell/) paper and its [relevant implementation](https://gitlab.mpi-sws.org/FP/ghostcell) show this kind of work off. While the borrow checker is restrictive, there are still ways to use escape hatches and then _show that the ways you used those escape hatches are consistent and safe._
+ The [GhostCell (2021)](https://plv.mpi-sws.org/rustbelt/ghostcell/) paper and
+ its [relevant implementation](https://gitlab.mpi-sws.org/FP/ghostcell) show
+ this kind of work off. While the borrow checker is restrictive, there are
+ still ways to use escape hatches and then _show that the ways you used those
+ escape hatches are consistent and safe._
-
\ No newline at end of file
+
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
index 80612436ef8d..549cd160d94d 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -4,48 +4,59 @@ minutes: 0
# Single-use values
-In some circumstances we want values that can be used _exactly once_. One critical example of this is in cryptography: "Nonces."
+In some circumstances we want values that can be used _exactly once_. One
+critical example of this is in cryptography: "Nonces."
```rust,editable
fn main() {
-
-mod cryptography {
- pub struct Key;
- // Pretend this is a cryptographically sound, single-use number.
- pub struct Nonce(u32);
- // And pretend this is cryptographically sound random generator function.
- pub fn new_nonce() -> Nonce {
- Nonce(std::time::UNIX_EPOCH.elapsed().unwrap_or_default().subsec_nanos())
+ mod cryptography {
+ pub struct Key;
+ // Pretend this is a cryptographically sound, single-use number.
+ pub struct Nonce(u32);
+ // And pretend this is cryptographically sound random generator function.
+ pub fn new_nonce() -> Nonce {
+ Nonce(std::time::UNIX_EPOCH.elapsed().unwrap_or_default().subsec_nanos())
+ }
+
+ // We consume a nonce, but not the key or the data.
+ pub fn encrypt(nonce: Nonce, key: &Key, data: &[u8]) {}
}
-
- // We consume a nonce, but not the key or the data.
- pub fn encrypt(nonce: Nonce, key: &Key, data: &[u8]) {}
-}
-use cryptography::*;
+ use cryptography::*;
-let nonce = new_nonce();
-let data_1: [u8; 4] = [1, 2, 3, 4];
-let data_2: [u8; 4] = [4, 3, 2, 1];
-let key = Key;
+ let nonce = new_nonce();
+ let data_1: [u8; 4] = [1, 2, 3, 4];
+ let data_2: [u8; 4] = [4, 3, 2, 1];
+ let key = Key;
-// The key and data can be re-used, copied, etc. but the nonce cannot.
-encrypt(nonce, &key, &data_1);
-encrypt(nonce, &key, &data_2); // 🛠️❌
+ // The key and data can be re-used, copied, etc. but the nonce cannot.
+ encrypt(nonce, &key, &data_1);
+ encrypt(nonce, &key, &data_2); // 🛠️❌
}
```
+
- Owned "consumption" of values lets us model things that need to be single-use.
-- By keeping constructors private and not implementing clone/copy for a type, making the interior type opaque (as per the newtype pattern), we can prevent multiple uses of the same, API-controlled value.
+- By keeping constructors private and not implementing clone/copy for a type,
+ making the interior type opaque (as per the newtype pattern), we can prevent
+ multiple uses of the same, API-controlled value.
+
+- In the above example, a Nonce is a additional piece of random, unique data
+ during an encryption process that helps prevent "replay attacks".
+
+ - In practice people have ended up re-using nonces in circumstances where
+ security is important, making it possible for private key information to be
+ derived by attackers.
+
+ - By tying nonce creation and consumption up in rust's ownership model, and by
+ not implementing clone/copy on sensitive single-use data, we can prevent
+ this kind of dangerous misuse.
-- In the above example, a Nonce is a additional piece of random, unique data during an encryption process that helps prevent "replay attacks".
-
- - In practice people have ended up re-using nonces in circumstances where security is important, making it possible for private key information to be derived by attackers.
-
- - By tying nonce creation and consumption up in rust's ownership model, and by not implementing clone/copy on sensitive single-use data, we can prevent this kind of dangerous misuse.
-
- - Cryptography Nuance: There is still the case where a nonce may be used twice if it's created through purely a pseudo-random process with no additional metadata, and that circumstance can't be avoided through this particular method. This kind of API prevents one kind of misuse, but not all kinds.
+ - Cryptography Nuance: There is still the case where a nonce may be used twice
+ if it's created through purely a pseudo-random process with no additional
+ metadata, and that circumstance can't be avoided through this particular
+ method. This kind of API prevents one kind of misuse, but not all kinds.
-
\ No newline at end of file
+
From e7f874b2cce587f4ffbace2e5a31689d9c971ffe Mon Sep 17 00:00:00 2001
From: tall-vase
Date: Wed, 24 Sep 2025 15:55:17 +0100
Subject: [PATCH 12/24] fix test errors, make sure compilation succeeds after
erroneous lines are commented out.
---
.../aliasing-xor-mutability.md | 2 +-
.../generalizing-ownership.md | 18 +++++++++---------
.../lifetime-connections.md | 2 +-
.../single-use-values.md | 2 +-
4 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
index 125f0774dd7c..1f8fe79c1d33 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
@@ -7,7 +7,7 @@ minutes: 0
We can use the mutual exclusion of `&T` and `&mut T` references for a single
value to model some constraints.
-```rust,editable
+```rust,editable,compile_fail
pub struct Transaction(/* some kind of interior state */);
pub struct QueryResult(String);
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
index 5868504d203a..448f1085b16a 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -8,7 +8,7 @@ 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
+```rust,editable,compile_fail
// An internal data type to have something to hold onto.
pub struct Internal;
// The "outer" data.
@@ -17,19 +17,19 @@ 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) {}
-let mut value = Data(Internal);
-let deny_mut = shared_use(&value);
-let try_to_deny_immutable = exclusive_use(&mut value); // ❌🔨
-let more_mut_denial = &deny_mut;
-deny_future_use(value);
-let even_more_mut_denial = shared_use(&value); // ❌🔨
+fn main() {
+ let mut value = Data(Internal);
+ let deny_mut = shared_use(&value);
+ let try_to_deny_immutable = exclusive_use(&mut value); // ❌🔨
+ let more_mut_denial = &deny_mut;
+ deny_future_use(value);
+ let even_more_mut_denial = shared_use(&value); // ❌🔨
+}
```
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
index ffef98198a95..3ed93736dc40 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
@@ -8,7 +8,7 @@ Using `PhantomData` in conjunction with lifetimes lets us say "this value may
own its data, but it can only live as long as the value that generated it" in
rust's type system.
-```rust,editable
+```rust,editable,compile_fail
fn main() {
use std::marker::PhantomData;
pub struct Tag;
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
index 549cd160d94d..cfcb1bf19dc6 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -7,7 +7,7 @@ minutes: 0
In some circumstances we want values that can be used _exactly once_. One
critical example of this is in cryptography: "Nonces."
-```rust,editable
+```rust,editable,compile_fail
fn main() {
mod cryptography {
pub struct Key;
From 808de4f1059d02c18cc051b0a1897be53e981844 Mon Sep 17 00:00:00 2001
From: tall-vase
Date: Wed, 24 Sep 2025 16:06:51 +0100
Subject: [PATCH 13/24] Address lints
---
.../leveraging-the-type-system/borrow-checker-invariants.md | 4 ++--
.../borrow-checker-invariants/generalizing-ownership.md | 2 +-
.../borrow-checker-invariants/lifetime-connections.md | 6 +++---
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
index 9f5c34f8c4a3..ee0148242fcd 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
@@ -10,8 +10,8 @@ API misuse.
```rust,editable
fn main() {
- // Doors can be open or closed, and you need the right key to lock or unlock one.
- // Modelled with Shared Key and Owned Door. Nothing to do with "memory safety"!
+ // 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,
}
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
index 448f1085b16a..d8cd3b5e186f 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -24,7 +24,7 @@ fn deny_future_use(value: Data) {}
fn main() {
let mut value = Data(Internal);
- let deny_mut = shared_use(&value);
+ let deny_mut = shared_use(&value);
let try_to_deny_immutable = exclusive_use(&mut value); // ❌🔨
let more_mut_denial = &deny_mut;
deny_future_use(value);
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
index 3ed93736dc40..03ffc3f12152 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
@@ -31,8 +31,8 @@ fn main() {
}
pub fn consume(self) {}
pub fn get_erased(&self) -> ErasedData<'_> {
- // has an owned String, but _phantom holds onto the lifetime of the TaggedData
- // that created it.
+ // has an owned String, but _phantom holds onto the lifetime of the
+ // TaggedData that created it.
ErasedData { data: self.data.clone(), _phantom: PhantomData }
}
}
@@ -41,7 +41,7 @@ fn main() {
// Get the erased-but-still-linked data.
let erased_owned_and_linked = tagged_data.get_erased();
tagged_data.consume();
- // The data is owned by `erased_owned_and_linked` but still connected to `tagged_data`.
+ // Owned by `erased_owned_and_linked` but still connected to `tagged_data`.
println!("{}", erased_owned_and_linked.get()); // ❌🔨
}
```
From 85b70f0714b184828b0fa4b4505db86672798421 Mon Sep 17 00:00:00 2001
From: tall-vase
Date: Wed, 24 Sep 2025 16:12:39 +0100
Subject: [PATCH 14/24] Address lints
---
.../leveraging-the-type-system/borrow-checker-invariants.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
index ee0148242fcd..85128bb54429 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
@@ -10,7 +10,7 @@ API misuse.
```rust,editable
fn main() {
- // Doors can be open or closed, and you need the right key to lock or unlock
+ // 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,
From 44dff2a46f063be4f877b7fca05aab0d308cb8f7 Mon Sep 17 00:00:00 2001
From: tall-vase <228449146+tall-vase@users.noreply.github.com>
Date: Fri, 10 Oct 2025 09:09:06 +0100
Subject: [PATCH 15/24] Apply suggestions from code review
Co-authored-by: Dmitri Gribenko
---
.../borrow-checker-invariants/aliasing-xor-mutability.md | 2 +-
.../borrow-checker-invariants/generalizing-ownership.md | 2 +-
.../borrow-checker-invariants/single-use-values.md | 8 +++-----
3 files changed, 5 insertions(+), 7 deletions(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
index 1f8fe79c1d33..bba131a2ffa4 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
@@ -8,7 +8,7 @@ We can use the mutual exclusion of `&T` and `&mut T` references for a single
value to model some constraints.
```rust,editable,compile_fail
-pub struct Transaction(/* some kind of interior state */);
+pub struct Transaction(/* specifics omitted */);
pub struct QueryResult(String);
pub struct DatabaseConnection {
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
index d8cd3b5e186f..909c1b1ddb16 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -40,7 +40,7 @@ fn main() {
- 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 concurrency & dangling pointers, instead imagining and working within
+ of preventing use-after-frees and data races, instead imagining and working within
situations where the rules are the same but the meaning is slightly different.
- In rust's borrow checker we have access to three different ways of "taking" a
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
index cfcb1bf19dc6..1426b4ee923e 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -43,12 +43,10 @@ fn main() {
making the interior type opaque (as per the newtype pattern), we can prevent
multiple uses of the same, API-controlled value.
-- In the above example, a Nonce is a additional piece of random, unique data
- during an encryption process that helps prevent "replay attacks".
+- A nonce is a piece of random, unique data
+ used in cryptographic protocols to prevent replay attacks.
- - In practice people have ended up re-using nonces in circumstances where
- security is important, making it possible for private key information to be
- derived by attackers.
+ - In practice people have ended up accidentally re-using nonces. Most commonly, this causes the cryptographic protocol to completely break down and stop fulfilling its function. Depending on the specifics of nonce reuse and cryptography at hand, private keys can also become computable by attackers.
- By tying nonce creation and consumption up in rust's ownership model, and by
not implementing clone/copy on sensitive single-use data, we can prevent
From ebf00a2d539067ace47a30b0a0d2f0dfb20e7fd1 Mon Sep 17 00:00:00 2001
From: tall-vase
Date: Fri, 10 Oct 2025 16:46:40 +0100
Subject: [PATCH 16/24] Partially address feedback
---
.../borrow-checker-invariants.md | 50 ++++++++---------
.../aliasing-xor-mutability.md | 54 +++++++++++--------
.../generalizing-ownership.md | 32 ++++++++---
.../lifetime-connections.md | 54 ++++++++++---------
.../borrow-checker-invariants/phantomdata.md | 37 +++++++++++++
.../single-use-values.md | 46 ++++++++--------
6 files changed, 172 insertions(+), 101 deletions(-)
create mode 100644 src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata.md
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
index 85128bb54429..4dd41a42fe85 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
@@ -1,5 +1,5 @@
---
-minutes: 0
+minutes: 10
---
# Using the Borrow checker to enforce Invariants
@@ -9,35 +9,35 @@ abstracted away from this central use case to model other problems and prevent
API misuse.
```rust,editable
-fn main() {
- // 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,
- }
+// 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 {
- if door.lock_shape == key.key_shape {
- Ok(OpenDoor { lock_shape: door.lock_shape })
- } else {
- Err(door)
- }
+fn open_door(key: &DoorKey, door: LockedDoor) -> Result {
+ 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 {
- if door.lock_shape == key.key_shape {
- Ok(LockedDoor { lock_shape: door.lock_shape })
- } else {
- Err(door)
- }
+fn close_door(key: &DoorKey, door: OpenDoor) -> Result {
+ 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);
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
index bba131a2ffa4..01da3e01fccf 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
@@ -1,5 +1,5 @@
---
-minutes: 0
+minutes: 15
---
# Mutually Exclusive References, or "Aliasing XOR Mutability"
@@ -19,7 +19,7 @@ pub struct DatabaseConnection {
impl DatabaseConnection {
pub fn new() -> Self {
Self {
- transaction: Transaction(/* again, pretend there's some interior state */),
+ transaction: Transaction(/* again, specifics omitted */),
query_results: vec![],
}
}
@@ -30,6 +30,7 @@ impl DatabaseConnection {
&self.query_results
}
pub fn commit(&mut self) {
+ /* Work omitted, including sending/clearing the transaction */
println!("Transaction committed!")
}
}
@@ -55,13 +56,28 @@ fn main() {
- This example shows how we can use the mutual exclusion of these kinds of
references to dissuade a user from reading query results while using a
- transaction API, something that 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.
+ transaction API.
-- By borrowing one field of a struct under a mutable / exclusive reference we
- prevent access to the other fields of that struct under a shared /
- non-exclusive reference until the lifetime of that borrow ends.
+ 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.
+
+- By borrowing one field of a struct via a method that returns a mutable /
+ exclusive reference we prevent access to the other fields of that struct under
+ a shared / non-exclusive reference until the lifetime of that borrow ends.
+
+ Note: This has to be via a method, as the compiler can reason about borrowing
+ different fields in mutable/shared ways simultaneously if that borrowing is
+ done manually.
+
+ Demonstrate:
+
+ - Change the instances of `db.get_transaction()` and `db.results()` to manual
+ borrows (`&mut db.transaction` and `&db.query_results` respectively) to show
+ the difference in what the borrow checker allows.
+
+ - Put the non-`main` part of this example in a module to reiterate that this
+ manual access is not possible across module boundaries.
- As laid out in [generalizing ownership](generalizing-ownership.md) we can look
at the ways Mutable References and Shareable References interact to see if
@@ -71,19 +87,13 @@ fn main() {
function, we can enforce the invariant "users of this API are not looking at
the query results at the same time as they are writing to a transaction."
-
-
-
-The "don't look at query results while building a transaction" invariant can still be circumvented, how so?
-
-
- -
- The user could access the transaction solely through `db.get_transaction()`, leaving the lifetime too temporary to prevent access to `db.results()`.
-
- -
- How could we avoid this by working in other concepts from "Leveraging the Type System"?
-
-
-
+- The "don't look at query results while building a transaction" invariant can
+ still be circumvented, how so?
+
+ - The user could access the transaction solely through `db.get_transaction()`,
+ leaving the lifetime too temporary to prevent access to `db.results()`.
+
+ - How could we avoid this by working in other concepts from "Leveraging the
+ Type System"?
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
index 909c1b1ddb16..5448eb404fe6 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -1,5 +1,5 @@
---
-minutes: 0
+minutes: 15
---
# Generalizing Ownership
@@ -38,10 +38,28 @@ fn main() {
towards semantic meaning in non-memory-safety settings. Nothing is being
mutated, nothing is being sent across threads.
+- When a new feature is introduced to users, it is often done so with a specific
+ idea of what it will be used for.
+
+ Over time, users may develop ways of using that feature in ways that may have
+ not been foreseen.
+
+ 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.
+
+ What we aim to do here is similar: The borrow checker, after being introduced
+ to people with the purpose of avoiding use-after-free and data races, is being
+ used to model things that have nothing to do with preventing those classes of
+ misuse.
+
- 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, instead imagining and working within
- situations where the rules are the same but the meaning is slightly different.
+ of preventing use-after-frees and data races, instead imagining and working
+ within situations where the rules are the same but the meaning is slightly
+ different.
- In rust's borrow checker we have access to three different ways of "taking" a
value:
@@ -61,9 +79,9 @@ fn main() {
of these exist. We can't "dispatch" to methods and functions that take
mutable references when all we have is a shared reference.
-- Remember that every `&T` and `&mut T` has an _implicit lifetime._ We get to
- avoid annotating a lot of lifetimes because the rust compiler can infer the
- majority of them. See:
- [Lifetime Elision](../../../lifetimes/lifetime-elision.md).
+- 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 can elide the majority
+ of them. See: [Lifetime Elision](../../../lifetimes/lifetime-elision.md).
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
index 03ffc3f12152..16b8c9d28bce 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
@@ -1,5 +1,5 @@
---
-minutes: 0
+minutes: 20
---
# Lifetime "Connections" & External Resources
@@ -9,34 +9,34 @@ own its data, but it can only live as long as the value that generated it" in
rust's type system.
```rust,editable,compile_fail
-fn main() {
- use std::marker::PhantomData;
- pub struct Tag;
- pub struct ErasedData<'a> {
- data: String,
- _phantom: PhantomData<&'a ()>,
- }
- impl<'a> ErasedData<'a> {
- pub fn get(&self) -> &str {
- &self.data
- }
+use std::marker::PhantomData;
+pub struct Tag;
+pub struct ErasedData<'a> {
+ data: String,
+ _phantom: PhantomData<&'a ()>,
+}
+impl<'a> ErasedData<'a> {
+ pub fn get(&self) -> &str {
+ &self.data
}
- pub struct TaggedData {
- data: String,
- _phantom: PhantomData,
+}
+pub struct TaggedData {
+ data: String,
+ _phantom: PhantomData,
+}
+impl TaggedData {
+ pub fn new(data: String) -> Self {
+ Self { data, _phantom: PhantomData }
}
- impl TaggedData {
- pub fn new(data: String) -> Self {
- Self { data, _phantom: PhantomData }
- }
- pub fn consume(self) {}
- pub fn get_erased(&self) -> ErasedData<'_> {
- // has an owned String, but _phantom holds onto the lifetime of the
- // TaggedData that created it.
- ErasedData { data: self.data.clone(), _phantom: PhantomData }
- }
+ pub fn consume(self) {}
+ pub fn get_erased(&self) -> ErasedData<'_> {
+ // has an owned String, but _phantom holds onto the lifetime of the
+ // TaggedData that created it.
+ ErasedData { data: self.data.clone(), _phantom: PhantomData }
}
+}
+fn main() {
let tagged_data: TaggedData = TaggedData::new("Real Data".to_owned());
// Get the erased-but-still-linked data.
let erased_owned_and_linked = tagged_data.get_erased();
@@ -85,6 +85,8 @@ fn main() {
Its counterpart `OwnedFd` is instead a file descriptor that closes that file
on drop.
+## More to Explore
+
- This way of encoding information in types is _exceptionally powerful_ when
combined with unsafe, as the ways one can manipulate lifetimes becomes almost
arbitrary. This is also dangerous, but when combined with tools like external,
@@ -92,7 +94,7 @@ fn main() {
types while encoding lifetime & safety expectations in the relevant data
types._
- The [GhostCell (2021)](https://plv.mpi-sws.org/rustbelt/ghostcell/) paper and
+- The [GhostCell (2021)](https://plv.mpi-sws.org/rustbelt/ghostcell/) paper and
its [relevant implementation](https://gitlab.mpi-sws.org/FP/ghostcell) show
this kind of work off. While the borrow checker is restrictive, there are
still ways to use escape hatches and then _show that the ways you used those
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata.md
new file mode 100644
index 000000000000..422125956044
--- /dev/null
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata.md
@@ -0,0 +1,37 @@
+---
+minutes: 10
+---
+
+# PhantomData: Tagging identical data with different types
+
+```rust,editable
+pub struct TaggedData {
+ data: String,
+ _phantom: PhantomData,
+}
+
+fn main() {}
+```
+
+
+
+- Motivation: We want to be able to tag structures with different type
+ parameters as a way to tell them apart or pass on lifetime information to
+ them.
+
+ In practice, these "tags" tend to be zero-sized types. What they mean will
+ depend on the shape and context of the API they're a part of.
+
+- Demonstrate:
+
+- `PhantomData` lets developers "tag" types with type and lifetime parameters
+ that are not "really" present in the struct or enum.
+
+ `PhantomData` can be used with the Typestate pattern to have data with the
+ same structure i.e. `TaggedData` can have methods or trait
+ implementations that `TaggedData` doesn't.
+
+- This can be thought of as an extension over how zero-sized types are all "the
+ same structure" but with different types.
+
+
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
index 1426b4ee923e..b007d10b6da9 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -1,5 +1,5 @@
---
-minutes: 0
+minutes: 5
---
# Single-use values
@@ -7,31 +7,30 @@ minutes: 0
In some circumstances we want values that can be used _exactly once_. One
critical example of this is in cryptography: "Nonces."
-```rust,editable,compile_fail
-fn main() {
- mod cryptography {
- pub struct Key;
- // Pretend this is a cryptographically sound, single-use number.
- pub struct Nonce(u32);
- // And pretend this is cryptographically sound random generator function.
- pub fn new_nonce() -> Nonce {
- Nonce(std::time::UNIX_EPOCH.elapsed().unwrap_or_default().subsec_nanos())
- }
-
- // We consume a nonce, but not the key or the data.
- pub fn encrypt(nonce: Nonce, key: &Key, data: &[u8]) {}
+```rust,editable
+mod cryptography {
+ pub struct Key(/* specifics omitted */);
+ // Pretend this is a cryptographically sound, single-use number.
+ pub struct Nonce(u32);
+ // And pretend this is cryptographically sound random generator function.
+ pub fn new_nonce() -> Nonce {
+ Nonce(std::time::UNIX_EPOCH.elapsed().unwrap_or_default().subsec_nanos())
}
- use cryptography::*;
+ // We consume a nonce, but not the key or the data.
+ pub fn encrypt(nonce: Nonce, key: &Key, data: &[u8]) {}
+}
+use cryptography::*;
+fn main() {
let nonce = new_nonce();
let data_1: [u8; 4] = [1, 2, 3, 4];
let data_2: [u8; 4] = [4, 3, 2, 1];
- let key = Key;
+ let key = Key(/* specifics omitted */);
// The key and data can be re-used, copied, etc. but the nonce cannot.
encrypt(nonce, &key, &data_1);
- encrypt(nonce, &key, &data_2); // 🛠️❌
+ // encrypt(nonce, &key, &data_2); // 🛠️❌
}
```
@@ -41,12 +40,17 @@ fn main() {
- By keeping constructors private and not implementing clone/copy for a type,
making the interior type opaque (as per the newtype pattern), we can prevent
- multiple uses of the same, API-controlled value.
+ multiple uses of the same value while keeping control of how that value is
+ bui.
-- A nonce is a piece of random, unique data
- used in cryptographic protocols to prevent replay attacks.
+- A nonce is a piece of random, unique data used in cryptographic protocols to
+ prevent replay attacks.
- - In practice people have ended up accidentally re-using nonces. Most commonly, this causes the cryptographic protocol to completely break down and stop fulfilling its function. Depending on the specifics of nonce reuse and cryptography at hand, private keys can also become computable by attackers.
+ - In practice people have ended up accidentally re-using nonces. Most
+ commonly, this causes the cryptographic protocol to completely break down
+ and stop fulfilling its function. Depending on the specifics of nonce reuse
+ and cryptography at hand, private keys can also become computable by
+ attackers.
- By tying nonce creation and consumption up in rust's ownership model, and by
not implementing clone/copy on sensitive single-use data, we can prevent
From 7b72587af7d946d8c97d4c2a7e6821cc47c5fa20 Mon Sep 17 00:00:00 2001
From: tall-vase
Date: Fri, 10 Oct 2025 16:48:17 +0100
Subject: [PATCH 17/24] Add TODO
---
.../borrow-checker-invariants/generalizing-ownership.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
index 5448eb404fe6..c541b9eb8f06 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -48,7 +48,7 @@ fn main() {
[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.
+ to other areas of type safe API design.
What we aim to do here is similar: The borrow checker, after being introduced
to people with the purpose of avoiding use-after-free and data races, is being
From 9f49ba5070f71e93daabf9bafc1daecd72bde6dd Mon Sep 17 00:00:00 2001
From: tall-vase
Date: Fri, 10 Oct 2025 16:48:52 +0100
Subject: [PATCH 18/24] Formatting pass
---
.../borrow-checker-invariants/generalizing-ownership.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
index c541b9eb8f06..dd8236c9e926 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -48,7 +48,8 @@ fn main() {
[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.
+ to other areas of type safe API design.
+
What we aim to do here is similar: The borrow checker, after being introduced
to people with the purpose of avoiding use-after-free and data races, is being
From 6fcc4719f30cc998c05f2fdb73d9847c91b0eb56 Mon Sep 17 00:00:00 2001
From: tall-vase <228449146+tall-vase@users.noreply.github.com>
Date: Mon, 13 Oct 2025 08:28:55 +0100
Subject: [PATCH 19/24] Apply suggestions from code review
Co-authored-by: Dmitri Gribenko
---
.../aliasing-xor-mutability.md | 4 ++--
.../generalizing-ownership.md | 14 ++++++--------
.../borrow-checker-invariants/single-use-values.md | 12 ++++--------
3 files changed, 12 insertions(+), 18 deletions(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
index 01da3e01fccf..63194495726e 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
@@ -30,7 +30,7 @@ impl DatabaseConnection {
&self.query_results
}
pub fn commit(&mut self) {
- /* Work omitted, including sending/clearing the transaction */
+ // Work omitted, including sending/clearing the transaction
println!("Transaction committed!")
}
}
@@ -66,7 +66,7 @@ fn main() {
exclusive reference we prevent access to the other fields of that struct under
a shared / non-exclusive reference until the lifetime of that borrow ends.
- Note: This has to be via a method, as the compiler can reason about borrowing
+- The `transaction` field must be borrowed via a method, as the compiler can reason about borrowing
different fields in mutable/shared ways simultaneously if that borrowing is
done manually.
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
index dd8236c9e926..ddc0ba1c23b8 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -38,23 +38,21 @@ fn main() {
towards semantic meaning in non-memory-safety settings. Nothing is being
mutated, nothing is being sent across threads.
-- When a new feature is introduced to users, it is often done so with a specific
- idea of what it will be used for.
+- Language features are often introduced for a specific purpose.
Over time, users may develop ways of using that feature in ways that may have
not been foreseen.
In 2004, Java 5 introduced Generics with the
- [main stated purpose of enabling type safe collections](https://jcp.org/en/jsr/detail?id=14).
+ [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.
+ to other areas of type-safe API design.
- What we aim to do here is similar: The borrow checker, after being introduced
- to people with the purpose of avoiding use-after-free and data races, is being
- used to model things that have nothing to do with preventing those classes of
- misuse.
+ 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
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
index b007d10b6da9..b91a12232686 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -8,20 +8,16 @@ In some circumstances we want values that can be used _exactly once_. One
critical example of this is in cryptography: "Nonces."
```rust,editable
-mod cryptography {
pub struct Key(/* specifics omitted */);
- // Pretend this is a cryptographically sound, single-use number.
+ // A single-use number suitable for cryptographic purposes.
pub struct Nonce(u32);
- // And pretend this is cryptographically sound random generator function.
+ // A cryptographically sound random generator function.
pub fn new_nonce() -> Nonce {
- Nonce(std::time::UNIX_EPOCH.elapsed().unwrap_or_default().subsec_nanos())
+ Nonce(4) // chosen by a fair dice roll, https://xkcd.com/221/
}
- // We consume a nonce, but not the key or the data.
+ // Consume a nonce, but not the key or the data.
pub fn encrypt(nonce: Nonce, key: &Key, data: &[u8]) {}
-}
-
-use cryptography::*;
fn main() {
let nonce = new_nonce();
let data_1: [u8; 4] = [1, 2, 3, 4];
From 2d3f915c5daa8f06dae589040290128e721438c0 Mon Sep 17 00:00:00 2001
From: tall-vase
Date: Mon, 13 Oct 2025 14:36:29 +0100
Subject: [PATCH 20/24] Further address feedback and implement the phantomdata
slide
---
src/SUMMARY.md | 1 +
.../borrow-checker-invariants.md | 3 +-
.../aliasing-xor-mutability.md | 6 +-
.../generalizing-ownership.md | 12 +--
.../lifetime-connections.md | 27 +++----
.../borrow-checker-invariants/phantomdata.md | 77 ++++++++++++++++--
.../single-use-values.md | 78 +++++++++++--------
7 files changed, 137 insertions(+), 67 deletions(-)
diff --git a/src/SUMMARY.md b/src/SUMMARY.md
index d1793acad7fc..4ae74a64762f 100644
--- a/src/SUMMARY.md
+++ b/src/SUMMARY.md
@@ -449,6 +449,7 @@
- [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)
+ - [Lifetime Relationships primer: PhantomData](idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata.md)
- [Lifetime Relationships & External Resources](idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md)
---
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
index 4dd41a42fe85..904ff12f7271 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
@@ -58,7 +58,8 @@ fn main() {
- 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.
+ restrict use of APIs already using
+ [the Typestate pattern](../leveraging-the-type-system/typestate-pattern.md).
- This example uses the ownership & borrowing rules to model the locking and
unlocking of a door. We can try to open a door with a key, but if it's the
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
index 63194495726e..80bf80b5c06b 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
@@ -66,9 +66,9 @@ fn main() {
exclusive reference we prevent access to the other fields of that struct under
a shared / non-exclusive reference until the lifetime of that borrow ends.
-- The `transaction` field must be borrowed via a method, as the compiler can reason about borrowing
- different fields in mutable/shared ways simultaneously if that borrowing is
- done manually.
+- The `transaction` field must be borrowed via a method, as the compiler can
+ reason about borrowing different fields in mutable/shared ways simultaneously
+ if that borrowing is done manually.
Demonstrate:
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
index ddc0ba1c23b8..ef2e6b3416c5 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -50,9 +50,10 @@ fn main() {
to other areas of type-safe API design.
- 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.
+ 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
@@ -80,7 +81,8 @@ fn main() {
- 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 can elide the majority
- of them. See: [Lifetime Elision](../../../lifetimes/lifetime-elision.md).
+ 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).
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
index 16b8c9d28bce..4ea3776bc6e8 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
@@ -10,34 +10,33 @@ rust's type system.
```rust,editable,compile_fail
use std::marker::PhantomData;
-pub struct Tag;
-pub struct ErasedData<'a> {
+
+pub struct BorrowedButOwned<'a> {
data: String,
_phantom: PhantomData<&'a ()>,
}
-impl<'a> ErasedData<'a> {
+impl<'a> BorrowedButOwned<'a> {
pub fn get(&self) -> &str {
&self.data
}
}
-pub struct TaggedData {
+pub struct StringOrigin {
data: String,
- _phantom: PhantomData,
}
-impl TaggedData {
+impl StringOrigin {
pub fn new(data: String) -> Self {
Self { data, _phantom: PhantomData }
}
pub fn consume(self) {}
- pub fn get_erased(&self) -> ErasedData<'_> {
+ pub fn get_erased(&self) -> BorrowedButOwned<'_> {
// has an owned String, but _phantom holds onto the lifetime of the
- // TaggedData that created it.
- ErasedData { data: self.data.clone(), _phantom: PhantomData }
+ // StringOrigin that created it.
+ BorrowedButOwned { data: self.data.clone(), _phantom: PhantomData }
}
}
fn main() {
- let tagged_data: TaggedData = TaggedData::new("Real Data".to_owned());
+ let tagged_data: StringOrigin = StringOrigin::new("Real Data".to_owned());
// Get the erased-but-still-linked data.
let erased_owned_and_linked = tagged_data.get_erased();
tagged_data.consume();
@@ -48,12 +47,8 @@ fn main() {
-- `PhantomData` lets developers "tag" types with type and lifetime parameters
- that are not "really" present in the struct or enum.
-
- `PhantomData` can be used with the Typestate pattern to have data with the
- same structure i.e. `TaggedData` can have methods or trait
- implementations that `TaggedData` doesn't.
+- `PhantomData` lets developers tag data structures with type and lifetime
+ parameters that are not present in the body of the struct or enum.
It can also be used to encode a connection between the lifetime of one value
and another, while both values still maintain separate owned data within them.
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata.md
index 422125956044..c9539ff95365 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata.md
@@ -2,12 +2,16 @@
minutes: 10
---
-# PhantomData: Tagging identical data with different types
+# Lifetime Relationships primer: PhantomData
-```rust,editable
-pub struct TaggedData {
+Lying to the type system safely, to make things easier for us and encode
+invariants better.
+
+```rust,editable,compile_fail
+// use std::marker::PhantomData;
+
+pub struct TypeTaggedString {
data: String,
- _phantom: PhantomData,
}
fn main() {}
@@ -15,14 +19,41 @@ fn main() {}
-- Motivation: We want to be able to tag structures with different type
+- Ask: Why won't this slide compile?
+
+ Answer: Unused type parameters!
+
+- Motivation: We want to be able to "tag" structures with different type
parameters as a way to tell them apart or pass on lifetime information to
them.
+ See: [Typestate Generics](../typestate-pattern/typestate-generics.md) for
+ instances of telling apart different data relevant to stages of an algorithm
+ with type parameter differences.
+
In practice, these "tags" tend to be zero-sized types. What they mean will
depend on the shape and context of the API they're a part of.
-- Demonstrate:
+- Demonstrate: Add a field of type `marker: T` to `TypeTaggedString`
+
+ Ask: What issues does having it be an actual instance of that type pose?
+
+ Answer: If it's not a zero-sized type (like `()` or `struct MyTag;`), then
+ we're allocating more memory than we need to when all we care for is type
+ information.
+
+ Alternatively: Makes initializing the data a pain for users and the
+ maintainers of the library alike, as there needs to be an additional parameter
+ for unnecessary data.
+
+- Demonstrate: Uncomment the `PhantomData` import, and implement the following:
+
+ ```rust,compile_fail
+ pub struct TypeTaggedString {
+ data: String,
+ _phantom: PhantomData,
+ }
+ ```
- `PhantomData` lets developers "tag" types with type and lifetime parameters
that are not "really" present in the struct or enum.
@@ -31,7 +62,37 @@ fn main() {}
same structure i.e. `TaggedData` can have methods or trait
implementations that `TaggedData` doesn't.
-- This can be thought of as an extension over how zero-sized types are all "the
- same structure" but with different types.
+- This can be thought of as a more general take on how zero-sized types are all
+ "the same structure" (zero size = only one possible value) but with different
+ types.
+
+- Ask: Why don't we just do newtypes for every case as they come up?
+
+ Answer: Avoiding repeating ourselves! Would have to implement any methods
+ again for every new type. Having information in a `` argument for a type
+ that gets erased at run-time lets us write implementations that work for all
+ possible values of `T`. Newtyping all possible use cases means a lot of
+ re-implementation.
+
+ Alternatively, a type parameter for a type helps provide semantic context to a
+ user. `Stage` offers a lot more contextual detail than `StageStart`,
+ and implies there is shared behavior between stages.
+
+- Demonstrate: We can also capture lifetime parameters with `PhantomData`
+ without needing to hold onto a value with that lifetime.
+
+ Implement the following:
+
+ ```rust,compile_fail
+ pub struct LifetimeTaggedString<'a> {
+ data: String,
+ _phantom: PhantomData<&'a ()>,
+ }
+ ```
+
+ Ask: Why would we do this?
+
+ Answer: To tie an owned value to another value's lifetime elsewhere in the
+ program.
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
index b91a12232686..1af8c9ff484e 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -1,23 +1,23 @@
---
-minutes: 5
+minutes: 10
---
# Single-use values
-In some circumstances we want values that can be used _exactly once_. One
-critical example of this is in cryptography: "Nonces."
+In some circumstances we want values that _can only be used once_. One critical
+example of this is in cryptography: "Nonces."
```rust,editable
- pub struct Key(/* specifics omitted */);
- // A single-use number suitable for cryptographic purposes.
- pub struct Nonce(u32);
- // A cryptographically sound random generator function.
- pub fn new_nonce() -> Nonce {
- Nonce(4) // chosen by a fair dice roll, https://xkcd.com/221/
- }
-
- // Consume a nonce, but not the key or the data.
- pub fn encrypt(nonce: Nonce, key: &Key, data: &[u8]) {}
+pub struct Key(/* specifics omitted */);
+// A single-use number suitable for cryptographic purposes.
+pub struct Nonce(u32);
+// A cryptographically sound random generator function.
+pub fn new_nonce() -> Nonce {
+ Nonce(4) // chosen by a fair dice roll, https://xkcd.com/221/
+}
+// Consume a nonce, but not the key or the data.
+pub fn encrypt(nonce: Nonce, key: &Key, data: &[u8]) {}
+
fn main() {
let nonce = new_nonce();
let data_1: [u8; 4] = [1, 2, 3, 4];
@@ -32,29 +32,39 @@ fn main() {
-- Owned "consumption" of values lets us model things that need to be single-use.
+- What if we want to be able to guarantee that a value can only be used once?
+
+- Motivation: A nonce is a piece of random, unique data used in cryptographic
+ protocols to prevent replay attacks.
+
+ Background: In practice people have ended up accidentally re-using nonces.
+ Most commonly, this causes the cryptographic protocol to completely break down
+ and stop fulfilling its function. Depending on the specifics of nonce reuse
+ and cryptography at hand, private keys can also become computable by
+ attackers.
+
+- Rust has an obvious tool for "Once you use this, you can't use it anymore":
+ Using a value as an _owned argument_.
+
+- Highlight: the `encrypt` function takes references for `key` and `data` but
+ not `nonce`
- By keeping constructors private and not implementing clone/copy for a type,
making the interior type opaque (as per the newtype pattern), we can prevent
- multiple uses of the same value while keeping control of how that value is
- bui.
-
-- A nonce is a piece of random, unique data used in cryptographic protocols to
- prevent replay attacks.
-
- - In practice people have ended up accidentally re-using nonces. Most
- commonly, this causes the cryptographic protocol to completely break down
- and stop fulfilling its function. Depending on the specifics of nonce reuse
- and cryptography at hand, private keys can also become computable by
- attackers.
-
- - By tying nonce creation and consumption up in rust's ownership model, and by
- not implementing clone/copy on sensitive single-use data, we can prevent
- this kind of dangerous misuse.
-
- - Cryptography Nuance: There is still the case where a nonce may be used twice
- if it's created through purely a pseudo-random process with no additional
- metadata, and that circumstance can't be avoided through this particular
- method. This kind of API prevents one kind of misuse, but not all kinds.
+ multiple uses of the same value.
+
+- Ask: What are we missing from the newtype pattern in the slide's code?
+
+ Expect: Module boundary.
+
+ Demonstrate: Without a module boundary a user can construct a nonce on their
+ own.
+
+ Fix: Put `Key`, `Nonce`, and `new_nonce` behind a module.
+
+- Cryptography Nuance: There is still the case where a nonce may be used twice
+ if it's created through purely a pseudo-random process with no additional
+ metadata, and that circumstance can't be avoided through this particular
+ method. This API design prevents one kind of misuse, but not all kinds.
From 26cfe2b4307b739d76395ee23ee3ebae3a7d01db Mon Sep 17 00:00:00 2001
From: tall-vase <228449146+tall-vase@users.noreply.github.com>
Date: Tue, 21 Oct 2025 11:20:48 +0100
Subject: [PATCH 21/24] Apply suggestions from code review
Co-authored-by: Dmitri Gribenko
---
.../borrow-checker-invariants.md | 9 +++------
.../borrow-checker-invariants/single-use-values.md | 5 ++---
2 files changed, 5 insertions(+), 9 deletions(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
index 904ff12f7271..80373b5cce4d 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
@@ -4,9 +4,8 @@ minutes: 10
# Using the Borrow checker to enforce Invariants
-The logic of the borrow checker, while tied to "memory ownership", can be
-abstracted away from this central use case to model other problems and prevent
-API misuse.
+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
@@ -66,9 +65,7 @@ fn main() {
wrong key the door is still closed (here represented as an error) and the key
persists regardless.
-- The rules of the borrow checker exist to prevent developers from accessing,
- changing, and holding onto data in memory in unpredictable ways without being
- so restrictive that it would prevent _writing software_. The underlying
+- The rules of the borrow checker exist to prevent memory safety bugs. However, the underlying
logical system does not "know" what memory is. All it does is enforce a
specific set of rules of how different operations affect what later operations
are possible.
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
index 1af8c9ff484e..bbfc8ae3c261 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -32,7 +32,7 @@ fn main() {
-- What if we want to be able to guarantee that a value can only be used once?
+- Problem: How can we guarantee a value is used only once?
- Motivation: A nonce is a piece of random, unique data used in cryptographic
protocols to prevent replay attacks.
@@ -46,8 +46,7 @@ fn main() {
- Rust has an obvious tool for "Once you use this, you can't use it anymore":
Using a value as an _owned argument_.
-- Highlight: the `encrypt` function takes references for `key` and `data` but
- not `nonce`
+- Highlight: the `encrypt` function takes `nonce` by value (an owned argument), but `key` and `data` by reference.
- By keeping constructors private and not implementing clone/copy for a type,
making the interior type opaque (as per the newtype pattern), we can prevent
From d55ce181910ce2fe02e349b326b5d85c2a1be69c Mon Sep 17 00:00:00 2001
From: tall-vase <228449146+tall-vase@users.noreply.github.com>
Date: Thu, 23 Oct 2025 09:07:31 +0100
Subject: [PATCH 22/24] Make some comments doc comments
Co-authored-by: Dmitri Gribenko
---
.../leveraging-the-type-system/borrow-checker-invariants.md | 4 ++--
.../borrow-checker-invariants/single-use-values.md | 6 +++---
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
index 80373b5cce4d..a75df826cc78 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
@@ -8,8 +8,8 @@ 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.
+/// 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,
}
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
index bbfc8ae3c261..8849ffaf0bf9 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -9,13 +9,13 @@ example of this is in cryptography: "Nonces."
```rust,editable
pub struct Key(/* specifics omitted */);
-// A single-use number suitable for cryptographic purposes.
+/// A single-use number suitable for cryptographic purposes.
pub struct Nonce(u32);
-// A cryptographically sound random generator function.
+/// A cryptographically sound random generator function.
pub fn new_nonce() -> Nonce {
Nonce(4) // chosen by a fair dice roll, https://xkcd.com/221/
}
-// Consume a nonce, but not the key or the data.
+/// Consume a nonce, but not the key or the data.
pub fn encrypt(nonce: Nonce, key: &Key, data: &[u8]) {}
fn main() {
From fc3f3de80690dfda6b9f55daf526305af45f12b4 Mon Sep 17 00:00:00 2001
From: tall-vase
Date: Wed, 29 Oct 2025 14:36:15 +0000
Subject: [PATCH 23/24] Address latest structural feedback
---
src/SUMMARY.md | 4 +-
.../borrow-checker-invariants.md | 64 ++++++---
.../aliasing-xor-mutability.md | 132 ++++++++++--------
.../generalizing-ownership.md | 64 +++------
.../lifetime-connections.md | 98 -------------
.../phantomdata-01-types.md | 117 ++++++++++++++++
.../phantomdata-02-lifetimes.md | 129 +++++++++++++++++
.../borrow-checker-invariants/phantomdata.md | 98 -------------
.../single-use-values.md | 33 +++--
9 files changed, 412 insertions(+), 327 deletions(-)
delete mode 100644 src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
create mode 100644 src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-01-types.md
create mode 100644 src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-02-lifetimes.md
delete mode 100644 src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata.md
diff --git a/src/SUMMARY.md b/src/SUMMARY.md
index 4ae74a64762f..e44f623c1e03 100644
--- a/src/SUMMARY.md
+++ b/src/SUMMARY.md
@@ -449,8 +449,8 @@
- [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)
- - [Lifetime Relationships primer: PhantomData](idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata.md)
- - [Lifetime Relationships & External Resources](idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.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)
---
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
index a75df826cc78..c3fecdb96424 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants.md
@@ -1,11 +1,11 @@
---
-minutes: 10
+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.
+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
@@ -53,26 +53,56 @@ fn main() {
-
-
- 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).
-- This example uses the ownership & borrowing rules to model the locking and
- unlocking of a door. We can try to open a door with a key, but if it's the
- wrong key the door is still closed (here represented as an error) and the key
- persists regardless.
+- 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.
+
+
+ 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.
-- The rules of the borrow checker exist to prevent memory safety bugs. However, the underlying
- logical system does not "know" what memory is. All it does is enforce a
- specific set of rules of how different operations affect what later operations
- are possible.
+ All the borrow checker does is enforce a specific set of rules of how users
+ can order operations.
-- Those rules can apply to many other cases: We can piggy-back onto the rules of
- the borrow checker to design APIs to be harder or impossible to misuse, even
- when there's little or no "memory safety" concerns in the problem domain. This
- section will walk through some of those different domains.
+ 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.
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
index 80bf80b5c06b..4ea4f73c7c9f 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md
@@ -2,98 +2,112 @@
minutes: 15
---
-# Mutually Exclusive References, or "Aliasing XOR Mutability"
+# 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,compile_fail
-pub struct Transaction(/* specifics omitted */);
-pub struct QueryResult(String);
-
-pub struct DatabaseConnection {
- transaction: Transaction,
- query_results: Vec,
-}
+```rust,editable
+pub struct QueryResult;
+pub struct DatabaseConnection {/* fields omitted */}
impl DatabaseConnection {
pub fn new() -> Self {
- Self {
- transaction: Transaction(/* again, specifics omitted */),
- query_results: vec![],
- }
- }
- pub fn get_transaction(&mut self) -> &mut Transaction {
- &mut self.transaction
+ Self {}
}
pub fn results(&self) -> &[QueryResult] {
- &self.query_results
- }
- pub fn commit(&mut self) {
- // Work omitted, including sending/clearing the transaction
- println!("Transaction committed!")
+ &[] // fake results
}
}
-pub fn do_something_with_transaction(transaction: &mut Transaction) {}
+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();
- let mut transaction = db.get_transaction();
- do_something_with_transaction(transaction);
- let assumed_the_transactions_happened_immediately = db.results(); // ❌🔨
- do_something_with_transaction(transaction);
- // Works, as the lifetime of "transaction" as a reference ended above.
- let assumed_the_transactions_happened_immediately_again = db.results();
- db.commit();
+
+ // 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();
}
```
-- Aliasing XOR Mutability means "we can have multiple immutable references, a
- single mutable reference, but not both."
+- 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.
-- This example shows how we can use the mutual exclusion of these kinds of
- references to dissuade a user from reading query results while using a
- transaction API.
+ 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.
-- By borrowing one field of a struct via a method that returns a mutable /
- exclusive reference we prevent access to the other fields of that struct under
- a shared / non-exclusive reference until the lifetime of that borrow ends.
-
-- The `transaction` field must be borrowed via a method, as the compiler can
- reason about borrowing different fields in mutable/shared ways simultaneously
- if that borrowing is done manually.
+- The constructor for the Transaction type takes a mutable reference to the
+ database connection, which it holds onto that reference.
- Demonstrate:
+ 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.
- - Change the instances of `db.get_transaction()` and `db.results()` to manual
- borrows (`&mut db.transaction` and `&db.query_results` respectively) to show
- the difference in what the borrow checker allows.
+ The `mut` keyword in the type lets us determine that there is just one of
+ these references present per variable of type `DatabaseConnection`.
- - Put the non-`main` part of this example in a module to reiterate that this
- manual access is not possible across module boundaries.
+- While a `Transaction` exists, we can't touch the `DatabaseConnection` variable
+ that was created from it.
-- As laid out in [generalizing ownership](generalizing-ownership.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.
+ Demonstrate: uncomment the `db.results()` line.
-- In this case, having the query results not public and placed behind a getter
- function, we can enforce the invariant "users of this API are not looking at
- the query results at the same time as they are writing to a transaction."
+- 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.
-- The "don't look at query results while building a transaction" invariant can
- still be circumvented, how so?
+- 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.
- - The user could access the transaction solely through `db.get_transaction()`,
- leaving the lifetime too temporary to prevent access to `db.results()`.
+- 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."
- - How could we avoid this by working in other concepts from "Leveraging the
- Type System"?
+ If they're publicly available to the user outside of the definition module
+ then this invariant can be invalidated.
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
index ef2e6b3416c5..c875658553e7 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md
@@ -1,14 +1,14 @@
---
-minutes: 15
+minutes: 5
---
-# Generalizing Ownership
+# 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,compile_fail
+```rust,editable
// An internal data type to have something to hold onto.
pub struct Internal;
// The "outer" data.
@@ -24,11 +24,11 @@ fn deny_future_use(value: Data) {}
fn main() {
let mut value = Data(Internal);
- let deny_mut = shared_use(&value);
- let try_to_deny_immutable = exclusive_use(&mut value); // ❌🔨
- let more_mut_denial = &deny_mut;
+ let shared = shared_use(&value);
+ // let exclusive = exclusive_use(&mut value); // ❌🔨
+ let shared_again = &shared;
deny_future_use(value);
- let even_more_mut_denial = shared_use(&value); // ❌🔨
+ // let shared_again_again = shared_use(&value); // ❌🔨
}
```
@@ -38,51 +38,31 @@ fn main() {
towards semantic meaning in non-memory-safety settings. Nothing is being
mutated, nothing is being sent across threads.
-- Language features are often introduced for a specific purpose.
-
- Over time, users may develop ways of using that feature in ways that may have
- not been foreseen.
-
- 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.
-
+- In rust's borrow checker we have access to three different ways of "taking" a
+ value:
- 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.
+ - Owned value `T`. Value is dropped when the scope ends, unless it is not
+ returned to another scope.
-- 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, instead imagining and working
- within situations where the rules are the same but the meaning is slightly
- different.
+ - Shared Reference `&T`. Allows aliasing but prevents mutable access while
+ shared references are in use.
-- In rust's borrow checker we have access to three different ways of "taking" a
- value:
+ - 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.
-
- - Owned value `T`. Very permissive case, to the point where mutability can be
- re-set, but demands that nothing else is using it in any context and drops
- the value when scope ends (unless that scope returns this value) (see:
- RAII.)
+- Ask: The two commented-out lines in `main` would cause compilation errors,
+ Why?
- - Mutable Reference `&mut T`. While holding onto a mutable reference we can
- still "dispatch" to methods and functions that take an immutable, shared
- reference of the value but only as long as we're not aliasing immutable,
- shared references to related data "after" that dispatch.
+ 1: Because the `shared` value is still aliased after the `exclusive` reference
+ is taken.
- - Shared Reference `&T`. Allows aliasing but prevents mutable access while any
- of these exist. We can't "dispatch" to methods and functions that take
- mutable references when all we have is a shared reference.
+ 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).
+ [Lifetime Elision](../../../lifetimes/lifetime-elision.md)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
deleted file mode 100644
index 4ea3776bc6e8..000000000000
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/lifetime-connections.md
+++ /dev/null
@@ -1,98 +0,0 @@
----
-minutes: 20
----
-
-# Lifetime "Connections" & External Resources
-
-Using `PhantomData` in conjunction with lifetimes lets us say "this value may
-own its data, but it can only live as long as the value that generated it" in
-rust's type system.
-
-```rust,editable,compile_fail
-use std::marker::PhantomData;
-
-pub struct BorrowedButOwned<'a> {
- data: String,
- _phantom: PhantomData<&'a ()>,
-}
-impl<'a> BorrowedButOwned<'a> {
- pub fn get(&self) -> &str {
- &self.data
- }
-}
-pub struct StringOrigin {
- data: String,
-}
-impl StringOrigin {
- pub fn new(data: String) -> Self {
- Self { data, _phantom: PhantomData }
- }
- pub fn consume(self) {}
- pub fn get_erased(&self) -> BorrowedButOwned<'_> {
- // has an owned String, but _phantom holds onto the lifetime of the
- // StringOrigin that created it.
- BorrowedButOwned { data: self.data.clone(), _phantom: PhantomData }
- }
-}
-
-fn main() {
- let tagged_data: StringOrigin = StringOrigin::new("Real Data".to_owned());
- // Get the erased-but-still-linked data.
- let erased_owned_and_linked = tagged_data.get_erased();
- tagged_data.consume();
- // Owned by `erased_owned_and_linked` but still connected to `tagged_data`.
- println!("{}", erased_owned_and_linked.get()); // ❌🔨
-}
-```
-
-
-
-- `PhantomData` lets developers tag data structures with type and lifetime
- parameters that are not present in the body of the struct or enum.
-
- It can also be used to encode a connection between the lifetime of one value
- and another, while both values still maintain separate owned data within them.
-
-- This is really useful for modelling a bunch of relationships between data,
- where we want to establish that while a type has owned values within it is
- still connected to another piece of data and can only live as long as it.
-
- Consider a case where you want to return owned data from a method, but you
- don't want that data to live longer than the value that created it.
-
-- Lifetimes need to come from somewhere! We can't build functions of the form
- `fn lifetime_shenanigans<'a>(owned: OwnedData) -> &'b Data` (without tying
- `'b` to `'a` in some way).
-
- Lifetime elision hides where a lot of lifetimes come from, but that doesn't
- mean the explicitly named lifetimes "come from nowhere."
-
- Suggestion: Show off un-eliding the lifetimes in `get_erased` in this example.
-
-- [`BorrowedFd`](https://rust-lang.github.io/rfcs/3128-io-safety.html#ownedfd-and-borrowedfdfd)
- uses these captured lifetimes to enforce the invariant that "if this file
- descriptor exists, the OS file descriptor is still open" because a
- `BorrowedFd`'s lifetime parameter demands that there exists another value in
- your program that has the same lifetime as it, and this has been encoded by
- the API designer to mean _that value is what keeps the access to the file
- open_.
-
- Its counterpart `OwnedFd` is instead a file descriptor that closes that file
- on drop.
-
-## More to Explore
-
-- This way of encoding information in types is _exceptionally powerful_ when
- combined with unsafe, as the ways one can manipulate lifetimes becomes almost
- arbitrary. This is also dangerous, but when combined with tools like external,
- mechanically-verified proofs _we can safely encode cyclic/self-referential
- types while encoding lifetime & safety expectations in the relevant data
- types._
-
-- The [GhostCell (2021)](https://plv.mpi-sws.org/rustbelt/ghostcell/) paper and
- its [relevant implementation](https://gitlab.mpi-sws.org/FP/ghostcell) show
- this kind of work off. While the borrow checker is restrictive, there are
- still ways to use escape hatches and then _show that the ways you used those
- escape hatches are consistent and safe._
-
-
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-01-types.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-01-types.md
new file mode 100644
index 000000000000..ad0a0bf78727
--- /dev/null
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-01-types.md
@@ -0,0 +1,117 @@
+---
+minutes: 15
+---
+
+# PhantomData 1/2: Type-level tagging
+
+
+```rust,editable,compile_fail
+// use std::marker::PhantomData;
+
+pub trait ChatUser {}
+pub trait ChatModerator {}
+pub trait ChatAdmin {}
+
+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() {}
+```
+
+
+
+
+- Problem: We want to use the newtype pattern to differentiate permissions, but
+ we're duplicating identical implementations for identical data.
+
+- Motivation: We want to be able to "tag" structures with different type
+ parameters as a way to tell them apart or pass on lifetime information to
+ them.
+
+ See: [Typestate Generics](../typestate-pattern/typestate-generics.md) for
+ instances of telling apart different data relevant to stages of an algorithm
+ with type parameter differences.
+
+ In practice, these "tags" tend to be zero-sized types. What they mean will
+ depend on the shape and context of the API they're a part of.
+
+- Demonstrate: Change the implementation to the following:
+
+
+ ```rust
+ pub struct ChatId { id: u64, tag: T }
+
+ pub struct UserTag;
+ pub struct PatronTag;
+ pub struct ModeratorTag;
+ pub struct AdminTag;
+
+ impl ChatId { /* ... */ }
+ impl ChatId { /* ... */ }
+ impl ChatId { /* ... */ }
+ impl ChatId { /* ... */ }
+ ```
+
+
+ Ask: What issues does having it be an actual instance of that type pose?
+
+ Answer: If it's not a zero-sized type (like `()` or `struct MyTag;`), then
+ we're allocating more memory than we need to when all we care for is type
+ information that is only relevant at compile-time.
+
+ This also makes initializing the data a pain for users and the maintainers of
+ the library alike, as users need to manually create a value and pass that to
+ whatever constructors are exposed.
+
+- Demonstrate: in `main`, show how users of this API need to pass values of the
+ "tag" types. Construct values with the `Tag` types as above, then try to
+ construct values such as `ChatId` or
+ `ChatId<(UserTag, PatronTag, ModeratorTag, AdminTag, Vec)>` to push the
+ user-facing inconvenience to extremes.
+
+- Demonstrate: Uncomment the `PhantomData` import, and implement the following:
+
+ ```rust,compile_fail
+ pub struct ChatId {
+ id: u64,
+ tag: PhantomData,
+ }
+ ```
+
+- `PhantomData` is a zero-sized type with a type parameter. We can construct
+ values of it like other ZSTs with
+ `let phantom: PhantomData = PhantomData;` or with the
+ `PhantomData::default()` implementation.
+
+- `PhantomData` can be used as part of the Typestate pattern to have data with
+ the same structure i.e. `TaggedData` have methods or trait
+ implementations that `TaggedData` doesn't.
+
+## More to Explore
+
+- What have we lost behavior-wise with this change in implementation?
+
+ Answer: Different ID types implementing multiple traits depending on their
+ permissions.
+
+ This behavior means we can use an admin ID for methods that only require user
+ permissions.
+
+ We can implement this by having traits for the different permission levels,
+ then implementing methods on `ChatId` depending on what traits `T`
+ implements.
+
+
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-02-lifetimes.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-02-lifetimes.md
new file mode 100644
index 000000000000..ecf551e92149
--- /dev/null
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-02-lifetimes.md
@@ -0,0 +1,129 @@
+---
+minutes: 15
+---
+
+# PhantomData 2/2: Tagging with Lifetimes
+
+```rust,editable
+// use std::marker::PhantomData;
+
+/// Direct FFI to a database library in C.
+/// We got this API as is, we have no influence over it.
+mod ffi {
+ pub type DatabaseHandle = u8; // maximum 255 databases open at the same time
+
+ fn database_open(name: *const std::os::raw::c_char) -> DatabaseHandle {
+ unimplemented!()
+ }
+ // ... etc.
+}
+
+struct DatabaseConnection(ffi::DatabaseHandle);
+struct Transaction<'a>(&'a mut DatabaseConnection);
+
+impl DatabaseConnection {
+ fn new_transaction(&mut self) -> Transaction<'_> {
+ Transaction(self)
+ }
+}
+
+fn main() {}
+```
+
+
+
+- Remember the transaction API from the
+ [Aliasing XOR Mutability](./aliasing-xor-mutability.md) example.
+
+ We held onto a mutable reference to the database connection within the
+ transaction type, this asserts that while we have a transaction nothing else
+ can use the database handle.
+
+ But this isn't the way database connections tend to work in practice. We can
+ have multiple connections.
+
+ But we could theoretically be connected to multiple distinct databases.
+
+ Ask: how could we associate multiple connections to the same database in the
+ type system?
+
+ Expect: Just have multiple connections without annotating where they're from
+ in the type system. Or, create a new tag type for each database.
+
+ What's wrong with type tagging? It doesn't encode a relationship between
+ specific variables the way that lifetime annotations do.
+
+- But what we want to do is use the type system as much as possible to express
+ what is possible.
+
+ What is possible is to have multiple connections to the same database,
+ especially in the context of transactions being committed on the same machine,
+ so we want to encode that while maintaining a _relationship_ between databases
+ and transactions.
+
+- Additionally: We can save 7 bytes in the size of `Transaction` by having it be
+ owned vs being a reference on a 64bit platform.
+
+- We can use `PhantomData` to use Lifetime parameters that don't have "real"
+ values borrowed by making the type parameter of `PhantomData` use that
+ lifetime.
+
+- Demonstrate: change `Transaction` to the following
+
+ ```rust,compile_fail
+ pub struct Transaction<'a> {
+ connection: DatabaseConnection,
+ _phantom: PhantomData<&'a ()>,
+ }
+ ```
+
+ Change the `new_transaction` function for `DatabaseConnection` to the
+ following:
+
+ ```rust,compile_fail
+ fn new_transaction(&'a self) -> Transaction<'a> {
+ Transaction { connection: DatabaseConnection(self.0), _phantom: PhantomData }
+ }
+ ```
+
+ This gives an owned database connection to a specific database as per the FFI,
+ but creates a relationship between that connection and the "source"
+ `DatabaseConnection` that created it.
+
+- Demonstrate: We can give each `Transaction` an owned `DatabaseConnection`, but
+ change the constructor to instead be a method of `DatabaseConnection` itself.
+
+- [`BorrowedFd`](https://rust-lang.github.io/rfcs/3128-io-safety.html#ownedfd-and-borrowedfdfd)
+ uses these captured lifetimes to enforce the invariant that "if this file
+ descriptor exists, the OS file descriptor is still open."
+
+ `BorrowedFd`'s lifetime parameter demands that there exists another value (in
+ this case a file, in the Unix sense) in your program that lasts as long as the
+ `BorrowedFd` or outlives it.
+
+ This has been encoded by the API designer to mean _that other value is what
+ keeps the access to the file open_.
+
+ Because `BorrowedFd` has a lifetime parameter from that other value, users of
+ the API can assume "this file descriptor existing means the file is open, and
+ we don't need to manage or check that external state itself."
+
+ Its counterpart `OwnedFd` is instead a file descriptor that closes that file
+ on drop.
+
+## More to Explore
+
+- This way of encoding information in types is very powerful when combined with
+ unsafe, as the ways one can manipulate lifetimes becomes almost arbitrary.
+ This is also dangerous, but when combined with tools like external,
+ mechanically-verified proofs we can safely encode cyclic/self-referential
+ types while encoding lifetime & safety expectations in the relevant data
+ types.
+
+- The [GhostCell (2021)](https://plv.mpi-sws.org/rustbelt/ghostcell/) paper and
+ its [relevant implementation](https://gitlab.mpi-sws.org/FP/ghostcell) show
+ this kind of work off. While the borrow checker is restrictive, there are
+ still ways to use escape hatches and then _show that the ways you used those
+ escape hatches are consistent and safe._
+
+
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata.md
deleted file mode 100644
index c9539ff95365..000000000000
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata.md
+++ /dev/null
@@ -1,98 +0,0 @@
----
-minutes: 10
----
-
-# Lifetime Relationships primer: PhantomData
-
-Lying to the type system safely, to make things easier for us and encode
-invariants better.
-
-```rust,editable,compile_fail
-// use std::marker::PhantomData;
-
-pub struct TypeTaggedString {
- data: String,
-}
-
-fn main() {}
-```
-
-
-
-- Ask: Why won't this slide compile?
-
- Answer: Unused type parameters!
-
-- Motivation: We want to be able to "tag" structures with different type
- parameters as a way to tell them apart or pass on lifetime information to
- them.
-
- See: [Typestate Generics](../typestate-pattern/typestate-generics.md) for
- instances of telling apart different data relevant to stages of an algorithm
- with type parameter differences.
-
- In practice, these "tags" tend to be zero-sized types. What they mean will
- depend on the shape and context of the API they're a part of.
-
-- Demonstrate: Add a field of type `marker: T` to `TypeTaggedString`
-
- Ask: What issues does having it be an actual instance of that type pose?
-
- Answer: If it's not a zero-sized type (like `()` or `struct MyTag;`), then
- we're allocating more memory than we need to when all we care for is type
- information.
-
- Alternatively: Makes initializing the data a pain for users and the
- maintainers of the library alike, as there needs to be an additional parameter
- for unnecessary data.
-
-- Demonstrate: Uncomment the `PhantomData` import, and implement the following:
-
- ```rust,compile_fail
- pub struct TypeTaggedString {
- data: String,
- _phantom: PhantomData,
- }
- ```
-
-- `PhantomData` lets developers "tag" types with type and lifetime parameters
- that are not "really" present in the struct or enum.
-
- `PhantomData` can be used with the Typestate pattern to have data with the
- same structure i.e. `TaggedData` can have methods or trait
- implementations that `TaggedData` doesn't.
-
-- This can be thought of as a more general take on how zero-sized types are all
- "the same structure" (zero size = only one possible value) but with different
- types.
-
-- Ask: Why don't we just do newtypes for every case as they come up?
-
- Answer: Avoiding repeating ourselves! Would have to implement any methods
- again for every new type. Having information in a `` argument for a type
- that gets erased at run-time lets us write implementations that work for all
- possible values of `T`. Newtyping all possible use cases means a lot of
- re-implementation.
-
- Alternatively, a type parameter for a type helps provide semantic context to a
- user. `Stage` offers a lot more contextual detail than `StageStart`,
- and implies there is shared behavior between stages.
-
-- Demonstrate: We can also capture lifetime parameters with `PhantomData`
- without needing to hold onto a value with that lifetime.
-
- Implement the following:
-
- ```rust,compile_fail
- pub struct LifetimeTaggedString<'a> {
- data: String,
- _phantom: PhantomData<&'a ()>,
- }
- ```
-
- Ask: Why would we do this?
-
- Answer: To tie an owned value to another value's lifetime elsewhere in the
- program.
-
-
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
index 8849ffaf0bf9..e58eb898b29d 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md
@@ -5,7 +5,7 @@ minutes: 10
# Single-use values
In some circumstances we want values that _can only be used once_. One critical
-example of this is in cryptography: "Nonces."
+example of this is in cryptography: A "Nonce."
```rust,editable
pub struct Key(/* specifics omitted */);
@@ -39,18 +39,27 @@ fn main() {
Background: In practice people have ended up accidentally re-using nonces.
Most commonly, this causes the cryptographic protocol to completely break down
- and stop fulfilling its function. Depending on the specifics of nonce reuse
- and cryptography at hand, private keys can also become computable by
- attackers.
+ and stop fulfilling its function.
-- Rust has an obvious tool for "Once you use this, you can't use it anymore":
- Using a value as an _owned argument_.
+ Depending on the specifics of nonce reuse and cryptography at hand, private
+ keys can also become computable by attackers.
-- Highlight: the `encrypt` function takes `nonce` by value (an owned argument), but `key` and `data` by reference.
+- Rust has an obvious tool for achieving the invariant "Once you use this, you
+ can't use it again": Using a value as an _owned argument_.
-- By keeping constructors private and not implementing clone/copy for a type,
- making the interior type opaque (as per the newtype pattern), we can prevent
- multiple uses of the same value.
+- Highlight: the `encrypt` function takes `nonce` by value (an owned argument),
+ but `key` and `data` by reference.
+
+- The technique for single-use values is as follows:
+
+ - Keep constructors private, so a user can't construct values with the same
+ inner value twice.
+
+ - Don't implement `Clone`/`Copy` traits or equivalent methods, so a user can't
+ duplicate data we want to keep unique.
+
+ - Make the interior type opaque (like with the newtype pattern), so the user
+ cannot modify an existing value on their own.
- Ask: What are we missing from the newtype pattern in the slide's code?
@@ -61,7 +70,9 @@ fn main() {
Fix: Put `Key`, `Nonce`, and `new_nonce` behind a module.
-- Cryptography Nuance: There is still the case where a nonce may be used twice
+## More to Explore
+
+- Cryptography Nuance: There is still the case where a nonce might be used twice
if it's created through purely a pseudo-random process with no additional
metadata, and that circumstance can't be avoided through this particular
method. This API design prevents one kind of misuse, but not all kinds.
From d8fb34aa76726d447f032bb0f439294132663c5e Mon Sep 17 00:00:00 2001
From: tall-vase
Date: Wed, 29 Oct 2025 14:52:40 +0000
Subject: [PATCH 24/24] Remove compile_fail marker
---
.../borrow-checker-invariants/phantomdata-01-types.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-01-types.md b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-01-types.md
index ad0a0bf78727..15c09b6f8484 100644
--- a/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-01-types.md
+++ b/src/idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-01-types.md
@@ -5,7 +5,7 @@ minutes: 15
# PhantomData 1/2: Type-level tagging
-```rust,editable,compile_fail
+```rust,editable
// use std::marker::PhantomData;
pub trait ChatUser {}