learn.sol
Rust Foundations • Rust Setup And Core Syntax

Rust Setup and Core Syntax

Install the Rust toolchain, create your first Cargo project, and learn the variables, functions, types, and control flow that later Solana lessons depend on.

A lot of new Solana learners think Rust becomes hard only when ownership starts.

That is not true.

Many people get blocked earlier because their toolchain is half-set up, they do not know what cargo is doing, and basic Rust syntax still feels unfamiliar.

This lesson fixes that.

The goal is simple: get a working Rust environment, write code that compiles, and understand the small language rules that show up in every later Solana lesson.

Treat the compiler as part of the lesson.

If Rust rejects your code, that is not friction to ignore. It is the language telling you what rule you just violated.


The Mental Model For This Lesson

You do not need to learn all of Rust today.

You need to learn the workflow:

  1. write a small program
  2. compile it with cargo
  3. read what the compiler says
  4. fix the code until the program does what you expect

That loop is the foundation for everything else.

By the end of this lesson, you should be comfortable with:

  • the Rust toolchain: rustup, rustc, and cargo
  • creating and running a project
  • variables and mutability
  • basic scalar types
  • functions and return values
  • if, loop, while, and for

Install The Rust Toolchain

Rust development usually starts with three tools:

  • rustup: installs and manages Rust toolchains
  • rustc: the Rust compiler
  • cargo: the project manager and build tool

You will use cargo most of the time. rustc is the compiler under the hood.

Install Rust

On macOS and Linux:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

On Windows, install rustup-init.exe from rustup.rs.

Verify the installation

Open a new terminal and run:

rustc --version
cargo --version
rustup show

If these commands work, your toolchain is installed correctly.

If they fail, fix that now. Do not move deeper into Rust with a broken environment.

Update to the latest stable toolchain

rustup update
rustup default stable

This makes sure your local setup matches the stable Rust ecosystem most tutorials assume.


Set Up Your Editor

Use VS Code with the official rust-analyzer extension.

That gives you the minimum useful developer experience:

  • inline errors
  • type hints
  • jump to definitions
  • quick fixes

You can add extras later. rust-analyzer is the only extension that really matters right now.


Create Your First Cargo Project

Do not start with a random .rs file.

Start with Cargo.

Cargo gives you the correct project structure, handles builds, and will later manage dependencies for real Solana projects.

cargo new day1_rust
cd day1_rust
cargo run

When cargo run succeeds, three useful things happened:

  • Cargo created a project directory
  • Rust compiled your program into a binary
  • Cargo ran that binary for you

You should see output similar to:

Compiling day1_rust v0.1.0
Finished dev profile
Running `target/debug/day1_rust`
Hello, world!

That output matters.

It proves your local Rust workflow works end to end.


Read The Smallest Useful Rust Program

Open src/main.rs. You will see this:

fn main() {
    println!("Hello, world!");
}

This tiny file already teaches you several Rust rules.

  • fn main() declares the program entry point
  • braces {} define the function body
  • println! is a macro, which is why it ends with !
  • the line ends with ;, so it is a statement

At this stage, you do not need macro theory. You just need to recognize that println! writes text to the terminal.


Build One Small Program You Can Reason About

Instead of throwing isolated syntax examples at you, we will build one small Rust program.

The program will simulate a wallet balance check.

That keeps the logic simple while staying close to Solana-style thinking.

Replace src/main.rs with this:

src/main.rs
const NETWORK_FEE_LAMPORTS: u64 = 5_000;

fn main() {
    // `u64` is common for balances because balances cannot be negative.
    let starting_balance: u64 = 2_000_000;
    let transfer_amount: u64 = 750_000;

    println!("Starting balance: {} lamports", starting_balance);
    println!("Transfer amount: {} lamports", transfer_amount);

    let final_balance = calculate_remaining_balance(starting_balance, transfer_amount);

    println!("Final balance: {} lamports", final_balance);
}

fn calculate_remaining_balance(balance: u64, amount: u64) -> u64 {
    balance - amount - NETWORK_FEE_LAMPORTS
}

Run it:

cargo run

What this teaches

  • const creates a compile-time constant
  • u64 is an unsigned 64-bit integer
  • let creates an immutable variable
  • fn ... -> u64 means the function returns a u64
  • the last line of a function can return a value without using return

That last rule matters a lot in Rust.

The final line in calculate_remaining_balance is an expression, not a statement.

If you add a semicolon there, the function stops returning the number you expect.


Variables Are Immutable Unless You Opt In

This is one of the first Rust rules that surprises JavaScript learners.

In Rust, variables do not change unless you say they can change.

fn main() {
    let balance = 100;
    // balance = 200; // this does not compile
}

If a value really needs to change, use mut.

fn main() {
    let mut balance = 100;
    balance = 200;

    println!("{}", balance);
}

This is not just syntax preference.

Rust makes mutation explicit because hidden state changes are a common source of bugs.


The Basic Types You Will Use First

You do not need the whole type system today.

You need the small set that shows up constantly.

fn main() {
    let balance: u64 = 2_000_000;
    let retry_count: u8 = 3;
    let price: f64 = 1.25;
    let is_confirmed: bool = true;
    let grade: char = 'A';

    println!("{} {} {} {} {}", balance, retry_count, price, is_confirmed, grade);
}

