👑 Premium Sign in Sign up

Language lab · Advanced

🦀 Rust

Concept → trace → write code → run tests → explain why → improve → mini-project. Start in the browser if you like; set up your own computer when you are ready.

💻 Runs on your computerOfficial download & docs ↗

📖 Topics with real code (16)

Every verified output below was produced by running the code with rustc 1.82.0 (f6e511eec 2024-10-15) on 5 Nov 2026. Open a topic to edit the code and run it on your own computer with the real compiler, through the free GayaGnan agent.

🌱 Beginner (5)

Variables and data types

Variables are immutable unless marked mut. Types: i32, i64, u8, f64, bool, char, &str and String. Conversions are explicit with as, parse() or to_string().

fn main() {
    let age: i32 = 15;
    let mut height = 1.60_f64;
    height += 0.02;
    let is_student = true;
    let grade = 'A';
    let name = "Asha";
    let population: i64 = 1_428_000_000;
    println!("{} {} {:.2} {} {} {}", name, age, height, is_student, grade, population);
    let marks: i32 = "88".parse::<i32>().unwrap() + 2;
    let avg = 7 as f64 / 2.0;
    println!("{} {} {} {} {}", marks, avg, 7 / 2, 9.99_f64 as i32, 90.to_string() + "!");
    println!("max u8 {}, max i32 {}", u8::MAX, i32::MAX);
}

✅ Verified output

Asha 15 1.62 true A 1428000000
90 3.5 3 9 90!
max u8 255, max i32 2147483647
Operators and boolean logic

Arithmetic and logical operators as in C. Integer overflow is checked in debug builds; wrapping_add and checked_add say what should happen on overflow.

fn main() {
    let (a, b) = (17, 5);
    println!("{} {} {} {} {} {}", a + b, a - b, a * b, a / b, a % b, i32::pow(a, 2));
    let (has_ticket, is_vip) = (true, false);
    println!("enter: {} either: {}", has_ticket && !is_vip, has_ticket || is_vip);
    println!("6&3={} 6|3={} 1<<4={}", 6 & 3, 6 | 3, 1 << 4);
    let x: u8 = 250;
    println!("wrapping {} checked {:?} {:?}", x.wrapping_add(10), x.checked_add(10), x.checked_add(5));
}

✅ Verified output

22 12 85 3 2 289
enter: true either: true
6&3=2 6|3=7 1<<4=16
wrapping 4 checked None Some(255)
Conditionals: if expressions and match

if is an expression that returns a value. match must cover every case; it can match ranges and several values with |.

fn main() {
    let marks = 78;
    let grade = match marks {
        90..=100 => "A+",
        75..=89 => "A",
        35..=74 => "Pass",
        _ => "Fail",
    };
    println!("Grade: {}", grade);
    let day = "sat";
    let kind = if day == "sat" || day == "sun" { "Weekend" } else { "School day" };
    println!("{}", kind);
    let n = 7;
    match n % 2 {
        0 => println!("{} is even", n),
        _ => println!("{} is odd", n),
    }
}

✅ Verified output

Grade: A
Weekend
7 is odd
Loops: loop, while, for over ranges

loop runs forever until break (and can return a value), while checks a condition, for walks ranges and iterators.

fn main() {
    for i in 1..=5 {
        if i == 3 {
            continue;
        }
        println!("for {}", i);
    }
    let mut n = 10;
    while n > 0 {
        if n < 4 {
            break;
        }
        println!("while {}", n);
        n -= 3;
    }
    let mut tries = 0;
    let found = loop {
        tries += 1;
        if tries * tries > 50 {
            break tries;
        }
    };
    println!("loop returned {}", found);
    for r in 1..=3 {
        let row: Vec<String> = (1..=3).map(|c| format!("{:3}", r * c)).collect();
        println!("{}", row.join(""));
    }
}

✅ Verified output

for 1
for 2
for 4
for 5
while 10
while 7
while 4
loop returned 8
  1  2  3
  2  4  6
  3  6  9
Functions

Parameters and return types are written explicitly. The last expression without a semicolon is the return value. Tuples return several values.

fn area(length: u32, width: u32) -> u32 {
    length * width
}

fn stats(nums: &[i32]) -> (i32, i32, f64) {
    let low = *nums.iter().min().unwrap();
    let high = *nums.iter().max().unwrap();
    let avg = nums.iter().sum::<i32>() as f64 / nums.len() as f64;
    (low, high, avg)
}

fn greet(name: &str) -> String {
    format!("Namaste, {}!", name)
}

fn main() {
    println!("{} {}", area(5, 3), greet("Ravi"));
    let (low, high, avg) = stats(&[4, 9, 2, 7]);
    println!("{} {} {}", low, high, avg);
}

