Rust Syntax Overview
Rust's syntax is influenced by C and ML-family languages. It looks familiar at a glance but has several distinctive rules — especially around expressions, semicolons, and the type system — that are worth understanding early. This page gives you the bird's-eye view before diving into individual topics.
Statements vs expressions
Rust is an expression-based language. Almost everything produces a value. The distinction between statements and expressions is fundamental:
Produces a value? | Ends with semicolon? | Example | |
|---|---|---|---|
Expression | Yes | No (usually) |
|
Statement | No | Yes |
|
fn main() {
// Statement — does not produce a value
let x = 5;
// Expression — produces a value (used as the argument to println!)
println!("{}", x + 3);
// A block is an expression; its value is the last expression inside
let y = {
let a = 10;
let b = 20;
a + b // no semicolon — this is the value of the block
};
println!("{}", y); // 30
}Semicolons — with vs without
The presence or absence of a semicolon changes meaning in Rust. This is one of the most common sources of beginner confusion.
fn add(a: i32, b: i32) -> i32 {
a + b // NO semicolon — this expression is the return value
}
fn greet() -> &'static str {
"hello" // NO semicolon — returns the string slice
}
fn print_sum(a: i32, b: i32) {
println!("{}", a + b); // semicolon — discard the () return value of println!
}
fn broken() -> i32 {
5; // semicolon turns this into a statement — returns () not i32
// This would be a COMPILE ERROR because the return type is i32
}Blocks as expressions
Any block surrounded by curly braces is an expression. This means you can use if, match, loop, and plain blocks on the right-hand side of let.
fn main() {
let number = 7;
// if is an expression — both arms must have the same type
let description = if number % 2 == 0 {
"even"
} else {
"odd"
};
println!("{} is {}", number, description);
// A plain block as an expression
let result = {
let x = 3;
let y = 4;
x * x + y * y // Pythagorean — no semicolon, so this is the value
};
println!("Hypotenuse squared: {}", result); // 25
}Naming conventions — snake_case
Rust enforces naming conventions through compiler warnings. Follow them and your code will feel natural to every Rust developer.
Item | Convention | Example |
|---|---|---|
Variables | snake_case |
|
Functions | snake_case |
|
Modules | snake_case |
|
Types / Structs | PascalCase |
|
Enums | PascalCase |
|
Enum variants | PascalCase |
|
Constants | SCREAMING_SNAKE_CASE |
|
Traits | PascalCase |
|
Lifetimes | short lowercase | 'a, 'b, 'static |
Type annotations
Rust can infer types in most situations, but you can always write them explicitly. Annotations are required when the compiler cannot infer the type — for example, when parsing a string into a number.
fn main() {
// Inferred types — compiler figures these out
let x = 42; // i32
let y = 3.14; // f64
let name = "Alice"; // &str
let active = true; // bool
// Explicit type annotations — same as above, written out
let x: i32 = 42;
let y: f64 = 3.14;
let name: &str = "Alice";
let active: bool = true;
// Required annotation — the compiler cannot know which integer type to parse
let number: u64 = "12345".parse().unwrap();
// Annotation on a collection's element type
let scores: Vec<i32> = Vec::new();
}Function signatures
Function parameters always require explicit type annotations. The return type follows the -> arrow. If a function returns nothing, the return type is () (unit) and is usually omitted.
// No return value (returns unit — () implicitly)
fn greet(name: &str) {
println!("Hello, {}!", name);
}
// Returns an i32
fn add(a: i32, b: i32) -> i32 {
a + b
}
// Returns a boolean
fn is_even(n: i32) -> bool {
n % 2 == 0
}
// Multiple parameters, returns a String
fn repeat(text: &str, times: usize) -> String {
text.repeat(times)
}
// Generic function — works with any type T that implements Display
use std::fmt::Display;
fn print_value<T: Display>(value: T) {
println!("{}", value);
}Return values — last expression without semicolon
The idiomatic Rust way to return a value from a function is to write the expression as the last line with no semicolon. The return keyword is available but is reserved for early returns.
fn square(x: i32) -> i32 {
x * x // idiomatic — last expression, no semicolon
}
fn abs_value(x: i32) -> i32 {
if x < 0 {
return -x; // early return with the return keyword
}
x // normal return for the positive case
}
fn classify(n: i32) -> &'static str {
match n {
i32::MIN..=-1 => "negative",
0 => "zero",
1..=i32::MAX => "positive",
_ => unreachable!(),
}
// match is an expression — its value is returned
}Comments
Rust supports line comments, block comments, and documentation comments. Documentation comments use markdown and can contain runnable code examples.
// This is a line comment
/* This is a
block comment */
/// This is a doc comment for the item below it (function, struct, etc.)
/// It supports **markdown** and `inline code`.
fn my_function() {}
//! This is a doc comment for the enclosing module or crate (at the top of a file)Attributes
Attributes annotate items with metadata. They use the #[...] syntax and are placed directly above the item they modify. Crate-level attributes use #![...] with a bang.
// Item-level attribute — applies to the next item
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
// Suppress a specific compiler warning for one function
#[allow(dead_code)]
fn unused_function() {}
// Mark a function as a test
#[test]
fn test_addition() {
assert_eq!(2 + 2, 4);
}
// Conditional compilation — only compile on Linux
#[cfg(target_os = "linux")]
fn linux_only() {}
// Crate-level attribute — applies to the whole crate
#![deny(unsafe_code)]Basic operators
Category | Operators | Notes |
|---|---|---|
Arithmetic |
| Integer division truncates; no implicit casting |
Comparison |
| Returns bool; types must match exactly |
Logical |
| Short-circuits; operands must be bool |
Bitwise |
| Works on integer types |
Assignment |
| No ++ or -- operators in Rust |
Range |
|
|
A quick syntax reference
// Variable binding (immutable)
let x = 10;
// Mutable variable
let mut count = 0;
// Constant (must have explicit type)
const MAX: u32 = 1000;
// Shadowing — re-declare with the same name
let y = 5;
let y = y + 1; // new y, shadows old y
// Tuple
let point = (3.0, 4.0);
let (px, py) = point; // destructuring
// Array (fixed size, same type)
let arr = [1, 2, 3, 4, 5];
// if expression
let sign = if x > 0 { "positive" } else { "non-positive" };
// loop with a return value
let mut n = 0;
let found = loop {
n += 1;
if n == 5 { break n * 2; }
};
// found == 10
// while loop
while count < 10 {
count += 1;
}
// for loop over a range
for i in 0..5 {
println!("{}", i);
}