Files
learn-rust/NOTES.md
T

25 KiB
Raw Blame History

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.