Skip to content

The Rust Engineering Handbook

Appendix A — Rust Syntax and Reading Reference

Decode Rust 2024 bindings, expressions, patterns, paths, attributes, macros, and edition-sensitive syntax from context.

What does this line do, and which punctuation carries semantic weight?

let labels: Vec<_> = records.iter()
    .filter_map(|Record { id, label }| (*id > 1).then_some(label.as_str()))
    .collect();

Read from the outside inward. let introduces a binding, labels; : Vec<_> constrains its type while leaving the element type to inference. The right side is one chained expression. The closure parameter is a destructuring pattern. Because iter() yields references, match ergonomics make id and label borrowed bindings here. *id reads the integer through its reference. The comparison produces bool; then_some converts that condition and a borrowed string view into Option<&str>; filter_map removes None; collect selects Vec<&str> from the destination constraint. The final semicolon turns the expression into an expression statement whose value is discarded, while the let declaration has already initialized labels.

That is the appendix’s method: classify the construct, find its syntactic boundary, establish whether the position expects a pattern, type, item, statement, or expression, and only then apply precedence and semantic rules. Punctuation alone is not a reading strategy.

A five-context decoder

Most unfamiliar Rust fragments become tractable after locating their context.

Context Typical introducers What may appear Primary question
item fn, struct, enum, trait, impl, mod, use, const, static, type, macro_rules! named declarations and implementations What name or implementation enters this module?
statement let, an item, or an expression followed by ; local binding, scoped item, discarded expression value What changes in this block before its next expression?
expression literals, paths, blocks, calls, operators, if, match, loops, return, break a computation with a type and value or divergence What value is produced, moved, borrowed, or discarded?
pattern left of =, match arm, if let, while let, parameter destructuring and binding rules Which shapes match, and what gets bound by value or reference?
type after :, in signatures, generic arguments, associated-type constraints nominal, reference, tuple, array, function, trait-object, opaque types Which set of values and capabilities is admitted?

Braces do not identify one context. A block is an expression; braces also delimit item bodies, struct expressions, patterns, and macro token trees. Angle brackets may delimit generic arguments, but < can be comparison. A leading 'name can be a lifetime or loop label. Context resolves the token.

Bindings, mutability, shadowing, and initialization

let pattern: Type = expression; is the full local-binding form; the type and initializer can be omitted only where the remaining program makes the declaration valid. A binding is immutable by default. mut changes whether that binding permits mutation through its owned place; it does not recursively make borrowed or shared data mutable.

let mut retries = 2;
retries += 1;
let retries = retries.to_string(); // a new binding shadows the old one

Shadowing introduces a new binding and may change type. Assignment updates an existing mutable place and must preserve its type. const defines a compile-time constant item or associated item, not a mutable storage location. static names one program-duration storage location; static mut requires unsafe access and is not a substitute for synchronization.

Destructuring is part of the binding:

let (head, tail) = bytes.split_at(4);
let Record { id, label: name } = record;

The second line binds fields by the pattern’s binding mode. Whether fields move or borrow depends on the scrutinee and pattern context; inspect the resulting types instead of treating braces as “copy fields.” Prefix ref and ref mut explicitly create reference bindings, though matching a reference often obtains them through match ergonomics. An @ binding names a whole matched value while also checking a subpattern, as in n @ 1..=9.

Expressions, statements, blocks, and control flow

Nearly every computation is an expression. Literals, calls, method calls, operators, blocks, if, match, loops, closures, async blocks, return, break, and continue all occupy expression positions. A block’s value is its final expression without a semicolon:

let limit = {
    let base = 20;
    base * 2
};

Writing base * 2; would make the block produce (). This semicolon distinction explains many type mismatches. if and match used as values require compatible arm types. return expression exits the function; break expression can supply a value to a loop. Both have the never type in the path that exits, so they can appear where another type is expected.

