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:
- mark item, statement, expression, pattern, and type boundaries;
- resolve delimiters and postfix chains before binary precedence;
- identify places, computed values, moves, and borrows;
- expand destructuring into the bindings and binding modes it creates;
- resolve paths from the current module and trait context;
- treat attribute or macro-owned syntax according to its owner;
- check package edition and compiler/MSRV separately;
- 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.
Continue reading
Full table of contents