Skip to content

Commit

Permalink
Various fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
willcrichton committed Sep 26, 2024
1 parent e92457f commit 028a6c1
Show file tree
Hide file tree
Showing 3 changed files with 25 additions and 22 deletions.
8 changes: 4 additions & 4 deletions quizzes/ch17-05-design-challenge-intermediates.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
q = """
**Context:** You are designing a serialization library that converts Rust data types into formats like JSON.
**Functionality:** A `Serialize` trait that can be implemented by serializable types, and a `to_string` function that converts serializable types into JSON.
**Functionality:** A `Serialize` trait that can be implemented by serializable types, and a `to_json` function that converts serializable types into JSON.
**Designs:** Below are several proposed designs to implement the functionality.
Expand All @@ -21,7 +21,7 @@ fn value_to_json(value: Value) -> String {
/* .. */
}
pub fn to_json(data: impl Serialize) -> String {
pub fn to_json(data: impl Serialize) -> String {
let value = data.serialize();
value_to_json(value)
}
Expand All @@ -42,7 +42,7 @@ impl Serializer for JsonSerializer {
/* .. */
}
pub fn to_json(data: impl Serialize) -> String {
pub fn to_json(data: impl Serialize) -> String {
let mut serializer = JsonSerializer { buffer: String::new() };
data.serialize(&mut serializer);
serializer.buffer
Expand Down Expand Up @@ -100,7 +100,7 @@ prompt.distractors = ["2"]
prompt.sortAnswers = true
answer.answer = ["1"]
context = """
With Option 1, there is only a single instantiation of `Serialize` that converts a type `T` into a `Value`. Because Option 2 is generic over serializers `S`,
With Option 1, there is only a single instantiation of `Serialize` that converts a type `T` into a `Value`. Because Option 2 is generic over serializers `S`,
then every time `T::serialize` is called with a new `S`, the Rust compiler will monomorphize a new instance of `T::serialize` which would increase the size of
the binary by comparison to Option 1.
"""
Expand Down
31 changes: 17 additions & 14 deletions src/ch04-02-references-and-borrowing.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,12 @@ Data can be aliased. Data can be mutated. But data cannot be _both_ aliased _and

However, because references are non-owning pointers, they need different rules than boxes to ensure the *Pointer Safety Principle*. By design, references are meant to temporarily create aliases. In the rest of this section, we will explain the basics of how Rust ensures the safety of references through the **borrow checker.**

### References Change Permissions on Paths
### References Change Permissions on Places

The core idea behind the borrow checker is that variables have three kinds of **permissions** on their data:

- **Read** (@Perm{read}): data can be copied to another location.
- **Write** (@Perm{write}): data can be mutated in-place.
- **Write** (@Perm{write}): data can be mutated.
- **Own** (@Perm{own}): data can be moved or dropped.

These permissions don't exist at runtime, only within the compiler. They describe how the compiler "thinks" about your program before the program is executed.
Expand All @@ -205,7 +205,7 @@ Let's walk through each line:
2. After `let num = &v[2]`, the data in `v` has been **borrowed** by `num` (indicated by <i class="fa fa-arrow-right"></i>). Three things happen:
- The borrow removes @Perm[lost]{write}@Perm[lost]{own} permissions from `v` (the slash indicates loss). `v` cannot be written or owned, but it can still be read.
- The variable `num` has gained @Perm{read}@Perm{own} permissions. `num` is not writable (the missing @Perm{write} permission is shown as a dash <span class="perm write">‒</span>) because it was not marked `let mut`.
- The **path** `*num` has gained the @Perm{read} permission.
- The **place** `*num` has gained the @Perm{read} permission.
3. After `println!(...)`, then `num` is no longer in use, so `v` is no longer borrowed. Therefore:
- `v` regains its @Perm{write}@Perm{own} permissions (indicated by <i class="fa fa-rotate-left"></i>).
- `num` and `*num` have lost all of their permissions (indicated by <i class="fa fa-level-down"></i>).
Expand All @@ -221,18 +221,18 @@ let mut x_ref = &x;
#}
```

Notice that `x_ref` has the @Perm{write} permission, while `*x_ref` does not. That means we can assign `x_ref` to a different reference (e.g. `x_ref = &y`), but we cannot mutate the pointed data (e.g. `*x_ref += 1`).
Notice that `x_ref` has the @Perm{write} permission, while `*x_ref` does not. That means we can assign a different reference to the `x_ref` variable (e.g. `x_ref = &y`), but we cannot mutate the data it points to (e.g. `*x_ref += 1`).

More generally, permissions are defined on **paths** and not just variables. A path is anything you can put on the left-hand side of an assignment. Paths include:
More generally, permissions are defined on **places** and not just variables. A place is anything you can put on the left-hand side of an assignment. Places include:

- Variables, like `a`.
- Dereferences of paths, like `*a`.
- Array accesses of paths, like `a[0]`.
- Fields of paths, like `a.0` for tuples or `a.field` for structs (discussed next chapter).
- Dereferences of places, like `*a`.
- Array accesses of places, like `a[0]`.
- Fields of places, like `a.0` for tuples or `a.field` for structs (discussed next chapter).
- Any combination of the above, like `*((*a)[0].1)`.


Second, why do paths lose permissions when they become unused? Because some permissions are mutually exclusive. If `num = &v[2]`, then `v` cannot be mutated or dropped while `num` is in use. But that doesn't mean it's invalid to use `num` for more time. For example, if we add another `print` to the above program, then `num` simply loses its permissions later:
Second, why do places lose permissions when they become unused? Because some permissions are mutually exclusive. If you write `num = &v[2]`, then `v` cannot be mutated or dropped while `num` is in use. But that doesn't mean it's invalid to use `num` again. For example, if we add another `println!` to the above program, then `num` simply loses its permissions one line later:

```aquascope,permissions,stepper
#fn main() {
Expand All @@ -244,9 +244,12 @@ v.push(4);
#}
```

It's only a problem if you attempt to use `num` again *after* mutating `v`. Let's look at this in more detail.


### The Borrow Checker Finds Permission Violations

Recall the *Pointer Safety Principle*: data should not be aliased and mutated. The goal of these permissions is to ensure that data cannot be mutated if it is aliased. Creating a reference to data ("borrowing" it) causes that data to be temporarily read-only until the reference is no longer used.
Recall the *Pointer Safety Principle*: data should not be aliased and mutated. The goal of these permissions is to ensure that data cannot be mutated if it is aliased. Creating a reference to data ("borrowing" it) causes that data to be temporarily read-only until the reference is no longer in use.

Rust uses these permissions in its **borrow checker**. The borrow checker looks for potentially unsafe operations involving references. Let's return to the unsafe program we saw earlier, where `push` invalidates a reference. This time we'll add another aspect to the permissions diagram:

Expand All @@ -259,7 +262,7 @@ println!("Third element is {}", *num);
#}
```

Any time a path is used, Rust expects that path to have certain permissions depending on the operation. For example, the borrow `&v[2]` requires that `v` is readable. Therefore the @Perm{read} permission is shown between the operation `&` and the path `v`. The letter is filled-in because `v` has the read permission at that line.
Any time a place is used, Rust expects that place to have certain permissions depending on the operation. For example, the borrow `&v[2]` requires that `v` is readable. Therefore the @Perm{read} permission is shown between the operation `&` and the place `v`. The letter is filled-in because `v` has the read permission at that line.

By contrast, the mutating operation `v.push(4)` requires that `v` is readable and writable. Both @Perm{read} and @Perm{write} are shown. However, `v` does not have write permissions (it is borrowed by `num`). So the letter @Perm[missing]{write} is hollow, indicating that the write permission is *expected* but `v` does not have it.

Expand Down Expand Up @@ -301,9 +304,9 @@ println!("Vector is now {:?}", v);
A mutable reference is created with the `&mut` operator. The type of `num` is written as `&mut i32`. Compared to immutable references, you can see two important differences in the permissions:

1. When `num` was an immutable reference, `v` still had the @Perm{read} permission. Now that `num` is a mutable reference, `v` has lost _all_ permissions while `num` is in use.
2. When `num` was an immutable reference, the path `*num` only had the @Perm{read} permission. Now that `num` is a mutable reference, `*num` has also gained the @Perm{write} permission.
2. When `num` was an immutable reference, the place `*num` only had the @Perm{read} permission. Now that `num` is a mutable reference, `*num` has also gained the @Perm{write} permission.

The first observation is what makes mutable references *safe*. Mutable references allow mutation but prevent aliasing. The borrowed path `v` becomes temporarily unusable, so effectively not an alias.
The first observation is what makes mutable references *safe*. Mutable references allow mutation but prevent aliasing. The borrowed place `v` becomes temporarily unusable, so effectively not an alias.

The second observation is what makes mutable references *useful*. `v[2]` can be mutated through `*num`. For example, `*num += 1` mutates `v[2]`. Note that `*num` has the @Perm{write} permission, but `num` does not. `num` refers to the mutable reference itself, e.g. `num` cannot be reassigned to a *different* mutable reference.

Expand Down Expand Up @@ -450,7 +453,7 @@ References provide the ability to read and write data without consuming ownershi
However, references can be easily misused. Rust's borrow checker enforces a system of permissions that ensures references are used safely:

- All variables can read, own, and (optionally) write their data.
- Creating a reference will transfer permissions from the borrowed path to the reference.
- Creating a reference will transfer permissions from the borrowed place to the reference.
- Permissions are returned once the reference's lifetime has ended.
- Data must outlive all references that point to it.

Expand Down
8 changes: 4 additions & 4 deletions src/ch04-03-fixing-ownership-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ assert!(v.len() == 0);

### Fixing a Safe Program: Mutating Different Tuple Fields

The above examples are cases where a program is unsafe. Rust may also reject safe programs. One common issue is that Rust tries to track permissions at a fine-grained level. However, Rust may conflate two different paths as the same path.
The above examples are cases where a program is unsafe. Rust may also reject safe programs. One common issue is that Rust tries to track permissions at a fine-grained level. However, Rust may conflate two different places as the same place.

Let's first look at an example of fine-grained permission tracking that passes the borrow checker. This program shows how you can borrow one field of a tuple, and write to a different field of the same tuple:

Expand All @@ -335,7 +335,7 @@ println!("{first} {}", name.1);

The statement `let first = &name.0` borrows `name.0`. This borrow removes @Perm{write}@Perm{own} permissions from `name.0`. It also removes @Perm{write}@Perm{own} permissions from `name`. (For example, one could not pass `name` to a function that takes as input a value of type `(String, String)`.) But `name.1` still retains the @Perm{write} permission, so doing `name.1.push_str(...)` is a valid operation.

However, Rust can lose track of exactly which paths are borrowed. For example, let's say we refactor the expression `&name.0` into a function `get_first`. Notice how after calling `get_first(&name)`, Rust now removes the @Perm{write} permission on `name.1`:
However, Rust can lose track of exactly which places are borrowed. For example, let's say we refactor the expression `&name.0` into a function `get_first`. Notice how after calling `get_first(&name)`, Rust now removes the @Perm{write} permission on `name.1`:

```aquascope,permissions,stepper,boundaries,shouldFail
fn get_first(name: &(String, String)) -> &String {
Expand Down Expand Up @@ -375,7 +375,7 @@ Remember, the key idea is that **the program above is safe.** It has no undefine

### Fixing a Safe Program: Mutating Different Array Elements

A similar kind of problem arises when we borrow elements of an array. For example, observe what paths are borrowed when we take a mutable reference to an array:
A similar kind of problem arises when we borrow elements of an array. For example, observe what places are borrowed when we take a mutable reference to an array:

```aquascope,permissions,stepper,boundaries
#fn main() {
Expand All @@ -386,7 +386,7 @@ println!("{a:?}");
#}
```

Rust's borrow checker does not contain different paths for `a[0]`, `a[1]`, and so on. It uses a single path `a[_]` that represents *all* indexes of `a`. Rust does this because it cannot always determine the value of an index. For example, imagine a more complex scenario like this:
Rust's borrow checker does not contain different places for `a[0]`, `a[1]`, and so on. It uses a single place `a[_]` that represents *all* indexes of `a`. Rust does this because it cannot always determine the value of an index. For example, imagine a more complex scenario like this:

```rust,ignore
let idx = a_complex_function();
Expand Down

0 comments on commit 028a6c1

Please sign in to comment.