Use this mental shortcut:

  • integers like u64 for counts and balances
  • f64 for decimal values
  • bool for true or false
  • char for a single Unicode character

Rust can often infer types for you, but explicit types are useful when you want the code to be obvious.


Functions Are Where Rust Starts To Feel Structured

A function is just named behavior.

Here is the same wallet example, expanded slightly:

const NETWORK_FEE_LAMPORTS: u64 = 5_000;

fn main() {
    let balance = 2_000_000;
    let amount = 750_000;

    if can_afford_transfer(balance, amount) {
        let final_balance = calculate_remaining_balance(balance, amount);
        println!("Transfer is valid. Final balance: {}", final_balance);
    } else {
        println!("Transfer would fail: not enough lamports.");
    }
}

fn can_afford_transfer(balance: u64, amount: u64) -> bool {
    balance >= amount + NETWORK_FEE_LAMPORTS
}

fn calculate_remaining_balance(balance: u64, amount: u64) -> u64 {
    balance - amount - NETWORK_FEE_LAMPORTS
}

Two points matter here.

First, function parameters need explicit types.

Second, Rust makes return types explicit too. That is what -> bool and -> u64 mean.

That strictness helps later when Solana programs start passing around accounts, lamports, and custom structs. The compiler forces you to be precise.


if Is Not Just Control Flow

In Rust, if can decide execution, but it can also produce a value.

That makes it more powerful than many beginners expect.

fn main() {
    let priority_fee = true;

    let extra_fee = if priority_fee { 10_000 } else { 0 };

    println!("Extra fee: {}", extra_fee);
}

Both branches must return the same kind of value.

That rule prevents messy code where one branch produces a number and the other produces something unrelated.


Learn The Three Loop Forms Early

Rust gives you three common ways to repeat work.

loop

Use loop when you want a manual exit with break.

fn main() {
    let mut attempts = 0;

    let final_attempt = loop {
        attempts += 1;

        if attempts == 3 {
            break attempts;
        }
    };

    println!("Stopped after {} attempts", final_attempt);
}

while

Use while when repetition depends on a condition.

fn main() {
    let mut confirmations_left = 3;

    while confirmations_left > 0 {
        println!("Waiting... {}", confirmations_left);
        confirmations_left -= 1;
    }
}

for

Use for when iterating over a range or collection.

fn main() {
    let transfers = [100_000, 250_000, 400_000];

    for amount in transfers {
        println!("Observed transfer: {}", amount);
    }
}

For beginner Rust, for is usually the cleanest loop to reach for first.


Put The Pieces Together

Now build one complete program that uses constants, variables, functions, if, and for together.

Replace src/main.rs with this:

src/main.rs
const NETWORK_FEE_LAMPORTS: u64 = 5_000;

fn main() {
    let starting_balance: u64 = 2_000_000;
    let outgoing_transfers: [u64; 3] = [150_000, 300_000, 900_000];
    let mut current_balance = starting_balance;

    println!("Starting balance: {} lamports", current_balance);

    for amount in outgoing_transfers {
        if can_afford_transfer(current_balance, amount) {
            current_balance = calculate_remaining_balance(current_balance, amount);
            println!("Sent {} lamports", amount);
            println!("New balance: {} lamports", current_balance);
        } else {
            println!("Skipped {} lamports: insufficient balance", amount);
        }
    }

    println!("Finished with {} lamports", current_balance);
}

fn can_afford_transfer(balance: u64, amount: u64) -> bool {
    balance >= amount + NETWORK_FEE_LAMPORTS
}

fn calculate_remaining_balance(balance: u64, amount: u64) -> u64 {
    balance - amount - NETWORK_FEE_LAMPORTS
}

Run it, then change the numbers yourself.

Try these edits:

  • make one transfer too large and watch the else branch run
  • change the starting balance and predict the output before running
  • add a fourth transfer to the array and confirm the loop handles it

That is the right way to learn basic Rust syntax: change the program and observe what breaks or what changes.


Common Mistakes

Forgetting mut

If you try to reassign an immutable variable, Rust will stop you.

That error is correct. Add mut only when the value truly changes.

Accidentally returning () instead of a value

This fails:

fn add(a: i32, b: i32) -> i32 {
    a + b;
}

The semicolon turns the expression into a statement.

This works:

fn add(a: i32, b: i32) -> i32 {
    a + b
}

Mixing incompatible numeric types

Rust does not silently smooth over type mismatches for you.

If one value is u8 and another is u64, be explicit about conversions.


Summary

This lesson was about getting the Rust workflow under control.

You now have the basics that later Solana Rust lessons rely on:

  • a working Rust toolchain
  • a usable Cargo workflow
  • immutable and mutable variables
  • core scalar types
  • functions with explicit return types
  • if, loop, while, and for

That is enough to start reading and writing small Rust programs without guessing what the syntax is doing.

The next lesson goes deeper into the rule that makes Rust feel fundamentally different: ownership and borrowing.

Before you move on, do the practice page while this syntax is still fresh.

Solana Assistant

AI-powered documentation helper

Welcome to Solana Assistant

Ask specific questions about Solana development:

Ask specific questions for better results400px
    Rust Setup and Core Syntax | learn.sol