Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,34 @@ list of options.
- **`setup-rust-cache`:** A composite action that configures the
`Swatinem/rust-cache` action.

## Developing Exercises

Exercises allow students to practice what they have learned. When adding or
updating exercises, follow these structural conventions:

- **File Structure:**
- `exercise.md`: Contains the problem description and a code block with
placeholders.
- `exercise.rs`: Contains the full solution code, including a license header
and `ANCHOR` tags to delimit sections.
- `solution.md`: Includes the full solution code from `exercise.rs`.
- `Cargo.toml`: Must define a `[[bin]]` target pointing to `exercise.rs` so
that the solution code is compiled and tested.

- **Content Inclusion:**
- Use `{{#include exercise.rs:anchor_name}}` in `exercise.md` to show specific
parts of the code (e.g., setup, main).
- Use `{{#include exercise.rs:solution}}` in `solution.md` to show the
solution code _without_ the license header. Ensure `exercise.rs` has a
`// ANCHOR: solution` line before the first line of the solution. It is
unnecessary to add a `// ANCHOR_END: solution` line at the bottom of the
file.

- **Testing:**
- Run `cargo xtask rust-tests` to ensure the solution code compiles and runs
correctly.
- Run `cargo check -p <crate_name>` to verify the specific exercise crate.

## Markdown Conventions

