Workshop · Rust · Beginner
RS050 Rust from zero
Rust from the first println! to a tested command-line program, in forty short steps across eight sessions. Every step is a small, honest program you build in your own VS Code seat with the AI tutor beside you: cargo new, a compiler error read back to its line, numbers and money held in the right type, loops and match, vectors and hash maps, functions with tests, files that may be missing, and the Tally capstone, an expense tracker the machine checks on inputs you did not see.
Open Coding Tutor at codingtutor.agilesystemsoftware.com, pick RS050 from the course button in the chat and press Tutor me. Every lesson, demo and hands-on step is also published below.
Who it is for
Adults and teens who have never programmed and want to start with a language that tells the truth about what the computer is doing. You can type and follow a recipe. Rust's compiler is strict, and this course treats that strictness as a tutor, not an obstacle. No maths beyond percentages.
Where it leads. JV100 Java I · DN100 C# I: the working programmer’s toolkit · PY100 Python I · RS100 Rust I
What you will be able to do
- Create a cargo project, run it, and read a compiler error back to the line and the arrow it points at
- Hold text, whole numbers, decimals and money in the right type, and explain why 0.1 + 0.2 is not 0.3 and what as truncates
- Format a column of numbers with width and precision so a human can read it
- Choose with if/else and match, and repeat with for, while and loop without an off-by-one
- Keep things in a Vec and a HashMap, walk them, sort them and count them
- Write a function with typed inputs and a return value, and prove it with #[test] and cargo test
- Read and write text files with Result and ?, and survive a missing file without a panic
- Build a small command-line program with arguments and an exit-code contract
Syllabus
Every session ends in a lab the machine checks on your own code. Open a session's mini steps to read each lesson, demo and hands-on task; the badge shows how far the engine can verify it: checked · T3 a check runs over your files, proven · T4 the grader also tries inputs you did not see, tutored · T0 done on your word. Each step has its own link (for example #s1-1).
- 1. Your first programs
- 2. Data: text, numbers and money
- 3. Computing and formatting
- 4. Choices and repetition
- 5. Vectors and hash maps
- 6. Functions and tests
- 7. Files and errors
- 8. Capstone: Tally
Session 1 · Your first programs
Five steps that make the seat yours. What a Rust program is: a cargo project with one file, src/main.rs, that the compiler turns into a program before anything runs. The rhythm of the course: run, read, change, run again. Printing text and numbers with println! and the {} hole. Reading a compiler error back to the line and the little arrow it points at, instead of guessing. Live in the seat: cargo new and the files it makes, a deliberate typo and its message, three ways to break println! and what each one says, and the first saved program with a comment on top.
Lab Write greet in rust/rs050/s1: it prints a three-line banner, a sentence that uses a number computed from 2 + 3, and a line built from two pieces of text joined together. It must build clean on the first try after you have read your own code once. Session 1 checkpoint.
5 mini steps · ~39 min
1.1cargo new, cargo run8 minchecked · T3
Predict
src/main.rs contains fn main() { println!("Hello"); } What appears when you run cargo run?
Lesson
A Rust program is a cargo project: Cargo.toml describes it and src/main.rs is the code. cargo run compiles the code into a real program and then runs it. You learn in one rhythm: run, read what came out, change one thing, run again.
Demo
The tutor runs cargo new s1 inside rust/rs050/demo, opens the two files it made, runs cargo run, points at the Compiling and Running lines and then at Hello, world!, changes the text and runs again.
On screen
Terminal → cd rust/rs050/demo → cargo new s1 → Explorer: Cargo.toml, src/main.rs → cargo run → edit the string → cargo run.
Hands-on
Create rust/rs050/s1 with cargo new s1 inside rust/rs050, open src/main.rs, change the println! to your own words (not Hello, world!), run it, then add a second println! and run again.
Explain
In one sentence: what are the two things cargo run did?
Check
s1 prints two lines of your own — checked (T3):
seat_fileseat_run1.2Text and numbers are different things7 minchecked · T3
Predict
println!("{}", 2 + 3) prints what, and what does println!("{}", "2" + "3") do?
Lesson
Rust keeps text (&str, String) and numbers (i32, f64) apart and refuses to mix them by accident. {} is a hole in the text that a value fills. The compiler checks every value's type before the program can run.
Demo
The tutor prints 2 + 3, 7 / 2, 7 % 2 and 7.0 / 2.0 in rust/rs050/demo/s1/src/main.rs, then tries "2" + "3", reads the error, and shows format!("{}{}", "2", "3") as the way to join text.
Hands-on
In rust/rs050/s1/src/main.rs print four lines: 40 + 2, 7 / 2, 7 % 2 and 7.0 / 2.0, each with println!("{}", ...). Compare with what you predicted.
Explain
Why did 7 / 2 not print 3.5?
Check
Four arithmetic lines: 42, 3, 1, 3.5 — checked (T3):
seat_run1.3Reading a compiler error8 minchecked · T3
Predict
Line 3 is println!("Hi") with no semicolon and line 4 is println!("There"); Where does the error point?
Lesson
A Rust compiler error is a map: the first line names the kind of error, the next names file:line:column, and an arrow marks the exact spot. Go there first. Many messages also end with a help line that is usually right.
Demo
The tutor breaks the demo three ways in turn: a missing ;, a misspelt printl!, and a number added to text. Runs each, reads the message top to bottom, and points at the file:line:column and the help line.
Hands-on
Copy rust/rs050/samples/s1/oops.rs over rust/rs050/s1/src/main.rs. It has three errors. Fix them one at a time by reading the message, until it prints "Total: 12" and "Done".
Explain
Which part of the error do you read first, and why?
Check
oops.rs fixed: builds and prints Total: 12 and Done — checked (T3):
seat_buildseat_run1.4let, comments and the shape of a file7 minchecked · T3
Predict
let price = 5; price = 7; println!("{}", price); What happens?
Lesson
let gives a value a name. By default the name cannot be changed; let mut says it can. A comment starts with // and is for people. A tidy file has a comment on top, names that say what they hold, and one idea per line.
Demo
The tutor writes rust/rs050/demo/s1/src/main.rs with a top comment, let item = "Notebook", let qty = 3, let mut unit_price = 2.5, a println! using all three, then changes unit_price and shows why mut was needed.
Hands-on
Write rust/rs050/s1/src/main.rs with a // header comment and bindings item (text), qty (whole number) and unit_price (decimal); print one line like "3 x Notebook at 2.5" built from them.
Explain
Why does Rust make you write mut to change a value?
Check
Three named bindings and a comment — checked (T3):
seat_fileseat_run1.5Lab: the greeting banner9 minchecked · T3
Predict
println!("{}{} {}", "Ru", "st", 3) prints what?
Lesson
Everything from this session in one program: println!, text joined with format! or adjacent holes, a number computed in the text, a header comment, and the discipline of reading your own file once before you build it.
Demo
The tutor builds rust/rs050/demo/s1/src/main.rs live: three banner lines of = and a title, a sentence with 2 + 3, a line from format!("{}{}", "Agile ", "System Software"), runs it, then shows the lab check so the learner knows what the grader reads.
Hands-on
In rust/rs050/s1/src/main.rs: a // comment on line 1; a banner of exactly three lines (= signs, a title, = signs); one sentence that includes the number 5 computed as 2 + 3; and one line made by joining two text pieces with format!. Build clean first try.
Explain
Name one thing you now check in a file before you build it.
Check
greet: three-line banner, a computed 5, a joined line — checked (T3):
seat_fileseat_run
Session 2 · Data: text, numbers and money
Five steps on what a value is and how it is held. Whole numbers of different sizes, i32 and i64, and what overflow means. Decimals as f64 and the one fact that prevents a hundred bugs: 0.1 + 0.2 is not 0.3 in binary floating point. Text as &str and String, and why there are two. Converting between types with as, and what is silently lost when a decimal is cut to a whole number or a big number is squeezed into a small type. Money held as integer cents, with the rust_decimal crate as the grown-up option. Live in the seat: 0.1 + 0.2 printed to 20 places, 300 as u8, 12345.67 as i32, a String grown with push_str, and a receipt whose total is exact.
Lab The receipt: three items with quantities and prices in integer cents; print each line with the amount in pounds or dollars with two decimals, then the total, and prove the total is exact by printing it as cents too. Session 2 checkpoint.
5 mini steps · ~42 min
2.1Whole numbers: i32, i64 and overflow8 minchecked · T3
Predict
let n: u8 = 255; let m = n + 1; What happens in a debug build?
Lesson
Integers have sizes: i32 holds about ±2 billion, i64 far more, u8 only 0 to 255. Pick the size for the biggest value you will ever hold. Rust panics on overflow in debug builds instead of quietly wrapping around.
Demo
The tutor prints i32::MAX and i64::MAX in rust/rs050/demo/s2/src/main.rs, then writes let n: u8 = 255; and adds 1, runs, and reads the panic message with its line number.
Hands-on
In rust/rs050/s2/src/main.rs print i32::MAX, i64::MAX and u8::MAX on three lines. Then compute 200 as i32 + 100 and print it; note why it works as i32 but would not as u8.
Explain
Why does Rust panic on overflow instead of wrapping?
Check
The three MAX values and 300 — checked (T3):
seat_run2.2Decimals: f64 and the 0.1 + 0.2 problem8 minchecked · T3
Predict
println!("{}", 0.1 + 0.2 == 0.3) prints what?
Lesson
f64 is a binary fraction with about 16 good digits. It cannot hold 0.1 exactly, so sums drift by a hair. That is fine for measurements and wrong for money, which is why the next steps hold money as whole cents.
Demo
The tutor prints 0.1 + 0.2 with {:.20}, then 0.1 + 0.2 == 0.3, then adds 0.1 a thousand times in a loop and prints the result next to 100.0, all in rust/rs050/demo/s2/src/main.rs.
Hands-on
In rust/rs050/s2/src/main.rs print 0.1 + 0.2 with {:.20}, then print 0.1 + 0.2 == 0.3, then print 10.0_f64 / 3.0 with {:.3}. Read the three lines back.
Explain
Why is f64 the wrong type for a bank balance?
Check
0.1 + 0.2 to 20 places, false, 3.333 — checked (T3):
seat_run2.3Text: &str and String8 minchecked · T3
Predict
let mut s = String::from("Tally"); s.push_str(" v1"); println!("{}", s.len()); What prints?
Lesson
A text literal in quotes is a &str: fixed text the program carries. A String is text you can grow and own. Make a String with String::from or .to_string(), grow it with push_str, measure it with len(), and change its case with to_uppercase().
Demo
The tutor builds a String from "Receipt", pushes " for Pixel", prints it, its len(), its to_uppercase(), and then tries to push_str onto a &str literal to show the error that explains the difference.
Hands-on
In rust/rs050/s2/src/main.rs make a String from "Receipt", push_str " #1042", then print it, its length, and its uppercase form on three lines.
Explain
When do you need a String rather than a &str?
Check
Receipt #1042, 13, RECEIPT #1042 — checked (T3):
seat_fileseat_run2.4Converting with as: what survives9 minchecked · T3
Predict
let gross = 12345.67_f64; println!("{}", gross as i32); What prints?
Lesson
as changes a value's shape and the shape decides what survives: a decimal cut to an integer loses its fraction, a big number squeezed into a small type loses its high bits. Both are silent. Use round() first when you mean rounding, and hold money as i64 cents so nothing is lost.
Demo
The tutor prints 12345.67 as i32, (12345.67_f64).round() as i32, 300 as u8 and -1 as u8, reads each result, then shows the money move: 1234567_i64 cents printed as 12345.67 with / 100 and % 100.
Hands-on
In rust/rs050/s2/src/main.rs print four lines: 12345.67 as i32, 12345.67_f64.round() as i32, 300_i32 as u8, and the cents value 1234567_i64 printed as "12345.67" using / 100 and % 100 with {:02}.
Explain
Why is the lost fraction in as i32 silent?
Check
12345, 12346, 44, 12345.67 — checked (T3):
seat_run2.5Lab: the receipt in cents9 minchecked · T3
Predict
3 notebooks at 250 cents and 2 pens at 199 cents. Total in cents?
Lesson
Money as i64 cents: multiply quantity by unit cents, add the lines, and only when printing divide by 100 for the units and take % 100 for the cents, padded to two digits. The total is exact because no f64 was involved.
Demo
The tutor writes a receipt in rust/rs050/demo/s2/src/main.rs with three items as (name, qty, cents) bindings, a fn-free total, and a println! with {}.{:02} for each line, then prints the raw total in cents as proof.
Hands-on
In rust/rs050/s2/src/main.rs: three items — Notebook 3 at 250, Pen 2 at 199, Folder 1 at 1299 — print a line per item "Notebook x3 7.50", the total "Total 24.47" and the proof line "Total cents 2447". Hold every amount as i64 cents.
Explain
Why print the total in cents as well?
Check
Receipt lines, Total 24.47, Total cents 2447 — checked (T3):
seat_fileseat_run
Session 3 · Computing and formatting
Five steps on arithmetic you can trust and output a human can read. Operators and precedence, integer division and remainder, and why a percentage is computed in cents times a whole number. Rounding on purpose with round() and the banker's question of half-way cases. Formatting with width, alignment and precision: {:>8}, {:<10}, {:.2}, and a header line that lines up with the numbers below it. Reading a number from the command line and turning text into a number with parse. Live in the seat: 7 / 2 against 7.0 / 2.0, a tax that comes out a cent wrong with f64 and right with cents, a right-aligned column of five amounts, and a tip calculator driven by two arguments.
Lab The tip calculator: cargo run -- <bill cents> <tip percent> prints the bill, the tip and the total, each right-aligned with two decimals, with the tip rounded to the nearest cent. Session 3 checkpoint.
5 mini steps · ~40 min
3.1Operators, precedence and remainder7 minchecked · T3
Predict
println!("{}", 2 + 3 * 4 % 5) prints what?
Lesson
Rust follows school precedence: * / % before + -, left to right, brackets first. Integer / drops the remainder and % gives it back. 17 / 5 is 3 and 17 % 5 is 2, which is how seconds become minutes and cents become units.
Demo
The tutor prints 2 + 3 * 4, (2 + 3) * 4, 17 / 5, 17 % 5 and then converts 3725 seconds into 1:02:05 with / and % in rust/rs050/demo/s3/src/main.rs.
Hands-on
In rust/rs050/s3/src/main.rs convert 3725 seconds into hours, minutes and seconds with / and % and print "1:02:05" using {:02} for the minutes and seconds. Then print 100 % 7.
Explain
How did % help you find the minutes?
Check
3725 seconds is 1:02:05; 100 % 7 is 2 — checked (T3):
seat_fileseat_run3.2Rounding on purpose8 minchecked · T3
Predict
A tip of 15% on 1999 cents is 299.85 cents. Which cents value should the receipt show?
Lesson
A percentage of cents is rarely whole. Compute it as f64 only for the one multiplication, round() to the nearest cent, and go straight back to i64. Decide the rounding rule once and write it down; the receipt must match the bank.
Demo
The tutor computes 1999 × 15 / 100 three ways in rust/rs050/demo/s3/src/main.rs: integer division (299), f64 then as (299), f64 then round() then as (300), and prints all three side by side.
Hands-on
In rust/rs050/s3/src/main.rs compute a 15% tip on 1999 cents as i64 using (1999 as f64 * 0.15).round() as i64 and print "Tip cents 300". Then do the same for 18% on 4550 and print the result.
Explain
Why round before casting back to i64?
Check
Tip cents 300 and 819 — checked (T3):
seat_fileseat_run3.3Width, alignment and precision8 minchecked · T3
Predict
println!("[{:>6}]", 42) prints what?
Lesson
Inside {} you can say how wide, which side, and how many decimals: {:<10} left-aligns text in ten columns, {:>8} right-aligns a number in eight, {:>8.2} does both for a decimal. A header printed with the same widths lines up with the rows.
Demo
The tutor prints a header "Item Amount" and three rows with {:<10}{:>8.2} in rust/rs050/demo/s3/src/main.rs, then changes one width to show the column slide out of line.
Hands-on
In rust/rs050/s3/src/main.rs print a header line "Item Cents" and three rows (Notebook 750, Pen 398, Folder 1299) with {:<12}{:>8} so the numbers right-align under Cents.
Explain
Why print the header with the same widths as the rows?
Check
A right-aligned Cents column — checked (T3):
seat_fileseat_run3.4Arguments and parse8 minproven · T4
Predict
cargo run -- 1999 15 is run. std::env::args().nth(1) is what?
Lesson
A program takes its inputs from the command line as text. std::env::args() lists them; .nth(1) is the first one you passed. parse::<i64>() turns text into a number and gives back a Result, because the text might not be a number at all.
Demo
The tutor reads two arguments in rust/rs050/demo/s3/src/main.rs, parses them with .parse::<i64>().expect("a number"), prints their sum, then runs with a word instead of a number to show the expect message.
Hands-on
In rust/rs050/s3/src/main.rs read two arguments, parse both as i64 with expect, and print "Sum <a+b>". Run cargo run -- 1999 15 and then cargo run -- 1999 abc and read what happens.
Explain
Why does parse give a Result and not just a number?
Check
Sum of two arguments — proven (T4):
seat_fileseat_run3.5Lab: the tip calculator9 minproven · T4
Predict
Bill 4550 cents, tip 18%. Which total prints with two decimals?
Lesson
Everything from this session: two arguments parsed to i64, the tip rounded to the nearest cent, and three lines — Bill, Tip, Total — each printed with a label left-aligned in eight columns and the amount right-aligned with two decimals from cents.
Demo
The tutor builds the calculator in rust/rs050/demo/s3/src/main.rs, runs cargo run -- 1999 15 and cargo run -- 4550 18, then shows a one-cent difference by swapping round() for as to prove the check would catch it.
Hands-on
In rust/rs050/s3/src/main.rs: cargo run -- <bill cents> <tip percent> prints "Bill 19.99", "Tip 3.00", "Total 22.99" with {:<8}{:>8} formatting, amounts from cents, tip rounded to the nearest cent.
Explain
Where in your program could a cent go missing, and how did you stop it?
Check
Bill, Tip and Total right-aligned and rounded — proven (T4):
seat_fileseat_run
Session 4 · Choices and repetition
Five steps on control flow. if, else if and else, and conditions built with && || and !. match as the clean way to choose among many cases, with the _ arm the compiler insists on. The three loops: loop with break, while with a condition, for over a range, and how to avoid the off-by-one that turns 1..5 into four numbers. Loops that build a result: a running total, a count of matches. Live in the seat: a grade from a score, match on a menu letter, a loop that doubles a balance until it passes a target, a times table, and a quiz that scores itself.
Lab The quiz: five fixed questions with numeric answers held in the program; the learner's answers arrive as five arguments; print each question's verdict and a final score, with match giving a comment for 0–2, 3–4 and 5 correct. Session 4 checkpoint.
5 mini steps · ~40 min
4.1if, else if, else7 minproven · T4
Predict
score is 85. if score >= 90 { "A" } else if score >= 80 { "B" } else { "C" } gives what?
Lesson
if tests a condition that must be a bool; else if adds more tests in order; else catches the rest. The first branch whose test is true runs, nothing else. In Rust an if is also an expression, so let grade = if ... else ... works.
Demo
The tutor writes let grade = if score >= 90 { "A" } else if score >= 80 { "B" } else { "C" }; in rust/rs050/demo/s4/src/main.rs, runs it with the score from an argument at 95, 85 and 40, then shows what happens when the two tests are swapped.
Hands-on
In rust/rs050/s4/src/main.rs read a score argument, compute a grade with an if expression (A at 90+, B at 80+, C at 70+, F below), and print "Score 85 grade B". Try 95, 85, 72 and 40.
Explain
Why does the order of the else if tests matter?
Check
Grade from score — proven (T4):
seat_run4.2match: many cases, none forgotten8 minproven · T4
Predict
match choice { "a" => 1, "b" => 2 } with no other arm. What does the compiler say?
Lesson
match compares a value against patterns in order and runs the first arm that fits. Patterns can be literals, ranges like 3..=4, or _ for everything else. The compiler refuses a match that misses a case, which is how match prevents a whole family of bugs.
Demo
The tutor writes a menu match in rust/rs050/demo/s4/src/main.rs: "a" => "Add", "l" => "List", "q" => "Quit", _ => "Unknown", runs it from an argument, then deletes the _ arm and reads the non-exhaustive error.
Hands-on
In rust/rs050/s4/src/main.rs read a menu letter argument and print the action with match: a → Add, l → List, r → Report, q → Quit, anything else → Unknown. Then write a second match on a number 0..=5 using ranges: 0..=2 → "Try again", 3..=4 → "Good", 5 → "Perfect".
Explain
What does the compiler guarantee about a match that builds?
Check
Menu match and range match — proven (T4):
seat_fileseat_run4.3loop, while and for8 minchecked · T3
Predict
for i in 1..5 { print!("{} ", i); } prints how many numbers?
Lesson
for walks a range or a collection; 1..5 stops before 5 and 1..=5 includes it. while repeats as long as a condition holds. loop repeats forever until break, which can even hand back a value. Pick the loop that says what you mean.
Demo
The tutor prints a times table with for in rust/rs050/demo/s4/src/main.rs, then counts how many years a balance takes to double at 7% with a while loop, then rewrites it as loop with break and reads both.
Hands-on
In rust/rs050/s4/src/main.rs print the 7 times table from 7 x 1 to 7 x 12 with for, then use while to find how many years 1000 cents takes to pass 2000 at 7% per year (balance = balance * 107 / 100), and print "Years 11".
Explain
When would you pick while over for?
Check
Times table and Years 11 — checked (T3):
seat_fileseat_run4.4Loops that build a result8 minchecked · T3
Predict
let mut total = 0; for n in 1..=4 { if n % 2 == 0 { total += n; } } What is total?
Lesson
Most loops accumulate: a let mut total starts at zero and grows, a let mut count ticks up when a condition holds. Declare the accumulator before the loop, change it inside, use it after. This shape is the heart of every report.
Demo
The tutor sums the even numbers to 100 and counts the multiples of 7 in rust/rs050/demo/s4/src/main.rs, then adds a running maximum to show three accumulators in one loop.
Hands-on
In rust/rs050/s4/src/main.rs loop over 1..=100 and compute three things: the sum of the even numbers, the count of multiples of 7, and the largest multiple of 7. Print "Even sum 2550", "Sevens 14", "Largest seven 98".
Explain
Why is the accumulator declared before the loop?
Check
Three accumulators over 1..=100 — checked (T3):
seat_run4.5Lab: the quiz that scores itself9 minproven · T4
Predict
Five answers, three right. The match on the score has arms 0..=2, 3..=4 and 5. Which comment?
Lesson
Everything from this session: the correct answers held in an array, the learner's five answers parsed from arguments, a for loop over 0..5 that compares and counts, an if per question for the verdict, and a match on the final count for a comment.
Demo
The tutor builds the quiz in rust/rs050/demo/s4/src/main.rs with let answers = [4, 9, 12, 7, 25]; (2+2, 3x3, 24/2, 10-3, 5x5), runs it with all correct, then with two wrong, and reads the verdict lines and the comment.
Hands-on
In rust/rs050/s4/src/main.rs: the questions are 2+2, 3x3, 24/2, 10-3 and 5x5 with answers [4, 9, 12, 7, 25]. Read five arguments, print "Q1 right" or "Q1 wrong" for each, then "Score 3/5" and a match comment: 0..=2 "Try again", 3..=4 "Good", 5 "Perfect".
Explain
Which loop did you use and why?
Check
Quiz verdicts, score and comment — proven (T4):
seat_fileseat_run
Session 5 · Vectors and hash maps
Five steps on holding many things. Vec: a growable list you push to, index into, and walk with for; what happens when you index past the end. Sorting a Vec and why sort_by is needed for decimals. HashMap: a key that finds a value, the entry API for counting, and the fact that it has no order. Walking a Vec of pairs to build totals per category. Live in the seat: a shopping list that grows, a panic from vec[10], a sorted list of names, a word count, and a per-category total that becomes the shape of Tally's report.
Lab The shopping list: items arrive as arguments in the form name:cents (Notebook:250 Pen:199 ...); hold them in a Vec, print them sorted by price with aligned columns, then the total and the dearest item. Session 5 checkpoint.
5 mini steps · ~41 min
5.1Vec: push, index, iterate8 minchecked · T3
Predict
let v = vec![10, 20, 30]; println!("{}", v[3]); What happens?
Lesson
A Vec is a list that grows: vec![...] makes one, push adds, len() counts, v[i] reads from 0. for item in &v walks it without taking it. Reading past the end panics on purpose; v.get(i) returns an Option when you are not sure.
Demo
The tutor builds a Vec of item names in rust/rs050/demo/s5/src/main.rs, pushes two more, prints each with its index using .iter().enumerate(), then indexes past the end and reads the panic.
Hands-on
In rust/rs050/s5/src/main.rs make a Vec<&str> of three items, push two more, print "5 items", then print each as "1. Notebook" numbered from 1 using enumerate.
Explain
Why does Rust panic on v[10] instead of returning something?
Check
Five items numbered from 1 — checked (T3):
seat_fileseat_run5.2Sorting and searching8 minchecked · T3
Predict
let mut v = vec![3, 1, 2]; v.sort(); v.reverse(); What is v?
Lesson
sort() orders integers and text; sort_by with a closure orders anything, including a Vec of pairs by their second field. contains checks membership, iter().position finds an index, and iter().max() finds the biggest. Sorting first makes reports readable.
Demo
The tutor sorts a Vec of (name, cents) pairs by cents with sort_by(|a, b| a.1.cmp(&b.1)) in rust/rs050/demo/s5/src/main.rs, then by name, then finds the dearest with iter().max_by_key.
Hands-on
In rust/rs050/s5/src/main.rs make a Vec of pairs (name, cents): ("Pen", 199), ("Folder", 1299), ("Notebook", 250). Sort by cents ascending and print one per line as "Pen 199", then print "Dearest Folder".
Explain
Why does sorting pairs need sort_by?
Check
Sorted by price, dearest found — checked (T3):
seat_fileseat_run5.3HashMap: a value by key8 minchecked · T3
Predict
A HashMap has "Pen" → 199. map.get("Ink") returns what?
Lesson
HashMap<K, V> stores a value under a key: insert puts, get asks and returns an Option, and the entry API updates or creates in one go: *map.entry(key).or_insert(0) += 1. Keys are unique and the map keeps no order.
Demo
The tutor counts the words of a sentence in rust/rs050/demo/s5/src/main.rs with entry().or_insert(0), prints the map, runs twice to show the order change, then collects the keys into a Vec and sorts them for a stable print.
Hands-on
In rust/rs050/s5/src/main.rs count the words in "the cat and the dog and the bird" with a HashMap<&str, i32>, then print the counts sorted by word as "and 2", "bird 1", "cat 1", "dog 1", "the 3".
Explain
Why sort the keys before printing a HashMap?
Check
Word counts in key order — checked (T3):
seat_fileseat_run5.4Totals per category8 minchecked · T3
Predict
Rows: (food, 500), (travel, 1200), (food, 250). What is the food total?
Lesson
A Vec of rows plus a HashMap of totals is the shape of every summary report: walk the rows, add each amount into the entry for its category, then print the map in sorted key order with aligned amounts. Tally's report is exactly this.
Demo
The tutor builds a Vec of (category, cents) rows in rust/rs050/demo/s5/src/main.rs, totals them with entry().or_insert(0), sorts the categories, and prints a two-column report with a grand total line.
Hands-on
In rust/rs050/s5/src/main.rs total these rows by category: (food, 500), (travel, 1200), (food, 250), (books, 1299), (travel, 300). Print "books 12.99", "food 7.50", "travel 15.00" sorted by name, then "Total 35.49".
Explain
Why use a HashMap for the totals instead of one variable per category?
Check
Category totals and grand total — checked (T3):
seat_run5.5Lab: the shopping list9 minproven · T4
Predict
The argument "Pen:199" is split on ":". Which part is parsed to i64?
Lesson
Everything from this session: arguments in the form name:cents split with split_once, pushed into a Vec of pairs, sorted by cents, printed in aligned columns with two decimals, then the total from a loop and the dearest from iter().max_by_key.
Demo
The tutor builds the shopping list in rust/rs050/demo/s5/src/main.rs, runs cargo run -- Notebook:250 Pen:199 Folder:1299 and reads the sorted rows, the total and the dearest line.
Hands-on
In rust/rs050/s5/src/main.rs: read any number of name:cents arguments, hold them in a Vec<(String, i64)>, sort by cents, print each as "Pen 1.99" with {:<12}{:>8.2}-style alignment from cents, then "Total 17.48" and "Dearest Folder".
Explain
What happens to your program with ten items instead of three, and why?
Check
Shopping list sorted, totalled, dearest found — proven (T4):
seat_fileseat_run
Session 6 · Functions and tests
Five steps on naming a piece of work and proving it. fn with typed inputs and a return type, and the last expression as the result. Pure functions that only compute versus functions that print, and why the pure ones are the ones you can test. The first #[test], cargo test, and what a red test tells you. Returning two values in a tuple, and a table of cases driven through one test. The budget library: src/lib.rs with the functions, src/main.rs using them. Live in the seat: tip() from session 3 as a function, a test that fails before the fix and passes after, a table of five cases in one loop, and cargo test's summary line.
Lab The budget library in rust/rs050/budget: tip, split_bill, grade and category_total in src/lib.rs, each with at least two tests in a tests module, cargo test green, and a main that uses two of them. The grader also runs hidden tests. Session 6 checkpoint.
5 mini steps · ~40 min
6.1fn: inputs, output, the last expression8 minchecked · T3
Predict
fn double(n: i64) -> i64 { n * 2; } What does the compiler say?
Lesson
fn name(input: Type) -> ReturnType { body }. The last expression without a semicolon is the result. Inputs are typed so the compiler can check every call. A function in src/lib.rs marked pub can be used by main.rs and by tests.
Demo
The tutor runs cargo new budget --lib inside rust/rs050/demo, writes pub fn tip(bill_cents: i64, percent: i64) -> i64 in src/lib.rs with the rounding from 3.2, adds src/main.rs that calls budget::tip, and shows the semicolon mistake.
Hands-on
Create rust/rs050/budget with cargo new budget --lib, add src/main.rs, and write pub fn tip(bill_cents: i64, percent: i64) -> i64 in src/lib.rs that rounds to the nearest cent. main prints "Tip 300" for tip(1999, 15).
Explain
Why does the last line of a function have no semicolon?
Check
budget::tip exists and main prints Tip 300 — checked (T3):
seat_fileseat_run6.2Pure functions and side effects7 minchecked · T3
Predict
Which is easier to test: fn tip(...) -> i64 that returns, or fn print_tip(...) that prints?
Lesson
A pure function only computes: same inputs, same answer, nothing printed, nothing changed outside. Printing is a side effect that belongs in main. Split every job into a pure part and a printing part, and the pure part becomes testable.
Demo
The tutor adds pub fn grade(score: u32) -> &'static str to the demo lib, keeps the println! in main, then shows a version that prints inside grade and explains why a test could not check it.
Hands-on
Add pub fn grade(score: u32) -> &'static str (A 90+, B 80+, C 70+, F) to src/lib.rs with no println! inside. In main print "Grade B" for grade(85) and "Grade F" for grade(40).
Explain
What makes grade() pure?
Check
grade() is pure and used by main — checked (T3):
seat_fileseat_run6.3Your first #[test]8 minchecked · T3
Predict
A test says assert_eq!(tip(1999, 15), 299); and tip rounds correctly. What does cargo test print?
Lesson
A test is a function marked #[test] inside #[cfg(test)] mod tests that calls your code and asserts the answer with assert_eq!. cargo test runs them all and names each one. A red test shows left and right: read both before touching code.
Demo
The tutor adds mod tests with tip_rounds_up and grade_boundary to the demo lib, runs cargo test, breaks tip on purpose, runs again to read the left/right lines, and fixes it.
Hands-on
Add #[cfg(test)] mod tests to src/lib.rs with tests tip_rounds_up (tip(1999, 15) == 300), tip_exact (tip(1000, 10) == 100) and grade_boundaries (grade(90) == "A", grade(89) == "B"). Run cargo test until all three pass.
Explain
What do the left and right lines of a failed assert_eq! tell you?
Check
Three named tests pass — checked (T3):
seat_test6.4Tuples and a table of cases8 minchecked · T3
Predict
split_bill(1000, 3) returns (share, remainder). What is it?
Lesson
A function can return two values in a tuple: (i64, i64). Read them with let (share, rest) = split_bill(...). A table of cases is a Vec of (inputs, expected) that one test walks with a for loop, so five cases cost five lines, not five tests.
Demo
The tutor writes pub fn split_bill(total: i64, people: i64) -> (i64, i64) in the demo lib, then a test with let cases = vec![(1000, 3, 333, 1), (1000, 4, 250, 0)] looped with assert_eq! and a message that names the case.
Hands-on
Add pub fn split_bill(total: i64, people: i64) -> (i64, i64) to src/lib.rs. Write one test split_table with at least four cases in a Vec, including (1000, 3, 333, 1) and (999, 1, 999, 0), looped with assert_eq!.
Explain
Why return the remainder instead of hiding it?
Check
split_bill with a table test — checked (T3):
seat_fileseat_test6.5Lab: the budget library9 minproven · T4
Predict
category_total(&[("food", 500), ("travel", 1200), ("food", 250)], "food") returns what?
Lesson
Everything from this session as one library: tip, grade, split_bill and a new category_total over a slice of (category, cents) rows, each pure, each with tests, and a main that uses two of them. The grader runs your tests and then its own hidden ones.
Demo
The tutor adds pub fn category_total(rows: &[(&str, i64)], cat: &str) -> i64 to the demo lib with a for loop and an if, writes two tests including the no-match case returning 0, runs cargo test, and shows the summary line with all tests ok.
Hands-on
In rust/rs050/budget/src/lib.rs add pub fn category_total(rows: &[(&str, i64)], cat: &str) -> i64 with tests category_sums and category_missing_is_zero. Every function has at least two tests; cargo test is green; main prints a tip and a grade.
Explain
Why does the grader run tests you did not write?
Check
Budget library: four pure functions, tests green, hidden tests green — proven (T4):
seat_fileseat_testcraft
Session 7 · Files and errors
Five steps on programs that remember. Writing a file with std::fs::write and reading it back with read_to_string. Walking a file line by line with lines(). Result as the honest answer to "did it work": match on Ok and Err, the ? operator that passes an error up, and main returning Result so a missing file ends the program with a message and an exit code instead of a panic. A line of comma-separated fields split and parsed into numbers. Appending to a file so a ledger grows run after run. Live in the seat: a file written and read, a missing file's error printed in plain words, a CSV of three rows summed, and a ledger that keeps its total across runs.
Lab The saved ledger: cargo run -- <category> <cents> <note> appends one line to ledger.csv in the project folder, creating the file if it is missing, then reads the whole file back and prints the entry count and the running total. Session 7 checkpoint.
5 mini steps · ~40 min
7.1Write a file, read it back7 minchecked · T3
Predict
std::fs::write("note.txt", "hi") is run twice, the second time with "bye". What does the file hold?
Lesson
std::fs::write(path, text) creates or replaces a file; std::fs::read_to_string(path) gives its text back as a String. Both return a Result because the disk can say no. For now .expect("...") turns a failure into a clear panic message.
Demo
The tutor writes "Tally notes" to rust/rs050/demo/s7/notes.txt with fs::write, reads it back, prints it with its length, then writes again to show the replacement.
Hands-on
In rust/rs050/s7/src/main.rs write the text "Receipt 1042\nTotal 24.47\n" to receipt.txt in the project folder, read it back, and print the text followed by "Bytes 26".
Explain
Why does fs::write return a Result?
Check
receipt.txt written and read back — checked (T3):
seat_runseat_file7.2Line by line7 minchecked · T3
Predict
A file has three lines and ends with a newline. text.lines().count() is what?
Lesson
text.lines() walks a String one line at a time without the line endings. Combine it with enumerate for line numbers and with trim() to ignore stray spaces. Skipping empty lines early saves a parse error later.
Demo
The tutor reads rust/rs050/demo/s7/samples/names.txt, prints each line numbered from 1, then counts lines and skips the blank one in the middle with a continue.
Hands-on
Copy rust/rs050/samples/s7/names.txt into rust/rs050/s7/. Read it, print each non-empty line as "1: Pixel", and finish with "Lines 4" (the file has one blank line among five).
Explain
Why trim() each line before using it?
Check
Numbered non-empty lines and Lines 4 — checked (T3):
seat_fileseat_run7.3Result, ? and the missing file9 minproven · T4
Predict
main reads a file with ? and the file is missing. main returns Result<(), std::io::Error>. What happens?
Lesson
Result<T, E> is Ok(value) or Err(error). match handles both; ? says "if this is Err, return it to my caller now". When main returns Result, a returned Err becomes a one-line message and exit code 1. For your own exit codes, print to stderr with eprintln! and call std::process::exit(2).
Demo
The tutor reads a missing file with expect (panic), then with match printing a friendly message and exit(2), then with ? and main -> Result, running each and showing the exit code with echo $?.
Hands-on
In rust/rs050/s7/src/main.rs read the file named by the first argument. On Err print "Error: cannot read <name>" with eprintln! and exit with code 2; on Ok print "Lines <n>". Try it with names.txt and with missing.txt.
Explain
What is the difference between a panic and an exit code?
Check
Missing file ends with code 2, not a panic — proven (T4):
seat_fileseat_run7.4Comma-separated rows and appending8 minchecked · T3
Predict
"food,500,lunch".split(',').count() is what?
Lesson
A CSV row is fields split on commas: split(',') gives them in order, parse turns the number field into i64. To add a row, open the file with OpenOptions::new().append(true).create(true) and writeln! to it; the file closes itself when the handle goes out of scope.
Demo
The tutor reads rust/rs050/demo/s7/samples/rows.csv, splits each row, sums the cents field, then appends a new row with OpenOptions and reads the file again to show the sum grew.
Hands-on
Copy rust/rs050/samples/s7/rows.csv into rust/rs050/s7/. Read it, split each row on commas, parse the second field as i64, and print "Rows 3" and "Sum 1950". Then append the row "books,1299,rust book" and print "Sum 3249".
Explain
Who closes the file you opened with OpenOptions?
Check
Rows summed, one row appended — checked (T3):
seat_fileseat_run7.5Lab: the saved ledger9 minchecked · T3
Predict
ledger.csv does not exist yet. The ledger program is run once with food 500 lunch. What should print?
Lesson
Everything from this session: three arguments become a CSV row appended to ledger.csv (created if missing), then the file is read back, each row split and parsed, and the count and running total printed from cents. A bad cents argument exits 3.
Demo
The tutor builds the ledger in rust/rs050/demo/s7, runs it three times with different rows, shows the file growing in the editor, then passes a non-number for cents and reads the exit code 3.
Hands-on
In rust/rs050/s7/src/main.rs: cargo run -- <category> <cents> <note> appends "<category>,<cents>,<note>" to ledger.csv (create if missing), reads it all back, prints "Entries <n>" and "Total <units.cents>". A cents argument that will not parse prints "Error: cents must be a number" to stderr and exits 3.
Explain
Why read the whole file back instead of keeping a total in a variable?
Check
Ledger appends, re-reads and totals — checked (T3):
seat_fileseat_runseat_runseat_runseat_file
Session 8 · Capstone: Tally
Five steps that build Tally, a command-line expense tracker, from a specification. Reading the spec and sketching the parts before typing. A library crate with a parse_line function that turns a CSV row into an Entry or a clear error, and a totals function that sums cents per category in sorted order. A main that reads the file named on the command line, prints the category report with aligned amounts, the total and the entry count, and ends with the exit-code contract: 0 all good, 2 cannot read the file, 3 a bad line, each with a one-line message on stderr. Tests for the library and a run over the published sample file. Live in the seat: the sketch, the skeleton that builds with no logic, parse_line under test, the report over expenses.csv, and the grader's hidden files.
Lab Tally: cargo run -- <file.csv> prints the sorted category report, Total and Entries for any well-formed file, exits 2 when the file cannot be read and 3 on the first bad line, with the published output for samples/expenses.csv matched exactly and the grader's hidden files and hidden tests passing. Session 8 checkpoint and the course placement step.
5 mini steps · ~46 min
8.1Reading the spec, sketching the parts8 minartefact · T2
Predict
Tally must exit 3 on a bad line. Which part decides a line is bad?
Lesson
Before code, read the whole spec and write the parts down: a struct Entry { category, cents }, parse_line(n, line) -> Result<Entry, String>, totals(&[Entry]) -> Vec<(String, i64)> sorted by name, and a main that owns printing and exit codes. A sketch on paper is the cheapest place to find a missing piece.
Demo
The tutor reads rust/rs050/samples/s8/TALLY-SPEC.md aloud, writes the four parts as comments in a new lib crate rust/rs050/demo/tally, and shows the published expected output for expenses.csv next to the spec.
Hands-on
Read rust/rs050/samples/s8/TALLY-SPEC.md. Create rust/rs050/tally with cargo new tally --lib and add src/main.rs. In src/lib.rs write the sketch as comments: the Entry struct, the two function signatures and the three exit codes.
Explain
Which exit code goes with which failure, in your own words?
Check
The tally crate exists with the sketch — artefact (T2):
seat_fileseat_build8.2The Entry struct and parse_line under test10 minchecked · T3
Predict
parse_line(3, "2026-11-03,food,abc,coffee") should return what?
Lesson
pub struct Entry { pub category: String, pub cents: i64 } names the data. parse_line(n: usize, line: &str) -> Result<Entry, String> splits on commas, demands exactly four fields (date, category, cents, note) and a parseable cents, and returns Err("line n: reason") otherwise. Test the good row, the short row and the bad number.
Demo
The tutor writes Entry and parse_line in the demo lib, then three tests — parses_good_row, rejects_three_fields, rejects_bad_cents — runs cargo test red, fills the function, and runs it green.
Hands-on
In rust/rs050/tally/src/lib.rs write pub struct Entry and pub fn parse_line(n: usize, line: &str) -> Result<Entry, String> with the four-field and number rules. Add tests parses_good_row, rejects_three_fields and rejects_bad_cents; cargo test green.
Explain
Why does parse_line return Err instead of printing and exiting?
Check
Entry and parse_line, three tests green — checked (T3):
seat_fileseat_test8.3totals and the sorted report9 minchecked · T3
Predict
totals over food 500, travel 1200, food 250, books 1299, travel 300 returns which first pair?
Lesson
pub fn totals(entries: &[Entry]) -> Vec<(String, i64)> adds cents into a HashMap by category, collects the pairs into a Vec and sorts by name, so the report is the same on every run. Printing is {:<10}{:>10} with the amount as units.cents from i64.
Demo
The tutor writes totals in the demo lib with entry().or_insert(0), collects and sorts, adds a test over five entries, then prints the report in main for expenses.csv and lines it up against the published output.
Hands-on
Add pub fn totals(entries: &[Entry]) -> Vec<(String, i64)> to src/lib.rs with a test totals_sorted_by_name over the five sample rows. In main print each pair as "books 12.99" ({:<10}{:>10}), then "Total 35.49" and "Entries 5".
Explain
Why sort the totals by name rather than print the HashMap?
Check
totals sorted, report lines aligned — checked (T3):
seat_fileseat_testseat_run8.4The exit-code contract9 minchecked · T3
Predict
The file exists but line 4 has cents "12.50". Which exit code and where is the message printed?
Lesson
main is the only place that prints errors and ends the program. No argument or an unreadable file: eprintln! the reason, exit(2). The first bad line: eprintln! the parse_line message, exit(3). Everything good: the report on stdout, exit 0. Nothing ever panics on bad input.
Demo
The tutor wires the contract in the demo main, then runs it four ways — no argument, a missing file, samples/bad_cents.csv and expenses.csv — reading each exit code with echo $? and showing that stdout stays empty on failure.
Hands-on
Finish rust/rs050/tally/src/main.rs: usage or unreadable file → "Error: cannot read <name>" on stderr, exit 2; first bad line → "Error: line <n>: <reason>" on stderr, exit 3; otherwise the report and exit 0. Header line skipped; blank lines ignored.
Explain
Why do error messages go to stderr and not stdout?
Check
Exit 2 on no file, 3 on a bad line, 0 on good — checked (T3):
seat_fileseat_runseat_runseat_run8.5Lab: Tally proven10 minproven · T4
Predict
A hidden file has only the header line. What should Tally print and return?
Lesson
The proof step. Your tests run, then the grader's hidden tests call parse_line and totals with rows you did not write, and the grader runs the program over hidden files: a different ledger, an empty one, one with a bad line deep inside. Passing them all is what the certificate attests.
Demo
The tutor runs the demo Tally over the published sample and over a header-only file, then over a file whose line 7 is bad, reads the exit codes, and finally runs cargo test to show the library tests green before pressing Check.
Hands-on
Make rust/rs050/tally complete: all library tests green, the published output for samples/expenses.csv matched exactly, a header-only file giving Total 0.00 and Entries 0, and the exit codes honoured. Press Check and read every line the grader returns.
Explain
What would you tell a friend Tally guarantees, in two sentences?
Check
Tally: report, exit codes, hidden files and hidden tests — proven (T4):
seat_testseat_runcraft
Outcome
By the end of the course the tutor's checks will have verified: a compiled program for every hands-on step; eight session labs that each grow the last — the greeting, the receipt, the tip calculator, the quiz, the shopping list, the tested budget functions, the saved ledger; and the Tally capstone, a command-line expense tracker whose totals, category report and exit codes match the published expected values on inputs you did not see.
Certificate
Agile System Software Certificate — Rust from zero (RS050) attests that you can write, build, test and debug a small command-line Rust program that reads and writes files, with functions, collections and an exit-code contract, verified by a machine from your own seat.
To earn it:
- Pass the lab check of every session (steps 1.5, 2.5, 3.5, 4.5, 5.5, 6.5 and 7.5) on your own code.
- Pass the Tally capstone check (8.5) including the hidden input files and tests the grader supplies.
- Score 80% or better on the 20-question reading quiz.
Gates (step · tier required):
Plus a 20-question reading quiz at 80%, retake after 48 h. Certificates are issued once the seat check runner is live; until then every step records what the engine saw.
Environment
A VS Code seat in your browser with Rust 1.99, cargo, clippy and rustfmt ready and offline; nothing to install. Your work lives in rust/rs050; the tutor's demo files live apart from yours and the tutor never edits your hands-on file while you are working on it.
Open Coding Tutor at codingtutor.agilesystemsoftware.com, pick RS050 from the course button in the chat and press Tutor me. Every lesson, demo and hands-on step is also published below.