Evaluation order is not “whatever precedence suggests.” Precedence groups an expression; documented evaluation-order rules determine when operands run. Short-circuit && and || may skip the right operand. ? either extracts the success value through the relevant residual machinery or returns control from the enclosing try context. Method-call syntax may perform documented autoderef and autoref during lookup; it does not grant arbitrary implicit conversion.

Ranges are especially context-sensitive: a..b excludes b; a..=b includes it; omitted bounds form open-ended ranges. A range expression is a value, while the corresponding range syntax in a pattern constrains what matches.

Operator grouping and places

Use parentheses when a review depends on instant recognition. The compact precedence ladder below runs from tighter to looser grouping; postfix forms such as calls, indexing, field access, method calls, and .await bind before the listed binary operators.

Group Operators or forms Notes
path/type association :: module paths, associated items, generic arguments
postfix (), [], .field, .method(), .await, ? chains read left to right after grouping
unary -, !, *, &, &mut negation, dereference, borrowing
cast as explicit conversion; audit truncation and provenance implications
product *, /, % arithmetic traits may overload behavior
sum +, - likewise overloadable
shifts <<, >> validate widths and signedness
bitwise &, then ^, then ` `
comparison ==, !=, <, >, <=, >= comparisons do not chain as mathematics does
logical &&, then `
range .., ..= low precedence; bounds may be absent
assignment =, +=, -=, and peers updates a place and yields ()

A place expression identifies a memory location: a local, static, dereference, array element, or field, for example. A value expression computes a value. This distinction matters because the left side of assignment must be a mutable place, borrowing takes access to a place, and moving from some places may be restricted. Do not infer allocation or machine instructions from an operator spelling; trait implementations and optimization affect mechanism, while language semantics constrain observable behavior.

Patterns: test shape, bind capabilities

Patterns appear in let, parameters, match, if let, while let, for, and closure parameters. Classify them by job:

Pattern Example Reading
wildcard _ matches without binding
binding value, mut value, ref value names all or part of the scrutinee with a binding mode
literal/range 0, 'a'..='z' tests a value
tuple/slice (first, ..), [head, tail @ ..] destructures positional shape
struct/enum Record { id, .. }, Some(x) selects a constructor and fields
alternation `Ok(x) Err(x)`
guard Some(x) if x > 0 arm selected only if pattern and guard succeed
at-binding small @ 1..=9 binds the whole matched value and constrains it

_ does not bind, while _name does bind and can therefore move a non-Copy value. .. ignores remaining fields or elements; it is not a general wildcard expression. A refutable pattern may fail and therefore belongs in match, conditional-let forms, or a let ... else whose else diverges. Plain function parameters and ordinary let require irrefutable patterns.

Paths, names, visibility, and raw identifiers

A path locates a module, type, trait, value, macro, or associated item. crate:: starts at the current crate root; self:: at the current module; super:: at its parent; a leading :: has edition-sensitive historical meaning and should not replace explicit crate-relative paths in new Rust 2024 code. Type::item selects an inherent or trait-associated item. Fully qualified syntax, <Type as Trait>::item, resolves ambiguity and states which trait contract is intended.

use brings paths into scope; it does not copy definitions or create inheritance. pub makes an item visible subject to the visibility of its containing path. Restricted forms such as pub(crate) and pub(super) express architectural boundaries. A re-export, pub use path::Name, gives a public path to an existing item and can therefore be part of a compatibility contract.

A raw identifier, r#type, names an identifier whose spelling is otherwise a keyword. Use it for interoperation and migrations, not as a naming style. It does not turn arbitrary punctuation into an identifier. Some reserved words and special tokens remain unavailable even with r#.

Attributes, comments, and documentation

An outer attribute #[...] applies to the item or expression that follows. An inner attribute #![...] applies to the enclosing crate or module. Common families include conditional compilation, lint levels, derives, representation, tests, and documentation. Attribute meaning belongs to the language, compiler, built-in attribute, or macro that owns it; do not assume every attribute is inert metadata.

// and /* ... */ are comments; block comments nest. /// and /** ... */ are outer documentation comments, while //! and /*! ... */ document the enclosing item. Documentation comments are translated to doc attributes and processed by rustdoc. Code fences in documentation can compile as doctests, including intentional compile_fail examples, so documentation can carry executable API evidence.

Conditional compilation removes or includes source before later analysis of the selected configuration. Test every supported feature/target configuration rather than assuming an inactive branch remains valid. Lint attributes change diagnostic policy, not language semantics. #[repr(...)] can change layout guarantees and requires a boundary-specific reason.

Macros: syntax accepted by another parser

name!(...), name![...], and name! {...} invoke macros. A declarative macro matches token trees and transcribes output; a procedural macro receives and returns token streams. Derive and attribute procedural macros attach through attributes, while function-like procedural macros use invocation syntax. Expansion produces Rust that is then parsed and checked in context.

When reading a macro call, separate three questions: what token grammar the macro accepts, what Rust it expands to, and which names resolve at definition or invocation context. Normal operator precedence cannot decode punctuation that remains inside an opaque token tree. Inspect documented expansion behavior or use an appropriate expansion tool as evidence, but do not treat current expansion formatting as a stable API. Hygiene prevents some accidental capture; it does not make generated APIs, spans, diagnostics, or build cost automatically sound.

Edition-sensitive reading and version labels

Editions allow compatible syntax and name-resolution changes within one compiler ecosystem. A package’s edition controls how its source is interpreted; it is not the compiler version or MSRV. Dependencies may use different editions. Migration tooling rewrites source where possible, but compilation and tests remain the evidence.

Audit these categories when reading historical or generated code: keywords and raw identifiers; path behavior; prelude additions; macro fragment behavior; closure and pattern rules; temporary scopes; unsafe requirements; and lints used to prepare migrations. Label a statement as a language guarantee, current stable behavior, edition rule, or tool convention. This draft targets Rust 2024 and was checked with the installed stable compiler; consult the Edition Guide for changes from earlier editions.

Worked reading drill

Annotate this without running it first:

let selected = records.iter().find_map(|record| match record {
    Record { id: 0, .. } => None,
    Record { id, label } if label.starts_with('w') => Some((*id, label.as_str())),
    _ => None,
});

Check your trace: the outer form is a let statement; find_map takes a closure; the closure body is a match expression; each arm pattern borrows through the iterator’s &Record; the guard runs only after its pattern matches; *id copies a u64; label.as_str() borrows from the record; and the resulting Option<(u64, &str)> cannot outlive the records. Rework it once with into_iter(). The syntax barely changes, but the ownership of the iterator items changes; that semantic delta is the point.

The complete companion fixture is under examples/rust-engineering-handbook/appendices/reference-lab/. Its tests preserve the first trace and its doctests preserve intentional rejections.

Pocket procedure

When a line resists reading:

  1. mark item, statement, expression, pattern, and type boundaries;
  2. resolve delimiters and postfix chains before binary precedence;
  3. identify places, computed values, moves, and borrows;
  4. expand destructuring into the bindings and binding modes it creates;
  5. resolve paths from the current module and trait context;
  6. treat attribute or macro-owned syntax according to its owner;
  7. check package edition and compiler/MSRV separately;
  8. confirm the claim with the smallest executable or rejected program.

Syntax tells you the program’s grammatical shape. Types, ownership, trait selection, edition rules, and macro expansion determine what that shape means. Once the line is decoded, the next decision is which capability its API boundary should grant; Appendix B supplies that ownership decision model.

Sources and version notes

  • The Rust Reference is authoritative for expressions, statements, patterns, paths, attributes, comments, macros, and precedence tables.
  • The Rust Edition Guide records edition-specific migrations and Rust 2024 changes.
  • The Rust Book provides the supported learning treatment of bindings, control flow, patterns, and macros.
  • The rustdoc book documents documentation syntax and doctest behavior.
  • Exact diagnostics and inferred types can change between compiler releases. Semantic claims here target stable Rust 2024; compiler output is reproducible evidence, not a promise that wording remains fixed.