Skip to content

Reading the Rust Reference

Use the Rust Reference to answer a precise language question, not as a list of features to memorise. This final lesson connects the course's examples to the rules for names, types, expressions and behaviour, then checks where compiler evidence stops and an application contract begins.

Before you start

Complete the system-status project and revisit the earlier part checkpoints when a question exposes a gap. The example below needs only a Rust toolchain and Cargo, not a sensor or an async runtime. It uses edition 2024 and dependency-free fixture data.

By the end, you should be able to locate a governing rule, construct a small experiment, and state what the result does not establish. This is practical coverage of stable Rust with a reading map, not an exhaustive treatment of every grammar production or unsafe-code proof.

Which Rust documentation answers the question?

The Reference introduction distinguishes language rules from library and tool documentation. Use the Book for a guided explanation, the Reference for a specific rule, standard-library documentation for an API, and Cargo's documentation for package/build behaviour.

For example, match exhaustiveness is a language question. Vec::push, Mutex poisoning and Future::poll have library contracts. Feature selection and profiles belong to Cargo. Reading /proc/meminfo additionally depends on Linux. A successful build does not establish that an OS file exists or that a thermal zone identifies the CPU.

Record rustc --version, cargo --version, the package edition and any features when investigating a difference. The unversioned Reference tracks the current stable release; the Rust 1.99.0 Reference matches this lesson's verified compiler. Read edition-difference notes as well as the main rule. Rule-anchor names may change between releases, so retain the page title and a short description of the question, not just an opaque fragment link.

Rust Reference reading map

Start with the question in the first column, then return to the corresponding course examples. The linked pages are starting points, not a claim that this course demonstrates every rule on them.

Question Course lessons Official starting point
What is a literal, identifier or token? 2, 3, 25 Tokens, identifiers
Which declaration does this name refer to? 6, 10, 24 Items, namespaces, visibility
What types and representations can I choose? 3, 6, 7, 18, 26 Types, type layout
Why does an expression return this value or type? 2, 3, this lesson Expressions, coercions, never type
What does this pattern bind, and is it exhaustive? 7, 9 Patterns, match expressions
Which value owns storage, and when is it dropped? 4, 5, 18, 19 Place/value expressions, destructors
Which borrowing relationship must a caller maintain? 13, 15, 23 Lifetime elision, subtyping and variance
What does a generic or trait requirement mean? 11, 12, 14 Bounds, traits, implementations
How is a callable captured or suspended? 15, 22, 23 Closure expressions, async blocks, await
What does a concurrency API promise? 20, 21, 22 std::sync, atomic ordering, Future — library documentation
How does syntax expansion interact with names? 24 Macros by example, procedural macros
Which code is compiled in this configuration? 25 Attributes, conditional compilation, Cargo features
What must an unsafe boundary guarantee? 26 Unsafe operations, undefined behaviour, external blocks
How do I test a public API and handle external input? 17, 27, 28 Cargo tests, std::io, Linux proc documentation

Follow one question: does an alias create a distinct unit?

Suppose you want to prevent Celsius values from being passed to a millidegree function. An annotation named MilliAlias might look sufficient, but the type-alias rule says it is another name for an existing type. A tuple struct instead declares a distinct type. Predict the difference, then check both cases.

Create a new package:

cargo new reference_probe --edition 2024
cd reference_probe

Replace src/main.rs with this complete program:

type MilliAlias = i32;

#[derive(Debug, PartialEq)]
struct Milli(i32);

#[derive(Debug, PartialEq)]
struct Celsius(i32);

fn alias_identity(value: MilliAlias) -> i32 {
    value
}

fn to_celsius(value: Milli) -> f64 {
    f64::from(value.0) / 1_000.0
}

fn missing_fixture() -> ! {
    panic!("missing fixture");
}

fn require_fixture(value: Option<Milli>) -> Milli {
    match value {
        Some(value) => value,
        None => missing_fixture(),
    }
}

fn main() {
    let plain: i32 = 46_700;
    println!("alias accepts i32={}", alias_identity(plain));
    let reading = require_fixture(Some(Milli(plain)));
    println!("newtype display={:.1} C", to_celsius(reading));
    let whole_degrees = Celsius(46);
    println!("distinct Celsius={}", whole_degrees.0);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn alias_accepts_an_unwrapped_integer() {
        assert_eq!(alias_identity(46_700_i32), 46_700);
    }

    #[test]
    fn conversion_preserves_zero_and_negative_values() {
        assert_eq!(to_celsius(Milli(0)), 0.0);
        assert_eq!(to_celsius(Milli(-500)), -0.5);
    }

    #[test]
    fn present_fixture_retains_its_unit() {
        assert_eq!(require_fixture(Some(Milli(46_700))), Milli(46_700));
    }

    #[test]
    #[should_panic(expected = "missing fixture")]
    fn absent_fixture_panics_by_this_example_contract() {
        require_fixture(None);
    }
}

