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.
📖 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
- 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
- Editor: Visual Studio Code with rust-analyzer.
- Create
src/main.rs (cargo new hello)fn main() { println!("Hello, GayaGnan!"); } - Run it and check the version
cargo new hello && cd hello && cargo run cargo --version
- 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
- ownership
- borrowing
- references
- structs and enums
- pattern matching
- collections
- Result and Option
- traits
- modules
- tests
- iterators
- concurrency safety
🏗️ Mini-projects
- CLI parser
- Safe file indexer on a fixture folder
- Algorithm challenge suite
Projects use synthetic data only.