Syntax
The syntax of Rust is defined by the Rust Reference and refined through editions — named language versions (Rust 2015, 2018, 2021, 2024) that admit substantial syntactic and semantic changes while preserving compatibility within the 1.x series. The grammar is C-family at the surface (curly braces, semicolons), with substantial additions: pattern matching as a first-class construct, expression-orientation throughout (most things produce values), the ownership annotations (&, &mut, mut, lifetimes), and a substantial macro surface (println!, vec!, format!, dbg!). The conventional contemporary edition is Rust 2021; new projects from late 2024 use Rust 2024 by default.
This page covers the surface a working programmer encounters routinely. The dedicated pages cover the major sub-grammars (ownership, traits, generics, pattern matching).
A complete program
The classical hello world:
fn main() {
println!("Hello, world!");
}
A more substantial example with several features:
use std::env;
fn greet(name: &str) -> String {
format!("Hello, {}.", name)
}
fn main() {
let args: Vec<String> = env::args().skip(1).collect();
let names = if args.is_empty() {
vec![String::from("world")]
} else {
let mut sorted = args;
sorted.sort();
sorted
};
for name in names {
println!("{}", greet(&name));
}
}
The principal features visible:
use std::env— module imports.fn name(...) -> ReturnType { ... }— function definition.&str— borrowed string slice.String— owned string.let— variable binding (immutable by default).let mut— mutable binding.if … elseas an expression (returns a value).println!,format!— macros (the!distinguishes them from functions).for x in iter— iteration.&name— borrowing.
Build and execution:
rustc main.rs && ./main alice bob
The conventional workflow uses Cargo:
cargo new myproject
cd myproject
cargo run
Source character set
Rust source is interpreted as UTF-8. Identifiers may use Unicode letters, though ASCII identifiers are conventional. The compiler normalises identifiers (e.g., NFC); two visually-distinct sequences of code points that normalise to the same form are treated as the same identifier.
Identifiers and keywords
Identifiers begin with a letter or underscore and continue with letters, digits, or underscores. The convention follows the Rust API Guidelines:
snake_case— variables, functions, modules, crate names.CamelCase— types (structs, enums, traits, type aliases).SCREAMING_SNAKE_CASE— constants and statics.- A single leading underscore (
_x) marks a deliberately-unused name (the compiler suppresses the unused-variable warning). - A double leading underscore is conventional for the macro-hygiene sanitised forms.
Reserved keywords:
as break const continue crate else
enum extern false fn for if
impl in let loop match mod
move mut pub ref return self
Self static struct super trait true
type unsafe use where while async
await dyn abstract become box do
final macro override priv try typeof
unsized virtual yield union* raw*
Several are reserved for future use (abstract, become, do, priv, typeof, virtual, yield). The union and raw are contextual keywords — reserved only in specific syntactic positions.
The r#name syntax admits using a reserved word as a raw identifier:
let r#type = 5; // type is a keyword; r#type is the identifier
The mechanism is principally for interop with code that uses keywords as identifiers (older Rust code, FFI with other languages).
Comments
Three comment forms:
// A line comment, terminated by the end of the line.
/* A block comment.
Block comments may nest:
/* nested comment */
*/
/// An outer doc-comment for the next item.
fn documented() {}
//! An inner doc-comment for the enclosing item (typically the module or crate).
The /// and //! (or their block-style /** ... */ and /*! ... */) are doc-comments consumed by rustdoc to produce HTML documentation. The conventional use:
///— for the following declaration.//!— at the top of a module orlib.rsfile, for the enclosing scope.
/// Computes the square of `n`.
///
/// # Examples
///
/// ```
/// assert_eq!(square(5), 25);
/// ```
fn square(n: i32) -> i32 {
n * n
}
Doc-comments admit Markdown formatting and code examples. cargo test runs the code examples as tests; cargo doc produces HTML documentation.
The expression-oriented language
Rust is expression-oriented: most constructs produce values. The body of a function is a block expression; the last expression in a block (without a trailing semicolon) is the block’s value:
fn max(a: i32, b: i32) -> i32 {
if a > b { a } else { b } // if-else is an expression; this is the return value
}
let x = {
let a = 1;
let b = 2;
a + b // last expression; block evaluates to 3
};
The trailing semicolon turns an expression into a statement (which produces no value). The conventional Rust idiom omits the semicolon on the final expression of a block when the block’s value matters.
Statements:
let x = 5; // let statement
fn helper() {} // item declaration
expression; // expression statement (value discarded)
Expressions:
5 // literal
x + 1 // arithmetic
if cond { a } else { b } // conditional
match x { 1 => a, _ => b } // pattern match
loop { break 42; } // loop with value
{ let y = 5; y * 2 } // block
[1, 2, 3] // array
(1, 2) // tuple
The combination admits substantial conciseness; many functions are single expressions:
fn double(n: i32) -> i32 {
n * 2
}
Variable bindings
The let keyword introduces a binding:
let x = 5; // immutable; type inferred as i32
let y: f64 = 3.14; // explicit type
let mut z = 0; // mutable; can be reassigned
z = 10;
Bindings are immutable by default — the variable cannot be reassigned. The mut keyword admits reassignment.
Shadowing admits redeclaring a name:
let x = 5;
let x = x + 1; // shadows; x is now 6
let x = "hello"; // shadows again with a different type
Shadowing differs from mut reassignment: shadowing creates a new binding (admitting type changes); mut admits modifying the same binding.
The conventional discipline is to default to immutable bindings and to introduce mut only when reassignment is genuinely needed.
Statements
The principal statement forms:
let x = 5; // let statement
fn helper() {} // item declaration
struct Point { x: f64, y: f64 } // type declaration
use std::collections::HashMap; // use declaration
expression; // expression statement
if cond { ... } else { ... } // selection (also an expression)
match expr { patterns } // pattern match
while cond { ... } // iteration
loop { ... } // infinite loop
for x in iterable { ... } // for-each
break;
continue;
return value;
Each statement (including item declarations) is at the same level; Rust does not require if __name__ -style guards.
Functions
The fn keyword introduces a function:
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn print_greeting(name: &str) {
println!("Hello, {}", name);
// implicit return of () (unit)
}
The form: fn <name>(<params>) -> <return_type> { <body> }. The return type is () (unit, the empty tuple) if omitted. Parameters require explicit type annotations.
The function’s body is a block expression; the last expression (without a semicolon) is the return value. An explicit return is admitted but conventional only for early returns:
fn classify(n: i32) -> &'static str {
if n < 0 {
return "negative"; // early return
}
if n == 0 {
"zero" // implicit return
} else {
"positive"
}
}
The conventional Rust style favours the implicit form (last expression without semicolon).
Functions are first-class values; their type is fn(...) -> .... The full treatment is in Functions and closures.
Type qualifiers and ownership annotations
Several modifiers attach to types and bindings:
| Modifier | Effect |
|---|---|
mut | Mutable (reassignable for bindings; modifiable for references) |
& | Shared borrow (reference); immutable view |
&mut | Exclusive borrow (mutable reference) |
'a | Lifetime annotation |
* | Raw pointer (*const T or *mut T); requires unsafe to dereference |
dyn | Dynamic dispatch (trait object) |
impl | Existential type (in argument or return position) |
static | Lifetime equal to the program |
const | Compile-time constant |
unsafe | Admits unsafe operations within the block/function |
let x: i32 = 5; // owned, immutable
let y: &i32 = &x; // immutable reference
let mut z: i32 = 5; // owned, mutable
let w: &mut i32 = &mut z; // mutable reference
let s: &'static str = "hello"; // string slice with 'static lifetime
The full treatment of ownership is in Ownership and borrowing.
Macros
Rust admits macros — code that runs at compile time and generates more code. The principal forms:
println!("Hello, {}", name); // formatted print
format!("{} is {}", a, b); // formatted string
vec![1, 2, 3]; // vector literal
assert!(condition); // runtime assertion
assert_eq!(a, b); // equality assertion
dbg!(&value); // debug print and return
todo!(); // placeholder
unimplemented!(); // placeholder
panic!("error"); // unconditional panic
The ! distinguishes a macro invocation from a function call. Macros admit variable arguments, type-aware behaviour, and code generation that functions cannot.
The full treatment is in Macros.
Attributes
Attributes admit metadata on items:
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
#[allow(unused_variables)]
fn unused() {
let x = 5;
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
#[inline]
fn fast() {}
#[deprecated(since = "1.0", note = "use new_function instead")]
fn old_function() {}
The principal categories:
- Derives:
#[derive(Debug, Clone, ...)]— auto-implement traits. - Conditional compilation:
#[cfg(...)]— include or exclude based on configuration. - Lints:
#[allow(...)],#[warn(...)],#[deny(...)]— control compiler warnings. - Documentation:
#[doc = "..."](more commonly the///form). - Stability:
#[deprecated],#[stable],#[unstable]. - Performance:
#[inline],#[cold].
Inner attributes (#![...]) apply to the enclosing item (typically the crate or module).
Editions
Rust admits editions — named language versions:
- Rust 2015 — the original.
- Rust 2018 — module system rework,
dynkeyword for trait objects,async/awaitreservation. - Rust 2021 — disjoint closure captures, IntoIterator for arrays.
- Rust 2024 — let-else stabilisation, Cargo improvements, several minor breaking changes.
Each crate declares its edition in Cargo.toml:
[package]
edition = "2021"
Editions are opt-in per crate; a project may have crates of different editions, and they interoperate. The conventional contemporary advice is to use the latest edition for new code; existing code can migrate via cargo fix --edition.
A note on what Rust admits
Several features the C-family takes for granted are absent or different:
null— not admitted;Option<T>admits absence explicitly.- Implicit conversions — Rust requires explicit casts (
as) andFrom/Intofor type conversion. - Variable shadowing across types — admitted; the same name can be re-bound to a different type.
- Inheritance — not admitted; traits and composition are the substitutes.
- Exceptions —
panic!exists but is for unrecoverable errors;Result<T, E>is the conventional error mechanism. - Garbage collection — none; ownership and borrowing replace it.
asyncruntime — not in the language; provided by libraries (tokio,async-std,smol).
The combination — small language with no GC, ownership-based memory model, expression-orientation, traits-based polymorphism — is the substance of Rust’s identity. The discipline of writing Rust is largely the discipline of working with the borrow checker and choosing the appropriate ownership and reference shapes.