- **Headings:**
Expand Down
2 changes: 1 addition & 1 deletion src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@
- [Interior Mutability](borrowing/interior-mutability.md)
- [`Cell`](borrowing/interior-mutability/cell.md)
- [`RefCell`](borrowing/interior-mutability/refcell.md)
- [Exercise: Health Statistics](borrowing/exercise.md)
- [Exercise: Wizard's Inventory](borrowing/exercise.md)
- [Solution](borrowing/solution.md)
- [Lifetimes](lifetimes.md)
- [Borrowing and Functions](lifetimes/simple-borrows.md)
Expand Down
4 changes: 2 additions & 2 deletions src/borrowing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ version = "0.1.0"
edition = "2024"
publish = false

[lib]
[[bin]]
name = "borrowing"
path = "../../third_party/rust-on-exercism/health-statistics.rs"
path = "exercise.rs"
57 changes: 48 additions & 9 deletions src/borrowing/exercise.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,59 @@
minutes: 20
---

# Exercise: Health Statistics
# Exercise: Wizard's Inventory

{{#include ../../third_party/rust-on-exercism/health-statistics.md}}
In this exercise, you will manage a wizard's inventory using what you have
learned about borrowing and ownership.

Copy the code below to <https://play.rust-lang.org/> and fill in the missing
method:
- The wizard has a collection of spells. You need to implement functions to add
spells to the inventory and to cast spells from them.

```rust,editable
{{#include ../../third_party/rust-on-exercism/health-statistics.rs:setup}}
- Spells have a limited number of uses. When a spell has no uses left, it must
be removed from the wizard's inventory.

{{#include ../../third_party/rust-on-exercism/health-statistics.rs:User_visit_doctor}}
todo!("Update a user's statistics based on measurements from a visit to the doctor")
```rust,editable,compile_fail
{{#include exercise.rs:setup}}

// TODO: Implement `add_spell` to take ownership of a spell and add it to
// the wizard's inventory.
fn add_spell(..., spell: ...) {
todo!()
}

// TODO: Implement `cast_spell` to borrow a spell from the inventory and
// cast it. The wizard's mana should decrease by the spell's cost and the
// number of uses for the spell should decrease by 1.
//
// If the wizard doesn't have enough mana, the spell should fail.
// If the spell has no uses left, it is removed from the inventory.
fn cast_spell(..., name: ...) {
todo!()
}
}

{{#include ../../third_party/rust-on-exercism/health-statistics.rs:tests}}
{{#include exercise.rs:main}}

{{#include exercise.rs:tests}}
```

<details>

- The goal of this exercise is to practice the core concepts of ownership and
borrowing, specifically the rule that you cannot mutate a collection while
holding a reference to one of its elements.
- `add_spell` should take ownership of a `Spell` and move it into the `Wizard`'s
inventory.
- `cast_spell` is the core of the exercise. It needs to:
1. Find the spell (by index or by reference).
2. Check mana and decrement it.
3. Decrement the spell's `uses`.
4. Remove the spell if `uses == 0`.
- **Borrow Checker Conflict:** If students try to hold a reference to the spell
(e.g., `let spell = &mut self.spells[i]`) and then call
`self.spells.remove(i)` while that reference is still "alive" in the same
scope, the borrow checker will complain. This is a great opportunity to show
how to structure code to satisfy the borrow checker (e.g., by using indices or
by ensuring the borrow ends before the mutation).

</details>
141 changes: 141 additions & 0 deletions src/borrowing/exercise.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// ANCHOR: solution
// ANCHOR: setup
struct Spell {
name: String,
cost: u32,
uses: u32,
}

struct Wizard {
spells: Vec<Spell>,
mana: u32,
}

impl Wizard {
fn new(mana: u32) -> Self {
Wizard { spells: vec![], mana }
}
// ANCHOR_END: setup

fn add_spell(&mut self, spell: Spell) {
self.spells.push(spell);
}

fn cast_spell(&mut self, name: &str) {
let mut spell_idx = None;
for idx in 0..self.spells.len() {
if self.spells[idx].name == name {
spell_idx = Some(idx);
break;
}
}

let Some(idx) = spell_idx else {
println!("Spell {} not found!", name);
return;
};

let spell = &mut self.spells[idx];
if self.mana >= spell.cost {
self.mana -= spell.cost;
spell.uses -= 1;
println!(
"Casting {}! Mana left: {}. Uses left: {}",
spell.name, self.mana, spell.uses
);
if spell.uses == 0 {
self.spells.remove(idx);
}
} else {
println!("Not enough mana to cast {}!", spell.name);
}
}
}

// ANCHOR: main
fn main() {
let mut merlin = Wizard::new(20);
let fireball = Spell { name: String::from("Fireball"), cost: 10, uses: 2 };
let ice_blast = Spell { name: String::from("Ice Blast"), cost: 15, uses: 1 };

merlin.add_spell(fireball);
merlin.add_spell(ice_blast);

merlin.cast_spell("Fireball"); // Casts successfully
merlin.cast_spell("Ice Blast"); // Casts successfully, then removed
merlin.cast_spell("Ice Blast"); // Fails (not found)
merlin.cast_spell("Fireball"); // Casts successfully, then removed
merlin.cast_spell("Fireball"); // Fails (not found)
}
// ANCHOR_END: main

// ANCHOR: tests
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_add_spell() {
let mut wizard = Wizard::new(10);
let spell = Spell { name: String::from("Fireball"), cost: 5, uses: 3 };
wizard.add_spell(spell);
assert_eq!(wizard.spells.len(), 1);
}

#[test]
fn test_cast_spell() {
let mut wizard = Wizard::new(10);
let spell = Spell { name: String::from("Fireball"), cost: 5, uses: 3 };
wizard.add_spell(spell);

wizard.cast_spell("Fireball");
assert_eq!(wizard.mana, 5);
assert_eq!(wizard.spells.len(), 1);
assert_eq!(wizard.spells[0].uses, 2);
}

#[test]
fn test_cast_spell_insufficient_mana() {
let mut wizard = Wizard::new(10);
let spell = Spell { name: String::from("Fireball"), cost: 15, uses: 3 };
wizard.add_spell(spell);

wizard.cast_spell("Fireball");
assert_eq!(wizard.mana, 10);
assert_eq!(wizard.spells.len(), 1);
assert_eq!(wizard.spells[0].uses, 3);
}

#[test]
fn test_cast_spell_not_found() {
let mut wizard = Wizard::new(10);
wizard.cast_spell("Fireball");
assert_eq!(wizard.mana, 10);
}

#[test]
fn test_cast_spell_removal() {
let mut wizard = Wizard::new(10);
let spell = Spell { name: String::from("Fireball"), cost: 5, uses: 1 };
wizard.add_spell(spell);

wizard.cast_spell("Fireball");
assert_eq!(wizard.mana, 5);
assert_eq!(wizard.spells.len(), 0);
}
}
// ANCHOR_END: tests
8 changes: 6 additions & 2 deletions src/borrowing/solution.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Solution
---
minutes: 20
---

# Solution: Wizard's Inventory

```rust,editable
{{#include ../../third_party/rust-on-exercism/health-statistics.rs:solution}}
{{#include exercise.rs:solution}}
```
7 changes: 0 additions & 7 deletions src/credits.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,6 @@ license, please see
[`LICENSE`](https://github.com/google/comprehensive-rust/blob/main/LICENSE) for
details.

## Rust on Exercism

Some exercises have been copied and adapted from
[Rust on Exercism](https://exercism.org/tracks/rust). Please see the
`third_party/rust-on-exercism/` directory for details, including the license
terms.

## CXX

The [Interoperability with C++](android/interoperability/cpp.md) section uses an
Expand Down
21 changes: 0 additions & 21 deletions third_party/rust-on-exercism/LICENSE

This file was deleted.

8 changes: 0 additions & 8 deletions third_party/rust-on-exercism/README.md

This file was deleted.

6 changes: 0 additions & 6 deletions third_party/rust-on-exercism/health-statistics.md

This file was deleted.

69 changes: 0 additions & 69 deletions third_party/rust-on-exercism/health-statistics.rs

This file was deleted.