See ROADMAP.md for a scannable overview of all 80 lessons.
This repo contains small Rust lessons, each with:
- an
examplebinary (complete reference) - an
exercisebinary (starter stubs withTODOs) - an integration test file for validation
src/bin/c01_example.rs...src/bin/c80_example.rssrc/bin/c01_exercise.rs...src/bin/c80_exercise.rstests/c01_tests.rs...tests/c80_tests.rs
- Read and run the example.
- Open the matching exercise file.
- Fill in
TODOsections to match behavior. - Run the matching test file until green.
Run a lesson example:
cargo run --bin c05_exampleRun a lesson exercise:
cargo run --bin c05_exerciseRun validation for one lesson:
cargo test --test c05_testsRun all lesson tests:
cargo test --testsTwo views over the same save file (.rustacean_save.json, gitignored).
Terminal character sheet — scans your test results, levels you up, unlocks abilities:
cargo run --bin progress # incremental scan
cargo run --bin progress -- --rescan # re-test all 80Gamified HTML dashboard — XP ring, chapter radar, activity heatmap, chapter map, boss HP bars, trophy case, and pace stats:
cargo run --bin dashboard # writes dashboard.html and opens it
cargo run --bin dashboard -- --no-openprogress owns all writes; dashboard only reads. Run progress first, then dashboard.
Work through one lesson at a time. Read the example, then complete the exercise. Don't move on until the exercise compiles and runs correctly.
Learn: let, mut, basic types (i32, u8, bool)
Exercise: Write a function that doubles a number.
You're done when: You can declare variables with explicit types and return values from functions.
Learn: &str vs String, format!, println!
Exercise: Write a function that returns a formatted greeting string.
You're done when: You understand why removing a semicolon changes what a function returns.
Learn: Fixed-size arrays [T; N], .iter(), .sum()
Exercise: Sum all elements of a four-element array.
You're done when: You can iterate over an array and use iterator methods.
Learn: Tuple returns, destructuring, as casting
Exercise: Return the min and max of a five-element array as a tuple.
You're done when: You can write a function that returns multiple values and cast between numeric types.
Learn: if/else, for ranges, Vec::new(), .push()
Exercise: Collect all even numbers from 1 to n into a Vec.
You're done when: You can use a loop with a condition to build a vector.
Learn: match, .to_string(), .join()
Exercise: Implement FizzBuzz returning a Vec<String>.
You're done when: You can use match with multiple patterns and build a vector in a loop.
Learn: Ownership rules, String::from(), borrowing with &
Exercise: Write a function that uppercases a borrowed string and appends "!".
You're done when: You can explain why passing &str instead of String avoids transferring ownership.
Learn: .split_whitespace(), .next(), Option with .map_or()
Exercise: Find the length of the first word in a string slice.
You're done when: You can chain iterator methods and handle Option values.
Learn: struct definition, instances, field access, #[derive(Debug)]
Exercise: Create a Point struct and compute its distance from the origin.
You're done when: You can define a struct and pass references to it.
Learn: impl, &self, methods taking references
Exercise: Add area() and can_hold() methods to a Rectangle struct.
You're done when: You can add behaviour to a struct and call methods on it.
Learn: enum definition, variants with data, matching on enums
Exercise: Match on command variants and return result strings.
You're done when: You can define an enum with data and match on every variant.
Learn: Option<T>, Some/None, .unwrap_or(), if let
Exercise: Safely retrieve an element from a slice by index.
You're done when: You can return Some or None and handle both cases.
Learn: HashMap, .insert(), .get(), iteration
Exercise: Count the frequency of each character in a string.
You're done when: You can build and query a HashMap.
Learn: .entry(), .or_insert(), .or_default(), .and_modify()
Exercise: Count occurrences of each integer in a slice using the entry API.
You're done when: You can use the entry API without looking up the syntax.
Learn: .to_lowercase(), .trim_matches(), char predicates like .is_alphanumeric()
Exercise: Clean a single word — lowercase it and strip surrounding punctuation.
You're done when: You can write a normalization function that handles messy input.
Learn: Combining the entry API with string normalization on real input Exercise: Count word frequencies in a sentence, normalising case and punctuation. You're done when: You can compose a multi-step text-processing pipeline.
Learn: Result<T, E>, Ok/Err, .parse::<T>(), match on Result
Exercise: Parse a string as a u8 age, returning Err for invalid input.
You're done when: You can return meaningful error messages from a function.
Learn: ? early-return on Err when both calls share the same error type
Exercise: Parse two numbers and return them as a tuple using ? twice.
You're done when: You can chain fallible calls with ? without match.
Learn: .map_err() to convert error types, building friendly error strings
Exercise: Parse two strings as f64 and divide them, handling all error cases with descriptive messages.
You're done when: You can bridge different error types into a single Result.
Learn: Anonymous functions (|x| x + 1), capturing environment, impl Fn(...)
Exercise: Write an apply(x, f) function that runs a closure on a value.
You're done when: You can pass closures around and use ones that capture local state.
Learn: .map().collect() — transforming an iterator into a new Vec
Exercise: Double every number in a slice and collect into a Vec.
You're done when: You can turn an iterator chain into a concrete collection with .collect().
Learn: .filter() — selecting elements from an iterator; the &&n double-reference pattern
Exercise: Return the squares of the even numbers in a slice.
You're done when: You understand why .filter() gives &&i32 and can destructure it.
Learn: .sum() — the simplest aggregator, collapsing an iterator into one value
Exercise: Sum all integers in a slice.
You're done when: You can aggregate an iterator without a manual loop.
Learn: .fold(init, |acc, &n| ...) — general-purpose reduction
Exercise: Compute the product of all integers in a slice.
You're done when: You can express any accumulation pattern with .fold().
Learn: {:?} vs {}, format!("{:?}", ...), how #[derive(Debug)] connects to traits
Exercise: Return the debug representation of a slice as a String.
You're done when: You know when to use {:?} and why Vec can't use {}.
Learn: trait definition, impl Trait for Type, &impl Trait arguments
Exercise: Define a Describable trait and implement it for an Item struct.
You're done when: You can define a trait and write a function that accepts any type implementing it.
Learn: Type parameters <T> on functions, impl blocks, and structs
Exercise: Write a generic swap<T>(a, b) that returns (b, a) for any type.
You're done when: You understand what you can and can't do with an unconstrained T.
Learn: T: Ord — constraining generics so you can compare, copy, or use specific methods
Exercise: Write larger<T: Ord>(a, b) -> T that returns the bigger of two values.
You're done when: You can pick the minimum bounds needed for a generic function.
Learn: 'a annotations, why the borrow checker needs them, elision rules
Exercise: Write longer<'a>(a: &'a str, b: &'a str) -> &'a str that returns the longer slice.
You're done when: You can annotate a function that returns a borrowed reference.
Learn: Box::new(), heap allocation, auto-deref
Exercise: Write boxed_price(cents) -> Box<i32> to heap-allocate a salon service price.
You're done when: You can explain when boxing is necessary (recursive types, trait objects).
Learn: Reference-counted shared ownership, Rc::clone(), Rc::strong_count()
Exercise: 3 stations share one price list via Rc — return the strong count.
You're done when: You can share data between multiple owners without the compiler complaining.
Learn: mod, pub, use, file-based modules via #[path]
Exercise: Complete a ClientCounter walk-in tracker in a separate module.
You're done when: You can organise code across files and control visibility with pub.
Learn: Everything combined — modules + HashMap + Vec + Option + methods
Exercise: Complete a ServiceLog — track revenue per technician (Mai, Linh, Trang).
You're done when: All 33 core lesson tests pass.
Learn: enum BookingError { SlotTaken, TechnicianOff } — typed error variants
Exercise: Write validate_booking() returning the correct error variant.
You're done when: You can define and return custom errors from functions.
Learn: fmt::Display trait — how errors become human-readable strings
Exercise: Implement Display for BookingError with descriptive messages.
You're done when: Your errors print friendly messages, not just {:?} debug output.
Learn: std::error::Error — the trait that makes errors composable
Exercise: Wire BookingError into Box<dyn Error> by implementing the Error trait.
You're done when: Your custom error works anywhere Box<dyn Error> is expected.
Learn: #[derive(thiserror::Error)] — the boilerplate vanishes
Exercise: Replace manual Display + Error impls with thiserror derive macros.
You're done when: You see the same behavior with a fraction of the code.
Learn: anyhow::Result, bail!, .context() — one Result type for everything
Exercise: Write process_booking() that can fail 3 ways — anyhow catches all.
You're done when: You can handle mixed error types without custom enums.
Learn: Explicit slices, DST semantics, &vec[..]
Exercise: Write daily_revenue(prices: &[u32]) -> u32 — sum a slice of service prices.
You're done when: You can accept and operate on slices without owning the data.
Learn: struct Appointment<'a> — structs that borrow data
Exercise: Create an Appointment with borrowed client and service names.
You're done when: You can embed references in structs with lifetime annotations.
Learn: When Rust infers lifetimes for you — elision rules on methods
Exercise: Write methods on Appointment that return &str without explicit 'a.
You're done when: You know when you can omit lifetime annotations.
Learn: Interior mutability with Cell — mutate through &self
Exercise: Build a TipJar where add() takes &self (not &mut self).
You're done when: You can mutate Copy types through shared references.
Learn: Runtime borrow checking with RefCell — borrow() / borrow_mut()
Exercise: Build a Schedule that pushes appointments through &self.
You're done when: You can mutate non-Copy types through shared references.
Learn: Shared + mutable — the Rc<RefCell<T>> pattern
Exercise: Two stations share and mutate one appointment book.
You're done when: You can share mutable state between multiple owners.
Learn: impl Iterator for MyType — the next() method
Exercise: Implement Iterator for WalkInQueue — yields waiting clients one by one.
You're done when: You can for client in queue over your custom type.
Learn: .filter() and .map() work on any Iterator — including yours
Exercise: Filter WalkInQueue by service type, map to prices.
You're done when: You can chain standard adaptors on your custom iterator.
Learn: csv::Reader + #[derive(Deserialize)] — parse CSV rows into structs
Exercise: Write load_price_list(path) that reads a name,price CSV into a Vec<Service>.
You're done when: You can turn a CSV file into typed structs — no splitn/parse by hand.
Learn: csv::Writer + #[derive(Serialize)] — write structs as CSV rows
Exercise: Write save_daily_log(path, entries) — serialize Service rows to a CSV (the header is written automatically).
You're done when: You can persist typed structs as a CSV without building strings by hand.
Learn: #[derive(Serialize, Deserialize)] — automatic JSON conversion
Exercise: Serialize and deserialize a ServiceEntry struct as JSON.
You're done when: You can round-trip Rust structs through JSON.
Learn: async fn, .await, #[tokio::main]
Exercise: Write check_availability(slot) — your first async function.
You're done when: You can write and await async functions.
Learn: Spawning concurrent tasks
Exercise: Two clients booking concurrently — spawn tasks, collect results.
You're done when: You can run independent work concurrently with tokio::spawn.
Learn: tokio::sync::mpsc — message passing between tasks
Exercise: Technician sends "done" on a channel, front desk receives.
You're done when: You can communicate between async tasks.
Learn: #[derive(Parser)] — structured CLI argument parsing
Exercise: Parse --technician "Mai" --service "Gel Manicure" --price 45.
You're done when: You can build type-safe CLI interfaces.
Learn: Everything combined — structs, HashMap, methods, CLI Exercise: Full salon scheduler: book, list, revenue by technician. You're done when: The salon scheduler runs end to end — book, list, and report revenue.
One continuous cyberpunk heist in five chapters. You are Chrome Surgeon, hired to breach the Aegis-9 corporate vault — Mai, Linh, and Trang ride along as your crew. Same Rust concepts as before, one story. ⚡ = warmup (c01–c54 tools only). ★ = BUG HUNT: the exercise ships broken code that compiles — you diagnose and fix it.
Chapter 1 — LOADOUT (c55–c58): crack the keygen, plot the route, slot your programs, debug your chrome.
Learn: Iterative computation with a rolling pair — no recursion needed
Exercise: access_code(n) — the vault's Fibonacci-derived rolling code — plus keystream(len).
You're done when: You can turn a recurrence into a loop with (a, b) = (b, a + b).
Learn: Why a self-referential enum needs Box (it would have infinite size otherwise)
Exercise: Build the Route cons-list: build_route, hop_count, and last_node — the vault's doorstep.
You're done when: You understand that Box gives a recursive type a fixed, known size.
Learn: Box<dyn Trait>, dynamic dispatch, heterogeneous collections
Exercise: Implement Program for Icepick/Siphon/Ghost, rack all three in one Vec<Box<dyn Program>>, total the power draw.
You're done when: You can store different concrete types together and call shared methods at runtime.
Learn: impl Deref, the * operator, and auto-deref coercion
Exercise: BUG HUNT — every implant reads factory-fresh even though the firmware is installed. Find which slot Deref serves.
You're done when: boost(&implant) reaches the installed firmware through auto-deref — the deck reads 42.
Chapter 2 — GHOST PROTOCOL (c59–c61): every connection burns its trace, and nothing follows you home.
Learn: One-pass hashing — trade space for time
Exercise: master_key_pair(codes, master) — indices of the two intercepted codes that sum to the master key.
You're done when: You can find a complement in O(n) instead of a nested loop.
Learn: impl Drop, deterministic cleanup, reverse (LIFO) drop order
Exercise: Make every Uplink push "<handle> trace burned" on drop; burn a compromised one early with drop().
You're done when: Cleanup runs automatically when a value leaves scope — no GC, no forgotten close.
Learn: Rc::downgrade, Weak::upgrade, why cycles leak and how Weak breaks them
Exercise: BUG HUNT — the trace daemon's strong Rc grip keeps your session alive after jack-out. Sever it.
You're done when: link_counts() is (1, 1) and jacking out makes the daemon's upgrade() return None.
Chapter 3 — INSIDE THE ICE (c62–c64): interior mutability under fire.
Learn: In-place mutation with two pointers
Exercise: reverse_packet — flip the buffer by swapping i with n-1-i, no allocation.
You're done when: You can mutate a collection in place without allocating a new one.
Learn: Cell::replace and Cell::take — mutate Copy data through &self
Exercise: reload (swap the charge cell, return the spent one) and discharge (take it all, leave 0).
You're done when: You can swap interior state through a shared reference and recover the old value.
Learn: Runtime borrow checking — borrow_mut panics, try_borrow_mut returns Err
Exercise: BUG HUNT — a write during a sweep panics the whole deck. Refuse the contended write gracefully.
You're done when: Contended writes come back Err, clean handoffs succeed, nothing panics.
Chapter 4 — THE CREW (c65–c68): threads, locks, and a stash that had better add up.
Learn: HashSet membership — a HashMap with keys only
Exercise: signature_reused — a replayed access signature trips the alarm; detect it in one pass.
You're done when: You can use HashSet::insert's return value to detect duplicates.
Learn: Arc, thread::spawn, join — sharing immutable data across threads
Exercise: crew_estimates — three crew threads each tally one shared vault map.
You're done when: You understand why threads need Arc (it's Send) and Rc won't compile.
Learn: Mutex for thread-safe mutation — the threaded sibling of Rc<RefCell<T>>
Exercise: BUG HUNT — ten runners deposit 100 creds each; the stash reads 0. The compiler is waving a warning at the culprit.
You're done when: pool_the_take() reads 1000 every run — deposits go through the lock, not into a copy.
Learn: RwLock — many concurrent readers OR one exclusive writer
Exercise: alert_board_count — one spotter writes, three lookouts read concurrently.
You're done when: You can share data across threads with read/write locks — the crew is in position.
Chapter 5 — THE VAULT (c69–c74): the datavault — persistent storage for the haul, and the finale that assembles your own code.
Learn: sled::open + db.insert — a pure-Rust embedded key-value database, no SQL
Exercise: stash(db, key, shard) — every ripped shard goes straight to disk.
You're done when: You can open a sled database and write entries that survive a dropped uplink.
Learn: db.get → Option<IVec> — reading values back by key
Exercise: retrieve(db, key) — pull a shard back out; burned intel is None, not a crash.
You're done when: You can read values out of the store and handle the missing-key case.
Learn: store whole structs — serde_json::to_vec / from_slice (c49), errors unified with anyhow (c38)
Exercise: encode_intel / decode_intel — round-trip Intel { codename, value } through the vault.
You're done when: Structs persist and reload intact. (The c74 finale imports THIS file — build it well.)
Learn: db.iter() — scanning every entry in the store
Exercise: full_scan(db) — enumerate the entire haul, sorted by codename.
You're done when: You can walk the whole database and decode each record.
Learn: "querying" without SQL — filter + map over the records (c21-22 / c45-46)
Exercise: shortlist(db, min_value) and shortlist_codenames — only intel the fence will move.
You're done when: You can answer questions about the data with iterator combinators.
Learn: integration — read a CSV (c47), store it via YOUR c71 codec (imported with #[path]), aggregate the haul
Exercise: import_dump(db, csv_path) totals the haul; crown_jewel(db) finds the most valuable record.
You're done when: The haul totals 124500, GHOSTKEY surfaces, and the run prints SYSTEM FULLY COMPROMISED.
Side jobs back at the salon: the shop's back-office code is broken, and you debug it. Every ★ exercise COMPILES but fails its tests — read the // BUG: symptom at the top of the file, run the tests, diagnose, fix. No new concepts (everything is c01–c54 material); the example file holds the corrected reference.
Learn: Why counting needs entry(k).or_insert(0) — insert overwrites instead of accumulating
Exercise: Mai's per-technician count is stuck at 1; fix service_counts so tallies accumulate.
You're done when: Mai reads 6, Linh 2, Trang 1, and the empty day yields an empty map.
Learn: Don't swallow parse errors — a bad row must propagate as Err, not become 0
Exercise: A corrupt price silently undercounts the day; make daily_total report it.
You're done when: A clean list sums correctly and a corrupt row returns Err.
Learn: filter keeps the items whose predicate is true — direction matters
Exercise: The tip jar totals the Regulars instead of the VIPs; fix the filter.
You're done when: vip_tip_total sums only VIP tips (3000) and 0 when there are none.
Learn: The last window starts at n - width, so the loop must be inclusive (..=)
Exercise: The busiest-3-hour report always misses the closing rush; fix the bound.
You're done when: The busiest window is found even when it's the last one.
Learn: A borrow() guard blocks borrow_mut() until it's dropped — scope it tightly
Exercise: Booking a new walk-in panics with BorrowMutError; release the read borrow first.
You're done when: add_if_absent adds new names, rejects duplicates, and never panics.
Learn: while let Some(msg) = rx.recv().await drains a channel; one if let reads only one
Exercise: Three techs clock out but only Mai is logged; drain the whole channel.
You're done when: collect_done returns all three "done" messages in order.
The lessons above cover the core language, practical application, and debugging reps. Next topics:
- Procedural and derive macros
- Advanced async (
select!,RwLock, cancellation) - Web server with
axumoractix-web - Relational/SQL databases with
sqlxorSeaORM(you've done embedded key-value withsled) - Building the salon scheduler as a web API