Rust learning: lessons, notes, and exercise crates

This commit is contained in:
2026-09-06 23:59:08 +07:00
commit 6ddf41aa81
119 changed files with 10873 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
# Notes
- User has 2 months of prior exercises in this dir (ch1–9 of the Rust Book: variables, functions, control flow, ownership, slices, structs, enums, modules, collections, error handling) but reports forgetting all of it. Treat ch1–9 as "exposure, not mastery" until a lesson or diagnostic proves otherwise.
- Goal: backend/CLI job. Bias future lessons toward `Result`/error handling, traits/generics, and eventually a backend framework (axum/tokio) over embedded topics.
- Preference confirmed 2026-08-28: diagnostic-first — find actual gaps before building recap lessons, rather than re-teaching everything blind.
- `get-dependecies/` has an unused `trpl = "0.2.0"` dependency (the async-book helper crate) — signals prior intent to reach ch17 (async), not yet attempted.
- **Lesson format rule (2026-08-28):** typing-first. User forgot how to *write* code, not just what it means — every lesson needs hands on keyboard in a real cargo project, `cargo run`/`cargo test` as the feedback loop. See LR-0001.
- User asked to cover topics the diagnostic marked "solid" too. Do not skip them — fold them in as supporting material in typing lessons rather than dedicating lessons to them.
- Do not let the user copy-paste lesson code. State the no-paste rule explicitly in each lesson.
- Always link `reference/rust-syntax.html` from lessons; forgotten syntax was eating the working memory needed for concepts.
- Verify claimed compiler error text by actually running it before shipping a lesson (done for E0004 non-exhaustive, E0382 moved value in lesson 0002).
- Env: rustc/cargo 1.96.1 installed and working. Chrome opens lesson files via `nohup google-chrome --new-window "file://$(realpath …)" & disown` (plain `wslview` blocks the shell; `rm -rf` is blocked by policy — use `mktemp -d` for scratch projects).
- Next lesson candidates, in priority order: (1) reading compiler errors fluently, (2) Modules & Paths (0/1, and `restauran`/`learn-modules` exist as material), (3) enums + `Option`/`Result` modelling, (4) structs+impl by building something, (5) collections drill.
- **Format shift (lesson 0003):** user finished 0002, reported still "feeling like lacking", and asked for spec-driven work — a project brief with acceptance tests, NOT step-by-step syntax guidance, plus syntax examples they can consult. Self-identified weak: packages, enums, structs. Deliver specs + test suites from here; put syntax help in a *different domain* so it cannot be pasted, and collapse hints behind `<details>` so they choose the difficulty.
- Always verify a shipped test suite is passable by writing a private reference impl in a temp dir first (done for 0003: 17/17). Never ship an unproven spec.
- `cargo new <name> --lib` does NOT create `tests/` — instructions must include `mkdir tests`.
- **Two-lesson rule (2026-09-02):** for any topic the user names as weak, ship a *concept* lesson (reading, real compiler output, from zero) BEFORE the *project* lesson. 0003 shipped project-only and was unusable; 0004 backfilled it. See LR-0002.
- **Specs need prose.** Signatures alone are not a specification. Every project brief needs: what the program is for, what its data is, what its commands are — in plain language, written so the type choices (struct vs enum) fall out of the description. Then signatures.
- Diagnostic scores overstate ability: recognition-style questions pass without design ability. Self-reported weakness > quiz score. (Struct 2/2, Enum 1/2 on 0001, yet both needed teaching from zero.)
- Teaching demos: use a domain *different* from the project domain (café for 0004 vs tasks for 0003) so nothing is pasteable. Capture real `cargo run`/`cargo build` output including deliberate errors — never write error text from memory.
- **0003 result (2026-09-02):** 17/17 green, idiomatic code (closures, `iter_mut`, `?`, private field + `&[Task]` accessor). Structs/enums/packages are now *produced*, not just recognised — stop teaching them as weak. See LR-0003.
- **Untested prose is undone prose.** The spec's stderr/exit-1 requirement was the one thing the 17 integration tests could not reach, and it is the one thing that was not done (`println!` for errors, exit 0, `.unwrap()` panic on `done 9`). Either test it or make it the next lesson's drill.
- Drill format that works: operate on the crate the user already wrote, one shell-level check per step (`cargo run --quiet -- fly ; echo $?`). No new test file, and their real project improves.
- Next lesson candidates after 0005: (1) **0006 project** — `String` → `TaskError` enum with `Display`/`Error`/`From` across all four files, plus persistence to a file (`fs`, `io::Error`, real `From` conversion) — the natural project for traits; (2) collections + iterators drill (`HashMap`, `filter`/`map`/`collect`), still untouched since the 1/2 diagnostic; (3) lifetimes, only once traits are solid; (4) then axum/serde, where traits pay off.
- **Dark mode is the house style (2026-09-02, user request).** `assets/style.css` is dark by default and every lesson/reference doc gets it by linking that one file — so a new lesson needs no colour work, just `<link rel="stylesheet" href="../assets/style.css">`. Never inline colours or a per-lesson palette. Panels use `var(--panel)` / `var(--panel-hover)`; `@media print` re-declares the whole variable block as ink-on-paper so printouts stay readable.
- **Typography (2026-09-02, user request):** the old serif stack was unreadable — none of "Iowan Old Style"/Palatino/Georgia/Charter is installed on this machine, so it fell back to DejaVu Serif. Now a humanist sans stack (`Inter`, `Ubuntu Sans`, `system-ui`, …) at `1.2rem` / `1.68` line-height, measure narrowed to `40em`, code at `0.87em` in `DejaVu Sans Mono`. Only fonts actually installed here: Ubuntu Sans, DejaVu Sans, Liberation Sans, DejaVu Sans Mono, Ubuntu Mono — check with `fc-list : family` before naming a font.
- **Quiz authoring checklist (bug found 2026-09-02).** A recall question without `<button class="reveal-btn">Show answer</button>` renders dead: `initRecall` in `assets/quiz.js` bails out if reveal/answer/grade are not all present, so the Got it / Missed it buttons never appear. Four questions in 0005 shipped that way. Before shipping any lesson with a quiz, click every question in the browser — do not eyeball the HTML.
- `quiz.js` marks a graded recall with `opt-correct` / `opt-incorrect`, which the stylesheet did not define — Got it / Missed it gave no colour feedback in any lesson. Fixed in `assets/style.css`; the graded choice now turns green/red and the other button dims to 0.45 opacity.
- **0005 drill result (2026-09-03): all three checks green.** `Display for Task` correct, `unwrap` gone, `run() -> Result<(), String>`, `Command::parse(args)?` with the match on `Command` values (fixed after review), `eprintln!` + `process::exit(1)`, exit 1 on both failure paths, 17/17 still pass. The stderr/exit-1 contract missed in 0003 is now produced. Remaining nits are cosmetic only (`&args` on a `&[String]` param, `match` where `if let Err` would do). Traits/Display/`?` are produced, not just recognised — 0006 can assume them.
- Concrete material for 0006 (used): `"id not found"` is returned by both `command.rs` (no id *argument* given) and `store.rs` (id *not in the store*) — two different failures, identical prose. Their own defect, not an invented example. Enums scored 0/1 on the 0005 recall for exactly this reason.
- **0006 shipped (2026-09-03):** `TaskError` enum + `Display` + `Error::source()` + `From<ParseIntError>` across `error.rs`/`command.rs`/`store.rs`/`main.rs`, driven by a shipped `tests/errors.rs` (7 tests). Reference impl verified in a temp dir first: 17 + 7 = 24 green. Deliberately NOT in 0006: file persistence (that is 0007, and `From<io::Error>` is easier once `TaskError` exists).
- New in 0006 beyond 0005's recipe: `Error::source()`, plus the std rule that a wrapped cause goes in *either* `source()` or `Display`, never both. Cited from the std page, and the tests enforce it (`BadId` Display says "task id must be a number"; the `ParseIntError` sentence is only reachable via `source()`).
- Real compiler output captured for 0006 (never from memory): `E0004` non-exhaustive after adding a variant, `E0432` unresolved import when `pub mod error;` is missing, `E0308` leftover `String` error, `E0271` `?` with no `From` impl (the annotated-target variant of `E0277`, exactly as 0005 predicted), `E0369` missing `PartialEq` seen from the test file.
- **`reference/book-coverage.html` (new, 2026-09-03):** ch1–21 mapped to Read / Produced / Gap, chapter list taken verbatim from the book's `SUMMARY.md`. Answers "am I missing something important?" without guessing. Keep it updated after every lesson — it is now the thing that picks the next lesson. Named gaps: ch8 collections/`HashMap`, ch11 writing own tests, ch13 iterator chains, ch10.3 lifetimes, ch16/17 concurrency+async.
- Lesson order after 0006: (1) **0007** persistence — `fs`, `io::Error`, `From<io::Error>`, `FromStr` to read a task back from a line; (2) **0008** collections + iterators (`HashMap`, `filter`/`map`/`collect`, first hand-written generic fn); (3) **0009** writing your own tests (ch11) — the user has consumed 24 of my tests and written zero; (4) lifetimes as reading practice; then serde → axum → async.
- Quiz checklist ran for 0006 (2026-09-03): served the workspace over `python3 -m http.server` (Chrome/Playwright blocks `file://`), clicked all 6 questions via Playwright — 4 recalls have reveal+grade, 2 MCQs mark correct/incorrect, summary and Copy report work, only console error is a missing favicon. Do this for every lesson; it is three tool calls.
- Style additions for 0006: `blockquote` (verbatim primary-source quotes) and `.gap` (red cell in the coverage table) now live in `assets/style.css`. Both print correctly because the print block re-declares the variables.
- **0006 drill result (2026-09-03): 24/24 green** (17 spec + 7 errors), `tests/errors.rs` byte-identical to the shipped spec. `TaskError` has all 7 variants, `Display` correct, `source()` returns the `ParseIntError` only for `BadId`, `From<ParseIntError>` written. **But `From` is never exercised**: `command.rs` hand-matches `id.parse()` with `Err(e) => return Err(TaskError::BadId(e))` in both `done` and `remove`, duplicated, so the `?` conversion the lesson taught is dead code. Also unchanged from 0005: `match`/`Ok(_) => ()` in `main` instead of `if let Err`. Traits are produced; the gap is *reaching for* `?`+`From` instead of manual matching.
- **0007 shipped (2026-09-03):** files + `FromStr`. `fs::read_to_string`/`fs::write`, `io::Error` + `ErrorKind::NotFound` match guard, `From<io::Error>` (the second `From`, which answers the user's own question), `FromStr for Task` with an associated `type Err`, `env::var` with a default, and a hand-written `PartialEq` because `io::Error` is not `PartialEq`. Reference impl verified in `/tmp/ref7` first: 17 + 7 + 8 = 32 green, spec shipped as `lessons/0007-persist-spec.rs`.
- Real compiler output captured for 0007 by running it: `E0369` (derive `PartialEq` over an `io::Error` field), `E0277` (`?` with no `From<io::Error>`), `E0277` (`Task: FromStr` not satisfied), `E0046` (missing `type Err`), `E0119` (conflicting `From` impls). Never from memory.
- **Drill steps need a mechanical check, not prose (third repeat — see LR-0005).** 0006 asked in prose for the two `match id.parse()` blocks to collapse to `?`; it did not happen. 0007 makes it step 0 with `grep -c "match id.parse" src/command.rs` → `0`.
- The user asks mechanism questions after each lesson (this session: can two `From` impls exist, where does the wrapped message go, must a dev walk `source()` by hand). Answer with runnable evidence — a scratch binary in `/tmp` and its real output beats prose, and takes one tool call.
- Deliberately left un-idiomatic in 0007 so 0008 has the user's own code to rewrite: the `for` loop in `Store::load` that pushes into a `Vec` is a `collect::<Result<Vec<_>, _>>()` waiting to happen.
- **0008 shipped (2026-09-04):** iterators + `HashMap` + first generic function. Reference impl verified in `/tmp/ref8` first: 17 + 7 + 8 + 14 = **46 green**, `cargo clippy --all-targets` clean apart from one pre-existing `Default for Store` suggestion. Spec shipped as `lessons/0008-collections-spec.rs` (14 tests, 162 lines).
- What 0008 makes the user produce: `Store::load` rewritten as `collect::<Result<Vec<Task>, TaskError>>()?` (the loop 0007 left behind), `count_by_priority` on a `HashMap<Priority, usize>`, `remove_completed` via `Vec::retain`, `titles_with` as a `filter`+`map`+`collect` chain, `find` vs `position` distinguished, and `fn tally<T, K, F>(items: &[T], key: F) -> HashMap<K, usize> where K: Eq + Hash, F: Fn(&T) -> K` written from the signature up in a new `stats.rs`.
- Real compiler output captured for 0008 by running it (never from memory): `unused Map that must be used` + "iterators are lazy and do nothing unless consumed" (no consumer), `E0283` type annotations needed on a bare `collect()`, `E0277` `Priority: Eq`/`Priority: Hash` not satisfied at the `tally` call site, `E0502` an `iter_mut()` borrow held across a `self.books.len()` read, `E0507` cannot move `task.priority` out from behind `&Task`, `E0004` non-exhaustive `match` after adding `Stats`/`Clear`.
- **The `Copy` derive is the interesting one.** Keying a `HashMap` by an enum field read through `&T` forces the user to choose between `.clone()`, `#[derive(Clone, Copy)]`, and keying by `&str` — a real ownership decision with three defensible answers, not a syntax lookup. It is quiz question 4 (Ownership) for that reason.
- 0008 ends with a runnable payoff, not just green tests: two new CLI commands (`stats`, `clear`) wired through `Command`/`main`, so the `HashMap` and the `retain` are visible from the terminal. Real session captured in the lesson verbatim (`stats` → `high 1 / medium 1 / low 1`, `clear` → `cleared 1 completed`, exit 0).
- Reference doc grew three sections, all anchored and linked from 0008: `#iterators` (the one-method trait, the three ways in, adapters vs consumers, `collect` targets, `lines()` vs `split('\n')`, the four errors), `#hashmap-keys` (`Eq + Hash`, the `entry` idiom, arbitrary order, `BTreeMap` as the sorted alternative), and a generic-function block in `#traits` (monomorphisation, the `where` clause as a two-way contract, `Fn`/`FnMut`/`FnOnce`).
- Quiz checklist ran for 0008 (2026-09-04): served over `python3 -m http.server 8899`, clicked all 6 questions through Playwright — 2 MCQs mark correct/incorrect, 4 recalls reveal + grade, summary reports `6 of 6 answered, 6 correct` broken down by topic, every local link and `#anchor` resolves, only console error is the missing favicon.
- Interleaving in the 0008 quiz is deliberate: only 2 of 6 questions are about iterators. The rest revisit `HashMap` keys, the `Copy`/`clone` ownership choice, and trait bounds — spaced retrieval of 0005/0006/0007 material inside a new lesson.
- **Coverage map after 0008:** ch8 and ch13 flip Gap/Partial → Produced, ch10.1 Read → Produced. One core gap remains: **ch11, writing your own tests** — the user has now run 46 of my tests and written zero `#[test]`. That is 0009, and the map's "order that follows" section says so. Then lifetimes (ch10.3) as reading practice — 0008 had the user write one unknowingly, since `titles_with` returns `Vec<&str>` borrowed from `&self` and elision hid the annotation.
- Nothing deliberately left un-idiomatic in 0008 — 0009 supplies its own material, because the target is the tests the user writes rather than the code they refactor.
- **Lesson format rule (2026-09-04, user-reported):** *define the demo domain's data model before the first example.* 0008 shipped with `Book`, `Shelf`, `book()`, and `shelf_of()` used across ten snippets and defined nowhere — the user could not tell what fields a book had, so every example needed guessing. Fixed by adding a "The demo domain, in full" section before Part 1: both type definitions, both helpers, the literal four-book `Vec`, and an explicit note that `shelf` (lowercase) is the `Vec<Book>` while `Shelf` (capitalised) is the enum. Every future lesson with a demo domain does this first.
- Related trap the same complaint exposed: **one variable name must have one element type.** 0008 used `shelf` for both a `Vec<Book>` (closure `|b|`) and a `Vec<&str>` (closure `|t|`), because the two error examples came from a different scratch file. Fixed by re-running `/tmp/demo8/examples/{e1,e2}.rs` with a `titles` binding and re-capturing real rustc output — never hand-edit a variable name inside quoted compiler output, re-run it.
- **Layout rule (2026-09-04, user-decided): narrow prose column, small code font, and let the few verbatim blocks scroll.** 13 of 28 code blocks in 0008 were scrolling horizontally, because prose was capped at `max-width: 40em` while verbatim rustc output runs to ~105 characters. Two fixes were tried and rejected: (1) letting `pre` break out of the prose column with negative margins — the user said code hanging past the paragraph above it looks broken; (2) widening the body to `65rem` so prose and code share one edge — the user said the long measure strained their eyes to read. Final: `body { max-width: 40em }` is back, `pre` drops to `font-size: 0.73em` (0.62em in print), and the handful of blocks still too wide scroll on the wheel.
- What that buys: the column fits ~81 characters of code. **Every authored line in every lesson and in `rust-syntax.html` is now trimmed to fit it** — the pass that did it lives in the session log, but the rule going forward is simply: keep authored code and its trailing `//` comment inside 81 characters.
- What still scrolls, and why it must: verbatim rustc output only — 83 to 103 characters, `E0502`/`E0277`/`E0507`/`E0432`, the `^^^^` and `-----` spans. Those cannot be wrapped or hand-shortened, because the underlines have to stay column-aligned or they point at the wrong token. Measured at 1385px with layout settled: `rust-syntax` 4/51, 0005 3/34, 0007 2/22, 0008 4/28, and 0/N on 0001-0004 and 0006. All quizzes still click through.
- Measure `pre` overflow **after** a settle delay. A bare `iframe.onload` read reported 22/51 on `rust-syntax` where the settled read gives 4/51 — the scrollbar geometry is not final at load.
- **Keep terminal sessions short with a shell helper** (`run(){ ... }`), as 0008 does. 0007 repeated the full `TASKS_FILE=t.txt cargo run -q --manifest-path ...` on every line and was the one block still too wide; re-recorded with a `run()` helper — re-run the session, never retype it by hand.
- **Lesson format rule (2026-09-04, user-reported): a lesson must read top to bottom, and no name may appear before it is defined.** 0008 put `tally(self.tasks(), |task| task.priority)` inside the Part 5 error output, three sections before Part 6 defines `tally` — the user could not read the error, because the call site it pointed at was meaningless. I did not notice, because I wrote the lesson as a whole and read it as a whole; the user reads it in one direction, once. Captured compiler output is verbatim and must never be hand-edited (see the `shelf`/`titles` trap above), so the fix is always to introduce the name *earlier* — a signature plus a one-line gloss before the first sighting — never to alter the quote or reorder the parts. **Ship check: read the lesson in order and list every identifier at its first appearance; if it is not defined above that point, it is a bug.** This is the demo-domain rule one level up: that one covers the demo's data model, this one covers every other name, including ones the drill will define later.
- **The "specs need prose" rule (line 18) covers drill steps too — enforcement failure found 2026-09-05.** 0008 Step 3 shipped as two bare signatures (`titles_with`, `remove_completed`) plus one note about `&str`, and the user was blocked: a signature says what goes in and out, never what the function is *for* or what its edge cases are. I had the behaviour, it was in the six spec tests — order preserved, empty `Vec` not an error, ids not renumbered, counter untouched, `0` is a legal answer — and I left the user to reverse-engineer prose out of `assert_eq!`. Fixed by writing those out. **Rule going forward: every function a drill asks for gets a plain-language sentence on what it is for, plus the edge cases its tests pin down, written from the test file at authoring time. A signature is never the specification.** Step 5 of the same drill is the pattern that works — the captured terminal session specifies both new commands by example, so no prose was needed there.
- **0008 drill result (2026-09-05): 46/46 green, library produced, CLI half missed three requirements.** `tally` written from the signature, `load` as one `collect::<Result<Vec<Task>, TaskError>>()?`, `retain`, the `filter`/`map`/`collect` chain — all correct. But `stats` printed high/low/medium (`main.rs` looped `[Priority::High, Low, Medium]`), `clear` printed nothing and dropped the `usize`, and nothing tested either, because `run` lives in `src/main.rs` where no `tests/` file can import it. Third repeat of "untested prose is undone prose" (LR-0003, LR-0005), first time the cause is structural. See LR-0006.
- **0009 shipped (2026-09-05):** writing your own tests (ch11). Demo domain is `/tmp/heating` — a thermostat with a private field, a private helper, one panicking constructor and one `Result` setter, so the page can show `#[should_panic]` and a `Result`-returning test against the same type. All 28 code/output blocks captured by running them; no new spec file, because the skill is producing assertions rather than satisfying mine.
- **New grading device: `lessons/0009-mutants.sh`.** Copies the crate to a temp dir, applies one `sed` mutation, runs the user's tests, restores nothing (the copy is thrown away) — six mutations covering stats order, the zero-count line, the clear count, the `Display` line, `Status::parse`'s `in-progress` arm, and `Command::parse`'s case folding. Verified to discriminate: `0 killed, 6 survived` against only the 46 shipped tests, `6 killed, 0 survived` against the reference impl in `/tmp/ref9`. Pre-drill run on the user's crate: `0 killed, 3 survived, 3 skipped` (three mutations target `src/cli.rs`, which the drill creates). Drill's finishing condition is the report, not a test count — reuse this device for any lesson where the deliverable is tests.
- The 0009 refactor is the point, not scaffolding: `run` moves from `src/main.rs` to `src/cli.rs` and takes `out: &mut impl Write`, so `main` passes `io::stdout().lock()` and a test passes `Vec<u8>` and asserts on the bytes. That single parameter is what makes the three 0008 CLI defects testable, and it is the same move that makes an axum handler testable later. Reference impl `/tmp/ref9`: 4 unit + 6 cli + 14 + 7 + 3 + 8 + 17 = 59 green, no warnings, CLI session verified end to end (`stats` in high/medium/low order, `cleared 1 completed`, `done 9` → exit 1).
- Deliberately NOT in 0009: doc tests (`///` examples, ch14), `#[bench]`, `assert_cmd`/`predicates` for subprocess testing, `proptest`, and `cargo-mutants` (the real version of the shipped script). Named in a "then stop" section so the user knows they exist and why they are not next.
- **Test names drive the width of captured output.** Five of 28 blocks in 0009 still scroll, all of them on cargo's own 93–97 character `test result:` line, which cannot be shortened. Four more were scrolling only because my demo test names were 34–40 characters long (`a_room_below_the_target_is_heating`, `every_legal_target_survives_a_round_trip`); renaming them to ~17–30 characters and **re-running the captures** fixed those blocks. Never hand-edit the quoted output to fit — rename in the source and re-run (same rule as the `shelf`/`titles` trap, NOTES line 55).
- Quiz checklist ran for 0009 (2026-09-05): served over `python3 -m http.server 8899`, clicked all 6 questions through Playwright — 3 MCQs mark correct/incorrect, 3 recalls reveal + grade, summary reports `6 of 6 answered, 6 correct` split Tests 3/3, Traits 1/1, Collections 1/1, Modules 1/1; every local link and `#anchor` resolves (including the new `rust-syntax.html#tests`); console clean, zero errors this time (the favicon 404 is gone because the page is served, not opened from `file://`).
- Reference doc gained `#tests` (the attribute pair, the three macros and what each failure prints, `should_panic` vs a `Result` test, the unit-vs-integration access table, the `tests/common/mod.rs` spelling, the writer-injection shape, the runner flags, `E0433`/`E0603`/`E0616`) and its title/subtitle now say ch. 1–13. Coverage map: ch8, ch10.1 and ch13 flip to Produced after the 0008 drill; ch11 is "Taught — drill pending"; the "order that follows" section now names lifetimes (10.3) and patterns (19) as the remainder, then serde → axum → async.
- After the 0009 drill the book's core has no Gap left except ch10.3 lifetimes (reading practice) and ch19 patterns (one page). Next real decision is serde vs a first axum service — ask the user which, since both are now reachable.