Type System Fundamentals
Static Type Checking
Static type checking is the compile-time verification phase in which rustc proves that every expression, variable binding, function call, and trait bound in a program obeys the rules of its type before any machine code is emitted.
Properties of Rust's Static Type System
| Property | How Rust Enforces It | What It Prevents at Compile Time |
|---|---|---|
| Soundness (Strong typing) | No implicit lossy numeric conversions (i32 to u32 or f64), no arbitrary pointer casts in safe code |
Silent truncation, sign-extension bugs, type confusion |
| Exhaustive sum-type checking | Every match on an enum, Option<T>, or Result<T, E> must cover all variants |
Unhandled null dereferences (NullPointerException), forgotten states |
| Affine / Ownership typing | Non-Copy values can be moved at most once (E0382) |
Use-after-free, double-free |
| Region / Lifetime typing | Every reference &'a T and &'a mut T carries a compile-time lifetime 'a |
Dangling pointers, data races, iterator invalidation |
| Zero runtime type tags | Concrete types are erased after monomorphization (unless dyn Any / dyn Trait is requested explicitly) |
Per-object boxing and runtime type-check overhead |
Compile-Time Rejection of Type Mismatches across Control-Flow Branches (E0308)
In Rust, if / else and match are expressions whose branches must evaluate to the exact same type. If two branches return different types, compilation fails immediately with E0308:
// ❌ does not compile
fn select_timeout(fast: bool) -> u64 {
if fast {
100_u64
} else {
"default_timeout"
}
}
fn main() {}
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:6:9
|
3 | / if fast {
4 | | 100_u64
| | ------- expected because of this
5 | | } else {
6 | | "default_timeout"
| | ^^^^^^^^^^^^^^^^^ expected `u64`, found `&str`
7 | | }
| |_____- `if` and `else` have incompatible types
Type-Directed Memory Layout and Dispatch
Because the compiler knows the exact static type of every local variable at compile time, it determines:
- Exact stack frame offsets and register allocation (
size_of::<T>()andalign_of::<T>()). - Which trait implementation to invoke (resolving
a + borx.cmp(&y)to direct machine instructions). - Where destructors (
Drop::drop) must be inserted at scope exits and unwind paths.
fn clamp_u16(value: u16, min: u16, max: u16) -> u16 {
if value < min {
min
} else if value > max {
max
} else {
value
}
}
fn main() {
let port = clamp_u16(80, 1024, 65535);
println!("Clamped port: {port}");
}
Clamped port: 1024