This episode builds your first complete Rust program: the structure of binary and library crates, basic data types, variable immutability and shadowing, functions and expressions, as well as control flow with match and if. All examples can be run directly with cargo.

After understanding the architecture, it is time to write code that actually compiles. Episode 3 builds the basic syntax of Rust: how programs are structured, how variables and types work, how functions are written as expressions, and how control flow and pattern matching make decisions.
Rust has an expressive yet disciplined syntax. You will see big differences from other languages: variables are immutable by default, functions are expression-based, and match is exhaustive. All examples can be run with cargo run in the project created in episode 0.
A binary crate starts from src/main.rs with a main function. Cargo makes it the entry point of the program:
cat > src/main.rs <<'EOF'
fn main() {
let nama = "Rust";
println!("Halo, {}!", nama);
}
EOF
cargo runprintln!("Halo, {}!", nama) prints text with a placeholder. This simple binary crate is enough to explore almost all of the material in episode 3.
For code that can be tested and reused, use a library crate: src/lib.rs declares public functions that are called from the binary. Modules organize code further:
cargo new --lib perpustakaan
mkdir perpustakaan/src/mathThe file src/math/kuadrat.rs is called from lib.rs with the declaration pub mod math. Modules keep large codebases structured — a pattern you will use in production projects.
Rust provides scalar types: fixed-width integers like i32 and u64, floating point f32 and f64, boolean bool, and character char. Types can be inferred automatically by the compiler, but they can still be declared explicitly.
cat > src/main.rs <<'EOF'
fn main() {
let umur: u32 = 25;
let harga = 19.99_f64;
let aktif = true;
let inisial = 'A';
println!("{} {} {} {}", umur, harga, aktif, inisial);
}
EOF
cargo runlet umur: u32 = 25 declares an unsigned integer. The suffix 19.99_f64 asserts the type of the literal. Most of the time let is enough without a type annotation because the compiler infers it.
Rust variables are immutable by default: once bound with let, their value cannot be changed. For values that change, add mut. Shadowing allows you to redeclare a name with a new let, changing the type at the same time:
cat > src/main.rs <<'EOF'
fn main() {
let nilai = 5;
let nilai = nilai + 2;
let nilai = nilai * 3;
println!("hasil: {}", nilai);
let teks = "angka";
let teks = teks.len();
println!("panjang: {}", teks);
}
EOF
cargo runShadowing creates a new value with the same name without changing the old value. This differs from mut, which changes the value in place. Shadowing is useful for deriving a value step by step without adding new variable names.
Rust functions are declared with fn, parameters are given explicit types, and the return type is written after the arrow:
cat > src/main.rs <<'EOF'
fn kuadrat(x: i32) -> i32 {
x * x
}
fn main() {
let hasil = kuadrat(7);
let bersyarat = if hasil > 10 { "besar" } else { "kecil" };
println!("{} {}", hasil, bersyarat);
}
EOF
cargo runNotice x * x without a semicolon: that is the expression that becomes the return value. In Rust, statements end with a semicolon while expressions do not. if is also an expression that produces a value — an idiomatic pattern that replaces the ternary operator.
The golden rule: no semicolon means expression, semicolon means statement. This understanding matters because it determines the value a function returns. The compiler will even warn you if a let produces nothing.
cat > src/main.rs <<'EOF'
fn main() {
let mut counter = 0;
loop {
counter += 1;
if counter == 3 {
break;
}
}
for i in 0..3 {
println!("iterasi {}", i);
}
println!("counter: {}", counter);
}
EOF
cargo runloop runs endlessly until break, while for iterates over the range 0..3. A range with two dots .. is exclusive: 0..3 covers 0, 1, and 2. for is the most idiomatic way to iterate over collections.
match compares a value against patterns and runs the first branch that matches. Rust requires exhaustiveness: all possibilities must be handled, or the compiler rejects the program:
cat > src/main.rs <<'EOF'
fn label(status: u8) -> &'static str {
match status {
0 => "pending",
1 => "proses",
2 => "selesai",
_ => "tidak dikenal",
}
}
fn main() {
println!("{}", label(1));
}
EOF
cargo runThe _ pattern catches all values that do not match. match is Rust's main tool for making value-based decisions — you will use it constantly, especially when handling Result and Option in episode 4.
Key takeaways:
src/main.rs; a library crate from src/lib.rs.mut to change and a new let for shadowing.if, loop, and for are statements; if can also be an expression.match must be exhaustive and is the foundation of pattern matching in Rust.In the next episode 4 we will discuss error handling and Result — the Result and Option types as error-safe values, the ? operator for error propagation, as well as custom errors with thiserror and anyhow. This is the skill most frequently used in production Rust code.