✅ Verified output

15 Namaste, Ravi!
2 9 5.5

🌿 Intermediate (7)

Vectors and iterators

Vec<T> is a growable list. Iterator adaptors (filter, map, sum) are fast and safe; collect() builds a new collection.

fn main() {
    let mut marks = vec![72, 95, 64, 88];
    marks.push(91);
    marks.sort();
    println!("{:?} first {} last {} top2 {:?}", marks, marks[0], marks[marks.len() - 1], &marks[marks.len() - 2..]);
    let odd_squares: Vec<i32> = (1..=5).filter(|x| x % 2 == 1).map(|x| x * x).collect();
    println!("odd squares {:?} sum {}", odd_squares, odd_squares.iter().sum::<i32>());
    println!("any above 90: {}, position of 88: {:?}", marks.iter().any(|&m| m > 90), marks.iter().position(|&m| m == 88));
    println!("get(10) is safe: {:?}", marks.get(10));
}

✅ Verified output

[64, 72, 88, 91, 95] first 64 last 95 top2 [91, 95]
odd squares [1, 9, 25] sum 35
any above 90: true, position of 88: Some(2)
get(10) is safe: None
String and &str

String owns growable text; &str borrows a slice of text. Methods: trim, to_uppercase, replace, split_whitespace, chars.

fn main() {
    let t = "  GayaGnan learning  ".trim();
    println!("{} | {} | {}", t.to_uppercase(), t.replace("learning", "labs"), t.len());
    let words: Vec<&str> = t.split_whitespace().collect();
    println!("{:?} -> {}", words, words.join("-"));
    let mut s = String::from("Gaya");
    s.push_str("Gnan");
    s.push('!');
    println!("{} chars {}", s, s.chars().count());
    let p = "level";
    println!("palindrome: {}", p.chars().rev().collect::<String>() == p);
    println!("{}", format!("Total: {:.2} for {} items", 49.5 * 3.0, 3));
}

✅ Verified output

GAYAGNAN LEARNING | GayaGnan labs | 17
["GayaGnan", "learning"] -> GayaGnan-learning
GayaGnan! chars 9
palindrome: true
Total: 148.50 for 3 items
HashMap and HashSet

HashMap stores key-value pairs; the entry API counts neatly. BTreeMap keeps keys sorted.

use std::collections::{BTreeMap, HashSet};

fn main() {
    let mut stock = BTreeMap::from([("pen", 40), ("book", 12)]);
    stock.insert("eraser", 25);
    *stock.get_mut("pen").unwrap() -= 5;
    for (item, count) in &stock {
        println!("{} {}", item, count);
    }
    println!("ruler: {:?}", stock.get("ruler"));
    let mut freq = BTreeMap::new();
    for w in "the cat and the hat and the bat".split_whitespace() {
        *freq.entry(w).or_insert(0) += 1;
    }
    println!("{:?}", freq);
    let a: HashSet<&str> = ["maths", "science", "art"].into_iter().collect();
    let b: HashSet<&str> = ["science", "music"].into_iter().collect();
    println!("both: {:?}", a.intersection(&b).collect::<Vec<_>>());
}

✅ Verified output

book 12
eraser 25
pen 35
ruler: None
{"and": 2, "bat": 1, "cat": 1, "hat": 1, "the": 3}
both: ["science"]
Structs and impl blocks

A struct holds data and impl adds methods. &self borrows, &mut self changes, Self::new is a constructor by convention. #[derive(Debug)] gives printing.

#[derive(Debug)]
struct BankAccount {
    owner: String,
    balance: f64,
}

impl BankAccount {
    fn new(owner: &str, balance: f64) -> Self {
        Self { owner: owner.to_string(), balance }
    }
    fn deposit(&mut self, amount: f64) -> Result<(), String> {
        if amount <= 0.0 {
            return Err("amount must be positive".into());
        }
        self.balance += amount;
        Ok(())
    }
    fn balance(&self) -> f64 {
        self.balance
    }
}

fn main() {
    let mut acc = BankAccount::new("Asha", 500.0);
    acc.deposit(250.0).unwrap();
    println!("{} {}", acc.owner, acc.balance());
    println!("{:?}", acc.deposit(-5.0));
    println!("{:?}", acc);
}

✅ Verified output

Asha 750
Err("amount must be positive")
BankAccount { owner: "Asha", balance: 750.0 }
Traits and polymorphism

A trait lists shared behaviour. Generic functions use traits at compile time; Box<dyn Trait> chooses the method at run time.

use std::f64::consts::PI;

trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> String {
        "shape".to_string()
    }
}
struct Circle(f64);
struct Rect(f64, f64);
impl Shape for Circle {
    fn area(&self) -> f64 { PI * self.0 * self.0 }
    fn name(&self) -> String { "Circle".into() }
}
impl Shape for Rect {
    fn area(&self) -> f64 { self.0 * self.1 }
    fn name(&self) -> String { "Rect".into() }
}
fn describe(s: &impl Shape) -> String {
    format!("{} {:.2}", s.name(), s.area())
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle(1.0)), Box::new(Rect(3.0, 4.0))];
    for s in &shapes {
        println!("{} {:.2}", s.name(), s.area());
    }
    println!("{}", describe(&Rect(2.0, 5.0)));
}

✅ Verified output

Circle 3.14
Rect 12.00
Rect 10.00
Errors with Result and Option

Rust has no exceptions. Functions return Result<T, E> or Option<T>; the ? operator passes an error up; match handles both outcomes.

use std::num::ParseIntError;

fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
    let n: i32 = s.trim().parse()?;
    Ok(n * 2)
}

fn withdraw(balance: u32, amount: u32) -> Result<u32, String> {
    balance.checked_sub(amount).ok_or(format!("need {} more", amount - balance))
}

fn main() {
    for input in ["21", "abc"] {
        match parse_and_double(input) {
            Ok(v) => println!("{} -> {}", input, v),
            Err(e) => println!("{} -> error: {}", input, e),
        }
    }
    println!("{:?} {:?}", withdraw(100, 30), withdraw(100, 150));
    let names = vec!["Asha", "Ravi"];
    println!("third: {:?}, or default: {}", names.get(2), names.get(2).unwrap_or(&"nobody"));
}

✅ Verified output

21 -> 42
abc -> error: invalid digit found in string
Ok(70) Err("need 50 more")
third: None, or default: nobody
Files with std::fs

fs::write and fs::read_to_string handle whole files; std::env::temp_dir gives a temp folder. Errors come back as io::Result.

use std::fs;

fn main() -> std::io::Result<()> {
    let path = std::env::temp_dir().join("marks.csv");
    fs::write(&path, "name,marks\nAsha,88\nRavi,95\n")?;
    let text = fs::read_to_string(&path)?;
    let mut total = 0;
    let mut count = 0;
    for line in text.lines().skip(1) {
        let mut parts = line.split(',');
        let name = parts.next().unwrap();
        let marks: i32 = parts.next().unwrap().parse().unwrap();
        println!("{} -> {}", name, marks);
        total += marks;
        count += 1;
    }
    println!("average {}", total as f64 / count as f64);
    fs::remove_file(&path)?;
    Ok(())
}

✅ Verified output

Asha -> 88
Ravi -> 95
average 91.5

🌳 Advanced (4)

Recursion and memoization

A HashMap passed by &mut caches results. u128 can hold very large Fibonacci numbers exactly.

use std::collections::HashMap;

fn fib(n: u32, memo: &mut HashMap<u32, u128>) -> u128 {
    if n < 2 {
        return n as u128;
    }
    if let Some(&v) = memo.get(&n) {
        return v;
    }
    let v = fib(n - 1, memo) + fib(n - 2, memo);
    memo.insert(n, v);
    v
}

fn binary_search(a: &[i32], t: i32) -> Option<usize> {
    if a.is_empty() {
        return None;
    }
    let mid = a.len() / 2;
    match a[mid].cmp(&t) {
        std::cmp::Ordering::Equal => Some(mid),
        std::cmp::Ordering::Less => binary_search(&a[mid + 1..], t).map(|i| i + mid + 1),
        std::cmp::Ordering::Greater => binary_search(&a[..mid], t),
    }
}

fn main() {
    let mut memo = HashMap::new();
    let first: Vec<u128> = (0..10).map(|i| fib(i, &mut memo)).collect();
    println!("{:?} | fib(150) = {}", first, fib(150, &mut memo));
    println!("index of 23: {:?}", binary_search(&[2, 5, 8, 12, 16, 23, 38], 23));
}

✅ Verified output

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34] | fib(150) = 9969216677189303386214405760200
index of 23: Some(5)
Generics and closures

Generic functions with trait bounds work for many types. Closures capture their environment; Fn, FnMut and FnOnce say how.

fn max_of<T: PartialOrd + Copy>(xs: &[T]) -> T {
    let mut best = xs[0];
    for &x in xs {
        if x > best {
            best = x;
        }
    }
    best
}

fn apply_twice<T, F: Fn(T) -> T>(f: F, x: T) -> T {
    f(f(x))
}

fn make_counter() -> impl FnMut() -> u32 {
    let mut n = 0;
    move || {
        n += 1;
        n
    }
}

