diff --git a/GEMINI.md b/GEMINI.md index 7c5b620e40f4..6aaffa2ec13c 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -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 ` to verify the specific exercise crate. + ## Markdown Conventions - **Headings:** diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 2a2473624de2..54a11efc60f4 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -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) diff --git a/src/borrowing/Cargo.toml b/src/borrowing/Cargo.toml index 38495770cdf1..8eb2b123f381 100644 --- a/src/borrowing/Cargo.toml +++ b/src/borrowing/Cargo.toml @@ -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" diff --git a/src/borrowing/exercise.md b/src/borrowing/exercise.md index 3d8a28f1f939..96081eb02338 100644 --- a/src/borrowing/exercise.md +++ b/src/borrowing/exercise.md @@ -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 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}} ``` + +
+ +- 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). + +
diff --git a/src/borrowing/exercise.rs b/src/borrowing/exercise.rs new file mode 100644 index 000000000000..334439b6218f --- /dev/null +++ b/src/borrowing/exercise.rs @@ -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, + 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 diff --git a/src/borrowing/solution.md b/src/borrowing/solution.md index 208f24d3d29d..52ae54f94304 100644 --- a/src/borrowing/solution.md +++ b/src/borrowing/solution.md @@ -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}} ``` diff --git a/src/credits.md b/src/credits.md index 3824305dcf3d..6b0b283d7ced 100644 --- a/src/credits.md +++ b/src/credits.md @@ -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 diff --git a/third_party/rust-on-exercism/LICENSE b/third_party/rust-on-exercism/LICENSE deleted file mode 100644 index 90e73be03b5e..000000000000 --- a/third_party/rust-on-exercism/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 Exercism - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/third_party/rust-on-exercism/README.md b/third_party/rust-on-exercism/README.md deleted file mode 100644 index e813bb89af96..000000000000 --- a/third_party/rust-on-exercism/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Exercism Rust Track - -This directory contains exercises copied from the Exercism Rust Track. Please -see for the full project. - -## License - -The Exercism Rust Track is licensed under the MIT license ([LICENSE](LICENSE)). diff --git a/third_party/rust-on-exercism/health-statistics.md b/third_party/rust-on-exercism/health-statistics.md deleted file mode 100644 index 72b2a774d24f..000000000000 --- a/third_party/rust-on-exercism/health-statistics.md +++ /dev/null @@ -1,6 +0,0 @@ -You're working on implementing a health-monitoring system. As part of that, you -need to keep track of users' health statistics. - -You'll start with a stubbed function in an `impl` block as well as a `User` -struct definition. Your goal is to implement the stubbed out method on the -`User` `struct` defined in the `impl` block. diff --git a/third_party/rust-on-exercism/health-statistics.rs b/third_party/rust-on-exercism/health-statistics.rs deleted file mode 100644 index c0d38f8a4ac5..000000000000 --- a/third_party/rust-on-exercism/health-statistics.rs +++ /dev/null @@ -1,69 +0,0 @@ -// ANCHOR: solution -// ANCHOR: setup - -#![allow(dead_code)] -pub struct User { - name: String, - age: u32, - height: f32, - visit_count: u32, - last_blood_pressure: Option<(u32, u32)>, -} - -pub struct Measurements { - height: f32, - blood_pressure: (u32, u32), -} - -pub struct HealthReport<'a> { - patient_name: &'a str, - visit_count: u32, - height_change: f32, - blood_pressure_change: Option<(i32, i32)>, -} - -impl User { - pub fn new(name: String, age: u32, height: f32) -> Self { - Self { name, age, height, visit_count: 0, last_blood_pressure: None } - } - // ANCHOR_END: setup - - // ANCHOR: User_visit_doctor - pub fn visit_doctor(&mut self, measurements: Measurements) -> HealthReport<'_> { - // ANCHOR_END: User_visit_doctor - self.visit_count += 1; - let bp = measurements.blood_pressure; - let report = HealthReport { - patient_name: &self.name, - visit_count: self.visit_count, - height_change: measurements.height - self.height, - blood_pressure_change: self - .last_blood_pressure - .map(|lbp| (bp.0 as i32 - lbp.0 as i32, bp.1 as i32 - lbp.1 as i32)), - }; - self.height = measurements.height; - self.last_blood_pressure = Some(bp); - report - } -} - -// ANCHOR: tests -#[test] -fn test_visit() { - let mut bob = User::new(String::from("Bob"), 32, 155.2); - assert_eq!(bob.visit_count, 0); - let report = - bob.visit_doctor(Measurements { height: 156.1, blood_pressure: (120, 80) }); - assert_eq!(report.patient_name, "Bob"); - assert_eq!(report.visit_count, 1); - assert_eq!(report.blood_pressure_change, None); - assert!((report.height_change - 0.9).abs() < 0.00001); - - let report = - bob.visit_doctor(Measurements { height: 156.1, blood_pressure: (115, 76) }); - - assert_eq!(report.visit_count, 2); - assert_eq!(report.blood_pressure_change, Some((-5, -4))); - assert_eq!(report.height_change, 0.0); -} -// ANCHOR_END: tests