Run the checks and compare the output:

1
2
3
4
5
6
cargo check
cargo fmt --check
cargo test
cargo test --release
cargo run --quiet
cargo run --release --quiet
1
2
3
alias accepts i32=46700
newtype display=46.7 C
distinct Celsius=46

Four tests should pass. The last one expects a panic; it does not treat panic as a normal input-validation strategy. In the CLI projects, missing or malformed external data produces an error or unavailable reading instead.

The newtype helps prevent accidental type mixing at function calls. It does not validate a physical range, make .0 arithmetic correct, or stop code in this module from explicitly wrapping the wrong units. A production API can make fields private and expose checked constructors; that is a separate contract.

A compile failure tests the type boundary

Save the working program. Temporarily replace src/main.rs with this independent, deliberately invalid program:

struct Milli(i32);
struct Celsius(i32);

fn accept_milli(value: Milli) -> i32 {
    value.0
}

fn main() {
    let degrees = Celsius(46);
    let _ = accept_milli(degrees);
}

cargo check must reject the call: expected Milli, found Celsius (E0308 on the verified compiler). Restore the working program afterward. Replacing the call with accept_milli(Milli(degrees.0 * 1_000)) is an explicit conversion and compiles for this small fixture. That repair is not a general checked-overflow policy.

A compiler rejection supports the claim that these two types are distinct. Four runtime tests alone could not establish rejection of a program that never compiled.

Why can one match arm call a function returning !?

missing_fixture has return type !, the never type: it does not return a value to its caller. Its expression can coerce to the expected Milli type of the match, while the Some arm returns a Milli. This does not mean a missing value becomes a Milli; the panic leaves that execution path.

Do not infer that every use of ! as a type argument or local annotation is stable. This example uses it in a function return position. Nor does a diverging function have to panic: a loop that never exits also does not return, but deliberately hanging a test adds nothing here.

Namespaces offer another reading exercise. In lesson 24, a derive macro and a trait could share a spelling because macro and type resolution are separate. Tuple-struct constructors additionally occupy the value namespace. Use the namespace classification before assuming all identical spellings conflict, or that a type alias imports a tuple constructor.

A repeatable method for investigating a Rust rule

  1. State one question: “Can this function accept a Celsius struct without conversion?” is narrower than “Are newtypes safe?”
  2. Locate the language rule and any linked library contract. Record the edition and compiler.
  3. Predict success, a diagnostic, or an observable result before running the program.
  4. Isolate the behaviour in a complete small package. Keep failing source separate from working examples.
  5. Check the prediction in the appropriate way: compiler rejection, tests, output, or a separately justified runtime experiment.
  6. Write down the remaining obligation. For this example it includes unit conversion and range/overflow policy, not just the function signature.

Do not run undefined behaviour to “find out what happens.” A non-crashing raw-pointer experiment does not prove a safe wrapper sound; return to the unsafe contract and the documented validity/aliasing obligations. Likewise, timing one fixture does not establish a performance advantage, and a passing async test does not prove every shutdown schedule correct.

Final course review

Use these checkpoints to find the next concept to revisit rather than trying to remember 29 titles:

For each, change one boundary rule, run the existing tests, add a test for the new requirement, and explain whether a failure comes from language typing, a library API or the application policy. If you can locate the governing rule and name the untested assumptions, the reading map is doing its job.

Exercises and verification

  1. Pass a plain i32 to alias_identity. It succeeds because the alias is not a new type.
  2. Pass Celsius to accept_milli. It fails; the explicit * 1_000 conversion repair succeeds for the fixture 46.
  3. Change the present fixture to zero and -500. Conversion produces 0.0 and -0.5, not missing data.
  4. Change the panic text without changing #[should_panic(expected = ...)]. The test should fail even though the program still compiles. Restore the text.
  5. Find the rules for an alias to a tuple struct and its constructor. Do not assume type Alias = Milli makes Alias(0) a valid constructor call.

On October 10, 2026, the article's complete programs were checked on the authorised Raspberry Pi 4B, with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust and Cargo 1.99.0, and edition 2024. Formatting, four tests in debug/release, exact outputs and five further output comparisons passed. The unit-conversion repair compiled; passing the other newtype and calling an alias as a constructor failed with E0308 and E0423. Changing the panic message compiled but failed its intended test. These examples establish selected rules, not a complete language conformance suite. They do not access hardware or measure performance.

After the language course

The course overview separates the C++ and design comparisons from these foundations. Begin with values, ownership and RAII, then follow the available advanced lessons. Study matched requirements, not name similarity or mechanical class translation. The advanced series is not required to complete this language course.

Previous: system-status project · Next: Rust and C++ ownership · Course overview

Donate