fn main() {
    println!("{} {}", max_of(&[4, 9, 2]), max_of(&[1.5, 0.2]));
    println!("{} {}", apply_twice(|n| n * 3, 2), apply_twice(|s: String| s + "!", "hi".to_string()));
    let mut c = make_counter();
    c();
    c();
    println!("count {}", c());
    let mut students = vec![("Asha", 88), ("Ravi", 95), ("Meena", 72)];
    students.sort_by(|a, b| b.1.cmp(&a.1));
    println!("{:?}", students);
}

✅ Verified output

9 1.5
18 hi!!
count 3
[("Ravi", 95), ("Asha", 88), ("Meena", 72)]
Threads, channels, Arc and Mutex

std::thread::spawn runs code in parallel. Arc shares ownership across threads and Mutex allows one writer at a time. The compiler refuses unsafe sharing.

use std::sync::{mpsc, Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};

fn main() {
    let start = Instant::now();
    let (tx, rx) = mpsc::channel();
    for i in 1..=3 {
        let tx = tx.clone();
        thread::spawn(move || {
            thread::sleep(Duration::from_millis(200));
            tx.send(i * i).unwrap();
        });
    }
    drop(tx);
    let mut sq: Vec<i32> = rx.iter().collect();
    sq.sort();
    println!("squares {:?} fast: {}", sq, start.elapsed() < Duration::from_millis(550));

    let total = Arc::new(Mutex::new(0));
    let handles: Vec<_> = (0..4)
        .map(|_| {
            let total = Arc::clone(&total);
            thread::spawn(move || {
                for _ in 0..1000 {
                    *total.lock().unwrap() += 1;
                }
            })
        })
        .collect();
    for h in handles {
        h.join().unwrap();
    }
    println!("total {}", *total.lock().unwrap());
}

✅ Verified output

squares [1, 4, 9] fast: true
total 4000
Ownership, borrowing and lifetimes

Each value has one owner; moving it makes the old name invalid. & borrows to read, &mut borrows to change (only one at a time). Lifetimes say how long a borrow is valid.

fn total_len(words: &[String]) -> usize {
    words.iter().map(|w| w.len()).sum()
}

fn add_suffix(s: &mut String) {
    s.push_str("-labs");
}

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() { a } else { b }
}

fn main() {
    let s1 = String::from("gaya");
    let s2 = s1;                       // moved: s1 can no longer be used
    let s3 = s2.clone();               // deep copy
    println!("{} {}", s2, s3);
    let mut name = String::from("gayagnan");
    add_suffix(&mut name);
    let words = vec![name.clone(), String::from("rust")];
    println!("{} total {}", name, total_len(&words));
    println!("longest: {}", longest("ownership", "borrow"));
    let n = 5;
    let m = n;                         // i32 is Copy, so n is still usable
    println!("copy types: {} {}", n, m);
}

✅ Verified output

gaya gaya
gayagnan-labs total 17
longest: ownership
copy types: 5 5

💻 Terminal on my computer

Like a cloud shell, but on your own laptop: install compilers and run real commands from this page through the free GayaGnan agent. It asks you before connecting, installing or running anything.

🛠️ Set up and verify

  1. Install
    winget install Rustlang.Rustup
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  2. Editor: Visual Studio Code with rust-analyzer.
  3. Create src/main.rs (cargo new hello)
    fn main() {
        println!("Hello, GayaGnan!");
    }
  4. Run it and check the version
    cargo new hello && cd hello && cargo run
    cargo --version
  5. Health check: paste what your terminal printed.

Install one language at a time. These commands only install or print versions; nothing here deletes files. Versions change: check the official page and note the version you installed.

🧭 The 14-lab path

  • 00 · Setup and verify — install the runtime and editor, run hello world, print the version, fix PATH
  • 01 · Syntax and trace — follow each line and the value of every variable
  • 02 · Logic — conditionals and decision trees
  • 03 · Loops — trace tables and loop challenges
  • 04 · Functions — break problems down and test each piece
  • 05 · Data structures — lists, maps, stacks, queues and trees you can see
  • 06 · Debugging — bug cases with a hint ladder
  • 07 · Algorithms — search, sort and Big-O comparison
  • 08 · Files and data — read and write fixture files
  • 09 · API or database — talk to a mock API or a sandbox database
  • 10 · Testing — write tests and read failing assertions
  • 11 · Security — secure coding and dependency hygiene for this language
  • 12 · Project — one portfolio mini-project
  • 13 · Assessment — quiz, coding test, rubric and an explain-your-answer prompt

📚 Modules

  1. ownership
  2. borrowing
  3. references
  4. structs and enums
  5. pattern matching
  6. collections
  7. Result and Option
  8. traits
  9. modules
  10. tests
  11. iterators
  12. concurrency safety

🏗️ Mini-projects

  • CLI parser
  • Safe file indexer on a fixture folder
  • Algorithm challenge suite

Projects use synthetic data only.