Polyglot
Languages Rust syntax
Rust § syntax

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 … else as 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 or lib.rs file, 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:

ModifierEffect
mutMutable (reassignable for bindings; modifiable for references)
&Shared borrow (reference); immutable view
&mutExclusive borrow (mutable reference)
'aLifetime annotation
*Raw pointer (*const T or *mut T); requires unsafe to dereference
dynDynamic dispatch (trait object)
implExistential type (in argument or return position)
staticLifetime equal to the program
constCompile-time constant
unsafeAdmits 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, dyn keyword for trait objects, async/await reservation.
  • 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) and From/Into for 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.
  • Exceptionspanic! exists but is for unrecoverable errors; Result<T, E> is the conventional error mechanism.
  • Garbage collection — none; ownership and borrowing replace it.
  • async runtime — 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.