Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Your First Rust Program

Goal: you can read the gate lab line by line and explain every construct in it — including why one deliberate line panics.

The whole program

labs/gate-hello/src/main.rs (short version):

use std::env;

fn main() {
    let args: Vec<String> = env::args().skip(1).collect();

    match args.first().map(String::as_str) {
        Some("--add") => {
            let a: i64 = args.get(1).expect("usage: gate-hello --add <A> <B>")
                .parse().expect("first operand must be an integer");
            let b: i64 = args.get(2).expect("usage: gate-hello --add <A> <B>")
                .parse().expect("second operand must be an integer");
            println!("{a} + {b} = {}", a + b);
        }
        Some("--crash") => {
            let missing: Option<i64> = env::var("DELIBERATELY_MISSING")
                .ok()
                .and_then(|s| s.parse().ok());
            println!("{}", missing.expect("set DELIBERATELY_MISSING=<number> to avoid this panic"));
        }
        _ => {
            println!("Hello from Rust {} — toolchain works!", env!("CARGO_PKG_VERSION"));
        }
    }
}

What each piece teaches

LineConceptNotes
use std::env;importsbring the env module into scope
env::args().skip(1).collect()iteratorsargs() yields Strings; skip(1) drops the binary name
args.first().map(String::as_str)Optionempty args → None, no out-of-bounds possible
match ... { Some("--add") => ..., _ => ... }matchexhaustive by design — forget a case and it will not compile
let a: i64 = ... .parse().expect(...)turbofish-free inferenceparse::<i64>() via the type annotation; parse returns Result
.expect("...")fail fastfine in labs/CLIs; never in library or service code — that is M2’s topic
env!("CARGO_PKG_VERSION")compile-time envbaked in by cargo from Cargo.toml
{a} + {b} in the stringinline formattingformat captures, stable since 1.58

Panic vs error — the distinction that shapes everything

Run the two failure modes side by side:

cargo run -- --crash                    # panics: process dies, rc=101
DELIBERATELY_MISSING=7 cargo run -- --crash   # value existed: rc=0
  • A panic is for programmer bugs — impossible states, broken invariants. Fail fast, loud, with a message.
  • An error value (Result) is for expected failure — bad input, missing file, unreachable network. The type system forces the caller to handle it.

Why is --crash a panic then? Because it simulates a broken invariant for teaching. In real code, a CLI reading user input should return ExitCode errors (M1) and a library should return Result (M2). Our Axum services never unwrap() user input — that discipline starts here.

Tests live next to the code

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    #[test]
    fn binary_has_a_version() {
        assert!(!env!("CARGO_PKG_VERSION").is_empty());
    }
}
}
  • #[cfg(test)] — compiled only for cargo test, zero cost in release builds.
  • #[test] — the runner discovers and runs it; a panic inside = failure.
  • assert! / assert_eq! — the workhorses. No separate test framework needed.

Exercises

  1. Trace it — add dbg!(&args); as the first line of main, run all three modes, and write down what dbg! prints (file, line, expression, value).
  2. Extend — add a --echo branch that prints all remaining args joined by spaces.
  3. Feel the compiler — remove the _ => arm. Try to compile. Read the error. Put it back. That error is match exhaustiveness — you will learn to love it.

Done when: all three modes work, the removed-_-arm compile error is something you can explain in one sentence, and the house loop is green.